mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 06:24:22 +02:00
checkpoint
This commit is contained in:
parent
e26658f4ed
commit
f528d6139e
@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Authoring Content
|
title: Authoring Content
|
||||||
|
aliases:
|
||||||
|
- test/author 1
|
||||||
---
|
---
|
||||||
|
|
||||||
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initialized. Any Markdown in this folder will get processed by Quartz.
|
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initialized. Any Markdown in this folder will get processed by Quartz.
|
||||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.4.1",
|
"version": "4.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.4.1",
|
"version": "4.5.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.10.0",
|
"@clack/prompts": "^0.10.0",
|
||||||
@ -14,6 +14,7 @@
|
|||||||
"@myriaddreamin/rehype-typst": "^0.5.4",
|
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||||
"@napi-rs/simple-git": "0.1.19",
|
"@napi-rs/simple-git": "0.1.19",
|
||||||
"@tweenjs/tween.js": "^25.0.0",
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
|
"ansi-truncate": "^1.2.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@ -2032,6 +2033,15 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-truncate": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-truncate/-/ansi-truncate-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-/SLVrxNIP8o8iRHjdK3K9s2hDqdvb86NEjZOAB6ecWFsOo+9obaby97prnvAPn6j7ExXCpbvtlJFYPkkspg4BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-string-truncated-width": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@ -3058,6 +3068,12 @@
|
|||||||
"node": ">=8.6.0"
|
"node": ">=8.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-string-truncated-width": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"description": "🌱 publish your digital garden and notes as a website",
|
"description": "🌱 publish your digital garden and notes as a website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "4.4.1",
|
"version": "4.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"@myriaddreamin/rehype-typst": "^0.5.4",
|
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||||
"@napi-rs/simple-git": "0.1.19",
|
"@napi-rs/simple-git": "0.1.19",
|
||||||
"@tweenjs/tween.js": "^25.0.0",
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
|
"ansi-truncate": "^1.2.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
@ -57,7 +57,7 @@ const config: QuartzConfig = {
|
|||||||
transformers: [
|
transformers: [
|
||||||
Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ["frontmatter", "filesystem"],
|
priority: ["git", "frontmatter", "filesystem"],
|
||||||
}),
|
}),
|
||||||
Plugin.SyntaxHighlighting({
|
Plugin.SyntaxHighlighting({
|
||||||
theme: {
|
theme: {
|
||||||
|
391
quartz/build.ts
391
quartz/build.ts
@ -17,34 +17,38 @@ import { glob, toPosixPath } from "./util/glob"
|
|||||||
import { trace } from "./util/trace"
|
import { trace } from "./util/trace"
|
||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
import DepGraph from "./depgraph"
|
|
||||||
import { getStaticResourcesFromPlugins } from "./plugins"
|
import { getStaticResourcesFromPlugins } from "./plugins"
|
||||||
import { randomIdNonSecure } from "./util/random"
|
import { randomIdNonSecure } from "./util/random"
|
||||||
|
import { ChangeEvent } from "./plugins/types"
|
||||||
|
|
||||||
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
type ContentMap = Map<
|
||||||
|
FilePath,
|
||||||
|
| {
|
||||||
|
type: "markdown"
|
||||||
|
content: ProcessedContent
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "other"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
type BuildData = {
|
type BuildData = {
|
||||||
ctx: BuildCtx
|
ctx: BuildCtx
|
||||||
ignored: GlobbyFilterFunction
|
ignored: GlobbyFilterFunction
|
||||||
mut: Mutex
|
mut: Mutex
|
||||||
initialSlugs: FullSlug[]
|
contentMap: ContentMap
|
||||||
// TODO merge contentMap and trackedAssets
|
changesSinceLastBuild: Record<FilePath, ChangeEvent["type"]>
|
||||||
contentMap: Map<FilePath, ProcessedContent>
|
|
||||||
trackedAssets: Set<FilePath>
|
|
||||||
toRebuild: Set<FilePath>
|
|
||||||
toRemove: Set<FilePath>
|
|
||||||
lastBuildMs: number
|
lastBuildMs: number
|
||||||
dependencies: Dependencies
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileEvent = "add" | "change" | "delete"
|
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
buildId: randomIdNonSecure(),
|
buildId: randomIdNonSecure(),
|
||||||
argv,
|
argv,
|
||||||
cfg,
|
cfg,
|
||||||
allSlugs: [],
|
allSlugs: [],
|
||||||
|
allFiles: [],
|
||||||
|
incremental: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
@ -67,64 +71,58 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
|
|
||||||
perf.addEvent("glob")
|
perf.addEvent("glob")
|
||||||
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
||||||
const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort()
|
const markdownPaths = allFiles.filter((fp) => fp.endsWith(".md")).sort()
|
||||||
console.log(
|
console.log(
|
||||||
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
`Found ${markdownPaths.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const filePaths = fps.map((fp) => joinSegments(argv.directory, fp) as FilePath)
|
const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath)
|
||||||
|
ctx.allFiles = allFiles
|
||||||
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||||
|
|
||||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
|
|
||||||
|
|
||||||
// Only build dependency graphs if we're doing a fast rebuild
|
|
||||||
if (argv.fastRebuild) {
|
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
dependencies[emitter.name] =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done processing ${markdownPaths.length} files in ${perf.timeSince()}`))
|
||||||
release()
|
release()
|
||||||
|
|
||||||
if (argv.serve) {
|
if (argv.watch) {
|
||||||
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
|
ctx.incremental = true
|
||||||
|
return startWatching(ctx, mut, parsedFiles, clientRefresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup watcher for rebuilds
|
// setup watcher for rebuilds
|
||||||
async function startServing(
|
async function startWatching(
|
||||||
ctx: BuildCtx,
|
ctx: BuildCtx,
|
||||||
mut: Mutex,
|
mut: Mutex,
|
||||||
initialContent: ProcessedContent[],
|
initialContent: ProcessedContent[],
|
||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
dependencies: Dependencies, // emitter name: dep graph
|
|
||||||
) {
|
) {
|
||||||
const { argv } = ctx
|
const { argv, allFiles } = ctx
|
||||||
|
|
||||||
|
const contentMap: ContentMap = new Map()
|
||||||
|
for (const filePath of allFiles) {
|
||||||
|
contentMap.set(filePath, {
|
||||||
|
type: "other",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// cache file parse results
|
|
||||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
|
||||||
for (const content of initialContent) {
|
for (const content of initialContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
contentMap.set(vfile.data.relativePath!, {
|
||||||
|
type: "markdown",
|
||||||
|
content,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildData: BuildData = {
|
const buildData: BuildData = {
|
||||||
ctx,
|
ctx,
|
||||||
mut,
|
mut,
|
||||||
dependencies,
|
|
||||||
contentMap,
|
contentMap,
|
||||||
ignored: await isGitIgnored(),
|
ignored: await isGitIgnored(),
|
||||||
initialSlugs: ctx.allSlugs,
|
changesSinceLastBuild: {},
|
||||||
toRebuild: new Set<FilePath>(),
|
|
||||||
toRemove: new Set<FilePath>(),
|
|
||||||
trackedAssets: new Set<FilePath>(),
|
|
||||||
lastBuildMs: 0,
|
lastBuildMs: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,31 +132,33 @@ async function startServing(
|
|||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
const changes: ChangeEvent[] = []
|
||||||
watcher
|
watcher
|
||||||
.on("add", (fp) => buildFromEntry(fp as string, "add", clientRefresh, buildData))
|
.on("add", (fp) => {
|
||||||
.on("change", (fp) => buildFromEntry(fp as string, "change", clientRefresh, buildData))
|
if (buildData.ignored(fp)) return
|
||||||
.on("unlink", (fp) => buildFromEntry(fp as string, "delete", clientRefresh, buildData))
|
changes.push({ path: fp as FilePath, type: "add" })
|
||||||
|
rebuild(changes, clientRefresh, buildData)
|
||||||
|
})
|
||||||
|
.on("change", (fp) => {
|
||||||
|
if (buildData.ignored(fp)) return
|
||||||
|
changes.push({ path: fp as FilePath, type: "change" })
|
||||||
|
rebuild(changes, clientRefresh, buildData)
|
||||||
|
})
|
||||||
|
.on("unlink", (fp) => {
|
||||||
|
if (buildData.ignored(fp)) return
|
||||||
|
changes.push({ path: fp as FilePath, type: "delete" })
|
||||||
|
rebuild(changes, clientRefresh, buildData)
|
||||||
|
})
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await watcher.close()
|
await watcher.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function partialRebuildFromEntrypoint(
|
async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildData: BuildData) {
|
||||||
filepath: string,
|
const { ctx, contentMap, mut, changesSinceLastBuild } = buildData
|
||||||
action: FileEvent,
|
|
||||||
clientRefresh: () => void,
|
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
|
||||||
) {
|
|
||||||
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
|
|
||||||
const { argv, cfg } = ctx
|
const { argv, cfg } = ctx
|
||||||
|
|
||||||
// don't do anything for gitignored files
|
|
||||||
if (ignored(filepath)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildId = randomIdNonSecure()
|
const buildId = randomIdNonSecure()
|
||||||
ctx.buildId = buildId
|
ctx.buildId = buildId
|
||||||
buildData.lastBuildMs = new Date().getTime()
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
@ -171,126 +171,81 @@ async function partialRebuildFromEntrypoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
|
perf.addEvent("rebuild")
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
console.log(chalk.yellow("Detected change, rebuilding..."))
|
||||||
|
console.log(changes)
|
||||||
|
|
||||||
// UPDATE DEP GRAPH
|
// update changesSinceLastBuild
|
||||||
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
|
for (const change of changes) {
|
||||||
|
changesSinceLastBuild[change.path] = change.type
|
||||||
|
}
|
||||||
|
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||||
let processedFiles: ProcessedContent[] = []
|
const processedFiles = await Promise.all(
|
||||||
|
Object.entries(changesSinceLastBuild)
|
||||||
|
.filter(([fp, type]) => type !== "delete" && path.extname(fp) === ".md")
|
||||||
|
.map(async ([fp, _type]) => {
|
||||||
|
const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
|
||||||
|
const parsed = await parseMarkdown(ctx, [fullPath])
|
||||||
|
parsed.forEach((content) =>
|
||||||
|
contentMap.set(content[1].data.relativePath!, {
|
||||||
|
type: "markdown",
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return parsed
|
||||||
|
}),
|
||||||
|
).then((results) => results.flat())
|
||||||
|
|
||||||
switch (action) {
|
// update state using changesSinceLastBuild
|
||||||
case "add":
|
// we do this weird play of add => compute change events => remove
|
||||||
// add to cache when new file is added
|
// so that partialEmitters can do appropriate cleanup based on the content of deleted files
|
||||||
processedFiles = await parseMarkdown(ctx, [fp])
|
for (const [file, change] of Object.entries(changesSinceLastBuild)) {
|
||||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
if (change === "delete") {
|
||||||
|
// universal delete case
|
||||||
// update the dep graph by asking all emitters whether they depend on this file
|
contentMap.delete(file as FilePath)
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
const emitterGraph =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
|
||||||
|
|
||||||
if (emitterGraph) {
|
|
||||||
const existingGraph = dependencies[emitter.name]
|
|
||||||
if (existingGraph !== null) {
|
|
||||||
existingGraph.mergeGraph(emitterGraph)
|
|
||||||
} else {
|
|
||||||
// might be the first time we're adding a mardown file
|
|
||||||
dependencies[emitter.name] = emitterGraph
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "change":
|
|
||||||
// invalidate cache when file is changed
|
|
||||||
processedFiles = await parseMarkdown(ctx, [fp])
|
|
||||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
|
||||||
|
|
||||||
// only content files can have added/removed dependencies because of transclusions
|
|
||||||
if (path.extname(fp) === ".md") {
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
// get new dependencies from all emitters for this file
|
|
||||||
const emitterGraph =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
|
||||||
|
|
||||||
// only update the graph if the emitter plugin uses the changed file
|
|
||||||
// eg. Assets plugin ignores md files, so we skip updating the graph
|
|
||||||
if (emitterGraph?.hasNode(fp)) {
|
|
||||||
// merge the new dependencies into the dep graph
|
|
||||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "delete":
|
|
||||||
toRemove.add(fp)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argv.verbose) {
|
// manually track non-markdown files as processed files only
|
||||||
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
|
// contains markdown files
|
||||||
|
if (change === "add" && path.extname(file) !== ".md") {
|
||||||
|
contentMap.set(file as FilePath, {
|
||||||
|
type: "other",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EMIT
|
const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => {
|
||||||
perf.addEvent("rebuild")
|
const path = fp as FilePath
|
||||||
|
const processedContent = contentMap.get(path)
|
||||||
|
if (processedContent?.type === "markdown") {
|
||||||
|
const [_tree, file] = processedContent.content
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// update allFiles and then allSlugs with the consistent view of content map
|
||||||
|
ctx.allFiles = Array.from(contentMap.keys())
|
||||||
|
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||||
|
|
||||||
let emittedFiles = 0
|
let emittedFiles = 0
|
||||||
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
const depGraph = dependencies[emitter.name]
|
// Try to use partialEmit if available, otherwise assume the output is static
|
||||||
|
const emitFn = emitter.partialEmit
|
||||||
// emitter hasn't defined a dependency graph. call it with all processed files
|
if (!emitFn) {
|
||||||
if (depGraph === null) {
|
|
||||||
if (argv.verbose) {
|
|
||||||
console.log(
|
|
||||||
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = [...contentMap.values()].filter(
|
|
||||||
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
|
|
||||||
)
|
|
||||||
|
|
||||||
const emitted = await emitter.emit(ctx, files, staticResources)
|
|
||||||
if (Symbol.asyncIterator in emitted) {
|
|
||||||
// Async generator case
|
|
||||||
for await (const file of emitted) {
|
|
||||||
emittedFiles++
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Array case
|
|
||||||
emittedFiles += emitted.length
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
for (const file of emitted) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// only call the emitter if it uses this file
|
const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
|
||||||
if (depGraph.hasNode(fp)) {
|
|
||||||
// re-emit using all files that are needed for the downstream of this file
|
|
||||||
// eg. for ContentIndex, the dep graph could be:
|
|
||||||
// a.md --> contentIndex.json
|
|
||||||
// b.md ------^
|
|
||||||
//
|
|
||||||
// if a.md changes, we need to re-emit contentIndex.json,
|
|
||||||
// and supply [a.md, b.md] to the emitter
|
|
||||||
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
|
|
||||||
|
|
||||||
const upstreamContent = upstreams
|
|
||||||
// filter out non-markdown files
|
|
||||||
.filter((file) => contentMap.has(file))
|
|
||||||
// if file was deleted, don't give it to the emitter
|
|
||||||
.filter((file) => !toRemove.has(file))
|
|
||||||
.map((file) => contentMap.get(file)!)
|
|
||||||
|
|
||||||
const emitted = await emitter.emit(ctx, upstreamContent, staticResources)
|
|
||||||
if (Symbol.asyncIterator in emitted) {
|
if (Symbol.asyncIterator in emitted) {
|
||||||
// Async generator case
|
// Async generator case
|
||||||
for await (const file of emitted) {
|
for await (const file of emitted) {
|
||||||
@ -309,123 +264,11 @@ async function partialRebuildFromEntrypoint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
||||||
|
|
||||||
// CLEANUP
|
|
||||||
const destinationsToDelete = new Set<FilePath>()
|
|
||||||
for (const file of toRemove) {
|
|
||||||
// remove from cache
|
|
||||||
contentMap.delete(file)
|
|
||||||
Object.values(dependencies).forEach((depGraph) => {
|
|
||||||
// remove the node from dependency graphs
|
|
||||||
depGraph?.removeNode(file)
|
|
||||||
// remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed
|
|
||||||
const orphanNodes = depGraph?.removeOrphanNodes()
|
|
||||||
orphanNodes?.forEach((node) => {
|
|
||||||
// only delete files that are in the output directory
|
|
||||||
if (node.startsWith(argv.output)) {
|
|
||||||
destinationsToDelete.add(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await rimraf([...destinationsToDelete])
|
|
||||||
|
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||||
|
changes.length = 0
|
||||||
toRemove.clear()
|
|
||||||
release()
|
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildFromEntrypoint(
|
|
||||||
fp: string,
|
|
||||||
action: FileEvent,
|
|
||||||
clientRefresh: () => void,
|
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
|
||||||
) {
|
|
||||||
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
|
|
||||||
buildData
|
|
||||||
|
|
||||||
const { argv } = ctx
|
|
||||||
|
|
||||||
// don't do anything for gitignored files
|
|
||||||
if (ignored(fp)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// dont bother rebuilding for non-content files, just track and refresh
|
|
||||||
fp = toPosixPath(fp)
|
|
||||||
const filePath = joinSegments(argv.directory, fp) as FilePath
|
|
||||||
if (path.extname(fp) !== ".md") {
|
|
||||||
if (action === "add" || action === "change") {
|
|
||||||
trackedAssets.add(filePath)
|
|
||||||
} else if (action === "delete") {
|
|
||||||
trackedAssets.delete(filePath)
|
|
||||||
}
|
|
||||||
clientRefresh()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "add" || action === "change") {
|
|
||||||
toRebuild.add(filePath)
|
|
||||||
} else if (action === "delete") {
|
|
||||||
toRemove.add(filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildId = randomIdNonSecure()
|
|
||||||
ctx.buildId = buildId
|
|
||||||
buildData.lastBuildMs = new Date().getTime()
|
|
||||||
const release = await mut.acquire()
|
|
||||||
|
|
||||||
// there's another build after us, release and let them do it
|
|
||||||
if (ctx.buildId !== buildId) {
|
|
||||||
release()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
|
||||||
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
|
||||||
for (const content of parsedContent) {
|
|
||||||
const [_tree, vfile] = content
|
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fp of toRemove) {
|
|
||||||
contentMap.delete(fp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedFiles = [...contentMap.values()]
|
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
|
||||||
|
|
||||||
// re-update slugs
|
|
||||||
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
|
||||||
.filter((fp) => !toRemove.has(fp))
|
|
||||||
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
|
||||||
|
|
||||||
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
|
||||||
|
|
||||||
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
|
||||||
// instead of just deleting everything
|
|
||||||
await rimraf(path.join(argv.output, ".*"), { glob: true })
|
|
||||||
await emitContent(ctx, filteredContent)
|
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
|
||||||
} catch (err) {
|
|
||||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
|
||||||
if (argv.verbose) {
|
|
||||||
console.log(chalk.red(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clientRefresh()
|
|
||||||
toRebuild.clear()
|
|
||||||
toRemove.clear()
|
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,10 +71,10 @@ export const BuildArgv = {
|
|||||||
default: false,
|
default: false,
|
||||||
describe: "run a local server to live-preview your Quartz",
|
describe: "run a local server to live-preview your Quartz",
|
||||||
},
|
},
|
||||||
fastRebuild: {
|
watch: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: false,
|
default: false,
|
||||||
describe: "[experimental] rebuild only the changed files",
|
describe: "watch for changes and rebuild automatically",
|
||||||
},
|
},
|
||||||
baseDir: {
|
baseDir: {
|
||||||
string: true,
|
string: true,
|
||||||
|
@ -225,6 +225,10 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
* @param {*} argv arguments for `build`
|
* @param {*} argv arguments for `build`
|
||||||
*/
|
*/
|
||||||
export async function handleBuild(argv) {
|
export async function handleBuild(argv) {
|
||||||
|
if (argv.serve) {
|
||||||
|
argv.watch = true
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
const ctx = await esbuild.context({
|
const ctx = await esbuild.context({
|
||||||
entryPoints: [fp],
|
entryPoints: [fp],
|
||||||
@ -331,9 +335,10 @@ export async function handleBuild(argv) {
|
|||||||
clientRefresh()
|
clientRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let clientRefresh = () => {}
|
||||||
if (argv.serve) {
|
if (argv.serve) {
|
||||||
const connections = []
|
const connections = []
|
||||||
const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
|
clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
|
||||||
|
|
||||||
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
|
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
|
||||||
argv.baseDir = "/" + argv.baseDir
|
argv.baseDir = "/" + argv.baseDir
|
||||||
@ -433,6 +438,7 @@ export async function handleBuild(argv) {
|
|||||||
|
|
||||||
return serve()
|
return serve()
|
||||||
})
|
})
|
||||||
|
|
||||||
server.listen(argv.port)
|
server.listen(argv.port)
|
||||||
const wss = new WebSocketServer({ port: argv.wsPort })
|
const wss = new WebSocketServer({ port: argv.wsPort })
|
||||||
wss.on("connection", (ws) => connections.push(ws))
|
wss.on("connection", (ws) => connections.push(ws))
|
||||||
@ -441,16 +447,26 @@ export async function handleBuild(argv) {
|
|||||||
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
|
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
console.log("hint: exit with ctrl+c")
|
} else {
|
||||||
const paths = await globby(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"])
|
await build(clientRefresh)
|
||||||
|
ctx.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.watch) {
|
||||||
|
const paths = await globby([
|
||||||
|
"**/*.ts",
|
||||||
|
"quartz/cli/*.js",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.scss",
|
||||||
|
"package.json",
|
||||||
|
])
|
||||||
chokidar
|
chokidar
|
||||||
.watch(paths, { ignoreInitial: true })
|
.watch(paths, { ignoreInitial: true })
|
||||||
.on("add", () => build(clientRefresh))
|
.on("add", () => build(clientRefresh))
|
||||||
.on("change", () => build(clientRefresh))
|
.on("change", () => build(clientRefresh))
|
||||||
.on("unlink", () => build(clientRefresh))
|
.on("unlink", () => build(clientRefresh))
|
||||||
} else {
|
|
||||||
await build(() => {})
|
console.log(chalk.grey("hint: exit with ctrl+c"))
|
||||||
ctx.dispose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,17 +65,12 @@ export function pageResources(
|
|||||||
return resources
|
return resources
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPage(
|
function renderTranscludes(
|
||||||
|
root: Root,
|
||||||
cfg: GlobalConfiguration,
|
cfg: GlobalConfiguration,
|
||||||
slug: FullSlug,
|
slug: FullSlug,
|
||||||
componentData: QuartzComponentProps,
|
componentData: QuartzComponentProps,
|
||||||
components: RenderComponents,
|
) {
|
||||||
pageResources: StaticResources,
|
|
||||||
): string {
|
|
||||||
// make a deep copy of the tree so we don't remove the transclusion references
|
|
||||||
// for the file cached in contentMap in build.ts
|
|
||||||
const root = clone(componentData.tree) as Root
|
|
||||||
|
|
||||||
// process transcludes in componentData
|
// process transcludes in componentData
|
||||||
visit(root, "element", (node, _index, _parent) => {
|
visit(root, "element", (node, _index, _parent) => {
|
||||||
if (node.tagName === "blockquote") {
|
if (node.tagName === "blockquote") {
|
||||||
@ -191,6 +186,19 @@ export function renderPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderPage(
|
||||||
|
cfg: GlobalConfiguration,
|
||||||
|
slug: FullSlug,
|
||||||
|
componentData: QuartzComponentProps,
|
||||||
|
components: RenderComponents,
|
||||||
|
pageResources: StaticResources,
|
||||||
|
): string {
|
||||||
|
// make a deep copy of the tree so we don't remove the transclusion references
|
||||||
|
// for the file cached in contentMap in build.ts
|
||||||
|
const root = clone(componentData.tree) as Root
|
||||||
|
renderTranscludes(root, cfg, slug, componentData)
|
||||||
|
|
||||||
// set componentData.tree to the edited html that has transclusions rendered
|
// set componentData.tree to the edited html that has transclusions rendered
|
||||||
componentData.tree = root
|
componentData.tree = root
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
import test, { describe } from "node:test"
|
|
||||||
import DepGraph from "./depgraph"
|
|
||||||
import assert from "node:assert"
|
|
||||||
|
|
||||||
describe("DepGraph", () => {
|
|
||||||
test("getLeafNodes", () => {
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A", "B")
|
|
||||||
graph.addEdge("B", "C")
|
|
||||||
graph.addEdge("D", "C")
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodes("A"), new Set(["C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodes("B"), new Set(["C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodes("C"), new Set(["C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodes("D"), new Set(["C"]))
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getLeafNodeAncestors", () => {
|
|
||||||
test("gets correct ancestors in a graph without cycles", () => {
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A", "B")
|
|
||||||
graph.addEdge("B", "C")
|
|
||||||
graph.addEdge("D", "B")
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "D"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "D"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "D"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "D"]))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("gets correct ancestors in a graph with cycles", () => {
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A", "B")
|
|
||||||
graph.addEdge("B", "C")
|
|
||||||
graph.addEdge("C", "A")
|
|
||||||
graph.addEdge("C", "D")
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "C"]))
|
|
||||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "C"]))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("mergeGraph", () => {
|
|
||||||
test("merges two graphs", () => {
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A.md", "A.html")
|
|
||||||
|
|
||||||
const other = new DepGraph<string>()
|
|
||||||
other.addEdge("B.md", "B.html")
|
|
||||||
|
|
||||||
graph.mergeGraph(other)
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
nodes: ["A.md", "A.html", "B.md", "B.html"],
|
|
||||||
edges: [
|
|
||||||
["A.md", "A.html"],
|
|
||||||
["B.md", "B.html"],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.deepStrictEqual(graph.export(), expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("updateIncomingEdgesForNode", () => {
|
|
||||||
test("merges when node exists", () => {
|
|
||||||
// A.md -> B.md -> B.html
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A.md", "B.md")
|
|
||||||
graph.addEdge("B.md", "B.html")
|
|
||||||
|
|
||||||
// B.md is edited so it removes the A.md transclusion
|
|
||||||
// and adds C.md transclusion
|
|
||||||
// C.md -> B.md
|
|
||||||
const other = new DepGraph<string>()
|
|
||||||
other.addEdge("C.md", "B.md")
|
|
||||||
other.addEdge("B.md", "B.html")
|
|
||||||
|
|
||||||
// A.md -> B.md removed, C.md -> B.md added
|
|
||||||
// C.md -> B.md -> B.html
|
|
||||||
graph.updateIncomingEdgesForNode(other, "B.md")
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
nodes: ["A.md", "B.md", "B.html", "C.md"],
|
|
||||||
edges: [
|
|
||||||
["B.md", "B.html"],
|
|
||||||
["C.md", "B.md"],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.deepStrictEqual(graph.export(), expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("adds node if it does not exist", () => {
|
|
||||||
// A.md -> B.md
|
|
||||||
const graph = new DepGraph<string>()
|
|
||||||
graph.addEdge("A.md", "B.md")
|
|
||||||
|
|
||||||
// Add a new file C.md that transcludes B.md
|
|
||||||
// B.md -> C.md
|
|
||||||
const other = new DepGraph<string>()
|
|
||||||
other.addEdge("B.md", "C.md")
|
|
||||||
|
|
||||||
// B.md -> C.md added
|
|
||||||
// A.md -> B.md -> C.md
|
|
||||||
graph.updateIncomingEdgesForNode(other, "C.md")
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
nodes: ["A.md", "B.md", "C.md"],
|
|
||||||
edges: [
|
|
||||||
["A.md", "B.md"],
|
|
||||||
["B.md", "C.md"],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.deepStrictEqual(graph.export(), expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,228 +0,0 @@
|
|||||||
export default class DepGraph<T> {
|
|
||||||
// node: incoming and outgoing edges
|
|
||||||
_graph = new Map<T, { incoming: Set<T>; outgoing: Set<T> }>()
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._graph = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
export(): Object {
|
|
||||||
return {
|
|
||||||
nodes: this.nodes,
|
|
||||||
edges: this.edges,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return JSON.stringify(this.export(), null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BASIC GRAPH OPERATIONS
|
|
||||||
|
|
||||||
get nodes(): T[] {
|
|
||||||
return Array.from(this._graph.keys())
|
|
||||||
}
|
|
||||||
|
|
||||||
get edges(): [T, T][] {
|
|
||||||
let edges: [T, T][] = []
|
|
||||||
this.forEachEdge((edge) => edges.push(edge))
|
|
||||||
return edges
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNode(node: T): boolean {
|
|
||||||
return this._graph.has(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
addNode(node: T): void {
|
|
||||||
if (!this._graph.has(node)) {
|
|
||||||
this._graph.set(node, { incoming: new Set(), outgoing: new Set() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove node and all edges connected to it
|
|
||||||
removeNode(node: T): void {
|
|
||||||
if (this._graph.has(node)) {
|
|
||||||
// first remove all edges so other nodes don't have references to this node
|
|
||||||
for (const target of this._graph.get(node)!.outgoing) {
|
|
||||||
this.removeEdge(node, target)
|
|
||||||
}
|
|
||||||
for (const source of this._graph.get(node)!.incoming) {
|
|
||||||
this.removeEdge(source, node)
|
|
||||||
}
|
|
||||||
this._graph.delete(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachNode(callback: (node: T) => void): void {
|
|
||||||
for (const node of this._graph.keys()) {
|
|
||||||
callback(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEdge(from: T, to: T): boolean {
|
|
||||||
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
|
||||||
}
|
|
||||||
|
|
||||||
addEdge(from: T, to: T): void {
|
|
||||||
this.addNode(from)
|
|
||||||
this.addNode(to)
|
|
||||||
|
|
||||||
this._graph.get(from)!.outgoing.add(to)
|
|
||||||
this._graph.get(to)!.incoming.add(from)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEdge(from: T, to: T): void {
|
|
||||||
if (this._graph.has(from) && this._graph.has(to)) {
|
|
||||||
this._graph.get(from)!.outgoing.delete(to)
|
|
||||||
this._graph.get(to)!.incoming.delete(from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns -1 if node does not exist
|
|
||||||
outDegree(node: T): number {
|
|
||||||
return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns -1 if node does not exist
|
|
||||||
inDegree(node: T): number {
|
|
||||||
return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void {
|
|
||||||
this._graph.get(node)?.outgoing.forEach(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachInNeighbor(node: T, callback: (neighbor: T) => void): void {
|
|
||||||
this._graph.get(node)?.incoming.forEach(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachEdge(callback: (edge: [T, T]) => void): void {
|
|
||||||
for (const [source, { outgoing }] of this._graph.entries()) {
|
|
||||||
for (const target of outgoing) {
|
|
||||||
callback([source, target])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPENDENCY ALGORITHMS
|
|
||||||
|
|
||||||
// Add all nodes and edges from other graph to this graph
|
|
||||||
mergeGraph(other: DepGraph<T>): void {
|
|
||||||
other.forEachEdge(([source, target]) => {
|
|
||||||
this.addNode(source)
|
|
||||||
this.addNode(target)
|
|
||||||
this.addEdge(source, target)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the node provided:
|
|
||||||
// If node does not exist, add it
|
|
||||||
// If an incoming edge was added in other, it is added in this graph
|
|
||||||
// If an incoming edge was deleted in other, it is deleted in this graph
|
|
||||||
updateIncomingEdgesForNode(other: DepGraph<T>, node: T): void {
|
|
||||||
this.addNode(node)
|
|
||||||
|
|
||||||
// Add edge if it is present in other
|
|
||||||
other.forEachInNeighbor(node, (neighbor) => {
|
|
||||||
this.addEdge(neighbor, node)
|
|
||||||
})
|
|
||||||
|
|
||||||
// For node provided, remove incoming edge if it is absent in other
|
|
||||||
this.forEachEdge(([source, target]) => {
|
|
||||||
if (target === node && !other.hasEdge(source, target)) {
|
|
||||||
this.removeEdge(source, target)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all nodes that do not have any incoming or outgoing edges
|
|
||||||
// A node may be orphaned if the only node pointing to it was removed
|
|
||||||
removeOrphanNodes(): Set<T> {
|
|
||||||
let orphanNodes = new Set<T>()
|
|
||||||
|
|
||||||
this.forEachNode((node) => {
|
|
||||||
if (this.inDegree(node) === 0 && this.outDegree(node) === 0) {
|
|
||||||
orphanNodes.add(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
orphanNodes.forEach((node) => {
|
|
||||||
this.removeNode(node)
|
|
||||||
})
|
|
||||||
|
|
||||||
return orphanNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
|
|
||||||
// Eg. if the graph is A -> B -> C
|
|
||||||
// D ---^
|
|
||||||
// and the node is B, this function returns [C]
|
|
||||||
getLeafNodes(node: T): Set<T> {
|
|
||||||
let stack: T[] = [node]
|
|
||||||
let visited = new Set<T>()
|
|
||||||
let leafNodes = new Set<T>()
|
|
||||||
|
|
||||||
// DFS
|
|
||||||
while (stack.length > 0) {
|
|
||||||
let node = stack.pop()!
|
|
||||||
|
|
||||||
// If the node is already visited, skip it
|
|
||||||
if (visited.has(node)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
visited.add(node)
|
|
||||||
|
|
||||||
// Check if the node is a leaf node (i.e. destination path)
|
|
||||||
if (this.outDegree(node) === 0) {
|
|
||||||
leafNodes.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all unvisited neighbors to the stack
|
|
||||||
this.forEachOutNeighbor(node, (neighbor) => {
|
|
||||||
if (!visited.has(neighbor)) {
|
|
||||||
stack.push(neighbor)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return leafNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all ancestors of the leaf nodes reachable from the node provided
|
|
||||||
// Eg. if the graph is A -> B -> C
|
|
||||||
// D ---^
|
|
||||||
// and the node is B, this function returns [A, B, D]
|
|
||||||
getLeafNodeAncestors(node: T): Set<T> {
|
|
||||||
const leafNodes = this.getLeafNodes(node)
|
|
||||||
let visited = new Set<T>()
|
|
||||||
let upstreamNodes = new Set<T>()
|
|
||||||
|
|
||||||
// Backwards DFS for each leaf node
|
|
||||||
leafNodes.forEach((leafNode) => {
|
|
||||||
let stack: T[] = [leafNode]
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
let node = stack.pop()!
|
|
||||||
|
|
||||||
if (visited.has(node)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
visited.add(node)
|
|
||||||
// Add node if it's not a leaf node (i.e. destination path)
|
|
||||||
// Assumes destination file cannot depend on another destination file
|
|
||||||
if (this.outDegree(node) !== 0) {
|
|
||||||
upstreamNodes.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all unvisited parents to the stack
|
|
||||||
this.forEachInNeighbor(node, (parentNode) => {
|
|
||||||
if (!visited.has(parentNode)) {
|
|
||||||
stack.push(parentNode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return upstreamNodes
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,6 @@ import { NotFound } from "../../components"
|
|||||||
import { defaultProcessedContent } from "../vfile"
|
import { defaultProcessedContent } from "../vfile"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@ -28,9 +27,6 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Body, pageBody, Footer]
|
return [Head, Body, pageBody, Footer]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(_ctx, _content, _resources) {
|
|
||||||
return new DepGraph<FilePath>()
|
|
||||||
},
|
|
||||||
async *emit(ctx, _content, resources) {
|
async *emit(ctx, _content, resources) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const slug = "404" as FullSlug
|
const slug = "404" as FullSlug
|
||||||
|
@ -1,25 +1,10 @@
|
|||||||
import { FilePath, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
import { resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import DepGraph from "../../depgraph"
|
import { BuildCtx } from "../../util/ctx"
|
||||||
import { getAliasSlugs } from "../transformers/frontmatter"
|
import { VFile } from "vfile"
|
||||||
|
|
||||||
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
async function *processFile(ctx: BuildCtx, file: VFile) {
|
||||||
name: "AliasRedirects",
|
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
const { argv } = ctx
|
|
||||||
for (const [_tree, file] of content) {
|
|
||||||
for (const slug of getAliasSlugs(file.data.frontmatter?.aliases ?? [], argv, file)) {
|
|
||||||
graph.addEdge(file.data.filePath!, joinSegments(argv.output, slug + ".html") as FilePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit(ctx, content, _resources) {
|
|
||||||
for (const [_tree, file] of content) {
|
|
||||||
const ogSlug = simplifySlug(file.data.slug!)
|
const ogSlug = simplifySlug(file.data.slug!)
|
||||||
|
|
||||||
for (const slug of file.data.aliases ?? []) {
|
for (const slug of file.data.aliases ?? []) {
|
||||||
@ -42,6 +27,22 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
ext: ".html",
|
ext: ".html",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
|
name: "AliasRedirects",
|
||||||
|
async *emit(ctx, content) {
|
||||||
|
for (const [_tree, file] of content) {
|
||||||
|
yield* processFile(ctx, file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
||||||
|
for (const changeEvent of changeEvents) {
|
||||||
|
if (!changeEvent.file) continue
|
||||||
|
if (changeEvent.type === "add" || changeEvent.type === "change") {
|
||||||
|
// add new ones if this file still exists
|
||||||
|
yield* processFile(ctx, changeEvent.file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,6 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { glob } from "../../util/glob"
|
import { glob } from "../../util/glob"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
import { Argv } from "../../util/ctx"
|
import { Argv } from "../../util/ctx"
|
||||||
import { QuartzConfig } from "../../cfg"
|
import { QuartzConfig } from "../../cfg"
|
||||||
|
|
||||||
@ -12,41 +11,42 @@ const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
|||||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyFile = async (argv: Argv, fp: FilePath) => {
|
||||||
|
const src = joinSegments(argv.directory, fp) as FilePath
|
||||||
|
|
||||||
|
const name = slugifyFilePath(fp)
|
||||||
|
const dest = joinSegments(argv.output, name) as FilePath
|
||||||
|
|
||||||
|
// ensure dir exists
|
||||||
|
const dir = path.dirname(dest) as FilePath
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true })
|
||||||
|
|
||||||
|
await fs.promises.copyFile(src, dest)
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
export const Assets: QuartzEmitterPlugin = () => {
|
export const Assets: QuartzEmitterPlugin = () => {
|
||||||
return {
|
return {
|
||||||
name: "Assets",
|
name: "Assets",
|
||||||
async getDependencyGraph(ctx, _content, _resources) {
|
async *emit({ argv, cfg }) {
|
||||||
const { argv, cfg } = ctx
|
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
const fps = await filesToCopy(argv, cfg)
|
|
||||||
|
|
||||||
for (const fp of fps) {
|
|
||||||
const ext = path.extname(fp)
|
|
||||||
const src = joinSegments(argv.directory, fp) as FilePath
|
|
||||||
const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath
|
|
||||||
|
|
||||||
const dest = joinSegments(argv.output, name) as FilePath
|
|
||||||
|
|
||||||
graph.addEdge(src, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit({ argv, cfg }, _content, _resources) {
|
|
||||||
const assetsPath = argv.output
|
|
||||||
const fps = await filesToCopy(argv, cfg)
|
const fps = await filesToCopy(argv, cfg)
|
||||||
for (const fp of fps) {
|
for (const fp of fps) {
|
||||||
const ext = path.extname(fp)
|
yield copyFile(argv, fp)
|
||||||
const src = joinSegments(argv.directory, fp) as FilePath
|
|
||||||
const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath
|
|
||||||
|
|
||||||
const dest = joinSegments(assetsPath, name) as FilePath
|
|
||||||
const dir = path.dirname(dest) as FilePath
|
|
||||||
await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
|
|
||||||
await fs.promises.copyFile(src, dest)
|
|
||||||
yield dest
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
||||||
|
for (const changeEvent of changeEvents) {
|
||||||
|
const ext = path.extname(changeEvent.path)
|
||||||
|
if (ext === ".md") continue
|
||||||
|
|
||||||
|
if (changeEvent.type === "add" || changeEvent.type === "change") {
|
||||||
|
yield copyFile(ctx.argv, changeEvent.path)
|
||||||
|
} else if (changeEvent.type === 'delete') {
|
||||||
|
const name = slugifyFilePath(changeEvent.path)
|
||||||
|
const dest = joinSegments(ctx.argv.output, name) as FilePath
|
||||||
|
await fs.promises.unlink(dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { FilePath, joinSegments } from "../../util/path"
|
|||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
export function extractDomainFromBaseUrl(baseUrl: string) {
|
export function extractDomainFromBaseUrl(baseUrl: string) {
|
||||||
const url = new URL(`https://${baseUrl}`)
|
const url = new URL(`https://${baseUrl}`)
|
||||||
@ -11,10 +10,7 @@ export function extractDomainFromBaseUrl(baseUrl: string) {
|
|||||||
|
|
||||||
export const CNAME: QuartzEmitterPlugin = () => ({
|
export const CNAME: QuartzEmitterPlugin = () => ({
|
||||||
name: "CNAME",
|
name: "CNAME",
|
||||||
async getDependencyGraph(_ctx, _content, _resources) {
|
async emit({ argv, cfg }) {
|
||||||
return new DepGraph<FilePath>()
|
|
||||||
},
|
|
||||||
async emit({ argv, cfg }, _content, _resources) {
|
|
||||||
if (!cfg.configuration.baseUrl) {
|
if (!cfg.configuration.baseUrl) {
|
||||||
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||||
return []
|
return []
|
||||||
|
@ -13,7 +13,6 @@ import { googleFontHref, joinStyles, processGoogleFonts } from "../../util/theme
|
|||||||
import { Features, transform } from "lightningcss"
|
import { Features, transform } from "lightningcss"
|
||||||
import { transform as transpile } from "esbuild"
|
import { transform as transpile } from "esbuild"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
type ComponentResources = {
|
type ComponentResources = {
|
||||||
css: string[]
|
css: string[]
|
||||||
@ -203,9 +202,6 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
|
|||||||
export const ComponentResources: QuartzEmitterPlugin = () => {
|
export const ComponentResources: QuartzEmitterPlugin = () => {
|
||||||
return {
|
return {
|
||||||
name: "ComponentResources",
|
name: "ComponentResources",
|
||||||
async getDependencyGraph(_ctx, _content, _resources) {
|
|
||||||
return new DepGraph<FilePath>()
|
|
||||||
},
|
|
||||||
async *emit(ctx, _content, _resources) {
|
async *emit(ctx, _content, _resources) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
// component specific scripts and styles
|
// component specific scripts and styles
|
||||||
@ -281,13 +277,15 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
|
|||||||
},
|
},
|
||||||
include: Features.MediaQueries,
|
include: Features.MediaQueries,
|
||||||
}).code.toString(),
|
}).code.toString(),
|
||||||
}),
|
})
|
||||||
|
|
||||||
yield write({
|
yield write({
|
||||||
ctx,
|
ctx,
|
||||||
slug: "prescript" as FullSlug,
|
slug: "prescript" as FullSlug,
|
||||||
ext: ".js",
|
ext: ".js",
|
||||||
content: prescript,
|
content: prescript,
|
||||||
}),
|
})
|
||||||
|
|
||||||
yield write({
|
yield write({
|
||||||
ctx,
|
ctx,
|
||||||
slug: "postscript" as FullSlug,
|
slug: "postscript" as FullSlug,
|
||||||
|
@ -7,7 +7,6 @@ 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 DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
@ -97,27 +96,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
opts = { ...defaultOptions, ...opts }
|
opts = { ...defaultOptions, ...opts }
|
||||||
return {
|
return {
|
||||||
name: "ContentIndex",
|
name: "ContentIndex",
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
async *emit(ctx, content) {
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
for (const [_tree, file] of content) {
|
|
||||||
const sourcePath = file.data.filePath!
|
|
||||||
|
|
||||||
graph.addEdge(
|
|
||||||
sourcePath,
|
|
||||||
joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath,
|
|
||||||
)
|
|
||||||
if (opts?.enableSiteMap) {
|
|
||||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath)
|
|
||||||
}
|
|
||||||
if (opts?.enableRSS) {
|
|
||||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit(ctx, content, _resources) {
|
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const linkIndex: ContentIndexMap = new Map()
|
const linkIndex: ContentIndexMap = new Map()
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
@ -126,7 +105,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
||||||
linkIndex.set(slug, {
|
linkIndex.set(slug, {
|
||||||
slug,
|
slug,
|
||||||
filePath: file.data.filePath!,
|
filePath: file.data.relativePath!,
|
||||||
title: file.data.frontmatter?.title!,
|
title: file.data.frontmatter?.title!,
|
||||||
links: file.data.links ?? [],
|
links: file.data.links ?? [],
|
||||||
tags: file.data.frontmatter?.tags ?? [],
|
tags: file.data.frontmatter?.tags ?? [],
|
||||||
|
@ -14,43 +14,8 @@ import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.
|
|||||||
import { Content } from "../../components"
|
import { Content } from "../../components"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
// get all the dependencies for the markdown file
|
|
||||||
// eg. images, scripts, stylesheets, transclusions
|
|
||||||
const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
|
|
||||||
const dependencies: string[] = []
|
|
||||||
|
|
||||||
visit(hast, "element", (elem): void => {
|
|
||||||
let ref: string | null = null
|
|
||||||
|
|
||||||
if (
|
|
||||||
["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) &&
|
|
||||||
elem?.properties?.src
|
|
||||||
) {
|
|
||||||
ref = elem.properties.src.toString()
|
|
||||||
} else if (["a", "link"].includes(elem.tagName) && elem?.properties?.href) {
|
|
||||||
// transclusions will create a tags with relative hrefs
|
|
||||||
ref = elem.properties.href.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it is a relative url, its a local file and we need to add
|
|
||||||
// it to the dependency graph. otherwise, ignore
|
|
||||||
if (ref === null || !isRelativeURL(ref)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fp = path.join(file.data.filePath!, path.relative(argv.directory, ref)).replace(/\\/g, "/")
|
|
||||||
// markdown files have the .md extension stripped in hrefs, add it back here
|
|
||||||
if (!fp.split("/").pop()?.includes(".")) {
|
|
||||||
fp += ".md"
|
|
||||||
}
|
|
||||||
dependencies.push(fp)
|
|
||||||
})
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO check for transclusions in partial rebuild
|
||||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
@ -79,21 +44,6 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
Footer,
|
Footer,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
for (const [tree, file] of content) {
|
|
||||||
const sourcePath = file.data.filePath!
|
|
||||||
const slug = file.data.slug!
|
|
||||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
|
|
||||||
|
|
||||||
parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => {
|
|
||||||
graph.addEdge(dep as FilePath, sourcePath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit(ctx, content, resources) {
|
async *emit(ctx, content, resources) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
@ -129,7 +79,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!containsIndex && !ctx.argv.fastRebuild) {
|
if (!containsIndex) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder (\`${path.join(ctx.argv.directory, "index.md")} does not exist\`). This may cause errors when deploying.`,
|
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder (\`${path.join(ctx.argv.directory, "index.md")} does not exist\`). This may cause errors when deploying.`,
|
||||||
|
@ -19,7 +19,6 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
|
|||||||
import { FolderContent } from "../../components"
|
import { FolderContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
interface FolderPageOptions extends FullPageLayout {
|
interface FolderPageOptions extends FullPageLayout {
|
||||||
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
@ -53,22 +52,6 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
Footer,
|
Footer,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(_ctx, content, _resources) {
|
|
||||||
// Example graph:
|
|
||||||
// nested/file.md --> nested/index.html
|
|
||||||
// nested/file2.md ------^
|
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
content.map(([_tree, vfile]) => {
|
|
||||||
const slug = vfile.data.slug
|
|
||||||
const folderName = path.dirname(slug ?? "") as SimpleSlug
|
|
||||||
if (slug && folderName !== "." && folderName !== "tags") {
|
|
||||||
graph.addEdge(vfile.data.filePath!, joinSegments(folderName, "index.html") as FilePath)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit(ctx, content, resources) {
|
async *emit(ctx, content, resources) {
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
|
@ -11,6 +11,8 @@ type WriteOptions = {
|
|||||||
content: string | Buffer | Readable
|
content: string | Buffer | Readable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteOptions = Omit<WriteOptions, "content">
|
||||||
|
|
||||||
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
|
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
|
||||||
const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
|
const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
|
||||||
const dir = path.dirname(pathToPage)
|
const dir = path.dirname(pathToPage)
|
||||||
|
@ -2,26 +2,11 @@ import { FilePath, QUARTZ, joinSegments } from "../../util/path"
|
|||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { glob } from "../../util/glob"
|
import { glob } from "../../util/glob"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
|
|
||||||
export const Static: QuartzEmitterPlugin = () => ({
|
export const Static: QuartzEmitterPlugin = () => ({
|
||||||
name: "Static",
|
name: "Static",
|
||||||
async getDependencyGraph({ argv, cfg }, _content, _resources) {
|
async *emit({ argv, cfg }) {
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
const staticPath = joinSegments(QUARTZ, "static")
|
|
||||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
|
||||||
for (const fp of fps) {
|
|
||||||
graph.addEdge(
|
|
||||||
joinSegments("static", fp) as FilePath,
|
|
||||||
joinSegments(argv.output, "static", fp) as FilePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit({ argv, cfg }, _content) {
|
|
||||||
const staticPath = joinSegments(QUARTZ, "static")
|
const staticPath = joinSegments(QUARTZ, "static")
|
||||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||||
const outputStaticPath = joinSegments(argv.output, "static")
|
const outputStaticPath = joinSegments(argv.output, "static")
|
||||||
|
@ -16,7 +16,6 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
|
|||||||
import { TagContent } from "../../components"
|
import { TagContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import DepGraph from "../../depgraph"
|
|
||||||
|
|
||||||
interface TagPageOptions extends FullPageLayout {
|
interface TagPageOptions extends FullPageLayout {
|
||||||
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
@ -50,27 +49,6 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
|||||||
Footer,
|
Footer,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
|
||||||
const graph = new DepGraph<FilePath>()
|
|
||||||
|
|
||||||
for (const [_tree, file] of content) {
|
|
||||||
const sourcePath = file.data.filePath!
|
|
||||||
const tags = (file.data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
|
||||||
// if the file has at least one tag, it is used in the tag index page
|
|
||||||
if (tags.length > 0) {
|
|
||||||
tags.push("index")
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tag of tags) {
|
|
||||||
graph.addEdge(
|
|
||||||
sourcePath,
|
|
||||||
joinSegments(ctx.argv.output, "tags", tag + ".html") as FilePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
},
|
|
||||||
async *emit(ctx, content, resources) {
|
async *emit(ctx, content, resources) {
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
|
@ -3,12 +3,9 @@ import remarkFrontmatter from "remark-frontmatter"
|
|||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
import toml from "toml"
|
import toml from "toml"
|
||||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath, slugTag } from "../../util/path"
|
import { FilePath, FullSlug, getFileExtension, joinSegments, slugifyFilePath, slugTag, trimSuffix } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../vfile"
|
import { QuartzPluginData } from "../vfile"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { Argv } from "../../util/ctx"
|
|
||||||
import { VFile } from "vfile"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
delimiters: string | [string, string]
|
delimiters: string | [string, string]
|
||||||
@ -43,26 +40,24 @@ function coerceToArray(input: string | string[]): string[] | undefined {
|
|||||||
.map((tag: string | number) => tag.toString())
|
.map((tag: string | number) => tag.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAliasSlugs(aliases: string[], argv: Argv, file: VFile): FullSlug[] {
|
function getAliasSlugs(aliases: string[]): FullSlug[] {
|
||||||
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
const res: FullSlug[] = []
|
||||||
const slugs: FullSlug[] = aliases.map(
|
for (const alias of aliases) {
|
||||||
(alias) => path.posix.join(dir, slugifyFilePath(alias as FilePath)) as FullSlug,
|
const isMd = getFileExtension(alias) === "md"
|
||||||
)
|
const mockFp = isMd ? alias : alias + ".md"
|
||||||
const permalink = file.data.frontmatter?.permalink
|
const slug = slugifyFilePath(mockFp as FilePath)
|
||||||
if (typeof permalink === "string") {
|
res.push(slug)
|
||||||
slugs.push(permalink as FullSlug)
|
|
||||||
}
|
}
|
||||||
// fix any slugs that have trailing slash
|
|
||||||
return slugs.map((slug) =>
|
return res
|
||||||
slug.endsWith("/") ? (joinSegments(slug, "index") as FullSlug) : slug,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "FrontMatter",
|
name: "FrontMatter",
|
||||||
markdownPlugins({ cfg, allSlugs, argv }) {
|
markdownPlugins(ctx) {
|
||||||
|
const { cfg, allSlugs } = ctx
|
||||||
return [
|
return [
|
||||||
[remarkFrontmatter, ["yaml", "toml"]],
|
[remarkFrontmatter, ["yaml", "toml"]],
|
||||||
() => {
|
() => {
|
||||||
@ -88,9 +83,18 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
||||||
if (aliases) {
|
if (aliases) {
|
||||||
data.aliases = aliases // frontmatter
|
data.aliases = aliases // frontmatter
|
||||||
const slugs = (file.data.aliases = getAliasSlugs(aliases, argv, file))
|
file.data.aliases = getAliasSlugs(aliases)
|
||||||
allSlugs.push(...slugs)
|
allSlugs.push(...file.data.aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.permalink != null && data.permalink.toString() !== "") {
|
||||||
|
data.permalink = data.permalink.toString() as FullSlug
|
||||||
|
const aliases = file.data.aliases ?? []
|
||||||
|
aliases.push(data.permalink)
|
||||||
|
file.data.aliases = aliases
|
||||||
|
allSlugs.push(data.permalink)
|
||||||
|
}
|
||||||
|
|
||||||
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
||||||
if (cssclasses) data.cssclasses = cssclasses
|
if (cssclasses) data.cssclasses = cssclasses
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
|||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "CreatedModifiedDate",
|
name: "CreatedModifiedDate",
|
||||||
markdownPlugins() {
|
markdownPlugins(ctx) {
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
let repo: Repository | undefined = undefined
|
let repo: Repository | undefined = undefined
|
||||||
@ -40,8 +40,8 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
|||||||
let modified: MaybeDate = undefined
|
let modified: MaybeDate = undefined
|
||||||
let published: MaybeDate = undefined
|
let published: MaybeDate = undefined
|
||||||
|
|
||||||
const fp = file.data.filePath!
|
const fp = file.data.relativePath!
|
||||||
const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp)
|
const fullFp = path.posix.join(ctx.argv.directory, fp)
|
||||||
for (const source of opts.priority) {
|
for (const source of opts.priority) {
|
||||||
if (source === "filesystem") {
|
if (source === "filesystem") {
|
||||||
const st = await fs.promises.stat(fullFp)
|
const st = await fs.promises.stat(fullFp)
|
||||||
@ -56,11 +56,11 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
|||||||
// Get a reference to the main git repo.
|
// Get a reference to the main git repo.
|
||||||
// It's either the same as the workdir,
|
// It's either the same as the workdir,
|
||||||
// or 1+ level higher in case of a submodule/subtree setup
|
// or 1+ level higher in case of a submodule/subtree setup
|
||||||
repo = Repository.discover(file.cwd)
|
repo = Repository.discover(ctx.argv.directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
|
modified ||= await repo.getFileLatestModifiedDateAsync(fullFp)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
|
@ -4,7 +4,7 @@ import { ProcessedContent } from "./vfile"
|
|||||||
import { QuartzComponent } from "../components/types"
|
import { QuartzComponent } from "../components/types"
|
||||||
import { FilePath } from "../util/path"
|
import { FilePath } from "../util/path"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
import DepGraph from "../depgraph"
|
import { VFile } from "vfile"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
transformers: QuartzTransformerPluginInstance[]
|
transformers: QuartzTransformerPluginInstance[]
|
||||||
@ -33,6 +33,12 @@ export type QuartzFilterPluginInstance = {
|
|||||||
shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
|
shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChangeEvent = {
|
||||||
|
type: "add" | "change" | "delete"
|
||||||
|
path: FilePath
|
||||||
|
file?: VFile
|
||||||
|
}
|
||||||
|
|
||||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
||||||
opts?: Options,
|
opts?: Options,
|
||||||
) => QuartzEmitterPluginInstance
|
) => QuartzEmitterPluginInstance
|
||||||
@ -43,16 +49,17 @@ export type QuartzEmitterPluginInstance = {
|
|||||||
content: ProcessedContent[],
|
content: ProcessedContent[],
|
||||||
resources: StaticResources,
|
resources: StaticResources,
|
||||||
): Promise<FilePath[]> | AsyncGenerator<FilePath>
|
): Promise<FilePath[]> | AsyncGenerator<FilePath>
|
||||||
|
partialEmit?(
|
||||||
|
ctx: BuildCtx,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
changeEvents: ChangeEvent[],
|
||||||
|
): Promise<FilePath[]> | AsyncGenerator<FilePath>
|
||||||
/**
|
/**
|
||||||
* Returns the components (if any) that are used in rendering the page.
|
* Returns the components (if any) that are used in rendering the page.
|
||||||
* This helps Quartz optimize the page by only including necessary resources
|
* This helps Quartz optimize the page by only including necessary resources
|
||||||
* for components that are actually used.
|
* for components that are actually used.
|
||||||
*/
|
*/
|
||||||
getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
|
getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
|
||||||
getDependencyGraph?(
|
|
||||||
ctx: BuildCtx,
|
|
||||||
content: ProcessedContent[],
|
|
||||||
resources: StaticResources,
|
|
||||||
): Promise<DepGraph<FilePath>>
|
|
||||||
externalResources?: ExternalResourcesFn
|
externalResources?: ExternalResourcesFn
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
|||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
const log = new QuartzLogger(ctx.argv.verbose)
|
const log = new QuartzLogger(ctx.argv.verbose)
|
||||||
|
|
||||||
log.start(`Emitting output files`)
|
log.start(`Emitting files`)
|
||||||
|
|
||||||
let emittedFiles = 0
|
let emittedFiles = 0
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||||
@ -26,7 +26,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
|||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
} else {
|
} else {
|
||||||
log.updateText(`Emitting output files: ${emitter.name} -> ${chalk.gray(file)}`)
|
log.updateText(`${emitter.name} -> ${chalk.gray(file)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -36,7 +36,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
|||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
} else {
|
} else {
|
||||||
log.updateText(`Emitting output files: ${emitter.name} -> ${chalk.gray(file)}`)
|
log.updateText(`${emitter.name} -> ${chalk.gray(file)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@ import path from "path"
|
|||||||
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
||||||
import { QuartzLogger } from "../util/log"
|
import { QuartzLogger } from "../util/log"
|
||||||
import { trace } from "../util/trace"
|
import { trace } from "../util/trace"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx, WorkerSerializableBuildCtx } from "../util/ctx"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
|
export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
|
||||||
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
|
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
|
||||||
@ -175,21 +176,42 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdPromises: WorkerPromise<[MarkdownContent[], FullSlug[]]>[] = []
|
const serializableCtx: WorkerSerializableBuildCtx = {
|
||||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
buildId: ctx.buildId,
|
||||||
mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, argv, chunk]))
|
argv: ctx.argv,
|
||||||
|
allSlugs: ctx.allSlugs,
|
||||||
|
allFiles: ctx.allFiles,
|
||||||
|
incremental: ctx.incremental,
|
||||||
}
|
}
|
||||||
const mdResults: [MarkdownContent[], FullSlug[]][] =
|
|
||||||
await WorkerPromise.all(mdPromises).catch(errorHandler)
|
|
||||||
|
|
||||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
const textToMarkdownPromises: WorkerPromise<MarkdownContent[]>[] = []
|
||||||
for (const [_, extraSlugs] of mdResults) {
|
let processedFiles = 0
|
||||||
ctx.allSlugs.push(...extraSlugs)
|
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||||
|
textToMarkdownPromises.push(pool.exec("parseMarkdown", [serializableCtx, chunk]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mdResults: Array<MarkdownContent[]> = await Promise.all(
|
||||||
|
textToMarkdownPromises.map(async (promise) => {
|
||||||
|
const result = await promise
|
||||||
|
processedFiles += result.length
|
||||||
|
log.updateText(`text->markdown ${chalk.gray(`${processedFiles}/${fps.length}`)}`)
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
).catch(errorHandler)
|
||||||
|
|
||||||
|
const markdownToHtmlPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||||
|
processedFiles = 0
|
||||||
for (const [mdChunk, _] of mdResults) {
|
for (const [mdChunk, _] of mdResults) {
|
||||||
childPromises.push(pool.exec("processHtml", [ctx.buildId, argv, mdChunk, ctx.allSlugs]))
|
markdownToHtmlPromises.push(pool.exec("processHtml", [serializableCtx, mdChunk]))
|
||||||
}
|
}
|
||||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch(errorHandler)
|
const results: ProcessedContent[][] = await Promise.all(
|
||||||
|
markdownToHtmlPromises.map(async (promise) => {
|
||||||
|
const result = await promise
|
||||||
|
processedFiles += result.length
|
||||||
|
log.updateText(`markdown->html ${chalk.gray(`${processedFiles}/${fps.length}`)}`)
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
).catch(errorHandler)
|
||||||
|
|
||||||
res = results.flat()
|
res = results.flat()
|
||||||
await pool.terminate()
|
await pool.terminate()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { QuartzConfig } from "../cfg"
|
import { QuartzConfig } from "../cfg"
|
||||||
import { FullSlug } from "./path"
|
import { FilePath, FullSlug } from "./path"
|
||||||
|
|
||||||
export interface Argv {
|
export interface Argv {
|
||||||
directory: string
|
directory: string
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
output: string
|
output: string
|
||||||
serve: boolean
|
serve: boolean
|
||||||
fastRebuild: boolean
|
watch: boolean
|
||||||
port: number
|
port: number
|
||||||
wsPort: number
|
wsPort: number
|
||||||
remoteDevHost?: string
|
remoteDevHost?: string
|
||||||
@ -18,4 +18,8 @@ export interface BuildCtx {
|
|||||||
argv: Argv
|
argv: Argv
|
||||||
cfg: QuartzConfig
|
cfg: QuartzConfig
|
||||||
allSlugs: FullSlug[]
|
allSlugs: FullSlug[]
|
||||||
|
allFiles: FilePath[]
|
||||||
|
incremental: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerSerializableBuildCtx = Omit<BuildCtx, "cfg">
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import truncate from "ansi-truncate"
|
||||||
import readline from "readline"
|
import readline from "readline"
|
||||||
|
|
||||||
export class QuartzLogger {
|
export class QuartzLogger {
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
private spinnerInterval: NodeJS.Timeout | undefined
|
private spinnerInterval: NodeJS.Timeout | undefined
|
||||||
private spinnerText: string = ""
|
private spinnerText: string = ""
|
||||||
|
private updateSuffix: string = ""
|
||||||
private spinnerIndex: number = 0
|
private spinnerIndex: number = 0
|
||||||
private readonly spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
private readonly spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ export class QuartzLogger {
|
|||||||
|
|
||||||
start(text: string) {
|
start(text: string) {
|
||||||
this.spinnerText = text
|
this.spinnerText = text
|
||||||
|
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
console.log(text)
|
console.log(text)
|
||||||
} else {
|
} else {
|
||||||
@ -20,14 +23,22 @@ export class QuartzLogger {
|
|||||||
this.spinnerInterval = setInterval(() => {
|
this.spinnerInterval = setInterval(() => {
|
||||||
readline.clearLine(process.stdout, 0)
|
readline.clearLine(process.stdout, 0)
|
||||||
readline.cursorTo(process.stdout, 0)
|
readline.cursorTo(process.stdout, 0)
|
||||||
process.stdout.write(`${this.spinnerChars[this.spinnerIndex]} ${this.spinnerText}`)
|
|
||||||
|
const columns = process.stdout.columns || 80
|
||||||
|
let output = `${this.spinnerChars[this.spinnerIndex]} ${this.spinnerText}`
|
||||||
|
if (this.updateSuffix) {
|
||||||
|
output += `: ${this.updateSuffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncated = truncate(output, columns)
|
||||||
|
process.stdout.write(truncated)
|
||||||
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length
|
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length
|
||||||
}, 20)
|
}, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateText(text: string) {
|
updateText(text: string) {
|
||||||
this.spinnerText = text
|
this.updateSuffix = text
|
||||||
}
|
}
|
||||||
|
|
||||||
end(text?: string) {
|
end(text?: string) {
|
||||||
|
@ -260,7 +260,7 @@ export function endsWith(s: string, suffix: string): boolean {
|
|||||||
return s === suffix || s.endsWith("/" + suffix)
|
return s === suffix || s.endsWith("/" + suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimSuffix(s: string, suffix: string): string {
|
export function trimSuffix(s: string, suffix: string): string {
|
||||||
if (endsWith(s, suffix)) {
|
if (endsWith(s, suffix)) {
|
||||||
s = s.slice(0, -suffix.length)
|
s = s.slice(0, -suffix.length)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import sourceMapSupport from "source-map-support"
|
import sourceMapSupport from "source-map-support"
|
||||||
sourceMapSupport.install(options)
|
sourceMapSupport.install(options)
|
||||||
import cfg from "../quartz.config"
|
import cfg from "../quartz.config"
|
||||||
import { Argv, BuildCtx } from "./util/ctx"
|
import { Argv, BuildCtx, WorkerSerializableBuildCtx } from "./util/ctx"
|
||||||
import { FilePath, FullSlug } from "./util/path"
|
import { FilePath, FullSlug } from "./util/path"
|
||||||
import {
|
import {
|
||||||
createFileParser,
|
createFileParser,
|
||||||
@ -14,35 +14,24 @@ import { MarkdownContent, ProcessedContent } from "./plugins/vfile"
|
|||||||
|
|
||||||
// only called from worker thread
|
// only called from worker thread
|
||||||
export async function parseMarkdown(
|
export async function parseMarkdown(
|
||||||
buildId: string,
|
partialCtx: WorkerSerializableBuildCtx,
|
||||||
argv: Argv,
|
|
||||||
fps: FilePath[],
|
fps: FilePath[],
|
||||||
): Promise<[MarkdownContent[], FullSlug[]]> {
|
): Promise<MarkdownContent[]> {
|
||||||
// this is a hack
|
|
||||||
// we assume markdown parsers can add to `allSlugs`,
|
|
||||||
// but don't actually use them
|
|
||||||
const allSlugs: FullSlug[] = []
|
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
buildId,
|
...partialCtx,
|
||||||
cfg,
|
cfg,
|
||||||
argv,
|
|
||||||
allSlugs,
|
|
||||||
}
|
}
|
||||||
return [await createFileParser(ctx, fps)(createMdProcessor(ctx)), allSlugs]
|
return await createFileParser(ctx, fps)(createMdProcessor(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// only called from worker thread
|
// only called from worker thread
|
||||||
export function processHtml(
|
export function processHtml(
|
||||||
buildId: string,
|
partialCtx: WorkerSerializableBuildCtx,
|
||||||
argv: Argv,
|
|
||||||
mds: MarkdownContent[],
|
mds: MarkdownContent[],
|
||||||
allSlugs: FullSlug[],
|
|
||||||
): Promise<ProcessedContent[]> {
|
): Promise<ProcessedContent[]> {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
buildId,
|
...partialCtx,
|
||||||
cfg,
|
cfg,
|
||||||
argv,
|
|
||||||
allSlugs,
|
|
||||||
}
|
}
|
||||||
return createMarkdownParser(ctx, mds)(createHtmlProcessor(ctx))
|
return createMarkdownParser(ctx, mds)(createHtmlProcessor(ctx))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user