Jacky Zhao
2023-07-16 3ac6b42e16dca5a44ed3fec2c0314f1dbbc2322b
finish path refactoring, add sourcemap + better trace support
1 files deleted
1 files added
34 files modified
1489 ■■■■ changed files
.gitignore 1 ●●●● patch | view | raw | blame | history
content/configuration.md 4 ●●●● patch | view | raw | blame | history
content/features/Latex.md 5 ●●●●● patch | view | raw | blame | history
content/features/syntax highlighting.md 2 ●●●●● patch | view | raw | blame | history
content/features/table of contents.md 5 ●●●●● patch | view | raw | blame | history
content/features/upcoming features.md 6 ●●●●● patch | view | raw | blame | history
package-lock.json 953 ●●●●● patch | view | raw | blame | history
package.json 4 ●●● patch | view | raw | blame | history
quartz/bootstrap-cli.mjs 11 ●●●●● patch | view | raw | blame | history
quartz/build.ts 4 ●●● patch | view | raw | blame | history
quartz/components/Backlinks.tsx 7 ●●●●● patch | view | raw | blame | history
quartz/components/Head.tsx 4 ●●●● patch | view | raw | blame | history
quartz/components/PageList.tsx 12 ●●●● patch | view | raw | blame | history
quartz/components/PageTitle.tsx 4 ●●●● patch | view | raw | blame | history
quartz/components/TagList.tsx 4 ●●●● patch | view | raw | blame | history
quartz/components/pages/FolderContent.tsx 4 ●●●● patch | view | raw | blame | history
quartz/components/pages/TagContent.tsx 6 ●●●● patch | view | raw | blame | history
quartz/components/scripts/graph.inline.ts 28 ●●●● patch | view | raw | blame | history
quartz/components/scripts/search.inline.ts 13 ●●●● patch | view | raw | blame | history
quartz/components/scripts/spa.inline.ts 7 ●●●●● patch | view | raw | blame | history
quartz/path.test.ts 96 ●●●● patch | view | raw | blame | history
quartz/path.ts 144 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/aliases.ts 10 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentIndex.ts 18 ●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentPage.tsx 4 ●●●● patch | view | raw | blame | history
quartz/plugins/emitters/folderPage.tsx 22 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/tagPage.tsx 6 ●●●● patch | view | raw | blame | history
quartz/plugins/index.ts 6 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 49 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 14 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/toc.ts 3 ●●●● patch | view | raw | blame | history
quartz/processors/emit.ts 3 ●●●● patch | view | raw | blame | history
quartz/processors/parse.ts 3 ●●●● patch | view | raw | blame | history
quartz/trace.ts 25 ●●●●● patch | view | raw | blame | history
tsconfig.json 1 ●●●● patch | view | raw | blame | history
tsconfig.tsbuildinfo 1 ●●●● patch | view | raw | blame | history
.gitignore
@@ -2,5 +2,6 @@
.gitignore
node_modules
public
tsconfig.tsbuildinfo
.obsidian
.quartz-cache
content/configuration.md
@@ -40,7 +40,7 @@
        - `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 @@
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.
content/features/Latex.md
@@ -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
content/features/syntax highlighting.md
@@ -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.
content/features/table of contents.md
@@ -0,0 +1,5 @@
---
title: "Table of Contents"
tags:
- component
---
content/features/upcoming features.md
@@ -1,5 +1,7 @@
- fixes
    - changing `_index` files
    - typography
- CLI    
    - update
    - push
@@ -30,3 +32,7 @@
        - [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)
- parse all images in page
    - use this for page lists if applicable?
- CV mode?
    - with print stylesheet
package-lock.json
@@ -30,6 +30,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",
@@ -47,6 +48,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",
@@ -68,7 +70,6 @@
        "@types/serve-handler": "^6.1.1",
        "@types/workerpool": "^6.4.0",
        "@types/yargs": "^17.0.24",
        "ava": "^5.3.1",
        "esbuild": "^0.18.11",
        "tsx": "^3.12.7",
        "typescript": "^5.0.4"
@@ -1567,22 +1568,6 @@
        "node": ">= 6.0.0"
      }
    },
    "node_modules/aggregate-error": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz",
      "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==",
      "dev": true,
      "dependencies": {
        "clean-stack": "^4.0.0",
        "indent-string": "^5.0.0"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ansi-regex": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -1631,15 +1616,6 @@
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
    },
    "node_modules/array-find-index": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
      "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==",
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/array-iterate": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz",
@@ -1649,136 +1625,11 @@
        "url": "https://github.com/sponsors/wooorm"
      }
    },
    "node_modules/arrgv": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz",
      "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==",
      "dev": true,
      "engines": {
        "node": ">=8.0.0"
      }
    },
    "node_modules/arrify": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz",
      "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
    },
    "node_modules/ava": {
      "version": "5.3.1",
      "resolved": "https://registry.npmjs.org/ava/-/ava-5.3.1.tgz",
      "integrity": "sha512-Scv9a4gMOXB6+ni4toLuhAm9KYWEjsgBglJl+kMGI5+IVDt120CCDZyB5HNU9DjmLI2t4I0GbnxGLmmRfGTJGg==",
      "dev": true,
      "dependencies": {
        "acorn": "^8.8.2",
        "acorn-walk": "^8.2.0",
        "ansi-styles": "^6.2.1",
        "arrgv": "^1.0.2",
        "arrify": "^3.0.0",
        "callsites": "^4.0.0",
        "cbor": "^8.1.0",
        "chalk": "^5.2.0",
        "chokidar": "^3.5.3",
        "chunkd": "^2.0.1",
        "ci-info": "^3.8.0",
        "ci-parallel-vars": "^1.0.1",
        "clean-yaml-object": "^0.1.0",
        "cli-truncate": "^3.1.0",
        "code-excerpt": "^4.0.0",
        "common-path-prefix": "^3.0.0",
        "concordance": "^5.0.4",
        "currently-unhandled": "^0.4.1",
        "debug": "^4.3.4",
        "emittery": "^1.0.1",
        "figures": "^5.0.0",
        "globby": "^13.1.4",
        "ignore-by-default": "^2.1.0",
        "indent-string": "^5.0.0",
        "is-error": "^2.2.2",
        "is-plain-object": "^5.0.0",
        "is-promise": "^4.0.0",
        "matcher": "^5.0.0",
        "mem": "^9.0.2",
        "ms": "^2.1.3",
        "p-event": "^5.0.1",
        "p-map": "^5.5.0",
        "picomatch": "^2.3.1",
        "pkg-conf": "^4.0.0",
        "plur": "^5.1.0",
        "pretty-ms": "^8.0.0",
        "resolve-cwd": "^3.0.0",
        "stack-utils": "^2.0.6",
        "strip-ansi": "^7.0.1",
        "supertap": "^3.0.1",
        "temp-dir": "^3.0.0",
        "write-file-atomic": "^5.0.1",
        "yargs": "^17.7.2"
      },
      "bin": {
        "ava": "entrypoints/cli.mjs"
      },
      "engines": {
        "node": ">=14.19 <15 || >=16.15 <17 || >=18"
      },
      "peerDependencies": {
        "@ava/typescript": "*"
      },
      "peerDependenciesMeta": {
        "@ava/typescript": {
          "optional": true
        }
      }
    },
    "node_modules/ava/node_modules/acorn-walk": {
      "version": "8.2.0",
      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
      "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
      "dev": true,
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/ava/node_modules/ansi-styles": {
      "version": "6.2.1",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/ava/node_modules/chalk": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
      "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
      "dev": true,
      "engines": {
        "node": "^12.17.0 || ^14.13 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/ava/node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
      "dev": true
    },
    "node_modules/bail": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -1801,12 +1652,6 @@
        "node": ">=8"
      }
    },
    "node_modules/blueimp-md5": {
      "version": "2.19.0",
      "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
      "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
      "dev": true
    },
    "node_modules/brace-expansion": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1834,8 +1679,7 @@
    "node_modules/buffer-from": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
      "dev": true
      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
    },
    "node_modules/bytes": {
      "version": "3.0.0",
@@ -1845,30 +1689,6 @@
        "node": ">= 0.8"
      }
    },
    "node_modules/callsites": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.0.0.tgz",
      "integrity": "sha512-y3jRROutgpKdz5vzEhWM34TidDU8vkJppF8dszITeb1PQmSqV3DTxyV8G/lyO/DNvtE1YTedehmw9MPZsCBHxQ==",
      "dev": true,
      "engines": {
        "node": ">=12.20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/cbor": {
      "version": "8.1.0",
      "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz",
      "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==",
      "dev": true,
      "dependencies": {
        "nofilter": "^3.1.0"
      },
      "engines": {
        "node": ">=12.19"
      }
    },
    "node_modules/ccount": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -1928,57 +1748,6 @@
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/chunkd": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz",
      "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==",
      "dev": true
    },
    "node_modules/ci-info": {
      "version": "3.8.0",
      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
      "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/sibiraj-s"
        }
      ],
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ci-parallel-vars": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz",
      "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==",
      "dev": true
    },
    "node_modules/clean-stack": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz",
      "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==",
      "dev": true,
      "dependencies": {
        "escape-string-regexp": "5.0.0"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/clean-yaml-object": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz",
      "integrity": "sha512-3yONmlN9CSAkzNwnRCiJQ7Q2xK5mWuEfL3PuTZcAUzhObbXsfsnMptJzXwz93nc5zn9V9TwCVMmV7w4xsm43dw==",
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/cli-spinner": {
      "version": "0.2.10",
      "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz",
@@ -1987,22 +1756,6 @@
        "node": ">=0.10"
      }
    },
    "node_modules/cli-truncate": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
      "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
      "dev": true,
      "dependencies": {
        "slice-ansi": "^5.0.0",
        "string-width": "^5.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/cliui": {
      "version": "8.0.1",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -2069,18 +1822,6 @@
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/code-excerpt": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
      "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
      "dev": true,
      "dependencies": {
        "convert-to-spaces": "^2.0.1"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2125,36 +1866,11 @@
        "node": ">= 10"
      }
    },
    "node_modules/common-path-prefix": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
      "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
      "dev": true
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
    },
    "node_modules/concordance": {
      "version": "5.0.4",
      "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz",
      "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==",
      "dev": true,
      "dependencies": {
        "date-time": "^3.1.0",
        "esutils": "^2.0.3",
        "fast-diff": "^1.2.0",
        "js-string-escape": "^1.0.1",
        "lodash": "^4.17.15",
        "md5-hex": "^3.0.1",
        "semver": "^7.3.2",
        "well-known-symbols": "^2.0.0"
      },
      "engines": {
        "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
      }
    },
    "node_modules/content-disposition": {
      "version": "0.5.2",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -2163,15 +1879,6 @@
        "node": ">= 0.6"
      }
    },
    "node_modules/convert-to-spaces": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
      "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
      "dev": true,
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.3",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2206,18 +1913,6 @@
      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
      "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
    },
    "node_modules/currently-unhandled": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
      "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==",
      "dev": true,
      "dependencies": {
        "array-find-index": "^1.0.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/d3": {
      "version": "7.8.5",
      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
@@ -2613,18 +2308,6 @@
        "node": ">=12"
      }
    },
    "node_modules/date-time": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz",
      "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==",
      "dev": true,
      "dependencies": {
        "time-zone": "^1.0.0"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/debug": {
      "version": "4.3.4",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -2717,18 +2400,6 @@
      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
    },
    "node_modules/emittery": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.0.1.tgz",
      "integrity": "sha512-2ID6FdrMD9KDLldGesP6317G78K7km/kMcwItRtVFva7I/cSEOIaLpewaUb+YLXVwdAp3Ctfxh/V5zIl1sj7dQ==",
      "dev": true,
      "engines": {
        "node": ">=14.16"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/emittery?sponsor=1"
      }
    },
    "node_modules/emoji-regex": {
      "version": "9.2.2",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -2884,12 +2555,6 @@
        "node": ">=0.10.0"
      }
    },
    "node_modules/fast-diff": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
      "dev": true
    },
    "node_modules/fast-glob": {
      "version": "3.3.0",
      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
@@ -2933,22 +2598,6 @@
        "url": "https://github.com/sponsors/wooorm"
      }
    },
    "node_modules/figures": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
      "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
      "dev": true,
      "dependencies": {
        "escape-string-regexp": "^5.0.0",
        "is-unicode-supported": "^1.2.0"
      },
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/fill-range": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2960,22 +2609,6 @@
        "node": ">=8"
      }
    },
    "node_modules/find-up": {
      "version": "6.3.0",
      "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
      "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
      "dev": true,
      "dependencies": {
        "locate-path": "^7.1.0",
        "path-exists": "^5.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/flexsearch": {
      "version": "0.7.21",
      "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.21.tgz",
@@ -3467,41 +3100,11 @@
        "node": ">= 4"
      }
    },
    "node_modules/ignore-by-default": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz",
      "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==",
      "dev": true,
      "engines": {
        "node": ">=10 <11 || >=12 <13 || >=14"
      }
    },
    "node_modules/immutable": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
      "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
      "dev": true,
      "engines": {
        "node": ">=0.8.19"
      }
    },
    "node_modules/indent-string": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
      "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/inline-style-parser": {
      "version": "0.1.1",
      "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
@@ -3515,15 +3118,6 @@
        "node": ">=12"
      }
    },
    "node_modules/irregular-plurals": {
      "version": "3.5.0",
      "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
      "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==",
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-absolute-url": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz",
@@ -3579,12 +3173,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-error": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz",
      "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==",
      "dev": true
    },
    "node_modules/is-extendable": {
      "version": "0.1.1",
      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -3650,38 +3238,11 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/is-plain-object": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-potential-custom-element-name": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
      "dev": true
    },
    "node_modules/is-unicode-supported": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3704,15 +3265,6 @@
        "@pkgjs/parseargs": "^0.11.0"
      }
    },
    "node_modules/js-string-escape": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
      "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
      "dev": true,
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/js-yaml": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3819,39 +3371,6 @@
        "node": ">=6"
      }
    },
    "node_modules/load-json-file": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz",
      "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==",
      "dev": true,
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/locate-path": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
      "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
      "dev": true,
      "dependencies": {
        "p-locate": "^6.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
      "dev": true
    },
    "node_modules/longest-streak": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -3869,18 +3388,6 @@
        "node": "14 || >=16.14"
      }
    },
    "node_modules/map-age-cleaner": {
      "version": "0.1.3",
      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
      "dev": true,
      "dependencies": {
        "p-defer": "^1.0.0"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/markdown-table": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz",
@@ -3890,21 +3397,6 @@
        "url": "https://github.com/sponsors/wooorm"
      }
    },
    "node_modules/matcher": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz",
      "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==",
      "dev": true,
      "dependencies": {
        "escape-string-regexp": "^5.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mathjax-full": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz",
@@ -3916,18 +3408,6 @@
        "speech-rule-engine": "^4.0.6"
      }
    },
    "node_modules/md5-hex": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
      "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==",
      "dev": true,
      "dependencies": {
        "blueimp-md5": "^2.10.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/mdast-util-definitions": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
@@ -4159,22 +3639,6 @@
        "url": "https://opencollective.com/unified"
      }
    },
    "node_modules/mem": {
      "version": "9.0.2",
      "resolved": "https://registry.npmjs.org/mem/-/mem-9.0.2.tgz",
      "integrity": "sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A==",
      "dev": true,
      "dependencies": {
        "map-age-cleaner": "^0.1.3",
        "mimic-fn": "^4.0.0"
      },
      "engines": {
        "node": ">=12.20"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/mem?sponsor=1"
      }
    },
    "node_modules/merge2": {
      "version": "1.4.1",
      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4797,18 +4261,6 @@
        "node": ">= 0.6"
      }
    },
    "node_modules/mimic-fn": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/minimatch": {
      "version": "9.0.2",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz",
@@ -4861,15 +4313,6 @@
        "url": "https://opencollective.com/unified"
      }
    },
    "node_modules/nofilter": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz",
      "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==",
      "dev": true,
      "engines": {
        "node": ">=12.19"
      }
    },
    "node_modules/normalize-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4883,87 +4326,6 @@
      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
      "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ=="
    },
    "node_modules/p-defer": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
      "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==",
      "dev": true,
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/p-event": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/p-event/-/p-event-5.0.1.tgz",
      "integrity": "sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==",
      "dev": true,
      "dependencies": {
        "p-timeout": "^5.0.2"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-limit": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
      "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
      "dev": true,
      "dependencies": {
        "yocto-queue": "^1.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-locate": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
      "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
      "dev": true,
      "dependencies": {
        "p-limit": "^4.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-map": {
      "version": "5.5.0",
      "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz",
      "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==",
      "dev": true,
      "dependencies": {
        "aggregate-error": "^4.0.0"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-timeout": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz",
      "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parse-latin": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-5.0.1.tgz",
@@ -4978,18 +4340,6 @@
        "url": "https://github.com/sponsors/wooorm"
      }
    },
    "node_modules/parse-ms": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz",
      "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parse-numeric-range": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
@@ -5006,15 +4356,6 @@
        "url": "https://github.com/inikulin/parse5?sponsor=1"
      }
    },
    "node_modules/path-exists": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
      "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
      "dev": true,
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      }
    },
    "node_modules/path-is-inside": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
@@ -5077,22 +4418,6 @@
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/pkg-conf": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-4.0.0.tgz",
      "integrity": "sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w==",
      "dev": true,
      "dependencies": {
        "find-up": "^6.0.0",
        "load-json-file": "^7.0.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/plausible-tracker": {
      "version": "0.3.8",
      "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz",
@@ -5101,21 +4426,6 @@
        "node": ">=10"
      }
    },
    "node_modules/plur": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz",
      "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==",
      "dev": true,
      "dependencies": {
        "irregular-plurals": "^3.3.0"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/preact": {
      "version": "10.15.1",
      "resolved": "https://registry.npmjs.org/preact/-/preact-10.15.1.tgz",
@@ -5136,26 +4446,22 @@
        "preact": ">=10"
      }
    },
    "node_modules/pretty-format": {
      "version": "3.8.0",
      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
      "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
    },
    "node_modules/pretty-ms": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz",
      "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==",
      "dev": true,
      "dependencies": {
        "parse-ms": "^3.0.0"
      },
    "node_modules/pretty-bytes": {
      "version": "6.1.0",
      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.0.tgz",
      "integrity": "sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==",
      "engines": {
        "node": ">=14.16"
        "node": "^14.13.1 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/pretty-format": {
      "version": "3.8.0",
      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
      "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
    },
    "node_modules/pretty-time": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz",
@@ -5479,27 +4785,6 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/resolve-cwd": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
      "dev": true,
      "dependencies": {
        "resolve-from": "^5.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve-from": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/resolve-pkg-maps": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -5681,60 +4966,6 @@
        "node": ">=4"
      }
    },
    "node_modules/semver": {
      "version": "7.5.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
      "dev": true,
      "dependencies": {
        "lru-cache": "^6.0.0"
      },
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/semver/node_modules/lru-cache": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
      "dev": true,
      "dependencies": {
        "yallist": "^4.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/serialize-error": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
      "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
      "dev": true,
      "dependencies": {
        "type-fest": "^0.13.1"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/serialize-error/node_modules/type-fest": {
      "version": "0.13.1",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
      "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
      "dev": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/serve-handler": {
      "version": "6.1.5",
      "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz",
@@ -5828,46 +5059,6 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/slice-ansi": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
      "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
      "dev": true,
      "dependencies": {
        "ansi-styles": "^6.0.0",
        "is-fullwidth-code-point": "^4.0.0"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
      }
    },
    "node_modules/slice-ansi/node_modules/ansi-styles": {
      "version": "6.2.1",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
      "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
      "dev": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/sort-keys": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz",
@@ -5886,7 +5077,6 @@
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
      "devOptional": true,
      "engines": {
        "node": ">=0.10.0"
      }
@@ -5903,7 +5093,6 @@
      "version": "0.5.21",
      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
      "dev": true,
      "dependencies": {
        "buffer-from": "^1.0.0",
        "source-map": "^0.6.0"
@@ -5944,27 +5133,6 @@
      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
    },
    "node_modules/stack-utils": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
      "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
      "dev": true,
      "dependencies": {
        "escape-string-regexp": "^2.0.0"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/stack-utils/node_modules/escape-string-regexp": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
      "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/string-width": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -6069,43 +5237,6 @@
        "inline-style-parser": "0.1.1"
      }
    },
    "node_modules/supertap": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz",
      "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==",
      "dev": true,
      "dependencies": {
        "indent-string": "^5.0.0",
        "js-yaml": "^3.14.1",
        "serialize-error": "^7.0.1",
        "strip-ansi": "^7.0.1"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      }
    },
    "node_modules/supertap/node_modules/argparse": {
      "version": "1.0.10",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
      "dev": true,
      "dependencies": {
        "sprintf-js": "~1.0.2"
      }
    },
    "node_modules/supertap/node_modules/js-yaml": {
      "version": "3.14.1",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
      "dev": true,
      "dependencies": {
        "argparse": "^1.0.7",
        "esprima": "^4.0.0"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6133,24 +5264,6 @@
      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
    },
    "node_modules/temp-dir": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz",
      "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==",
      "dev": true,
      "engines": {
        "node": ">=14.16"
      }
    },
    "node_modules/time-zone": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz",
      "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==",
      "dev": true,
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6549,15 +5662,6 @@
        "node": ">=12"
      }
    },
    "node_modules/well-known-symbols": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz",
      "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==",
      "dev": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/whatwg-encoding": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
@@ -6694,19 +5798,6 @@
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/write-file-atomic": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
      "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
      "dev": true,
      "dependencies": {
        "imurmurhash": "^0.1.4",
        "signal-exit": "^4.0.1"
      },
      "engines": {
        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
      }
    },
    "node_modules/ws": {
      "version": "8.13.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
@@ -6756,12 +5847,6 @@
        "node": ">=10"
      }
    },
    "node_modules/yallist": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
      "dev": true
    },
    "node_modules/yargs": {
      "version": "17.7.2",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -6824,18 +5909,6 @@
        "node": ">=8"
      }
    },
    "node_modules/yocto-queue": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
      "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
      "dev": true,
      "engines": {
        "node": ">=12.20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/zwitch": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
package.json
@@ -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",
quartz/bootstrap-cli.mjs
@@ -9,6 +9,7 @@
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 @@
`)
  })
  .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 @@
      jsx: "automatic",
      jsxImportSource: "preact",
      packages: "external",
      metafile: true,
      sourcemap: true,
      plugins: [
        sassPlugin({
          type: 'css-text',
@@ -186,6 +189,12 @@
      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)
  })
quartz/build.ts
@@ -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 { 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 @@
  })
  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)
quartz/components/Backlinks.tsx
@@ -1,16 +1,15 @@
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> 
quartz/components/Head.tsx
@@ -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
quartz/components/PageList.tsx
@@ -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 @@
}
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>
quartz/components/PageTitle.tsx
@@ -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>
}
quartz/components/TagList.tsx
@@ -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 => {
quartz/components/pages/FolderContent.tsx
@@ -5,11 +5,11 @@
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)
quartz/components/pages/TagContent.tsx
@@ -3,14 +3,14 @@
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 @@
      </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}`)
  }
}
quartz/components/scripts/graph.inline.ts
@@ -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 @@
  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 @@
    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 @@
      }
    }
  } 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 @@
    .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 @@
}
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")
quartz/components/scripts/search.inline.ts
@@ -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 @@
    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 @@
  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
quartz/components/scripts/spa.inline.ts
@@ -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 @@
    .catch(() => {
      window.location.assign(url)
    })
  if (!contents) return;
  if (!isBack) {
    history.pushState({}, "", url)
@@ -70,7 +71,7 @@
  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 @@
}
createRouter()
notifyNav(document.body.dataset.slug!)
notifyNav(getCanonicalSlug(window))
if (!customElements.get('route-announcer')) {
  const attrs = {
quartz/path.test.ts
@@ -24,23 +24,21 @@
  })
  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 @@
    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 @@
  })
  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 @@
    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 @@
      ["/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)
  })
})
quartz/path.ts
@@ -1,5 +1,5 @@
import path from 'path'
import { slug as slugAnchor } from 'github-slugger'
import { trace } from './trace'
// Quartz Paths
// Things in boxes are not actual types but rather sources which these types can be acquired from
@@ -16,40 +16,53 @@
//                    │        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                              │
//                                         /note/abc                                 │
//                                                                                   │
//                                             ▲                                     │
//                    │   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 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 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 @@
/** 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 @@
    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 _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 @@
  }
  if (!s.startsWith(".")) {
    s = "./" + s
    s = joinSegments(".", s)
  }
  return s
quartz/plugins/emitters/aliases.ts
@@ -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 @@
    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 @@
      }
      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>
quartz/plugins/emitters/contentIndex.ts
@@ -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 @@
  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 @@
      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 @@
        slug: fp,
        ext: ".json",
      })
      emitted.push(`${fp}.json`)
      emitted.push(`${fp}.json` as FilePath)
      return emitted
    },
quartz/plugins/emitters/contentPage.tsx
@@ -4,7 +4,7 @@
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 @@
      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,
quartz/plugins/emitters/folderPage.tsx
@@ -6,7 +6,7 @@
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,21 +23,27 @@
      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]
        }
@@ -63,7 +69,7 @@
          externalResources
        )
        const fp = file.data.slug + ".html"
        const fp = file.data.slug! + ".html" as FilePath
        await emit({
          content,
          slug: file.data.slug!,
quartz/plugins/emitters/tagPage.tsx
@@ -5,7 +5,7 @@
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 @@
      ])))
      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 @@
      }
      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 = {
quartz/plugins/index.ts
@@ -55,17 +55,17 @@
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)
    })
quartz/plugins/transformers/links.ts
@@ -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 @@
  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 @@
    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 @@
              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
quartz/plugins/transformers/ofm.ts
@@ -2,7 +2,6 @@
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 { 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 @@
        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 @@
                    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,
quartz/plugins/transformers/toc.ts
@@ -3,7 +3,6 @@
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 @@
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) => {
quartz/processors/emit.ts
@@ -19,6 +19,7 @@
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 @@
        }
      }
    } 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)
    }
  }
quartz/processors/parse.ts
@@ -14,6 +14,7 @@
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 @@
          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)
      }
    }
quartz/trace.ts
New 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
      }
    }
  }
}
tsconfig.json
@@ -5,6 +5,7 @@
      "DOM",
      "DOM.Iterable"
    ],
    "experimentalDecorators": true,
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "node",
tsconfig.tsbuildinfo
File was deleted