base path refactor to better support subpath hosting
This commit is contained in:
		
							parent
							
								
									3201f83b70
								
							
						
					
					
						commit
						c874e7e937
					
				
					 29 changed files with 257 additions and 389 deletions
				
			
		| 
						 | 
					@ -4,7 +4,7 @@ title: Paths in Quartz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places.
 | 
					Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The current browser URL? Technically a path. A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path.
 | 
					A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it.
 | 
					It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,13 +12,13 @@ Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.o
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```typescript
 | 
					```typescript
 | 
				
			||||||
// instead of
 | 
					// instead of
 | 
				
			||||||
type ClientSlug = string
 | 
					type FullSlug = string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// we do
 | 
					// we do
 | 
				
			||||||
type ClientSlug = string & { __brand: "client" }
 | 
					type FullSlug = string & { __brand: "full" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// that way, the following will fail typechecking
 | 
					// that way, the following will fail typechecking
 | 
				
			||||||
const slug: ClientSlug = "some random slug"
 | 
					const slug: FullSlug = "some random string"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
While this prevents most typing mistakes _within_ our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from _accidentally_ mistaking a string for a client slug when we forcibly cast it.
 | 
					While this prevents most typing mistakes _within_ our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from _accidentally_ mistaking a string for a client slug when we forcibly cast it.
 | 
				
			||||||
| 
						 | 
					@ -29,27 +29,23 @@ The following diagram draws the relationships between all the path sources, nomi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```mermaid
 | 
					```mermaid
 | 
				
			||||||
graph LR
 | 
					graph LR
 | 
				
			||||||
    Browser{{Browser}} --> Window{{Window}} & LinkElement{{Link Element}}
 | 
					    Browser{{Browser}} --> Window{{Body}} & LinkElement{{Link Element}}
 | 
				
			||||||
    Window --"getCanonicalSlug()"--> Canonical[Canonical Slug]
 | 
					    Window --"getFullSlug()"--> FullSlug[Full Slug]
 | 
				
			||||||
    Window --"getClientSlug()"--> Client[Client Slug]
 | 
					 | 
				
			||||||
    LinkElement --".href"--> Relative[Relative URL]
 | 
					    LinkElement --".href"--> Relative[Relative URL]
 | 
				
			||||||
    Client --"canonicalizeClient()"--> Canonical
 | 
					    FullSlug --"simplifySlug()" --> SimpleSlug[Simple Slug]
 | 
				
			||||||
    Canonical --"pathToRoot()"--> Relative
 | 
					    SimpleSlug --"pathToRoot()"--> Relative
 | 
				
			||||||
    Canonical --"resolveRelative()" --> Relative
 | 
					    SimpleSlug --"resolveRelative()" --> Relative
 | 
				
			||||||
    MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links]
 | 
					    MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links]
 | 
				
			||||||
    Links --"transformLink()"--> Relative
 | 
					    Links --"transformLink()"--> Relative
 | 
				
			||||||
    FilePath --"slugifyFilePath()"--> Server[Server Slug]
 | 
					    FilePath --"slugifyFilePath()"--> FullSlug[Full Slug]
 | 
				
			||||||
    Server --> HTML["HTML File"]
 | 
					    style FullSlug stroke-width:4px
 | 
				
			||||||
    Server --"canonicalizeServer()"--> Canonical
 | 
					 | 
				
			||||||
    style Canonical stroke-width:4px
 | 
					 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Here are the main types of slugs with a rough description of each type of path:
 | 
					Here are the main types of slugs with a rough description of each type of path:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `ClientSlug`: client-side slug, usually obtained through `window.location`. Contains the protocol (i.e. starts with `https://`)
 | 
					 | 
				
			||||||
- `CanonicalSlug`: should be used whenever you need to refer to the location of a file/note. Shouldn't be a relative path and shouldn't have leading or trailing slashes `/` either. Also shouldn't have `/index` as an ending or a file extension.
 | 
					 | 
				
			||||||
- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension.
 | 
					 | 
				
			||||||
- `ServerSlug`: cannot be relative and may not have leading or trailing slashes.
 | 
					 | 
				
			||||||
- `FilePath`: a real file path to a file on disk. Cannot be relative and must have a file extension.
 | 
					- `FilePath`: a real file path to a file on disk. Cannot be relative and must have a file extension.
 | 
				
			||||||
 | 
					- `FullSlug`: cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug.
 | 
				
			||||||
 | 
					- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path.
 | 
				
			||||||
 | 
					- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/path.test.ts`.
 | 
					To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/path.test.ts`.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ However, if you'd like to publish your site to the world, you need a way to host
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Configuration option   | Value              |
 | 
					| Configuration option   | Value              |
 | 
				
			||||||
| ---------------------- | ------------------ |
 | 
					| ---------------------- | ------------------ |
 | 
				
			||||||
| Production branch      | `v4`         |
 | 
					| Production branch      | `v4`               |
 | 
				
			||||||
| Framework preset       | `None`             |
 | 
					| Framework preset       | `None`             |
 | 
				
			||||||
| Build command          | `npx quartz build` |
 | 
					| Build command          | `npx quartz build` |
 | 
				
			||||||
| Build output directory | `public`           |
 | 
					| Build output directory | `public`           |
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -5,7 +5,7 @@ declare module "*.scss" {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// dom custom event
 | 
					// dom custom event
 | 
				
			||||||
interface CustomEventMap {
 | 
					interface CustomEventMap {
 | 
				
			||||||
  nav: CustomEvent<{ url: CanonicalSlug }>
 | 
					  nav: CustomEvent<{ url: FullSlug }>
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare const fetchData: Promise<ContentIndex>
 | 
					declare const fetchData: Promise<ContentIndex>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
  "name": "@jackyzha0/quartz",
 | 
					  "name": "@jackyzha0/quartz",
 | 
				
			||||||
  "description": "🌱 publish your digital garden and notes as a website",
 | 
					  "description": "🌱 publish your digital garden and notes as a website",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "version": "4.0.7",
 | 
					  "version": "4.0.8",
 | 
				
			||||||
  "type": "module",
 | 
					  "type": "module",
 | 
				
			||||||
  "author": "jackyzha0 <j.zhao2k19@gmail.com>",
 | 
					  "author": "jackyzha0 <j.zhao2k19@gmail.com>",
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
import style from "./styles/backlinks.scss"
 | 
					import style from "./styles/backlinks.scss"
 | 
				
			||||||
import { canonicalizeServer, resolveRelative } from "../util/path"
 | 
					import { resolveRelative, simplifySlug } from "../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
 | 
					function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
 | 
				
			||||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
					  const slug = simplifySlug(fileData.slug!)
 | 
				
			||||||
  const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
 | 
					  const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div class="backlinks">
 | 
					    <div class="backlinks">
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
 | 
				
			||||||
        {backlinkFiles.length > 0 ? (
 | 
					        {backlinkFiles.length > 0 ? (
 | 
				
			||||||
          backlinkFiles.map((f) => (
 | 
					          backlinkFiles.map((f) => (
 | 
				
			||||||
            <li>
 | 
					            <li>
 | 
				
			||||||
              <a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">
 | 
					              <a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
 | 
				
			||||||
                {f.frontmatter?.title}
 | 
					                {f.frontmatter?.title}
 | 
				
			||||||
              </a>
 | 
					              </a>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,13 @@
 | 
				
			||||||
import { canonicalizeServer, pathToRoot } from "../util/path"
 | 
					import { pathToRoot } from "../util/path"
 | 
				
			||||||
import { JSResourceToScriptElement } from "../util/resources"
 | 
					import { JSResourceToScriptElement } from "../util/resources"
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default (() => {
 | 
					export default (() => {
 | 
				
			||||||
  function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
 | 
					  function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
 | 
				
			||||||
    const slug = canonicalizeServer(fileData.slug!)
 | 
					 | 
				
			||||||
    const title = fileData.frontmatter?.title ?? "Untitled"
 | 
					    const title = fileData.frontmatter?.title ?? "Untitled"
 | 
				
			||||||
    const description = fileData.description?.trim() ?? "No description provided"
 | 
					    const description = fileData.description?.trim() ?? "No description provided"
 | 
				
			||||||
    const { css, js } = externalResources
 | 
					    const { css, js } = externalResources
 | 
				
			||||||
    const baseDir = pathToRoot(slug)
 | 
					    const baseDir = pathToRoot(fileData.slug!)
 | 
				
			||||||
    const iconPath = baseDir + "/static/icon.png"
 | 
					    const iconPath = baseDir + "/static/icon.png"
 | 
				
			||||||
    const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
 | 
					    const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../util/path"
 | 
					import { FullSlug, resolveRelative } from "../util/path"
 | 
				
			||||||
import { QuartzPluginData } from "../plugins/vfile"
 | 
					import { QuartzPluginData } from "../plugins/vfile"
 | 
				
			||||||
import { Date } from "./Date"
 | 
					import { Date } from "./Date"
 | 
				
			||||||
import { QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentProps } from "./types"
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,6 @@ type Props = {
 | 
				
			||||||
} & QuartzComponentProps
 | 
					} & QuartzComponentProps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function PageList({ fileData, allFiles, limit }: Props) {
 | 
					export function PageList({ fileData, allFiles, limit }: Props) {
 | 
				
			||||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
					 | 
				
			||||||
  let list = allFiles.sort(byDateAndAlphabetical)
 | 
					  let list = allFiles.sort(byDateAndAlphabetical)
 | 
				
			||||||
  if (limit) {
 | 
					  if (limit) {
 | 
				
			||||||
    list = list.slice(0, limit)
 | 
					    list = list.slice(0, limit)
 | 
				
			||||||
| 
						 | 
					@ -35,7 +34,6 @@ export function PageList({ fileData, allFiles, limit }: Props) {
 | 
				
			||||||
    <ul class="section-ul">
 | 
					    <ul class="section-ul">
 | 
				
			||||||
      {list.map((page) => {
 | 
					      {list.map((page) => {
 | 
				
			||||||
        const title = page.frontmatter?.title
 | 
					        const title = page.frontmatter?.title
 | 
				
			||||||
        const pageSlug = canonicalizeServer(page.slug!)
 | 
					 | 
				
			||||||
        const tags = page.frontmatter?.tags ?? []
 | 
					        const tags = page.frontmatter?.tags ?? []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
| 
						 | 
					@ -48,7 +46,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
              <div class="desc">
 | 
					              <div class="desc">
 | 
				
			||||||
                <h3>
 | 
					                <h3>
 | 
				
			||||||
                  <a href={resolveRelative(slug, pageSlug)} class="internal">
 | 
					                  <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
 | 
				
			||||||
                    {title}
 | 
					                    {title}
 | 
				
			||||||
                  </a>
 | 
					                  </a>
 | 
				
			||||||
                </h3>
 | 
					                </h3>
 | 
				
			||||||
| 
						 | 
					@ -58,7 +56,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
 | 
				
			||||||
                  <li>
 | 
					                  <li>
 | 
				
			||||||
                    <a
 | 
					                    <a
 | 
				
			||||||
                      class="internal tag-link"
 | 
					                      class="internal tag-link"
 | 
				
			||||||
                      href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}
 | 
					                      href={resolveRelative(fileData.slug!, `tags/${tag}/index` as FullSlug)}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                      #{tag}
 | 
					                      #{tag}
 | 
				
			||||||
                    </a>
 | 
					                    </a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,9 @@
 | 
				
			||||||
import { canonicalizeServer, pathToRoot } from "../util/path"
 | 
					import { pathToRoot } from "../util/path"
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function PageTitle({ fileData, cfg }: QuartzComponentProps) {
 | 
					function PageTitle({ fileData, cfg }: QuartzComponentProps) {
 | 
				
			||||||
  const title = cfg?.pageTitle ?? "Untitled Quartz"
 | 
					  const title = cfg?.pageTitle ?? "Untitled Quartz"
 | 
				
			||||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
					  const baseDir = pathToRoot(fileData.slug!)
 | 
				
			||||||
  const baseDir = pathToRoot(slug)
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <h1 class="page-title">
 | 
					    <h1 class="page-title">
 | 
				
			||||||
      <a href={baseDir}>{title}</a>
 | 
					      <a href={baseDir}>{title}</a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,9 @@
 | 
				
			||||||
import { canonicalizeServer, pathToRoot, slugTag } from "../util/path"
 | 
					import { pathToRoot, slugTag } from "../util/path"
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function TagList({ fileData }: QuartzComponentProps) {
 | 
					function TagList({ fileData }: QuartzComponentProps) {
 | 
				
			||||||
  const tags = fileData.frontmatter?.tags
 | 
					  const tags = fileData.frontmatter?.tags
 | 
				
			||||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
					  const baseDir = pathToRoot(fileData.slug!)
 | 
				
			||||||
  const baseDir = pathToRoot(slug)
 | 
					 | 
				
			||||||
  if (tags && tags.length > 0) {
 | 
					  if (tags && tags.length > 0) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <ul class="tags">
 | 
					      <ul class="tags">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,13 +5,13 @@ import path from "path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import style from "../styles/listPage.scss"
 | 
					import style from "../styles/listPage.scss"
 | 
				
			||||||
import { PageList } from "../PageList"
 | 
					import { PageList } from "../PageList"
 | 
				
			||||||
import { canonicalizeServer } from "../../util/path"
 | 
					import { simplifySlug } from "../../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function FolderContent(props: QuartzComponentProps) {
 | 
					function FolderContent(props: QuartzComponentProps) {
 | 
				
			||||||
  const { tree, fileData, allFiles } = props
 | 
					  const { tree, fileData, allFiles } = props
 | 
				
			||||||
  const folderSlug = canonicalizeServer(fileData.slug!)
 | 
					  const folderSlug = simplifySlug(fileData.slug!)
 | 
				
			||||||
  const allPagesInFolder = allFiles.filter((file) => {
 | 
					  const allPagesInFolder = allFiles.filter((file) => {
 | 
				
			||||||
    const fileSlug = canonicalizeServer(file.slug!)
 | 
					    const fileSlug = simplifySlug(file.slug!)
 | 
				
			||||||
    const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
 | 
					    const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
 | 
				
			||||||
    const folderParts = folderSlug.split(path.posix.sep)
 | 
					    const folderParts = folderSlug.split(path.posix.sep)
 | 
				
			||||||
    const fileParts = fileSlug.split(path.posix.sep)
 | 
					    const fileParts = fileSlug.split(path.posix.sep)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
 | 
				
			||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 | 
					import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 | 
				
			||||||
import style from "../styles/listPage.scss"
 | 
					import style from "../styles/listPage.scss"
 | 
				
			||||||
import { PageList } from "../PageList"
 | 
					import { PageList } from "../PageList"
 | 
				
			||||||
import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../util/path"
 | 
					import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
 | 
				
			||||||
import { QuartzPluginData } from "../../plugins/vfile"
 | 
					import { QuartzPluginData } from "../../plugins/vfile"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const numPages = 10
 | 
					const numPages = 10
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ function TagContent(props: QuartzComponentProps) {
 | 
				
			||||||
    throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
 | 
					    throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
 | 
					  const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
 | 
				
			||||||
  const allPagesWithTag = (tag: string) =>
 | 
					  const allPagesWithTag = (tag: string) =>
 | 
				
			||||||
    allFiles.filter((file) =>
 | 
					    allFiles.filter((file) =>
 | 
				
			||||||
      (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
 | 
					      (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
 | 
				
			||||||
import HeaderConstructor from "./Header"
 | 
					import HeaderConstructor from "./Header"
 | 
				
			||||||
import BodyConstructor from "./Body"
 | 
					import BodyConstructor from "./Body"
 | 
				
			||||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
 | 
					import { JSResourceToScriptElement, StaticResources } from "../util/resources"
 | 
				
			||||||
import { CanonicalSlug, pathToRoot } from "../util/path"
 | 
					import { FullSlug, joinSegments, pathToRoot } from "../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface RenderComponents {
 | 
					interface RenderComponents {
 | 
				
			||||||
  head: QuartzComponent
 | 
					  head: QuartzComponent
 | 
				
			||||||
| 
						 | 
					@ -15,19 +15,20 @@ interface RenderComponents {
 | 
				
			||||||
  footer: QuartzComponent
 | 
					  footer: QuartzComponent
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function pageResources(
 | 
					export function pageResources(slug: FullSlug, staticResources: StaticResources): StaticResources {
 | 
				
			||||||
  slug: CanonicalSlug,
 | 
					 | 
				
			||||||
  staticResources: StaticResources,
 | 
					 | 
				
			||||||
): StaticResources {
 | 
					 | 
				
			||||||
  const baseDir = pathToRoot(slug)
 | 
					  const baseDir = pathToRoot(slug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const contentIndexPath = baseDir + "/static/contentIndex.json"
 | 
					  const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
 | 
				
			||||||
  const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
 | 
					  const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    css: [baseDir + "/index.css", ...staticResources.css],
 | 
					    css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
 | 
				
			||||||
    js: [
 | 
					    js: [
 | 
				
			||||||
      { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
 | 
					      {
 | 
				
			||||||
 | 
					        src: joinSegments(baseDir, "/prescript.js"),
 | 
				
			||||||
 | 
					        loadTime: "beforeDOMReady",
 | 
				
			||||||
 | 
					        contentType: "external",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        loadTime: "beforeDOMReady",
 | 
					        loadTime: "beforeDOMReady",
 | 
				
			||||||
        contentType: "inline",
 | 
					        contentType: "inline",
 | 
				
			||||||
| 
						 | 
					@ -46,7 +47,7 @@ export function pageResources(
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function renderPage(
 | 
					export function renderPage(
 | 
				
			||||||
  slug: CanonicalSlug,
 | 
					  slug: FullSlug,
 | 
				
			||||||
  componentData: QuartzComponentProps,
 | 
					  componentData: QuartzComponentProps,
 | 
				
			||||||
  components: RenderComponents,
 | 
					  components: RenderComponents,
 | 
				
			||||||
  pageResources: StaticResources,
 | 
					  pageResources: StaticResources,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,31 +1,32 @@
 | 
				
			||||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
					import type { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
				
			||||||
import * as d3 from "d3"
 | 
					import * as d3 from "d3"
 | 
				
			||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
					import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
				
			||||||
import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
 | 
					import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type NodeData = {
 | 
					type NodeData = {
 | 
				
			||||||
  id: CanonicalSlug
 | 
					  id: SimpleSlug
 | 
				
			||||||
  text: string
 | 
					  text: string
 | 
				
			||||||
  tags: string[]
 | 
					  tags: string[]
 | 
				
			||||||
} & d3.SimulationNodeDatum
 | 
					} & d3.SimulationNodeDatum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LinkData = {
 | 
					type LinkData = {
 | 
				
			||||||
  source: CanonicalSlug
 | 
					  source: SimpleSlug
 | 
				
			||||||
  target: CanonicalSlug
 | 
					  target: SimpleSlug
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const localStorageKey = "graph-visited"
 | 
					const localStorageKey = "graph-visited"
 | 
				
			||||||
function getVisited(): Set<CanonicalSlug> {
 | 
					function getVisited(): Set<SimpleSlug> {
 | 
				
			||||||
  return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
 | 
					  return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function addToVisited(slug: CanonicalSlug) {
 | 
					function addToVisited(slug: SimpleSlug) {
 | 
				
			||||||
  const visited = getVisited()
 | 
					  const visited = getVisited()
 | 
				
			||||||
  visited.add(slug)
 | 
					  visited.add(slug)
 | 
				
			||||||
  localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
 | 
					  localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
					async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
				
			||||||
 | 
					  const slug = simplifySlug(fullSlug)
 | 
				
			||||||
  const visited = getVisited()
 | 
					  const visited = getVisited()
 | 
				
			||||||
  const graph = document.getElementById(container)
 | 
					  const graph = document.getElementById(container)
 | 
				
			||||||
  if (!graph) return
 | 
					  if (!graph) return
 | 
				
			||||||
| 
						 | 
					@ -47,16 +48,17 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const links: LinkData[] = []
 | 
					  const links: LinkData[] = []
 | 
				
			||||||
  for (const [src, details] of Object.entries<ContentDetails>(data)) {
 | 
					  for (const [src, details] of Object.entries<ContentDetails>(data)) {
 | 
				
			||||||
 | 
					    const source = simplifySlug(src as FullSlug)
 | 
				
			||||||
    const outgoing = details.links ?? []
 | 
					    const outgoing = details.links ?? []
 | 
				
			||||||
    for (const dest of outgoing) {
 | 
					    for (const dest of outgoing) {
 | 
				
			||||||
      if (src in data && dest in data) {
 | 
					      if (dest in data) {
 | 
				
			||||||
        links.push({ source: src as CanonicalSlug, target: dest })
 | 
					        links.push({ source, target: dest })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const neighbourhood = new Set<CanonicalSlug>()
 | 
					  const neighbourhood = new Set<SimpleSlug>()
 | 
				
			||||||
  const wl: (CanonicalSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
 | 
					  const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
 | 
				
			||||||
  if (depth >= 0) {
 | 
					  if (depth >= 0) {
 | 
				
			||||||
    while (depth >= 0 && wl.length > 0) {
 | 
					    while (depth >= 0 && wl.length > 0) {
 | 
				
			||||||
      // compute neighbours
 | 
					      // compute neighbours
 | 
				
			||||||
| 
						 | 
					@ -72,7 +74,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    Object.keys(data).forEach((id) => neighbourhood.add(id as CanonicalSlug))
 | 
					    Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const graphData: { nodes: NodeData[]; links: LinkData[] } = {
 | 
					  const graphData: { nodes: NodeData[]; links: LinkData[] } = {
 | 
				
			||||||
| 
						 | 
					@ -171,11 +173,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
				
			||||||
    .attr("fill", color)
 | 
					    .attr("fill", color)
 | 
				
			||||||
    .style("cursor", "pointer")
 | 
					    .style("cursor", "pointer")
 | 
				
			||||||
    .on("click", (_, d) => {
 | 
					    .on("click", (_, d) => {
 | 
				
			||||||
      const targ = resolveRelative(slug, d.id)
 | 
					      const targ = resolveRelative(fullSlug, d.id)
 | 
				
			||||||
      window.spaNavigate(new URL(targ, getClientSlug(window)))
 | 
					      window.spaNavigate(new URL(targ, window.location.toString()))
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .on("mouseover", function (_, d) {
 | 
					    .on("mouseover", function (_, d) {
 | 
				
			||||||
      const neighbours: CanonicalSlug[] = data[slug].links ?? []
 | 
					      const neighbours: SimpleSlug[] = data[slug].links ?? []
 | 
				
			||||||
      const neighbourNodes = d3
 | 
					      const neighbourNodes = d3
 | 
				
			||||||
        .selectAll<HTMLElement, NodeData>(".node")
 | 
					        .selectAll<HTMLElement, NodeData>(".node")
 | 
				
			||||||
        .filter((d) => neighbours.includes(d.id))
 | 
					        .filter((d) => neighbours.includes(d.id))
 | 
				
			||||||
| 
						 | 
					@ -271,7 +273,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function renderGlobalGraph() {
 | 
					function renderGlobalGraph() {
 | 
				
			||||||
  const slug = getCanonicalSlug(window)
 | 
					  const slug = getFullSlug(window)
 | 
				
			||||||
  const container = document.getElementById("global-graph-outer")
 | 
					  const container = document.getElementById("global-graph-outer")
 | 
				
			||||||
  const sidebar = container?.closest(".sidebar") as HTMLElement
 | 
					  const sidebar = container?.closest(".sidebar") as HTMLElement
 | 
				
			||||||
  container?.classList.add("active")
 | 
					  container?.classList.add("active")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
import { Document } from "flexsearch"
 | 
					import { Document } from "flexsearch"
 | 
				
			||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
					import { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
				
			||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
					import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
				
			||||||
import { CanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
 | 
					import { FullSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Item {
 | 
					interface Item {
 | 
				
			||||||
  id: number
 | 
					  id: number
 | 
				
			||||||
  slug: CanonicalSlug
 | 
					  slug: FullSlug
 | 
				
			||||||
  title: string
 | 
					  title: string
 | 
				
			||||||
  content: string
 | 
					  content: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -73,7 +73,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
				
			||||||
  const searchIcon = document.getElementById("search-icon")
 | 
					  const searchIcon = document.getElementById("search-icon")
 | 
				
			||||||
  const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
 | 
					  const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
 | 
				
			||||||
  const results = document.getElementById("results-container")
 | 
					  const results = document.getElementById("results-container")
 | 
				
			||||||
  const idDataMap = Object.keys(data) as CanonicalSlug[]
 | 
					  const idDataMap = Object.keys(data) as FullSlug[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function hideSearch() {
 | 
					  function hideSearch() {
 | 
				
			||||||
    container?.classList.remove("active")
 | 
					    container?.classList.remove("active")
 | 
				
			||||||
| 
						 | 
					@ -126,7 +126,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
				
			||||||
    button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
 | 
					    button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
 | 
				
			||||||
    button.addEventListener("click", () => {
 | 
					    button.addEventListener("click", () => {
 | 
				
			||||||
      const targ = resolveRelative(currentSlug, slug)
 | 
					      const targ = resolveRelative(currentSlug, slug)
 | 
				
			||||||
      window.spaNavigate(new URL(targ, getClientSlug(window)))
 | 
					      window.spaNavigate(new URL(targ, window.location.toString()))
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    return button
 | 
					    return button
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -192,7 +192,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
				
			||||||
    for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
 | 
					    for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
 | 
				
			||||||
      await index.addAsync(id, {
 | 
					      await index.addAsync(id, {
 | 
				
			||||||
        id,
 | 
					        id,
 | 
				
			||||||
        slug: slug as CanonicalSlug,
 | 
					        slug: slug as FullSlug,
 | 
				
			||||||
        title: fileData.title,
 | 
					        title: fileData.title,
 | 
				
			||||||
        content: fileData.content,
 | 
					        content: fileData.content,
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import micromorph from "micromorph"
 | 
					import micromorph from "micromorph"
 | 
				
			||||||
import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../util/path"
 | 
					import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// adapted from `micromorph`
 | 
					// adapted from `micromorph`
 | 
				
			||||||
// https://github.com/natemoo-re/micromorph
 | 
					// https://github.com/natemoo-re/micromorph
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined
 | 
				
			||||||
  return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
 | 
					  return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function notifyNav(url: CanonicalSlug) {
 | 
					function notifyNav(url: FullSlug) {
 | 
				
			||||||
  const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
 | 
					  const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
 | 
				
			||||||
  document.dispatchEvent(event)
 | 
					  document.dispatchEvent(event)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -81,7 +81,7 @@ async function navigate(url: URL, isBack: boolean = false) {
 | 
				
			||||||
  const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
 | 
					  const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
 | 
				
			||||||
  elementsToAdd.forEach((el) => document.head.appendChild(el))
 | 
					  elementsToAdd.forEach((el) => document.head.appendChild(el))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notifyNav(getCanonicalSlug(window))
 | 
					  notifyNav(getFullSlug(window))
 | 
				
			||||||
  delete announcer.dataset.persist
 | 
					  delete announcer.dataset.persist
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,7 +129,7 @@ function createRouter() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
createRouter()
 | 
					createRouter()
 | 
				
			||||||
notifyNav(getCanonicalSlug(window))
 | 
					notifyNav(getFullSlug(window))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (!customElements.get("route-announcer")) {
 | 
					if (!customElements.get("route-announcer")) {
 | 
				
			||||||
  const attrs = {
 | 
					  const attrs = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,4 @@
 | 
				
			||||||
import {
 | 
					import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
 | 
				
			||||||
  CanonicalSlug,
 | 
					 | 
				
			||||||
  FilePath,
 | 
					 | 
				
			||||||
  ServerSlug,
 | 
					 | 
				
			||||||
  canonicalizeServer,
 | 
					 | 
				
			||||||
  resolveRelative,
 | 
					 | 
				
			||||||
} from "../../util/path"
 | 
					 | 
				
			||||||
import { QuartzEmitterPlugin } from "../types"
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
import path from "path"
 | 
					import path from "path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,10 +11,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
				
			||||||
    const fps: FilePath[] = []
 | 
					    const fps: FilePath[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const [_tree, file] of content) {
 | 
					    for (const [_tree, file] of content) {
 | 
				
			||||||
      const ogSlug = canonicalizeServer(file.data.slug!)
 | 
					      const ogSlug = simplifySlug(file.data.slug!)
 | 
				
			||||||
      const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory)
 | 
					      const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let aliases: CanonicalSlug[] = []
 | 
					      let aliases: FullSlug[] = []
 | 
				
			||||||
      if (file.data.frontmatter?.aliases) {
 | 
					      if (file.data.frontmatter?.aliases) {
 | 
				
			||||||
        aliases = file.data.frontmatter?.aliases
 | 
					        aliases = file.data.frontmatter?.aliases
 | 
				
			||||||
      } else if (file.data.frontmatter?.alias) {
 | 
					      } else if (file.data.frontmatter?.alias) {
 | 
				
			||||||
| 
						 | 
					@ -28,9 +22,8 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const alias of aliases) {
 | 
					      for (const alias of aliases) {
 | 
				
			||||||
        const slug = path.posix.join(dir, alias) as ServerSlug
 | 
					        const slug = path.posix.join(dir, alias) as FullSlug
 | 
				
			||||||
 | 
					        const redirUrl = resolveRelative(slug, file.data.slug!)
 | 
				
			||||||
        const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
 | 
					 | 
				
			||||||
        const fp = await emit({
 | 
					        const fp = await emit({
 | 
				
			||||||
          content: `
 | 
					          content: `
 | 
				
			||||||
            <!DOCTYPE html>
 | 
					            <!DOCTYPE html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { FilePath, ServerSlug } from "../../util/path"
 | 
					import { FilePath, FullSlug } from "../../util/path"
 | 
				
			||||||
import { QuartzEmitterPlugin } from "../types"
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @ts-ignore
 | 
					// @ts-ignore
 | 
				
			||||||
| 
						 | 
					@ -154,7 +154,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
 | 
				
			||||||
      const postscript = joinScripts(componentResources.afterDOMLoaded)
 | 
					      const postscript = joinScripts(componentResources.afterDOMLoaded)
 | 
				
			||||||
      const fps = await Promise.all([
 | 
					      const fps = await Promise.all([
 | 
				
			||||||
        emit({
 | 
					        emit({
 | 
				
			||||||
          slug: "index" as ServerSlug,
 | 
					          slug: "index" as FullSlug,
 | 
				
			||||||
          ext: ".css",
 | 
					          ext: ".css",
 | 
				
			||||||
          content: transform({
 | 
					          content: transform({
 | 
				
			||||||
            filename: "index.css",
 | 
					            filename: "index.css",
 | 
				
			||||||
| 
						 | 
					@ -171,12 +171,12 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
 | 
				
			||||||
          }).code.toString(),
 | 
					          }).code.toString(),
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        emit({
 | 
					        emit({
 | 
				
			||||||
          slug: "prescript" as ServerSlug,
 | 
					          slug: "prescript" as FullSlug,
 | 
				
			||||||
          ext: ".js",
 | 
					          ext: ".js",
 | 
				
			||||||
          content: prescript,
 | 
					          content: prescript,
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        emit({
 | 
					        emit({
 | 
				
			||||||
          slug: "postscript" as ServerSlug,
 | 
					          slug: "postscript" as FullSlug,
 | 
				
			||||||
          ext: ".js",
 | 
					          ext: ".js",
 | 
				
			||||||
          content: postscript,
 | 
					          content: postscript,
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,12 @@
 | 
				
			||||||
import { GlobalConfiguration } from "../../cfg"
 | 
					import { GlobalConfiguration } from "../../cfg"
 | 
				
			||||||
import {
 | 
					import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
 | 
				
			||||||
  CanonicalSlug,
 | 
					 | 
				
			||||||
  ClientSlug,
 | 
					 | 
				
			||||||
  FilePath,
 | 
					 | 
				
			||||||
  ServerSlug,
 | 
					 | 
				
			||||||
  canonicalizeServer,
 | 
					 | 
				
			||||||
} from "../../util/path"
 | 
					 | 
				
			||||||
import { QuartzEmitterPlugin } from "../types"
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
import path from "path"
 | 
					import path from "path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ContentIndex = Map<CanonicalSlug, ContentDetails>
 | 
					export type ContentIndex = Map<FullSlug, ContentDetails>
 | 
				
			||||||
export type ContentDetails = {
 | 
					export type ContentDetails = {
 | 
				
			||||||
  title: string
 | 
					  title: string
 | 
				
			||||||
  links: CanonicalSlug[]
 | 
					  links: SimpleSlug[]
 | 
				
			||||||
  tags: string[]
 | 
					  tags: string[]
 | 
				
			||||||
  content: string
 | 
					  content: string
 | 
				
			||||||
  date?: Date
 | 
					  date?: Date
 | 
				
			||||||
| 
						 | 
					@ -33,21 +27,21 @@ const defaultOptions: Options = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
					function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
				
			||||||
  const base = cfg.baseUrl ?? ""
 | 
					  const base = cfg.baseUrl ?? ""
 | 
				
			||||||
  const createURLEntry = (slug: CanonicalSlug, content: ContentDetails): string => `<url>
 | 
					  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
 | 
				
			||||||
    <loc>https://${base}/${slug}</loc>
 | 
					    <loc>https://${base}/${slug}</loc>
 | 
				
			||||||
    <lastmod>${content.date?.toISOString()}</lastmod>
 | 
					    <lastmod>${content.date?.toISOString()}</lastmod>
 | 
				
			||||||
  </url>`
 | 
					  </url>`
 | 
				
			||||||
  const urls = Array.from(idx)
 | 
					  const urls = Array.from(idx)
 | 
				
			||||||
    .map(([slug, content]) => createURLEntry(slug, content))
 | 
					    .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
 | 
				
			||||||
    .join("")
 | 
					    .join("")
 | 
				
			||||||
  return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
 | 
					  return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
					function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
				
			||||||
  const base = cfg.baseUrl ?? ""
 | 
					  const base = cfg.baseUrl ?? ""
 | 
				
			||||||
  const root = `https://${base}` as ClientSlug
 | 
					  const root = `https://${base}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createURLEntry = (slug: CanonicalSlug, content: ContentDetails): string => `<items>
 | 
					  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<items>
 | 
				
			||||||
    <title>${content.title}</title>
 | 
					    <title>${content.title}</title>
 | 
				
			||||||
    <link>${root}/${slug}</link>
 | 
					    <link>${root}/${slug}</link>
 | 
				
			||||||
    <guid>${root}/${slug}</guid>
 | 
					    <guid>${root}/${slug}</guid>
 | 
				
			||||||
| 
						 | 
					@ -56,7 +50,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
				
			||||||
  </items>`
 | 
					  </items>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const items = Array.from(idx)
 | 
					  const items = Array.from(idx)
 | 
				
			||||||
    .map(([slug, content]) => createURLEntry(slug, content))
 | 
					    .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
 | 
				
			||||||
    .join("")
 | 
					    .join("")
 | 
				
			||||||
  return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
 | 
					  return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
 | 
				
			||||||
    <channel>
 | 
					    <channel>
 | 
				
			||||||
| 
						 | 
					@ -79,7 +73,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
				
			||||||
      const emitted: FilePath[] = []
 | 
					      const emitted: FilePath[] = []
 | 
				
			||||||
      const linkIndex: ContentIndex = new Map()
 | 
					      const linkIndex: ContentIndex = new Map()
 | 
				
			||||||
      for (const [_tree, file] of content) {
 | 
					      for (const [_tree, file] of content) {
 | 
				
			||||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
					        const slug = file.data.slug!
 | 
				
			||||||
        const date = file.data.dates?.modified ?? new Date()
 | 
					        const date = file.data.dates?.modified ?? new Date()
 | 
				
			||||||
        if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
 | 
					        if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
 | 
				
			||||||
          linkIndex.set(slug, {
 | 
					          linkIndex.set(slug, {
 | 
				
			||||||
| 
						 | 
					@ -97,7 +91,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
				
			||||||
        emitted.push(
 | 
					        emitted.push(
 | 
				
			||||||
          await emit({
 | 
					          await emit({
 | 
				
			||||||
            content: generateSiteMap(cfg, linkIndex),
 | 
					            content: generateSiteMap(cfg, linkIndex),
 | 
				
			||||||
            slug: "sitemap" as ServerSlug,
 | 
					            slug: "sitemap" as FullSlug,
 | 
				
			||||||
            ext: ".xml",
 | 
					            ext: ".xml",
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -107,13 +101,13 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
				
			||||||
        emitted.push(
 | 
					        emitted.push(
 | 
				
			||||||
          await emit({
 | 
					          await emit({
 | 
				
			||||||
            content: generateRSSFeed(cfg, linkIndex),
 | 
					            content: generateRSSFeed(cfg, linkIndex),
 | 
				
			||||||
            slug: "index" as ServerSlug,
 | 
					            slug: "index" as FullSlug,
 | 
				
			||||||
            ext: ".xml",
 | 
					            ext: ".xml",
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fp = path.join("static", "contentIndex") as ServerSlug
 | 
					      const fp = path.join("static", "contentIndex") as FullSlug
 | 
				
			||||||
      const simplifiedIndex = Object.fromEntries(
 | 
					      const simplifiedIndex = Object.fromEntries(
 | 
				
			||||||
        Array.from(linkIndex).map(([slug, content]) => {
 | 
					        Array.from(linkIndex).map(([slug, content]) => {
 | 
				
			||||||
          // remove description and from content index as nothing downstream
 | 
					          // remove description and from content index as nothing downstream
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header"
 | 
				
			||||||
import BodyConstructor from "../../components/Body"
 | 
					import BodyConstructor from "../../components/Body"
 | 
				
			||||||
import { pageResources, renderPage } from "../../components/renderPage"
 | 
					import { pageResources, renderPage } from "../../components/renderPage"
 | 
				
			||||||
import { FullPageLayout } from "../../cfg"
 | 
					import { FullPageLayout } from "../../cfg"
 | 
				
			||||||
import { FilePath, canonicalizeServer } from "../../util/path"
 | 
					import { FilePath } from "../../util/path"
 | 
				
			||||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
					import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
				
			||||||
import { Content } from "../../components"
 | 
					import { Content } from "../../components"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
 | 
				
			||||||
      const fps: FilePath[] = []
 | 
					      const fps: FilePath[] = []
 | 
				
			||||||
      const allFiles = content.map((c) => c[1].data)
 | 
					      const allFiles = content.map((c) => c[1].data)
 | 
				
			||||||
      for (const [tree, file] of content) {
 | 
					      for (const [tree, file] of content) {
 | 
				
			||||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
					        const slug = file.data.slug!
 | 
				
			||||||
        const externalResources = pageResources(slug, resources)
 | 
					        const externalResources = pageResources(slug, resources)
 | 
				
			||||||
        const componentData: QuartzComponentProps = {
 | 
					        const componentData: QuartzComponentProps = {
 | 
				
			||||||
          fileData: file.data,
 | 
					          fileData: file.data,
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
 | 
				
			||||||
        const content = renderPage(slug, componentData, opts, externalResources)
 | 
					        const content = renderPage(slug, componentData, opts, externalResources)
 | 
				
			||||||
        const fp = await emit({
 | 
					        const fp = await emit({
 | 
				
			||||||
          content,
 | 
					          content,
 | 
				
			||||||
          slug: file.data.slug!,
 | 
					          slug,
 | 
				
			||||||
          ext: ".html",
 | 
					          ext: ".html",
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,13 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage"
 | 
				
			||||||
import { ProcessedContent, defaultProcessedContent } from "../vfile"
 | 
					import { ProcessedContent, defaultProcessedContent } from "../vfile"
 | 
				
			||||||
import { FullPageLayout } from "../../cfg"
 | 
					import { FullPageLayout } from "../../cfg"
 | 
				
			||||||
import path from "path"
 | 
					import path from "path"
 | 
				
			||||||
import {
 | 
					import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
 | 
				
			||||||
  CanonicalSlug,
 | 
					 | 
				
			||||||
  FilePath,
 | 
					 | 
				
			||||||
  ServerSlug,
 | 
					 | 
				
			||||||
  canonicalizeServer,
 | 
					 | 
				
			||||||
  joinSegments,
 | 
					 | 
				
			||||||
} from "../../util/path"
 | 
					 | 
				
			||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
					import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
				
			||||||
import { FolderContent } from "../../components"
 | 
					import { FolderContent } from "../../components"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,10 +32,10 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
				
			||||||
      const allFiles = content.map((c) => c[1].data)
 | 
					      const allFiles = content.map((c) => c[1].data)
 | 
				
			||||||
      const cfg = ctx.cfg.configuration
 | 
					      const cfg = ctx.cfg.configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const folders: Set<CanonicalSlug> = new Set(
 | 
					      const folders: Set<SimpleSlug> = new Set(
 | 
				
			||||||
        allFiles.flatMap((data) => {
 | 
					        allFiles.flatMap((data) => {
 | 
				
			||||||
          const slug = data.slug
 | 
					          const slug = data.slug
 | 
				
			||||||
          const folderName = path.dirname(slug ?? "") as CanonicalSlug
 | 
					          const folderName = path.dirname(slug ?? "") as SimpleSlug
 | 
				
			||||||
          if (slug && folderName !== "." && folderName !== "tags") {
 | 
					          if (slug && folderName !== "." && folderName !== "tags") {
 | 
				
			||||||
            return [folderName]
 | 
					            return [folderName]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
| 
						 | 
					@ -53,21 +47,21 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
				
			||||||
        [...folders].map((folder) => [
 | 
					        [...folders].map((folder) => [
 | 
				
			||||||
          folder,
 | 
					          folder,
 | 
				
			||||||
          defaultProcessedContent({
 | 
					          defaultProcessedContent({
 | 
				
			||||||
            slug: joinSegments(folder, "index") as ServerSlug,
 | 
					            slug: joinSegments(folder, "index") as FullSlug,
 | 
				
			||||||
            frontmatter: { title: `Folder: ${folder}`, tags: [] },
 | 
					            frontmatter: { title: `Folder: ${folder}`, tags: [] },
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
        ]),
 | 
					        ]),
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const [tree, file] of content) {
 | 
					      for (const [tree, file] of content) {
 | 
				
			||||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
					        const slug = simplifySlug(file.data.slug!)
 | 
				
			||||||
        if (folders.has(slug)) {
 | 
					        if (folders.has(slug)) {
 | 
				
			||||||
          folderDescriptions[slug] = [tree, file]
 | 
					          folderDescriptions[slug] = [tree, file]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const folder of folders) {
 | 
					      for (const folder of folders) {
 | 
				
			||||||
        const slug = folder
 | 
					        const slug = joinSegments(folder, "index") as FullSlug
 | 
				
			||||||
        const externalResources = pageResources(slug, resources)
 | 
					        const externalResources = pageResources(slug, resources)
 | 
				
			||||||
        const [tree, file] = folderDescriptions[folder]
 | 
					        const [tree, file] = folderDescriptions[folder]
 | 
				
			||||||
        const componentData: QuartzComponentProps = {
 | 
					        const componentData: QuartzComponentProps = {
 | 
				
			||||||
| 
						 | 
					@ -82,7 +76,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
				
			||||||
        const content = renderPage(slug, componentData, opts, externalResources)
 | 
					        const content = renderPage(slug, componentData, opts, externalResources)
 | 
				
			||||||
        const fp = await emit({
 | 
					        const fp = await emit({
 | 
				
			||||||
          content,
 | 
					          content,
 | 
				
			||||||
          slug: file.data.slug!,
 | 
					          slug,
 | 
				
			||||||
          ext: ".html",
 | 
					          ext: ".html",
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,13 +5,7 @@ import BodyConstructor from "../../components/Body"
 | 
				
			||||||
import { pageResources, renderPage } from "../../components/renderPage"
 | 
					import { pageResources, renderPage } from "../../components/renderPage"
 | 
				
			||||||
import { ProcessedContent, defaultProcessedContent } from "../vfile"
 | 
					import { ProcessedContent, defaultProcessedContent } from "../vfile"
 | 
				
			||||||
import { FullPageLayout } from "../../cfg"
 | 
					import { FullPageLayout } from "../../cfg"
 | 
				
			||||||
import {
 | 
					import { FilePath, FullSlug, getAllSegmentPrefixes, joinSegments } from "../../util/path"
 | 
				
			||||||
  CanonicalSlug,
 | 
					 | 
				
			||||||
  FilePath,
 | 
					 | 
				
			||||||
  ServerSlug,
 | 
					 | 
				
			||||||
  getAllSegmentPrefixes,
 | 
					 | 
				
			||||||
  joinSegments,
 | 
					 | 
				
			||||||
} from "../../util/path"
 | 
					 | 
				
			||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
					import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
				
			||||||
import { TagContent } from "../../components"
 | 
					import { TagContent } from "../../components"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +43,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
				
			||||||
          return [
 | 
					          return [
 | 
				
			||||||
            tag,
 | 
					            tag,
 | 
				
			||||||
            defaultProcessedContent({
 | 
					            defaultProcessedContent({
 | 
				
			||||||
              slug: joinSegments("tags", tag) as ServerSlug,
 | 
					              slug: joinSegments("tags", tag) as FullSlug,
 | 
				
			||||||
              frontmatter: { title, tags: [] },
 | 
					              frontmatter: { title, tags: [] },
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
| 
						 | 
					@ -67,7 +61,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const tag of tags) {
 | 
					      for (const tag of tags) {
 | 
				
			||||||
        const slug = joinSegments("tags", tag) as CanonicalSlug
 | 
					        const slug = joinSegments("tags", tag) as FullSlug
 | 
				
			||||||
        const externalResources = pageResources(slug, resources)
 | 
					        const externalResources = pageResources(slug, resources)
 | 
				
			||||||
        const [tree, file] = tagDescriptions[tag]
 | 
					        const [tree, file] = tagDescriptions[tag]
 | 
				
			||||||
        const componentData: QuartzComponentProps = {
 | 
					        const componentData: QuartzComponentProps = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { StaticResources } from "../util/resources"
 | 
					import { StaticResources } from "../util/resources"
 | 
				
			||||||
import { FilePath, ServerSlug } from "../util/path"
 | 
					import { FilePath, FullSlug } from "../util/path"
 | 
				
			||||||
import { BuildCtx } from "../util/ctx"
 | 
					import { BuildCtx } from "../util/ctx"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
 | 
					export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ export * from "./emitters"
 | 
				
			||||||
declare module "vfile" {
 | 
					declare module "vfile" {
 | 
				
			||||||
  // inserted in processors.ts
 | 
					  // inserted in processors.ts
 | 
				
			||||||
  interface DataMap {
 | 
					  interface DataMap {
 | 
				
			||||||
    slug: ServerSlug
 | 
					    slug: FullSlug
 | 
				
			||||||
    filePath: FilePath
 | 
					    filePath: FilePath
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,12 @@
 | 
				
			||||||
import { QuartzTransformerPlugin } from "../types"
 | 
					import { QuartzTransformerPlugin } from "../types"
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CanonicalSlug,
 | 
					  FullSlug,
 | 
				
			||||||
  RelativeURL,
 | 
					  RelativeURL,
 | 
				
			||||||
 | 
					  SimpleSlug,
 | 
				
			||||||
  TransformOptions,
 | 
					  TransformOptions,
 | 
				
			||||||
  _stripSlashes,
 | 
					  _stripSlashes,
 | 
				
			||||||
  canonicalizeServer,
 | 
					 | 
				
			||||||
  joinSegments,
 | 
					  joinSegments,
 | 
				
			||||||
 | 
					  simplifySlug,
 | 
				
			||||||
  splitAnchor,
 | 
					  splitAnchor,
 | 
				
			||||||
  transformLink,
 | 
					  transformLink,
 | 
				
			||||||
} from "../../util/path"
 | 
					} from "../../util/path"
 | 
				
			||||||
| 
						 | 
					@ -33,8 +34,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        () => {
 | 
					        () => {
 | 
				
			||||||
          return (tree, file) => {
 | 
					          return (tree, file) => {
 | 
				
			||||||
            const curSlug = canonicalizeServer(file.data.slug!)
 | 
					            const curSlug = simplifySlug(file.data.slug!)
 | 
				
			||||||
            const outgoing: Set<CanonicalSlug> = new Set()
 | 
					            const outgoing: Set<SimpleSlug> = new Set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const transformOptions: TransformOptions = {
 | 
					            const transformOptions: TransformOptions = {
 | 
				
			||||||
              strategy: opts.markdownLinkResolution,
 | 
					              strategy: opts.markdownLinkResolution,
 | 
				
			||||||
| 
						 | 
					@ -54,10 +55,15 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // don't process external links or intra-document anchors
 | 
					                // don't process external links or intra-document anchors
 | 
				
			||||||
                if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
					                if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
				
			||||||
                  dest = node.properties.href = transformLink(curSlug, dest, transformOptions)
 | 
					                  dest = node.properties.href = transformLink(
 | 
				
			||||||
 | 
					                    file.data.slug!,
 | 
				
			||||||
 | 
					                    dest,
 | 
				
			||||||
 | 
					                    transformOptions,
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
                  const canonicalDest = path.posix.normalize(joinSegments(curSlug, dest))
 | 
					                  const canonicalDest = path.posix.normalize(joinSegments(curSlug, dest))
 | 
				
			||||||
                  const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
					                  const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
				
			||||||
                  outgoing.add(destCanonical as CanonicalSlug)
 | 
					                  const simple = simplifySlug(destCanonical as FullSlug)
 | 
				
			||||||
 | 
					                  outgoing.add(simple)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // rewrite link internals if prettylinks is on
 | 
					                // rewrite link internals if prettylinks is on
 | 
				
			||||||
| 
						 | 
					@ -79,7 +85,11 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
              ) {
 | 
					              ) {
 | 
				
			||||||
                if (!isAbsoluteUrl(node.properties.src)) {
 | 
					                if (!isAbsoluteUrl(node.properties.src)) {
 | 
				
			||||||
                  let dest = node.properties.src as RelativeURL
 | 
					                  let dest = node.properties.src as RelativeURL
 | 
				
			||||||
                  dest = node.properties.src = transformLink(curSlug, dest, transformOptions)
 | 
					                  dest = node.properties.src = transformLink(
 | 
				
			||||||
 | 
					                    file.data.slug!,
 | 
				
			||||||
 | 
					                    dest,
 | 
				
			||||||
 | 
					                    transformOptions,
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
                  node.properties.src = dest
 | 
					                  node.properties.src = dest
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
| 
						 | 
					@ -95,6 +105,6 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module "vfile" {
 | 
					declare module "vfile" {
 | 
				
			||||||
  interface DataMap {
 | 
					  interface DataMap {
 | 
				
			||||||
    links: CanonicalSlug[]
 | 
					    links: SimpleSlug[]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ import path from "path"
 | 
				
			||||||
import { JSResource } from "../../util/resources"
 | 
					import { JSResource } from "../../util/resources"
 | 
				
			||||||
// @ts-ignore
 | 
					// @ts-ignore
 | 
				
			||||||
import calloutScript from "../../components/scripts/callout.inline.ts"
 | 
					import calloutScript from "../../components/scripts/callout.inline.ts"
 | 
				
			||||||
import { FilePath, canonicalizeServer, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
 | 
					import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
 | 
				
			||||||
import { toHast } from "mdast-util-to-hast"
 | 
					import { toHast } from "mdast-util-to-hast"
 | 
				
			||||||
import { toHtml } from "hast-util-to-html"
 | 
					import { toHtml } from "hast-util-to-html"
 | 
				
			||||||
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
 | 
					import { PhrasingContent } from "mdast-util-find-and-replace/lib"
 | 
				
			||||||
| 
						 | 
					@ -381,8 +381,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
				
			||||||
      if (opts.parseTags) {
 | 
					      if (opts.parseTags) {
 | 
				
			||||||
        plugins.push(() => {
 | 
					        plugins.push(() => {
 | 
				
			||||||
          return (tree: Root, file) => {
 | 
					          return (tree: Root, file) => {
 | 
				
			||||||
            const slug = canonicalizeServer(file.data.slug!)
 | 
					            const base = pathToRoot(file.data.slug!)
 | 
				
			||||||
            const base = pathToRoot(slug)
 | 
					 | 
				
			||||||
            findAndReplace(tree, tagRegex, (value: string, tag: string) => {
 | 
					            findAndReplace(tree, tagRegex, (value: string, tag: string) => {
 | 
				
			||||||
              if (file.data.frontmatter) {
 | 
					              if (file.data.frontmatter) {
 | 
				
			||||||
                file.data.frontmatter.tags.push(tag)
 | 
					                file.data.frontmatter.tags.push(tag)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import { PluggableList } from "unified"
 | 
				
			||||||
import { StaticResources } from "../util/resources"
 | 
					import { StaticResources } from "../util/resources"
 | 
				
			||||||
import { ProcessedContent } from "./vfile"
 | 
					import { ProcessedContent } from "./vfile"
 | 
				
			||||||
import { QuartzComponent } from "../components/types"
 | 
					import { QuartzComponent } from "../components/types"
 | 
				
			||||||
import { FilePath, ServerSlug } from "../util/path"
 | 
					import { FilePath, FullSlug } from "../util/path"
 | 
				
			||||||
import { BuildCtx } from "../util/ctx"
 | 
					import { BuildCtx } from "../util/ctx"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface PluginTypes {
 | 
					export interface PluginTypes {
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ export type QuartzEmitterPluginInstance = {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface EmitOptions {
 | 
					export interface EmitOptions {
 | 
				
			||||||
  slug: ServerSlug
 | 
					  slug: FullSlug
 | 
				
			||||||
  ext: `.${string}` | ""
 | 
					  ext: `.${string}` | ""
 | 
				
			||||||
  content: string
 | 
					  content: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { QuartzConfig } from "../cfg"
 | 
					import { QuartzConfig } from "../cfg"
 | 
				
			||||||
import { ServerSlug } from "./path"
 | 
					import { FullSlug } from "./path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Argv {
 | 
					export interface Argv {
 | 
				
			||||||
  directory: string
 | 
					  directory: string
 | 
				
			||||||
| 
						 | 
					@ -13,5 +13,5 @@ export interface Argv {
 | 
				
			||||||
export interface BuildCtx {
 | 
					export interface BuildCtx {
 | 
				
			||||||
  argv: Argv
 | 
					  argv: Argv
 | 
				
			||||||
  cfg: QuartzConfig
 | 
					  cfg: QuartzConfig
 | 
				
			||||||
  allSlugs: ServerSlug[]
 | 
					  allSlugs: FullSlug[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,45 +1,25 @@
 | 
				
			||||||
import test, { describe } from "node:test"
 | 
					import test, { describe } from "node:test"
 | 
				
			||||||
import * as path from "./path"
 | 
					import * as path from "./path"
 | 
				
			||||||
import assert from "node:assert"
 | 
					import assert from "node:assert"
 | 
				
			||||||
import { CanonicalSlug, ServerSlug, TransformOptions } from "./path"
 | 
					import { FullSlug, TransformOptions } from "./path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("typeguards", () => {
 | 
					describe("typeguards", () => {
 | 
				
			||||||
  test("isClientSlug", () => {
 | 
					  test("isSimpleSlug", () => {
 | 
				
			||||||
    assert(path.isClientSlug("http://example.com"))
 | 
					    assert(path.isSimpleSlug(""))
 | 
				
			||||||
    assert(path.isClientSlug("http://example.com/index"))
 | 
					    assert(path.isSimpleSlug("abc"))
 | 
				
			||||||
    assert(path.isClientSlug("http://example.com/index.html"))
 | 
					    assert(path.isSimpleSlug("abc/"))
 | 
				
			||||||
    assert(path.isClientSlug("http://example.com/"))
 | 
					    assert(path.isSimpleSlug("notindex"))
 | 
				
			||||||
    assert(path.isClientSlug("https://example.com"))
 | 
					    assert(path.isSimpleSlug("notindex/def"))
 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def"))
 | 
					 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def/"))
 | 
					 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def#cool"))
 | 
					 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def?field=1&another=2"))
 | 
					 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def?field=1&another=2#cool"))
 | 
					 | 
				
			||||||
    assert(path.isClientSlug("https://example.com/abc/def.html?field=1&another=2#cool"))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert(!path.isClientSlug("./"))
 | 
					    assert(!path.isSimpleSlug("//"))
 | 
				
			||||||
    assert(!path.isClientSlug(""))
 | 
					    assert(!path.isSimpleSlug("index"))
 | 
				
			||||||
    assert(!path.isClientSlug("ipfs://example.com"))
 | 
					    assert(!path.isSimpleSlug("https://example.com"))
 | 
				
			||||||
    assert(!path.isClientSlug("http"))
 | 
					    assert(!path.isSimpleSlug("/abc"))
 | 
				
			||||||
    assert(!path.isClientSlug("https"))
 | 
					    assert(!path.isSimpleSlug("abc/index"))
 | 
				
			||||||
  })
 | 
					    assert(!path.isSimpleSlug("abc#anchor"))
 | 
				
			||||||
 | 
					    assert(!path.isSimpleSlug("abc?query=1"))
 | 
				
			||||||
  test("isCanonicalSlug", () => {
 | 
					    assert(!path.isSimpleSlug("index.md"))
 | 
				
			||||||
    assert(path.isCanonicalSlug(""))
 | 
					    assert(!path.isSimpleSlug("index.html"))
 | 
				
			||||||
    assert(path.isCanonicalSlug("abc"))
 | 
					 | 
				
			||||||
    assert(path.isCanonicalSlug("notindex"))
 | 
					 | 
				
			||||||
    assert(path.isCanonicalSlug("notindex/def"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assert(!path.isCanonicalSlug("//"))
 | 
					 | 
				
			||||||
    assert(!path.isCanonicalSlug("index"))
 | 
					 | 
				
			||||||
    assert(!path.isCanonicalSlug("https://example.com"))
 | 
					 | 
				
			||||||
    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", () => {
 | 
					  test("isRelativeURL", () => {
 | 
				
			||||||
| 
						 | 
					@ -58,18 +38,18 @@ describe("typeguards", () => {
 | 
				
			||||||
    assert(!path.isRelativeURL("./abc/def.md"))
 | 
					    assert(!path.isRelativeURL("./abc/def.md"))
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test("isServerSlug", () => {
 | 
					  test("isFullSlug", () => {
 | 
				
			||||||
    assert(path.isServerSlug("index"))
 | 
					    assert(path.isFullSlug("index"))
 | 
				
			||||||
    assert(path.isServerSlug("abc/def"))
 | 
					    assert(path.isFullSlug("abc/def"))
 | 
				
			||||||
    assert(path.isServerSlug("html.energy"))
 | 
					    assert(path.isFullSlug("html.energy"))
 | 
				
			||||||
    assert(path.isServerSlug("test.pdf"))
 | 
					    assert(path.isFullSlug("test.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert(!path.isServerSlug("."))
 | 
					    assert(!path.isFullSlug("."))
 | 
				
			||||||
    assert(!path.isServerSlug("./abc/def"))
 | 
					    assert(!path.isFullSlug("./abc/def"))
 | 
				
			||||||
    assert(!path.isServerSlug("../abc/def"))
 | 
					    assert(!path.isFullSlug("../abc/def"))
 | 
				
			||||||
    assert(!path.isServerSlug("abc/def#anchor"))
 | 
					    assert(!path.isFullSlug("abc/def#anchor"))
 | 
				
			||||||
    assert(!path.isServerSlug("abc/def?query=1"))
 | 
					    assert(!path.isFullSlug("abc/def?query=1"))
 | 
				
			||||||
    assert(!path.isServerSlug("note with spaces"))
 | 
					    assert(!path.isFullSlug("note with spaces"))
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test("isFilePath", () => {
 | 
					  test("isFilePath", () => {
 | 
				
			||||||
| 
						 | 
					@ -100,40 +80,17 @@ describe("transforms", () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test("canonicalizeServer", () => {
 | 
					  test("simplifySlug", () => {
 | 
				
			||||||
    asserts(
 | 
					    asserts(
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        ["index", ""],
 | 
					        ["index", ""],
 | 
				
			||||||
        ["abc/index", "abc"],
 | 
					        ["abc", "abc"],
 | 
				
			||||||
 | 
					        ["abc/index", "abc/"],
 | 
				
			||||||
        ["abc/def", "abc/def"],
 | 
					        ["abc/def", "abc/def"],
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      path.canonicalizeServer,
 | 
					      path.simplifySlug,
 | 
				
			||||||
      path.isServerSlug,
 | 
					      path.isFullSlug,
 | 
				
			||||||
      path.isCanonicalSlug,
 | 
					      path.isSimpleSlug,
 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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"],
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      path.canonicalizeClient,
 | 
					 | 
				
			||||||
      path.isClientSlug,
 | 
					 | 
				
			||||||
      path.isCanonicalSlug,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -151,7 +108,7 @@ describe("transforms", () => {
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      path.slugifyFilePath,
 | 
					      path.slugifyFilePath,
 | 
				
			||||||
      path.isFilePath,
 | 
					      path.isFilePath,
 | 
				
			||||||
      path.isServerSlug,
 | 
					      path.isFullSlug,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -186,12 +143,14 @@ describe("transforms", () => {
 | 
				
			||||||
  test("pathToRoot", () => {
 | 
					  test("pathToRoot", () => {
 | 
				
			||||||
    asserts(
 | 
					    asserts(
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        ["", "."],
 | 
					        ["index", "."],
 | 
				
			||||||
        ["abc", ".."],
 | 
					        ["abc", "."],
 | 
				
			||||||
        ["abc/def", "../.."],
 | 
					        ["abc/def", ".."],
 | 
				
			||||||
 | 
					        ["abc/def/ghi", "../.."],
 | 
				
			||||||
 | 
					        ["abc/def/index", "../.."],
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      path.pathToRoot,
 | 
					      path.pathToRoot,
 | 
				
			||||||
      path.isCanonicalSlug,
 | 
					      path.isFullSlug,
 | 
				
			||||||
      path.isRelativeURL,
 | 
					      path.isRelativeURL,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
| 
						 | 
					@ -206,7 +165,7 @@ describe("link strategies", () => {
 | 
				
			||||||
    "e/g/h",
 | 
					    "e/g/h",
 | 
				
			||||||
    "index",
 | 
					    "index",
 | 
				
			||||||
    "a/test.png",
 | 
					    "a/test.png",
 | 
				
			||||||
  ] as ServerSlug[]
 | 
					  ] as FullSlug[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("absolute", () => {
 | 
					  describe("absolute", () => {
 | 
				
			||||||
    const opts: TransformOptions = {
 | 
					    const opts: TransformOptions = {
 | 
				
			||||||
| 
						 | 
					@ -215,28 +174,28 @@ describe("link strategies", () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/c", () => {
 | 
					    test("from a/b/c", () => {
 | 
				
			||||||
      const cur = "a/b/c" as CanonicalSlug
 | 
					      const cur = "a/b/c" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../../a/b/d")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../../e/f")
 | 
					      assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../e/f")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../../e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../e/g/h")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index", opts), "../../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png")
 | 
					      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc")
 | 
					      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../../tag/test")
 | 
					      assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../tag/test")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../../a/b/c#test")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../a/b/c#test")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../../a/test.png")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../a/test.png")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/index", () => {
 | 
					    test("from a/b/index", () => {
 | 
				
			||||||
      const cur = "a/b" as CanonicalSlug
 | 
					      const cur = "a/b/index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b", opts), "../../a/b")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b", opts), "../../a/b")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from index", () => {
 | 
					    test("from index", () => {
 | 
				
			||||||
      const cur = "" as CanonicalSlug
 | 
					      const cur = "index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index", opts), "./")
 | 
					      assert.strictEqual(path.transformLink(cur, "index", opts), "./")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/c", opts), "./a/b/c")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/c", opts), "./a/b/c")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
				
			||||||
| 
						 | 
					@ -250,20 +209,20 @@ describe("link strategies", () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/c", () => {
 | 
					    test("from a/b/c", () => {
 | 
				
			||||||
      const cur = "a/b/c" as CanonicalSlug
 | 
					      const cur = "a/b/c" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "d", opts), "../../../a/b/d")
 | 
					      assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "h", opts), "../../../e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../../a/b/index.png")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../a/b/index.png")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../../a/b/#abc")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../a/b/#abc")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index", opts), "../../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png")
 | 
					      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../../a/test.png")
 | 
					      assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../a/test.png")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc")
 | 
					      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/index", () => {
 | 
					    test("from a/b/index", () => {
 | 
				
			||||||
      const cur = "a/b" as CanonicalSlug
 | 
					      const cur = "a/b/index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
 | 
					      assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
 | 
				
			||||||
| 
						 | 
					@ -271,7 +230,7 @@ describe("link strategies", () => {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from index", () => {
 | 
					    test("from index", () => {
 | 
				
			||||||
      const cur = "" as CanonicalSlug
 | 
					      const cur = "index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "d", opts), "./a/b/d")
 | 
					      assert.strictEqual(path.transformLink(cur, "d", opts), "./a/b/d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "h", opts), "./e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "h", opts), "./e/g/h")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
				
			||||||
| 
						 | 
					@ -286,7 +245,7 @@ describe("link strategies", () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/c", () => {
 | 
					    test("from a/b/c", () => {
 | 
				
			||||||
      const cur = "a/b/c" as CanonicalSlug
 | 
					      const cur = "a/b/c" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "d", opts), "./d")
 | 
					      assert.strictEqual(path.transformLink(cur, "d", opts), "./d")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "index", opts), "./")
 | 
					      assert.strictEqual(path.transformLink(cur, "index", opts), "./")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../")
 | 
				
			||||||
| 
						 | 
					@ -303,7 +262,7 @@ describe("link strategies", () => {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from a/b/index", () => {
 | 
					    test("from a/b/index", () => {
 | 
				
			||||||
      const cur = "a/b" as CanonicalSlug
 | 
					      const cur = "a/b/index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "../../", opts), "../../")
 | 
					      assert.strictEqual(path.transformLink(cur, "../../", opts), "../../")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h")
 | 
				
			||||||
| 
						 | 
					@ -311,7 +270,7 @@ describe("link strategies", () => {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test("from index", () => {
 | 
					    test("from index", () => {
 | 
				
			||||||
      const cur = "" as CanonicalSlug
 | 
					      const cur = "index" as FullSlug
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h")
 | 
					      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h")
 | 
				
			||||||
      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
					      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,73 +1,35 @@
 | 
				
			||||||
import { slug } from "github-slugger"
 | 
					import { slug } from "github-slugger"
 | 
				
			||||||
// this file must be isomorphic so it can't use node libs (e.g. path)
 | 
					// this file must be isomorphic so it can't use node libs (e.g. path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Quartz Paths
 | 
					 | 
				
			||||||
// Things in boxes are not actual types but rather sources which these types can be acquired from
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
//                                                         ┌────────────┐
 | 
					 | 
				
			||||||
//                                             ┌───────────┤  Browser   ├────────────┐
 | 
					 | 
				
			||||||
//                                             │           └────────────┘            │
 | 
					 | 
				
			||||||
//                                             │                                     │
 | 
					 | 
				
			||||||
//                                             ▼                                     ▼
 | 
					 | 
				
			||||||
//                                        ┌────────┐                          ┌─────────────┐
 | 
					 | 
				
			||||||
//                    ┌───────────────────┤ Window │                          │ LinkElement │
 | 
					 | 
				
			||||||
//                    │                   └────┬───┘                          └──────┬──────┘
 | 
					 | 
				
			||||||
//                    │                        │                                     │
 | 
					 | 
				
			||||||
//                    │        getClientSlug() │                               .href │
 | 
					 | 
				
			||||||
//                    │                        ▼                                     ▼
 | 
					 | 
				
			||||||
//                    │
 | 
					 | 
				
			||||||
//                    │                  Client Slug                    ┌───►  Relative URL
 | 
					 | 
				
			||||||
// getCanonicalSlug() │     https://test.ca/note/abc#anchor?query=123   │      ../note/def#anchor
 | 
					 | 
				
			||||||
//                    │                                                 │
 | 
					 | 
				
			||||||
//                    │   canonicalizeClient() │                        │      ▲     ▲
 | 
					 | 
				
			||||||
//                    │                        ▼                        │      │     │
 | 
					 | 
				
			||||||
//                    │                                  pathToRoot()   │      │     │
 | 
					 | 
				
			||||||
//                    └───────────────►  Canonical Slug ────────────────┘      │     │
 | 
					 | 
				
			||||||
//                                          note/abc                           │     │
 | 
					 | 
				
			||||||
//                                                   ──────────────────────────┘     │
 | 
					 | 
				
			||||||
//                                             ▲             resolveRelative()       │
 | 
					 | 
				
			||||||
//                        canonicalizeServer() │                                     │
 | 
					 | 
				
			||||||
//                                                                                   │
 | 
					 | 
				
			||||||
// HTML File                               Server Slug                               │
 | 
					 | 
				
			||||||
//  note/abc/index.html  ◄─────────────   note/abc/index                             │
 | 
					 | 
				
			||||||
//                                                                                   │
 | 
					 | 
				
			||||||
//                                             ▲                            ┌────────┴────────┐
 | 
					 | 
				
			||||||
//                           slugifyFilePath() │            transformLink() │                 │
 | 
					 | 
				
			||||||
//                                             │                            │                 │
 | 
					 | 
				
			||||||
//                                   ┌─────────┴──────────┐           ┌─────┴─────┐  ┌────────┴──────┐
 | 
					 | 
				
			||||||
//                                   │     File Path      │           │ Wikilinks │  │ Markdown Link │
 | 
					 | 
				
			||||||
//                                   │  note/abc/index.md │           └───────────┘  └───────────────┘
 | 
					 | 
				
			||||||
//                                   └────────────────────┘                 ▲                 ▲
 | 
					 | 
				
			||||||
//                                             ▲                            │                 │
 | 
					 | 
				
			||||||
//                                             │            ┌─────────┐     │                 │
 | 
					 | 
				
			||||||
//                                             └────────────┤ MD File ├─────┴─────────────────┘
 | 
					 | 
				
			||||||
//                                                          └─────────┘
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const QUARTZ = "quartz"
 | 
					export const QUARTZ = "quartz"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Utility type to simulate nominal types in TypeScript
 | 
					/// Utility type to simulate nominal types in TypeScript
 | 
				
			||||||
type SlugLike<T> = string & { __brand: T }
 | 
					type SlugLike<T> = string & { __brand: T }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Client-side slug, usually obtained through `window.location` */
 | 
					/** Cannot be relative and must have a file extension. */
 | 
				
			||||||
export type ClientSlug = SlugLike<"client">
 | 
					export type FilePath = SlugLike<"filepath">
 | 
				
			||||||
export function isClientSlug(s: string): s is ClientSlug {
 | 
					export function isFilePath(s: string): s is FilePath {
 | 
				
			||||||
  const res = /^https?:\/\/.+/.test(s)
 | 
					  const validStart = !s.startsWith(".")
 | 
				
			||||||
  return res
 | 
					  return validStart && _hasFileExtension(s)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Canonical slug, should be used whenever you need to refer to the location of a file/note.
 | 
					/** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */
 | 
				
			||||||
 * On the client, this is normally stored in `document.body.dataset.slug`
 | 
					export type FullSlug = SlugLike<"full">
 | 
				
			||||||
 */
 | 
					export function isFullSlug(s: string): s is FullSlug {
 | 
				
			||||||
export type CanonicalSlug = SlugLike<"canonical">
 | 
					 | 
				
			||||||
export function isCanonicalSlug(s: string): s is CanonicalSlug {
 | 
					 | 
				
			||||||
  const validStart = !(s.startsWith(".") || s.startsWith("/"))
 | 
					  const validStart = !(s.startsWith(".") || s.startsWith("/"))
 | 
				
			||||||
  const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index")
 | 
					  const validEnding = !s.endsWith("/")
 | 
				
			||||||
 | 
					  return validStart && validEnding && !_containsForbiddenCharacters(s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
 | 
				
			||||||
 | 
					export type SimpleSlug = SlugLike<"simple">
 | 
				
			||||||
 | 
					export function isSimpleSlug(s: string): s is SimpleSlug {
 | 
				
			||||||
 | 
					  const validStart = !(s.startsWith(".") || s.startsWith("/"))
 | 
				
			||||||
 | 
					  const validEnding = !(s.endsWith("/index") || s === "index")
 | 
				
			||||||
  return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
 | 
					  return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** A relative link, can be found on `href`s but can also be constructed for
 | 
					/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
 | 
				
			||||||
 * client-side navigation (e.g. search and graph)
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export type RelativeURL = SlugLike<"relative">
 | 
					export type RelativeURL = SlugLike<"relative">
 | 
				
			||||||
export function isRelativeURL(s: string): s is RelativeURL {
 | 
					export function isRelativeURL(s: string): s is RelativeURL {
 | 
				
			||||||
  const validStart = /^\.{1,2}/.test(s)
 | 
					  const validStart = /^\.{1,2}/.test(s)
 | 
				
			||||||
| 
						 | 
					@ -75,46 +37,12 @@ export function isRelativeURL(s: string): s is RelativeURL {
 | 
				
			||||||
  return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
 | 
					  return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** A server side slug. This is what Quartz uses to emit files so uses index suffixes */
 | 
					export function getFullSlug(window: Window): FullSlug {
 | 
				
			||||||
export type ServerSlug = SlugLike<"server">
 | 
					  const res = window.document.body.dataset.slug! as FullSlug
 | 
				
			||||||
export function isServerSlug(s: string): s is ServerSlug {
 | 
					 | 
				
			||||||
  const validStart = !(s.startsWith(".") || s.startsWith("/"))
 | 
					 | 
				
			||||||
  const validEnding = !s.endsWith("/")
 | 
					 | 
				
			||||||
  return validStart && validEnding && !_containsForbiddenCharacters(s)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** The real file path to a file on disk */
 | 
					 | 
				
			||||||
export type FilePath = SlugLike<"filepath">
 | 
					 | 
				
			||||||
export function isFilePath(s: string): s is FilePath {
 | 
					 | 
				
			||||||
  const validStart = !s.startsWith(".")
 | 
					 | 
				
			||||||
  return validStart && _hasFileExtension(s)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getClientSlug(window: Window): ClientSlug {
 | 
					 | 
				
			||||||
  const res = window.location.href as ClientSlug
 | 
					 | 
				
			||||||
  return res
 | 
					  return res
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getCanonicalSlug(window: Window): CanonicalSlug {
 | 
					export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
 | 
				
			||||||
  const res = window.document.body.dataset.slug! as CanonicalSlug
 | 
					 | 
				
			||||||
  return res
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
 | 
					 | 
				
			||||||
  const { pathname } = new URL(slug)
 | 
					 | 
				
			||||||
  let fp = pathname.slice(1)
 | 
					 | 
				
			||||||
  fp = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
 | 
					 | 
				
			||||||
  const res = _canonicalize(fp) as CanonicalSlug
 | 
					 | 
				
			||||||
  return res
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
 | 
					 | 
				
			||||||
  let fp = slug as string
 | 
					 | 
				
			||||||
  const res = _canonicalize(fp) as CanonicalSlug
 | 
					 | 
				
			||||||
  return res
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug {
 | 
					 | 
				
			||||||
  fp = _stripSlashes(fp) as FilePath
 | 
					  fp = _stripSlashes(fp) as FilePath
 | 
				
			||||||
  let ext = _getFileExtension(fp)
 | 
					  let ext = _getFileExtension(fp)
 | 
				
			||||||
  const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
 | 
					  const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
 | 
				
			||||||
| 
						 | 
					@ -133,44 +61,47 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug
 | 
				
			||||||
    slug = slug.replace(/_index$/, "index")
 | 
					    slug = slug.replace(/_index$/, "index")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (slug + ext) as ServerSlug
 | 
					  return (slug + ext) as FullSlug
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function simplifySlug(fp: FullSlug): SimpleSlug {
 | 
				
			||||||
 | 
					  return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function transformInternalLink(link: string): RelativeURL {
 | 
					export function transformInternalLink(link: string): RelativeURL {
 | 
				
			||||||
  let [fplike, anchor] = splitAnchor(decodeURI(link))
 | 
					  let [fplike, anchor] = splitAnchor(decodeURI(link))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const folderPath =
 | 
					  const folderPath = _isFolderPath(fplike)
 | 
				
			||||||
    fplike.endsWith("index") ||
 | 
					 | 
				
			||||||
    fplike.endsWith("index.md") ||
 | 
					 | 
				
			||||||
    fplike.endsWith("index.html") ||
 | 
					 | 
				
			||||||
    fplike.endsWith("/")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let segments = fplike.split("/").filter((x) => x.length > 0)
 | 
					  let segments = fplike.split("/").filter((x) => x.length > 0)
 | 
				
			||||||
  let prefix = segments.filter(_isRelativeSegment).join("/")
 | 
					  let prefix = segments.filter(_isRelativeSegment).join("/")
 | 
				
			||||||
  let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/")
 | 
					  let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // manually add ext here as we want to not strip 'index' if it has an extension
 | 
					  // manually add ext here as we want to not strip 'index' if it has an extension
 | 
				
			||||||
  fp = canonicalizeServer(slugifyFilePath(fp as FilePath) as ServerSlug)
 | 
					  const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
 | 
				
			||||||
  const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
 | 
					  const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug))
 | 
				
			||||||
  const trail = folderPath ? "/" : ""
 | 
					  const trail = folderPath ? "/" : ""
 | 
				
			||||||
  const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
 | 
					  const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
 | 
				
			||||||
  return res
 | 
					  return res
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolve /a/b/c to ../../..
 | 
					// resolve /a/b/c to ../..
 | 
				
			||||||
export function pathToRoot(slug: CanonicalSlug): RelativeURL {
 | 
					export function pathToRoot(slug: FullSlug): RelativeURL {
 | 
				
			||||||
  let rootPath = slug
 | 
					  let rootPath = slug
 | 
				
			||||||
    .split("/")
 | 
					    .split("/")
 | 
				
			||||||
    .filter((x) => x !== "")
 | 
					    .filter((x) => x !== "")
 | 
				
			||||||
 | 
					    .slice(0, -1)
 | 
				
			||||||
    .map((_) => "..")
 | 
					    .map((_) => "..")
 | 
				
			||||||
    .join("/")
 | 
					    .join("/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = _addRelativeToStart(rootPath) as RelativeURL
 | 
					  if (rootPath.length === 0) {
 | 
				
			||||||
  return res
 | 
					    rootPath = "."
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return rootPath as RelativeURL
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL {
 | 
					export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
 | 
				
			||||||
  const res = joinSegments(pathToRoot(current), target) as RelativeURL
 | 
					  const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL
 | 
				
			||||||
  return res
 | 
					  return res
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -206,20 +137,16 @@ export function getAllSegmentPrefixes(tags: string): string[] {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface TransformOptions {
 | 
					export interface TransformOptions {
 | 
				
			||||||
  strategy: "absolute" | "relative" | "shortest"
 | 
					  strategy: "absolute" | "relative" | "shortest"
 | 
				
			||||||
  allSlugs: ServerSlug[]
 | 
					  allSlugs: FullSlug[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function transformLink(
 | 
					export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
 | 
				
			||||||
  src: CanonicalSlug,
 | 
					  let targetSlug = transformInternalLink(target)
 | 
				
			||||||
  target: string,
 | 
					 | 
				
			||||||
  opts: TransformOptions,
 | 
					 | 
				
			||||||
): RelativeURL {
 | 
					 | 
				
			||||||
  let targetSlug: string = transformInternalLink(target)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (opts.strategy === "relative") {
 | 
					  if (opts.strategy === "relative") {
 | 
				
			||||||
    return _addRelativeToStart(targetSlug) as RelativeURL
 | 
					    return targetSlug as RelativeURL
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    const folderTail = targetSlug.endsWith("/") ? "/" : ""
 | 
					    const folderTail = _isFolderPath(targetSlug) ? "/" : ""
 | 
				
			||||||
    const canonicalSlug = _stripSlashes(targetSlug.slice(".".length))
 | 
					    const canonicalSlug = _stripSlashes(targetSlug.slice(".".length))
 | 
				
			||||||
    let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
 | 
					    let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,7 +160,7 @@ export function transformLink(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // only match, just use it
 | 
					      // only match, just use it
 | 
				
			||||||
      if (matchingFileNames.length === 1) {
 | 
					      if (matchingFileNames.length === 1) {
 | 
				
			||||||
        const targetSlug = canonicalizeServer(matchingFileNames[0])
 | 
					        const targetSlug = matchingFileNames[0]
 | 
				
			||||||
        return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL
 | 
					        return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -243,9 +170,13 @@ export function transformLink(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function _canonicalize(fp: string): string {
 | 
					function _isFolderPath(fplike: string): boolean {
 | 
				
			||||||
  fp = _trimSuffix(fp, "index")
 | 
					  return (
 | 
				
			||||||
  return _stripSlashes(fp)
 | 
					    fplike.endsWith("/") ||
 | 
				
			||||||
 | 
					    _endsWith(fplike, "index") ||
 | 
				
			||||||
 | 
					    _endsWith(fplike, "index.md") ||
 | 
				
			||||||
 | 
					    _endsWith(fplike, "index.html")
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function _endsWith(s: string, suffix: string): boolean {
 | 
					function _endsWith(s: string, suffix: string): boolean {
 | 
				
			||||||
| 
						 | 
					@ -275,12 +206,12 @@ function _isRelativeSegment(s: string): boolean {
 | 
				
			||||||
  return /^\.{0,2}$/.test(s)
 | 
					  return /^\.{0,2}$/.test(s)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function _stripSlashes(s: string): string {
 | 
					export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string {
 | 
				
			||||||
  if (s.startsWith("/")) {
 | 
					  if (s.startsWith("/")) {
 | 
				
			||||||
    s = s.substring(1)
 | 
					    s = s.substring(1)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (s.endsWith("/")) {
 | 
					  if (!onlyStripPrefix && s.endsWith("/")) {
 | 
				
			||||||
    s = s.slice(0, -1)
 | 
					    s = s.slice(0, -1)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,12 +2,12 @@ import sourceMapSupport from "source-map-support"
 | 
				
			||||||
sourceMapSupport.install(options)
 | 
					sourceMapSupport.install(options)
 | 
				
			||||||
import cfg from "../quartz.config"
 | 
					import cfg from "../quartz.config"
 | 
				
			||||||
import { Argv, BuildCtx } from "./util/ctx"
 | 
					import { Argv, BuildCtx } from "./util/ctx"
 | 
				
			||||||
import { FilePath, ServerSlug } from "./util/path"
 | 
					import { FilePath, FullSlug } from "./util/path"
 | 
				
			||||||
import { createFileParser, createProcessor } from "./processors/parse"
 | 
					import { createFileParser, createProcessor } from "./processors/parse"
 | 
				
			||||||
import { options } from "./util/sourcemap"
 | 
					import { options } from "./util/sourcemap"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// only called from worker thread
 | 
					// only called from worker thread
 | 
				
			||||||
export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: ServerSlug[]) {
 | 
					export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) {
 | 
				
			||||||
  const ctx: BuildCtx = {
 | 
					  const ctx: BuildCtx = {
 | 
				
			||||||
    cfg,
 | 
					    cfg,
 | 
				
			||||||
    argv,
 | 
					    argv,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue