diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 7a523aa5936e1..d2a0cfed2fd4d 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -7,6 +7,7 @@ import { DefinitionContent, Paragraph, Code, + Parent, } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" @@ -100,6 +101,19 @@ const arrowMapping: Record = { "<=": "⇐", "<==": "⇐", } +// Marker node used for highlighting +interface HighlightMarker { + type: "highlightMarker" +} + +function isHighlightMarker(node: unknown): node is HighlightMarker { + return ( + typeof node === "object" && + node !== null && + "type" in node && + (node as { type: unknown }).type === "highlightMarker" + ) +} function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping @@ -128,7 +142,7 @@ export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\| // matches any wikilink, only used for escaping wikilinks inside tables export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\]|\[\^[^\]]*?\])/g) -const highlightRegex = new RegExp(/==([^=]+)==/g) +const highlightRegex = new RegExp(/==/g) const commentRegex = new RegExp(/%%[\s\S]*?%%/g) // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts const calloutRegex = new RegExp(/^\[\!([\w-]+)\|?(.+?)?\]([+-]?)/) @@ -306,18 +320,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> ]) } - if (opts.highlight) { - replacements.push([ - highlightRegex, - (_value: string, ...capture: string[]) => { - const [inner] = capture - return { - type: "html", - value: `${inner}`, - } - }, - ]) - } + // Highlight logic is handled separately below (lines 385-430) to support nested markdown if (opts.parseArrows) { replacements.push([ @@ -393,6 +396,55 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> } }) + if (opts.highlight) { + plugins.push(() => { + return (tree: Root) => { + mdastFindReplace(tree, [ + [highlightRegex, (_value: string) => ({ type: "highlightMarker" }) as any], + ]) + + visit(tree, (node) => { + if (!("children" in node)) return + const parent = node as Parent + const children = parent.children + if (children.length === 0) return + + const markers: number[] = [] + for (let i = 0; i < children.length; i++) { + if (isHighlightMarker(children[i])) { + markers.push(i) + } + } + + if (markers.length < 2) return + + const pairs: [number, number][] = [] + for (let i = 0; i < markers.length - 1; i += 2) { + pairs.push([markers[i], markers[i + 1]]) + } + + for (let i = pairs.length - 1; i >= 0; i--) { + const [start, end] = pairs[i] + const content = children.slice(start + 1, end) + const htmlContent = content + .map((n) => { + const hast = toHast(n, { allowDangerousHtml: true }) + return hast ? toHtml(hast, { allowDangerousHtml: true }) : "" + }) + .join("") + + const newNode: Html = { + type: "html", + value: `${htmlContent}`, + } + + children.splice(start, end - start + 1, newNode) + } + }) + } + }) + } + if (opts.enableVideoEmbed) { plugins.push(() => { return (tree: Root, _file) => {