diff --git a/quartz/plugins/parsers/obsidian/arrows.ts b/quartz/plugins/parsers/obsidian/arrows.ts index 05ae8362f..575ed48f0 100644 --- a/quartz/plugins/parsers/obsidian/arrows.ts +++ b/quartz/plugins/parsers/obsidian/arrows.ts @@ -2,6 +2,7 @@ import { QuartzTransformerPlugin } from "../../types" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { SKIP } from "unist-util-visit" import { Root } from "mdast" +import { PluggableList } from "unified" interface Options { enabled: Boolean @@ -24,11 +25,11 @@ const arrowMapping: Record = { const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/g) -export const ObsidianMarkdownArrow: QuartzTransformerPlugin> = (userOpts) => { +export const ObsidianArrow: QuartzTransformerPlugin> = (userOpts) => { const opts: Options = { ...defaultOptions, ...userOpts } return { - name: "ObsidianMarkdownArrow", - markdownPlugins() { + name: "ObsidianArrow", + markdownPlugins(_ctx) { return [ (tree: Root) => { if (opts.enabled) { @@ -47,7 +48,7 @@ export const ObsidianMarkdownArrow: QuartzTransformerPlugin> = mdastFindReplace(tree, replacements) } }, - ] + ] as PluggableList }, } } diff --git a/quartz/plugins/parsers/obsidian/highlights.ts b/quartz/plugins/parsers/obsidian/highlights.ts index 03d2b02f1..6e9e11058 100644 --- a/quartz/plugins/parsers/obsidian/highlights.ts +++ b/quartz/plugins/parsers/obsidian/highlights.ts @@ -1,6 +1,7 @@ import { QuartzTransformerPlugin } from "../../types" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { Root } from "mdast" +import { PluggableList } from "unified" interface Options { enabled: Boolean @@ -12,11 +13,11 @@ const defaultOptions: Options = { const highlightRegex = new RegExp(/==([^=]+)==/g) -export const ObsidianMarkdownHighlights: QuartzTransformerPlugin> = (userOpts) => { +export const ObsidianHighlights: QuartzTransformerPlugin> = (userOpts) => { const opts: Options = { ...defaultOptions, ...userOpts } return { - name: "ObsidianMarkdownHighlights", - markdownPlugins() { + name: "ObsidianHighlights", + markdownPlugins(ctx) { return [ (tree: Root) => { if (opts.enabled) { @@ -34,7 +35,7 @@ export const ObsidianMarkdownHighlights: QuartzTransformerPlugin matches the header row +// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator +// (\|([^\n])+\|\n)+ -> matches the body rows +const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm) + +// matches any wikilink, only used for escaping wikilinks inside tables +const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g) + +// !? -> optional embedding +// \[\[ -> open brace +// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) +// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) +// (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias) +const wikilinkRegex = new RegExp( + /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/g, +) + +const wikilinkImageEmbedRegex = new RegExp( + /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, +) + +export const ObsidianWikilinks: QuartzTransformerPlugin> = (userOpts) => { + const opts: Options = { ...defaultOptions, ...userOpts } + return { + name: "ObsidianWikilinks", + textTransform(_ctx, src: string | Buffer) { + if (src instanceof Buffer) { + src = src.toString() + } + + // replace all wikilinks inside a table first + src = src.replace(tableRegex, (value) => { + // escape all aliases and headers in wikilinks inside a table + return value.replace(tableWikilinkRegex, (_value, raw) => { + // const [raw]: (string | undefined)[] = capture + let escaped = raw ?? "" + escaped = escaped.replace("#", "\\#") + // escape pipe characters if they are not already escaped + escaped = escaped.replace(/((^|[^\\])(\\\\)*)\|/g, "$1\\|") + + return escaped + }) + }) + + // replace all other wikilinks + src = src.replace(wikilinkRegex, (value, ...capture) => { + const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture + + const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) + const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : "" + const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : "" + const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" + const embedDisplay = value.startsWith("!") ? "!" : "" + + if (rawFp?.match(externalLinkRegex)) { + return `${embedDisplay}[${displayAlias.replace(/^\|/, "")}](${rawFp})` + } + + return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` + }) + + return src + }, + markdownPlugins(_ctx) { + return [ + (tree: Root, path) => { + if (opts.enabled) { + const replacements: [RegExp, string | ReplaceFunction][] = [] + replacements.push([ + wikilinkRegex, + (value: string, ...capture: string[]) => { + let [rawFp, rawHeader, rawAlias] = capture + const fp = rawFp?.trim() ?? "" + const anchor = rawHeader?.trim() ?? "" + const alias = rawAlias?.slice(1).trim() + + // embed cases + if (value.startsWith("!")) { + const ext: string = path.extname(fp).toLowerCase() + const url = slugifyFilePath(fp as FilePath) + if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) { + const match = wikilinkImageEmbedRegex.exec(alias ?? "") + const alt = match?.groups?.alt ?? "" + const width = match?.groups?.width ?? "auto" + const height = match?.groups?.height ?? "auto" + return { + type: "image", + url, + data: { + hProperties: { + width, + height, + alt, + }, + }, + } + } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) { + return { + type: "html", + value: ``, + } + } else if ( + [".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext) + ) { + return { + type: "html", + value: ``, + } + } else if ([".pdf"].includes(ext)) { + return { + type: "html", + value: ``, + } + } else { + const block = anchor + return { + type: "html", + data: { hProperties: { transclude: true } }, + value: `
Transclude of ${url}${block}
`, + } + } + + // otherwise, fall through to regular link + } + + // internal link + const url = fp + anchor + return { + type: "link", + url, + children: [ + { + type: "text", + value: alias ?? fp, + }, + ], + } + }, + ]) + mdastFindReplace(tree, replacements) + } + }, + ] as PluggableList + }, + } +} diff --git a/quartz/plugins/transformers/markdown.ts b/quartz/plugins/transformers/markdown.ts index dda268314..d733f8e33 100644 --- a/quartz/plugins/transformers/markdown.ts +++ b/quartz/plugins/transformers/markdown.ts @@ -35,7 +35,7 @@ import smartypants from "remark-smartypants" import rehypeSlug from "rehype-slug" import rehypeAutolinkHeadings from "rehype-autolink-headings" -import { ObsidianMarkdownArrow, ObsidianMarkdownHighlights } from "../parsers/obsidian" +import { ObsidianArrow, ObsidianHighlights, ObsidianWikilinks } from "../parsers/obsidian" export interface CommonMarkOptions { option1: Boolean @@ -172,10 +172,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin { @@ -183,9 +185,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin