diff --git a/src/resources/common.ts b/src/resources/common.ts index 96f028d..7dced2d 100644 --- a/src/resources/common.ts +++ b/src/resources/common.ts @@ -277,9 +277,12 @@ export function standardContentManipulations($: any) { 'innerpurpose' ); - // Strip side-by-side materials structure if contains inline activities - DOM.rename($, 'materials:has(wb\\:inline)', 'mtemp'); - DOM.eliminateLevel($, 'mtemp>material:has(wb\\:inline)'); + // Strip side-by-side materials structure if it contains inline activities + // or media/interactive blocks that do not author reliably inside table cells. + const flattenableMaterialsSelector = + 'wb\\:inline, iframe, video, audio, youtube, command_button'; + DOM.rename($, `materials:has(${flattenableMaterialsSelector})`, 'mtemp'); + DOM.eliminateLevel($, `mtemp>material:has(${flattenableMaterialsSelector})`); DOM.rename($, 'mtemp>material', 'p'); DOM.eliminateLevel($, 'mtemp'); @@ -386,11 +389,9 @@ function handleCommandButtons($: any) { } }); - // Now wrap all command_button instances in a paragraph. This can - // lead to situations where we have paragraphs inside of paragaphs, or - // paragraphs inside of list-items, but downstream code eliminates those - // conditions. - $('command_button').wrap('

'); + // Do not force-wrap command buttons in paragraphs here. + // In mixed legacy structures (e.g. materials flattening + inline activities), + // wrap/strip cycles can cause command buttons to be dropped or reordered. } function stripInvalidParagraphNesting($: any) { @@ -476,6 +477,9 @@ function handleJmolApplets($: any) { if (idref) { iframe.attr('id', idref); } + // Jmol applets commonly receive command-button messages; mark as listening + // so conversion can map legacy id -> iframe targetId consistently. + iframe.attr('listen', 'true'); iframe.attr('src', src); iframe.attr('scrolling', 'no'); iframe.attr('frameborder', '0'); diff --git a/src/utils/xml.ts b/src/utils/xml.ts index ab973fb..1662b45 100644 --- a/src/utils/xml.ts +++ b/src/utils/xml.ts @@ -577,17 +577,32 @@ export function toJSON( const handleCommandButton = () => { if (tag === 'command_button') { - // We have to set 'pronunciation' as a property - // as well introduce 'table' as a property to hold all of the - // 'tr' children - const messages = getAllOfType(top().children, 'message'); top().message = messages[0].children[0].text; + if (messages.length > 1) { + // Legacy message list semantics uses each message title as the label to be + // shown *after* that message is sent. Torus toggle states use the label for + // the current state, so shift titles forward when building states. + top().toggleStates = messages.map((m: any, i: number) => ({ + title: i === 0 ? top().title : messages[i - 1].title, + message: m.children[0].text, + })); + } top().children = [{ text: top().title }]; } }; + const handleIframeTargeting = () => { + if ( + tag === 'iframe' && + (top().listen === true || top().listen === 'true') && + top().id + ) { + top().targetId = top().id; + } + }; + const renameCaptionForFigure = () => { if (tag === 'figure') { if (top().caption !== null && top().caption !== undefined) { @@ -682,6 +697,7 @@ export function toJSON( ensureParagraph('pronunciation'); handleConjugation(); handleCommandButton(); + handleIframeTargeting(); handleDescriptionList(); handleAlternatives(); stringToBoolean('formula_inline', 'legacyBlockRendered'); diff --git a/test/jmol-applet-test.ts b/test/jmol-applet-test.ts index c91c4ce..cb64fb3 100644 --- a/test/jmol-applet-test.ts +++ b/test/jmol-applet-test.ts @@ -55,5 +55,7 @@ describe('jmol applet', () => { expect(iframe.src).toBe(expectedSrc); expect(iframe.width).toBe('130'); expect(iframe.height).toBe('130'); + expect(iframe.listen).toBe('true'); + expect(iframe.targetId).toBe('methane2'); }); }); diff --git a/test/utils/xml-test.ts b/test/utils/xml-test.ts index f300d12..fb289de 100644 --- a/test/utils/xml-test.ts +++ b/test/utils/xml-test.ts @@ -110,6 +110,18 @@ describe('xml conversion', () => { }); }); + test('should map listening iframe id to targetId', async () => { + const xml = + ''; + + const result: any = await toJSON(xml, projectSummary, preserved); + const iframe = result.children[0]; + + expect(iframe.type).toBe('iframe'); + expect(iframe.id).toBe('targetx'); + expect(iframe.targetId).toBe('targetx'); + }); + test('should convert MathJAX LaTeX embedded in feedback with $$ to formula_inline', async () => { const xml = `