various css fixes, fix new image loading bug when previewing, path docs
This commit is contained in:
		
							parent
							
								
									d02af6a8ae
								
							
						
					
					
						commit
						527ce6546e
					
				| @ -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. | ||||
| 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). | ||||
|  | ||||
| @ -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 | ||||
| git checkout v4-alpha | ||||
| git pull upstream v4-alpha | ||||
| npm i | ||||
| npx quartz create | ||||
| ``` | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { parseMarkdown } from "./processors/parse" | ||||
| 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" | ||||
| @ -91,6 +91,7 @@ async function startServing( | ||||
|     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() | ||||
| @ -102,20 +103,19 @@ async function startServing( | ||||
|     } | ||||
| 
 | ||||
|     // 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") { | ||||
| @ -133,10 +133,12 @@ async function startServing( | ||||
|       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 | ||||
|  | ||||
| @ -413,12 +413,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | ||||
|         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", | ||||
|  | ||||
| @ -7,7 +7,7 @@ html { | ||||
|   scroll-behavior: smooth; | ||||
|   -webkit-text-size-adjust: none; | ||||
|   text-size-adjust: none; | ||||
|   overflow-x: none; | ||||
|   overflow-x: hidden; | ||||
|   width: 100vw; | ||||
| } | ||||
| 
 | ||||
| @ -311,10 +311,10 @@ pre { | ||||
|   border-radius: 5px; | ||||
|   overflow-x: auto; | ||||
|   border: 1px solid var(--lightgray); | ||||
|   position: relative; | ||||
| 
 | ||||
|   &:has(> code.mermaid) { | ||||
|     border: none; | ||||
|     position: relative; | ||||
|   } | ||||
| 
 | ||||
|   & > code { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user