fix: retain links across formatted text runs

This commit is contained in:
2026-07-22 19:56:07 +02:00
parent 95341ed81f
commit 3c3904f896
2 changed files with 31 additions and 8 deletions

View File

@@ -349,6 +349,11 @@ function createTextRuns(
(style.hyperlinkProtected === true || style.hyperlink === true) (style.hyperlinkProtected === true || style.hyperlink === true)
) { ) {
run.href = pendingHref; run.href = pendingHref;
} else if (
!style.hidden &&
style.hyperlinkProtected !== true &&
style.hyperlink !== true
) {
pendingHref = undefined; pendingHref = undefined;
} }
result.push(run); result.push(run);

View File

@@ -114,15 +114,20 @@ describe('OneNote rich-content model', () => {
it('associates only allow-listed hyperlink marker targets', () => { it('associates only allow-listed hyperlink marker targets', () => {
const safe = parseSyntheticLink('https://example.com/path'); const safe = parseSyntheticLink('https://example.com/path');
expect(safe.text).toBe('Example'); expect(safe.text).toBe('Example');
expect(safe.runs.find((run) => run.text === 'Example')?.href).toBe( expect(
'https://example.com/path' 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)'); const unsafe = parseSyntheticLink('javascript:alert(1)');
expect(unsafe.text).toBe('Example'); expect(unsafe.text).toBe('Example');
expect( expect(
unsafe.runs.find((run) => run.text === 'Example')?.href unsafe.runs.filter((run) => !run.style.hidden).map((run) => run.href)
).toBeUndefined(); ).toEqual([undefined, undefined]);
}); });
it('retains an embedded-file node as a downloadable lazy resource', () => { 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 paragraphId = id(guid, 2);
const hiddenId = id(guid, 3); const hiddenId = id(guid, 3);
const visibleId = id(guid, 4); const visibleId = id(guid, 4);
const secondVisibleId = id(guid, 5);
const marker = `\ufddfHYPERLINK "${href}"`; const marker = `\ufddfHYPERLINK "${href}"`;
const text = `${marker}Example`; const text = `${marker}Example`;
const objects = [ const objects = [
@@ -284,12 +290,15 @@ function parseSyntheticLink(href: string): OneNoteTextBlock {
JCID.RichTextNode, JCID.RichTextNode,
{ {
...emptyProps(), ...emptyProps(),
objectIds: [compact(2), compact(3), compact(4)], objectIds: [compact(2), compact(3), compact(4), compact(5)],
properties: { properties: {
entries: [ entries: [
referenceEntry(PROPERTY.ParagraphStyle, 'one'), referenceEntry(PROPERTY.ParagraphStyle, 'one'),
referenceEntry(PROPERTY.TextRunFormatting, 'many', 2), referenceEntry(PROPERTY.TextRunFormatting, 'many', 3),
bytesEntry(PROPERTY.TextRunIndex, u32Vector(marker.length)), bytesEntry(
PROPERTY.TextRunIndex,
u32Vector(marker.length, marker.length + 4)
),
bytesEntry( bytesEntry(
PROPERTY.RichEditTextUnicode, PROPERTY.RichEditTextUnicode,
Buffer.from(text, 'utf16le') Buffer.from(text, 'utf16le')
@@ -318,6 +327,15 @@ function parseSyntheticLink(href: string): OneNoteTextBlock {
), ),
guid guid
), ),
object(
secondVisibleId,
JCID.ParagraphStyleObject,
props(
boolEntry(PROPERTY.HyperlinkProtected, true),
boolEntry(PROPERTY.Italic, true)
),
guid
),
]; ];
const space: ObjectSpace = { const space: ObjectSpace = {
id: id(guid, 0), id: id(guid, 0),