Repository navigation
Parse QTI items to rewrite them at publish instead of matching regexes - #6241
Conversation
805f224 to
41fc509
Compare
ffdcad0 to
8a71d91
Compare
rtibbles
left a comment
There was a problem hiding this comment.
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*") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 inqti/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.
20318f2 to
3c453e2
Compare
|
Outside this PR's scope — tracked in:
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly How was this generated?
|
3c453e2 to
40c82a3
Compare
40c82a3 to
d9352ec
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Everything makes sense, just one question about parser consistency.
| return int(width), int(height) | ||
|
|
||
|
|
||
| def _parse(raw_data): |
There was a problem hiding this comment.
Why aren't we reusing the secure parser here?
Feels like it would be better for all of our parsing configuration to be identical?
There was a problem hiding this comment.
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._parseandconvert._adapt_unsupported_markup(default parser). Both now usesecure_parser; the rest already did. - Publish now reads an item exactly as validation does: a non-UTF-8
encodingdeclaration 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.
|
Outside this PR's scope — tracked in:
Written by rtibblesbot, an LLM-based coding agent. |
rtibbles
left a comment
There was a problem hiding this comment.
Just one more question to make sure I'm understanding.
|
|
||
|
|
||
| def _parse(raw_data): | ||
| return parse_qti_xml(raw_data.encode("utf-8")) |
There was a problem hiding this comment.
If the encoding step is important, shouldn't we add that to the parse_qti_xml function too? Or is it only needed here?
There was a problem hiding this comment.
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 ofparse_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._parseis deleted. validate_qti_itemdropped its own copy too.
…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>
7783b83 to
6a9d16a
Compare
rtibbles
left a comment
There was a problem hiding this comment.
All concerns addressed, good to go.
|
Quick check on rtibblesbot's work on this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific. |
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>
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>
Closes #6235
Refs #6236
Refs #6240
Publish-time QTI item rewrites in
utils/assessment/qti/media.pynow parse the item with lxml and edit the tree, replacing the regex text substitutions.set_qti_item_languagesetsxml:langon the real root, past prolog comments ([QTI] A QTI item with a comment quoting its root tag is published without the node's language #6235) and on a namespace-prefixed root (Published QTI item with a namespace-prefixed root gets no xml:lang #6236).rewrite_qti_media_pathsremaps asrcwritten as an entity or character reference ([QTI] A QTI item with an entity in a media src attribute is published with an unrewritten path #6240).rewrite_qti_sized_image_pathsandstrip_studio_attributesmove to the same parse/serialize path.secure_parser, so publish reads an item exactly as validation did.🤖 Generated with Claude Code
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
⚪ Updating PR
Last updated: 2026-10-07 13:35 UTC