From 2d9da242ddc3ab9a3cb0ed08a77961a5b11fd88a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:30:06 +0100 Subject: [PATCH] refactor: decouple plugins from direct utility and component imports (#5) * Initial plan * Break component-emitter coupling by introducing shared-types module Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Decouple transformer from component scripts via resource registry Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Document plugin data dependencies with @plugin, @reads, @writes annotations Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Address code review feedback: improve docs and exhaustiveness checking Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Add exhaustiveness checking with unreachable assertions Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Fix getComponentJS return type and remove unnecessary null checks Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> --- quartz/components/Head.tsx | 2 +- quartz/components/resources.ts | 89 ++++++++++++++++++++ quartz/components/scripts/explorer.inline.ts | 2 +- quartz/components/scripts/graph.inline.ts | 2 +- quartz/components/scripts/search.inline.ts | 2 +- quartz/plugins/emitters/contentIndex.tsx | 17 +--- quartz/plugins/emitters/ogImage.tsx | 5 +- quartz/plugins/shared-types.ts | 35 ++++++++ quartz/plugins/transformers/citations.ts | 9 ++ quartz/plugins/transformers/lastmod.ts | 11 +++ quartz/plugins/transformers/latex.ts | 11 +++ quartz/plugins/transformers/ofm.ts | 52 ++++++------ 12 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 quartz/components/resources.ts create mode 100644 quartz/plugins/shared-types.ts diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 23183ca8c..2746687f3 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -4,7 +4,7 @@ import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/re import { googleFontHref, googleFontSubsetHref } from "../util/theme" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { unescapeHTML } from "../util/escape" -import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage" +import { CustomOgImagesEmitterName } from "../plugins/shared-types" export default (() => { const Head: QuartzComponent = ({ cfg, diff --git a/quartz/components/resources.ts b/quartz/components/resources.ts new file mode 100644 index 000000000..bee6c2d31 --- /dev/null +++ b/quartz/components/resources.ts @@ -0,0 +1,89 @@ +/** + * Component Resources Registry + * + * This module provides a centralized registry for component scripts and styles. + * Plugins can request component resources without directly importing from components/scripts/, + * which helps decouple plugins from component implementations. + * + * This follows the decoupling strategy outlined in DESIGN_DOCUMENT_DECOUPLING.md section 3.3. + */ + +import { JSResource, CSSResource } from "../util/resources" + +// Import all component scripts +// @ts-ignore +import calloutScript from "./scripts/callout.inline" +// @ts-ignore +import checkboxScript from "./scripts/checkbox.inline" +// @ts-ignore +import mermaidScript from "./scripts/mermaid.inline" +import mermaidStyle from "./styles/mermaid.inline.scss" + +/** + * Available component resource types that can be requested by plugins + */ +export type ComponentResourceType = "callout" | "checkbox" | "mermaid" + +/** + * Get JavaScript resources for a specific component + */ +export function getComponentJS(type: ComponentResourceType): JSResource { + switch (type) { + case "callout": + return { + script: calloutScript, + loadTime: "afterDOMReady", + contentType: "inline", + } + case "checkbox": + return { + script: checkboxScript, + loadTime: "afterDOMReady", + contentType: "inline", + } + case "mermaid": + return { + script: mermaidScript, + loadTime: "afterDOMReady", + contentType: "inline", + moduleType: "module", + } + } + const _exhaustive: never = type + throw new Error(`Unhandled component type: ${_exhaustive}`) +} + +/** + * Get CSS resources for a specific component + */ +export function getComponentCSS(type: ComponentResourceType): CSSResource | null { + switch (type) { + case "callout": + case "checkbox": + return null + case "mermaid": + return { + content: mermaidStyle, + inline: true, + } + } + const _exhaustive: never = type + throw new Error(`Unhandled component type: ${_exhaustive}`) +} + +/** + * Get both JS and CSS resources for a component + * + * Note: This function is provided for convenience and future extensibility. + * Currently not used in the codebase as plugins call getComponentJS and + * getComponentCSS separately to handle conditional resource loading. + */ +export function getComponentResources(type: ComponentResourceType): { + js: JSResource + css: CSSResource | null +} { + return { + js: getComponentJS(type), + css: getComponentCSS(type), + } +} diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index 9c8341169..21a242f66 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -1,6 +1,6 @@ import { FileTrieNode } from "../../util/fileTrie" import { FullSlug, resolveRelative, simplifySlug } from "../../util/path" -import { ContentDetails } from "../../plugins/emitters/contentIndex" +import { ContentDetails } from "../../plugins/shared-types" type MaybeHTMLElement = HTMLElement | undefined diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index a669b0547..8651f20cc 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -1,4 +1,4 @@ -import type { ContentDetails } from "../../plugins/emitters/contentIndex" +import type { ContentDetails } from "../../plugins/shared-types" import { SimulationNodeDatum, SimulationLinkDatum, diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 6a84a50e0..ef3431ce6 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -1,5 +1,5 @@ import FlexSearch, { DefaultDocumentSearchResults } from "flexsearch" -import { ContentDetails } from "../../plugins/emitters/contentIndex" +import { ContentDetails } from "../../plugins/shared-types" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path" diff --git a/quartz/plugins/emitters/contentIndex.tsx b/quartz/plugins/emitters/contentIndex.tsx index 503cec6b8..ce8e6c00d 100644 --- a/quartz/plugins/emitters/contentIndex.tsx +++ b/quartz/plugins/emitters/contentIndex.tsx @@ -1,25 +1,16 @@ import { Root } from "hast" import { GlobalConfiguration } from "../../cfg" import { getDate } from "../../components/Date" -import { FilePath, FullSlug, SimpleSlug } from "../../util/path" +import { FullSlug, SimpleSlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import { toHtml } from "hast-util-to-html" import { write } from "./helpers" import { i18n } from "../../i18n" import { PluginUtilities } from "../plugin-context" +import { ContentDetails, ContentIndexMap } from "../shared-types" -export type ContentIndexMap = Map -export type ContentDetails = { - slug: FullSlug - filePath: FilePath - title: string - links: SimpleSlug[] - tags: string[] - content: string - richContent?: string - date?: Date - description?: string -} +// Re-export for backward compatibility +export type { ContentDetails, ContentIndexMap } interface Options { enableSiteMap: boolean diff --git a/quartz/plugins/emitters/ogImage.tsx b/quartz/plugins/emitters/ogImage.tsx index c666c1175..5f42b6909 100644 --- a/quartz/plugins/emitters/ogImage.tsx +++ b/quartz/plugins/emitters/ogImage.tsx @@ -12,6 +12,10 @@ import { QuartzPluginData } from "../vfile" import fs from "node:fs/promises" import { styleText } from "util" import { PluginUtilities } from "../plugin-context" +import { CustomOgImagesEmitterName } from "../shared-types" + +// Re-export for backward compatibility +export { CustomOgImagesEmitterName } const defaultOptions: SocialImageOptions = { colorScheme: "lightMode", @@ -105,7 +109,6 @@ async function processOgImage( }) } -export const CustomOgImagesEmitterName = "CustomOgImages" export const CustomOgImages: QuartzEmitterPlugin> = (userOpts) => { const fullOptions = { ...defaultOptions, ...userOpts } diff --git a/quartz/plugins/shared-types.ts b/quartz/plugins/shared-types.ts new file mode 100644 index 000000000..6a1cd6dbb --- /dev/null +++ b/quartz/plugins/shared-types.ts @@ -0,0 +1,35 @@ +/** + * Shared type definitions used across plugins and components. + * + * This module breaks coupling between components and emitters by providing + * common type definitions that both can import without creating circular dependencies. + */ + +import { FilePath, FullSlug, SimpleSlug } from "../util/path" + +/** + * Content index entry representing metadata about a single content file. + * + * This type is used by: + * - ContentIndex emitter to generate the content index + * - Search, Explorer, and Graph components to display and navigate content + */ +export type ContentDetails = { + slug: FullSlug + filePath: FilePath + title: string + links: SimpleSlug[] + tags: string[] + content: string + richContent?: string + date?: Date + description?: string +} + +export type ContentIndexMap = Map + +/** + * Name of the custom OG images emitter. + * Used by Head component to check if custom OG images are enabled. + */ +export const CustomOgImagesEmitterName = "CustomOgImages" diff --git a/quartz/plugins/transformers/citations.ts b/quartz/plugins/transformers/citations.ts index dcac41b2e..5744d3180 100644 --- a/quartz/plugins/transformers/citations.ts +++ b/quartz/plugins/transformers/citations.ts @@ -17,6 +17,15 @@ const defaultOptions: Options = { csl: "apa", } +/** + * @plugin Citations + * @category Transformer + * + * @reads None + * @writes vfile.data.citations + * + * @dependencies None + */ export const Citations: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 32a89cc23..3d5f8f335 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -38,6 +38,17 @@ function coerceDate(fp: string, d: any): Date { } type MaybeDate = undefined | string | number +/** + * @plugin CreatedModifiedDate + * @category Transformer + * + * @reads vfile.data.frontmatter.created + * @reads vfile.data.frontmatter.modified + * @reads vfile.data.frontmatter.published + * @writes vfile.data.dates + * + * @dependencies None + */ export const CreatedModifiedDate: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts index 40939d5e9..8087baaa0 100644 --- a/quartz/plugins/transformers/latex.ts +++ b/quartz/plugins/transformers/latex.ts @@ -21,6 +21,17 @@ interface MacroType { [key: string]: string } +/** + * @plugin Latex + * @category Transformer + * + * @reads None + * @writes None (adds HTML but no vfile.data) + * + * @dependencies None + * + * @description Transforms markdown math notation (using remark-math) and renders LaTeX math expressions using KaTeX, MathJax, or Typst engines. Provides external CSS/JS resources for the selected rendering engine. + */ export const Latex: QuartzTransformerPlugin> = (opts) => { const engine = opts?.renderEngine ?? "katex" const macros = opts?.customMacros ?? {} diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 95f074780..d27a271ad 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -14,13 +14,7 @@ import rehypeRaw from "rehype-raw" import { SKIP, visit } from "unist-util-visit" import path from "path" import { JSResource, CSSResource } from "../../util/resources" -// @ts-ignore -import calloutScript from "../../components/scripts/callout.inline" -// @ts-ignore -import checkboxScript from "../../components/scripts/checkbox.inline" -// @ts-ignore -import mermaidScript from "../../components/scripts/mermaid.inline" -import mermaidStyle from "../../components/styles/mermaid.inline.scss" +import { getComponentJS, getComponentCSS } from "../../components/resources" import { FilePath } from "../../util/path" import { toHast } from "mdast-util-to-hast" import { toHtml } from "hast-util-to-html" @@ -147,6 +141,23 @@ const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, ) +/** + * @plugin ObsidianFlavoredMarkdown + * @category Transformer + * + * @reads vfile.data.slug + * @reads vfile.data.frontmatter (for wikilink processing and tag extraction) + * @writes vfile.data.frontmatter.tags (when parseTags is enabled) + * @writes vfile.data.blocks + * @writes vfile.data.htmlAst + * @writes vfile.data.hasMermaidDiagram + * + * @dependencies None + * + * @description Processes Obsidian-flavored markdown including wikilinks, callouts, + * highlights, comments, mermaid diagrams, checkboxes, and tables. Conditionally + * registers component resources (callout, checkbox, mermaid) only if the corresponding options are enabled. + */ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } @@ -751,33 +762,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> const css: CSSResource[] = [] if (opts.enableCheckbox) { - js.push({ - script: checkboxScript, - loadTime: "afterDOMReady", - contentType: "inline", - }) + js.push(getComponentJS("checkbox")) } if (opts.callouts) { - js.push({ - script: calloutScript, - loadTime: "afterDOMReady", - contentType: "inline", - }) + js.push(getComponentJS("callout")) } if (opts.mermaid) { - js.push({ - script: mermaidScript, - loadTime: "afterDOMReady", - contentType: "inline", - moduleType: "module", - }) + js.push(getComponentJS("mermaid")) - css.push({ - content: mermaidStyle, - inline: true, - }) + const mermaidCSSRes = getComponentCSS("mermaid") + if (mermaidCSSRes) { + css.push(mermaidCSSRes) + } } return { js, css }