finish path refactoring, add sourcemap + better trace support

This commit is contained in:
Jacky Zhao 2023-07-15 23:02:12 -07:00
parent ffb2c9ba57
commit 2c0f21a97d
36 changed files with 331 additions and 1170 deletions

1
.gitignore vendored
View File

@ -2,5 +2,6 @@
.gitignore .gitignore
node_modules node_modules
public public
tsconfig.tsbuildinfo
.obsidian .obsidian
.quartz-cache .quartz-cache

View File

@ -40,7 +40,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `dark`: header text and icons - `dark`: header text and icons
- `secondary`: link colour, current [[graph view|graph]] node - `secondary`: link colour, current [[graph view|graph]] node
- `tertiary`: hover states and visited [[graph view|graph]] nodes - `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 ## Plugins
You can think of Quartz plugins as a series of transformations over content. 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. By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz.
> [!note] > [!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. 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.

View File

@ -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. Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
## Formatting ## Formatting

View File

@ -1,7 +1,5 @@
--- ---
title: Syntax Highlighting 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. 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.

View File

@ -0,0 +1,5 @@
---
title: "Table of Contents"
tags:
- component
---

View File

@ -1,5 +1,7 @@
- fixes - fixes
- changing `_index` files
- typography
- CLI - CLI
- update - update
- push - 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) - 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) - [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) - 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

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.0.4", "version": "4.0.5",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@ -48,6 +48,7 @@
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.8",
"preact": "^10.14.1", "preact": "^10.14.1",
"preact-render-to-string": "^6.0.3", "preact-render-to-string": "^6.0.3",
"pretty-bytes": "^6.1.0",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
@ -65,6 +66,7 @@
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.0.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"source-map-support": "^0.5.21",
"to-vfile": "^7.2.4", "to-vfile": "^7.2.4",
"unified": "^10.1.2", "unified": "^10.1.2",
"unist-util-visit": "^4.1.2", "unist-util-visit": "^4.1.2",

View File

@ -9,6 +9,7 @@ import { sassPlugin } from 'esbuild-sass-plugin'
import fs from 'fs' import fs from 'fs'
import { intro, isCancel, outro, select, text } from '@clack/prompts' import { intro, isCancel, outro, select, text } from '@clack/prompts'
import { rimraf } from 'rimraf' import { rimraf } from 'rimraf'
import prettyBytes from 'pretty-bytes'
const cacheFile = "./.quartz-cache/transpiled-build.mjs" const cacheFile = "./.quartz-cache/transpiled-build.mjs"
const fp = "./quartz/build.ts" 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) => { .command('build', 'Build Quartz into a bundle of static HTML files', BuildArgv, async (argv) => {
await esbuild.build({ const result = await esbuild.build({
entryPoints: [fp], entryPoints: [fp],
outfile: path.join("quartz", cacheFile), outfile: path.join("quartz", cacheFile),
bundle: true, bundle: true,
@ -143,6 +144,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
jsx: "automatic", jsx: "automatic",
jsxImportSource: "preact", jsxImportSource: "preact",
packages: "external", packages: "external",
metafile: true,
sourcemap: true,
plugins: [ plugins: [
sassPlugin({ sassPlugin({
type: 'css-text', type: 'css-text',
@ -186,6 +189,12 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
process.exit(1) 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) const { default: init } = await import(cacheFile)
init(argv, version) init(argv, version)
}) })

View File

@ -1,3 +1,4 @@
import 'source-map-support/register.js'
import path from "path" import path from "path"
import { PerfTimer } from "./perf" import { PerfTimer } from "./perf"
import { rimraf } from "rimraf" import { rimraf } from "rimraf"
@ -9,6 +10,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 } from "./path"
interface Argv { interface Argv {
directory: string 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')}`) 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 parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose)
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose) const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
await emitContent(argv.directory, output, cfg, filteredContent, argv.verbose) await emitContent(argv.directory, output, cfg, filteredContent, argv.verbose)

View File

@ -1,20 +1,19 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss" import style from "./styles/backlinks.scss"
import { relativeToRoot } from "../path" import { canonicalizeServer, resolveRelative } from "../path"
import { clientSideSlug } from "./scripts/util"
function Backlinks({ fileData, allFiles }: QuartzComponentProps) { function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
const slug = fileData.slug! const slug = canonicalizeServer(fileData.slug!)
const backlinkFiles = allFiles.filter(file => file.links?.includes(slug)) const backlinkFiles = allFiles.filter(file => file.links?.includes(slug))
return <div class="backlinks"> return <div class="backlinks">
<h3>Backlinks</h3> <h3>Backlinks</h3>
<ul class="overflow"> <ul class="overflow">
{backlinkFiles.length > 0 ? {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>} : <li>No backlinks found</li>}
</ul> </ul>
</div> </div>
} }
Backlinks.css = style Backlinks.css = style
export default (() => Backlinks) satisfies QuartzComponentConstructor export default (() => Backlinks) satisfies QuartzComponentConstructor

View File

@ -1,10 +1,10 @@
import { toServerSlug, pathToRoot } from "../path" import { canonicalizeServer, pathToRoot } from "../path"
import { JSResourceToScriptElement } from "../resources" import { JSResourceToScriptElement } from "../resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default (() => { export default (() => {
function Head({ fileData, externalResources }: QuartzComponentProps) { function Head({ fileData, externalResources }: QuartzComponentProps) {
const slug = toServerSlug(fileData.slug!) const slug = canonicalizeServer(fileData.slug!)
const title = fileData.frontmatter?.title ?? "Untitled" const title = fileData.frontmatter?.title ?? "Untitled"
const description = fileData.description ?? "No description provided" const description = fileData.description ?? "No description provided"
const { css, js } = externalResources const { css, js } = externalResources

View File

@ -1,7 +1,6 @@
import { relativeToRoot } from "../path" import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { Date } from "./Date" import { Date } from "./Date"
import { clientSideSlug } from "./scripts/util"
import { QuartzComponentProps } from "./types" import { QuartzComponentProps } from "./types"
function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number { function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number {
@ -22,22 +21,23 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb
} }
export function PageList({ fileData, allFiles }: QuartzComponentProps) { export function PageList({ fileData, allFiles }: QuartzComponentProps) {
const slug = fileData.slug! const slug = canonicalizeServer(fileData.slug!)
return <ul class="section-ul"> return <ul class="section-ul">
{allFiles.sort(byDateAndAlphabetical).map(page => { {allFiles.sort(byDateAndAlphabetical).map(page => {
const title = page.frontmatter?.title const title = page.frontmatter?.title
const pageSlug = page.slug! const pageSlug = canonicalizeServer(page.slug!)
const tags = page.frontmatter?.tags ?? [] const tags = page.frontmatter?.tags ?? []
return <li class="section-li"> return <li class="section-li">
<div class="section"> <div class="section">
{page.dates && <p class="meta"> {page.dates && <p class="meta">
<Date date={page.dates.modified} /> <Date date={page.dates.modified} />
</p>} </p>}
<div class="desc"> <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> </div>
<ul class="tags"> <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> </ul>
</div> </div>
</li> </li>

View File

@ -1,9 +1,9 @@
import { pathToRoot } from "../path" import { canonicalizeServer, pathToRoot } from "../path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function PageTitle({ fileData, cfg }: QuartzComponentProps) { function PageTitle({ fileData, cfg }: QuartzComponentProps) {
const title = cfg?.pageTitle ?? "Untitled Quartz" const title = cfg?.pageTitle ?? "Untitled Quartz"
const slug = fileData.slug! const slug = canonicalizeServer(fileData.slug!)
const baseDir = pathToRoot(slug) const baseDir = pathToRoot(slug)
return <h1 class="page-title"><a href={baseDir}>{title}</a></h1> return <h1 class="page-title"><a href={baseDir}>{title}</a></h1>
} }

View File

@ -1,10 +1,10 @@
import { pathToRoot } from "../path" import { canonicalizeServer, pathToRoot } from "../path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { slug as slugAnchor } from 'github-slugger' import { slug as slugAnchor } from 'github-slugger'
function TagList({ fileData }: QuartzComponentProps) { function TagList({ fileData }: QuartzComponentProps) {
const tags = fileData.frontmatter?.tags const tags = fileData.frontmatter?.tags
const slug = fileData.slug! const slug = canonicalizeServer(fileData.slug!)
const baseDir = pathToRoot(slug) const baseDir = pathToRoot(slug)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
return <ul class="tags">{tags.map(tag => { return <ul class="tags">{tags.map(tag => {

View File

@ -5,11 +5,11 @@ import path from "path"
import style from '../styles/listPage.scss' import style from '../styles/listPage.scss'
import { PageList } from "../PageList" import { PageList } from "../PageList"
import { toServerSlug } from "../../path" import { canonicalizeServer } from "../../path"
function FolderContent(props: QuartzComponentProps) { function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props const { tree, fileData, allFiles } = props
const folderSlug = toServerSlug(fileData.slug!) const folderSlug = canonicalizeServer(fileData.slug!)
const allPagesInFolder = allFiles.filter(file => { const allPagesInFolder = allFiles.filter(file => {
const fileSlug = file.slug ?? "" const fileSlug = file.slug ?? ""
const prefixed = fileSlug.startsWith(folderSlug) const prefixed = fileSlug.startsWith(folderSlug)
@ -23,7 +23,7 @@ function FolderContent(props: QuartzComponentProps) {
...props, ...props,
allFiles: allPagesInFolder allFiles: allPagesInFolder
} }
// @ts-ignore // @ts-ignore
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <div class="popover-hint"> return <div class="popover-hint">

View File

@ -3,14 +3,14 @@ import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime" import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import style from '../styles/listPage.scss' import style from '../styles/listPage.scss'
import { PageList } from "../PageList" import { PageList } from "../PageList"
import { toServerSlug } from "../../path" import { ServerSlug, canonicalizeServer } from "../../path"
function TagContent(props: QuartzComponentProps) { function TagContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props const { tree, fileData, allFiles } = props
const slug = fileData.slug const slug = fileData.slug
if (slug?.startsWith("tags/")) { 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 allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag))
const listProps = { const listProps = {
...props, ...props,
@ -27,7 +27,7 @@ function TagContent(props: QuartzComponentProps) {
</div> </div>
</div> </div>
} else { } 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}`)
} }
} }

View File

@ -1,7 +1,7 @@
import { ContentDetails } from "../../plugins/emitters/contentIndex" import { ContentDetails } from "../../plugins/emitters/contentIndex"
import * as d3 from 'd3' import * as d3 from 'd3'
import { registerEscapeHandler, clientSideRelativePath, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { CanonicalSlug } from "../../path" import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path"
type NodeData = { type NodeData = {
id: CanonicalSlug, id: CanonicalSlug,
@ -25,7 +25,7 @@ function addToVisited(slug: CanonicalSlug) {
localStorage.setItem(localStorageKey, JSON.stringify([...visited])) localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
} }
async function renderGraph(container: string, slug: string) { async function renderGraph(container: string, slug: CanonicalSlug) {
const visited = getVisited() const visited = getVisited()
const graph = document.getElementById(container) const graph = document.getElementById(container)
if (!graph) return if (!graph) return
@ -50,18 +50,17 @@ async function renderGraph(container: string, slug: string) {
const outgoing = details.links ?? [] const outgoing = details.links ?? []
for (const dest of outgoing) { for (const dest of outgoing) {
if (src in data && dest in data) { 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 neighbourhood = new Set<CanonicalSlug>()
const wl: (CanonicalSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
const wl = [slug, "__SENTINEL"]
if (depth >= 0) { if (depth >= 0) {
while (depth >= 0 && wl.length > 0) { while (depth >= 0 && wl.length > 0) {
// compute neighbours // compute neighbours
const cur = wl.shift() const cur = wl.shift()!
if (cur === "__SENTINEL") { if (cur === "__SENTINEL") {
depth-- depth--
wl.push("__SENTINEL") wl.push("__SENTINEL")
@ -73,11 +72,11 @@ async function renderGraph(container: string, slug: string) {
} }
} }
} else { } 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[] } = { 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)) 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) .attr("fill", color)
.style("cursor", "pointer") .style("cursor", "pointer")
.on("click", (_, d) => { .on("click", (_, d) => {
const targ = clientSideRelativePath(slug, d.id) const targ = resolveRelative(slug, d.id)
window.spaNavigate(new URL(targ)) window.spaNavigate(new URL(targ, getClientSlug(window)))
}) })
.on("mouseover", function(_, d) { .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)) const neighbourNodes = d3.selectAll<HTMLElement, NodeData>(".node").filter((d) => neighbours.includes(d.id))
console.log(neighbourNodes)
const currentId = d.id const currentId = d.id
const linkNodes = d3 const linkNodes = d3
.selectAll(".link") .selectAll(".link")
@ -273,7 +273,7 @@ async function renderGraph(container: string, slug: string) {
} }
function renderGlobalGraph() { function renderGlobalGraph() {
const slug = document.body.dataset["slug"]! const slug = getCanonicalSlug(window)
const container = document.getElementById("global-graph-outer") const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement const sidebar = container?.closest(".sidebar") as HTMLElement
container?.classList.add("active") container?.classList.add("active")

View File

@ -1,13 +1,14 @@
import { Document } from "flexsearch" import { Document } from "flexsearch"
import { ContentDetails } from "../../plugins/emitters/contentIndex" import { ContentDetails } from "../../plugins/emitters/contentIndex"
import { registerEscapeHandler, clientSideRelativePath, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { CanonicalSlug } from "../../path" import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path"
interface Item { interface Item {
slug: CanonicalSlug, slug: CanonicalSlug,
title: string, title: string,
content: string, content: string,
} }
let index: Document<Item> | undefined = undefined let index: Document<Item> | undefined = undefined
const contextWindowWords = 30 const contextWindowWords = 30
@ -113,8 +114,8 @@ document.addEventListener("nav", async (e: unknown) => {
button.id = slug button.id = slug
button.innerHTML = `<h3>${title}</h3><p>${content}</p>` button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
button.addEventListener('click', () => { button.addEventListener('click', () => {
const targ = clientSideRelativePath(currentSlug, slug) const targ = resolveRelative(currentSlug, slug)
window.spaNavigate(new URL(targ)) window.spaNavigate(new URL(targ, getClientSlug(window)))
}) })
return button return button
} }
@ -137,9 +138,9 @@ document.addEventListener("nav", async (e: unknown) => {
function onType(e: HTMLElementEventMap["input"]) { function onType(e: HTMLElementEventMap["input"]) {
const term = (e.target as HTMLInputElement).value const term = (e.target as HTMLInputElement).value
const searchResults = index?.search(term, numSearchResults) ?? [] const searchResults = index?.search(term, numSearchResults) ?? []
const getByField = (field: string): string[] => { const getByField = (field: string): CanonicalSlug[] => {
const results = searchResults.filter((x) => x.field === field) 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 // order titles ahead of content

View File

@ -1,5 +1,5 @@
import micromorph from "micromorph" import micromorph from "micromorph"
import { CanonicalSlug, RelativeURL } from "../../path" import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../path"
// adapted from `micromorph` // adapted from `micromorph`
// https://github.com/natemoo-re/micromorph // https://github.com/natemoo-re/micromorph
@ -43,6 +43,7 @@ async function navigate(url: URL, isBack: boolean = false) {
.catch(() => { .catch(() => {
window.location.assign(url) window.location.assign(url)
}) })
if (!contents) return; if (!contents) return;
if (!isBack) { if (!isBack) {
history.pushState({}, "", url) history.pushState({}, "", url)
@ -70,7 +71,7 @@ async function navigate(url: URL, isBack: boolean = false) {
const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])') const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])')
elementsToAdd.forEach(el => document.head.appendChild(el)) elementsToAdd.forEach(el => document.head.appendChild(el))
notifyNav(document.body.dataset.slug!) notifyNav(getCanonicalSlug(window))
delete announcer.dataset.persist delete announcer.dataset.persist
} }
@ -117,7 +118,7 @@ function createRouter() {
} }
createRouter() createRouter()
notifyNav(document.body.dataset.slug!) notifyNav(getCanonicalSlug(window))
if (!customElements.get('route-announcer')) { if (!customElements.get('route-announcer')) {
const attrs = { const attrs = {

View File

@ -24,23 +24,21 @@ describe('typeguards', () => {
}) })
test('isCanonicalSlug', () => { test('isCanonicalSlug', () => {
assert(path.isCanonicalSlug("/")) assert(path.isCanonicalSlug(""))
assert(path.isCanonicalSlug("/abc")) assert(path.isCanonicalSlug("abc"))
assert(path.isCanonicalSlug("/notindex")) assert(path.isCanonicalSlug("notindex"))
assert(path.isCanonicalSlug("/notindex/def")) assert(path.isCanonicalSlug("notindex/def"))
assert(!path.isCanonicalSlug("//")) assert(!path.isCanonicalSlug("//"))
assert(!path.isCanonicalSlug("/index"))
assert(!path.isCanonicalSlug(""))
assert(!path.isCanonicalSlug("index")) assert(!path.isCanonicalSlug("index"))
assert(!path.isCanonicalSlug("index/abc"))
assert(!path.isCanonicalSlug("https://example.com")) assert(!path.isCanonicalSlug("https://example.com"))
assert(!path.isCanonicalSlug("/abc/")) assert(!path.isCanonicalSlug("/abc"))
assert(!path.isCanonicalSlug("/abc/index")) assert(!path.isCanonicalSlug("abc/"))
assert(!path.isCanonicalSlug("/abc#anchor")) assert(!path.isCanonicalSlug("abc/index"))
assert(!path.isCanonicalSlug("/abc?query=1")) assert(!path.isCanonicalSlug("abc#anchor"))
assert(!path.isCanonicalSlug("/index.md")) assert(!path.isCanonicalSlug("abc?query=1"))
assert(!path.isCanonicalSlug("/index.html")) assert(!path.isCanonicalSlug("index.md"))
assert(!path.isCanonicalSlug("index.html"))
}) })
test('isRelativeURL', () => { test('isRelativeURL', () => {
@ -52,6 +50,7 @@ describe('typeguards', () => {
assert(path.isRelativeURL("../abc/def")) assert(path.isRelativeURL("../abc/def"))
assert(!path.isRelativeURL("abc")) assert(!path.isRelativeURL("abc"))
assert(!path.isRelativeURL("/abc/def"))
assert(!path.isRelativeURL("")) assert(!path.isRelativeURL(""))
assert(!path.isRelativeURL("../")) assert(!path.isRelativeURL("../"))
assert(!path.isRelativeURL("./")) assert(!path.isRelativeURL("./"))
@ -60,25 +59,23 @@ describe('typeguards', () => {
}) })
test('isServerSlug', () => { test('isServerSlug', () => {
assert(path.isServerSlug("/index")) assert(path.isServerSlug("index"))
assert(path.isServerSlug("/abc/def")) assert(path.isServerSlug("abc/def"))
assert(!path.isServerSlug("/"))
assert(!path.isServerSlug(".")) assert(!path.isServerSlug("."))
assert(!path.isServerSlug("./abc/def")) assert(!path.isServerSlug("./abc/def"))
assert(!path.isServerSlug("../abc/def")) assert(!path.isServerSlug("../abc/def"))
assert(!path.isServerSlug("/index.html")) assert(!path.isServerSlug("index.html"))
assert(!path.isServerSlug("/abc/def.html")) assert(!path.isServerSlug("abc/def.html"))
assert(!path.isServerSlug("/abc/def#anchor")) assert(!path.isServerSlug("abc/def#anchor"))
assert(!path.isServerSlug("/abc/def?query=1")) assert(!path.isServerSlug("abc/def?query=1"))
assert(!path.isServerSlug("/note with spaces")) assert(!path.isServerSlug("note with spaces"))
}) })
test('isFilePath', () => { test('isFilePath', () => {
assert(path.isFilePath("/content/index.md")) assert(path.isFilePath("content/index.md"))
assert(path.isFilePath("/content/test.png")) assert(path.isFilePath("content/test.png"))
assert(!path.isFilePath("../test.pdf")) assert(!path.isFilePath("../test.pdf"))
assert(!path.isFilePath("content/test.png"))
assert(!path.isFilePath("content/test")) assert(!path.isFilePath("content/test"))
assert(!path.isFilePath("./content/test")) assert(!path.isFilePath("./content/test"))
}) })
@ -90,43 +87,45 @@ describe('transforms', () => {
for (const [inp, expected] of pairs) { for (const [inp, expected] of pairs) {
assert(checkPre(inp), `${inp} wasn't the expected input type`) assert(checkPre(inp), `${inp} wasn't the expected input type`)
const actual = transform(inp) 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`) assert(checkPost(actual), `${actual} wasn't the expected output type`)
} }
} }
test('canonicalizeServer', () => { test('canonicalizeServer', () => {
asserts([ asserts([
["/index", "/"], ["index", ""],
["/abc/def", "/abc/def"], ["abc/index", "abc"],
["abc/def", "abc/def"],
], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug) ], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug)
}) })
test('canonicalizeClient', () => { test('canonicalizeClient', () => {
asserts([ asserts([
["http://localhost:3000", "/"], ["http://localhost:3000", ""],
["http://localhost:3000/index", "/"], ["http://localhost:3000/index", ""],
["http://localhost:3000/test", "/test"], ["http://localhost:3000/test", "test"],
["http://example.com", "/"], ["http://example.com", ""],
["http://example.com/index", "/"], ["http://example.com/index", ""],
["http://example.com/index.html", "/"], ["http://example.com/index.html", ""],
["http://example.com/", "/"], ["http://example.com/", ""],
["https://example.com", "/"], ["https://example.com", ""],
["https://example.com/abc/def", "/abc/def"], ["https://example.com/abc/def", "abc/def"],
["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#cool", "abc/def"],
["https://example.com/abc/def?field=1&another=2", "/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?field=1&another=2#cool", "abc/def"],
["https://example.com/abc/def.html?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) ], path.canonicalizeClient, path.isClientSlug, path.isCanonicalSlug)
}) })
describe('slugifyFilePath', () => { describe('slugifyFilePath', () => {
asserts([ asserts([
["/content/index.md", "/content/index"], ["content/index.md", "content/index"],
["/content/cool.png", "/content/cool"], ["/content/index.md", "content/index"],
["/index.md", "/index"], ["content/cool.png", "content/cool"],
["/note with spaces.md", "/note-with-spaces"], ["index.md", "index"],
["note with spaces.md", "note-with-spaces"],
], path.slugifyFilePath, path.isFilePath, path.isServerSlug) ], path.slugifyFilePath, path.isFilePath, path.isServerSlug)
}) })
@ -146,13 +145,14 @@ describe('transforms', () => {
["/tags/", "./tags"], ["/tags/", "./tags"],
["content/with spaces", "./content/with-spaces"], ["content/with spaces", "./content/with-spaces"],
["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"], ["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', () => { describe('pathToRoot', () => {
asserts([ asserts([
["/", "."], ["", "."],
["/abc/def", "../.."], ["abc", ".."],
["abc/def", "../.."],
], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL) ], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL)
}) })
}) })

View File

@ -1,5 +1,5 @@
import path from 'path'
import { slug as slugAnchor } from 'github-slugger' import { slug as slugAnchor } from 'github-slugger'
import { trace } from './trace'
// Quartz Paths // Quartz Paths
// Things in boxes are not actual types but rather sources which these types can be acquired from // 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 │ // │ getClientSlug() │ .href │
// │ ▼ ▼ // │ ▼ ▼
// │
// │ Client Slug Relative URL
// getCanonicalSlug() │ https://test.ca/note/abc#anchor?query=123 ../note/def#anchor
// │ // │
// │ canonicalizeClient() │ ▲ // │ Client Slug ┌───► Relative URL
// │ ▼ │ // getCanonicalSlug() │ https://test.ca/note/abc#anchor?query=123 │ ../note/def#anchor
// │ │ // │ │
// └───────────────► Canonical Slug │ // │ canonicalizeClient() │ │ ▲ ▲
// /note/abc │ // │ ▼ │ │ │
// │ // │ pathToRoot() │ │ │
// ▲ │ // └───────────────► Canonical Slug ────────────────┘ │ │
// note/abc │ │
// ──────────────────────────┘ │
// ▲ resolveRelative() │
// canonicalizeServer() │ │ // canonicalizeServer() │ │
// │ // │
// HTML File Server Slug │ // 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 │ // │ File Path │ │ Wikilinks │ │ Markdown Link │
// │ /note/abc/index.md │ └───────────┘ └───────────────┘ // │ note/abc/index.md │ └───────────┘ └───────────────┘
// └────────────────────┘ ▲ ▲ // └────────────────────┘ ▲ ▲
// ▲ │ │ // ▲ │ │
// │ ┌─────────┐ │ │ // │ ┌─────────┐ │ │
// └────────────┤ MD File ├─────┴─────────────────┘ // └────────────┤ 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 /// Utility type to simulate nominal types in TypeScript
type SlugLike<T> = string & { __brand: T } type SlugLike<T> = string & { __brand: T }
/** Client-side slug, usually obtained through `window.location` */ /** Client-side slug, usually obtained through `window.location` */
export type ClientSlug = SlugLike<"client"> export type ClientSlug = SlugLike<"client">
export function isClientSlug(s: string): s is ClientSlug { 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. /** 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 type CanonicalSlug = SlugLike<"canonical">
export function isCanonicalSlug(s: string): s is CanonicalSlug { export function isCanonicalSlug(s: string): s is CanonicalSlug {
const validStart = s.startsWith("/") const validStart = !(s.startsWith(".") || s.startsWith("/"))
const validEnding = s.length === 1 || (!s.endsWith("/") && !s.endsWith("/index")) const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index")
return !_containsForbiddenCharacters(s) && validStart && validEnding && !_hasFileExtension(s) return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
} }
/** A relative link, can be found on `href`s but can also be constructed for /** 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 type RelativeURL = SlugLike<"relative">
export function isRelativeURL(s: string): s is RelativeURL { export function isRelativeURL(s: string): s is RelativeURL {
const validStart = /^\.{1,2}/.test(s) 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) return validStart && validEnding && !_hasFileExtension(s)
} }
/** A server side slug. This is what Quartz uses to emit files so uses index suffixes */ /** A server side slug. This is what Quartz uses to emit files so uses index suffixes */
export type ServerSlug = SlugLike<"server"> export type ServerSlug = SlugLike<"server">
export function isServerSlug(s: string): s is ServerSlug { export function isServerSlug(s: string): s is ServerSlug {
// must start with forward slash const validStart = !(s.startsWith(".") || s.startsWith("/"))
const validStart = s.startsWith("/")
const validEnding = !s.endsWith("/") const validEnding = !s.endsWith("/")
return validStart && validEnding && !_containsForbiddenCharacters(s) && !_hasFileExtension(s) 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 */ /** The real file path to a file on disk */
export type FilePath = SlugLike<"filepath"> export type FilePath = SlugLike<"filepath">
export function isFilePath(s: string): s is 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 { 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 { 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 { export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
conditionCheck(canonicalizeClient.name, 'pre', slug, isClientSlug)
const { pathname } = new URL(slug) const { pathname } = new URL(slug)
let fp = pathname let fp = pathname.slice(1)
fp = fp.replace(new RegExp(path.extname(fp) + '$'), '') fp = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
return _canonicalize(fp) as CanonicalSlug const res = _canonicalize(fp) as CanonicalSlug
conditionCheck(canonicalizeClient.name, 'post', res, isCanonicalSlug)
return res
} }
export function canonicalizeServer(slug: ServerSlug): CanonicalSlug { export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
conditionCheck(canonicalizeServer.name, 'pre', slug, isServerSlug)
let fp = slug as string 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 { export function slugifyFilePath(fp: FilePath): ServerSlug {
// strip file extension conditionCheck(slugifyFilePath.name, 'pre', fp, isFilePath)
const withoutFileExt = fp.replace(new RegExp(path.extname(fp) + '$'), '') fp = _stripSlashes(fp) as FilePath
const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
const slug = withoutFileExt const slug = withoutFileExt
.split(path.sep) // fs can have diff interpretations of / .split('/')
.map((segment) => segment.replace(/\s/g, '-')) // slugify all segments .map((segment) => segment.replace(/\s/g, '-')) // slugify all segments
.join('/') // always use / as sep .join('/') // always use / as sep
.replace(/\/$/, '') // remove trailing slash .replace(/\/$/, '') // remove trailing slash
conditionCheck(slugifyFilePath.name, 'post', slug, isServerSlug)
return slug as ServerSlug return slug as ServerSlug
} }
export function transformInternalLink(link: string): RelativeURL { 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 segments = fplike.split("/").filter(x => x.length > 0)
let prefix = segments.filter(_isRelativeSegment).join("/") 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)) fp = canonicalizeServer(slugifyFilePath(fp as FilePath))
if (fp.endsWith("index")) { if (fp.endsWith("index")) {
fp = fp.slice(0, -"index".length) fp = fp.slice(0, -"index".length)
} }
let joined = [_stripSlashes(prefix), _stripSlashes(fp)].filter(x => x !== "").join("/") let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor) const res = _addRelativeToStart(joined) + anchor as RelativeURL
return _addRelativeToStart(joined) + anchor as RelativeURL conditionCheck(transformInternalLink.name, 'post', res, isRelativeURL)
return res
} }
// resolve /a/b/c to ../../ // resolve /a/b/c to ../../
export function pathToRoot(slug: CanonicalSlug): RelativeURL { export function pathToRoot(slug: CanonicalSlug): RelativeURL {
conditionCheck(pathToRoot.name, 'pre', slug, isCanonicalSlug)
let rootPath = slug let rootPath = slug
.split('/') .split('/')
.filter(x => x !== '') .filter(x => x !== '')
.map(_ => '..') .map(_ => '..')
.join('/') .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" export const QUARTZ = "quartz"
@ -153,16 +206,7 @@ function _canonicalize(fp: string): string {
fp = fp.slice(0, -"index".length) fp = fp.slice(0, -"index".length)
} }
// remove trailing slash return _stripSlashes(fp)
if (fp.endsWith("/")) {
fp = fp.slice(0, -1)
}
if (fp.length === 0) {
return "/" as CanonicalSlug
}
return fp
} }
function _containsForbiddenCharacters(s: string): boolean { function _containsForbiddenCharacters(s: string): boolean {
@ -170,7 +214,11 @@ function _containsForbiddenCharacters(s: string): boolean {
} }
function _hasFileExtension(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 { function _isRelativeSegment(s: string): boolean {
@ -195,7 +243,7 @@ function _addRelativeToStart(s: string): string {
} }
if (!s.startsWith(".")) { if (!s.startsWith(".")) {
s = "./" + s s = joinSegments(".", s)
} }
return s return s

View File

@ -1,4 +1,4 @@
import { CanonicalSlug, FilePath, ServerSlug, relativeToRoot } from "../../path" import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, resolveRelative } from "../../path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import path from 'path' import path from 'path'
@ -11,7 +11,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
const fps: FilePath[] = [] const fps: FilePath[] = []
for (const [_tree, file] of content) { 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) const dir = path.relative(contentFolder, file.dirname ?? contentFolder)
let aliases: CanonicalSlug[] = [] let aliases: CanonicalSlug[] = []
@ -22,12 +22,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
} }
for (const alias of aliases) { for (const alias of aliases) {
const slug = (alias.startsWith("/") const slug = path.posix.join(dir, alias) as ServerSlug
? alias
: path.posix.join(dir, alias)) as ServerSlug
const fp = slug + ".html" as FilePath const fp = slug + ".html" as FilePath
const redirUrl = relativeToRoot(slug, ogSlug) const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
await emit({ await emit({
content: ` content: `
<!DOCTYPE html> <!DOCTYPE html>

View File

@ -1,5 +1,5 @@
import { GlobalConfiguration } from "../../cfg" import { GlobalConfiguration } from "../../cfg"
import { CanonicalSlug, ClientSlug } from "../../path" import { CanonicalSlug, ClientSlug, FilePath, ServerSlug, canonicalizeServer } from "../../path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import path from "path" import path from "path"
@ -65,10 +65,10 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
return { return {
name: "ContentIndex", name: "ContentIndex",
async emit(_contentDir, cfg, content, _resources, emit) { async emit(_contentDir, cfg, content, _resources, emit) {
const emitted: string[] = [] const emitted: FilePath[] = []
const linkIndex: ContentIndex = new Map() const linkIndex: ContentIndex = new Map()
for (const [_tree, file] of content) { 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() const date = file.data.dates?.modified ?? new Date()
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
linkIndex.set(slug, { linkIndex.set(slug, {
@ -85,22 +85,22 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
if (opts?.enableSiteMap) { if (opts?.enableSiteMap) {
await emit({ await emit({
content: generateSiteMap(cfg, linkIndex), content: generateSiteMap(cfg, linkIndex),
slug: "sitemap", slug: "sitemap" as ServerSlug,
ext: ".xml" ext: ".xml"
}) })
emitted.push("sitemap.xml") emitted.push("sitemap.xml" as FilePath)
} }
if (opts?.enableRSS) { if (opts?.enableRSS) {
await emit({ await emit({
content: generateRSSFeed(cfg, linkIndex), content: generateRSSFeed(cfg, linkIndex),
slug: "index", slug: "index" as ServerSlug,
ext: ".xml" 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( const simplifiedIndex = Object.fromEntries(
Array.from(linkIndex).map(([slug, content]) => { Array.from(linkIndex).map(([slug, content]) => {
// remove description and from content index as nothing downstream // remove description and from content index as nothing downstream
@ -117,7 +117,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
slug: fp, slug: fp,
ext: ".json", ext: ".json",
}) })
emitted.push(`${fp}.json`) emitted.push(`${fp}.json` as FilePath)
return emitted return emitted
}, },

View File

@ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg" import { FullPageLayout } from "../../cfg"
import { FilePath } from "../../path" import { FilePath, canonicalizeServer } from "../../path"
export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
if (!opts) { if (!opts) {
@ -24,7 +24,7 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
const fps: FilePath[] = [] const fps: FilePath[] = []
const allFiles = content.map(c => c[1].data) const allFiles = content.map(c => c[1].data)
for (const [tree, file] of content) { for (const [tree, file] of content) {
const slug = file.data.slug! const slug = canonicalizeServer(file.data.slug!)
const externalResources = pageResources(slug, resources) const externalResources = pageResources(slug, resources)
const componentData: QuartzComponentProps = { const componentData: QuartzComponentProps = {
fileData: file.data, fileData: file.data,

View File

@ -6,7 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, defaultProcessedContent } from "../vfile" import { ProcessedContent, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg" import { FullPageLayout } from "../../cfg"
import path from "path" import path from "path"
import { FilePath, toServerSlug } from "../../path" import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, joinSegments } from "../../path"
export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
if (!opts) { if (!opts) {
@ -23,28 +23,34 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer] return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer]
}, },
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> { async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
const fps: string[] = [] const fps: FilePath[] = []
const allFiles = content.map(c => c[1].data) 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 // remove special prefixes
folders.delete(".") folders.delete("tags" as CanonicalSlug)
folders.delete("tags")
const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([ 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) { for (const [tree, file] of content) {
const slug = toServerSlug(file.data.slug!) const slug = canonicalizeServer(file.data.slug!)
if (folders.has(slug)) { if (folders.has(slug)) {
folderDescriptions[slug] = [tree, file] folderDescriptions[slug] = [tree, file]
} }
} }
for (const folder of folders) { for (const folder of folders) {
const slug = folder const slug = folder
const externalResources = pageResources(slug, resources) const externalResources = pageResources(slug, resources)
const [tree, file] = folderDescriptions[folder] const [tree, file] = folderDescriptions[folder]
const componentData: QuartzComponentProps = { const componentData: QuartzComponentProps = {
@ -63,7 +69,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
externalResources externalResources
) )
const fp = file.data.slug + ".html" const fp = file.data.slug! + ".html" as FilePath
await emit({ await emit({
content, content,
slug: file.data.slug!, slug: file.data.slug!,

View File

@ -5,7 +5,7 @@ import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, defaultProcessedContent } from "../vfile" import { ProcessedContent, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg" import { FullPageLayout } from "../../cfg"
import { FilePath, ServerSlug, toServerSlug } from "../../path" import { CanonicalSlug, FilePath, ServerSlug } from "../../path"
export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
if (!opts) { if (!opts) {
@ -31,7 +31,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
]))) ])))
for (const [tree, file] of content) { for (const [tree, file] of content) {
const slug = toServerSlug(file.data.slug!) const slug = file.data.slug!
if (slug.startsWith("tags/")) { if (slug.startsWith("tags/")) {
const tag = slug.slice("tags/".length) const tag = slug.slice("tags/".length)
if (tags.has(tag)) { if (tags.has(tag)) {
@ -41,7 +41,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
} }
for (const tag of tags) { for (const tag of tags) {
const slug = `tags/${tag}` const slug = `tags/${tag}` as CanonicalSlug
const externalResources = pageResources(slug, resources) const externalResources = pageResources(slug, resources)
const [tree, file] = tagDescriptions[tag] const [tree, file] = tagDescriptions[tag]
const componentData: QuartzComponentProps = { const componentData: QuartzComponentProps = {

View File

@ -55,17 +55,17 @@ function joinScripts(scripts: string[]): string {
export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> { export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> {
const fps = await Promise.all([ const fps = await Promise.all([
emit({ emit({
slug: "index", slug: "index" as ServerSlug,
ext: ".css", ext: ".css",
content: joinStyles(cfg.theme, styles, ...res.css) content: joinStyles(cfg.theme, styles, ...res.css)
}), }),
emit({ emit({
slug: "prescript", slug: "prescript" as ServerSlug,
ext: ".js", ext: ".js",
content: joinScripts(res.beforeDOMLoaded) content: joinScripts(res.beforeDOMLoaded)
}), }),
emit({ emit({
slug: "postscript", slug: "postscript" as ServerSlug,
ext: ".js", ext: ".js",
content: joinScripts(res.afterDOMLoaded) content: joinScripts(res.afterDOMLoaded)
}) })

View File

@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types" 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 path from "path"
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
import isAbsoluteUrl from "is-absolute-url" import isAbsoluteUrl from "is-absolute-url"
@ -9,15 +9,11 @@ interface Options {
markdownLinkResolution: 'absolute' | 'relative' | 'shortest' markdownLinkResolution: 'absolute' | 'relative' | 'shortest'
/** Strips folders from a link so that it looks nice */ /** Strips folders from a link so that it looks nice */
prettyLinks: boolean prettyLinks: boolean
indexAnchorLinks: boolean
indexExternalLinks: boolean
} }
const defaultOptions: Options = { const defaultOptions: Options = {
markdownLinkResolution: 'absolute', markdownLinkResolution: 'absolute',
prettyLinks: true, prettyLinks: true,
indexAnchorLinks: false,
indexExternalLinks: false,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
@ -27,32 +23,34 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
htmlPlugins() { htmlPlugins() {
return [() => { return [() => {
return (tree, file) => { return (tree, file) => {
const curSlug = file.data.slug! const curSlug = canonicalizeServer(file.data.slug!)
const transformLink = (target: string) => { const transformLink = (target: string): RelativeURL => {
const targetSlug = transformInternalLink(target) const targetSlug = transformInternalLink(target).slice("./".length)
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) { let [targetCanonical, targetAnchor] = splitAnchor(targetSlug)
return './' + relative(curSlug, targetSlug) if (opts.markdownLinkResolution === 'relative') {
return targetSlug as RelativeURL
} else if (opts.markdownLinkResolution === 'shortest') { } else if (opts.markdownLinkResolution === 'shortest') {
// https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5 // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
const allSlugs = file.data.allSlugs! const allSlugs = file.data.allSlugs!
// if the file name is unique, then it's just the filename // if the file name is unique, then it's just the filename
const matchingFileNames = allSlugs.filter(slug => { 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) const fileName = parts.at(-1)
return targetSlug === fileName return targetCanonical === fileName
}) })
if (matchingFileNames.length === 1) { if (matchingFileNames.length === 1) {
const targetSlug = toServerSlug(matchingFileNames[0]) const targetSlug = canonicalizeServer(matchingFileNames[0])
return './' + relativeToRoot(curSlug, targetSlug) return resolveRelative(curSlug, targetSlug) + targetAnchor as RelativeURL
} }
// if it's not unique, then it's the absolute path from the vault root // if it's not unique, then it's the absolute path from the vault root
// (fall-through case) // (fall-through case)
} }
// treat as absolute // treat as absolute
return './' + relativeToRoot(curSlug, targetSlug) return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL
} }
const outgoing: Set<CanonicalSlug> = new Set() const outgoing: Set<CanonicalSlug> = new Set()
@ -63,26 +61,15 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
node.properties && node.properties &&
typeof node.properties.href === 'string' typeof node.properties.href === 'string'
) { ) {
let dest = node.properties.href let dest = node.properties.href as RelativeURL
node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
// don't process external links or intra-document anchors // don't process external links or intra-document anchors
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
node.properties.href = transformLink(dest) dest = node.properties.href = transformLink(dest)
} const canonicalDest = path.normalize(joinSegments(curSlug, dest))
const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
dest = node.properties.href outgoing.add(destCanonical as CanonicalSlug)
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)
}
} }
// rewrite link internals if prettylinks is on // rewrite link internals if prettylinks is on

View File

@ -2,7 +2,6 @@ import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast' import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast'
import { findAndReplace } from "mdast-util-find-and-replace" import { findAndReplace } from "mdast-util-find-and-replace"
import { slugify } from "../../path"
import { slug as slugAnchor } from 'github-slugger' import { slug as slugAnchor } from 'github-slugger'
import rehypeRaw from "rehype-raw" import rehypeRaw from "rehype-raw"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
@ -10,6 +9,7 @@ import path from "path"
import { JSResource } from "../../resources" import { JSResource } from "../../resources"
// @ts-ignore // @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts" import calloutScript from "../../components/scripts/callout.inline.ts"
import { FilePath, slugifyFilePath, transformInternalLink } from "../../path"
export interface Options { export interface Options {
comments: boolean comments: boolean
@ -139,14 +139,15 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
plugins.push(() => { plugins.push(() => {
return (tree: Root, _file) => { return (tree: Root, _file) => {
findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => { 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 anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim() const alias = rawAlias?.slice(1).trim()
// embed cases // embed cases
if (value.startsWith("!")) { if (value.startsWith("!")) {
const ext = path.extname(fp).toLowerCase() const ext: string | undefined = path.extname(fp).toLowerCase()
const url = slugify(fp.trim()) + ext const url = slugifyFilePath(fp as FilePath) + ext
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) { if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
const dims = alias ?? "" const dims = alias ?? ""
let [width, height] = dims.split("x", 2) let [width, height] = dims.split("x", 2)
@ -176,12 +177,15 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
type: 'html', type: 'html',
value: `<iframe src="${url}"></iframe>` value: `<iframe src="${url}"></iframe>`
} }
} else {
// TODO: this is the node embed case
} }
// otherwise, fall through to regular link // otherwise, fall through to regular link
} }
// internal link // internal link
const url = slugify(fp.trim() + anchor) // const url = transformInternalLink(fp + anchor)
const url = fp + anchor
return { return {
type: 'link', type: 'link',
url, url,

View File

@ -3,7 +3,6 @@ import { Root } from "mdast"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
import { toString } from "mdast-util-to-string" import { toString } from "mdast-util-to-string"
import { slug as slugAnchor } from 'github-slugger' import { slug as slugAnchor } from 'github-slugger'
import { CanonicalSlug } from "../../path"
export interface Options { export interface Options {
maxDepth: 1 | 2 | 3 | 4 | 5 | 6, maxDepth: 1 | 2 | 3 | 4 | 5 | 6,
@ -20,7 +19,7 @@ const defaultOptions: Options = {
interface TocEntry { interface TocEntry {
depth: number, depth: number,
text: string, 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) => { export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {

View File

@ -19,6 +19,7 @@ import popoverStyle from '../components/styles/popover.scss'
import { StaticResources } from "../resources" import { StaticResources } from "../resources"
import { QuartzLogger } from "../log" import { QuartzLogger } from "../log"
import { googleFontHref } from "../theme" import { googleFontHref } from "../theme"
import { trace } from "../trace"
function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) { function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) {
staticResources.css.push(googleFontHref(cfg.theme)) staticResources.css.push(googleFontHref(cfg.theme))
@ -110,7 +111,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
} }
} }
} catch (err) { } 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) process.exit(1)
} }
} }

View File

@ -14,6 +14,7 @@ import workerpool, { Promise as WorkerPromise } from 'workerpool'
import { QuartzTransformerPluginInstance } from '../plugins/types' import { QuartzTransformerPluginInstance } from '../plugins/types'
import { QuartzLogger } from '../log' import { QuartzLogger } from '../log'
import chalk from 'chalk' import chalk from 'chalk'
import { trace } from '../trace'
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void> export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor { export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
@ -101,7 +102,7 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
console.log(`[process] ${fp} -> ${file.data.slug}`) console.log(`[process] ${fp} -> ${file.data.slug}`)
} }
} catch (err) { } catch (err) {
console.log(chalk.red(`\nFailed to process \`${fp}\`: `) + err) trace(`\nFailed to process \`${fp}\``, err as Error)
process.exit(1) process.exit(1)
} }
} }

25
quartz/trace.ts Normal file
View 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
}
}
}
}

View File

@ -5,6 +5,7 @@
"DOM", "DOM",
"DOM.Iterable" "DOM.Iterable"
], ],
"experimentalDecorators": true,
"module": "esnext", "module": "esnext",
"target": "esnext", "target": "esnext",
"moduleResolution": "node", "moduleResolution": "node",

File diff suppressed because one or more lines are too long