fix roam matching

This commit is contained in:
Matt Vogel 2024-09-10 20:48:46 +02:00
parent 4cc8a5a192
commit 2263bcd1c2
2 changed files with 142 additions and 181 deletions

View File

@ -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

View File

@ -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: `<audio controls>
<source src="${url}" type="audio/mpeg">
<source src="${url}" type="audio/ogg">
Your browser does not support the audio tag.
</audio>`,
}
: 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: `<iframe
class="external-embed youtube"
width="600px"
height="350px"
src="https://www.youtube.com/embed/${videoId}${playlistId ? `?list=${playlistId}` : ""}"
frameborder="0"
allow="fullscreen"
></iframe>`,
}
} else {
return {
type: "html",
value: `<video controls>
<source src="${url}" type="video/mp4">
<source src="${url}" type="video/webm">
Your browser does not support the video tag.
</video>`,
}
}
case "pdf":
return opts.pdfComponent
? {
type: "html",
value: `<embed src="${url}" type="application/pdf" width="100%" height="600px" />`,
}
: null
default:
return null
}
}
export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
@ -53,167 +131,93 @@ export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | 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 = `<iframe width="600px" height="350px" src="https://www.youtube.com/embed/${text}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
// return video
// })
// src = src.replaceAll(videoRegex, (value, ...capture) => {
// const [match, text] = capture
// const video = `<video controls><source src="{${match}}" type="video/mp4"><source src="${match}" type="video/webm"></video>`
// return video
// })
// }
// if (opts.audioComponent) {
// src = src.toString()
// src = src.replaceAll(audioRegex, (value, ...capture) => {
// const [match, text] = capture
// const audio = `<audio controls>
// <source src="${match}" type="audio/mpeg">
// <source src="${match}" type="audio/ogg">
// Your browser does not support the audio tag.
// </audio>`
// return audio
// })
// }
// if (opts.pdfComponent) {
// src = src.toString()
// src = src.replaceAll(pdfRegex, (value, ...capture) => {
// const [match, text] = capture
// const pdf = `<embed src="${match}" type="application/pdf" width="600" height="800">`
// 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<Root, "paragraph">)
}
// 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: `<span class="text-highlight"> ${inner} </span>`,
}
},
(_value: string, inner: string) => ({
type: "html",
value: `<span class="text-highlight">${inner}</span>`,
}),
])
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 <select> element with options
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`;
return {
type: "html",
value: selectHtml,
};
const optionsString: string = matchResult[1]
const options: string[] = optionsString.split("|")
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`
return { type: "html", value: selectHtml }
},
]);
])
}
if (opts.TODOComponent) {
replacements.push([
TODORegex,
(match) => {
return {
type: "html",
value: `<input type="checkbox" disabled>`,
}
},
() => ({
type: "html",
value: `<input type="checkbox" disabled>`,
}),
])
}
if (opts.DONEComponent) {
replacements.push([
DONERegex,
(match) => {
return {
type: "html",
value: `<input type="checkbox" checked disabled>`,
}
},
() => ({
type: "html",
value: `<input type="checkbox" checked disabled>`,
}),
])
}
if (opts.blockquoteComponent) {
replacements.push([
blockquoteRegex,
(match, marker, content) => {
const blockquoteHtml = `<blockquote>${content.trim()}</blockquote>`;
return {
type: "html",
value: blockquoteHtml,
};
},
]);
(_match: string, _marker: string, content: string) => ({
type: "html",
value: `<blockquote>${content.trim()}</blockquote>`,
}),
])
}
if (opts.audioComponent) {
replacements.push([
audioRegex,
(_value: string, ...capture: string[]) => {
const [url] = capture;
console.log("");
console.log(" ~", capture);
const audioHtml = `<audio controls>
<source src="${url}" type="audio/mpeg">
<source src="${url}" type="audio/ogg">
Your browser does not support the audio tag.
</audio>`;
return {
type: "html",
value: audioHtml,
};
}
]);
}
mdastFindReplace(tree, replacements)
}
})
return plugins
},
}