diff --git a/package-lock.json b/package-lock.json index 6fc560a..3b872f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,19 @@ { - "name": "@quartz-community/plugin-template", + "name": "@quartz-community/github-flavored-markdown", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@quartz-community/plugin-template", + "name": "@quartz-community/github-flavored-markdown", "version": "0.1.0", "license": "MIT", "dependencies": { "@quartz-community/types": "github:quartz-community/types", - "mdast-util-find-and-replace": "^3.0.1", + "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.3" + "remark-smartypants": "^3.0.2" }, "devDependencies": { "@types/hast": "^3.0.4", @@ -1123,6 +1119,15 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "24.10.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", @@ -1338,7 +1343,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@vitest/expect": { @@ -1549,6 +1553,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2459,6 +2473,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-string": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", @@ -3609,6 +3636,19 @@ "dev": true, "license": "MIT" }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3692,6 +3732,24 @@ "node": ">=6" } }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3946,6 +4004,24 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-slug": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", @@ -3997,6 +4073,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -4022,6 +4113,67 @@ "node": ">=4" } }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4594,6 +4746,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -4622,6 +4788,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", diff --git a/package.json b/package.json index 4c507b6..f049d19 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,21 @@ { - "name": "@quartz-community/plugin-template", + "name": "@quartz-community/github-flavored-markdown", "version": "0.1.0", - "description": "Template repository for Quartz community plugins.", + "description": "GitHub Flavored Markdown transformer plugin for Quartz", "type": "module", "license": "MIT", "author": "Quartz Community", - "homepage": "https://quartz.jzhao.xyz", + "homepage": "https://github.com/quartz-community/github-flavored-markdown", "repository": { "type": "git", - "url": "https://github.com/quartz-community/plugin-template" + "url": "https://github.com/quartz-community/github-flavored-markdown" }, "keywords": [ "quartz", "quartz-plugin", - "plugin-template", - "remark", - "rehype", - "mdast", - "hast" + "gfm", + "github-flavored-markdown", + "remark" ], "files": [ "dist", @@ -30,14 +28,6 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./types": { - "types": "./dist/types.d.ts", - "import": "./dist/types.js" - }, - "./components": { - "types": "./dist/components/index.d.ts", - "import": "./dist/components/index.js" - }, "./package.json": "./package.json" }, "main": "./dist/index.js", @@ -67,14 +57,10 @@ }, "dependencies": { "@quartz-community/types": "github:quartz-community/types", - "mdast-util-find-and-replace": "^3.0.1", - "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.3" + "remark-smartypants": "^3.0.2", + "rehype-slug": "^6.0.0", + "rehype-autolink-headings": "^7.1.0" }, "devDependencies": { "@types/hast": "^3.0.4", diff --git a/src/components/ExampleComponent.tsx b/src/components/ExampleComponent.tsx deleted file mode 100644 index d18ee6a..0000000 --- a/src/components/ExampleComponent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { - QuartzComponent, - QuartzComponentProps, - QuartzComponentConstructor, -} from "@quartz-community/types"; -import { classNames } from "../util/lang"; -import { i18n } from "../i18n"; -import style from "./styles/example.scss"; -// @ts-ignore -import script from "./scripts/example.inline.ts"; - -export interface ExampleComponentOptions { - prefix?: string; - suffix?: string; - className?: string; -} - -export default ((opts?: ExampleComponentOptions) => { - const { prefix = "", suffix = "", className = "example-component" } = opts ?? {}; - - const Component: QuartzComponent = (props: QuartzComponentProps) => { - const frontmatter = props.fileData?.frontmatter as { title?: string } | undefined; - const title = frontmatter?.title ?? "Untitled"; - const fullText = `${prefix}${title}${suffix}`; - - return
{fullText}
; - }; - - Component.css = style; - Component.afterDOMLoaded = script; - - return Component; -}) satisfies QuartzComponentConstructor; diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index f2ca23a..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as ExampleComponent } from "./ExampleComponent"; -export type { ExampleComponentOptions } from "./ExampleComponent"; diff --git a/src/components/scripts.d.ts b/src/components/scripts.d.ts deleted file mode 100644 index 89df165..0000000 --- a/src/components/scripts.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.inline.ts" { - const content: string; - export default content; -} diff --git a/src/components/scripts/example.inline.ts b/src/components/scripts/example.inline.ts deleted file mode 100644 index 366f8e5..0000000 --- a/src/components/scripts/example.inline.ts +++ /dev/null @@ -1,107 +0,0 @@ -// @ts-nocheck -// ============================================================================ -// Example Inline Script for Quartz Community Plugin -// ============================================================================ -// This file demonstrates patterns commonly used in Quartz plugin client-side code. -// It is bundled as a string and injected via Component.afterDOMLoaded. -// -// Key patterns demonstrated: -// 1. Listening to Quartz navigation events ('nav', 'prenav') -// 2. Fetching content index data -// 3. DOM manipulation with cleanup -// 4. State persistence (localStorage/sessionStorage) -// 5. Keyboard shortcut handling -// 6. Proper event listener cleanup -// ============================================================================ - -// Helper: Remove all children from an element -function removeAllChildren(element) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } -} - -// Helper: Simplify slug by removing trailing /index -function simplifySlug(slug) { - if (slug.endsWith("/index")) { - return slug.slice(0, -6); - } - return slug; -} - -// Helper: Get current page slug from URL -function getCurrentSlug() { - let slug = window.location.pathname; - if (slug.startsWith("/")) slug = slug.slice(1); - if (slug.endsWith("/")) slug = slug.slice(0, -1); - return slug || "index"; -} - -// Helper: Fetch content index (commonly needed for search, graph, explorer) -async function fetchContentIndex() { - try { - const response = await fetch("/static/contentIndex.json"); - const data = await response.json(); - // Handle both formats: { "slug": {...} } or { "content": { "slug": {...} } } - return data.content || data; - } catch (error) { - console.error("[Plugin] Error fetching content index:", error); - return null; - } -} - -// Main initialization function -function init() { - const components = document.querySelectorAll(".example-component"); - if (components.length === 0) return; - - // Example: Track cleanup functions for event listeners - const cleanupFns = []; - - // Example: Add a keyboard shortcut (Ctrl/Cmd + Shift + E) - function keyboardHandler(e) { - if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "e") { - e.preventDefault(); - console.log("[ExampleComponent] Keyboard shortcut triggered!"); - // Do something interesting here - } - } - - document.addEventListener("keydown", keyboardHandler); - cleanupFns.push(() => document.removeEventListener("keydown", keyboardHandler)); - - // Example: Click handler with proper cleanup - for (const component of components) { - const clickHandler = () => { - console.log("[ExampleComponent] Clicked!"); - }; - component.addEventListener("click", clickHandler); - cleanupFns.push(() => component.removeEventListener("click", clickHandler)); - } - - // Register cleanup with Quartz's cleanup system - if (typeof window !== "undefined" && window.addCleanup) { - window.addCleanup(() => { - cleanupFns.forEach((fn) => fn()); - }); - } - - console.log("[ExampleComponent] Initialized with", components.length, "component(s)"); -} - -// Listen to Quartz navigation events -// 'nav' fires after page navigation (including initial load) -document.addEventListener("nav", (e) => { - const slug = e.detail?.url || getCurrentSlug(); - console.log("[ExampleComponent] Navigation to:", slug); - init(); -}); - -// 'prenav' fires before navigation - use for saving state -document.addEventListener("prenav", () => { - // Example: Save scroll position before navigation - const component = document.querySelector(".example-component"); - if (component) { - sessionStorage.setItem("exampleScrollTop", component.scrollTop?.toString() || "0"); - } -}); diff --git a/src/components/styles.d.ts b/src/components/styles.d.ts deleted file mode 100644 index 0a31e5e..0000000 --- a/src/components/styles.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.scss" { - const content: string; - export default content; -} diff --git a/src/components/styles/example.scss b/src/components/styles/example.scss deleted file mode 100644 index 49b4379..0000000 --- a/src/components/styles/example.scss +++ /dev/null @@ -1,8 +0,0 @@ -.example-component { - padding: 8px 16px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-radius: 4px; - font-weight: 600; - display: inline-block; -} diff --git a/src/emitter.ts b/src/emitter.ts deleted file mode 100644 index 00d69be..0000000 --- a/src/emitter.ts +++ /dev/null @@ -1,91 +0,0 @@ -import path from "node:path"; -import fs from "node:fs/promises"; -import type { - QuartzEmitterPlugin, - ProcessedContent, - BuildCtx, - FilePath, - FullSlug, -} from "@quartz-community/types"; -import type { ExampleEmitterOptions } from "./types"; - -const defaultOptions: ExampleEmitterOptions = { - manifestSlug: "plugin-manifest", - includeFrontmatter: true, - metadata: { - generator: "Quartz Plugin Template", - }, -}; - -const joinSegments = (...segments: string[]) => - segments - .filter((segment) => segment.length > 0) - .join("/") - .replace(/\/+/g, "/") as FilePath; - -const writeFile = async ( - outputDir: string, - slug: FullSlug, - ext: `.${string}` | "", - content: string, -) => { - const outputPath = joinSegments(outputDir, `${slug}${ext}`) as FilePath; - await fs.mkdir(path.dirname(outputPath), { recursive: true }); - await fs.writeFile(outputPath, content); - return outputPath; -}; - -/** - * Example emitter that writes a JSON manifest of content metadata. - */ -export const ExampleEmitter: QuartzEmitterPlugin> = ( - userOptions?: Partial, -) => { - const options = { ...defaultOptions, ...userOptions }; - const emitManifest = async (ctx: BuildCtx, content: ProcessedContent[]) => { - const manifest = { - ...options.metadata, - generatedAt: new Date().toISOString(), - pages: content.map(([_tree, vfile]) => { - const frontmatter = (vfile.data?.frontmatter ?? {}) as { - title?: string; - tags?: string[]; - [key: string]: unknown; - }; - return { - slug: vfile.data?.slug ?? null, - title: frontmatter.title ?? null, - tags: frontmatter.tags ?? null, - filePath: vfile.data?.filePath ?? null, - frontmatter: options.includeFrontmatter ? frontmatter : undefined, - }; - }), - }; - - let json = `${JSON.stringify(manifest, null, 2)}\n`; - if (options.transformManifest) { - json = options.transformManifest(json); - } - - const output = await writeFile( - ctx.argv.output, - options.manifestSlug as FullSlug, - ".json", - json, - ); - return [output]; - }; - - return { - name: "ExampleEmitter", - async emit(ctx, content, _resources) { - return emitManifest(ctx, content); - }, - async *partialEmit(ctx, content, _resources, _changeEvents) { - const outputPaths = await emitManifest(ctx, content); - for (const outputPath of outputPaths) { - yield outputPath; - } - }, - }; -}; diff --git a/src/filter.ts b/src/filter.ts deleted file mode 100644 index 5d308c6..0000000 --- a/src/filter.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { QuartzFilterPlugin, ProcessedContent, BuildCtx } from "@quartz-community/types"; -import type { ExampleFilterOptions } from "./types"; - -const defaultOptions: ExampleFilterOptions = { - allowDrafts: false, - excludeTags: ["private"], - excludePathPrefixes: ["_drafts/", "_private/"], -}; - -const normalizeTag = (tag: unknown) => (typeof tag === "string" ? tag.trim().toLowerCase() : ""); - -const includesTag = (tags: unknown, excludedTags: string[]) => { - if (!Array.isArray(tags)) { - return false; - } - - const normalizedExcluded = excludedTags.map((tag) => tag.toLowerCase()); - return tags.some((tag) => normalizedExcluded.includes(normalizeTag(tag))); -}; - -/** - * Example filter that removes drafts, tagged pages, and excluded path prefixes. - */ -export const ExampleFilter: QuartzFilterPlugin> = ( - userOptions?: Partial, -) => { - const options = { ...defaultOptions, ...userOptions }; - return { - name: "ExampleFilter", - shouldPublish(_ctx: BuildCtx, [_tree, vfile]: ProcessedContent) { - const frontmatter = (vfile.data?.frontmatter ?? {}) as { - draft?: boolean | string; - tags?: string[]; - }; - const isDraft = frontmatter.draft === true || frontmatter.draft === "true"; - if (isDraft && !options.allowDrafts) { - return false; - } - - if (includesTag(frontmatter.tags, options.excludeTags)) { - return false; - } - - const filePath = typeof vfile.data?.filePath === "string" ? vfile.data.filePath : ""; - const normalizedPath = filePath.replace(/\\/g, "/"); - if (options.excludePathPrefixes.some((prefix) => normalizedPath.startsWith(prefix))) { - return false; - } - - return true; - }, - }; -}; diff --git a/src/i18n/index.ts b/src/i18n/index.ts deleted file mode 100644 index 8ad01cf..0000000 --- a/src/i18n/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import enUS from "./locales/en-US"; - -const locales: Record = { - "en-US": enUS, -}; - -export function i18n(locale: string) { - return locales[locale] || enUS; -} diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts deleted file mode 100644 index 001f5c2..0000000 --- a/src/i18n/locales/en-US.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default { - components: { - example: { - title: "Example", - }, - }, -}; diff --git a/src/index.ts b/src/index.ts index d5c31b9..7f812b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,3 @@ -export { ExampleTransformer } from "./transformer"; -export { ExampleFilter } from "./filter"; -export { ExampleEmitter } from "./emitter"; -export { default as ExampleComponent } from "./components/ExampleComponent"; - -export type { - ExampleTransformerOptions, - ExampleFilterOptions, - ExampleEmitterOptions, -} from "./types"; - -export type { ExampleComponentOptions } from "./components/ExampleComponent"; - -// Re-export shared types from @quartz-community/types -export type { - QuartzComponent, - QuartzComponentProps, - QuartzComponentConstructor, - StringResource, - QuartzTransformerPlugin, - QuartzFilterPlugin, - QuartzEmitterPlugin, - QuartzPageTypePlugin, - QuartzPageTypePluginInstance, - PageMatcher, - PageGenerator, - VirtualPage, -} from "@quartz-community/types"; +export { GitHubFlavoredMarkdown } from "./transformer"; +export type { GfmOptions } from "./transformer"; +export type { QuartzTransformerPlugin } from "@quartz-community/types"; diff --git a/src/transformer.ts b/src/transformer.ts index 6afeead..426d3eb 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -1,103 +1,78 @@ -import type { PluggableList, Plugin } from "unified"; -import type { Root as MdastRoot } from "mdast"; -import type { Root as HastRoot, Element } from "hast"; -import type { VFile } from "vfile"; import remarkGfm from "remark-gfm"; +import smartypants from "remark-smartypants"; +import type { QuartzTransformerPlugin } from "@quartz-community/types"; import rehypeSlug from "rehype-slug"; -import { findAndReplace } from "mdast-util-find-and-replace"; -import { visit } from "unist-util-visit"; -import type { QuartzTransformerPlugin, BuildCtx } from "@quartz-community/types"; -import type { ExampleTransformerOptions } from "./types"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; -const defaultOptions: ExampleTransformerOptions = { - highlightToken: "==", - headingClass: "example-plugin-heading", - enableGfm: true, - addHeadingSlugs: true, +export interface GfmOptions { + enableSmartyPants: boolean; + linkHeadings: boolean; +} + +const defaultOptions: GfmOptions = { + enableSmartyPants: true, + linkHeadings: true, }; -const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - -const remarkHighlightToken = (token: string): Plugin<[], MdastRoot> => { - const escapedToken = escapeRegExp(token); - const pattern = new RegExp(`${escapedToken}([^\n]+?)${escapedToken}`, "g"); - return () => (tree: MdastRoot, _file: VFile) => { - findAndReplace(tree, [ - [ - pattern, - (_match: string, value: string) => ({ - type: "strong", - children: [{ type: "text", value }], - }), - ], - ]); - }; -}; - -const rehypeHeadingClass = (className: string): Plugin<[], HastRoot> => { - return () => (tree: HastRoot, _file: VFile) => { - visit(tree, "element", (node: Element) => { - if (!/^h[1-6]$/.test(node.tagName)) { - return; - } - - const existing = node.properties?.className; - const classes: string[] = Array.isArray(existing) - ? existing.filter((value): value is string => typeof value === "string") - : typeof existing === "string" - ? [existing] - : []; - node.properties = { - ...node.properties, - className: [...classes, className], - }; - }); - }; -}; - -/** - * Example transformer showing remark/rehype usage and resource injection. - */ -export const ExampleTransformer: QuartzTransformerPlugin> = ( - userOptions?: Partial, -) => { - const options = { ...defaultOptions, ...userOptions }; +export const GitHubFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts }; return { - name: "ExampleTransformer", - textTransform(_ctx: BuildCtx, src: string) { - return src.endsWith("\n") ? src : `${src}\n`; + name: "GitHubFlavoredMarkdown", + markdownPlugins() { + return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]; }, - markdownPlugins(): PluggableList { - const plugins: PluggableList = [remarkHighlightToken(options.highlightToken)]; - if (options.enableGfm) { - plugins.unshift(remarkGfm); + htmlPlugins() { + if (opts.linkHeadings) { + return [ + rehypeSlug, + [ + rehypeAutolinkHeadings, + { + behavior: "append", + properties: { + role: "anchor", + ariaHidden: true, + tabIndex: -1, + "data-no-popover": true, + }, + content: { + type: "element", + tagName: "svg", + properties: { + width: 18, + height: 18, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", + }, + children: [], + }, + { + type: "element", + tagName: "path", + properties: { + d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", + }, + children: [], + }, + ], + }, + }, + ], + ]; + } else { + return []; } - return plugins; - }, - htmlPlugins(): PluggableList { - const plugins: PluggableList = [rehypeHeadingClass(options.headingClass)]; - if (options.addHeadingSlugs) { - plugins.unshift(rehypeSlug); - } - return plugins; - }, - externalResources() { - return { - css: [ - { - content: `.${options.headingClass} { letter-spacing: 0.02em; }`, - inline: true, - }, - ], - js: [ - { - contentType: "inline", - loadTime: "afterDOMReady", - script: "document.documentElement.dataset.exampleTransformer = 'true'", - }, - ], - additionalHead: [], - }; }, }; }; diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index f3fac5a..0000000 --- a/src/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -export type { - BuildCtx, - ChangeEvent, - CSSResource, - JSResource, - ProcessedContent, - QuartzEmitterPlugin, - QuartzEmitterPluginInstance, - QuartzFilterPlugin, - QuartzFilterPluginInstance, - QuartzPluginData, - QuartzTransformerPlugin, - QuartzTransformerPluginInstance, - StaticResources, - PageMatcher, - PageGenerator, - VirtualPage, - QuartzPageTypePlugin, - QuartzPageTypePluginInstance, -} from "@quartz-community/types"; - -export interface ExampleTransformerOptions { - /** Token used to highlight text, defaults to ==highlight== */ - highlightToken: string; - /** Add a CSS class to all headings in the rendered HTML. */ - headingClass: string; - /** Enable remark-gfm for tables/task lists. */ - enableGfm: boolean; - /** Enable adding slug IDs to headings. */ - addHeadingSlugs: boolean; -} - -export interface ExampleFilterOptions { - /** Allow pages marked draft: true to publish. */ - allowDrafts: boolean; - /** Exclude pages that contain any of these frontmatter tags. */ - excludeTags: string[]; - /** Exclude paths that start with any of these prefixes (relative to content root). */ - excludePathPrefixes: string[]; -} - -export interface ExampleEmitterOptions { - /** Filename to emit at the site root. */ - manifestSlug: string; - /** Whether to include the frontmatter block in the manifest. */ - includeFrontmatter: boolean; - /** Extra metadata to write at the top level of the manifest. */ - metadata: Record; - /** Optional hook to transform the emitted manifest JSON string. */ - transformManifest?: (json: string) => string; - /** Add a custom class to the emitted manifest