From 3c3904f896b2f03c34f67dd69db11c928386fbee Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 19:56:07 +0200 Subject: [PATCH] fix: retain links across formatted text runs --- src/onenote/one/content.ts | 5 +++++ tests/content-model.test.ts | 34 ++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/onenote/one/content.ts b/src/onenote/one/content.ts index 5705528..06a8702 100644 --- a/src/onenote/one/content.ts +++ b/src/onenote/one/content.ts @@ -349,6 +349,11 @@ function createTextRuns( (style.hyperlinkProtected === true || style.hyperlink === true) ) { run.href = pendingHref; + } else if ( + !style.hidden && + style.hyperlinkProtected !== true && + style.hyperlink !== true + ) { pendingHref = undefined; } result.push(run); diff --git a/tests/content-model.test.ts b/tests/content-model.test.ts index b693738..17db288 100644 --- a/tests/content-model.test.ts +++ b/tests/content-model.test.ts @@ -114,15 +114,20 @@ describe('OneNote rich-content model', () => { it('associates only allow-listed hyperlink marker targets', () => { const safe = parseSyntheticLink('https://example.com/path'); expect(safe.text).toBe('Example'); - expect(safe.runs.find((run) => run.text === 'Example')?.href).toBe( - 'https://example.com/path' - ); + expect( + safe.runs + .filter((run) => !run.style.hidden) + .map((run) => [run.text, run.href]) + ).toEqual([ + ['Exam', 'https://example.com/path'], + ['ple', 'https://example.com/path'], + ]); const unsafe = parseSyntheticLink('javascript:alert(1)'); expect(unsafe.text).toBe('Example'); expect( - unsafe.runs.find((run) => run.text === 'Example')?.href - ).toBeUndefined(); + unsafe.runs.filter((run) => !run.style.hidden).map((run) => run.href) + ).toEqual([undefined, undefined]); }); it('retains an embedded-file node as a downloadable lazy resource', () => { @@ -276,6 +281,7 @@ function parseSyntheticLink(href: string): OneNoteTextBlock { const paragraphId = id(guid, 2); const hiddenId = id(guid, 3); const visibleId = id(guid, 4); + const secondVisibleId = id(guid, 5); const marker = `\ufddfHYPERLINK "${href}"`; const text = `${marker}Example`; const objects = [ @@ -284,12 +290,15 @@ function parseSyntheticLink(href: string): OneNoteTextBlock { JCID.RichTextNode, { ...emptyProps(), - objectIds: [compact(2), compact(3), compact(4)], + objectIds: [compact(2), compact(3), compact(4), compact(5)], properties: { entries: [ referenceEntry(PROPERTY.ParagraphStyle, 'one'), - referenceEntry(PROPERTY.TextRunFormatting, 'many', 2), - bytesEntry(PROPERTY.TextRunIndex, u32Vector(marker.length)), + referenceEntry(PROPERTY.TextRunFormatting, 'many', 3), + bytesEntry( + PROPERTY.TextRunIndex, + u32Vector(marker.length, marker.length + 4) + ), bytesEntry( PROPERTY.RichEditTextUnicode, Buffer.from(text, 'utf16le') @@ -318,6 +327,15 @@ function parseSyntheticLink(href: string): OneNoteTextBlock { ), guid ), + object( + secondVisibleId, + JCID.ParagraphStyleObject, + props( + boolEntry(PROPERTY.HyperlinkProtected, true), + boolEntry(PROPERTY.Italic, true) + ), + guid + ), ]; const space: ObjectSpace = { id: id(guid, 0),