From 2263bcd1c2c278a6d8d9798057d3f7bdfe51ecc1 Mon Sep 17 00:00:00 2001 From: Matt Vogel <> Date: Tue, 10 Sep 2024 20:48:46 +0200 Subject: [PATCH] fix roam matching --- content/roam-test.md | 43 ----- quartz/plugins/transformers/roam.ts | 280 ++++++++++++++-------------- 2 files changed, 142 insertions(+), 181 deletions(-) delete mode 100644 content/roam-test.md diff --git a/content/roam-test.md b/content/roam-test.md deleted file mode 100644 index ddc9a03b8..000000000 --- a/content/roam-test.md +++ /dev/null @@ -1,43 +0,0 @@ -### OR - -{{or:ONE|TWO|THREE}} - -### Tasks - -- {{[[TODO]]}} unchecked -- {{TODO}} unchecked -- {{[[DONE]]}} checked -- {{DONE}} checked - -### Text Styling - -- **Bold** -- __Italics__ -- Highlights ^^of some^^ words -- this one, ~~not this one~~ -- Blockquotes - - [[>]] Regulare quote using page brackets - - > bare quote without brackets - -### Uploaded Files - -- image - - ![](https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2Ftiz3q_Bm-P.png?alt=media&token=ede018a1-ba7d-4099-8e6c-fa225550ef0e) -- video - - {{[[video]]: https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2FBY1tSter6R.mp4?alt=media&token=07031095-4736-4b0b-899a-26852adbf245}} - - youtube - - {{[[video]]: https://www.youtube.com/watch?v=y1f6bfiSH94}} -- gif - - ![](https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2FzAgByL_oF2.gif?alt=media&token=f754e4ba-aadb-43ad-82de-a2f79dc66370) -- pdf - - {{[[pdf]]: https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2F6FtyOTB3mx.pdf?alt=media&token=0736266f-54d2-46dc-99a8-08f7e7874210}} -- audio - - This is being selected - - {{[[audio]]: this_works_and_is_selected}} - - This is not for some reason - - {{[[audio]]: https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2FZ37C1oBWmW.mp3?alt=media&token=433c0926-dbea-47f5-ab4f-3ec4659ba815}} -- binary files - - regular file - - https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2FkQXZxVL_6G.txt?alt=media&token=f35c8688-0ff4-4d59-845a-3bcaa53fc1ea - - .zip file - - https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMattVogel%2FEzyEq4p2Zn.zip?alt=media&token=0697c12e-654d-46a6-943e-fe13293870ba diff --git a/quartz/plugins/transformers/roam.ts b/quartz/plugins/transformers/roam.ts index e2f2a8967..b3be8f542 100644 --- a/quartz/plugins/transformers/roam.ts +++ b/quartz/plugins/transformers/roam.ts @@ -1,9 +1,11 @@ import { QuartzTransformerPlugin } from "../types" import { PluggableList } from "unified" import { SKIP, visit } from "unist-util-visit" -import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" -import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" +import { Root, Html, Paragraph, Text, Link, Parent } from "mdast" +import { Node } from "unist" +import { VFile } from "vfile" +import { BuildVisitor } from "unist-util-visit" export interface Options { orComponent: boolean @@ -32,20 +34,96 @@ const defaultOptions: Options = { const orRegex = new RegExp(/{{or:(.*?)}}/, "g") const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g") const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g") -const videoRegex = new RegExp(/{{.*?\bvideo\b.*?\:(.*?)}}/, "g") -const youtubeRegex = new RegExp(/{{.*?\bvideo\b.*?(\bhttp.*?\byoutu.*?)watch\?v=(.*?)}}/, "g") +const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g") +const youtubeRegex = new RegExp( + /{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/, + "g", +) // const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g") - -const audioRegex = new RegExp(/{{.*?\baudio\b.*?\:(.*?)}}/, "g") -const pdfRegex = new RegExp(/{{.*?\bpdf\b.*?\:(.*?)}}/, "g") -const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g"); +const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g") +const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g") +const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g") const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g") const roamItalicRegex = new RegExp(/__(.+)__/, "g") const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */ const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */ +function isSpecialEmbed(node: Paragraph): boolean { + if (node.children.length !== 2) return false + + const [textNode, linkNode] = node.children + return ( + textNode.type === "text" && + textNode.value.startsWith("{{[[") && + linkNode.type === "link" && + linkNode.children[0].type === "text" && + linkNode.children[0].value.endsWith("}}") + ) +} + +function transformSpecialEmbed(node: Paragraph, opts: Options): Html | null { + const [textNode, linkNode] = node.children as [Text, Link] + const embedType = textNode.value.match(/\{\{\[\[(.*?)\]\]:/)?.[1]?.toLowerCase() + const url = linkNode.url.slice(0, -2) // Remove the trailing '}}' + + switch (embedType) { + case "audio": + return opts.audioComponent + ? { + type: "html", + value: ``, + } + : null + case "video": + if (!opts.videoComponent) return null + // Check if it's a YouTube video + const youtubeMatch = url.match( + /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/, + ) + if (youtubeMatch) { + const videoId = youtubeMatch[1].split("&")[0] // Remove additional parameters + const playlistMatch = url.match(/[?&]list=([^#\&\?]*)/) + const playlistId = playlistMatch ? playlistMatch[1] : null + + return { + type: "html", + value: ``, + } + } else { + return { + type: "html", + value: ``, + } + } + case "pdf": + return opts.pdfComponent + ? { + type: "html", + value: ``, + } + : null + default: + return null + } +} + export const RoamFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( userOpts, ) => { @@ -53,167 +131,93 @@ export const RoamFlavoredMarkdown: QuartzTransformerPlugin | un return { name: "RoamFlavoredMarkdown", - textTransform(_ctx, src) { - // if (opts.videoComponent) { - // //youtube first then regular video links - // src = src.toString() - // src = src.replaceAll(youtubeRegex, (value, ...capture) => { - // const [match, text] = capture - // const video = `` - // return video - // }) - // src = src.replaceAll(videoRegex, (value, ...capture) => { - // const [match, text] = capture - // const video = `` - // return video - // }) - // } - // if (opts.audioComponent) { - // src = src.toString() - // src = src.replaceAll(audioRegex, (value, ...capture) => { - // const [match, text] = capture - // const audio = `` - // return audio - // }) - // } - // if (opts.pdfComponent) { - // src = src.toString() - // src = src.replaceAll(pdfRegex, (value, ...capture) => { - // const [match, text] = capture - // const pdf = `` - // return pdf - // }) - // } - // if (opts.blockquoteComponent) { - // src = src.toString() - // src = src.replaceAll(blockquoteRegex, (value, ...capture) => { - // const bq = `>` - // return bq - // }) - // } - // TODO attributes in roam are sort of like block level frontmatter or tags - - // roam italics - if (src instanceof Buffer) { - src = src.toString() - } - - src = src.replaceAll(roamItalicRegex, (value, ...capture) => { - const [match, text] = capture - const italic = `*${match}*` - return italic - }) - - - return src - }, - - markdownPlugins(ctx) { + markdownPlugins() { const plugins: PluggableList = [] - const cfg = ctx.cfg.configuration plugins.push(() => { - return (tree: Root, file) => { - const replacements: [RegExp, string | ReplaceFunction][] = [] - const base = pathToRoot(file.data.slug!) - // roam highlight syntax + return (tree: Root, file: VFile) => { + const replacements: [RegExp, ReplaceFunction][] = [] + + // Handle special embeds (audio, video, PDF) + if (opts.audioComponent || opts.videoComponent || opts.pdfComponent) { + visit(tree, "paragraph", ((node: Paragraph, index: number, parent: Parent | null) => { + if (isSpecialEmbed(node)) { + const transformedNode = transformSpecialEmbed(node, opts) + if (transformedNode && parent) { + parent.children[index] = transformedNode + } + } + }) as BuildVisitor) + } + + // Roam italic syntax + replacements.push([ + roamItalicRegex, + (_value: string, match: string) => ({ + type: "emphasis", + children: [{ type: "text", value: match }], + }), + ]) + + // Roam highlight syntax replacements.push([ roamHighlightRegex, - (_value: string, ...capture: string[]) => { - const [inner] = capture - return { - type: "html", - value: ` ${inner} `, - } - }, + (_value: string, inner: string) => ({ + type: "html", + value: `${inner}`, + }), ]) + if (opts.orComponent) { replacements.push([ orRegex, (match: string) => { - // Attempt to extract the content within the curly braces and split by '|' - const matchResult = match.match(/{{or:(.*?)}}/); + const matchResult = match.match(/{{or:(.*?)}}/) if (matchResult === null) { - // Handle the case where no match is found, e.g., return an empty string or some default value - return { type: "html", value: "" }; + return { type: "html", value: "" } } - - const optionsString: string = matchResult[1]; - const options: string[] = optionsString.split("|"); - - // Generate the HTML ${options.map((option: string) => ``).join("")}`; - - return { - type: "html", - value: selectHtml, - }; + const optionsString: string = matchResult[1] + const options: string[] = optionsString.split("|") + const selectHtml: string = `` + return { type: "html", value: selectHtml } }, - ]); + ]) } + if (opts.TODOComponent) { replacements.push([ TODORegex, - (match) => { - return { - type: "html", - value: ``, - } - }, + () => ({ + type: "html", + value: ``, + }), ]) } + if (opts.DONEComponent) { replacements.push([ DONERegex, - (match) => { - return { - type: "html", - value: ``, - } - }, + () => ({ + type: "html", + value: ``, + }), ]) } + if (opts.blockquoteComponent) { replacements.push([ blockquoteRegex, - (match, marker, content) => { - const blockquoteHtml = `
${content.trim()}
`; - return { - type: "html", - value: blockquoteHtml, - }; - }, - ]); + (_match: string, _marker: string, content: string) => ({ + type: "html", + value: `
${content.trim()}
`, + }), + ]) } - if (opts.audioComponent) { - replacements.push([ - audioRegex, - (_value: string, ...capture: string[]) => { - const [url] = capture; - console.log(""); - console.log(" ~", capture); - - const audioHtml = ``; - - return { - type: "html", - value: audioHtml, - }; - } - ]); - } - + mdastFindReplace(tree, replacements) } }) + return plugins }, }