Skip to content

Keep text after a formula when reopening a QTI question - #6297

Merged
AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6269-7cf169
Oct 7, 2026
Merged

AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6269-7cf169

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Prompts, options and hints load from XML as HTML, so text after a formula survives.
  • script/style are dropped; raw-text elements like iframe reduce to text.
  • Image src resolution skips quoted attributes (alt text with >).
  • <br></br> reads as two breaks, <br/> as one, matching the HTML parser.
  • New QTIEditor/README.md (new work, not a fold): parse/build flow and the getContentHTML rule.

References

Fixes #6269.

Reviewer guidance

Stored XML that loads differently:

  • <p>a <strong/> b</p> → a b; <a href="u"/>tail keeps tail outside the link.
  • <p><![CDATA[a < b]]></p> → a < b, not PROMPT_REQUIRED.
  • Stored formula-only option and the same formula retyped raise DUPLICATE_CHOICE_CONTENT; match/associate merge answers.

Open question: <noscript> text now shows and saves. Drop it like script?

QA team: text and line breaks after a formula now stay on reopen. Regressions are most probable beside a dropdown or an image.

Use a new exercise. "Formula text": type "Solve ", insert the "Math formula" x^2, type " for x". "Reopen": save, close and open the exercise.

  1. Single choice (issue reproduction): In "Question", option 1 and "Edit hint 1", add formula text, press Shift+Enter, type "next". Reopen. Click into each field, then out. Each field reads "Solve x² for x", one break, then "next".
  2. Text entry: Do item 1 in "Question". Add an answer. Reopen. The question opens in the editor. Click into "Question", then out. The text, one break and the answer stay.
  3. Dropdown: Click "Insert" to add a dropdown. Add formula text on both sides. Mark one option correct. Reopen, click into the passage, then out. The options and correct answer stay. No text shows twice.
  4. Image (Requires: a PNG file): In "Question", insert an image described "a > b", then formula text. Reopen. The description and " for x" stay.

Look out for:

  • In Associate, give two prompts the same formula text. Reopen. The shared response can match only one prompt.

Evidence

Single choice question with a formula mid-sentence

Step Screenshot
Collapsed card: "Solve x² for x" Collapsed card: "Solve x² for x"
Editor: trailing text kept Editor: trailing text kept

Multiple choice with formulas in several choices

Step Screenshot
Trailing text kept, B ticked, order A–D Trailing text kept, B ticked, order A–D

Hint with a formula mid-sentence

Step Screenshot
"Divide d/t to get speed" on one line; "Start here" unchanged "Divide d/t to get speed" on one line; "Start here" unchanged

Ordering question

Step Screenshot
After reorder: Earth, Mercury first; trailing text kept After reorder: Earth, Mercury first; trailing text kept

Match question

Step Screenshot
After pairing change: Whale has Mammal and Swims; text kept After pairing change: Whale has Mammal and Swims; text kept

Associate question

Step Screenshot
Prompt and pairs keep trailing text; pairs 1 and 3 share "Kenya ½ east" Prompt and pairs keep trailing text; pairs 1 and 3 share "Kenya ½ east"

Image with alt text "a > b" beside a formula

No file storage on the QA stack: images show as placeholders.

Step Screenshot
Reopened question: alt "a > b", formula and "then win" kept; option keeps " ok" and its image Reopened question: alt "a > b", formula and "then win" kept; option keeps " ok" and its image
Reopened hint: " tail", image alt "a > b" and "after" kept Reopened hint: " tail", image alt "a > b" and "after" kept

Formulas in several fields: opening each field in turn

Step Screenshot
Single choice: prompt and option 1 keep their own trailing text; option 2 open shows only "Venus" Single choice: prompt and option 1 keep their own trailing text; option 2 open shows only "Venus"
Match: row 1 prompt and answer keep trailing text; other rows unchanged Match: row 1 prompt and answer keep trailing text; other rows unchanged
s2-reopen-cycle-fields.webm
More captures (24)
Step Screenshot
Image alt: question being edited before save Image alt: question being edited before save
Image alt: hint before save Image alt: hint before save
Several fields: single choice prompt and option being edited Several fields: single choice prompt and option being edited
Several fields: match row 1 prompt and answer being edited Several fields: match row 1 prompt and answer being edited
Single choice: before closing the exercise Single choice: before closing the exercise
Single choice: formula dialog with x^2 Single choice: formula dialog with x^2
Multiple choice: before save Multiple choice: before save
Hint: editing the formula hint Hint: editing the formula hint
Hint: reopened, hint field open Hint: reopened, hint field open
Ordering: saved before reopen Ordering: saved before reopen
Ordering: reopened, prompt open Ordering: reopened, prompt open
Ordering: after reorder Ordering: after reorder
Match: before save Match: before save
Match: reopened Match: reopened
Associate: after formula edits Associate: after formula edits
Text entry: editor before edits Text entry: editor before edits
Text entry: prompt with formula and trailing text Text entry: prompt with formula and trailing text
Text entry: reopened prompt and answers Text entry: reopened prompt and answers
Inline choice: shows as unknown type, not editable Inline choice: shows as unknown type, not editable
Rich content: formula beside bold, italic, list and image Rich content: formula beside bold, italic, list and image
Rich content: reopened Rich content: reopened
Several questions: Q1 reopened Several questions: Q1 reopened
Several questions: Q2 reopened Several questions: Q2 reopened
Several questions: Q3 reopened Several questions: Q3 reopened

AI usage

Claude Code wrote the fix and tests; verified with the QTIEditor and TipTapEditor Jest suites and pre-commit.


Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-07 18:10 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review October 7, 2026 03:33
@rtibblesbot
rtibblesbot requested review from a team and leq-bot October 7, 2026 03:33

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look good, this is the correct fix. Could you also add a very small README in the QTIEditor folder, similar to the TipTapEditor, describing the high-level details of the Editor? It doesnt have to be thorough but it'd be great to have this now, and then add a flag saying that we should use the getContentHTML on parse for fields that will be treated as HTML in the TipTapEditor.

expect(errorIds).not.toContain('e');
});

it('flags a stored formula option and the same formula typed in again', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (pre-existing, follow-up): This makes a formula-only option match its retyped twin, which is right. The opposite case is still broken: richTextComparisonKey uses only the text whenever there is any, and formulas contribute no text. So <p>Solve <span data-latex="x^2"></span> for x</p> and <p>Solve <span data-latex="y^3"></span> for x</p> both key to "Solve for x". I confirmed that buildMatchInteractionXML then merges two rows' answers x² is it / y³ is it into a single choice (m1, match-max="2"), and the y³ content is lost. Choice/ordering validation will also report them as duplicates. Now that formula text survives reopening, authors are more likely to hit this. Could we file an issue to include data-latex (and maybe img src) in the key? Not for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside this PR's scope — tracked as #6301 under #6103.


Written by rtibblesbot, an LLM-based coding agent.

const IMG_TAG = /<img\b[^>]*>/gi;
// Quoted values are skipped whole: the HTML serializer leaves `>` unescaped in them.
const IMG_TAG = /<img\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
const SRC_ATTRIBUTE = /\bsrc\s*=\s*(["'])(.*?)\1/i;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: IMG_TAG now skips quoted values, but SRC_ATTRIBUTE still searches the whole tag. So alt='see src="a.png"' (or any data-src) matches before the real src. That's rare enough to leave alone, but the comment on line 17 reads as if quoted values are safe throughout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5fdf2a781: src is now read by walking the tag's attributes, so src= inside a quoted value or data-src is skipped, and the first real src wins. Test added with both cases.

Searched the frontend for other regexes that pull an attribute out of a tag: SRC_ATTRIBUTE was the only one.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Added QTIEditor/README.md: the parse → state → build flow, folder layout, adding an interaction, and a note to read TipTap-bound fields with getContentHTML. Searched every parse.js and hints.js for innerHTML/XMLSerializer reads of rich text fields: none remain.

rtibblesbot and others added 3 commits October 7, 2026 09:26
…AsHtml

- Drop noscript, script and style, as TipTap does.
- Turn iframe, noembed, noframes, plaintext and xmp into their text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prompts, options and hints were read back with XML innerHTML, which
self-closes the empty formula span; TipTap then nested the trailing text
inside it and dropped it. Read them with serializeAsHtml instead, via a
shared getContentHTML.

Fixes learningequality#6269

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look good, manual QA checks out, follow-up issue filled. LGTM!

@AlexVelezLl
AlexVelezLl merged commit 10be77e into learningequality:unstable Oct 7, 2026
13 checks passed
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Quick check on rtibblesbot's work on this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Reopening a question drops the text after a formula

2 participants