various css fixes, fix new image loading bug when previewing, path docs
This commit is contained in:
parent
15be857598
commit
4e97695eee
@ -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).
|
||||||
|
@ -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 |
@ -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
|
||||||
```
|
```
|
||||||
|
@ -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
|
||||||
if (path.extname(fp) !== ".md") {
|
|
||||||
fp = toPosixPath(fp)
|
fp = toPosixPath(fp)
|
||||||
const filePath = joinSegments(argv.directory, fp) as FilePath
|
const filePath = joinSegments(argv.directory, fp) as FilePath
|
||||||
|
if (path.extname(fp) !== ".md") {
|
||||||
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 =
|
||||||
|
[...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
||||||
.filter((fp) => !toRemove.has(fp))
|
.filter((fp) => !toRemove.has(fp))
|
||||||
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
.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
|
||||||
|
@ -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';
|
||||||
document.addEventListener('nav', async () => {
|
|
||||||
const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
|
const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
securityLevel: 'loose',
|
securityLevel: 'loose',
|
||||||
theme: darkMode ? 'dark' : 'default'
|
theme: darkMode ? 'dark' : 'default'
|
||||||
});
|
});
|
||||||
|
document.addEventListener('nav', async () => {
|
||||||
|
await mermaid.run({
|
||||||
|
querySelector: '.mermaid'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user