From 03402690d9d9542be6c53eecf5d86bcd62efb542 Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 09:49:05 -0400 Subject: [PATCH 1/6] convert right-arrow elements to plain unicode char --- src/resources/common.ts | 1 + test/resources/common-test.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/resources/common.ts b/src/resources/common.ts index 7dced2d..12c0ec9 100644 --- a/src/resources/common.ts +++ b/src/resources/common.ts @@ -231,6 +231,7 @@ export function standardContentManipulations($: any) { $('testandconfigure').remove(); $('theme').remove(); $('popout').remove(); + $('sym[name="rarr"]').replaceWith(''); $('sym').remove(); $('applet').remove(); $('director').remove(); diff --git a/test/resources/common-test.ts b/test/resources/common-test.ts index a8555f1..fc0990a 100644 --- a/test/resources/common-test.ts +++ b/test/resources/common-test.ts @@ -144,4 +144,24 @@ describe('cdata and codeblocks', () => { expect(video.type).toBe('video'); expect(video.src[0].contenttype).toBe('video/webm'); }); + + test('should convert legacy right-arrow symbols to bold Unicode', async () => { + const content = + '

before after

'; + + const $ = cheerio.load(content, { + normalizeWhitespace: true, + xmlMode: true, + }); + + standardContentManipulations($); + + expect($('em').attr('style')).toBe('bold'); + expect($('em').text()).toBe('→'); + expect($('sym')).toHaveLength(0); + + const result: any = await toJSON($.xml(), projectSummary); + const paragraph = result.children[0].children[0]; + expect(paragraph.children).toContainEqual({ text: '→', strong: true }); + }); }); From 9136633fc0efe59d9af5e68ae6eda66dbe56ec59 Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 10:21:57 -0400 Subject: [PATCH 2/6] preserve text in entry-less elements --- src/resources/common.ts | 1 + src/resources/workbook.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/resources/common.ts b/src/resources/common.ts index 12c0ec9..efa186a 100644 --- a/src/resources/common.ts +++ b/src/resources/common.ts @@ -231,6 +231,7 @@ export function standardContentManipulations($: any) { $('testandconfigure').remove(); $('theme').remove(); $('popout').remove(); + // One course used for right arrow, just replace with bold unicode char $('sym[name="rarr"]').replaceWith(''); $('sym').remove(); $('applet').remove(); diff --git a/src/resources/workbook.ts b/src/resources/workbook.ts index 55a3ded..fb0d9aa 100644 --- a/src/resources/workbook.ts +++ b/src/resources/workbook.ts @@ -201,7 +201,8 @@ export class WorkbookPage extends Resource { `[citation]` ); } else { - $(elem).remove(); + // found course using empty cites to wrap text, so strip rather than remove to preserve + DOM.stripElement($, elem); } }); // Also include any uncited refs. Handles case we've seen of references From 556f4cae7839158e0e62bab9b556e4c48c35e31c Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 10:29:04 -0400 Subject: [PATCH 3/6] regression test for invalid cite handling --- .../x-oli-workbook_page/invalid-cite.xml | 10 ++++++ test/invalid-cite-test.ts | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/content/x-oli-workbook_page/invalid-cite.xml create mode 100644 test/invalid-cite-test.ts diff --git a/test/content/x-oli-workbook_page/invalid-cite.xml b/test/content/x-oli-workbook_page/invalid-cite.xml new file mode 100644 index 0000000..a68dc67 --- /dev/null +++ b/test/content/x-oli-workbook_page/invalid-cite.xml @@ -0,0 +1,10 @@ + + + + + Invalid cite + + +

Before Map by Babsy after.

+ +
diff --git a/test/invalid-cite-test.ts b/test/invalid-cite-test.ts new file mode 100644 index 0000000..d5ff1c3 --- /dev/null +++ b/test/invalid-cite-test.ts @@ -0,0 +1,31 @@ +import { MediaSummary } from 'src/media'; +import { ProjectSummary } from 'src/project'; +import { WorkbookPage } from 'src/resources/workbook'; + +const mediaSummary: MediaSummary = { + mediaItems: {}, + missing: [], + urlPrefix: '', + downloadRemote: false, + flattenedNames: {}, +}; + +const projectSummary = new ProjectSummary('', '', '', mediaSummary); + +describe('invalid citations', () => { + test('should strip cite wrappers while preserving their content', async () => { + const results: any = await new WorkbookPage( + './test/content/x-oli-workbook_page/invalid-cite.xml', + true + ).convert(projectSummary); + + const paragraph = results[0].content.model[0].children[0]; + + expect(paragraph.children).toEqual([ + { text: 'Before Map by ' }, + { text: 'Babsy', strong: true }, + { text: ' after.' }, + ]); + expect(JSON.stringify(paragraph)).not.toContain('"type":"cite"'); + }); +}); From ec2659735b5416c28be0adec30fd806fab322de2 Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 11:36:12 -0400 Subject: [PATCH 4/6] trim leading/trailing whitespace from url src attributes --- src/media.ts | 2 +- test/resources/media-test.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/media.ts b/src/media.ts index db27fc4..7cc4b29 100644 --- a/src/media.ts +++ b/src/media.ts @@ -577,7 +577,7 @@ function generateNewName( // XML document into a full path to file in project directory export function resolve(reference: MediaItemReference): string { let dir = path.dirname(reference.filePath); - const originalRef = removeQueryParams(reference.assetReference); + const originalRef = removeQueryParams(reference.assetReference.trim()); // First prefer the path as-authored and normal package upward search. // This preserves references to nested sibling webcontent folders. diff --git a/test/resources/media-test.ts b/test/resources/media-test.ts index 89c7456..b43119b 100644 --- a/test/resources/media-test.ts +++ b/test/resources/media-test.ts @@ -50,6 +50,26 @@ describe('Media conversions', () => { ); }); + it('Should trim whitespace around media references', () => { + expect( + flatten( + { + filePath: fixturePath( + 'test', + 'content', + 'subdir', + 'deeper', + 'fake.xml' + ), + assetReference: ' ../../webcontent/abby.jpg ', + }, + testSummary() + ) + ).toEqual( + 'unit-test://media/62/62dd67c254e1d067d385a32c3f51bf4d/abby.jpg' + ); + }); + it('Should prefer nested sibling webcontent folders when present', () => { expect( flatten( From 1550ea3f9506705d91826ce5a7fba9528d4ea3b3 Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 13:21:31 -0400 Subject: [PATCH 5/6] normalize trailing space in assessment ids --- src/resources/formative.ts | 7 ++ .../trailing-assessment-id.xml | 23 +++++++ test/formative-test.ts | 64 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 test/content/x-oli-inline-assessment/trailing-assessment-id.xml diff --git a/src/resources/formative.ts b/src/resources/formative.ts index 3d216b9..46e8cc7 100644 --- a/src/resources/formative.ts +++ b/src/resources/formative.ts @@ -668,6 +668,13 @@ export function determineSubType(question: any): ItemTypes { } export function performRestructure($: any) { + $('assessment').each((_i: any, assessment: any) => { + const id = $(assessment).attr('id'); + if (id !== undefined) { + $(assessment).attr('id', id.trim()); + } + }); + standardContentManipulations($); DOM.rename($, 'question body', 'stem'); diff --git a/test/content/x-oli-inline-assessment/trailing-assessment-id.xml b/test/content/x-oli-inline-assessment/trailing-assessment-id.xml new file mode 100644 index 0000000..be142ea --- /dev/null +++ b/test/content/x-oli-inline-assessment/trailing-assessment-id.xml @@ -0,0 +1,23 @@ + + + + Assessment title + + +

Question text

+ + + Correct + Incorrect + + + + Correct + + + Incorrect + + +
+
diff --git a/test/formative-test.ts b/test/formative-test.ts index 79f6ee5..47b3c18 100644 --- a/test/formative-test.ts +++ b/test/formative-test.ts @@ -1,6 +1,8 @@ import { MediaSummary } from 'src/media'; import { ProjectSummary } from 'src/project'; import { Formative } from 'src/resources/formative'; +import { updateDerivativeReferences } from 'src/convert'; +import { Page } from 'src/resources/resource'; const mediaSummary: MediaSummary = { mediaItems: {}, @@ -56,4 +58,66 @@ describe('conversion to Torus resources', () => { expect(pt5CorrectResponse.score).toEqual(10); }); }); + + test('should trim assessment ids before deriving activities', async () => { + const results = await new Formative( + './test/content/x-oli-inline-assessment/trailing-assessment-id.xml', + true + ).convert(projectSummary); + + expect(results).toHaveLength(1); + expect(results[0]).toEqual( + expect.objectContaining({ + id: 'assessment-with-whitespace-question-one', + legacyId: 'assessment-with-whitespace', + title: 'assessment-with-whitespace-question-one', + }) + ); + + const page = { + type: 'Page', + id: 'referencing-page', + legacyPath: '', + legacyId: 'referencing-page', + title: 'Referencing page', + tags: [], + unresolvedReferences: ['assessment-with-whitespace'], + warnings: [], + content: { + model: [ + { + type: 'activity_placeholder', + idref: 'assessment-with-whitespace', + }, + ], + }, + isGraded: false, + isSurvey: false, + collabSpace: { + status: 'disabled', + threaded: true, + auto_accept: true, + show_full_history: true, + participation_min_posts: 0, + participation_min_replies: 0, + }, + objectives: [], + } as Page; + + const [updatedPage] = updateDerivativeReferences([ + page, + ...(results as any), + ]); + expect((updatedPage as Page).content.model).toEqual([ + expect.objectContaining({ + type: 'group', + children: [ + expect.objectContaining({ + type: 'activity-reference', + activity_id: 'assessment-with-whitespace-question-one', + }), + ], + }), + ]); + }); }); From 95c165ae45efb30f647abbf1e2e0690b53124b0c Mon Sep 17 00:00:00 2001 From: Anders Weinstein Date: Fri, 10 Jul 2026 15:19:19 -0400 Subject: [PATCH 6/6] preserve whitespace within elements & test --- src/resources/workbook.ts | 1 + .../foreign-whitespace.xml | 10 +++++++ test/foreign-whitespace-test.ts | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 test/content/x-oli-workbook_page/foreign-whitespace.xml create mode 100644 test/foreign-whitespace-test.ts diff --git a/src/resources/workbook.ts b/src/resources/workbook.ts index fb0d9aa..761594f 100644 --- a/src/resources/workbook.ts +++ b/src/resources/workbook.ts @@ -248,6 +248,7 @@ export class WorkbookPage extends Resource { material: true, anchor: true, translation: true, + foreign: true, dt: true, dd: true, }).then((r: any) => { diff --git a/test/content/x-oli-workbook_page/foreign-whitespace.xml b/test/content/x-oli-workbook_page/foreign-whitespace.xml new file mode 100644 index 0000000..5bc4fb1 --- /dev/null +++ b/test/content/x-oli-workbook_page/foreign-whitespace.xml @@ -0,0 +1,10 @@ + + + + + Foreign whitespace + + +

Il est grand. Il est grand.

+ +
diff --git a/test/foreign-whitespace-test.ts b/test/foreign-whitespace-test.ts new file mode 100644 index 0000000..06e408f --- /dev/null +++ b/test/foreign-whitespace-test.ts @@ -0,0 +1,29 @@ +import { MediaSummary } from 'src/media'; +import { ProjectSummary } from 'src/project'; +import { WorkbookPage } from 'src/resources/workbook'; + +const mediaSummary: MediaSummary = { + mediaItems: {}, + missing: [], + urlPrefix: '', + downloadRemote: false, + flattenedNames: {}, +}; + +const projectSummary = new ProjectSummary('', '', '', mediaSummary); + +describe('foreign text whitespace', () => { + test('should preserve whitespace at foreign element boundaries', async () => { + const results: any = await new WorkbookPage( + './test/content/x-oli-workbook_page/foreign-whitespace.xml', + true + ).convert(projectSummary); + + const paragraph = results[0].content.model[0].children[0]; + const foreignText = paragraph.children.map( + (foreign: any) => foreign.children[0].text + ); + + expect(foreignText).toEqual(['Il est grand. ', ' Il ', 'est ', ' grand.']); + }); +});