From 0aa9d1a0410054cf8c4b737a56f7eefdb60f68a7 Mon Sep 17 00:00:00 2001 From: Eric Agnew Date: Thu, 2 Apr 2026 11:43:41 -0500 Subject: [PATCH 1/4] Add highlight text transform --- __test__/text-node-to-html.test.ts | 18 ++++++++++++++++-- src/helper/enumerate-entries.ts | 18 +++++++++++------- src/nodes/mark-type.ts | 3 ++- src/nodes/text-node.ts | 1 + src/options/default-node-options.ts | 19 +++++++++++-------- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/__test__/text-node-to-html.test.ts b/__test__/text-node-to-html.test.ts index 876df5a..77a150c 100644 --- a/__test__/text-node-to-html.test.ts +++ b/__test__/text-node-to-html.test.ts @@ -15,7 +15,7 @@ describe('Text Node To HTML', () => { ...textNode, bold: true } - + const resultHtml = textNodeToHTML(node, { ...defaultNodeOption }) @@ -212,4 +212,18 @@ describe('Text Node To HTML', () => { expect(resultHtml).toEqual(`
${node.text}
`) done() }) -}) \ No newline at end of file + + it('Should return Highlighted string text', done => { + const node = { + ...textNode, + highlight: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual(`${textNode.text}`) + done() + }) +}) diff --git a/src/helper/enumerate-entries.ts b/src/helper/enumerate-entries.ts index 50c790f..da8ac50 100644 --- a/src/helper/enumerate-entries.ts +++ b/src/helper/enumerate-entries.ts @@ -44,11 +44,11 @@ export function enumerateContents( export function textNodeToHTML(node: TextNode, renderOption: RenderOption): string { let text = replaceHtmlEntities(node.text); - + // Convert newlines to
tags if there are no other marks // This ensures newlines are always handled consistently let hasMarks = false; - + if (node.classname || node.id) { text = (renderOption[MarkType.CLASSNAME_OR_ID] as RenderMark)(text, node.classname, node.id); hasMarks = true; @@ -85,12 +85,16 @@ export function textNodeToHTML(node: TextNode, renderOption: RenderOption): stri text = (renderOption[MarkType.BOLD] as RenderMark)(text); hasMarks = true; } - + if (node.highlight) { + text = (renderOption[MarkType.HIGHLIGHT] as RenderMark)(text); + hasMarks = true; + } + // If no marks were applied, but text contains newlines, convert them to
if (!hasMarks && text.includes('\n')) { text = text.replace(/\n/g, '
'); } - + return text; } export function referenceToHTML( @@ -121,7 +125,7 @@ export function referenceToHTML( const aTag = `${entryText}`; return aTag; } - + if (!renderEmbed && renderOption[node.type] !== undefined) { return sendToRenderOption(node); } @@ -136,7 +140,7 @@ export function referenceToHTML( if (!item && renderOption[node.type] !== undefined) { return sendToRenderOption(node); } - + return findRenderString(item, metadata, renderOption); } @@ -184,4 +188,4 @@ function nodeToHTML( return (renderOption.default as RenderNode)(node, next); } } -} \ No newline at end of file +} diff --git a/src/nodes/mark-type.ts b/src/nodes/mark-type.ts index c9bc9d1..01fade3 100644 --- a/src/nodes/mark-type.ts +++ b/src/nodes/mark-type.ts @@ -6,6 +6,7 @@ enum MarkType { STRIKE_THROUGH = 'strikethrough', INLINE_CODE = 'inlineCode', + HIGHLIGHT = 'highlight', SUBSCRIPT = 'subscript', @@ -13,4 +14,4 @@ enum MarkType { BREAK = 'break' } -export default MarkType \ No newline at end of file +export default MarkType diff --git a/src/nodes/text-node.ts b/src/nodes/text-node.ts index 02f9de0..7bd95f5 100644 --- a/src/nodes/text-node.ts +++ b/src/nodes/text-node.ts @@ -9,6 +9,7 @@ export default class TextNode extends Node { superscript?: boolean subscript?: boolean break?: boolean + highlight?: boolean classname?: string id?: string diff --git a/src/options/default-node-options.ts b/src/options/default-node-options.ts index 2d7d4e8..04f2481 100644 --- a/src/options/default-node-options.ts +++ b/src/options/default-node-options.ts @@ -24,7 +24,7 @@ function getAttrString(node: Node, key: string): string | undefined { */ function buildCommonAttrs(node: Node): string { if (!node.attrs) return ''; - + const attrs: string[] = []; if (node.attrs.style) { attrs.push(` style="${node.attrs.style}"`); @@ -134,7 +134,7 @@ export const defaultNodeOption: RenderOption = { }); colgroupHTML += ``; } - + // Generate table with colgroup and other attributes return `${colgroupHTML}${sanitizeHTML(next(node.children))}`; }, @@ -157,7 +157,7 @@ export const defaultNodeOption: RenderOption = { const colSpan = getAttr(node, 'colSpan'); const rowSpanAttr = rowSpan ? ` rowspan="${rowSpan}"` : ''; const colSpanAttr = colSpan ? ` colspan="${colSpan}"` : ''; - + return `${sanitizeHTML(next(node.children))}` }, [NodeType.TABLE_DATA]:(node: Node, next: Next) => { @@ -167,7 +167,7 @@ export const defaultNodeOption: RenderOption = { const colSpan = getAttr(node, 'colSpan'); const rowSpanAttr = rowSpan ? ` rowspan="${rowSpan}"` : ''; const colSpanAttr = colSpan ? ` colspan="${colSpan}"` : ''; - + return `${sanitizeHTML(next(node.children))}` }, [NodeType.BLOCK_QUOTE]:(node: Node, next: Next) => { @@ -180,12 +180,12 @@ export const defaultNodeOption: RenderOption = { ['reference']:(node: Node, next: Next) => { const type = getAttr(node, 'type'); const displayType = getAttr(node, 'display-type'); - + if ((type === 'entry' || type === 'asset') && displayType === 'link'){ const href = getAttrString(node, 'href') || getAttrString(node, 'url') || ''; const target = getAttrString(node, 'target'); const assetUid = getAttrString(node, 'asset-uid'); - + let aTagAttrs = buildCommonAttrs(node); if (href) aTagAttrs += ` href="${href}"`; if (target) { @@ -200,7 +200,7 @@ export const defaultNodeOption: RenderOption = { } return `${sanitizeHTML(next(node.children))}`; } - + if (type === 'asset') { const assetLink = getAttrString(node, 'asset-link'); const src = assetLink ? encodeURI(assetLink) : ''; @@ -219,7 +219,7 @@ export const defaultNodeOption: RenderOption = { const altAttr = alt ? ` alt="${alt}"` : ''; const targetAttr = target === "_blank" ? ` target="_blank"` : ''; const styleAttr = style ? ` style="${style}"` : ''; - + const imageTag = ``; const styleAttrFig = style ? ` style="${style}"` : ''; @@ -251,6 +251,9 @@ export const defaultNodeOption: RenderOption = { [MarkType.INLINE_CODE]:(text: string) => { return `${sanitizeHTML(text)}` }, + [MarkType.HIGHLIGHT]:(text: string) => { + return `${sanitizeHTML(text)}` + }, [MarkType.SUBSCRIPT]:(text: string) => { return `${sanitizeHTML(text)}` }, From 69e5509f0a47a528c4e10ee81df6697d42c3aa2a Mon Sep 17 00:00:00 2001 From: Eric Agnew Date: Thu, 2 Apr 2026 12:12:22 -0500 Subject: [PATCH 2/4] Remove auto formatting --- __test__/text-node-to-html.test.ts | 4 ++-- src/helper/enumerate-entries.ts | 14 +++++++------- src/nodes/mark-type.ts | 2 +- src/options/default-node-options.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/__test__/text-node-to-html.test.ts b/__test__/text-node-to-html.test.ts index 77a150c..c89aba7 100644 --- a/__test__/text-node-to-html.test.ts +++ b/__test__/text-node-to-html.test.ts @@ -15,7 +15,7 @@ describe('Text Node To HTML', () => { ...textNode, bold: true } - + const resultHtml = textNodeToHTML(node, { ...defaultNodeOption }) @@ -226,4 +226,4 @@ describe('Text Node To HTML', () => { expect(resultHtml).toEqual(`${textNode.text}`) done() }) -}) +}) \ No newline at end of file diff --git a/src/helper/enumerate-entries.ts b/src/helper/enumerate-entries.ts index da8ac50..df9c532 100644 --- a/src/helper/enumerate-entries.ts +++ b/src/helper/enumerate-entries.ts @@ -44,11 +44,11 @@ export function enumerateContents( export function textNodeToHTML(node: TextNode, renderOption: RenderOption): string { let text = replaceHtmlEntities(node.text); - + // Convert newlines to
tags if there are no other marks // This ensures newlines are always handled consistently let hasMarks = false; - + if (node.classname || node.id) { text = (renderOption[MarkType.CLASSNAME_OR_ID] as RenderMark)(text, node.classname, node.id); hasMarks = true; @@ -89,12 +89,12 @@ export function textNodeToHTML(node: TextNode, renderOption: RenderOption): stri text = (renderOption[MarkType.HIGHLIGHT] as RenderMark)(text); hasMarks = true; } - + // If no marks were applied, but text contains newlines, convert them to
if (!hasMarks && text.includes('\n')) { text = text.replace(/\n/g, '
'); } - + return text; } export function referenceToHTML( @@ -125,7 +125,7 @@ export function referenceToHTML( const aTag = `${entryText}`; return aTag; } - + if (!renderEmbed && renderOption[node.type] !== undefined) { return sendToRenderOption(node); } @@ -140,7 +140,7 @@ export function referenceToHTML( if (!item && renderOption[node.type] !== undefined) { return sendToRenderOption(node); } - + return findRenderString(item, metadata, renderOption); } @@ -188,4 +188,4 @@ function nodeToHTML( return (renderOption.default as RenderNode)(node, next); } } -} +} \ No newline at end of file diff --git a/src/nodes/mark-type.ts b/src/nodes/mark-type.ts index 01fade3..0d809db 100644 --- a/src/nodes/mark-type.ts +++ b/src/nodes/mark-type.ts @@ -14,4 +14,4 @@ enum MarkType { BREAK = 'break' } -export default MarkType +export default MarkType \ No newline at end of file diff --git a/src/options/default-node-options.ts b/src/options/default-node-options.ts index 04f2481..0339ec4 100644 --- a/src/options/default-node-options.ts +++ b/src/options/default-node-options.ts @@ -24,7 +24,7 @@ function getAttrString(node: Node, key: string): string | undefined { */ function buildCommonAttrs(node: Node): string { if (!node.attrs) return ''; - + const attrs: string[] = []; if (node.attrs.style) { attrs.push(` style="${node.attrs.style}"`); @@ -134,7 +134,7 @@ export const defaultNodeOption: RenderOption = { }); colgroupHTML += ``; } - + // Generate table with colgroup and other attributes return `${colgroupHTML}${sanitizeHTML(next(node.children))}`; }, @@ -157,7 +157,7 @@ export const defaultNodeOption: RenderOption = { const colSpan = getAttr(node, 'colSpan'); const rowSpanAttr = rowSpan ? ` rowspan="${rowSpan}"` : ''; const colSpanAttr = colSpan ? ` colspan="${colSpan}"` : ''; - + return `${sanitizeHTML(next(node.children))}` }, [NodeType.TABLE_DATA]:(node: Node, next: Next) => { @@ -167,7 +167,7 @@ export const defaultNodeOption: RenderOption = { const colSpan = getAttr(node, 'colSpan'); const rowSpanAttr = rowSpan ? ` rowspan="${rowSpan}"` : ''; const colSpanAttr = colSpan ? ` colspan="${colSpan}"` : ''; - + return `${sanitizeHTML(next(node.children))}` }, [NodeType.BLOCK_QUOTE]:(node: Node, next: Next) => { @@ -180,12 +180,12 @@ export const defaultNodeOption: RenderOption = { ['reference']:(node: Node, next: Next) => { const type = getAttr(node, 'type'); const displayType = getAttr(node, 'display-type'); - + if ((type === 'entry' || type === 'asset') && displayType === 'link'){ const href = getAttrString(node, 'href') || getAttrString(node, 'url') || ''; const target = getAttrString(node, 'target'); const assetUid = getAttrString(node, 'asset-uid'); - + let aTagAttrs = buildCommonAttrs(node); if (href) aTagAttrs += ` href="${href}"`; if (target) { @@ -200,7 +200,7 @@ export const defaultNodeOption: RenderOption = { } return `${sanitizeHTML(next(node.children))}`; } - + if (type === 'asset') { const assetLink = getAttrString(node, 'asset-link'); const src = assetLink ? encodeURI(assetLink) : ''; @@ -219,7 +219,7 @@ export const defaultNodeOption: RenderOption = { const altAttr = alt ? ` alt="${alt}"` : ''; const targetAttr = target === "_blank" ? ` target="_blank"` : ''; const styleAttr = style ? ` style="${style}"` : ''; - + const imageTag = ``; const styleAttrFig = style ? ` style="${style}"` : ''; From 3ea411064024978004cc922ada3274c26448b102 Mon Sep 17 00:00:00 2001 From: Eric Agnew Date: Thu, 2 Apr 2026 12:37:20 -0500 Subject: [PATCH 3/4] Add mark tag support --- src/helper/sanitize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper/sanitize.ts b/src/helper/sanitize.ts index 40839b1..e2ed8b4 100644 --- a/src/helper/sanitize.ts +++ b/src/helper/sanitize.ts @@ -1,5 +1,5 @@ -type AllowedTags = 'p' | 'a' | 'strong' | 'em' | 'ul' | 'ol' | 'li' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'sub' | 'u' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td' | 'span' | 'fragment' | 'strike' | 'sup' | 'br'| 'img' | 'colgroup' | 'col' | 'div'; +type AllowedTags = 'p' | 'a' | 'strong' | 'em' | 'ul' | 'ol' | 'li' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'sub' | 'u' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td' | 'span' | 'fragment' | 'strike' | 'sup' | 'br'| 'img' | 'colgroup' | 'col' | 'div' | 'mark'; type AllowedAttributes = 'href' | 'title' | 'target' | 'alt' | 'src' | 'class' | 'id' | 'style' | 'colspan' | 'rowspan' | 'content-type-uid' | 'data-sys-asset-uid' | 'sys-style-type' | 'data-type' | 'data-width' | 'data-rows' | 'data-cols' | 'data-mtec'; export function sanitizeHTML(input: string, allowedTags: AllowedTags[] = ['p', 'a', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'sub', 'u', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'span', 'fragment', 'sup', 'strike', 'br', 'img', 'colgroup', 'col', 'div'], allowedAttributes: AllowedAttributes[] = ['href', 'title', 'target', 'alt', 'src', 'class', 'id', 'style', 'colspan', 'rowspan', 'content-type-uid', 'data-sys-asset-uid', 'sys-style-type', 'data-type', 'data-width', 'data-rows', 'data-cols', 'data-mtec']): string { From e1196ef53bf32c0ef0021e5038eb9383e456d53d Mon Sep 17 00:00:00 2001 From: Eric Agnew Date: Thu, 2 Apr 2026 13:19:52 -0500 Subject: [PATCH 4/4] Add mark to default arguments whitelist --- src/helper/sanitize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper/sanitize.ts b/src/helper/sanitize.ts index e2ed8b4..26125f2 100644 --- a/src/helper/sanitize.ts +++ b/src/helper/sanitize.ts @@ -2,7 +2,7 @@ type AllowedTags = 'p' | 'a' | 'strong' | 'em' | 'ul' | 'ol' | 'li' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'sub' | 'u' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td' | 'span' | 'fragment' | 'strike' | 'sup' | 'br'| 'img' | 'colgroup' | 'col' | 'div' | 'mark'; type AllowedAttributes = 'href' | 'title' | 'target' | 'alt' | 'src' | 'class' | 'id' | 'style' | 'colspan' | 'rowspan' | 'content-type-uid' | 'data-sys-asset-uid' | 'sys-style-type' | 'data-type' | 'data-width' | 'data-rows' | 'data-cols' | 'data-mtec'; -export function sanitizeHTML(input: string, allowedTags: AllowedTags[] = ['p', 'a', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'sub', 'u', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'span', 'fragment', 'sup', 'strike', 'br', 'img', 'colgroup', 'col', 'div'], allowedAttributes: AllowedAttributes[] = ['href', 'title', 'target', 'alt', 'src', 'class', 'id', 'style', 'colspan', 'rowspan', 'content-type-uid', 'data-sys-asset-uid', 'sys-style-type', 'data-type', 'data-width', 'data-rows', 'data-cols', 'data-mtec']): string { +export function sanitizeHTML(input: string, allowedTags: AllowedTags[] = ['p', 'a', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'sub', 'u', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'span', 'fragment', 'sup', 'strike', 'br', 'img', 'colgroup', 'col', 'div', 'mark'], allowedAttributes: AllowedAttributes[] = ['href', 'title', 'target', 'alt', 'src', 'class', 'id', 'style', 'colspan', 'rowspan', 'content-type-uid', 'data-sys-asset-uid', 'sys-style-type', 'data-type', 'data-width', 'data-rows', 'data-cols', 'data-mtec']): string { // Replace newline characters with
before processing the HTML tags input = input.replace(/\n/g, '
');