mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-19 10:54:06 -06:00
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>
This commit is contained in:
parent
6babcea029
commit
2d9da242dd
@ -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,
|
||||
|
||||
89
quartz/components/resources.ts
Normal file
89
quartz/components/resources.ts
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||
import type { ContentDetails } from "../../plugins/shared-types"
|
||||
import {
|
||||
SimulationNodeDatum,
|
||||
SimulationLinkDatum,
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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<FullSlug, ContentDetails>
|
||||
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
|
||||
|
||||
@ -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<Partial<SocialImageOptions>> = (userOpts) => {
|
||||
const fullOptions = { ...defaultOptions, ...userOpts }
|
||||
|
||||
|
||||
35
quartz/plugins/shared-types.ts
Normal file
35
quartz/plugins/shared-types.ts
Normal file
@ -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<FullSlug, ContentDetails>
|
||||
|
||||
/**
|
||||
* Name of the custom OG images emitter.
|
||||
* Used by Head component to check if custom OG images are enabled.
|
||||
*/
|
||||
export const CustomOgImagesEmitterName = "CustomOgImages"
|
||||
@ -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<Partial<Options>> = (userOpts) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
|
||||
@ -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<Partial<Options>> = (userOpts) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
|
||||
@ -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<Partial<Options>> = (opts) => {
|
||||
const engine = opts?.renderEngine ?? "katex"
|
||||
const macros = opts?.customMacros ?? {}
|
||||
|
||||
@ -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(
|
||||
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\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<Partial<Options>> = (userOpts) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
|
||||
@ -751,33 +762,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
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 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user