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 { googleFontHref, googleFontSubsetHref } from "../util/theme"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { unescapeHTML } from "../util/escape"
|
import { unescapeHTML } from "../util/escape"
|
||||||
import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
|
import { CustomOgImagesEmitterName } from "../plugins/shared-types"
|
||||||
export default (() => {
|
export default (() => {
|
||||||
const Head: QuartzComponent = ({
|
const Head: QuartzComponent = ({
|
||||||
cfg,
|
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 { FileTrieNode } from "../../util/fileTrie"
|
||||||
import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/shared-types"
|
||||||
|
|
||||||
type MaybeHTMLElement = HTMLElement | undefined
|
type MaybeHTMLElement = HTMLElement | undefined
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import type { ContentDetails } from "../../plugins/shared-types"
|
||||||
import {
|
import {
|
||||||
SimulationNodeDatum,
|
SimulationNodeDatum,
|
||||||
SimulationLinkDatum,
|
SimulationLinkDatum,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import FlexSearch, { DefaultDocumentSearchResults } from "flexsearch"
|
import FlexSearch, { DefaultDocumentSearchResults } from "flexsearch"
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/shared-types"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,16 @@
|
|||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { GlobalConfiguration } from "../../cfg"
|
import { GlobalConfiguration } from "../../cfg"
|
||||||
import { getDate } from "../../components/Date"
|
import { getDate } from "../../components/Date"
|
||||||
import { FilePath, FullSlug, SimpleSlug } from "../../util/path"
|
import { FullSlug, SimpleSlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { PluginUtilities } from "../plugin-context"
|
import { PluginUtilities } from "../plugin-context"
|
||||||
|
import { ContentDetails, ContentIndexMap } from "../shared-types"
|
||||||
|
|
||||||
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
// Re-export for backward compatibility
|
||||||
export type ContentDetails = {
|
export type { ContentDetails, ContentIndexMap }
|
||||||
slug: FullSlug
|
|
||||||
filePath: FilePath
|
|
||||||
title: string
|
|
||||||
links: SimpleSlug[]
|
|
||||||
tags: string[]
|
|
||||||
content: string
|
|
||||||
richContent?: string
|
|
||||||
date?: Date
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
enableSiteMap: boolean
|
enableSiteMap: boolean
|
||||||
|
|||||||
@ -12,6 +12,10 @@ import { QuartzPluginData } from "../vfile"
|
|||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import { styleText } from "util"
|
import { styleText } from "util"
|
||||||
import { PluginUtilities } from "../plugin-context"
|
import { PluginUtilities } from "../plugin-context"
|
||||||
|
import { CustomOgImagesEmitterName } from "../shared-types"
|
||||||
|
|
||||||
|
// Re-export for backward compatibility
|
||||||
|
export { CustomOgImagesEmitterName }
|
||||||
|
|
||||||
const defaultOptions: SocialImageOptions = {
|
const defaultOptions: SocialImageOptions = {
|
||||||
colorScheme: "lightMode",
|
colorScheme: "lightMode",
|
||||||
@ -105,7 +109,6 @@ async function processOgImage(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomOgImagesEmitterName = "CustomOgImages"
|
|
||||||
export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> = (userOpts) => {
|
export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> = (userOpts) => {
|
||||||
const fullOptions = { ...defaultOptions, ...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",
|
csl: "apa",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @plugin Citations
|
||||||
|
* @category Transformer
|
||||||
|
*
|
||||||
|
* @reads None
|
||||||
|
* @writes vfile.data.citations
|
||||||
|
*
|
||||||
|
* @dependencies None
|
||||||
|
*/
|
||||||
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -38,6 +38,17 @@ function coerceDate(fp: string, d: any): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MaybeDate = undefined | string | number
|
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) => {
|
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -21,6 +21,17 @@ interface MacroType {
|
|||||||
[key: string]: string
|
[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) => {
|
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
|
||||||
const engine = opts?.renderEngine ?? "katex"
|
const engine = opts?.renderEngine ?? "katex"
|
||||||
const macros = opts?.customMacros ?? {}
|
const macros = opts?.customMacros ?? {}
|
||||||
|
|||||||
@ -14,13 +14,7 @@ import rehypeRaw from "rehype-raw"
|
|||||||
import { SKIP, visit } from "unist-util-visit"
|
import { SKIP, visit } from "unist-util-visit"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { JSResource, CSSResource } from "../../util/resources"
|
import { JSResource, CSSResource } from "../../util/resources"
|
||||||
// @ts-ignore
|
import { getComponentJS, getComponentCSS } from "../../components/resources"
|
||||||
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 { FilePath } from "../../util/path"
|
import { FilePath } from "../../util/path"
|
||||||
import { toHast } from "mdast-util-to-hast"
|
import { toHast } from "mdast-util-to-hast"
|
||||||
import { toHtml } from "hast-util-to-html"
|
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+))?)?$/,
|
/^(?<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) => {
|
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
|
|
||||||
@ -751,33 +762,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
const css: CSSResource[] = []
|
const css: CSSResource[] = []
|
||||||
|
|
||||||
if (opts.enableCheckbox) {
|
if (opts.enableCheckbox) {
|
||||||
js.push({
|
js.push(getComponentJS("checkbox"))
|
||||||
script: checkboxScript,
|
|
||||||
loadTime: "afterDOMReady",
|
|
||||||
contentType: "inline",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
js.push({
|
js.push(getComponentJS("callout"))
|
||||||
script: calloutScript,
|
|
||||||
loadTime: "afterDOMReady",
|
|
||||||
contentType: "inline",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.mermaid) {
|
if (opts.mermaid) {
|
||||||
js.push({
|
js.push(getComponentJS("mermaid"))
|
||||||
script: mermaidScript,
|
|
||||||
loadTime: "afterDOMReady",
|
|
||||||
contentType: "inline",
|
|
||||||
moduleType: "module",
|
|
||||||
})
|
|
||||||
|
|
||||||
css.push({
|
const mermaidCSSRes = getComponentCSS("mermaid")
|
||||||
content: mermaidStyle,
|
if (mermaidCSSRes) {
|
||||||
inline: true,
|
css.push(mermaidCSSRes)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { js, css }
|
return { js, css }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user