mirror of
https://github.com/alrayyes/wiki.git
synced 2024-11-29 22:46:22 +00:00
225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
|
import { QuartzTransformerPlugin } from "../types"
|
||
|
import { PluggableList } from "unified"
|
||
|
import { SKIP, visit } from "unist-util-visit"
|
||
|
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
|
||
|
TODOComponent: boolean
|
||
|
DONEComponent: boolean
|
||
|
videoComponent: boolean
|
||
|
audioComponent: boolean
|
||
|
pdfComponent: boolean
|
||
|
blockquoteComponent: boolean
|
||
|
tableComponent: boolean
|
||
|
attributeComponent: boolean
|
||
|
}
|
||
|
|
||
|
const defaultOptions: Options = {
|
||
|
orComponent: true,
|
||
|
TODOComponent: true,
|
||
|
DONEComponent: true,
|
||
|
videoComponent: true,
|
||
|
audioComponent: true,
|
||
|
pdfComponent: true,
|
||
|
blockquoteComponent: true,
|
||
|
tableComponent: true,
|
||
|
attributeComponent: true,
|
||
|
}
|
||
|
|
||
|
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
|
||
|
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
|
||
|
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "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(/{{.*?\[\[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,
|
||
|
) => {
|
||
|
const opts = { ...defaultOptions, ...userOpts }
|
||
|
|
||
|
return {
|
||
|
name: "RoamFlavoredMarkdown",
|
||
|
markdownPlugins() {
|
||
|
const plugins: PluggableList = []
|
||
|
|
||
|
plugins.push(() => {
|
||
|
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, inner: string) => ({
|
||
|
type: "html",
|
||
|
value: `<span class="text-highlight">${inner}</span>`,
|
||
|
}),
|
||
|
])
|
||
|
|
||
|
if (opts.orComponent) {
|
||
|
replacements.push([
|
||
|
orRegex,
|
||
|
(match: string) => {
|
||
|
const matchResult = match.match(/{{or:(.*?)}}/)
|
||
|
if (matchResult === null) {
|
||
|
return { type: "html", value: "" }
|
||
|
}
|
||
|
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,
|
||
|
() => ({
|
||
|
type: "html",
|
||
|
value: `<input type="checkbox" disabled>`,
|
||
|
}),
|
||
|
])
|
||
|
}
|
||
|
|
||
|
if (opts.DONEComponent) {
|
||
|
replacements.push([
|
||
|
DONERegex,
|
||
|
() => ({
|
||
|
type: "html",
|
||
|
value: `<input type="checkbox" checked disabled>`,
|
||
|
}),
|
||
|
])
|
||
|
}
|
||
|
|
||
|
if (opts.blockquoteComponent) {
|
||
|
replacements.push([
|
||
|
blockquoteRegex,
|
||
|
(_match: string, _marker: string, content: string) => ({
|
||
|
type: "html",
|
||
|
value: `<blockquote>${content.trim()}</blockquote>`,
|
||
|
}),
|
||
|
])
|
||
|
}
|
||
|
|
||
|
mdastFindReplace(tree, replacements)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return plugins
|
||
|
},
|
||
|
}
|
||
|
}
|