diff --git a/quartz/build.ts b/quartz/build.ts index 3d95f315f4bf5..d72b8ddf4a934 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -185,9 +185,14 @@ async function partialRebuildFromEntrypoint( const emitterGraph = (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null - // emmiter may not define a dependency graph. nothing to update if so if (emitterGraph) { - dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + 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 @@ -224,7 +229,6 @@ async function partialRebuildFromEntrypoint( // EMIT perf.addEvent("rebuild") let emittedFiles = 0 - const destinationsToDelete = new Set() for (const emitter of cfg.plugins.emitters) { const depGraph = dependencies[emitter.name] @@ -264,11 +268,6 @@ async function partialRebuildFromEntrypoint( // and supply [a.md, b.md] to the emitter const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] - if (action === "delete" && upstreams.length === 1) { - // if there's only one upstream, the destination is solely dependent on this file - destinationsToDelete.add(upstreams[0]) - } - const upstreamContent = upstreams // filter out non-markdown files .filter((file) => contentMap.has(file)) @@ -291,14 +290,24 @@ async function partialRebuildFromEntrypoint( console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) // CLEANUP - // delete files that are solely dependent on this file - await rimraf([...destinationsToDelete]) + const destinationsToDelete = new Set() for (const file of toRemove) { // remove from cache contentMap.delete(file) - // remove the node from dependency graphs - Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(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]) toRemove.clear() release() diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index d0346b05b0c26..972d3c638819c 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -47,8 +47,8 @@ async function mouseEnterHandler( } if (!response) return - const contentType = response.headers.get("Content-Type") - const contentTypeCategory = contentType?.split("/")[0] ?? "text" + const [contentType] = response.headers.get("Content-Type")!.split(";") + const [contentTypeCategory, typeInfo] = contentType.split("/") const popoverElement = document.createElement("div") popoverElement.classList.add("popover") @@ -56,19 +56,27 @@ async function mouseEnterHandler( popoverInner.classList.add("popover-inner") popoverElement.appendChild(popoverInner) - popoverInner.dataset.contentType = contentTypeCategory + popoverInner.dataset.contentType = contentType ?? undefined switch (contentTypeCategory) { case "image": const img = document.createElement("img") - - response.blob().then((blob) => { - img.src = URL.createObjectURL(blob) - }) + img.src = targetUrl.toString() img.alt = targetUrl.pathname popoverInner.appendChild(img) break + case "application": + switch (typeInfo) { + case "pdf": + const pdf = document.createElement("iframe") + pdf.src = targetUrl.toString() + popoverInner.appendChild(pdf) + break + default: + break + } + break default: const contents = await response.text() const html = p.parseFromString(contents, "text/html") diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 141b89ddf0f9f..b1694f97cb787 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -38,14 +38,25 @@ white-space: normal; } - & > .popover-inner[data-content-type="image"] { - padding: 0; - max-height: 100%; + & > .popover-inner[data-content-type] { + &[data-content-type*="pdf"], + &[data-content-type*="image"] { + padding: 0; + max-height: 100%; + } + + &[data-content-type*="image"] { + img { + margin: 0; + border-radius: 0; + display: block; + } + } - img { - margin: 0; - border-radius: 0; - display: block; + &[data-content-type*="pdf"] { + iframe { + width: 100%; + } } } diff --git a/quartz/components/types.ts b/quartz/components/types.ts index f3782b8098b94..d238bff2370af 100644 --- a/quartz/components/types.ts +++ b/quartz/components/types.ts @@ -3,8 +3,10 @@ import { StaticResources } from "../util/resources" import { QuartzPluginData } from "../plugins/vfile" import { GlobalConfiguration } from "../cfg" import { Node } from "hast" +import { BuildCtx } from "../util/ctx" export type QuartzComponentProps = { + ctx: BuildCtx externalResources: StaticResources fileData: QuartzPluginData cfg: GlobalConfiguration diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts index 43eb4024ff3e1..062f13e35c00b 100644 --- a/quartz/depgraph.test.ts +++ b/quartz/depgraph.test.ts @@ -39,6 +39,28 @@ describe("DepGraph", () => { }) }) + describe("mergeGraph", () => { + test("merges two graphs", () => { + const graph = new DepGraph() + graph.addEdge("A.md", "A.html") + + const other = new DepGraph() + 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 diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts index 1efad077054ea..3d048cd83250d 100644 --- a/quartz/depgraph.ts +++ b/quartz/depgraph.ts @@ -39,12 +39,26 @@ export default class DepGraph { } } + // 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)) } @@ -92,6 +106,15 @@ export default class DepGraph { // DEPENDENCY ALGORITHMS + // Add all nodes and edges from other graph to this graph + mergeGraph(other: DepGraph): 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 @@ -112,6 +135,24 @@ export default class DepGraph { }) } + // 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 { + let orphanNodes = new Set() + + 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 ---^ diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx index f9d7a8620f5bd..e4605cfcd2f3e 100644 --- a/quartz/plugins/emitters/404.tsx +++ b/quartz/plugins/emitters/404.tsx @@ -46,6 +46,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => { frontmatter: { title: notFound, tags: [] }, }) const componentData: QuartzComponentProps = { + ctx, fileData: vfile.data, externalResources, cfg, diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index 904a8a8cae21d..f4938026e779f 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -97,6 +97,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp const externalResources = pageResources(pathToRoot(slug), resources) const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index bf69d298782e1..d892b282a1c7b 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -95,6 +95,7 @@ export const FolderPage: QuartzEmitterPlugin> = (userOpt const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = folderDescriptions[folder] const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 3eb6975f71b21..6f65ae477db8c 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -99,6 +99,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = tagDescriptions[tag] const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 6aad230523ed0..fab7cf894740d 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -412,12 +412,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin children: [ { type: "text", - value: useDefaultTitle - ? capitalize( - i18n(cfg.locale).components.callout[calloutType as ValidCallout] ?? - calloutType, - ) - : titleContent + " ", + value: useDefaultTitle ? capitalize(typeString) : titleContent + " ", }, ...restOfTitle, ],