diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php
index 620113ddb95..5f107e9007c 100644
--- a/lang/en/qtype_stack.php
+++ b/lang/en/qtype_stack.php
@@ -1128,6 +1128,7 @@
$string['stackBlock_ascii_answer_format'] = 'The answer attribute of the ASCII block must be in the format \"[ans1,extractor,filter],[ans2,extractor,filter],...\".';
$string['stackBlock_ascii_unknown_param'] = 'Unknown parameter \'{$a}\' for the ASCII block.';
$string['stackBlock_ascii_param'] = 'Valid parameters are: {$a->param}.';
+$string['stackBlock_ascii_incorrect_alignment'] = 'The output must be aligned left or right.';
$string['stackBlock_extractor_targetinput_required'] = 'You must define the targetinput for the extractor block to send the answer to.';
$string['stackBlock_extractor_type_required'] = 'You must define the type of extractor you want to use.';
diff --git a/stack/cas/castext2/blocks/ascii.block.php b/stack/cas/castext2/blocks/ascii.block.php
index 9c6fb418ca7..3704af64dd7 100644
--- a/stack/cas/castext2/blocks/ascii.block.php
+++ b/stack/cas/castext2/blocks/ascii.block.php
@@ -108,6 +108,13 @@ public function compile($format, $options): ?MP_Node {
$height = $existsuserheight ? $xpars['height'] : "400px";
$xpars['width'] = $width;
$xpars['height'] = $height;
+ $direction = stack_get_system_direction();
+ if (($xpars['align'] ?? null) === 'left') {
+ $direction = 'ltr';
+ } else if (($xpars['align'] ?? null) === 'right') {
+ $direction = 'rtl';
+ }
+ $xpars['stack-ascii-direction'] = $direction;
// Set a title.
$xpars['title'] = 'STACK ASCII ///ASCII_COUNT///';
@@ -287,6 +294,12 @@ public function validate(
$valid = false;
$err[] = stack_string('stackBlock_ascii_underdefined_dimension');
}
+ if (
+ array_key_exists('align', $this->params) && !in_array($this->params['align'], ['left', 'right'])
+ ) {
+ $valid = false;
+ $err[] = stack_string('stackBlock_ascii_incorrect_alignment');
+ }
// Check that only valid parameters are passed to block header.
$valids = null;
@@ -294,7 +307,8 @@ public function validate(
if (
$key !== 'width' &&
$key !== 'height' &&
- $key !== 'aspect-ratio' &
+ $key !== 'aspect-ratio' &&
+ $key !== 'align' &&
$key !== 'input' &&
$key !== 'hidden'
) {
@@ -302,7 +316,7 @@ public function validate(
$valid = false;
if ($valids === null) {
$valids = [
- 'width', 'height', 'aspect-ratio', 'input', 'hidden',
+ 'width', 'height', 'aspect-ratio', 'align', 'input', 'hidden',
];
$err[] = stack_string('stackBlock_ascii_param', [
'param' => implode(', ', $valids),
diff --git a/stack/cas/castext2/blocks/iframe.block.php b/stack/cas/castext2/blocks/iframe.block.php
index 2c74bbf7ec8..3e6f0fb5950 100644
--- a/stack/cas/castext2/blocks/iframe.block.php
+++ b/stack/cas/castext2/blocks/iframe.block.php
@@ -184,8 +184,15 @@ public function postprocess(
$code = '' . "\n";
$code .= '' . "\n";
+ $directionattribute = '';
+ if (isset($parameters['stack-ascii-direction'])) {
+ $direction = $parameters['stack-ascii-direction'];
+ if ($direction === 'ltr' || $direction === 'rtl') {
+ $directionattribute = ' dir="' . $direction . '"';
+ }
+ }
$code .= '';
+ stack_get_system_language() . '"' . $directionattribute . '>';
// Include a title to help JS debugging.
$code .= '
' . $title . '';
$code .= $style;
diff --git a/styles.css b/styles.css
index 83395be659e..893aa396d75 100644
--- a/styles.css
+++ b/styles.css
@@ -1247,7 +1247,6 @@ table.HELM_table th:last-child {
.que.stack .equivinput,
.que.stack .varmatrixinput,
.que.stack .matrixtable,
-.que.stack .freetextinput,
/* Question authoring form */
#page-question-type-stack textarea#id_questionvariables,
#page-question-type-stack input#id_variantsselectionseed,
diff --git a/tests/ascii_block_test.php b/tests/ascii_block_test.php
index 04fdb072b0f..880b61b5d7b 100644
--- a/tests/ascii_block_test.php
+++ b/tests/ascii_block_test.php
@@ -23,6 +23,7 @@
namespace qtype_stack;
+use api\util\StackIframeHolder;
use castext2_evaluatable;
use qtype_stack_testcase;
use stack_cas_session2;
@@ -60,6 +61,30 @@ private function get_string_items(\MP_List $compiled): array {
return $strings;
}
+ /**
+ * Render an ASCII block and return the generated iframe document.
+ * @param string $raw
+ * @return string
+ */
+ private function render_ascii_iframe_content(string $raw): string {
+ stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///');
+ StackIframeHolder::$islibrary = true;
+ StackIframeHolder::$iframes = [];
+
+ try {
+ $at1 = castext2_evaluatable::make_from_source($raw, 'test-case');
+ $session = new stack_cas_session2([$at1]);
+ $session->instantiate();
+ $at1->apply_placeholder_holder($at1->get_rendered());
+
+ $this->assertCount(1, StackIframeHolder::$iframes);
+ return StackIframeHolder::$iframes[0][1];
+ } finally {
+ StackIframeHolder::$islibrary = false;
+ StackIframeHolder::$iframes = [];
+ }
+ }
+
public function test_basic_ascii_block(): void {
stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///');
@@ -73,6 +98,32 @@ public function test_basic_ascii_block(): void {
$this->assertEquals($expected, $at1->apply_placeholder_holder($at1->get_rendered()));
}
+ public function test_ascii_iframe_document_uses_system_direction(): void {
+ $iframecontent = $this->render_ascii_iframe_content('[[ascii input="ans1"]][[/ascii]]');
+
+ $this->assertStringContainsString(
+ '',
+ $iframecontent
+ );
+ $this->assertStringContainsString('assertStringNotContainsString('id="asciiContainerRow" dir=', $iframecontent);
+ }
+
+ public function test_ascii_align_parameter_overrides_iframe_document_direction(): void {
+ $leftcontent = $this->render_ascii_iframe_content('[[ascii input="ans1" align="left"]][[/ascii]]');
+ $rightcontent = $this->render_ascii_iframe_content('[[ascii input="ans1" align="right"]][[/ascii]]');
+
+ $this->assertStringContainsString(
+ '',
+ $leftcontent
+ );
+ $this->assertStringContainsString(
+ '',
+ $rightcontent
+ );
+ }
+
public function test_ascii_block_with_filter_and_extractor_children(): void {
stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///');
@@ -102,6 +153,7 @@ public function test_ascii_compile_adds_default_filter_and_input_request(): void
$xpars = json_decode($compiled->items[1]->value, true);
$this->assertEquals('100%', $xpars['width']);
$this->assertEquals('400px', $xpars['height']);
+ $this->assertEquals(stack_get_system_direction(), $xpars['stack-ascii-direction']);
$this->assertStringContainsString('STACK ASCII', $xpars['title']);
$strings = $this->get_string_items($compiled);
@@ -131,6 +183,35 @@ public function test_ascii_compile_without_input_parameter_uses_empty_input_requ
$this->assertStringContainsString($expectedlinkcode, $joined);
}
+ public function test_ascii_align_parameter_sets_document_direction_only(): void {
+ $blockright = new \stack_cas_castext2_ascii(['align' => 'right'], []);
+ $compiledright = $blockright->compile(null, []);
+
+ $this->assertInstanceOf(\MP_List::class, $compiledright);
+
+ $xparsright = json_decode($compiledright->items[1]->value, true);
+ $this->assertEquals('rtl', $xparsright['stack-ascii-direction']);
+ $strings = $this->get_string_items($compiledright);
+ $joined = implode("\n", $strings);
+ $this->assertStringContainsString(
+ '
assertStringNotContainsString('algebraic-right', $joined);
+
+ $blockleft = new \stack_cas_castext2_ascii(['align' => 'left'], []);
+ $compiledleft = $blockleft->compile(null, []);
+ $this->assertInstanceOf(\MP_List::class, $compiledleft);
+ $xparsleft = json_decode($compiledleft->items[1]->value, true);
+ $this->assertEquals('ltr', $xparsleft['stack-ascii-direction']);
+ $joinedleft = implode("\n", $this->get_string_items($compiledleft));
+ $this->assertStringContainsString(
+ '
assertStringNotContainsString('algebraic-right', $joinedleft);
+ }
+
public function test_ascii_compile_uses_child_filter_and_extractor_operations(): void {
$filter = new \stack_cas_castext2_filter([
'type' => 'markdown',
@@ -254,6 +335,22 @@ public function test_ascii_aspect_ratio_dimension_rules(): void {
$this->assertEquals(stack_string('stackBlock_ascii_underdefined_dimension'), $atunder->get_errors());
}
+ public function test_ascii_validate_alignment_parameter(): void {
+ $validleft = '[[ascii input="ans1" align="left"]][[/ascii]]';
+ $validright = '[[ascii input="ans1" align="right"]][[/ascii]]';
+ $invalid = '[[ascii input="ans1" align="center"]][[/ascii]]';
+
+ $atleft = castext2_evaluatable::make_from_source($validleft, 'test-case');
+ $this->assertTrue($atleft->get_valid());
+
+ $atright = castext2_evaluatable::make_from_source($validright, 'test-case');
+ $this->assertTrue($atright->get_valid());
+
+ $atinvalid = castext2_evaluatable::make_from_source($invalid, 'test-case');
+ $this->assertFalse($atinvalid->get_valid());
+ $this->assertEquals(stack_string('stackBlock_ascii_incorrect_alignment'), $atinvalid->get_errors());
+ }
+
public function test_ascii_unknown_param_rejected(): void {
$raw = '[[ascii input="ans1" bad_param="x"]][[/ascii]]';
@@ -262,7 +359,7 @@ public function test_ascii_unknown_param_rejected(): void {
$this->assertFalse($at1->get_valid());
$this->assertStringContainsString(stack_string('stackBlock_ascii_unknown_param', 'bad_param'), $at1->get_errors());
$this->assertStringContainsString(stack_string('stackBlock_ascii_param', [
- 'param' => 'width, height, aspect-ratio, input, hidden',
+ 'param' => 'width, height, aspect-ratio, align, input, hidden',
]), $at1->get_errors());
}
}
diff --git a/vle_specific.php b/vle_specific.php
index 5d725437320..e9c31e519a6 100644
--- a/vle_specific.php
+++ b/vle_specific.php
@@ -370,3 +370,10 @@ function stack_fetch_included_content(string $url) {
function stack_get_system_language(): string {
return current_language();
}
+
+/**
+ * Fetches the direction of the current VLE UI language.
+ */
+function stack_get_system_direction(): string {
+ return function_exists('right_to_left') && right_to_left() ? 'rtl' : 'ltr';
+}