various css fixes, fix new image loading bug when previewing, path docs

This commit is contained in:
Jacky Zhao 2023-08-07 21:41:18 -07:00
parent d02af6a8ae
commit 527ce6546e
7 changed files with 71 additions and 19 deletions

View file

@ -12,17 +12,17 @@ This question is best answered by tracing what happens when a user (you!) runs `
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. 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: 3. `bootstrap-cli.mjs` is responsible for a few things:
1. Parsing the command-line arguments using [yargs](http://yargs.js.org/). 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: 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). 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. 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. 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: 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. 1. Clean the output directory.
2. Recursively glob all files in the `content` folder, respecting the `.gitignore`. 2. Recursively glob all files in the `content` folder, respecting the `.gitignore`.
3. Parse the Markdown files. 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]]. 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: 3. Parsing has three steps:
1. Read the file into a [vfile](https://github.com/vfile/vfile). 1. Read the file into a [vfile](https://github.com/vfile/vfile).

View file

@ -0,0 +1,45 @@
---
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
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -6,6 +6,7 @@ As you already have Quartz locally, you don't need to fork or clone it again. Si
```bash ```bash
git checkout v4-alpha git checkout v4-alpha
git pull upstream v4-alpha
npm i npm i
npx quartz create npx quartz create
``` ```

View file

@ -23,7 +23,7 @@ import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter" import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit" import { emitContent } from "./processors/emit"
import cfg from "../quartz.config" import cfg from "../quartz.config"
import { FilePath, joinSegments, slugifyFilePath } from "./path" import { FilePath, ServerSlug, joinSegments, slugifyFilePath } from "./path"
import chokidar from "chokidar" import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile" import { ProcessedContent } from "./plugins/vfile"
import { Argv, BuildCtx } from "./ctx" import { Argv, BuildCtx } from "./ctx"
@ -91,6 +91,7 @@ async function startServing(
contentMap.set(vfile.data.filePath!, content) contentMap.set(vfile.data.filePath!, content)
} }
const initialSlugs = ctx.allSlugs
let timeoutId: ReturnType<typeof setTimeout> | null = null let timeoutId: ReturnType<typeof setTimeout> | null = null
let toRebuild: Set<FilePath> = new Set() let toRebuild: Set<FilePath> = new Set()
let toRemove: Set<FilePath> = new Set() let toRemove: Set<FilePath> = new Set()
@ -102,20 +103,19 @@ async function startServing(
} }
// dont bother rebuilding for non-content files, just track and refresh // 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 (path.extname(fp) !== ".md") {
fp = toPosixPath(fp)
const filePath = joinSegments(argv.directory, fp) as FilePath
if (action === "add" || action === "change") { if (action === "add" || action === "change") {
trackedAssets.add(filePath) trackedAssets.add(filePath)
} else if (action === "delete") { } else if (action === "delete") {
trackedAssets.add(filePath) trackedAssets.delete(filePath)
} }
clientRefresh() clientRefresh()
return return
} }
fp = toPosixPath(fp)
const filePath = joinSegments(argv.directory, fp) as FilePath
if (action === "add" || action === "change") { if (action === "add" || action === "change") {
toRebuild.add(filePath) toRebuild.add(filePath)
} else if (action === "delete") { } else if (action === "delete") {
@ -133,10 +133,12 @@ async function startServing(
try { try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
ctx.allSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] const trackedSlugs =
.filter((fp) => !toRemove.has(fp)) [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)) .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) const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) { for (const content of parsedContent) {
const [_tree, vfile] = content const [_tree, vfile] = content

View file

@ -413,12 +413,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
js.push({ js.push({
script: ` script: `
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; 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 () => { document.addEventListener('nav', async () => {
const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' await mermaid.run({
mermaid.initialize({ querySelector: '.mermaid'
securityLevel: 'loose', })
theme: darkMode ? 'dark' : 'default'
});
}); });
`, `,
loadTime: "afterDOMReady", loadTime: "afterDOMReady",

View file

@ -7,7 +7,7 @@ html {
scroll-behavior: smooth; scroll-behavior: smooth;
-webkit-text-size-adjust: none; -webkit-text-size-adjust: none;
text-size-adjust: none; text-size-adjust: none;
overflow-x: none; overflow-x: hidden;
width: 100vw; width: 100vw;
} }
@ -311,10 +311,10 @@ pre {
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
border: 1px solid var(--lightgray); border: 1px solid var(--lightgray);
position: relative;
&:has(> code.mermaid) { &:has(> code.mermaid) {
border: none; border: none;
position: relative;
} }
& > code { & > code {