From 92679f79d70fc0c7266e184fcbee59e02aaa4554 Mon Sep 17 00:00:00 2001 From: Reza Yousefpour Date: Mon, 7 Apr 2025 13:27:13 +0330 Subject: [PATCH] RTL --- .github/workflows/deploy.yml | 45 +++++ CNAME | 1 + docs/images/quartz layout.png | Bin 0 -> 66230 bytes quartz.config.ts | 4 +- quartz/components/ExplorerNode.tsx | 242 ++++++++++++++++++++++++ quartz/depgraph.test.ts | 118 ++++++++++++ quartz/depgraph.ts | 228 ++++++++++++++++++++++ quartz/plugins/emitters/contentIndex.ts | 185 ++++++++++++++++++ 8 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 CNAME create mode 100644 docs/images/quartz layout.png create mode 100644 quartz/components/ExplorerNode.tsx create mode 100644 quartz/depgraph.test.ts create mode 100644 quartz/depgraph.ts create mode 100644 quartz/plugins/emitters/contentIndex.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..d11b3976d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,45 @@ +name: Deploy Quartz site to GitHub Pages + +on: + push: + branches: + - v4 + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for git info + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Dependencies + run: npm ci + - name: Build Quartz + run: npx quartz build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: public + + deploy: + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..602b46608 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +systophia.ir diff --git a/docs/images/quartz layout.png b/docs/images/quartz layout.png new file mode 100644 index 0000000000000000000000000000000000000000..71ef3ac71edf91b81fc5cb7a051abf10e25dcd73 GIT binary patch literal 66230 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 number + filterFn: (node: FileNode) => boolean + mapFn: (node: FileNode) => void + order: OrderEntries[] +} + +type DataWrapper = { + file: QuartzPluginData + path: string[] +} + +export type FolderState = { + path: string + collapsed: boolean +} + +function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined { + if (!fp) { + return undefined + } + + return fp.split("/").at(idx) +} + +// Structure to add all files into a tree +export class FileNode { + children: Array + name: string // this is the slug segment + displayName: string + file: QuartzPluginData | null + depth: number + + constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) { + this.children = [] + this.name = slugSegment + this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment + this.file = file ? clone(file) : null + this.depth = depth ?? 0 + } + + private insert(fileData: DataWrapper) { + if (fileData.path.length === 0) { + return + } + + const nextSegment = fileData.path[0] + + // base case, insert here + if (fileData.path.length === 1) { + if (nextSegment === "") { + // index case (we are the root and we just found index.md), set our data appropriately + const title = fileData.file.frontmatter?.title + if (title && title !== "index") { + this.displayName = title + } + } else { + // direct child + this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1)) + } + + return + } + + // find the right child to insert into + fileData.path = fileData.path.splice(1) + const child = this.children.find((c) => c.name === nextSegment) + if (child) { + child.insert(fileData) + return + } + + const newChild = new FileNode( + nextSegment, + getPathSegment(fileData.file.relativePath, this.depth), + undefined, + this.depth + 1, + ) + newChild.insert(fileData) + this.children.push(newChild) + } + + // Add new file to tree + add(file: QuartzPluginData) { + this.insert({ file: file, path: simplifySlug(file.slug!).split("/") }) + } + + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.filter()`, but modifies tree in place + * @param filterFn function to filter tree with + */ + filter(filterFn: (node: FileNode) => boolean) { + this.children = this.children.filter(filterFn) + this.children.forEach((child) => child.filter(filterFn)) + } + + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place + * @param mapFn function to use for mapping over tree + */ + map(mapFn: (node: FileNode) => void) { + mapFn(this) + this.children.forEach((child) => child.map(mapFn)) + } + + /** + * Get folder representation with state of tree. + * Intended to only be called on root node before changes to the tree are made + * @param collapsed default state of folders (collapsed by default or not) + * @returns array containing folder state for tree + */ + getFolderPaths(collapsed: boolean): FolderState[] { + const folderPaths: FolderState[] = [] + + const traverse = (node: FileNode, currentPath: string) => { + if (!node.file) { + const folderPath = joinSegments(currentPath, node.name) + if (folderPath !== "") { + folderPaths.push({ path: folderPath, collapsed }) + } + + node.children.forEach((child) => traverse(child, folderPath)) + } + } + + traverse(this, "") + return folderPaths + } + + // Sort order: folders first, then files. Sort folders and files alphabetically + /** + * Sorts tree according to sort/compare function + * @param sortFn compare function used for `.sort()`, also used recursively for children + */ + sort(sortFn: (a: FileNode, b: FileNode) => number) { + this.children = this.children.sort(sortFn) + this.children.forEach((e) => e.sort(sortFn)) + } +} + +type ExplorerNodeProps = { + node: FileNode + opts: Options + fileData: QuartzPluginData + fullPath?: string +} + +export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) { + // Get options + const folderBehavior = opts.folderClickBehavior + const isDefaultOpen = opts.folderDefaultState === "open" + + // Calculate current folderPath + const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : "" + const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/" + + return ( + <> + {node.file ? ( + // Single file node +
  • + + {node.displayName} + +
  • + ) : ( +
  • + {node.name !== "" && ( + // Node with entire folder + // Render svg button + folder name, then children + + + )} + {/* Recursively render children of folder */} +
    +
      + {node.children.map((childNode, i) => ( + + ))} +
    +
    +
  • + )} + + ) +} diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts new file mode 100644 index 000000000..062f13e35 --- /dev/null +++ b/quartz/depgraph.test.ts @@ -0,0 +1,118 @@ +import test, { describe } from "node:test" +import DepGraph from "./depgraph" +import assert from "node:assert" + +describe("DepGraph", () => { + test("getLeafNodes", () => { + const graph = new DepGraph() + 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() + 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() + 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() + 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 + const graph = new DepGraph() + 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() + 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() + graph.addEdge("A.md", "B.md") + + // Add a new file C.md that transcludes B.md + // B.md -> C.md + const other = new DepGraph() + 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) + }) + }) +}) diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts new file mode 100644 index 000000000..3d048cd83 --- /dev/null +++ b/quartz/depgraph.ts @@ -0,0 +1,228 @@ +export default class DepGraph { + // node: incoming and outgoing edges + _graph = new Map; outgoing: Set }>() + + 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): 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, 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 { + 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 ---^ + // and the node is B, this function returns [C] + getLeafNodes(node: T): Set { + let stack: T[] = [node] + let visited = new Set() + let leafNodes = new Set() + + // 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 { + const leafNodes = this.getLeafNodes(node) + let visited = new Set() + let upstreamNodes = new Set() + + // 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 + } +} diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts new file mode 100644 index 000000000..c0fef86d2 --- /dev/null +++ b/quartz/plugins/emitters/contentIndex.ts @@ -0,0 +1,185 @@ +import { Root } from "hast" +import { GlobalConfiguration } from "../../cfg" +import { getDate } from "../../components/Date" +import { escapeHTML } from "../../util/escape" +import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import { toHtml } from "hast-util-to-html" +import { write } from "./helpers" +import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" + +export type ContentIndex = Map +export type ContentDetails = { + title: string + links: SimpleSlug[] + tags: string[] + content: string + richContent?: string + date?: Date + description?: string +} + +interface Options { + enableSiteMap: boolean + enableRSS: boolean + rssLimit?: number + rssFullHtml: boolean + includeEmptyFiles: boolean +} + +const defaultOptions: Options = { + enableSiteMap: true, + enableRSS: true, + rssLimit: 10, + rssFullHtml: false, + includeEmptyFiles: true, +} + +function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { + const base = cfg.baseUrl ?? "" + const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` + https://${joinSegments(base, encodeURI(slug))} + ${content.date && `${content.date.toISOString()}`} + ` + const urls = Array.from(idx) + .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) + .join("") + return `${urls}` +} + +function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string { + const base = cfg.baseUrl ?? "" + + const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` + ${escapeHTML(content.title)} + https://${joinSegments(base, encodeURI(slug))} + https://${joinSegments(base, encodeURI(slug))} + ${content.richContent ?? content.description} + ${content.date?.toUTCString()} + ` + + const items = Array.from(idx) + .sort(([_, f1], [__, f2]) => { + if (f1.date && f2.date) { + return f2.date.getTime() - f1.date.getTime() + } else if (f1.date && !f2.date) { + return -1 + } else if (!f1.date && f2.date) { + return 1 + } + + return f1.title.localeCompare(f2.title) + }) + .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) + .slice(0, limit ?? idx.size) + .join("") + + return ` + + + ${escapeHTML(cfg.pageTitle)} + https://${base} + ${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML( + cfg.pageTitle, + )} + Quartz -- quartz.jzhao.xyz + ${items} + + ` +} + +export const ContentIndex: QuartzEmitterPlugin> = (opts) => { + opts = { ...defaultOptions, ...opts } + return { + name: "ContentIndex", + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + 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 emitted: FilePath[] = [] + const linkIndex: ContentIndex = new Map() + for (const [tree, file] of content) { + const slug = file.data.slug! + const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() + if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { + linkIndex.set(slug, { + title: file.data.frontmatter?.title!, + links: file.data.links ?? [], + tags: file.data.frontmatter?.tags ?? [], + content: file.data.text ?? "", + richContent: opts?.rssFullHtml + ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) + : undefined, + date: date, + description: file.data.description ?? "", + }) + } + } + + if (opts?.enableSiteMap) { + emitted.push( + await write({ + ctx, + content: generateSiteMap(cfg, linkIndex), + slug: "sitemap" as FullSlug, + ext: ".xml", + }), + ) + } + + if (opts?.enableRSS) { + emitted.push( + await write({ + ctx, + content: generateRSSFeed(cfg, linkIndex, opts.rssLimit), + slug: "index" as FullSlug, + ext: ".xml", + }), + ) + } + + const fp = joinSegments("static", "contentIndex") as FullSlug + const simplifiedIndex = Object.fromEntries( + Array.from(linkIndex).map(([slug, content]) => { + // remove description and from content index as nothing downstream + // actually uses it. we only keep it in the index as we need it + // for the RSS feed + delete content.description + delete content.date + return [slug, content] + }), + ) + + emitted.push( + await write({ + ctx, + content: JSON.stringify(simplifiedIndex), + slug: fp, + ext: ".json", + }), + ) + + return emitted + }, + getQuartzComponents: () => [], + } +}