various css fixes, fix new image loading bug when previewing, path docs
| | |
| | | 2. This file has a [shebang](<https://en.wikipedia.org/wiki/Shebang_(Unix)>) line at the top which tells npm to execute it using Node. |
| | | 3. `bootstrap-cli.mjs` is responsible for a few things: |
| | | 1. Parsing the command-line arguments using [yargs](http://yargs.js.org/). |
| | | 2. Transpiling and bundling the rest of Quartz (which is in Typescript) to regular JavaScript using [esbuild](https://esbuild.github.io/). The `esbuild` configuration here is slightly special as it also handles `.scss` file imports using [esbuild-sass-plugin v2](https://www.npmjs.com/package/esbuild-sass-plugin). Additionally, we bundle 'inline' scripts (any `.inline.ts` file) that components can run client-side using a custom plugin that runs another instance of `esbuild` that bundles for browser instead of `node`. Both of these are imported as plain text. |
| | | 2. Transpiling and bundling the rest of Quartz (which is in Typescript) to regular JavaScript using [esbuild](https://esbuild.github.io/). The `esbuild` configuration here is slightly special as it also handles `.scss` file imports using [esbuild-sass-plugin v2](https://www.npmjs.com/package/esbuild-sass-plugin). Additionally, we bundle 'inline' client-side scripts (any `.inline.ts` file) that components declare usiong a custom `esbuild` plugin that runs another instance of `esbuild` that bundles for the browser instead of `node`. Modules of both types are imported as plain text. |
| | | 3. Running the local preview server if `--serve` is set. This starts two servers: |
| | | 1. A WebSocket server on port 3001 to handle hot-reload signals. This tracks all inbound connections and sends a 'rebuild' message a server-side change is detected (either content or configuration). |
| | | 2. An HTTP file-server on a user defined port (normally 8080) to serve the actual website files. |
| | | 4. Again, if the local preview server is running, it also starts a file watcher to detect source-code changes (e.g. anything that is `.ts`, `.tsx`, `.scss`, or packager files). On a change, we _rebuild_ the module (step 2 above) using esbuild's [rebuild API](https://esbuild.github.io/api/#rebuild) which drastically reduces the build times. |
| | | 4. If the `--serve` flag is set, it also starts a file watcher to detect source-code changes (e.g. anything that is `.ts`, `.tsx`, `.scss`, or packager files). On a change, we rebuild the module (step 2 above) using esbuild's [rebuild API](https://esbuild.github.io/api/#rebuild) which drastically reduces the build times. |
| | | 5. After transpiling the main Quartz build module (`quartz/build.ts`), we write it to a cache file `.quartz-cache/transpiled-build.mjs` and then dynamically import this using `await import(cacheFile)`. However, we need to be pretty smart about how to bust Node's [import cache](https://github.com/nodejs/modules/issues/307) so we add a random query string to fake Node into thinking it's a new module. This does, however, cause memory leaks so we just hope that the user doesn't hot-reload their configuration too many times in a single session :)) (it leaks about ~350kB memory on each reload). After importing the module, we then invoke it, passing in the command line arguments we parsed earlier along with a callback function to signal the client to refresh. |
| | | 4. In `build.ts`, we start by installing source map support manually to account for the query string cache busting hack we introduced earlier. Then, we start processing content: |
| | | 1. Clean the output directory. |
| | | 2. Recursively glob all files in the `content` folder, respecting the `.gitignore`. |
| | | 3. Parse the Markdown files. |
| | | 1. Quartz detects the number of threads available and chooses to spawn worker threads if there are >128 pieces of content to parse (rough heuristic). If it needs to spawn workers, it will do another esbuild transpile of the worker script `quartz/worker.ts`. Then, a work-stealing [workerpool](https://www.npmjs.com/package/workerpool) is then created and 'chunks' of 128 files are assigned to workers. |
| | | 1. Quartz detects the number of threads available and chooses to spawn worker threads if there are >128 pieces of content to parse (rough heuristic). If it needs to spawn workers, it will invoke esbuild again to transpile the worker script `quartz/worker.ts`. Then, a work-stealing [workerpool](https://www.npmjs.com/package/workerpool) is then created and batches of 128 files are assigned to workers. |
| | | 2. Each worker (or just the main thread if there is no concurrency) creates a [unified](https://github.com/unifiedjs/unified) parser based off of the plugins defined in the [[configuration]]. |
| | | 3. Parsing has three steps: |
| | | 1. Read the file into a [vfile](https://github.com/vfile/vfile). |
| | |
| | | --- |
| | | title: Paths in Quartz |
| | | --- |
| | | |
| | | Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places. |
| | | |
| | | The current browser URL? Technically a path. A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path. |
| | | |
| | | It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it. |
| | | |
| | | Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.org/play#example/nominal-typing). |
| | | |
| | | ```typescript |
| | | // instead of |
| | | type ClientSlug = string |
| | | |
| | | // we do |
| | | type ClientSlug = string & { __brand: "client" } |
| | | |
| | | // that way, the following will fail typechecking |
| | | const slug: ClientSlug = "some random slug" |
| | | ``` |
| | | |
| | | While this prevents most typing mistakes *within* our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from *accidentally* mistaking a string for a client slug when we forcibly cast it. |
| | | |
| | | Thus, we still need to be careful when casting from a string to one of these nominal types in the 'entrypoints', illustrated with hexagon shapes in the diagram below. |
| | | |
| | | The following diagram draws the relationships between all the path sources, nominal path types, and what functions in `quartz/path.ts` convert between them. |
| | | |
| | | ```mermaid |
| | | graph LR |
| | | Browser{{Browser}} --> Window{{Window}} & LinkElement{{Link Element}} |
| | | Window --"getCanonicalSlug()"--> Canonical[Canonical Slug] |
| | | Window --"getClientSlug()"--> Client[Client Slug] |
| | | LinkElement --".href"--> Relative[Relative URL] |
| | | Client --"canonicalizeClient()"--> Canonical |
| | | Canonical --"pathToRoot()"--> Relative |
| | | Canonical --"resolveRelative()" --> Relative |
| | | MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links] |
| | | Links --"transformLink()"--> Relative |
| | | FilePath --"slugifyFilePath()"--> Server[Server Slug] |
| | | Server --> HTML["HTML File"] |
| | | Server --"canonicalizeServer()"--> Canonical |
| | | style Canonical stroke-width:4px |
| | | ``` |
| | |
| | | |
| | | ```bash |
| | | git checkout v4-alpha |
| | | git pull upstream v4-alpha |
| | | npm i |
| | | npx quartz create |
| | | ``` |
| | |
| | | import { filterContent } from "./processors/filter" |
| | | import { emitContent } from "./processors/emit" |
| | | import cfg from "../quartz.config" |
| | | import { FilePath, joinSegments, slugifyFilePath } from "./path" |
| | | import { FilePath, ServerSlug, joinSegments, slugifyFilePath } from "./path" |
| | | import chokidar from "chokidar" |
| | | import { ProcessedContent } from "./plugins/vfile" |
| | | import { Argv, BuildCtx } from "./ctx" |
| | |
| | | contentMap.set(vfile.data.filePath!, content) |
| | | } |
| | | |
| | | const initialSlugs = ctx.allSlugs |
| | | let timeoutId: ReturnType<typeof setTimeout> | null = null |
| | | let toRebuild: Set<FilePath> = new Set() |
| | | let toRemove: Set<FilePath> = new Set() |
| | |
| | | } |
| | | |
| | | // 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") { |
| | | fp = toPosixPath(fp) |
| | | const filePath = joinSegments(argv.directory, fp) as FilePath |
| | | if (action === "add" || action === "change") { |
| | | trackedAssets.add(filePath) |
| | | } else if (action === "delete") { |
| | | trackedAssets.add(filePath) |
| | | trackedAssets.delete(filePath) |
| | | } |
| | | clientRefresh() |
| | | return |
| | | } |
| | | |
| | | fp = toPosixPath(fp) |
| | | const filePath = joinSegments(argv.directory, fp) as FilePath |
| | | |
| | | if (action === "add" || action === "change") { |
| | | toRebuild.add(filePath) |
| | | } else if (action === "delete") { |
| | |
| | | try { |
| | | const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) |
| | | |
| | | ctx.allSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] |
| | | .filter((fp) => !toRemove.has(fp)) |
| | | .map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)) |
| | | 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])] |
| | | const parsedContent = await parseMarkdown(ctx, filesToRebuild) |
| | | for (const content of parsedContent) { |
| | | const [_tree, vfile] = content |
| | |
| | | js.push({ |
| | | script: ` |
| | | import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; |
| | | const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' |
| | | mermaid.initialize({ |
| | | startOnLoad: false, |
| | | securityLevel: 'loose', |
| | | theme: darkMode ? 'dark' : 'default' |
| | | }); |
| | | document.addEventListener('nav', async () => { |
| | | const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' |
| | | mermaid.initialize({ |
| | | securityLevel: 'loose', |
| | | theme: darkMode ? 'dark' : 'default' |
| | | }); |
| | | await mermaid.run({ |
| | | querySelector: '.mermaid' |
| | | }) |
| | | }); |
| | | `, |
| | | loadTime: "afterDOMReady", |
| | |
| | | scroll-behavior: smooth; |
| | | -webkit-text-size-adjust: none; |
| | | text-size-adjust: none; |
| | | overflow-x: none; |
| | | overflow-x: hidden; |
| | | width: 100vw; |
| | | } |
| | | |
| | |
| | | border-radius: 5px; |
| | | overflow-x: auto; |
| | | border: 1px solid var(--lightgray); |
| | | position: relative; |
| | | |
| | | &:has(> code.mermaid) { |
| | | border: none; |
| | | position: relative; |
| | | } |
| | | |
| | | & > code { |