-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
fix(ofm): support nested markdown in highlights #2223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fbfaabd
0006ce2
ef0c650
250fe44
23d2cfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string, string> = { | |||||||||||||||||||||
| "<=": "⇐", | ||||||||||||||||||||||
| "<==": "⇐", | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // 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<Partial<Options>> | |||||||||||||||||||||
| ]) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (opts.highlight) { | ||||||||||||||||||||||
| replacements.push([ | ||||||||||||||||||||||
| highlightRegex, | ||||||||||||||||||||||
| (_value: string, ...capture: string[]) => { | ||||||||||||||||||||||
| const [inner] = capture | ||||||||||||||||||||||
| return { | ||||||||||||||||||||||
| type: "html", | ||||||||||||||||||||||
| value: `<span class="text-highlight">${inner}</span>`, | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ]) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // 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<Partial<Options>> | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
||||||||||||||||||||||
| // Check for odd number of markers | |
| if (markers.length % 2 !== 0) { | |
| console.warn("Unpaired highlight marker found in content"); | |
| // Remove the last unpaired marker | |
| children.splice(markers[markers.length - 1], 1); | |
| // Remove the last marker from the list | |
| markers.pop(); | |
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there is an odd number of highlight markers (e.g., ==text1== some text ==text2), the last unpaired marker remains in the AST as a highlightMarker node. This could cause rendering issues or appear as unexpected content in the output.
Consider adding logic to handle odd markers, such as:
- Removing unpaired markers after processing pairs
- Converting unpaired markers back to their original
==text - Logging a warning about malformed highlight syntax
This would make the behavior more robust and predictable when users have malformed markup.
| if (markers.length % 2 === 1) { | |
| const lastMarkerIndex = markers[markers.length - 1] | |
| // Convert the unpaired marker back into its literal representation | |
| children[lastMarkerIndex] = { type: "text", value: "==" } as any | |
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation only matches highlight markers within a single parent node's immediate children. If a highlight spans across different structural boundaries (e.g., across list items or block quotes), the opening and closing markers won't be paired, leaving unpaired markers in the output.
While this may be acceptable for typical use cases (highlights within paragraphs), it's worth noting this limitation. Consider adding a comment explaining that highlights must be contained within a single parent node, or alternatively, implement a more sophisticated cross-boundary matching algorithm if this is a common use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
as anytype assertion bypasses TypeScript's type safety. Consider defining a proper interface for the customhighlightMarkernode type to maintain type safety throughout the transformation pipeline.