mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
* Initial plan * Extend PluginUtilities with missing path and escape utilities Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Migrate transformers to use ctx.utils instead of direct imports Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Migrate emitters to use ctx.utils - part 1 (helpers, aliases, assets, contentIndex, contentPage, tagPage, static, favicon) Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Complete emitter migration to ctx.utils - all emitters decoupled Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Add comment explaining double-unescaping in test helper mock This mirrors the production implementation in util/escape.ts which has the same pattern. The CodeQL alert is a known limitation of simple HTML unescaping that exists in the production code as well. Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Fix type safety: change join() return type to string and simplify casts - Changed PluginUtilities.path.join() return type from FilePath to string to allow proper single-cast at call sites - Removed unsafe double-casts (as unknown as FullSlug) in favor of single casts (as FullSlug) - Fixed ogImage.tsx to use consistent destructuring pattern (ctx.cfg.configuration instead of cfg.configuration) - Updated implementation in plugin-context.ts and test-helpers.ts This addresses all code review feedback about type safety issues. Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * Fix test mock implementations to match production code behavior - Fixed getAllSegmentPrefixes to return cumulative prefixes (e.g., ["a", "a/b", "a/b/c"]) - Fixed isRelativeURL to include all three validation conditions - Fixed stripSlashes to use substring(1) instead of replace(/^\/+/) - Fixed slugTag to properly handle hierarchical tags with sluggify logic - Fixed split to handle PDF files and anchor normalization - Fixed HTML entity ' to ' for consistency - Changed QUARTZ imports to use utils.path.QUARTZ for consistency - Fixed favicon.ts to pass full ctx instead of reconstructing partial object All mocks now accurately reflect production code behavior for reliable testing. 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>
202 lines
6.2 KiB
TypeScript
202 lines
6.2 KiB
TypeScript
import { VFile } from "vfile"
|
|
import { QuartzVFileData } from "./vfile-schema"
|
|
import { FullSlug, FilePath, SimpleSlug, RelativeURL, TransformOptions } from "../util/path"
|
|
import { QuartzConfig } from "../cfg"
|
|
import { Argv } from "../util/ctx"
|
|
import { CSSResource } from "../util/resources"
|
|
import { PluginContext, PluginUtilities } from "./plugin-context"
|
|
|
|
/**
|
|
* Create a mock plugin context for testing
|
|
*/
|
|
export function createMockPluginContext(overrides?: Partial<PluginContext>): PluginContext {
|
|
return {
|
|
cfg: createMockConfig(),
|
|
buildId: "test-build",
|
|
argv: createMockArgv(),
|
|
allSlugs: [],
|
|
allFiles: [],
|
|
incremental: false,
|
|
utils: createMockUtilities(),
|
|
trie: undefined,
|
|
...overrides,
|
|
} as PluginContext
|
|
}
|
|
|
|
/**
|
|
* Create a mock VFile for testing
|
|
*/
|
|
export function createMockVFile(data?: Partial<QuartzVFileData>): VFile {
|
|
const file = new VFile("")
|
|
file.data = {
|
|
slug: "test" as FullSlug,
|
|
filePath: "test.md" as FilePath,
|
|
relativePath: "test.md" as FilePath,
|
|
...data,
|
|
} as Partial<QuartzVFileData>
|
|
return file
|
|
}
|
|
|
|
function createMockConfig(): QuartzConfig {
|
|
return {
|
|
configuration: {
|
|
pageTitle: "Test Site",
|
|
baseUrl: "test.com",
|
|
locale: "en-US",
|
|
enableSPA: true,
|
|
enablePopovers: true,
|
|
analytics: null,
|
|
ignorePatterns: [],
|
|
defaultDateType: "created",
|
|
theme: {
|
|
typography: {
|
|
header: "Schibsted Grotesk",
|
|
body: "Source Sans Pro",
|
|
code: "IBM Plex Mono",
|
|
},
|
|
colors: {
|
|
lightMode: {
|
|
light: "#faf8f8",
|
|
lightgray: "#e5e5e5",
|
|
gray: "#b8b8b8",
|
|
darkgray: "#4e4e4e",
|
|
dark: "#2b2b2b",
|
|
secondary: "#284b63",
|
|
tertiary: "#84a59d",
|
|
highlight: "rgba(143, 159, 169, 0.15)",
|
|
textHighlight: "#fff23688",
|
|
},
|
|
darkMode: {
|
|
light: "#161618",
|
|
lightgray: "#393639",
|
|
gray: "#646464",
|
|
darkgray: "#d4d4d4",
|
|
dark: "#ebebec",
|
|
secondary: "#7b97aa",
|
|
tertiary: "#84a59d",
|
|
highlight: "rgba(143, 159, 169, 0.15)",
|
|
textHighlight: "#b3aa0288",
|
|
},
|
|
},
|
|
fontOrigin: "googleFonts",
|
|
cdnCaching: true,
|
|
},
|
|
},
|
|
plugins: {
|
|
transformers: [],
|
|
filters: [],
|
|
emitters: [],
|
|
},
|
|
} as QuartzConfig
|
|
}
|
|
|
|
function createMockArgv(): Argv {
|
|
return {
|
|
directory: "content",
|
|
verbose: false,
|
|
output: "public",
|
|
serve: false,
|
|
watch: false,
|
|
port: 8080,
|
|
wsPort: 3001,
|
|
}
|
|
}
|
|
|
|
function createMockUtilities(): PluginUtilities {
|
|
return {
|
|
path: {
|
|
slugify: (path: FilePath) => path as unknown as FullSlug,
|
|
simplify: (slug: FullSlug) => slug as unknown as SimpleSlug,
|
|
transform: (_from: FullSlug, to: string, _opts: TransformOptions) => to as RelativeURL,
|
|
toRoot: (_slug: FullSlug) => "/" as RelativeURL,
|
|
split: (slug: string) => {
|
|
// Mock implementation of splitAnchor with special PDF handling
|
|
let [fp, anchor] = slug.split("#", 2)
|
|
if (fp.endsWith(".pdf")) {
|
|
return [fp, anchor === undefined ? "" : `#${anchor}`]
|
|
}
|
|
// Simplified anchor sluggification (production uses github-slugger)
|
|
anchor = anchor === undefined ? "" : "#" + anchor.toLowerCase().replace(/\s+/g, "-")
|
|
return [fp, anchor]
|
|
},
|
|
join: (...segments: string[]) => segments.join("/"),
|
|
getAllSegmentPrefixes: (tags: string) => {
|
|
const segments = tags.split("/")
|
|
const results: string[] = []
|
|
for (let i = 0; i < segments.length; i++) {
|
|
results.push(segments.slice(0, i + 1).join("/"))
|
|
}
|
|
return results
|
|
},
|
|
getFileExtension: (s: string) => s.match(/\.[A-Za-z0-9]+$/)?.[0],
|
|
isAbsoluteURL: (s: string) => {
|
|
try {
|
|
new URL(s)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
},
|
|
isRelativeURL: (s: string) => {
|
|
// 1. Starts with '.' or '..'
|
|
if (!/^\.{1,2}/.test(s)) return false
|
|
// 2. Does not end with 'index'
|
|
if (s.endsWith("index")) return false
|
|
// 3. File extension is not .md or .html
|
|
const ext = s.match(/\.[A-Za-z0-9]+$/)?.[0]?.toLowerCase()
|
|
if (ext === ".md" || ext === ".html") return false
|
|
return true
|
|
},
|
|
resolveRelative: (_current: FullSlug, target: FullSlug | SimpleSlug) =>
|
|
target as unknown as RelativeURL,
|
|
slugTag: (tag: string) => {
|
|
// Mock sluggify function similar to production
|
|
const sluggify = (segment: string) =>
|
|
segment
|
|
.toLowerCase()
|
|
.replace(/[&%?#]/g, "") // remove special chars
|
|
.replace(/\s+/g, "-") // replace spaces with dashes
|
|
.replace(/-+/g, "-") // collapse multiple dashes
|
|
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
|
|
return tag.split("/").map(sluggify).join("/")
|
|
},
|
|
stripSlashes: (s: string, onlyStripPrefix?: boolean) => {
|
|
if (s.startsWith("/")) {
|
|
s = s.substring(1)
|
|
}
|
|
if (!onlyStripPrefix && s.endsWith("/")) {
|
|
s = s.slice(0, -1)
|
|
}
|
|
return s
|
|
},
|
|
QUARTZ: "quartz",
|
|
},
|
|
resources: {
|
|
createExternalJS: (src: string, loadTime?: "beforeDOMReady" | "afterDOMReady") => ({
|
|
src,
|
|
contentType: "external" as const,
|
|
loadTime: loadTime ?? "afterDOMReady",
|
|
}),
|
|
createInlineJS: (script: string, loadTime?: "beforeDOMReady" | "afterDOMReady") => ({
|
|
script,
|
|
contentType: "inline" as const,
|
|
loadTime: loadTime ?? "afterDOMReady",
|
|
}),
|
|
createCSS: (resource: CSSResource) => resource,
|
|
},
|
|
escape: {
|
|
html: (text: string) => text.replace(/[&<>"']/g, (m) => `&#${m.charCodeAt(0)};`),
|
|
// Note: This mock implementation mirrors the production code in util/escape.ts
|
|
// which has a known limitation of potential double-unescaping.
|
|
// This is acceptable as it matches the real implementation for testing purposes.
|
|
unescape: (html: string) =>
|
|
html
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'"),
|
|
},
|
|
}
|
|
}
|