Skip to content

Parse QTI items to rewrite them at publish instead of matching regexes - #6241

Merged
rtibbles merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6235-136df6
Oct 7, 2026
Merged

rtibbles merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6235-136df6

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #6235
Refs #6236
Refs #6240

Publish-time QTI item rewrites in utils/assessment/qti/media.py now parse the item with lxml and edit the tree, replacing the regex text substitutions.

🤖 Generated with Claude Code


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

⚪ Updating PR

Last updated: 2026-10-07 13:35 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review September 30, 2026 19:06
@rtibblesbot
rtibblesbot requested review from a team and rtibbles September 30, 2026 19:06
@rtibbles rtibbles self-assigned this Sep 30, 2026
@rtibblesbot
rtibblesbot force-pushed the issue-6235-136df6 branch 3 times, most recently from ffdcad0 to 8a71d91 Compare October 1, 2026 15:27

@rtibbles rtibbles 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.

One question about continuing with a regex approach here.

STUDIO_ATTRIBUTE_OR_QUOTED_VALUE_REGEX = re.compile(
r"""(?P<studio>\s+data-studio-[\w.-]+\s*=\s*(?:"[^"]*"|'[^']*'))|"[^"]*"|'[^']*'"""
)
_WHITESPACE_REGEX = re.compile(r"\s*")

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.

Would it be better to just parse the XML at this point? Creating more and more sophisticated regex for all of these XML parsing operations just adds brittleness - how much does this really save us performance wise?

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.

Agreed — switched to parsing in 3c453e2. The savings were negligible: ~80µs per item for all three rewrites, each parsing separately.

  • Searched utils/assessment/ for regexes and string ops that read XML structure; 4 places matched in qti/media.py, all now parse with lxml: set_qti_item_language, strip_studio_attributes, rewrite_qti_media_paths, rewrite_qti_sized_image_paths.
  • Regexes left only match attribute values (checksum filename, pixel length, srcset entries).
  • Output is now re-serialized, so the author's formatting isn't kept byte-for-byte; tests assert on the parsed tree instead.

@rtibblesbot
rtibblesbot force-pushed the issue-6235-136df6 branch 3 times, most recently from 20318f2 to 3c453e2 Compare October 2, 2026 23:27
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Outside this PR's scope — tracked in:


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks

@rtibblesbot rtibblesbot changed the title Set the QTI item language on the real root past prolog comments Parse QTI items to rewrite them at publish instead of matching regexes Oct 3, 2026

@rtibbles rtibbles 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.

Everything makes sense, just one question about parser consistency.

return int(width), int(height)


def _parse(raw_data):

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.

Why aren't we reusing the secure parser here?

def secure_parser() -> etree.XMLParser:

Feels like it would be better for all of our parsing configuration to be identical?

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.

Agreed — _parse now calls parse_qti_xml (so secure_parser) in 7783b83.

  • Searched non-test code for XMLParser, etree.fromstring, etree.parse: 2 parsed with their own config — media._parse and convert._adapt_unsupported_markup (default parser). Both now use secure_parser; the rest already did.
  • Publish now reads an item exactly as validation does: a non-UTF-8 encoding declaration is honoured again, so the UTF-8 reading this branch had added is dropped and stays with [QTI] A QTI item declaring a non-UTF-8 encoding is published with garbled text #6283.
  • New test: an item that validates only under its declared encoding publishes.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Outside this PR's scope — tracked in:


Written by rtibblesbot, an LLM-based coding agent.

@rtibbles rtibbles 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.

Just one more question to make sure I'm understanding.



def _parse(raw_data):
return parse_qti_xml(raw_data.encode("utf-8"))

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.

If the encoding step is important, shouldn't we add that to the parse_qti_xml function too? Or is it only needed here?

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.

The step only turns text into bytes — lxml refuses a str carrying an encoding declaration — so yes, it belongs in parse_qti_xml. Moved there in 6a9d16a; parse_qti_xml now takes str or bytes.

  • Searched non-test code for .encode("utf-8") ahead of parse_qti_xml/validate_qti_item: 5 matches — media._parse, media.get_qti_media_references, ingest.find_perseus_custom_interaction_path, perseus_derive._parse, archive._create_native_qti_item. All now pass the text; media._parse is deleted.
  • validate_qti_item dropped its own copy too.

rtibblesbot and others added 3 commits October 7, 2026 05:33
…egexes

- set_qti_item_language finds the real root past prolog comments (learningequality#6235)
  and on a namespace-prefixed root (learningequality#6236).
- rewrite_qti_media_paths rewrites a src written as an entity or
  character reference (learningequality#6240).
- Items are parsed with the validator's parser, so a validated item publishes.
- Published items are lxml's serialization, not the authored bytes;
  canonical XML is unchanged for valid items.
- A converted item that is not well-formed XML is logged and left out.

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

@rtibbles rtibbles 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.

All concerns addressed, good to go.

@rtibbles
rtibbles merged commit 9a5848f 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.

@rtibblesbot
rtibblesbot deleted the issue-6235-136df6 branch October 7, 2026 22:26
rtibblesbot added a commit to rtibblesbot/studio that referenced this pull request Oct 8, 2026
learningequality#6241 made publish reserialize QTI item XML through lxml, so restored
raw_data and republished packages no longer match the source byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtibblesbot added a commit to rtibblesbot/studio that referenced this pull request Oct 8, 2026
Restore now rewrites media paths through lxml (learningequality#6241), which changes the
XML declaration and empty-element spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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] A QTI item with a comment quoting its root tag is published without the node's language

2 participants