| | |
| | | import { PluggableList } from "unified" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | 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 { slug as slugAnchor } from "github-slugger" |
| | | import rehypeRaw from "rehype-raw" |
| | |
| | | callouts: boolean |
| | | mermaid: boolean |
| | | parseTags: boolean |
| | | parseBlockReferences: boolean |
| | | enableInHtmlEmbed: boolean |
| | | } |
| | | |
| | |
| | | callouts: true, |
| | | mermaid: true, |
| | | parseTags: true, |
| | | parseBlockReferences: true, |
| | | enableInHtmlEmbed: false, |
| | | } |
| | | |
| | |
| | | const calloutMapping: Record<string, keyof typeof callouts> = { |
| | | note: "note", |
| | | abstract: "abstract", |
| | | summary: "abstract", |
| | | tldr: "abstract", |
| | | info: "info", |
| | | todo: "todo", |
| | | tip: "tip", |
| | |
| | | |
| | | function canonicalizeCallout(calloutName: string): keyof typeof callouts { |
| | | let callout = calloutName.toLowerCase() as keyof typeof calloutMapping |
| | | return calloutMapping[callout] ?? calloutName |
| | | return calloutMapping[callout] ?? "note" |
| | | } |
| | | |
| | | const capitalize = (s: string): string => { |
| | |
| | | // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) |
| | | // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias) |
| | | const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g") |
| | | const highlightRegex = new RegExp(/==(.+)==/, "g") |
| | | const highlightRegex = new RegExp(/==([^=]+)==/, "g") |
| | | const commentRegex = new RegExp(/%%(.+)%%/, "g") |
| | | // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts |
| | | const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) |
| | | const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") |
| | | // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line |
| | | // #(\w+) -> tag itself is # followed by a string of alpha-numeric characters |
| | | const tagRegex = new RegExp(/(?:^| )#(\p{L}+)/, "gu") |
| | | // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line |
| | | // #(...) -> capturing group, tag itself must start with # |
| | | // (?:[-_\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 "/" |
| | | 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> = ( |
| | | userOpts, |
| | |
| | | } |
| | | const findAndReplace = opts.enableInHtmlEmbed |
| | | ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { |
| | | if (replace) { |
| | | visit(tree, "html", (node: HTML) => { |
| | | if (typeof replace === "string") { |
| | | node.value = node.value.replace(regex, replace) |
| | | } else { |
| | | node.value = node.value.replaceAll(regex, (substring: string, ...args) => { |
| | | const replaceValue = replace(substring, ...args) |
| | | if (typeof replaceValue === "string") { |
| | | return replaceValue |
| | | } else if (Array.isArray(replaceValue)) { |
| | | return replaceValue.map(mdastToHtml).join("") |
| | | } else if (typeof replaceValue === "object" && replaceValue !== null) { |
| | | return mdastToHtml(replaceValue) |
| | | } else { |
| | | return substring |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | mdastFindReplace(tree, regex, replace) |
| | | if (replace) { |
| | | visit(tree, "html", (node: HTML) => { |
| | | if (typeof replace === "string") { |
| | | node.value = node.value.replace(regex, replace) |
| | | } else { |
| | | node.value = node.value.replaceAll(regex, (substring: string, ...args) => { |
| | | const replaceValue = replace(substring, ...args) |
| | | if (typeof replaceValue === "string") { |
| | | return replaceValue |
| | | } else if (Array.isArray(replaceValue)) { |
| | | return replaceValue.map(mdastToHtml).join("") |
| | | } else if (typeof replaceValue === "object" && replaceValue !== null) { |
| | | return mdastToHtml(replaceValue) |
| | | } else { |
| | | return substring |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | mdastFindReplace(tree, regex, replace) |
| | | } |
| | | : mdastFindReplace |
| | | |
| | | return { |
| | |
| | | |
| | | const titleHtml: HTML = { |
| | | type: "html", |
| | | value: `<div |
| | | value: `<div |
| | | class="callout-title" |
| | | > |
| | | <div class="callout-icon">${callouts[calloutType]}</div> |
| | |
| | | node.data = { |
| | | hProperties: { |
| | | ...(node.data?.hProperties ?? {}), |
| | | className: `callout ${collapse ? "is-collapsible" : ""} ${ |
| | | defaultState === "collapsed" ? "is-collapsed" : "" |
| | | }`, |
| | | className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : "" |
| | | }`, |
| | | "data-callout": calloutType, |
| | | "data-callout-fold": collapse, |
| | | }, |
| | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | return plugins |
| | | }, |
| | | 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() { |
| | | const js: JSResource[] = [] |
| | |
| | | script: ` |
| | | import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; |
| | | const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' |
| | | mermaid.initialize({ |
| | | mermaid.initialize({ |
| | | startOnLoad: false, |
| | | securityLevel: 'loose', |
| | | theme: darkMode ? 'dark' : 'default' |
| | |
| | | }, |
| | | } |
| | | } |
| | | |
| | | declare module "vfile" { |
| | | interface DataMap { |
| | | blocks: Record<string, Element> |
| | | } |
| | | } |
| | | |