From 39d9e2e2c1fda8ceddd2b76cf46d030e7abbcd98 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Wed, 20 Dec 2023 22:59:36 +0100 Subject: [PATCH] Tests --- .github/quartz/quartz.config.ts | 1 + .../quartz/plugins/transformers/comments.ts | 45 ++ .../quartz/quartz/plugins/transformers/ofm.ts | 545 ------------------ 3 files changed, 46 insertions(+), 545 deletions(-) create mode 100644 .github/quartz/quartz/plugins/transformers/comments.ts delete mode 100644 .github/quartz/quartz/plugins/transformers/ofm.ts diff --git a/.github/quartz/quartz.config.ts b/.github/quartz/quartz.config.ts index b5e66f1..7caea7c 100644 --- a/.github/quartz/quartz.config.ts +++ b/.github/quartz/quartz.config.ts @@ -48,6 +48,7 @@ const config: QuartzConfig = { priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower }), Plugin.SyntaxHighlighting(), + Plugin.Comments(), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false, comments: false }), Plugin.GitHubFlavoredMarkdown(), Plugin.CrawlLinks({ markdownLinkResolution: "relative" }), diff --git a/.github/quartz/quartz/plugins/transformers/comments.ts b/.github/quartz/quartz/plugins/transformers/comments.ts new file mode 100644 index 0000000..29e2651 --- /dev/null +++ b/.github/quartz/quartz/plugins/transformers/comments.ts @@ -0,0 +1,45 @@ +import { QuartzTransformerPlugin } from "../types" +import { Root } from "mdast" +import { Element, Root as HtmlRoot } from "hast" +import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" +import { PluggableList } from "unified" + +const commentRegex = new RegExp(/%%(.+)%%/, "g") + +export const Comments: QuartzTransformerPlugin = () => { + return { + name: "Comments", + markdownPlugins() { + const plugins: PluggableList = [] + + // regex replacements + plugins.push(() => { + return (tree: Root) => { + const replacements: [RegExp, string | ReplaceFunction][] = [] + + replacements.push([ + commentRegex, + (_value: string, ..._capture: string[]) => { + const [inner] = _capture + return { + type: "html", + value: `${inner}`, + } + }, + ]) + + mdastFindReplace(tree, replacements) + } + }) + + return plugins + }, + } +} + +declare module "vfile" { + interface DataMap { + blocks: Record + htmlAst: HtmlRoot + } +} diff --git a/.github/quartz/quartz/plugins/transformers/ofm.ts b/.github/quartz/quartz/plugins/transformers/ofm.ts deleted file mode 100644 index 76ce2aa..0000000 --- a/.github/quartz/quartz/plugins/transformers/ofm.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { QuartzTransformerPlugin } from "../types" -import { Root, Html, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" -import { Element, Literal, Root as HtmlRoot } from "hast" -import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" -import { slug as slugAnchor } from "github-slugger" -import rehypeRaw from "rehype-raw" -import { visit } from "unist-util-visit" -import path from "path" -import { JSResource } from "../../util/resources" -// @ts-ignore -import calloutScript from "../../components/scripts/callout.inline.ts" -import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" -import { toHast } from "mdast-util-to-hast" -import { toHtml } from "hast-util-to-html" -import { PhrasingContent } from "mdast-util-find-and-replace/lib" -import { capitalize } from "../../util/lang" -import { PluggableList } from "unified" - -export interface Options { - comments: boolean - highlight: boolean - wikilinks: boolean - callouts: boolean - mermaid: boolean - parseTags: boolean - parseBlockReferences: boolean - enableInHtmlEmbed: boolean -} - -const defaultOptions: Options = { - comments: true, - highlight: true, - wikilinks: true, - callouts: true, - mermaid: true, - parseTags: true, - parseBlockReferences: true, - enableInHtmlEmbed: false, -} - -const icons = { - infoIcon: ``, - pencilIcon: ``, - clipboardListIcon: ``, - checkCircleIcon: ``, - flameIcon: ``, - checkIcon: ``, - helpCircleIcon: ``, - alertTriangleIcon: ``, - xIcon: ``, - zapIcon: ``, - bugIcon: ``, - listIcon: ``, - quoteIcon: ``, -} - -const callouts = { - note: icons.pencilIcon, - abstract: icons.clipboardListIcon, - info: icons.infoIcon, - todo: icons.checkCircleIcon, - tip: icons.flameIcon, - success: icons.checkIcon, - question: icons.helpCircleIcon, - warning: icons.alertTriangleIcon, - failure: icons.xIcon, - danger: icons.zapIcon, - bug: icons.bugIcon, - example: icons.listIcon, - quote: icons.quoteIcon, -} - -const calloutMapping: Record = { - note: "note", - abstract: "abstract", - summary: "abstract", - tldr: "abstract", - info: "info", - todo: "todo", - tip: "tip", - hint: "tip", - important: "tip", - success: "success", - check: "success", - done: "success", - question: "question", - help: "question", - faq: "question", - warning: "warning", - attention: "warning", - caution: "warning", - failure: "failure", - missing: "failure", - fail: "failure", - danger: "danger", - error: "danger", - bug: "bug", - example: "example", - quote: "quote", - cite: "quote", -} - -function canonicalizeCallout(calloutName: string): keyof typeof callouts { - let callout = calloutName.toLowerCase() as keyof typeof calloutMapping - return calloutMapping[callout] ?? "note" -} - -// !? -> optional embedding -// \[\[ -> open brace -// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) -// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) -// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias) -export const wikilinkRegex = 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 -// #(...) -> capturing group, tag itself must start with # -// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores -// (?:\/[-_\p{L}\d\p{Z}]+)*) -> 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 | undefined> = ( - userOpts, -) => { - const opts = { ...defaultOptions, ...userOpts } - - const mdastToHtml = (ast: PhrasingContent | Paragraph) => { - const hast = toHast(ast, { allowDangerousHtml: true })! - return toHtml(hast, { allowDangerousHtml: true }) - } - - return { - name: "ObsidianFlavoredMarkdown", - textTransform(_ctx, src) { - // pre-transform blockquotes - if (opts.callouts) { - if (src instanceof Buffer) { - src = src.toString() - } - - src = src.replaceAll(calloutLineRegex, (value) => { - // force newline after title of callout - return value + "\n> " - }) - } - - // pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex) - if (opts.wikilinks) { - if (src instanceof Buffer) { - src = src.toString() - } - - src = src.replaceAll(wikilinkRegex, (value, ...capture) => { - const [rawFp, rawHeader, rawAlias] = capture - const fp = rawFp ?? "" - const anchor = rawHeader?.trim().replace(/^#+/, "") - const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" - const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : "" - const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" - const embedDisplay = value.startsWith("!") ? "!" : "" - return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` - }) - } - - return src - }, - markdownPlugins() { - const plugins: PluggableList = [] - - // regex replacements - plugins.push(() => { - return (tree: Root, file) => { - const replacements: [RegExp, string | ReplaceFunction][] = [] - const base = pathToRoot(file.data.slug!) - - if (opts.wikilinks) { - 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"].includes(ext)) { - const dims = alias ?? "" - let [width, height] = dims.split("x", 2) - width ||= "auto" - height ||= "auto" - return { - type: "image", - url, - data: { - hProperties: { - width, - height, - }, - }, - } - } 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 if (ext === "") { - 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, - }, - ], - } - }, - ]) - } - - if (opts.highlight) { - replacements.push([ - highlightRegex, - (_value: string, ...capture: string[]) => { - const [inner] = capture - return { - type: "html", - value: `${inner}`, - } - }, - ]) - } - - if (!opts.comments) { - replacements.push([ - commentRegex, - (_value: string, ..._capture: string[]) => { - const [inner] = capture - return { - type: "html", - value: `${inner}`, - } - }, - ]) - } - - if (opts.parseTags) { - replacements.push([ - tagRegex, - (_value: string, tag: string) => { - // Check if the tag only includes numbers - if (/^\d+$/.test(tag)) { - return false - } - - tag = slugTag(tag) - if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) { - file.data.frontmatter.tags.push(tag) - } - - return { - type: "link", - url: base + `/tags/${tag}`, - data: { - hProperties: { - className: ["tag-link"], - }, - }, - children: [ - { - type: "text", - value: `#${tag}`, - }, - ], - } - }, - ]) - } - - if (opts.enableInHtmlEmbed) { - visit(tree, "html", (node: Html) => { - for (const [regex, replace] of replacements) { - 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, replacements) - } - }) - - if (opts.callouts) { - plugins.push(() => { - return (tree: Root, _file) => { - visit(tree, "blockquote", (node) => { - if (node.children.length === 0) { - return - } - - // find first line - const firstChild = node.children[0] - if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") { - return - } - - const text = firstChild.children[0].value - const restChildren = firstChild.children.slice(1) - const [firstLine, ...remainingLines] = text.split("\n") - const remainingText = remainingLines.join("\n") - - const match = firstLine.match(calloutRegex) - if (match && match.input) { - const [calloutDirective, typeString, collapseChar] = match - const calloutType = canonicalizeCallout( - typeString.toLowerCase() as keyof typeof calloutMapping, - ) - const collapse = collapseChar === "+" || collapseChar === "-" - const defaultState = collapseChar === "-" ? "collapsed" : "expanded" - const titleContent = - match.input.slice(calloutDirective.length).trim() || capitalize(calloutType) - const titleNode: Paragraph = { - type: "paragraph", - children: [{ type: "text", value: titleContent + " " }, ...restChildren], - } - const title = mdastToHtml(titleNode) - - const toggleIcon = ` - - ` - - const titleHtml: Html = { - type: "html", - value: `
-
${callouts[calloutType]}
-
${title}
- ${collapse ? toggleIcon : ""} -
`, - } - - const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleHtml] - if (remainingText.length > 0) { - blockquoteContent.push({ - type: "paragraph", - children: [ - { - type: "text", - value: remainingText, - }, - ], - }) - } - - // replace first line of blockquote with title and rest of the paragraph text - node.children.splice(0, 1, ...blockquoteContent) - - // add properties to base blockquote - node.data = { - hProperties: { - ...(node.data?.hProperties ?? {}), - className: `callout ${collapse ? "is-collapsible" : ""} ${ - defaultState === "collapsed" ? "is-collapsed" : "" - }`, - "data-callout": calloutType, - "data-callout-fold": collapse, - }, - } - } - }) - } - }) - } - - if (opts.mermaid) { - plugins.push(() => { - return (tree: Root, _file) => { - visit(tree, "code", (node: Code) => { - if (node.lang === "mermaid") { - node.data = { - hProperties: { - className: ["mermaid"], - }, - } - } - }) - } - }) - } - - return plugins - }, - htmlPlugins() { - const plugins: PluggableList = [rehypeRaw] - if (opts.parseBlockReferences) { - plugins.push(() => { - const inlineTagTypes = new Set(["p", "li"]) - const blockTagTypes = new Set(["blockquote"]) - return (tree, file) => { - file.data.blocks = {} - - visit(tree, "element", (node, index, parent) => { - if (blockTagTypes.has(node.tagName)) { - const nextChild = parent?.children.at(index! + 2) as Element - if (nextChild && nextChild.tagName === "p") { - const text = nextChild.children.at(0) as Literal - if (text && text.value && text.type === "text") { - const matches = text.value.match(blockReferenceRegex) - if (matches && matches.length >= 1) { - parent!.children.splice(index! + 2, 1) - const block = matches[0].slice(1) - - if (!Object.keys(file.data.blocks!).includes(block)) { - node.properties = { - ...node.properties, - id: block, - } - file.data.blocks![block] = node - } - } - } - } - } else if (inlineTagTypes.has(node.tagName)) { - const last = node.children.at(-1) as Literal - if (last && 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) - - if (!Object.keys(file.data.blocks!).includes(block)) { - node.properties = { - ...node.properties, - id: block, - } - file.data.blocks![block] = node - } - } - } - } - }) - - file.data.htmlAst = tree - } - }) - } - - return plugins - }, - externalResources() { - const js: JSResource[] = [] - - if (opts.callouts) { - js.push({ - script: calloutScript, - loadTime: "afterDOMReady", - contentType: "inline", - }) - } - - if (opts.mermaid) { - js.push({ - 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({ - startOnLoad: false, - securityLevel: 'loose', - theme: darkMode ? 'dark' : 'default' - }); - document.addEventListener('nav', async () => { - await mermaid.run({ - querySelector: '.mermaid' - }) - }); - `, - loadTime: "afterDOMReady", - moduleType: "module", - contentType: "inline", - }) - } - - return { js } - }, - } -} - -declare module "vfile" { - interface DataMap { - blocks: Record - htmlAst: HtmlRoot - } -}