From e3481cdb1b03f16bbc148c9ea6fc0fe143a8df1f Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 22 Jul 2026 16:54:26 +0100 Subject: [PATCH 1/4] freetext-rtl - First pass --- corsscripts/ascii/stackascii.css | 3 ++ lang/en/qtype_stack.php | 1 + stack/cas/castext2/blocks/ascii.block.php | 15 ++++++-- tests/ascii_block_test.php | 42 ++++++++++++++++++++++- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/corsscripts/ascii/stackascii.css b/corsscripts/ascii/stackascii.css index 70d4873dfee..88d0e95828d 100644 --- a/corsscripts/ascii/stackascii.css +++ b/corsscripts/ascii/stackascii.css @@ -8,3 +8,6 @@ .plaintext { white-space: pre-line; } +.algebraic-right { + text-align: right; +} diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index d0879380791..524b71cc376 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -1097,6 +1097,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..4c0cf090606 100644 --- a/stack/cas/castext2/blocks/ascii.block.php +++ b/stack/cas/castext2/blocks/ascii.block.php @@ -162,7 +162,9 @@ public function compile($format, $options): ?MP_Node { $r->items = array_merge($r->items, $suppliedtext); $r->items[] = new MP_String(''); - $r->items[] = new MP_String('
'); + $alignment = (($xpars['align'] ?? null) == 'right') ? ' algebraic-right' : ''; + $r->items[] = new MP_String('
'); return $r; } @@ -287,6 +289,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 +302,8 @@ public function validate( if ( $key !== 'width' && $key !== 'height' && - $key !== 'aspect-ratio' & + $key !== 'aspect-ratio' && + $key !== 'align' && $key !== 'input' && $key !== 'hidden' ) { @@ -302,7 +311,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/tests/ascii_block_test.php b/tests/ascii_block_test.php index 04fdb072b0f..ea25ea230d8 100644 --- a/tests/ascii_block_test.php +++ b/tests/ascii_block_test.php @@ -131,6 +131,30 @@ public function test_ascii_compile_without_input_parameter_uses_empty_input_requ $this->assertStringContainsString($expectedlinkcode, $joined); } + public function test_ascii_compile_applies_right_alignment_class(): void { + $blockright = new \stack_cas_castext2_ascii(['align' => 'right'], []); + $compiledright = $blockright->compile(null, []); + + $this->assertInstanceOf(\MP_List::class, $compiledright); + + $strings = $this->get_string_items($compiledright); + $joined = implode("\n", $strings); + $this->assertStringContainsString( + '
'left'], []); + $compiledleft = $blockleft->compile(null, []); + $this->assertInstanceOf(\MP_List::class, $compiledleft); + $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 +278,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 +302,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()); } } From f59913b3e3975467a8f5cce60f21282c7daa58c4 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 7 Aug 2026 14:10:47 +0100 Subject: [PATCH 2/4] freetext-rtl - Add direction to ascii block iframe --- stack/cas/castext2/blocks/ascii.block.php | 1 + stack/cas/castext2/blocks/iframe.block.php | 6 ++++- styles.css | 1 - tests/ascii_block_test.php | 29 ++++++++++++++++++++++ vle_specific.php | 7 ++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/stack/cas/castext2/blocks/ascii.block.php b/stack/cas/castext2/blocks/ascii.block.php index 4c0cf090606..9d44a3169aa 100644 --- a/stack/cas/castext2/blocks/ascii.block.php +++ b/stack/cas/castext2/blocks/ascii.block.php @@ -108,6 +108,7 @@ public function compile($format, $options): ?MP_Node { $height = $existsuserheight ? $xpars['height'] : "400px"; $xpars['width'] = $width; $xpars['height'] = $height; + $xpars['stack-ascii-direction'] = true; // Set a title. $xpars['title'] = 'STACK ASCII ///ASCII_COUNT///'; diff --git a/stack/cas/castext2/blocks/iframe.block.php b/stack/cas/castext2/blocks/iframe.block.php index 4b80c4858bf..e63fa39ff49 100644 --- a/stack/cas/castext2/blocks/iframe.block.php +++ b/stack/cas/castext2/blocks/iframe.block.php @@ -181,8 +181,12 @@ public function postprocess( $code = '' . "\n"; $code .= '' . "\n"; + $directionattribute = ''; + if (isset($parameters['stack-ascii-direction']) && $parameters['stack-ascii-direction']) { + $directionattribute = ' dir="' . stack_get_system_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 1edcc2f6ccb..7b9094e5d56 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 ea25ea230d8..33b1972f028 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; @@ -73,6 +74,33 @@ 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 { + stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///'); + StackIframeHolder::$islibrary = true; + StackIframeHolder::$iframes = []; + + try { + $raw = '[[ascii input="ans1"]][[/ascii]]'; + $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); + $iframecontent = StackIframeHolder::$iframes[0][1]; + $this->assertStringContainsString( + '', + $iframecontent + ); + $this->assertStringContainsString('
assertStringNotContainsString('id="asciiContainerRow" dir=', $iframecontent); + } finally { + StackIframeHolder::$islibrary = false; + StackIframeHolder::$iframes = []; + } + } + public function test_ascii_block_with_filter_and_extractor_children(): void { stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///'); @@ -102,6 +130,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->assertTrue($xpars['stack-ascii-direction']); $this->assertStringContainsString('STACK ASCII', $xpars['title']); $strings = $this->get_string_items($compiled); diff --git a/vle_specific.php b/vle_specific.php index ea7cf0b0e0f..633979a030b 100644 --- a/vle_specific.php +++ b/vle_specific.php @@ -375,3 +375,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'; +} From 40843bb5a3ef69a2493fa30888b926642c84de6d Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 7 Aug 2026 14:39:26 +0100 Subject: [PATCH 3/4] freetext-rtl - Allow override via align parameter. --- corsscripts/ascii/stackascii.css | 3 - stack/cas/castext2/blocks/ascii.block.php | 13 ++-- stack/cas/castext2/blocks/iframe.block.php | 7 +- tests/ascii_block_test.php | 78 +++++++++++++++------- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/corsscripts/ascii/stackascii.css b/corsscripts/ascii/stackascii.css index 88d0e95828d..70d4873dfee 100644 --- a/corsscripts/ascii/stackascii.css +++ b/corsscripts/ascii/stackascii.css @@ -8,6 +8,3 @@ .plaintext { white-space: pre-line; } -.algebraic-right { - text-align: right; -} diff --git a/stack/cas/castext2/blocks/ascii.block.php b/stack/cas/castext2/blocks/ascii.block.php index 9d44a3169aa..01226981dfb 100644 --- a/stack/cas/castext2/blocks/ascii.block.php +++ b/stack/cas/castext2/blocks/ascii.block.php @@ -108,7 +108,13 @@ public function compile($format, $options): ?MP_Node { $height = $existsuserheight ? $xpars['height'] : "400px"; $xpars['width'] = $width; $xpars['height'] = $height; - $xpars['stack-ascii-direction'] = true; + $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///'; @@ -163,9 +169,8 @@ public function compile($format, $options): ?MP_Node { $r->items = array_merge($r->items, $suppliedtext); $r->items[] = new MP_String(''); - $alignment = (($xpars['align'] ?? null) == 'right') ? ' algebraic-right' : ''; - $r->items[] = new MP_String('
'); + $r->items[] = new MP_String('
'); return $r; } diff --git a/stack/cas/castext2/blocks/iframe.block.php b/stack/cas/castext2/blocks/iframe.block.php index e63fa39ff49..c1eb98d8328 100644 --- a/stack/cas/castext2/blocks/iframe.block.php +++ b/stack/cas/castext2/blocks/iframe.block.php @@ -182,8 +182,11 @@ public function postprocess( $code .= '' . "\n"; $directionattribute = ''; - if (isset($parameters['stack-ascii-direction']) && $parameters['stack-ascii-direction']) { - $directionattribute = ' dir="' . stack_get_system_direction() . '"'; + if (isset($parameters['stack-ascii-direction'])) { + $direction = $parameters['stack-ascii-direction']; + if ($direction === 'ltr' || $direction === 'rtl') { + $directionattribute = ' dir="' . $direction . '"'; + } } $code .= ''; diff --git a/tests/ascii_block_test.php b/tests/ascii_block_test.php index 33b1972f028..880b61b5d7b 100644 --- a/tests/ascii_block_test.php +++ b/tests/ascii_block_test.php @@ -61,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///'); @@ -75,30 +99,29 @@ public function test_basic_ascii_block(): void { } public function test_ascii_iframe_document_uses_system_direction(): void { - stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///'); - StackIframeHolder::$islibrary = true; - StackIframeHolder::$iframes = []; + $iframecontent = $this->render_ascii_iframe_content('[[ascii input="ans1"]][[/ascii]]'); - try { - $raw = '[[ascii input="ans1"]][[/ascii]]'; - $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->assertStringContainsString( + '', + $iframecontent + ); + $this->assertStringContainsString('
assertStringNotContainsString('id="asciiContainerRow" dir=', $iframecontent); + } - $this->assertCount(1, StackIframeHolder::$iframes); - $iframecontent = StackIframeHolder::$iframes[0][1]; - $this->assertStringContainsString( - '', - $iframecontent - ); - $this->assertStringContainsString('
assertStringNotContainsString('id="asciiContainerRow" dir=', $iframecontent); - } finally { - StackIframeHolder::$islibrary = false; - StackIframeHolder::$iframes = []; - } + 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 { @@ -130,7 +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->assertTrue($xpars['stack-ascii-direction']); + $this->assertEquals(stack_get_system_direction(), $xpars['stack-ascii-direction']); $this->assertStringContainsString('STACK ASCII', $xpars['title']); $strings = $this->get_string_items($compiled); @@ -160,22 +183,27 @@ public function test_ascii_compile_without_input_parameter_uses_empty_input_requ $this->assertStringContainsString($expectedlinkcode, $joined); } - public function test_ascii_compile_applies_right_alignment_class(): void { + 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( '
Date: Fri, 7 Aug 2026 15:08:03 +0100 Subject: [PATCH 4/4] freetext-rtl - Code tidy --- stack/cas/castext2/blocks/ascii.block.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stack/cas/castext2/blocks/ascii.block.php b/stack/cas/castext2/blocks/ascii.block.php index 01226981dfb..3704af64dd7 100644 --- a/stack/cas/castext2/blocks/ascii.block.php +++ b/stack/cas/castext2/blocks/ascii.block.php @@ -169,8 +169,7 @@ public function compile($format, $options): ?MP_Node { $r->items = array_merge($r->items, $suppliedtext); $r->items[] = new MP_String(''); - $r->items[] = new MP_String('
'); + $r->items[] = new MP_String('
'); return $r; }