diff --git a/package.json b/package.json index c3bf0e2db..5bb2cb09f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "remark-obsidian-callout": "^1.1.3", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "remark-smartypants": "^2.0.0", diff --git a/quartz/build.ts b/quartz/build.ts index db8e4a977..26e354c2e 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -26,8 +26,8 @@ export default async function buildQuartz(argv: Argv, version: string) { const pluginCount = Object.values(cfg.plugins).flat().length const pluginNames = (key: 'transformers' | 'filters' | 'emitters') => cfg.plugins[key].map(plugin => plugin.name) - console.log(`Loaded ${pluginCount} plugins`) if (argv.verbose) { + console.log(`Loaded ${pluginCount} plugins`) console.log(` Transformers: ${pluginNames('transformers').join(", ")}`) console.log(` Filters: ${pluginNames('filters').join(", ")}`) console.log(` Emitters: ${pluginNames('emitters').join(", ")}`) @@ -47,7 +47,7 @@ export default async function buildQuartz(argv: Argv, version: string) { ignore: cfg.configuration.ignorePatterns, gitignore: true, }) - console.log(`Found ${fps.length} input files in ${perf.timeSince('glob')}`) + console.log(`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince('glob')}`) const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}`) const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose) diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts index 1cb0fc69b..dd6bdec31 100644 --- a/quartz/plugins/transformers/gfm.ts +++ b/quartz/plugins/transformers/gfm.ts @@ -33,7 +33,7 @@ export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin { ? [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append', content: { type: 'text', - value: '§' + value: ' §' } }]] : [] diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 1dad0b4c3..2e70cf1a5 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -1,18 +1,91 @@ import { PluggableList } from "unified" import { QuartzTransformerPlugin } from "../types" -import { Root } from 'mdast' +import { Root, HTML, BlockContent, DefinitionContent } from 'mdast' import { findAndReplace } from "mdast-util-find-and-replace" import { slugify } from "../../path" import rehypeRaw from "rehype-raw" +import { visit } from "unist-util-visit" export interface Options { highlight: boolean wikilinks: boolean + callouts: boolean } const defaultOptions: Options = { highlight: true, wikilinks: true, + callouts: true +} + +const icons = { + infoIcon: ``, + pencilIcon: ``, + clipboardListIcon: ``, + checkCircleIcon: ``, + flameIcon: ``, + checkIcon: ``, + helpCircleIcon: ``, + alertTriangleIcon: ``, + xIcon: ``, + zapIcon: ``, + bugIcon: ``, + listIcon: ``, + quoteIcon: ``, +} + +function canonicalizeCallout(calloutName: string): keyof typeof callouts { + let callout = calloutName.toLowerCase() as keyof typeof calloutMapping + + const calloutMapping: Record = { + note: "note", + abstract: "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" + } + + return calloutMapping[callout] +} + +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 capitalize = (s: string): string => { + return s.substring(0, 1).toUpperCase() + s.substring(1); } export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin { @@ -76,6 +149,74 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin { }) } + if (this.opts.callouts) { + plugins.push(() => { + // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts + const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) + 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 [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 = typeString.toLowerCase() as keyof typeof callouts + const collapse = collapseChar === "+" || collapseChar === "-" + const defaultState = collapseChar === "-" ? "collapsed" : "expanded" + const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType) + + const titleNode: HTML = { + type: "html", + value: `
+
${callouts[canonicalizeCallout(calloutType)]}
+
${title}
+
` + } + + const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode] + 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, + } + } + } + }) + } + }) + } + return plugins } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 5b2863934..260f007e1 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -1,4 +1,5 @@ @import "./syntax.scss"; +@import "./callouts.scss"; html { scroll-behavior: smooth; @@ -66,7 +67,6 @@ a { } blockquote { - font-style: italic; margin-left: 0; border-left: 3px solid var(--secondary); padding-left: 1rem; diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss new file mode 100644 index 000000000..9e2ff2ddf --- /dev/null +++ b/quartz/styles/callouts.scss @@ -0,0 +1,86 @@ +@use "sass:color"; + +.callout { + border: 1px solid var(--border); + background-color: var(--bg); + border-radius: 5px; + padding: 0 0.7rem; + + &[data-callout="note"] { + --color: #448aff; + --border: #448aff22; + --bg: #448aff09; + } + + &[data-callout="abstract"] { + --color: #00b0ff; + --border: #00b0ff22; + --bg: #00b0ff09; + } + + &[data-callout="info"], &[data-callout="todo"] { + --color: #00b8d4; + --border: #00b8d422; + --bg: #00b8d409; + } + + &[data-callout="tip"] { + --color: #00bfa5; + --border: #00bfa522; + --bg: #00bfa509; + } + + &[data-callout="success"] { + --color: #09ad7a; + --border: #09ad7122; + --bg: #09ad7109; + } + + &[data-callout="question"] { + --color: #dba642; + --border: #dba64222; + --bg: #dba64209; + } + + &[data-callout="warning"] { + --color: #db8942; + --border: #db894222; + --bg: #db894209; + } + + &[data-callout="failure"], &[data-callout="danger"], &[data-callout="bug"] { + --color: #db4242; + --border: #db424222; + --bg: #db424209; + } + + &[data-callout="example"] { + --color: #7a43b5; + --border: #7a43b522; + --bg: #7a43b509; + } + + &[data-callout="quote"] { + --color: var(--secondary); + --border: var(--lightgray); + } +} + + +.callout-title { + display: flex; + align-items: center; + gap: 5px; + margin: 1rem 0; + color: var(--color); +} + +.callout-icon { + width: 18px; + height: 18px; +} + +.callout-title-inner { + font-weight: 700; +} +