Skip to content

Commit 60d7a99

Browse files
committed
Merge remote-tracking branch 'Bjoern-Ge/feature/unicode-superscripts' into iss1676
2 parents eef0d10 + 8684a6f commit 60d7a99

6 files changed

Lines changed: 42 additions & 17 deletions

File tree

stack/cas/parsingrules/180_char_based_superscripts.filter.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,18 @@ class stack_ast_filter_180_char_based_superscripts implements stack_cas_astfilte
3737
// phpcs:ignore moodle.Commenting.MissingDocblock.Function
3838
public function filter(MP_Node $ast, array &$errors, array &$answernotes, stack_cas_security $identifierrules): MP_Node {
3939
if (self::$ssmap === null) {
40-
self::$ssmap = json_decode(file_get_contents(__DIR__ . '/../../maximaparser/unicode/superscript-stack.json'), true);
40+
// Option C: Allow only the most common superscripts: ², ³, ¹
41+
// These are 2-byte UTF-8 characters and work without UTF-8mb4 requirement.
42+
self::$ssmap = ['²' => '2', '³' => '3', '¹' => '1'];
4143
}
4244

43-
$process = function ($node) use (&$errors, &$answernotes) {
45+
// Loop until no more changes are made.
46+
// This ensures all superscripts are converted, even in expressions like x²+x².
47+
$changed = true;
48+
while ($changed) {
49+
$changed = false;
50+
51+
$process = function($node) use (&$errors, &$answernotes, &$changed) {
4452
if ($node instanceof MP_Identifier && !(isset($node->position['invalid']) && $node->position['invalid'])) {
4553
// Iterate over the name to detect when we move from normal to superscript.
4654
$norm = true;
@@ -108,6 +116,7 @@ public function filter(MP_Node $ast, array &$errors, array &$answernotes, stack_
108116

109117
if (count($parts) === 1) {
110118
$node->parentnode->replace($node, $parts[0]);
119+
$changed = true;
111120
} else {
112121
if (array_search('missing_stars', $answernotes) === false) {
113122
$answernotes[] = 'missing_stars';
@@ -119,6 +128,7 @@ public function filter(MP_Node $ast, array &$errors, array &$answernotes, stack_
119128
$a->position['insertstars'] = true;
120129
}
121130
$node->parentnode->replace($node, $a);
131+
$changed = true;
122132
}
123133
return false;
124134
}
@@ -127,6 +137,7 @@ public function filter(MP_Node $ast, array &$errors, array &$answernotes, stack_
127137
};
128138

129139
$ast->callbackRecurse($process);
140+
}
130141
return $ast;
131142
}
132143
}

stack/input/inputbase.class.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,10 @@ protected function validate_contents_filters($basesecurity) {
10431043
$filterstoapply[] = '115_lexer_post_process_stackbasen';
10441044
}
10451045

1046+
// Filter 180 MUST run BEFORE Filter 150!
1047+
// Filter 180: Intelligently converts Unicode superscripts to exponents (x² → x^2)
1048+
// Filter 150: Simple replacement of remaining Unicode letters (α → alpha, × → *)
1049+
$filterstoapply[] = '180_char_based_superscripts';
10461050
$filterstoapply[] = '150_replace_unicode_letters';
10471051

10481052
if (get_class($this) === 'stack_units_input' || get_class($this) === 'stack_numerical_input') {

stack/maximaparser/corrective_parser.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,12 @@ public static function parse(string $string, array &$errors, array &$answernote,
118118

119119
// Check for invalid chars at this point as they may prove to be difficult to
120120
// handle latter, also strings are safe already.
121+
// Option C: Allow only the most common superscripts: ², ³, ¹
122+
// These are 2-byte UTF-8 characters and work without UTF-8mb4 requirement.
123+
$superscript = ['²' => '2', '³' => '3', '¹' => '1'];
124+
121125
$allowedcharsregex = '~[^' . preg_quote(
126+
implode('', array_keys($superscript)) .
122127
// @codingStandardsIgnoreStart
123128
// We do really want a backtick here.
124129
'0123456789,./\%#&{}[]()$@!"\'?`^~*_+qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM:;=><|: -', '~'

tests/fixtures/inputfixtures.class.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,8 @@ function at a point \(f(x)\). Maybe a 'gocha' for the question author....",
520520
['(x/y)/z', 'php_true', '(x/y)/z', 'cas_true', '\frac{\frac{x}{y}}{z}', '', ""],
521521
['x/(y/z)', 'php_true', 'x/(y/z)', 'cas_true', '\frac{x}{\frac{y}{z}}', '', ""],
522522
['x^y', 'php_true', 'x^y', 'cas_true', 'x^{y}', '', "Operations and functions with special TeX"],
523-
["x\u{00b2}", 'php_false', '', 'cas_false', '', 'forbiddenChar', ""],
524-
["x\u{00b2}*x\u{00b2}", 'php_false', '', 'cas_false', '', 'forbiddenChar', ""],
523+
["x\u{00b2}", 'php_true', 'x^2', 'cas_true', 'x^{2}', 'superscriptchars', ""],
524+
["x\u{00b2}*x\u{00b2}", 'php_true', 'x^2*x^2', 'cas_true', 'x^{2}\cdot x^{2}', 'superscriptchars', ""],
525525
['x^(y+z)', 'php_true', 'x^(y+z)', 'cas_true', 'x^{y+z}', '', ""],
526526
['x^(y/z)', 'php_true', 'x^(y/z)', 'cas_true', 'x^{\frac{y}{z}}', '', ""],
527527
['x^f(x)', 'php_true', 'x^f(x)', 'cas_true', 'x^{f\left(x\right)}', '', ""],
@@ -625,7 +625,7 @@ function at a point \(f(x)\). Maybe a 'gocha' for the question author....",
625625
['arsinh(x)', 'php_true', 'asinh(x)', 'cas_true', '{\rm sinh}^{-1}\left( x \right)', 'triginv', ""],
626626
['sin^-1(x)', 'php_false', 'sin^-1(x)', 'cas_false', '', 'missing_stars | trigexp', ""],
627627
['cos^2(x)', 'php_false', 'cos^2(x)', 'cas_false', '', 'missing_stars | trigexp', ""],
628-
["sin\u{00b2}(x)", 'php_false', 'sin^2(x)', 'cas_false', '', 'forbiddenChar', ""],
628+
["sin\u{00b2}(x)", 'php_false', 'sin^2(x)', 'cas_false', '', 'trigexp | superscriptchars | forbiddenVariable', ""],
629629
['sin*2*x', 'php_false', 'sin*2*x', 'cas_false', '', 'forbiddenVariable', ""],
630630
['sin[2*x]', 'php_false', 'sin[2*x]', 'cas_false', '', 'trigparens', ""],
631631
['cosh(x)', 'php_true', 'cosh(x)', 'cas_true', '\cosh \left( x \right)', '', ""],

tests/fixtures/test_strings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
[
2+
"",
3+
"",
4+
"",
5+
"x²²",
6+
"x²³",
7+
"x²+x²",
8+
"x²+x²+x²",
9+
"x²y³",
10+
"(x²)+(y²)",
211
"\"+\"(a,b)",
312
"\"1+1\"",
413
"\"Hello world\"",

tests/input_algebraic_test.php

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,20 +3197,16 @@ public function test_validate_student_response_single_var_chars_unicode_superscr
31973197
$el->set_parameter('insertStars', 2);
31983198
$state = $el->validate_student_response(
31993199
['sans1' => ''],
3200-
$options,
3201-
'x^2',
3200+
$options, 'x^2',
32023201
new stack_cas_security()
32033202
);
3204-
$this->assertEquals(stack_input::INVALID, $state->status);
3205-
// The rest needs to be updated once we know what the expected result is.
3206-
$this->assertEquals('forbiddenChar', $state->note);
3207-
$this->assertEquals(
3208-
'CAS commands may not contain the following characters: ².',
3209-
$state->errors
3210-
);
3211-
$this->assertEquals('', $state->contentsmodified);
3212-
$this->assertEquals('<span class="stacksyntaxexample">x&sup2;</span>', $state->contentsdisplayed);
3213-
$this->assertEquals('', $state->lvars);
3203+
// Unicode superscripts are now converted to exponents by Filter 180.
3204+
$this->assertEquals(stack_input::VALID, $state->status);
3205+
$this->assertEquals('superscriptchars', $state->note);
3206+
$this->assertEquals('', $state->errors);
3207+
$this->assertEquals('x^2', $state->contentsmodified);
3208+
$this->assertEquals('\[ x^{2} \]', $state->contentsdisplayed);
3209+
$this->assertEquals('x', $state->lvars);
32143210
}
32153211

32163212
public function test_validate_student_response_km(): void {

0 commit comments

Comments
 (0)