feat: resolve block references in obsidian markdown
This commit is contained in:
		| @@ -1,6 +1,7 @@ | |||||||
| import { PluggableList } from "unified" | import { PluggableList } from "unified" | ||||||
| import { QuartzTransformerPlugin } from "../types" | import { QuartzTransformerPlugin } from "../types" | ||||||
| import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" | import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" | ||||||
|  | import { Element, Literal } from 'hast' | ||||||
| import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" | import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" | ||||||
| import { slug as slugAnchor } from "github-slugger" | import { slug as slugAnchor } from "github-slugger" | ||||||
| import rehypeRaw from "rehype-raw" | import rehypeRaw from "rehype-raw" | ||||||
| @@ -21,6 +22,7 @@ export interface Options { | |||||||
|   callouts: boolean |   callouts: boolean | ||||||
|   mermaid: boolean |   mermaid: boolean | ||||||
|   parseTags: boolean |   parseTags: boolean | ||||||
|  |   parseBlockReferences: boolean | ||||||
|   enableInHtmlEmbed: boolean |   enableInHtmlEmbed: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -31,6 +33,7 @@ const defaultOptions: Options = { | |||||||
|   callouts: true, |   callouts: true, | ||||||
|   mermaid: true, |   mermaid: true, | ||||||
|   parseTags: true, |   parseTags: true, | ||||||
|  |   parseBlockReferences: true, | ||||||
|   enableInHtmlEmbed: false, |   enableInHtmlEmbed: false, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -121,6 +124,7 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") | |||||||
| // (?:[-_\p{L}])+       -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores | // (?:[-_\p{L}])+       -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores | ||||||
| // (?:\/[-_\p{L}]+)*)   -> non-capturing group, matches an arbitrary number of tag strings separated by "/" | // (?:\/[-_\p{L}]+)*)   -> non-capturing group, matches an arbitrary number of tag strings separated by "/" | ||||||
| const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu") | const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu") | ||||||
|  | const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g") | ||||||
|  |  | ||||||
| export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( | export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( | ||||||
|   userOpts, |   userOpts, | ||||||
| @@ -133,29 +137,29 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | |||||||
|   } |   } | ||||||
|   const findAndReplace = opts.enableInHtmlEmbed |   const findAndReplace = opts.enableInHtmlEmbed | ||||||
|     ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { |     ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { | ||||||
|         if (replace) { |       if (replace) { | ||||||
|           visit(tree, "html", (node: HTML) => { |         visit(tree, "html", (node: HTML) => { | ||||||
|             if (typeof replace === "string") { |           if (typeof replace === "string") { | ||||||
|               node.value = node.value.replace(regex, replace) |             node.value = node.value.replace(regex, replace) | ||||||
|             } else { |           } else { | ||||||
|               node.value = node.value.replaceAll(regex, (substring: string, ...args) => { |             node.value = node.value.replaceAll(regex, (substring: string, ...args) => { | ||||||
|                 const replaceValue = replace(substring, ...args) |               const replaceValue = replace(substring, ...args) | ||||||
|                 if (typeof replaceValue === "string") { |               if (typeof replaceValue === "string") { | ||||||
|                   return replaceValue |                 return replaceValue | ||||||
|                 } else if (Array.isArray(replaceValue)) { |               } else if (Array.isArray(replaceValue)) { | ||||||
|                   return replaceValue.map(mdastToHtml).join("") |                 return replaceValue.map(mdastToHtml).join("") | ||||||
|                 } else if (typeof replaceValue === "object" && replaceValue !== null) { |               } else if (typeof replaceValue === "object" && replaceValue !== null) { | ||||||
|                   return mdastToHtml(replaceValue) |                 return mdastToHtml(replaceValue) | ||||||
|                 } else { |               } else { | ||||||
|                   return substring |                 return substring | ||||||
|                 } |               } | ||||||
|               }) |             }) | ||||||
|             } |           } | ||||||
|           }) |         }) | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mdastFindReplace(tree, regex, replace) |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       mdastFindReplace(tree, regex, replace) | ||||||
|  |     } | ||||||
|     : mdastFindReplace |     : mdastFindReplace | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
| @@ -353,9 +357,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | |||||||
|                 node.data = { |                 node.data = { | ||||||
|                   hProperties: { |                   hProperties: { | ||||||
|                     ...(node.data?.hProperties ?? {}), |                     ...(node.data?.hProperties ?? {}), | ||||||
|                     className: `callout ${collapse ? "is-collapsible" : ""} ${ |                     className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : "" | ||||||
|                       defaultState === "collapsed" ? "is-collapsed" : "" |                       }`, | ||||||
|                     }`, |  | ||||||
|                     "data-callout": calloutType, |                     "data-callout": calloutType, | ||||||
|                     "data-callout-fold": collapse, |                     "data-callout-fold": collapse, | ||||||
|                   }, |                   }, | ||||||
| @@ -411,11 +414,38 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | |||||||
|           } |           } | ||||||
|         }) |         }) | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return plugins |       return plugins | ||||||
|     }, |     }, | ||||||
|     htmlPlugins() { |     htmlPlugins() { | ||||||
|       return [rehypeRaw] |       const plugins = [rehypeRaw] | ||||||
|  |  | ||||||
|  |       if (opts.parseBlockReferences) { | ||||||
|  |         plugins.push(() => { | ||||||
|  |           return (tree, file) => { | ||||||
|  |             file.data.blocks = {} | ||||||
|  |             const validTagTypes = new Set(["blockquote", "p", "li"]) | ||||||
|  |             visit(tree, "element", (node, _index, _parent) => { | ||||||
|  |               if (validTagTypes.has(node.tagName)) { | ||||||
|  |                 const last = node.children.at(-1) as Literal | ||||||
|  |                 if (last.value && typeof last.value === 'string') { | ||||||
|  |                   const matches = last.value.match(blockReferenceRegex) | ||||||
|  |                   if (matches && matches.length >= 1) { | ||||||
|  |                     last.value = last.value.slice(0, -matches[0].length) | ||||||
|  |                     const block = matches[0].slice(1) | ||||||
|  |                     node.properties = { | ||||||
|  |                       ...node.properties, | ||||||
|  |                       id: block | ||||||
|  |                     } | ||||||
|  |                     file.data.blocks![block] = node | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return plugins | ||||||
|     }, |     }, | ||||||
|     externalResources() { |     externalResources() { | ||||||
|       const js: JSResource[] = [] |       const js: JSResource[] = [] | ||||||
| @@ -454,3 +484,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | |||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | declare module "vfile" { | ||||||
|  |   interface DataMap { | ||||||
|  |     blocks: Record<string, Element> | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user