finish path refactoring, add sourcemap + better trace support
This commit is contained in:
		
							parent
							
								
									906f91f8ee
								
							
						
					
					
						commit
						3ac6b42e16
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -2,5 +2,6 @@ | ||||
| .gitignore | ||||
| node_modules | ||||
| public | ||||
| tsconfig.tsbuildinfo | ||||
| .obsidian | ||||
| .quartz-cache | ||||
|  | ||||
| @ -40,7 +40,7 @@ This part of the configuration concerns anything that can affect the whole site. | ||||
| 		- `dark`: header text and icons | ||||
| 		- `secondary`: link colour, current [[graph view|graph]] node | ||||
| 		- `tertiary`: hover states and visited [[graph view|graph]] nodes | ||||
| 		- `highlight`: internal link background, highlighted text, highlighted [[syntax highlighting|lines of code]] | ||||
| 		- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]] | ||||
| 
 | ||||
| ## Plugins | ||||
| You can think of Quartz plugins as a series of transformations over content. | ||||
| @ -62,7 +62,7 @@ plugins: { | ||||
| By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz. | ||||
| 
 | ||||
| > [!note] | ||||
| > Note that each node is modified by every transformer *in order*. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins.  | ||||
| > Each node is modified by every transformer *in order*. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins.  | ||||
| 
 | ||||
| Additionally, plugins may also have their own configuration settings that you can pass in. For example, the [[Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax. | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,3 @@ | ||||
| --- | ||||
| tags: | ||||
| - plugins/transformer | ||||
| --- | ||||
| 
 | ||||
| Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time. | ||||
| 
 | ||||
| ## Formatting | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| --- | ||||
| title: Syntax Highlighting | ||||
| tags:  | ||||
| - plugins/transformer | ||||
| --- | ||||
| 
 | ||||
| Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting. | ||||
|  | ||||
| @ -0,0 +1,5 @@ | ||||
| --- | ||||
| title: "Table of Contents" | ||||
| tags: | ||||
| - component | ||||
| --- | ||||
| @ -1,5 +1,7 @@ | ||||
| 
 | ||||
| - fixes | ||||
| 	- changing `_index` files | ||||
| 	- typography | ||||
| - CLI     | ||||
|     - update | ||||
|     - push | ||||
| @ -29,4 +31,8 @@ | ||||
|     - mermaid styling: [https://mermaid.js.org/config/theming.html#theme-variables-reference-table](https://mermaid.js.org/config/theming.html#theme-variables-reference-table) | ||||
|         - [https://github.com/jackyzha0/quartz/issues/331](https://github.com/jackyzha0/quartz/issues/331) | ||||
|     - block links: [https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note](https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note) | ||||
|     - note/header/block transcludes: [https://help.obsidian.md/Linking+notes+and+files/Embedding+files](https://help.obsidian.md/Linking+notes+and+files/Embedding+files) | ||||
|     - note/header/block transcludes: [https://help.obsidian.md/Linking+notes+and+files/Embedding+files](https://help.obsidian.md/Linking+notes+and+files/Embedding+files) | ||||
| - parse all images in page | ||||
| 	- use this for page lists if applicable? | ||||
| - CV mode? | ||||
| 	- with print stylesheet | ||||
							
								
								
									
										953
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										953
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -2,7 +2,7 @@ | ||||
|   "name": "@jackyzha0/quartz", | ||||
|   "description": "🌱 publish your digital garden and notes as a website", | ||||
|   "private": true, | ||||
|   "version": "4.0.4", | ||||
|   "version": "4.0.5", | ||||
|   "type": "module", | ||||
|   "author": "jackyzha0 <j.zhao2k19@gmail.com>", | ||||
|   "license": "MIT", | ||||
| @ -48,6 +48,7 @@ | ||||
|     "plausible-tracker": "^0.3.8", | ||||
|     "preact": "^10.14.1", | ||||
|     "preact-render-to-string": "^6.0.3", | ||||
|     "pretty-bytes": "^6.1.0", | ||||
|     "pretty-time": "^1.1.0", | ||||
|     "reading-time": "^1.5.0", | ||||
|     "rehype-autolink-headings": "^6.1.1", | ||||
| @ -65,6 +66,7 @@ | ||||
|     "remark-smartypants": "^2.0.0", | ||||
|     "rimraf": "^5.0.1", | ||||
|     "serve-handler": "^6.1.5", | ||||
|     "source-map-support": "^0.5.21", | ||||
|     "to-vfile": "^7.2.4", | ||||
|     "unified": "^10.1.2", | ||||
|     "unist-util-visit": "^4.1.2", | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { sassPlugin } from 'esbuild-sass-plugin' | ||||
| import fs from 'fs' | ||||
| import { intro, isCancel, outro, select, text } from '@clack/prompts' | ||||
| import { rimraf } from 'rimraf' | ||||
| import prettyBytes from 'pretty-bytes' | ||||
| 
 | ||||
| const cacheFile = "./.quartz-cache/transpiled-build.mjs" | ||||
| const fp = "./quartz/build.ts" | ||||
| @ -133,7 +134,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. | ||||
| `)
 | ||||
|   }) | ||||
|   .command('build', 'Build Quartz into a bundle of static HTML files', BuildArgv, async (argv) => { | ||||
|     await esbuild.build({ | ||||
|     const result = await esbuild.build({ | ||||
|       entryPoints: [fp], | ||||
|       outfile: path.join("quartz", cacheFile), | ||||
|       bundle: true, | ||||
| @ -143,6 +144,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. | ||||
|       jsx: "automatic", | ||||
|       jsxImportSource: "preact", | ||||
|       packages: "external", | ||||
|       metafile: true, | ||||
|       sourcemap: true, | ||||
|       plugins: [ | ||||
|         sassPlugin({ | ||||
|           type: 'css-text', | ||||
| @ -186,6 +189,12 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. | ||||
|       process.exit(1) | ||||
|     }) | ||||
| 
 | ||||
|     if (argv.verbose) { | ||||
|       const outputFileName = 'quartz/.quartz-cache/transpiled-build.mjs' | ||||
|       const meta = result.metafile.outputs[outputFileName] | ||||
|       console.log(chalk.gray(`[debug] Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(meta.bytes)})`)) | ||||
|     } | ||||
| 
 | ||||
|     const { default: init } = await import(cacheFile) | ||||
|     init(argv, version) | ||||
|   }) | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import 'source-map-support/register.js' | ||||
| import path from "path" | ||||
| import { PerfTimer } from "./perf" | ||||
| import { rimraf } from "rimraf" | ||||
| @ -9,6 +10,7 @@ import { parseMarkdown } from "./processors/parse" | ||||
| import { filterContent } from "./processors/filter" | ||||
| import { emitContent } from "./processors/emit" | ||||
| import cfg from "../quartz.config" | ||||
| import { FilePath } from "./path" | ||||
| 
 | ||||
| interface Argv { | ||||
|   directory: string | ||||
| @ -46,7 +48,7 @@ export default async function buildQuartz(argv: Argv, version: string) { | ||||
|   }) | ||||
|   console.log(`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince('glob')}`) | ||||
| 
 | ||||
|   const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}`) | ||||
|   const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}` as FilePath) | ||||
|   const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose) | ||||
|   const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose) | ||||
|   await emitContent(argv.directory, output, cfg, filteredContent, argv.verbose) | ||||
|  | ||||
| @ -1,20 +1,19 @@ | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| import style from "./styles/backlinks.scss" | ||||
| import { relativeToRoot } from "../path" | ||||
| import { clientSideSlug } from "./scripts/util" | ||||
| import { canonicalizeServer, resolveRelative } from "../path" | ||||
| 
 | ||||
| function Backlinks({ fileData, allFiles }: QuartzComponentProps) { | ||||
|   const slug = fileData.slug! | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const backlinkFiles = allFiles.filter(file => file.links?.includes(slug)) | ||||
|   return <div class="backlinks"> | ||||
|     <h3>Backlinks</h3> | ||||
|     <ul class="overflow"> | ||||
|       {backlinkFiles.length > 0 ? | ||||
|         backlinkFiles.map(f => <li><a href={clientSideSlug(relativeToRoot(slug, f.slug!))} class="internal">{f.frontmatter?.title}</a></li>) | ||||
|         backlinkFiles.map(f => <li><a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">{f.frontmatter?.title}</a></li>) | ||||
|         : <li>No backlinks found</li>} | ||||
|     </ul> | ||||
|   </div>  | ||||
|   </div> | ||||
| } | ||||
| 
 | ||||
| Backlinks.css = style  | ||||
| Backlinks.css = style | ||||
| export default (() => Backlinks) satisfies QuartzComponentConstructor | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { toServerSlug, pathToRoot } from "../path" | ||||
| import { canonicalizeServer, pathToRoot } from "../path" | ||||
| import { JSResourceToScriptElement } from "../resources" | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| 
 | ||||
| export default (() => { | ||||
|   function Head({ fileData, externalResources }: QuartzComponentProps) { | ||||
|     const slug = toServerSlug(fileData.slug!) | ||||
|     const slug = canonicalizeServer(fileData.slug!) | ||||
|     const title = fileData.frontmatter?.title ?? "Untitled" | ||||
|     const description = fileData.description ?? "No description provided" | ||||
|     const { css, js } = externalResources | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import { relativeToRoot } from "../path" | ||||
| import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../path" | ||||
| import { QuartzPluginData } from "../plugins/vfile" | ||||
| import { Date } from "./Date" | ||||
| import { clientSideSlug } from "./scripts/util" | ||||
| import { QuartzComponentProps } from "./types" | ||||
| 
 | ||||
| function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number { | ||||
| @ -22,22 +21,23 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb | ||||
| } | ||||
| 
 | ||||
| export function PageList({ fileData, allFiles }: QuartzComponentProps) { | ||||
|   const slug = fileData.slug! | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   return <ul class="section-ul"> | ||||
|     {allFiles.sort(byDateAndAlphabetical).map(page => { | ||||
|       const title = page.frontmatter?.title | ||||
|       const pageSlug = page.slug! | ||||
|       const pageSlug = canonicalizeServer(page.slug!) | ||||
|       const tags = page.frontmatter?.tags ?? [] | ||||
| 
 | ||||
|       return <li class="section-li"> | ||||
|         <div class="section"> | ||||
|           {page.dates && <p class="meta"> | ||||
|             <Date date={page.dates.modified} /> | ||||
|           </p>} | ||||
|           <div class="desc"> | ||||
|             <h3><a href={clientSideSlug(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3> | ||||
|             <h3><a href={resolveRelative(slug, pageSlug)} class="internal">{title}</a></h3> | ||||
|           </div> | ||||
|           <ul class="tags"> | ||||
|             {tags.map(tag => <li><a class="internal" href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)} | ||||
|             {tags.map(tag => <li><a class="internal" href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}>#{tag}</a></li>)} | ||||
|           </ul> | ||||
|         </div> | ||||
|       </li> | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { pathToRoot } from "../path" | ||||
| import { canonicalizeServer, pathToRoot } from "../path" | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| 
 | ||||
| function PageTitle({ fileData, cfg }: QuartzComponentProps) { | ||||
|   const title = cfg?.pageTitle ?? "Untitled Quartz" | ||||
|   const slug = fileData.slug! | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const baseDir = pathToRoot(slug) | ||||
|   return <h1 class="page-title"><a href={baseDir}>{title}</a></h1> | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { pathToRoot } from "../path" | ||||
| import { canonicalizeServer, pathToRoot } from "../path" | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| import { slug as slugAnchor } from 'github-slugger' | ||||
| 
 | ||||
| function TagList({ fileData }: QuartzComponentProps) { | ||||
|   const tags = fileData.frontmatter?.tags | ||||
|   const slug = fileData.slug! | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const baseDir = pathToRoot(slug) | ||||
|   if (tags && tags.length > 0) { | ||||
|     return <ul class="tags">{tags.map(tag => { | ||||
|  | ||||
| @ -5,11 +5,11 @@ import path from "path" | ||||
| 
 | ||||
| import style from '../styles/listPage.scss' | ||||
| import { PageList } from "../PageList" | ||||
| import { toServerSlug } from "../../path" | ||||
| import { canonicalizeServer } from "../../path" | ||||
| 
 | ||||
| function FolderContent(props: QuartzComponentProps) { | ||||
|   const { tree, fileData, allFiles } = props | ||||
|   const folderSlug = toServerSlug(fileData.slug!) | ||||
|   const folderSlug = canonicalizeServer(fileData.slug!) | ||||
|   const allPagesInFolder = allFiles.filter(file => { | ||||
|     const fileSlug = file.slug ?? "" | ||||
|     const prefixed = fileSlug.startsWith(folderSlug) | ||||
| @ -23,7 +23,7 @@ function FolderContent(props: QuartzComponentProps) { | ||||
|     ...props, | ||||
|     allFiles: allPagesInFolder | ||||
|   } | ||||
| 
 | ||||
|    | ||||
|   // @ts-ignore
 | ||||
|   const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) | ||||
|   return <div class="popover-hint"> | ||||
|  | ||||
| @ -3,14 +3,14 @@ import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' | ||||
| import { toJsxRuntime } from "hast-util-to-jsx-runtime" | ||||
| import style from '../styles/listPage.scss' | ||||
| import { PageList } from "../PageList" | ||||
| import { toServerSlug } from "../../path" | ||||
| import { ServerSlug, canonicalizeServer } from "../../path" | ||||
| 
 | ||||
| function TagContent(props: QuartzComponentProps) { | ||||
|   const { tree, fileData, allFiles } = props | ||||
|   const slug = fileData.slug | ||||
| 
 | ||||
|   if (slug?.startsWith("tags/")) { | ||||
|     const tag = toServerSlug(slug.slice("tags/".length)) | ||||
|     const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug) | ||||
|     const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag)) | ||||
|     const listProps = { | ||||
|       ...props, | ||||
| @ -27,7 +27,7 @@ function TagContent(props: QuartzComponentProps) { | ||||
|       </div> | ||||
|     </div> | ||||
|   } else { | ||||
|     throw `Component "TagContent" tried to render a non-tag page: ${slug}` | ||||
|     throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { ContentDetails } from "../../plugins/emitters/contentIndex" | ||||
| import * as d3 from 'd3' | ||||
| import { registerEscapeHandler, clientSideRelativePath, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug } from "../../path" | ||||
| import { registerEscapeHandler, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path" | ||||
| 
 | ||||
| type NodeData = { | ||||
|   id: CanonicalSlug, | ||||
| @ -25,7 +25,7 @@ function addToVisited(slug: CanonicalSlug) { | ||||
|   localStorage.setItem(localStorageKey, JSON.stringify([...visited])) | ||||
| } | ||||
| 
 | ||||
| async function renderGraph(container: string, slug: string) { | ||||
| async function renderGraph(container: string, slug: CanonicalSlug) { | ||||
|   const visited = getVisited() | ||||
|   const graph = document.getElementById(container) | ||||
|   if (!graph) return | ||||
| @ -50,18 +50,17 @@ async function renderGraph(container: string, slug: string) { | ||||
|     const outgoing = details.links ?? [] | ||||
|     for (const dest of outgoing) { | ||||
|       if (src in data && dest in data) { | ||||
|         links.push({ source: src, target: dest }) | ||||
|         links.push({ source: src as CanonicalSlug, target: dest }) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const neighbourhood = new Set() | ||||
| 
 | ||||
|   const wl = [slug, "__SENTINEL"] | ||||
|   const neighbourhood = new Set<CanonicalSlug>() | ||||
|   const wl: (CanonicalSlug | "__SENTINEL")[] = [slug, "__SENTINEL"] | ||||
|   if (depth >= 0) { | ||||
|     while (depth >= 0 && wl.length > 0) { | ||||
|       // compute neighbours
 | ||||
|       const cur = wl.shift() | ||||
|       const cur = wl.shift()! | ||||
|       if (cur === "__SENTINEL") { | ||||
|         depth-- | ||||
|         wl.push("__SENTINEL") | ||||
| @ -73,11 +72,11 @@ async function renderGraph(container: string, slug: string) { | ||||
|       } | ||||
|     } | ||||
|   } else { | ||||
|     Object.keys(data).forEach(id => neighbourhood.add(id)) | ||||
|     Object.keys(data).forEach(id => neighbourhood.add(id as CanonicalSlug)) | ||||
|   } | ||||
| 
 | ||||
|   const graphData: { nodes: NodeData[], links: LinkData[] } = { | ||||
|     nodes: Object.keys(data).filter(id => neighbourhood.has(id)).map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })), | ||||
|     nodes: [...neighbourhood].map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })), | ||||
|     links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) | ||||
|   } | ||||
| 
 | ||||
| @ -168,12 +167,13 @@ async function renderGraph(container: string, slug: string) { | ||||
|     .attr("fill", color) | ||||
|     .style("cursor", "pointer") | ||||
|     .on("click", (_, d) => { | ||||
|       const targ = clientSideRelativePath(slug, d.id) | ||||
|       window.spaNavigate(new URL(targ)) | ||||
|       const targ = resolveRelative(slug, d.id) | ||||
|       window.spaNavigate(new URL(targ, getClientSlug(window))) | ||||
|     }) | ||||
|     .on("mouseover", function(_, d) { | ||||
|       const neighbours: string[] = data[slug].links ?? [] | ||||
|       const neighbours: CanonicalSlug[] = data[slug].links ?? [] | ||||
|       const neighbourNodes = d3.selectAll<HTMLElement, NodeData>(".node").filter((d) => neighbours.includes(d.id)) | ||||
|       console.log(neighbourNodes) | ||||
|       const currentId = d.id | ||||
|       const linkNodes = d3 | ||||
|         .selectAll(".link") | ||||
| @ -273,7 +273,7 @@ async function renderGraph(container: string, slug: string) { | ||||
| } | ||||
| 
 | ||||
| function renderGlobalGraph() { | ||||
|   const slug = document.body.dataset["slug"]! | ||||
|   const slug = getCanonicalSlug(window)  | ||||
|   const container = document.getElementById("global-graph-outer") | ||||
|   const sidebar = container?.closest(".sidebar") as HTMLElement | ||||
|   container?.classList.add("active") | ||||
|  | ||||
| @ -1,13 +1,14 @@ | ||||
| import { Document } from "flexsearch" | ||||
| import { ContentDetails } from "../../plugins/emitters/contentIndex" | ||||
| import { registerEscapeHandler, clientSideRelativePath, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug } from "../../path" | ||||
| import { registerEscapeHandler, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path" | ||||
| 
 | ||||
| interface Item { | ||||
|   slug: CanonicalSlug, | ||||
|   title: string, | ||||
|   content: string, | ||||
| } | ||||
| 
 | ||||
| let index: Document<Item> | undefined = undefined | ||||
| 
 | ||||
| const contextWindowWords = 30 | ||||
| @ -113,8 +114,8 @@ document.addEventListener("nav", async (e: unknown) => { | ||||
|     button.id = slug | ||||
|     button.innerHTML = `<h3>${title}</h3><p>${content}</p>` | ||||
|     button.addEventListener('click', () => { | ||||
|       const targ = clientSideRelativePath(currentSlug, slug) | ||||
|       window.spaNavigate(new URL(targ)) | ||||
|       const targ = resolveRelative(currentSlug, slug) | ||||
|       window.spaNavigate(new URL(targ, getClientSlug(window))) | ||||
|     }) | ||||
|     return button | ||||
|   } | ||||
| @ -137,9 +138,9 @@ document.addEventListener("nav", async (e: unknown) => { | ||||
|   function onType(e: HTMLElementEventMap["input"]) { | ||||
|     const term = (e.target as HTMLInputElement).value | ||||
|     const searchResults = index?.search(term, numSearchResults) ?? [] | ||||
|     const getByField = (field: string): string[] => { | ||||
|     const getByField = (field: string): CanonicalSlug[] => { | ||||
|       const results = searchResults.filter((x) => x.field === field) | ||||
|       return results.length === 0 ? [] : [...results[0].result] as string[] | ||||
|       return results.length === 0 ? [] : [...results[0].result] as CanonicalSlug[] | ||||
|     } | ||||
| 
 | ||||
|     // order titles ahead of content
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import micromorph from "micromorph" | ||||
| import { CanonicalSlug, RelativeURL } from "../../path" | ||||
| import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../path" | ||||
| 
 | ||||
| // adapted from `micromorph`
 | ||||
| // https://github.com/natemoo-re/micromorph
 | ||||
| @ -43,6 +43,7 @@ async function navigate(url: URL, isBack: boolean = false) { | ||||
|     .catch(() => { | ||||
|       window.location.assign(url) | ||||
|     }) | ||||
| 
 | ||||
|   if (!contents) return; | ||||
|   if (!isBack) { | ||||
|     history.pushState({}, "", url) | ||||
| @ -70,7 +71,7 @@ async function navigate(url: URL, isBack: boolean = false) { | ||||
|   const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])') | ||||
|   elementsToAdd.forEach(el => document.head.appendChild(el)) | ||||
| 
 | ||||
|   notifyNav(document.body.dataset.slug!) | ||||
|   notifyNav(getCanonicalSlug(window)) | ||||
|   delete announcer.dataset.persist | ||||
| } | ||||
| 
 | ||||
| @ -117,7 +118,7 @@ function createRouter() { | ||||
| } | ||||
| 
 | ||||
| createRouter() | ||||
| notifyNav(document.body.dataset.slug!) | ||||
| notifyNav(getCanonicalSlug(window)) | ||||
| 
 | ||||
| if (!customElements.get('route-announcer')) { | ||||
|   const attrs = { | ||||
|  | ||||
| @ -24,23 +24,21 @@ describe('typeguards', () => { | ||||
|   }) | ||||
| 
 | ||||
|   test('isCanonicalSlug', () => { | ||||
|     assert(path.isCanonicalSlug("/")) | ||||
|     assert(path.isCanonicalSlug("/abc")) | ||||
|     assert(path.isCanonicalSlug("/notindex")) | ||||
|     assert(path.isCanonicalSlug("/notindex/def")) | ||||
|     assert(path.isCanonicalSlug("")) | ||||
|     assert(path.isCanonicalSlug("abc")) | ||||
|     assert(path.isCanonicalSlug("notindex")) | ||||
|     assert(path.isCanonicalSlug("notindex/def")) | ||||
| 
 | ||||
|     assert(!path.isCanonicalSlug("//")) | ||||
|     assert(!path.isCanonicalSlug("/index")) | ||||
|     assert(!path.isCanonicalSlug("")) | ||||
|     assert(!path.isCanonicalSlug("index")) | ||||
|     assert(!path.isCanonicalSlug("index/abc")) | ||||
|     assert(!path.isCanonicalSlug("https://example.com")) | ||||
|     assert(!path.isCanonicalSlug("/abc/")) | ||||
|     assert(!path.isCanonicalSlug("/abc/index")) | ||||
|     assert(!path.isCanonicalSlug("/abc#anchor")) | ||||
|     assert(!path.isCanonicalSlug("/abc?query=1")) | ||||
|     assert(!path.isCanonicalSlug("/index.md")) | ||||
|     assert(!path.isCanonicalSlug("/index.html")) | ||||
|     assert(!path.isCanonicalSlug("/abc")) | ||||
|     assert(!path.isCanonicalSlug("abc/")) | ||||
|     assert(!path.isCanonicalSlug("abc/index")) | ||||
|     assert(!path.isCanonicalSlug("abc#anchor")) | ||||
|     assert(!path.isCanonicalSlug("abc?query=1")) | ||||
|     assert(!path.isCanonicalSlug("index.md")) | ||||
|     assert(!path.isCanonicalSlug("index.html")) | ||||
|   }) | ||||
| 
 | ||||
|   test('isRelativeURL', () => { | ||||
| @ -52,6 +50,7 @@ describe('typeguards', () => { | ||||
|     assert(path.isRelativeURL("../abc/def")) | ||||
| 
 | ||||
|     assert(!path.isRelativeURL("abc")) | ||||
|     assert(!path.isRelativeURL("/abc/def")) | ||||
|     assert(!path.isRelativeURL("")) | ||||
|     assert(!path.isRelativeURL("../")) | ||||
|     assert(!path.isRelativeURL("./")) | ||||
| @ -60,25 +59,23 @@ describe('typeguards', () => { | ||||
|   }) | ||||
| 
 | ||||
|   test('isServerSlug', () => { | ||||
|     assert(path.isServerSlug("/index")) | ||||
|     assert(path.isServerSlug("/abc/def")) | ||||
|     assert(path.isServerSlug("index")) | ||||
|     assert(path.isServerSlug("abc/def")) | ||||
| 
 | ||||
|     assert(!path.isServerSlug("/")) | ||||
|     assert(!path.isServerSlug(".")) | ||||
|     assert(!path.isServerSlug("./abc/def")) | ||||
|     assert(!path.isServerSlug("../abc/def")) | ||||
|     assert(!path.isServerSlug("/index.html")) | ||||
|     assert(!path.isServerSlug("/abc/def.html")) | ||||
|     assert(!path.isServerSlug("/abc/def#anchor")) | ||||
|     assert(!path.isServerSlug("/abc/def?query=1")) | ||||
|     assert(!path.isServerSlug("/note with spaces")) | ||||
|     assert(!path.isServerSlug("index.html")) | ||||
|     assert(!path.isServerSlug("abc/def.html")) | ||||
|     assert(!path.isServerSlug("abc/def#anchor")) | ||||
|     assert(!path.isServerSlug("abc/def?query=1")) | ||||
|     assert(!path.isServerSlug("note with spaces")) | ||||
|   }) | ||||
| 
 | ||||
|   test('isFilePath', () => { | ||||
|     assert(path.isFilePath("/content/index.md")) | ||||
|     assert(path.isFilePath("/content/test.png")) | ||||
|     assert(path.isFilePath("content/index.md")) | ||||
|     assert(path.isFilePath("content/test.png")) | ||||
|     assert(!path.isFilePath("../test.pdf")) | ||||
|     assert(!path.isFilePath("content/test.png")) | ||||
|     assert(!path.isFilePath("content/test")) | ||||
|     assert(!path.isFilePath("./content/test")) | ||||
|   }) | ||||
| @ -90,43 +87,45 @@ describe('transforms', () => { | ||||
|     for (const [inp, expected] of pairs) { | ||||
|       assert(checkPre(inp), `${inp} wasn't the expected input type`) | ||||
|       const actual = transform(inp) | ||||
|       assert.strictEqual(actual, expected, `after transforming ${inp}, ${actual} was not ${expected}`) | ||||
|       assert.strictEqual(actual, expected, `after transforming ${inp}, '${actual}' was not '${expected}'`) | ||||
|       assert(checkPost(actual), `${actual} wasn't the expected output type`) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   test('canonicalizeServer', () => { | ||||
|     asserts([ | ||||
|       ["/index", "/"], | ||||
|       ["/abc/def", "/abc/def"], | ||||
|       ["index", ""], | ||||
|       ["abc/index", "abc"], | ||||
|       ["abc/def", "abc/def"], | ||||
|     ], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug) | ||||
|   }) | ||||
| 
 | ||||
|   test('canonicalizeClient', () => { | ||||
|     asserts([ | ||||
|       ["http://localhost:3000", "/"], | ||||
|       ["http://localhost:3000/index", "/"], | ||||
|       ["http://localhost:3000/test", "/test"], | ||||
|       ["http://example.com", "/"], | ||||
|       ["http://example.com/index", "/"], | ||||
|       ["http://example.com/index.html", "/"], | ||||
|       ["http://example.com/", "/"], | ||||
|       ["https://example.com", "/"], | ||||
|       ["https://example.com/abc/def", "/abc/def"], | ||||
|       ["https://example.com/abc/def/", "/abc/def"], | ||||
|       ["https://example.com/abc/def#cool", "/abc/def"], | ||||
|       ["https://example.com/abc/def?field=1&another=2", "/abc/def"], | ||||
|       ["https://example.com/abc/def?field=1&another=2#cool", "/abc/def"], | ||||
|       ["https://example.com/abc/def.html?field=1&another=2#cool", "/abc/def"], | ||||
|       ["http://localhost:3000", ""], | ||||
|       ["http://localhost:3000/index", ""], | ||||
|       ["http://localhost:3000/test", "test"], | ||||
|       ["http://example.com", ""], | ||||
|       ["http://example.com/index", ""], | ||||
|       ["http://example.com/index.html", ""], | ||||
|       ["http://example.com/", ""], | ||||
|       ["https://example.com", ""], | ||||
|       ["https://example.com/abc/def", "abc/def"], | ||||
|       ["https://example.com/abc/def/", "abc/def"], | ||||
|       ["https://example.com/abc/def#cool", "abc/def"], | ||||
|       ["https://example.com/abc/def?field=1&another=2", "abc/def"], | ||||
|       ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"], | ||||
|       ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"], | ||||
|     ], path.canonicalizeClient, path.isClientSlug, path.isCanonicalSlug) | ||||
|   }) | ||||
| 
 | ||||
|   describe('slugifyFilePath', () => { | ||||
|     asserts([ | ||||
|       ["/content/index.md", "/content/index"], | ||||
|       ["/content/cool.png", "/content/cool"], | ||||
|       ["/index.md", "/index"], | ||||
|       ["/note with spaces.md", "/note-with-spaces"], | ||||
|       ["content/index.md", "content/index"], | ||||
|       ["/content/index.md", "content/index"], | ||||
|       ["content/cool.png", "content/cool"], | ||||
|       ["index.md", "index"], | ||||
|       ["note with spaces.md", "note-with-spaces"], | ||||
|     ], path.slugifyFilePath, path.isFilePath, path.isServerSlug) | ||||
|   }) | ||||
| 
 | ||||
| @ -146,13 +145,14 @@ describe('transforms', () => { | ||||
|       ["/tags/", "./tags"], | ||||
|       ["content/with spaces", "./content/with-spaces"], | ||||
|       ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"], | ||||
|     ], path.transformInternalLink, (x: string): x is string => true, path.isRelativeURL) | ||||
|     ], path.transformInternalLink, (_x: string): _x is string => true, path.isRelativeURL) | ||||
|   }) | ||||
| 
 | ||||
|   describe('pathToRoot', () => { | ||||
|     asserts([ | ||||
|       ["/", "."], | ||||
|       ["/abc/def", "../.."], | ||||
|       ["", "."], | ||||
|       ["abc", ".."], | ||||
|       ["abc/def", "../.."], | ||||
|     ], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
							
								
								
									
										146
									
								
								quartz/path.ts
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								quartz/path.ts
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| import path from 'path' | ||||
| import { slug as slugAnchor } from 'github-slugger' | ||||
| import { trace } from './trace' | ||||
| 
 | ||||
| // Quartz Paths
 | ||||
| // Things in boxes are not actual types but rather sources which these types can be acquired from
 | ||||
| @ -15,41 +15,54 @@ import { slug as slugAnchor } from 'github-slugger' | ||||
| //                    │                        │                                     │
 | ||||
| //                    │        getClientSlug() │                               .href │
 | ||||
| //                    │                        ▼                                     ▼
 | ||||
| //                    │                                                   
 | ||||
| //                    │                  Client Slug                           Relative URL
 | ||||
| // getCanonicalSlug() │     https://test.ca/note/abc#anchor?query=123          ../note/def#anchor
 | ||||
| //                    │
 | ||||
| //                    │   canonicalizeClient() │                                     ▲
 | ||||
| //                    │                        ▼                                     │
 | ||||
| //                    │                                                              │
 | ||||
| //                    └───────────────►  Canonical Slug                              │
 | ||||
| //                                         /note/abc                                 │
 | ||||
| //                                                                                   │
 | ||||
| //                                             ▲                                     │
 | ||||
| //                    │                  Client Slug                    ┌───►  Relative URL
 | ||||
| // getCanonicalSlug() │     https://test.ca/note/abc#anchor?query=123   │      ../note/def#anchor
 | ||||
| //                    │                                                 │
 | ||||
| //                    │   canonicalizeClient() │                        │      ▲     ▲
 | ||||
| //                    │                        ▼                        │      │     │
 | ||||
| //                    │                                  pathToRoot()   │      │     │
 | ||||
| //                    └───────────────►  Canonical Slug ────────────────┘      │     │
 | ||||
| //                                          note/abc                           │     │
 | ||||
| //                                                   ──────────────────────────┘     │
 | ||||
| //                                             ▲             resolveRelative()       │
 | ||||
| //                        canonicalizeServer() │                                     │
 | ||||
| //                                                                                   │
 | ||||
| // HTML File                               Server Slug                               │
 | ||||
| // /note/abc/index.html  ◄─────────────  /note/abc/index                             │
 | ||||
| //  note/abc/index.html  ◄─────────────   note/abc/index                             │
 | ||||
| //                                                                                   │
 | ||||
| //                                             ▲                            ┌────────┴────────┐
 | ||||
| //                           slugifyFilePath() │    transformInternalLink() │                 │
 | ||||
| //                           slugifyFilePath() │            transformLink() │                 │
 | ||||
| //                                             │                            │                 │
 | ||||
| //                                   ┌─────────┴──────────┐           ┌─────┴─────┐  ┌────────┴──────┐
 | ||||
| //                                   │     File Path      │           │ Wikilinks │  │ Markdown Link │
 | ||||
| //                                   │ /note/abc/index.md │           └───────────┘  └───────────────┘
 | ||||
| //                                   │  note/abc/index.md │           └───────────┘  └───────────────┘
 | ||||
| //                                   └────────────────────┘                 ▲                 ▲
 | ||||
| //                                             ▲                            │                 │
 | ||||
| //                                             │            ┌─────────┐     │                 │
 | ||||
| //                                             └────────────┤ MD File ├─────┴─────────────────┘
 | ||||
| //                                                          └─────────┘
 | ||||
| 
 | ||||
| const STRICT_TYPE_CHECKS = true | ||||
| const HARD_EXIT_ON_FAIL = true | ||||
| 
 | ||||
| function conditionCheck<T>(name: string, label: 'pre' | 'post', s: T, chk: (x: any) => x is T) { | ||||
|   if (STRICT_TYPE_CHECKS && !chk(s)) { | ||||
|     trace(`${name} failed ${label}-condition check: ${s} does not pass ${chk.name}`, new Error()) | ||||
|     if (HARD_EXIT_ON_FAIL) { | ||||
|       process.exit(1) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Utility type to simulate nominal types in TypeScript
 | ||||
| type SlugLike<T> = string & { __brand: T } | ||||
| 
 | ||||
| /** Client-side slug, usually obtained through `window.location` */ | ||||
| export type ClientSlug = SlugLike<"client"> | ||||
| export function isClientSlug(s: string): s is ClientSlug { | ||||
|   return /^https?:\/\/.+/.test(s) | ||||
|   const res = /^https?:\/\/.+/.test(s) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| /** Canonical slug, should be used whenever you need to refer to the location of a file/note. | ||||
| @ -57,9 +70,9 @@ export function isClientSlug(s: string): s is ClientSlug { | ||||
|   */ | ||||
| export type CanonicalSlug = SlugLike<"canonical"> | ||||
| export function isCanonicalSlug(s: string): s is CanonicalSlug { | ||||
|   const validStart = s.startsWith("/") | ||||
|   const validEnding = s.length === 1 || (!s.endsWith("/") && !s.endsWith("/index")) | ||||
|   return !_containsForbiddenCharacters(s) && validStart && validEnding && !_hasFileExtension(s) | ||||
|   const validStart = !(s.startsWith(".") || s.startsWith("/")) | ||||
|   const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index") | ||||
|   return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s) | ||||
| } | ||||
| 
 | ||||
| /** A relative link, can be found on `href`s but can also be constructed for | ||||
| @ -68,15 +81,14 @@ export function isCanonicalSlug(s: string): s is CanonicalSlug { | ||||
| export type RelativeURL = SlugLike<"relative"> | ||||
| export function isRelativeURL(s: string): s is RelativeURL { | ||||
|   const validStart = /^\.{1,2}/.test(s) | ||||
|   const validEnding = !s.endsWith("/") && !s.endsWith("/index") | ||||
|   const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index") | ||||
|   return validStart && validEnding && !_hasFileExtension(s) | ||||
| } | ||||
| 
 | ||||
| /** A server side slug. This is what Quartz uses to emit files so uses index suffixes */ | ||||
| export type ServerSlug = SlugLike<"server"> | ||||
| export function isServerSlug(s: string): s is ServerSlug { | ||||
|   // must start with forward slash
 | ||||
|   const validStart = s.startsWith("/") | ||||
|   const validStart = !(s.startsWith(".") || s.startsWith("/")) | ||||
|   const validEnding = !s.endsWith("/") | ||||
|   return validStart && validEnding && !_containsForbiddenCharacters(s) && !_hasFileExtension(s) | ||||
| } | ||||
| @ -84,66 +96,107 @@ export function isServerSlug(s: string): s is ServerSlug { | ||||
| /** The real file path to a file on disk */ | ||||
| export type FilePath = SlugLike<"filepath"> | ||||
| export function isFilePath(s: string): s is FilePath { | ||||
|   return s.startsWith("/") && _hasFileExtension(s) | ||||
|   const validStart = !s.startsWith(".") | ||||
|   return validStart && _hasFileExtension(s) | ||||
| } | ||||
| 
 | ||||
| export function getClientSlug(window: Window): ClientSlug { | ||||
|   return window.location.href as ClientSlug | ||||
|   const res = window.location.href as ClientSlug | ||||
|   conditionCheck(getClientSlug.name, 'post', res, isClientSlug) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function getCanonicalSlug(window: Window): CanonicalSlug { | ||||
|   return window.document.body.dataset.slug! as CanonicalSlug | ||||
|   const res = window.document.body.dataset.slug! as CanonicalSlug | ||||
|   conditionCheck(getCanonicalSlug.name, 'post', res, isCanonicalSlug) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function canonicalizeClient(slug: ClientSlug): CanonicalSlug { | ||||
|   conditionCheck(canonicalizeClient.name, 'pre', slug, isClientSlug) | ||||
|   const { pathname } = new URL(slug) | ||||
|   let fp = pathname | ||||
|   fp = fp.replace(new RegExp(path.extname(fp) + '$'), '') | ||||
|   return _canonicalize(fp) as CanonicalSlug | ||||
|   let fp = pathname.slice(1) | ||||
|   fp = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '') | ||||
|   const res = _canonicalize(fp) as CanonicalSlug | ||||
|   conditionCheck(canonicalizeClient.name, 'post', res, isCanonicalSlug) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function canonicalizeServer(slug: ServerSlug): CanonicalSlug { | ||||
|   conditionCheck(canonicalizeServer.name, 'pre', slug, isServerSlug) | ||||
|   let fp = slug as string | ||||
|   return _canonicalize(fp) as CanonicalSlug | ||||
|   const res = _canonicalize(fp) as CanonicalSlug | ||||
|   conditionCheck(canonicalizeServer.name, 'post', res, isCanonicalSlug) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function slugifyFilePath(fp: FilePath): ServerSlug { | ||||
|   // strip file extension
 | ||||
|   const withoutFileExt = fp.replace(new RegExp(path.extname(fp) + '$'), '') | ||||
|   conditionCheck(slugifyFilePath.name, 'pre', fp, isFilePath) | ||||
|   fp = _stripSlashes(fp) as FilePath | ||||
|   const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '') | ||||
|   const slug = withoutFileExt | ||||
|     .split(path.sep) // fs can have diff interpretations of /
 | ||||
|     .split('/') | ||||
|     .map((segment) => segment.replace(/\s/g, '-')) // slugify all segments
 | ||||
|     .join('/') // always use / as sep
 | ||||
|     .replace(/\/$/, '') // remove trailing slash
 | ||||
| 
 | ||||
|   conditionCheck(slugifyFilePath.name, 'post', slug, isServerSlug) | ||||
|   return slug as ServerSlug | ||||
| } | ||||
| 
 | ||||
| export function transformInternalLink(link: string): RelativeURL { | ||||
|   let [fplike, anchor] = link.split("#", 2) | ||||
|   let [fplike, anchor] = splitAnchor(decodeURI(link)) | ||||
|   let segments = fplike.split("/").filter(x => x.length > 0) | ||||
|   let prefix = segments.filter(_isRelativeSegment).join("/") | ||||
|   let fp = "/" + segments.filter(seg => !_isRelativeSegment(seg)).join("/") | ||||
|   let fp = segments.filter(seg => !_isRelativeSegment(seg)).join("/") | ||||
| 
 | ||||
|   // implicit markdown
 | ||||
|   if (!_hasFileExtension(fp)) { | ||||
|     fp += ".md" | ||||
|   } | ||||
| 
 | ||||
|   fp = canonicalizeServer(slugifyFilePath(fp as FilePath)) | ||||
| 
 | ||||
|   if (fp.endsWith("index")) { | ||||
|     fp = fp.slice(0, -"index".length) | ||||
|   } | ||||
| 
 | ||||
|   let joined = [_stripSlashes(prefix), _stripSlashes(fp)].filter(x => x !== "").join("/") | ||||
|   anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor) | ||||
|   return _addRelativeToStart(joined) + anchor as RelativeURL | ||||
|   let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp)) | ||||
|   const res = _addRelativeToStart(joined) + anchor as RelativeURL | ||||
|   conditionCheck(transformInternalLink.name, 'post', res, isRelativeURL) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| // resolve /a/b/c to ../../
 | ||||
| export function pathToRoot(slug: CanonicalSlug): RelativeURL { | ||||
|   conditionCheck(pathToRoot.name, 'pre', slug, isCanonicalSlug) | ||||
|   let rootPath = slug | ||||
|     .split('/') | ||||
|     .filter(x => x !== '') | ||||
|     .map(_ => '..') | ||||
|     .join('/') | ||||
| 
 | ||||
|   return _addRelativeToStart(rootPath) as RelativeURL | ||||
|   const res = _addRelativeToStart(rootPath) as RelativeURL | ||||
|   conditionCheck(pathToRoot.name, 'post', res, isRelativeURL) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL { | ||||
|   conditionCheck(resolveRelative.name, 'pre', current, isCanonicalSlug) | ||||
|   conditionCheck(resolveRelative.name, 'pre', target, isCanonicalSlug) | ||||
|   const res = joinSegments(pathToRoot(current), target) as RelativeURL | ||||
|   conditionCheck(resolveRelative.name, 'post', res, isRelativeURL) | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function splitAnchor(link: string): [string, string] { | ||||
|   let [fp, anchor] = link.split("#", 2) | ||||
|   anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor) | ||||
|   return [fp, anchor] | ||||
| } | ||||
| 
 | ||||
| export function joinSegments(...args: string[]): string { | ||||
|   return args.filter(segment => segment !== "").join('/') | ||||
| } | ||||
| 
 | ||||
| export const QUARTZ = "quartz" | ||||
| @ -153,16 +206,7 @@ function _canonicalize(fp: string): string { | ||||
|     fp = fp.slice(0, -"index".length) | ||||
|   } | ||||
| 
 | ||||
|   // remove trailing slash
 | ||||
|   if (fp.endsWith("/")) { | ||||
|     fp = fp.slice(0, -1) | ||||
|   } | ||||
| 
 | ||||
|   if (fp.length === 0) { | ||||
|     return "/" as CanonicalSlug | ||||
|   } | ||||
| 
 | ||||
|   return fp | ||||
|   return _stripSlashes(fp)  | ||||
| } | ||||
| 
 | ||||
| function _containsForbiddenCharacters(s: string): boolean { | ||||
| @ -170,7 +214,11 @@ function _containsForbiddenCharacters(s: string): boolean { | ||||
| } | ||||
| 
 | ||||
| function _hasFileExtension(s: string): boolean { | ||||
|   return /\.[A-Za-z]+$/.test(s) | ||||
|   return _getFileExtension(s) !== undefined | ||||
| } | ||||
| 
 | ||||
| function _getFileExtension(s: string): string | undefined { | ||||
|   return s.match(/\.[A-Za-z]+$/)?.[0] | ||||
| } | ||||
| 
 | ||||
| function _isRelativeSegment(s: string): boolean { | ||||
| @ -195,7 +243,7 @@ function _addRelativeToStart(s: string): string { | ||||
|   } | ||||
| 
 | ||||
|   if (!s.startsWith(".")) { | ||||
|     s = "./" + s | ||||
|     s = joinSegments(".", s) | ||||
|   } | ||||
| 
 | ||||
|   return s | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { CanonicalSlug, FilePath, ServerSlug, relativeToRoot } from "../../path" | ||||
| import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, resolveRelative } from "../../path" | ||||
| import { QuartzEmitterPlugin } from "../types" | ||||
| import path from 'path' | ||||
| 
 | ||||
| @ -11,7 +11,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ | ||||
|     const fps: FilePath[] = [] | ||||
| 
 | ||||
|     for (const [_tree, file] of content) { | ||||
|       const ogSlug = file.data.slug! | ||||
|       const ogSlug = canonicalizeServer(file.data.slug!) | ||||
|       const dir = path.relative(contentFolder, file.dirname ?? contentFolder) | ||||
| 
 | ||||
|       let aliases: CanonicalSlug[] = [] | ||||
| @ -22,12 +22,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ | ||||
|       } | ||||
| 
 | ||||
|       for (const alias of aliases) { | ||||
|         const slug = (alias.startsWith("/") | ||||
|           ? alias | ||||
|           : path.posix.join(dir, alias)) as ServerSlug | ||||
|         const slug = path.posix.join(dir, alias) as ServerSlug | ||||
| 
 | ||||
|         const fp = slug + ".html" as FilePath | ||||
|         const redirUrl = relativeToRoot(slug, ogSlug) | ||||
|         const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug) | ||||
|         await emit({ | ||||
|           content: ` | ||||
|             <!DOCTYPE html> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { GlobalConfiguration } from "../../cfg" | ||||
| import { CanonicalSlug, ClientSlug } from "../../path" | ||||
| import { CanonicalSlug, ClientSlug, FilePath, ServerSlug, canonicalizeServer } from "../../path" | ||||
| import { QuartzEmitterPlugin } from "../types" | ||||
| import path from "path" | ||||
| 
 | ||||
| @ -65,10 +65,10 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|   return { | ||||
|     name: "ContentIndex", | ||||
|     async emit(_contentDir, cfg, content, _resources, emit) { | ||||
|       const emitted: string[] = [] | ||||
|       const emitted: FilePath[] = [] | ||||
|       const linkIndex: ContentIndex = new Map() | ||||
|       for (const [_tree, file] of content) { | ||||
|         const slug = file.data.slug! | ||||
|         const slug = canonicalizeServer(file.data.slug!) | ||||
|         const date = file.data.dates?.modified ?? new Date() | ||||
|         if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { | ||||
|         linkIndex.set(slug, { | ||||
| @ -85,22 +85,22 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|       if (opts?.enableSiteMap) { | ||||
|         await emit({ | ||||
|           content: generateSiteMap(cfg, linkIndex), | ||||
|           slug: "sitemap", | ||||
|           slug: "sitemap" as ServerSlug, | ||||
|           ext: ".xml" | ||||
|         }) | ||||
|         emitted.push("sitemap.xml") | ||||
|         emitted.push("sitemap.xml" as FilePath) | ||||
|       } | ||||
| 
 | ||||
|       if (opts?.enableRSS) { | ||||
|         await emit({ | ||||
|           content: generateRSSFeed(cfg, linkIndex), | ||||
|           slug: "index", | ||||
|           slug: "index" as ServerSlug, | ||||
|           ext: ".xml" | ||||
|         }) | ||||
|         emitted.push("index.xml") | ||||
|         emitted.push("index.xml" as FilePath) | ||||
|       } | ||||
| 
 | ||||
|       const fp = path.join("static", "contentIndex") | ||||
|       const fp = path.join("static", "contentIndex") as ServerSlug | ||||
|       const simplifiedIndex = Object.fromEntries( | ||||
|         Array.from(linkIndex).map(([slug, content]) => { | ||||
|           // remove description and from content index as nothing downstream
 | ||||
| @ -117,7 +117,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|         slug: fp, | ||||
|         ext: ".json", | ||||
|       }) | ||||
|       emitted.push(`${fp}.json`) | ||||
|       emitted.push(`${fp}.json` as FilePath) | ||||
| 
 | ||||
|       return emitted | ||||
|     }, | ||||
|  | ||||
| @ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header" | ||||
| import BodyConstructor from "../../components/Body" | ||||
| import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import { FilePath } from "../../path" | ||||
| import { FilePath, canonicalizeServer } from "../../path" | ||||
| 
 | ||||
| export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|   if (!opts) { | ||||
| @ -24,7 +24,7 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|       const fps: FilePath[] = [] | ||||
|       const allFiles = content.map(c => c[1].data) | ||||
|       for (const [tree, file] of content) { | ||||
|         const slug = file.data.slug! | ||||
|         const slug = canonicalizeServer(file.data.slug!) | ||||
|         const externalResources = pageResources(slug, resources) | ||||
|         const componentData: QuartzComponentProps = { | ||||
|           fileData: file.data, | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { ProcessedContent, defaultProcessedContent } from "../vfile" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import path from "path" | ||||
| import { FilePath, toServerSlug } from "../../path" | ||||
| import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, joinSegments } from "../../path" | ||||
| 
 | ||||
| export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|   if (!opts) { | ||||
| @ -23,28 +23,34 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|       return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer] | ||||
|     }, | ||||
|     async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> { | ||||
|       const fps: string[] = [] | ||||
|       const fps: FilePath[] = [] | ||||
|       const allFiles = content.map(c => c[1].data) | ||||
| 
 | ||||
|       const folders: Set<string> = new Set(allFiles.flatMap(data => data.slug ? [path.dirname(data.slug)] : [])) | ||||
|       const folders: Set<CanonicalSlug> = new Set(allFiles.flatMap(data => { | ||||
|         const slug = data.slug | ||||
|         const folderName = path.dirname(slug ?? "") as CanonicalSlug | ||||
|         if (slug && folderName !== ".") { | ||||
|           return [folderName] | ||||
|         } | ||||
|         return [] | ||||
|       })) | ||||
| 
 | ||||
|       // remove special prefixes
 | ||||
|       folders.delete(".") | ||||
|       folders.delete("tags") | ||||
|       folders.delete("tags" as CanonicalSlug) | ||||
| 
 | ||||
|       const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([ | ||||
|         folder, defaultProcessedContent({ slug: folder, frontmatter: { title: `Folder: ${folder}`, tags: [] } }) | ||||
|         folder, defaultProcessedContent({ slug: joinSegments(folder, "index") as ServerSlug, frontmatter: { title: `Folder: ${folder}`, tags: [] } }) | ||||
|       ]))) | ||||
| 
 | ||||
|       for (const [tree, file] of content) { | ||||
|         const slug = toServerSlug(file.data.slug!) | ||||
|         const slug = canonicalizeServer(file.data.slug!) | ||||
|         if (folders.has(slug)) { | ||||
|           folderDescriptions[slug] = [tree, file] | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       for (const folder of folders) { | ||||
|         const slug = folder  | ||||
|         const slug = folder | ||||
|         const externalResources = pageResources(slug, resources) | ||||
|         const [tree, file] = folderDescriptions[folder] | ||||
|         const componentData: QuartzComponentProps = { | ||||
| @ -63,7 +69,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|           externalResources | ||||
|         ) | ||||
| 
 | ||||
|         const fp = file.data.slug + ".html" | ||||
|         const fp = file.data.slug! + ".html" as FilePath | ||||
|         await emit({ | ||||
|           content, | ||||
|           slug: file.data.slug!, | ||||
|  | ||||
| @ -5,7 +5,7 @@ import BodyConstructor from "../../components/Body" | ||||
| import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { ProcessedContent, defaultProcessedContent } from "../vfile" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import { FilePath, ServerSlug, toServerSlug } from "../../path" | ||||
| import { CanonicalSlug, FilePath, ServerSlug } from "../../path" | ||||
| 
 | ||||
| export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|   if (!opts) { | ||||
| @ -31,7 +31,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|       ]))) | ||||
| 
 | ||||
|       for (const [tree, file] of content) { | ||||
|         const slug = toServerSlug(file.data.slug!) | ||||
|         const slug = file.data.slug! | ||||
|         if (slug.startsWith("tags/")) { | ||||
|           const tag = slug.slice("tags/".length) | ||||
|           if (tags.has(tag)) { | ||||
| @ -41,7 +41,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { | ||||
|       } | ||||
| 
 | ||||
|       for (const tag of tags) { | ||||
|         const slug = `tags/${tag}` | ||||
|         const slug = `tags/${tag}` as CanonicalSlug | ||||
|         const externalResources = pageResources(slug, resources) | ||||
|         const [tree, file] = tagDescriptions[tag] | ||||
|         const componentData: QuartzComponentProps = { | ||||
|  | ||||
| @ -55,17 +55,17 @@ function joinScripts(scripts: string[]): string { | ||||
| export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> { | ||||
|   const fps = await Promise.all([ | ||||
|     emit({ | ||||
|       slug: "index", | ||||
|       slug: "index" as ServerSlug, | ||||
|       ext: ".css", | ||||
|       content: joinStyles(cfg.theme, styles, ...res.css) | ||||
|     }), | ||||
|     emit({ | ||||
|       slug: "prescript", | ||||
|       slug: "prescript" as ServerSlug, | ||||
|       ext: ".js", | ||||
|       content: joinScripts(res.beforeDOMLoaded) | ||||
|     }), | ||||
|     emit({ | ||||
|       slug: "postscript", | ||||
|       slug: "postscript" as ServerSlug, | ||||
|       ext: ".js", | ||||
|       content: joinScripts(res.afterDOMLoaded) | ||||
|     }) | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { QuartzTransformerPlugin } from "../types" | ||||
| import { CanonicalSlug, transformInternalLink } from "../../path" | ||||
| import { CanonicalSlug, RelativeURL, canonicalizeServer, joinSegments, pathToRoot, resolveRelative, splitAnchor, transformInternalLink } from "../../path" | ||||
| import path from "path" | ||||
| import { visit } from 'unist-util-visit' | ||||
| import isAbsoluteUrl from "is-absolute-url" | ||||
| @ -9,15 +9,11 @@ interface Options { | ||||
|   markdownLinkResolution: 'absolute' | 'relative' | 'shortest' | ||||
|   /** Strips folders from a link so that it looks nice */ | ||||
|   prettyLinks: boolean | ||||
|   indexAnchorLinks: boolean | ||||
|   indexExternalLinks: boolean | ||||
| } | ||||
| 
 | ||||
| const defaultOptions: Options = { | ||||
|   markdownLinkResolution: 'absolute', | ||||
|   prettyLinks: true, | ||||
|   indexAnchorLinks: false, | ||||
|   indexExternalLinks: false, | ||||
| } | ||||
| 
 | ||||
| export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { | ||||
| @ -27,32 +23,34 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
|     htmlPlugins() { | ||||
|       return [() => { | ||||
|         return (tree, file) => { | ||||
|           const curSlug = file.data.slug! | ||||
|           const transformLink = (target: string) => { | ||||
|             const targetSlug = transformInternalLink(target)  | ||||
|             if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) { | ||||
|               return './' + relative(curSlug, targetSlug) | ||||
|           const curSlug = canonicalizeServer(file.data.slug!) | ||||
|           const transformLink = (target: string): RelativeURL => { | ||||
|             const targetSlug = transformInternalLink(target).slice("./".length) | ||||
|             let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) | ||||
|             if (opts.markdownLinkResolution === 'relative') { | ||||
|               return targetSlug as RelativeURL | ||||
|             } else if (opts.markdownLinkResolution === 'shortest') { | ||||
|               // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
 | ||||
|               const allSlugs = file.data.allSlugs! | ||||
| 
 | ||||
|               // if the file name is unique, then it's just the filename
 | ||||
|               const matchingFileNames = allSlugs.filter(slug => { | ||||
|                 const parts = toServerSlug(slug).split(path.posix.sep) | ||||
|                 const parts = slug.split(path.posix.sep) | ||||
|                 const fileName = parts.at(-1) | ||||
|                 return targetSlug === fileName | ||||
|                 return targetCanonical === fileName | ||||
|               }) | ||||
| 
 | ||||
|               if (matchingFileNames.length === 1) { | ||||
|                 const targetSlug = toServerSlug(matchingFileNames[0]) | ||||
|                 return './' + relativeToRoot(curSlug, targetSlug) | ||||
|                 const targetSlug = canonicalizeServer(matchingFileNames[0]) | ||||
|                 return resolveRelative(curSlug, targetSlug) + targetAnchor as RelativeURL | ||||
|               } | ||||
| 
 | ||||
|               // if it's not unique, then it's the absolute path from the vault root
 | ||||
|               // (fall-through case)
 | ||||
|             } | ||||
| 
 | ||||
|             // treat as absolute
 | ||||
|             return './' + relativeToRoot(curSlug, targetSlug) | ||||
|             return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL | ||||
|           } | ||||
| 
 | ||||
|           const outgoing: Set<CanonicalSlug> = new Set() | ||||
| @ -63,26 +61,15 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
|               node.properties && | ||||
|               typeof node.properties.href === 'string' | ||||
|             ) { | ||||
|               let dest = node.properties.href | ||||
|               let dest = node.properties.href as RelativeURL | ||||
|               node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" | ||||
| 
 | ||||
|               // don't process external links or intra-document anchors
 | ||||
|               if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { | ||||
|                 node.properties.href = transformLink(dest) | ||||
|               } | ||||
| 
 | ||||
|               dest = node.properties.href | ||||
|               if (dest.startsWith(".")) { | ||||
|                 const normalizedPath = path.normalize(path.join(curSlug, dest)) | ||||
|                 outgoing.add(trimPathSuffix(normalizedPath)) | ||||
|               } else if (dest.startsWith("#")) { | ||||
|                 if (opts.indexAnchorLinks) { | ||||
|                   outgoing.add(dest) | ||||
|                 } | ||||
|               } else { | ||||
|                 if (opts.indexExternalLinks) { | ||||
|                   outgoing.add(dest) | ||||
|                 } | ||||
|                 dest = node.properties.href = transformLink(dest) | ||||
|                 const canonicalDest = path.normalize(joinSegments(curSlug, dest)) | ||||
|                 const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) | ||||
|                 outgoing.add(destCanonical as CanonicalSlug) | ||||
|               } | ||||
| 
 | ||||
|               // rewrite link internals if prettylinks is on
 | ||||
|  | ||||
| @ -2,7 +2,6 @@ import { PluggableList } from "unified" | ||||
| import { QuartzTransformerPlugin } from "../types" | ||||
| import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast' | ||||
| import { findAndReplace } from "mdast-util-find-and-replace" | ||||
| import { slugify } from "../../path" | ||||
| import { slug as slugAnchor } from 'github-slugger' | ||||
| import rehypeRaw from "rehype-raw" | ||||
| import { visit } from "unist-util-visit" | ||||
| @ -10,6 +9,7 @@ import path from "path" | ||||
| import { JSResource } from "../../resources" | ||||
| // @ts-ignore
 | ||||
| import calloutScript from "../../components/scripts/callout.inline.ts" | ||||
| import { FilePath, slugifyFilePath, transformInternalLink } from "../../path" | ||||
| 
 | ||||
| export interface Options { | ||||
|   comments: boolean | ||||
| @ -139,14 +139,15 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | ||||
|         plugins.push(() => { | ||||
|           return (tree: Root, _file) => { | ||||
|             findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => { | ||||
|               const [fp, rawHeader, rawAlias] = capture | ||||
|               let [fp, rawHeader, rawAlias] = capture | ||||
|               fp = fp.trim() | ||||
|               const anchor = rawHeader?.trim() ?? "" | ||||
|               const alias = rawAlias?.slice(1).trim() | ||||
| 
 | ||||
|               // embed cases
 | ||||
|               if (value.startsWith("!")) { | ||||
|                 const ext = path.extname(fp).toLowerCase() | ||||
|                 const url = slugify(fp.trim()) + ext | ||||
|                 const ext: string | undefined = path.extname(fp).toLowerCase() | ||||
|                 const url = slugifyFilePath(fp as FilePath) + ext | ||||
|                 if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) { | ||||
|                   const dims = alias ?? "" | ||||
|                   let [width, height] = dims.split("x", 2) | ||||
| @ -176,12 +177,15 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | ||||
|                     type: 'html', | ||||
|                     value: `<iframe src="${url}"></iframe>` | ||||
|                   } | ||||
|                 } else { | ||||
|                   // TODO: this is the node embed case
 | ||||
|                 } | ||||
|                 // otherwise, fall through to regular link
 | ||||
|               } | ||||
| 
 | ||||
|               // internal link
 | ||||
|               const url = slugify(fp.trim() + anchor) | ||||
|               // const url = transformInternalLink(fp + anchor)
 | ||||
|               const url = fp + anchor | ||||
|               return { | ||||
|                 type: 'link', | ||||
|                 url, | ||||
|  | ||||
| @ -3,7 +3,6 @@ import { Root } from "mdast" | ||||
| import { visit } from "unist-util-visit" | ||||
| import { toString } from "mdast-util-to-string" | ||||
| import { slug as slugAnchor } from 'github-slugger' | ||||
| import { CanonicalSlug } from "../../path" | ||||
| 
 | ||||
| export interface Options { | ||||
|   maxDepth: 1 | 2 | 3 | 4 | 5 | 6, | ||||
| @ -20,7 +19,7 @@ const defaultOptions: Options = { | ||||
| interface TocEntry { | ||||
|   depth: number, | ||||
|   text: string, | ||||
|   slug: CanonicalSlug | ||||
|   slug: string // this is just the anchor (#some-slug), not the canonical slug
 | ||||
| } | ||||
| 
 | ||||
| export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { | ||||
|  | ||||
| @ -19,6 +19,7 @@ import popoverStyle from '../components/styles/popover.scss' | ||||
| import { StaticResources } from "../resources" | ||||
| import { QuartzLogger } from "../log" | ||||
| import { googleFontHref } from "../theme" | ||||
| import { trace } from "../trace" | ||||
| 
 | ||||
| function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) { | ||||
|   staticResources.css.push(googleFontHref(cfg.theme)) | ||||
| @ -110,7 +111,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.log(chalk.red(`Failed to emit from plugin \`${emitter.name}\`: `) + err) | ||||
|       trace(`Failed to emit from plugin \`${emitter.name}\``, err as Error) | ||||
|       process.exit(1) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -14,6 +14,7 @@ import workerpool, { Promise as WorkerPromise } from 'workerpool' | ||||
| import { QuartzTransformerPluginInstance } from '../plugins/types' | ||||
| import { QuartzLogger } from '../log' | ||||
| import chalk from 'chalk' | ||||
| import { trace } from '../trace' | ||||
| 
 | ||||
| export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void> | ||||
| export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor { | ||||
| @ -101,7 +102,7 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[] | ||||
|           console.log(`[process] ${fp} -> ${file.data.slug}`) | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.log(chalk.red(`\nFailed to process \`${fp}\`: `) + err) | ||||
|         trace(`\nFailed to process \`${fp}\``, err as Error) | ||||
|         process.exit(1) | ||||
|       } | ||||
|     } | ||||
|  | ||||
							
								
								
									
										25
									
								
								quartz/trace.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								quartz/trace.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import chalk from "chalk" | ||||
| 
 | ||||
| const rootFile = /.*at file:/ | ||||
| export function trace(msg: string, err: Error) { | ||||
|   const stack = err.stack | ||||
|   console.log() | ||||
|   console.log(chalk.bgRed.white.bold(" ERROR ") + chalk.red(` ${msg}`) + (err.message.length > 0 ? `: ${err.message}` : "")) | ||||
|   if (!stack) { | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   let reachedEndOfLegibleTrace = false | ||||
|   for (const line of stack.split('\n').slice(1)) { | ||||
|     if (reachedEndOfLegibleTrace) { | ||||
|       break | ||||
|     } | ||||
| 
 | ||||
|     if (!line.includes("node_modules")) { | ||||
|       console.log(` ${line}`) | ||||
|       if (rootFile.test(line)) { | ||||
|         reachedEndOfLegibleTrace = true | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -5,6 +5,7 @@ | ||||
|       "DOM", | ||||
|       "DOM.Iterable" | ||||
|     ], | ||||
|     "experimentalDecorators": true, | ||||
|     "module": "esnext", | ||||
|     "target": "esnext", | ||||
|     "moduleResolution": "node", | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user