finish path refactoring, add sourcemap + better trace support

This commit is contained in:
Jacky Zhao 2023-07-15 23:02:12 -07:00
parent 906f91f8ee
commit 3ac6b42e16
36 changed files with 331 additions and 1170 deletions

1
.gitignore vendored
View File

@ -2,5 +2,6 @@
.gitignore
node_modules
public
tsconfig.tsbuildinfo
.obsidian
.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
- `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.

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.
## Formatting

View File

@ -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.

View File

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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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)
})

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>
}

View File

@ -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 => {

View File

@ -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">

View File

@ -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}`)
}
}

View File

@ -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")

View File

@ -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

View File

@ -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 = {

View File

@ -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)
})
})

View File

@ -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

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 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>

View File

@ -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
},

View File

@ -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,

View File

@ -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!,

View File

@ -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 = {

View File

@ -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)
})

View File

@ -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

View File

@ -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,

View File

@ -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) => {

View File

@ -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)
}
}

View File

@ -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
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.Iterable"
],
"experimentalDecorators": true,
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",

File diff suppressed because one or more lines are too long