base path refactor to better support subpath hosting
This commit is contained in:
		
							parent
							
								
									3201f83b70
								
							
						
					
					
						commit
						c874e7e937
					
				| @ -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. | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| @ -12,13 +12,13 @@ Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.o | ||||
| 
 | ||||
| ```typescript | ||||
| // instead of | ||||
| type ClientSlug = string | ||||
| type FullSlug = string | ||||
| 
 | ||||
| // we do | ||||
| type ClientSlug = string & { __brand: "client" } | ||||
| type FullSlug = string & { __brand: "full" } | ||||
| 
 | ||||
| // 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. | ||||
| @ -29,27 +29,23 @@ The following diagram draws the relationships between all the path sources, nomi | ||||
| 
 | ||||
| ```mermaid | ||||
| graph LR | ||||
|     Browser{{Browser}} --> Window{{Window}} & LinkElement{{Link Element}} | ||||
|     Window --"getCanonicalSlug()"--> Canonical[Canonical Slug] | ||||
|     Window --"getClientSlug()"--> Client[Client Slug] | ||||
|     Browser{{Browser}} --> Window{{Body}} & LinkElement{{Link Element}} | ||||
|     Window --"getFullSlug()"--> FullSlug[Full Slug] | ||||
|     LinkElement --".href"--> Relative[Relative URL] | ||||
|     Client --"canonicalizeClient()"--> Canonical | ||||
|     Canonical --"pathToRoot()"--> Relative | ||||
|     Canonical --"resolveRelative()" --> Relative | ||||
|     FullSlug --"simplifySlug()" --> SimpleSlug[Simple Slug] | ||||
|     SimpleSlug --"pathToRoot()"--> Relative | ||||
|     SimpleSlug --"resolveRelative()" --> Relative | ||||
|     MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links] | ||||
|     Links --"transformLink()"--> Relative | ||||
|     FilePath --"slugifyFilePath()"--> Server[Server Slug] | ||||
|     Server --> HTML["HTML File"] | ||||
|     Server --"canonicalizeServer()"--> Canonical | ||||
|     style Canonical stroke-width:4px | ||||
|     FilePath --"slugifyFilePath()"--> FullSlug[Full Slug] | ||||
|     style FullSlug stroke-width:4px | ||||
| ``` | ||||
| 
 | ||||
| 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. | ||||
| - `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`. | ||||
|  | ||||
							
								
								
									
										2
									
								
								index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								index.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -5,7 +5,7 @@ declare module "*.scss" { | ||||
| 
 | ||||
| // dom custom event
 | ||||
| interface CustomEventMap { | ||||
|   nav: CustomEvent<{ url: CanonicalSlug }> | ||||
|   nav: CustomEvent<{ url: FullSlug }> | ||||
| } | ||||
| 
 | ||||
| declare const fetchData: Promise<ContentIndex> | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   "name": "@jackyzha0/quartz", | ||||
|   "description": "🌱 publish your digital garden and notes as a website", | ||||
|   "private": true, | ||||
|   "version": "4.0.7", | ||||
|   "version": "4.0.8", | ||||
|   "type": "module", | ||||
|   "author": "jackyzha0 <j.zhao2k19@gmail.com>", | ||||
|   "license": "MIT", | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| import style from "./styles/backlinks.scss" | ||||
| import { canonicalizeServer, resolveRelative } from "../util/path" | ||||
| import { resolveRelative, simplifySlug } from "../util/path" | ||||
| 
 | ||||
| function Backlinks({ fileData, allFiles }: QuartzComponentProps) { | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const slug = simplifySlug(fileData.slug!) | ||||
|   const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) | ||||
|   return ( | ||||
|     <div class="backlinks"> | ||||
| @ -12,7 +12,7 @@ function Backlinks({ fileData, allFiles }: QuartzComponentProps) { | ||||
|         {backlinkFiles.length > 0 ? ( | ||||
|           backlinkFiles.map((f) => ( | ||||
|             <li> | ||||
|               <a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal"> | ||||
|               <a href={resolveRelative(fileData.slug!, f.slug!)} class="internal"> | ||||
|                 {f.frontmatter?.title} | ||||
|               </a> | ||||
|             </li> | ||||
|  | ||||
| @ -1,14 +1,13 @@ | ||||
| import { canonicalizeServer, pathToRoot } from "../util/path" | ||||
| import { pathToRoot } from "../util/path" | ||||
| import { JSResourceToScriptElement } from "../util/resources" | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| 
 | ||||
| export default (() => { | ||||
|   function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { | ||||
|     const slug = canonicalizeServer(fileData.slug!) | ||||
|     const title = fileData.frontmatter?.title ?? "Untitled" | ||||
|     const description = fileData.description?.trim() ?? "No description provided" | ||||
|     const { css, js } = externalResources | ||||
|     const baseDir = pathToRoot(slug) | ||||
|     const baseDir = pathToRoot(fileData.slug!) | ||||
|     const iconPath = baseDir + "/static/icon.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 { Date } from "./Date" | ||||
| import { QuartzComponentProps } from "./types" | ||||
| @ -25,7 +25,6 @@ type Props = { | ||||
| } & QuartzComponentProps | ||||
| 
 | ||||
| export function PageList({ fileData, allFiles, limit }: Props) { | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   let list = allFiles.sort(byDateAndAlphabetical) | ||||
|   if (limit) { | ||||
|     list = list.slice(0, limit) | ||||
| @ -35,7 +34,6 @@ export function PageList({ fileData, allFiles, limit }: Props) { | ||||
|     <ul class="section-ul"> | ||||
|       {list.map((page) => { | ||||
|         const title = page.frontmatter?.title | ||||
|         const pageSlug = canonicalizeServer(page.slug!) | ||||
|         const tags = page.frontmatter?.tags ?? [] | ||||
| 
 | ||||
|         return ( | ||||
| @ -48,7 +46,7 @@ export function PageList({ fileData, allFiles, limit }: Props) { | ||||
|               )} | ||||
|               <div class="desc"> | ||||
|                 <h3> | ||||
|                   <a href={resolveRelative(slug, pageSlug)} class="internal"> | ||||
|                   <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> | ||||
|                     {title} | ||||
|                   </a> | ||||
|                 </h3> | ||||
| @ -58,7 +56,7 @@ export function PageList({ fileData, allFiles, limit }: Props) { | ||||
|                   <li> | ||||
|                     <a | ||||
|                       class="internal tag-link" | ||||
|                       href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)} | ||||
|                       href={resolveRelative(fileData.slug!, `tags/${tag}/index` as FullSlug)} | ||||
|                     > | ||||
|                       #{tag} | ||||
|                     </a> | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| import { canonicalizeServer, pathToRoot } from "../util/path" | ||||
| import { pathToRoot } from "../util/path" | ||||
| import { QuartzComponentConstructor, QuartzComponentProps } from "./types" | ||||
| 
 | ||||
| function PageTitle({ fileData, cfg }: QuartzComponentProps) { | ||||
|   const title = cfg?.pageTitle ?? "Untitled Quartz" | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const baseDir = pathToRoot(slug) | ||||
|   const baseDir = pathToRoot(fileData.slug!) | ||||
|   return ( | ||||
|     <h1 class="page-title"> | ||||
|       <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" | ||||
| 
 | ||||
| function TagList({ fileData }: QuartzComponentProps) { | ||||
|   const tags = fileData.frontmatter?.tags | ||||
|   const slug = canonicalizeServer(fileData.slug!) | ||||
|   const baseDir = pathToRoot(slug) | ||||
|   const baseDir = pathToRoot(fileData.slug!) | ||||
|   if (tags && tags.length > 0) { | ||||
|     return ( | ||||
|       <ul class="tags"> | ||||
|  | ||||
| @ -5,13 +5,13 @@ import path from "path" | ||||
| 
 | ||||
| import style from "../styles/listPage.scss" | ||||
| import { PageList } from "../PageList" | ||||
| import { canonicalizeServer } from "../../util/path" | ||||
| import { simplifySlug } from "../../util/path" | ||||
| 
 | ||||
| function FolderContent(props: QuartzComponentProps) { | ||||
|   const { tree, fileData, allFiles } = props | ||||
|   const folderSlug = canonicalizeServer(fileData.slug!) | ||||
|   const folderSlug = simplifySlug(fileData.slug!) | ||||
|   const allPagesInFolder = allFiles.filter((file) => { | ||||
|     const fileSlug = canonicalizeServer(file.slug!) | ||||
|     const fileSlug = simplifySlug(file.slug!) | ||||
|     const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug | ||||
|     const folderParts = folderSlug.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 style from "../styles/listPage.scss" | ||||
| import { PageList } from "../PageList" | ||||
| import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../util/path" | ||||
| import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" | ||||
| import { QuartzPluginData } from "../../plugins/vfile" | ||||
| 
 | ||||
| const numPages = 10 | ||||
| @ -15,7 +15,7 @@ function TagContent(props: QuartzComponentProps) { | ||||
|     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) => | ||||
|     allFiles.filter((file) => | ||||
|       (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentProps } from "./types" | ||||
| import HeaderConstructor from "./Header" | ||||
| import BodyConstructor from "./Body" | ||||
| import { JSResourceToScriptElement, StaticResources } from "../util/resources" | ||||
| import { CanonicalSlug, pathToRoot } from "../util/path" | ||||
| import { FullSlug, joinSegments, pathToRoot } from "../util/path" | ||||
| 
 | ||||
| interface RenderComponents { | ||||
|   head: QuartzComponent | ||||
| @ -15,19 +15,20 @@ interface RenderComponents { | ||||
|   footer: QuartzComponent | ||||
| } | ||||
| 
 | ||||
| export function pageResources( | ||||
|   slug: CanonicalSlug, | ||||
|   staticResources: StaticResources, | ||||
| ): StaticResources { | ||||
| export function pageResources(slug: FullSlug, staticResources: StaticResources): StaticResources { | ||||
|   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())` | ||||
| 
 | ||||
|   return { | ||||
|     css: [baseDir + "/index.css", ...staticResources.css], | ||||
|     css: [joinSegments(baseDir, "index.css"), ...staticResources.css], | ||||
|     js: [ | ||||
|       { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, | ||||
|       { | ||||
|         src: joinSegments(baseDir, "/prescript.js"), | ||||
|         loadTime: "beforeDOMReady", | ||||
|         contentType: "external", | ||||
|       }, | ||||
|       { | ||||
|         loadTime: "beforeDOMReady", | ||||
|         contentType: "inline", | ||||
| @ -46,7 +47,7 @@ export function pageResources( | ||||
| } | ||||
| 
 | ||||
| export function renderPage( | ||||
|   slug: CanonicalSlug, | ||||
|   slug: FullSlug, | ||||
|   componentData: QuartzComponentProps, | ||||
|   components: RenderComponents, | ||||
|   pageResources: StaticResources, | ||||
|  | ||||
| @ -1,31 +1,32 @@ | ||||
| import type { ContentDetails } from "../../plugins/emitters/contentIndex" | ||||
| import * as d3 from "d3" | ||||
| import { registerEscapeHandler, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../util/path" | ||||
| import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" | ||||
| 
 | ||||
| type NodeData = { | ||||
|   id: CanonicalSlug | ||||
|   id: SimpleSlug | ||||
|   text: string | ||||
|   tags: string[] | ||||
| } & d3.SimulationNodeDatum | ||||
| 
 | ||||
| type LinkData = { | ||||
|   source: CanonicalSlug | ||||
|   target: CanonicalSlug | ||||
|   source: SimpleSlug | ||||
|   target: SimpleSlug | ||||
| } | ||||
| 
 | ||||
| const localStorageKey = "graph-visited" | ||||
| function getVisited(): Set<CanonicalSlug> { | ||||
| function getVisited(): Set<SimpleSlug> { | ||||
|   return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) | ||||
| } | ||||
| 
 | ||||
| function addToVisited(slug: CanonicalSlug) { | ||||
| function addToVisited(slug: SimpleSlug) { | ||||
|   const visited = getVisited() | ||||
|   visited.add(slug) | ||||
|   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 graph = document.getElementById(container) | ||||
|   if (!graph) return | ||||
| @ -47,16 +48,17 @@ async function renderGraph(container: string, slug: CanonicalSlug) { | ||||
| 
 | ||||
|   const links: LinkData[] = [] | ||||
|   for (const [src, details] of Object.entries<ContentDetails>(data)) { | ||||
|     const source = simplifySlug(src as FullSlug) | ||||
|     const outgoing = details.links ?? [] | ||||
|     for (const dest of outgoing) { | ||||
|       if (src in data && dest in data) { | ||||
|         links.push({ source: src as CanonicalSlug, target: dest }) | ||||
|       if (dest in data) { | ||||
|         links.push({ source, target: dest }) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const neighbourhood = new Set<CanonicalSlug>() | ||||
|   const wl: (CanonicalSlug | "__SENTINEL")[] = [slug, "__SENTINEL"] | ||||
|   const neighbourhood = new Set<SimpleSlug>() | ||||
|   const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"] | ||||
|   if (depth >= 0) { | ||||
|     while (depth >= 0 && wl.length > 0) { | ||||
|       // compute neighbours
 | ||||
| @ -72,7 +74,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) { | ||||
|       } | ||||
|     } | ||||
|   } 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[] } = { | ||||
| @ -171,11 +173,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) { | ||||
|     .attr("fill", color) | ||||
|     .style("cursor", "pointer") | ||||
|     .on("click", (_, d) => { | ||||
|       const targ = resolveRelative(slug, d.id) | ||||
|       window.spaNavigate(new URL(targ, getClientSlug(window))) | ||||
|       const targ = resolveRelative(fullSlug, d.id) | ||||
|       window.spaNavigate(new URL(targ, window.location.toString())) | ||||
|     }) | ||||
|     .on("mouseover", function (_, d) { | ||||
|       const neighbours: CanonicalSlug[] = data[slug].links ?? [] | ||||
|       const neighbours: SimpleSlug[] = data[slug].links ?? [] | ||||
|       const neighbourNodes = d3 | ||||
|         .selectAll<HTMLElement, NodeData>(".node") | ||||
|         .filter((d) => neighbours.includes(d.id)) | ||||
| @ -271,7 +273,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) { | ||||
| } | ||||
| 
 | ||||
| function renderGlobalGraph() { | ||||
|   const slug = getCanonicalSlug(window) | ||||
|   const slug = getFullSlug(window) | ||||
|   const container = document.getElementById("global-graph-outer") | ||||
|   const sidebar = container?.closest(".sidebar") as HTMLElement | ||||
|   container?.classList.add("active") | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import { Document } from "flexsearch" | ||||
| import { ContentDetails } from "../../plugins/emitters/contentIndex" | ||||
| import { registerEscapeHandler, removeAllChildren } from "./util" | ||||
| import { CanonicalSlug, getClientSlug, resolveRelative } from "../../util/path" | ||||
| import { FullSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" | ||||
| 
 | ||||
| interface Item { | ||||
|   id: number | ||||
|   slug: CanonicalSlug | ||||
|   slug: FullSlug | ||||
|   title: string | ||||
|   content: string | ||||
| } | ||||
| @ -73,7 +73,7 @@ document.addEventListener("nav", async (e: unknown) => { | ||||
|   const searchIcon = document.getElementById("search-icon") | ||||
|   const searchBar = document.getElementById("search-bar") as HTMLInputElement | null | ||||
|   const results = document.getElementById("results-container") | ||||
|   const idDataMap = Object.keys(data) as CanonicalSlug[] | ||||
|   const idDataMap = Object.keys(data) as FullSlug[] | ||||
| 
 | ||||
|   function hideSearch() { | ||||
|     container?.classList.remove("active") | ||||
| @ -126,7 +126,7 @@ document.addEventListener("nav", async (e: unknown) => { | ||||
|     button.innerHTML = `<h3>${title}</h3><p>${content}</p>` | ||||
|     button.addEventListener("click", () => { | ||||
|       const targ = resolveRelative(currentSlug, slug) | ||||
|       window.spaNavigate(new URL(targ, getClientSlug(window))) | ||||
|       window.spaNavigate(new URL(targ, window.location.toString())) | ||||
|     }) | ||||
|     return button | ||||
|   } | ||||
| @ -192,7 +192,7 @@ document.addEventListener("nav", async (e: unknown) => { | ||||
|     for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { | ||||
|       await index.addAsync(id, { | ||||
|         id, | ||||
|         slug: slug as CanonicalSlug, | ||||
|         slug: slug as FullSlug, | ||||
|         title: fileData.title, | ||||
|         content: fileData.content, | ||||
|       }) | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import micromorph from "micromorph" | ||||
| import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../util/path" | ||||
| import { FullSlug, RelativeURL, getFullSlug } from "../../util/path" | ||||
| 
 | ||||
| // adapted from `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 } | ||||
| } | ||||
| 
 | ||||
| function notifyNav(url: CanonicalSlug) { | ||||
| function notifyNav(url: FullSlug) { | ||||
|   const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } }) | ||||
|   document.dispatchEvent(event) | ||||
| } | ||||
| @ -81,7 +81,7 @@ async function navigate(url: URL, isBack: boolean = false) { | ||||
|   const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") | ||||
|   elementsToAdd.forEach((el) => document.head.appendChild(el)) | ||||
| 
 | ||||
|   notifyNav(getCanonicalSlug(window)) | ||||
|   notifyNav(getFullSlug(window)) | ||||
|   delete announcer.dataset.persist | ||||
| } | ||||
| 
 | ||||
| @ -129,7 +129,7 @@ function createRouter() { | ||||
| } | ||||
| 
 | ||||
| createRouter() | ||||
| notifyNav(getCanonicalSlug(window)) | ||||
| notifyNav(getFullSlug(window)) | ||||
| 
 | ||||
| if (!customElements.get("route-announcer")) { | ||||
|   const attrs = { | ||||
|  | ||||
| @ -1,10 +1,4 @@ | ||||
| import { | ||||
|   CanonicalSlug, | ||||
|   FilePath, | ||||
|   ServerSlug, | ||||
|   canonicalizeServer, | ||||
|   resolveRelative, | ||||
| } from "../../util/path" | ||||
| import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path" | ||||
| import { QuartzEmitterPlugin } from "../types" | ||||
| import path from "path" | ||||
| 
 | ||||
| @ -17,10 +11,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ | ||||
|     const fps: FilePath[] = [] | ||||
| 
 | ||||
|     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) | ||||
| 
 | ||||
|       let aliases: CanonicalSlug[] = [] | ||||
|       let aliases: FullSlug[] = [] | ||||
|       if (file.data.frontmatter?.aliases) { | ||||
|         aliases = file.data.frontmatter?.aliases | ||||
|       } else if (file.data.frontmatter?.alias) { | ||||
| @ -28,9 +22,8 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ | ||||
|       } | ||||
| 
 | ||||
|       for (const alias of aliases) { | ||||
|         const slug = path.posix.join(dir, alias) as ServerSlug | ||||
| 
 | ||||
|         const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug) | ||||
|         const slug = path.posix.join(dir, alias) as FullSlug | ||||
|         const redirUrl = resolveRelative(slug, file.data.slug!) | ||||
|         const fp = await emit({ | ||||
|           content: ` | ||||
|             <!DOCTYPE html> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { FilePath, ServerSlug } from "../../util/path" | ||||
| import { FilePath, FullSlug } from "../../util/path" | ||||
| import { QuartzEmitterPlugin } from "../types" | ||||
| 
 | ||||
| // @ts-ignore
 | ||||
| @ -154,7 +154,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial< | ||||
|       const postscript = joinScripts(componentResources.afterDOMLoaded) | ||||
|       const fps = await Promise.all([ | ||||
|         emit({ | ||||
|           slug: "index" as ServerSlug, | ||||
|           slug: "index" as FullSlug, | ||||
|           ext: ".css", | ||||
|           content: transform({ | ||||
|             filename: "index.css", | ||||
| @ -171,12 +171,12 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial< | ||||
|           }).code.toString(), | ||||
|         }), | ||||
|         emit({ | ||||
|           slug: "prescript" as ServerSlug, | ||||
|           slug: "prescript" as FullSlug, | ||||
|           ext: ".js", | ||||
|           content: prescript, | ||||
|         }), | ||||
|         emit({ | ||||
|           slug: "postscript" as ServerSlug, | ||||
|           slug: "postscript" as FullSlug, | ||||
|           ext: ".js", | ||||
|           content: postscript, | ||||
|         }), | ||||
|  | ||||
| @ -1,18 +1,12 @@ | ||||
| import { GlobalConfiguration } from "../../cfg" | ||||
| import { | ||||
|   CanonicalSlug, | ||||
|   ClientSlug, | ||||
|   FilePath, | ||||
|   ServerSlug, | ||||
|   canonicalizeServer, | ||||
| } from "../../util/path" | ||||
| import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path" | ||||
| import { QuartzEmitterPlugin } from "../types" | ||||
| import path from "path" | ||||
| 
 | ||||
| export type ContentIndex = Map<CanonicalSlug, ContentDetails> | ||||
| export type ContentIndex = Map<FullSlug, ContentDetails> | ||||
| export type ContentDetails = { | ||||
|   title: string | ||||
|   links: CanonicalSlug[] | ||||
|   links: SimpleSlug[] | ||||
|   tags: string[] | ||||
|   content: string | ||||
|   date?: Date | ||||
| @ -33,21 +27,21 @@ const defaultOptions: Options = { | ||||
| 
 | ||||
| function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { | ||||
|   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>
 | ||||
|     <lastmod>${content.date?.toISOString()}</lastmod> | ||||
|   </url>` | ||||
|   const urls = Array.from(idx) | ||||
|     .map(([slug, content]) => createURLEntry(slug, content)) | ||||
|     .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) | ||||
|     .join("") | ||||
|   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 { | ||||
|   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> | ||||
|     <link>${root}/${slug}</link> | ||||
|     <guid>${root}/${slug}</guid> | ||||
| @ -56,7 +50,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { | ||||
|   </items>` | ||||
| 
 | ||||
|   const items = Array.from(idx) | ||||
|     .map(([slug, content]) => createURLEntry(slug, content)) | ||||
|     .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) | ||||
|     .join("") | ||||
|   return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
 | ||||
|     <channel> | ||||
| @ -79,7 +73,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|       const emitted: FilePath[] = [] | ||||
|       const linkIndex: ContentIndex = new Map() | ||||
|       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() | ||||
|         if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { | ||||
|           linkIndex.set(slug, { | ||||
| @ -97,7 +91,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|         emitted.push( | ||||
|           await emit({ | ||||
|             content: generateSiteMap(cfg, linkIndex), | ||||
|             slug: "sitemap" as ServerSlug, | ||||
|             slug: "sitemap" as FullSlug, | ||||
|             ext: ".xml", | ||||
|           }), | ||||
|         ) | ||||
| @ -107,13 +101,13 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { | ||||
|         emitted.push( | ||||
|           await emit({ | ||||
|             content: generateRSSFeed(cfg, linkIndex), | ||||
|             slug: "index" as ServerSlug, | ||||
|             slug: "index" as FullSlug, | ||||
|             ext: ".xml", | ||||
|           }), | ||||
|         ) | ||||
|       } | ||||
| 
 | ||||
|       const fp = path.join("static", "contentIndex") as ServerSlug | ||||
|       const fp = path.join("static", "contentIndex") as FullSlug | ||||
|       const simplifiedIndex = Object.fromEntries( | ||||
|         Array.from(linkIndex).map(([slug, content]) => { | ||||
|           // remove description and from content index as nothing downstream
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header" | ||||
| import BodyConstructor from "../../components/Body" | ||||
| import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import { FilePath, canonicalizeServer } from "../../util/path" | ||||
| import { FilePath } from "../../util/path" | ||||
| import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" | ||||
| import { Content } from "../../components" | ||||
| 
 | ||||
| @ -30,7 +30,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp | ||||
|       const fps: FilePath[] = [] | ||||
|       const allFiles = content.map((c) => c[1].data) | ||||
|       for (const [tree, file] of content) { | ||||
|         const slug = canonicalizeServer(file.data.slug!) | ||||
|         const slug = file.data.slug! | ||||
|         const externalResources = pageResources(slug, resources) | ||||
|         const componentData: QuartzComponentProps = { | ||||
|           fileData: file.data, | ||||
| @ -44,7 +44,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp | ||||
|         const content = renderPage(slug, componentData, opts, externalResources) | ||||
|         const fp = await emit({ | ||||
|           content, | ||||
|           slug: file.data.slug!, | ||||
|           slug, | ||||
|           ext: ".html", | ||||
|         }) | ||||
| 
 | ||||
|  | ||||
| @ -6,13 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { ProcessedContent, defaultProcessedContent } from "../vfile" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import path from "path" | ||||
| import { | ||||
|   CanonicalSlug, | ||||
|   FilePath, | ||||
|   ServerSlug, | ||||
|   canonicalizeServer, | ||||
|   joinSegments, | ||||
| } from "../../util/path" | ||||
| import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" | ||||
| import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" | ||||
| import { FolderContent } from "../../components" | ||||
| 
 | ||||
| @ -38,10 +32,10 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => { | ||||
|       const allFiles = content.map((c) => c[1].data) | ||||
|       const cfg = ctx.cfg.configuration | ||||
| 
 | ||||
|       const folders: Set<CanonicalSlug> = new Set( | ||||
|       const folders: Set<SimpleSlug> = new Set( | ||||
|         allFiles.flatMap((data) => { | ||||
|           const slug = data.slug | ||||
|           const folderName = path.dirname(slug ?? "") as CanonicalSlug | ||||
|           const folderName = path.dirname(slug ?? "") as SimpleSlug | ||||
|           if (slug && folderName !== "." && folderName !== "tags") { | ||||
|             return [folderName] | ||||
|           } | ||||
| @ -53,21 +47,21 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => { | ||||
|         [...folders].map((folder) => [ | ||||
|           folder, | ||||
|           defaultProcessedContent({ | ||||
|             slug: joinSegments(folder, "index") as ServerSlug, | ||||
|             slug: joinSegments(folder, "index") as FullSlug, | ||||
|             frontmatter: { title: `Folder: ${folder}`, tags: [] }, | ||||
|           }), | ||||
|         ]), | ||||
|       ) | ||||
| 
 | ||||
|       for (const [tree, file] of content) { | ||||
|         const slug = canonicalizeServer(file.data.slug!) | ||||
|         const slug = simplifySlug(file.data.slug!) | ||||
|         if (folders.has(slug)) { | ||||
|           folderDescriptions[slug] = [tree, file] | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       for (const folder of folders) { | ||||
|         const slug = folder | ||||
|         const slug = joinSegments(folder, "index") as FullSlug | ||||
|         const externalResources = pageResources(slug, resources) | ||||
|         const [tree, file] = folderDescriptions[folder] | ||||
|         const componentData: QuartzComponentProps = { | ||||
| @ -82,7 +76,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => { | ||||
|         const content = renderPage(slug, componentData, opts, externalResources) | ||||
|         const fp = await emit({ | ||||
|           content, | ||||
|           slug: file.data.slug!, | ||||
|           slug, | ||||
|           ext: ".html", | ||||
|         }) | ||||
| 
 | ||||
|  | ||||
| @ -5,13 +5,7 @@ import BodyConstructor from "../../components/Body" | ||||
| import { pageResources, renderPage } from "../../components/renderPage" | ||||
| import { ProcessedContent, defaultProcessedContent } from "../vfile" | ||||
| import { FullPageLayout } from "../../cfg" | ||||
| import { | ||||
|   CanonicalSlug, | ||||
|   FilePath, | ||||
|   ServerSlug, | ||||
|   getAllSegmentPrefixes, | ||||
|   joinSegments, | ||||
| } from "../../util/path" | ||||
| import { FilePath, FullSlug, getAllSegmentPrefixes, joinSegments } from "../../util/path" | ||||
| import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" | ||||
| import { TagContent } from "../../components" | ||||
| 
 | ||||
| @ -49,7 +43,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => { | ||||
|           return [ | ||||
|             tag, | ||||
|             defaultProcessedContent({ | ||||
|               slug: joinSegments("tags", tag) as ServerSlug, | ||||
|               slug: joinSegments("tags", tag) as FullSlug, | ||||
|               frontmatter: { title, tags: [] }, | ||||
|             }), | ||||
|           ] | ||||
| @ -67,7 +61,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => { | ||||
|       } | ||||
| 
 | ||||
|       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 [tree, file] = tagDescriptions[tag] | ||||
|         const componentData: QuartzComponentProps = { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { StaticResources } from "../util/resources" | ||||
| import { FilePath, ServerSlug } from "../util/path" | ||||
| import { FilePath, FullSlug } from "../util/path" | ||||
| import { BuildCtx } from "../util/ctx" | ||||
| 
 | ||||
| export function getStaticResourcesFromPlugins(ctx: BuildCtx) { | ||||
| @ -28,7 +28,7 @@ export * from "./emitters" | ||||
| declare module "vfile" { | ||||
|   // inserted in processors.ts
 | ||||
|   interface DataMap { | ||||
|     slug: ServerSlug | ||||
|     slug: FullSlug | ||||
|     filePath: FilePath | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| import { QuartzTransformerPlugin } from "../types" | ||||
| import { | ||||
|   CanonicalSlug, | ||||
|   FullSlug, | ||||
|   RelativeURL, | ||||
|   SimpleSlug, | ||||
|   TransformOptions, | ||||
|   _stripSlashes, | ||||
|   canonicalizeServer, | ||||
|   joinSegments, | ||||
|   simplifySlug, | ||||
|   splitAnchor, | ||||
|   transformLink, | ||||
| } from "../../util/path" | ||||
| @ -33,8 +34,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
|       return [ | ||||
|         () => { | ||||
|           return (tree, file) => { | ||||
|             const curSlug = canonicalizeServer(file.data.slug!) | ||||
|             const outgoing: Set<CanonicalSlug> = new Set() | ||||
|             const curSlug = simplifySlug(file.data.slug!) | ||||
|             const outgoing: Set<SimpleSlug> = new Set() | ||||
| 
 | ||||
|             const transformOptions: TransformOptions = { | ||||
|               strategy: opts.markdownLinkResolution, | ||||
| @ -54,10 +55,15 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
| 
 | ||||
|                 // don't process external links or intra-document anchors
 | ||||
|                 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 [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
 | ||||
| @ -79,7 +85,11 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
|               ) { | ||||
|                 if (!isAbsoluteUrl(node.properties.src)) { | ||||
|                   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 | ||||
|                 } | ||||
|               } | ||||
| @ -95,6 +105,6 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = | ||||
| 
 | ||||
| declare module "vfile" { | ||||
|   interface DataMap { | ||||
|     links: CanonicalSlug[] | ||||
|     links: SimpleSlug[] | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,7 @@ import path from "path" | ||||
| import { JSResource } from "../../util/resources" | ||||
| // @ts-ignore
 | ||||
| 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 { toHtml } from "hast-util-to-html" | ||||
| import { PhrasingContent } from "mdast-util-find-and-replace/lib" | ||||
| @ -381,8 +381,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | ||||
|       if (opts.parseTags) { | ||||
|         plugins.push(() => { | ||||
|           return (tree: Root, file) => { | ||||
|             const slug = canonicalizeServer(file.data.slug!) | ||||
|             const base = pathToRoot(slug) | ||||
|             const base = pathToRoot(file.data.slug!) | ||||
|             findAndReplace(tree, tagRegex, (value: string, tag: string) => { | ||||
|               if (file.data.frontmatter) { | ||||
|                 file.data.frontmatter.tags.push(tag) | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { PluggableList } from "unified" | ||||
| import { StaticResources } from "../util/resources" | ||||
| import { ProcessedContent } from "./vfile" | ||||
| import { QuartzComponent } from "../components/types" | ||||
| import { FilePath, ServerSlug } from "../util/path" | ||||
| import { FilePath, FullSlug } from "../util/path" | ||||
| import { BuildCtx } from "../util/ctx" | ||||
| 
 | ||||
| export interface PluginTypes { | ||||
| @ -46,7 +46,7 @@ export type QuartzEmitterPluginInstance = { | ||||
| } | ||||
| 
 | ||||
| export interface EmitOptions { | ||||
|   slug: ServerSlug | ||||
|   slug: FullSlug | ||||
|   ext: `.${string}` | "" | ||||
|   content: string | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { QuartzConfig } from "../cfg" | ||||
| import { ServerSlug } from "./path" | ||||
| import { FullSlug } from "./path" | ||||
| 
 | ||||
| export interface Argv { | ||||
|   directory: string | ||||
| @ -13,5 +13,5 @@ export interface Argv { | ||||
| export interface BuildCtx { | ||||
|   argv: Argv | ||||
|   cfg: QuartzConfig | ||||
|   allSlugs: ServerSlug[] | ||||
|   allSlugs: FullSlug[] | ||||
| } | ||||
|  | ||||
| @ -1,45 +1,25 @@ | ||||
| import test, { describe } from "node:test" | ||||
| import * as path from "./path" | ||||
| import assert from "node:assert" | ||||
| import { CanonicalSlug, ServerSlug, TransformOptions } from "./path" | ||||
| import { FullSlug, TransformOptions } from "./path" | ||||
| 
 | ||||
| describe("typeguards", () => { | ||||
|   test("isClientSlug", () => { | ||||
|     assert(path.isClientSlug("http://example.com")) | ||||
|     assert(path.isClientSlug("http://example.com/index")) | ||||
|     assert(path.isClientSlug("http://example.com/index.html")) | ||||
|     assert(path.isClientSlug("http://example.com/")) | ||||
|     assert(path.isClientSlug("https://example.com")) | ||||
|     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")) | ||||
|   test("isSimpleSlug", () => { | ||||
|     assert(path.isSimpleSlug("")) | ||||
|     assert(path.isSimpleSlug("abc")) | ||||
|     assert(path.isSimpleSlug("abc/")) | ||||
|     assert(path.isSimpleSlug("notindex")) | ||||
|     assert(path.isSimpleSlug("notindex/def")) | ||||
| 
 | ||||
|     assert(!path.isClientSlug("./")) | ||||
|     assert(!path.isClientSlug("")) | ||||
|     assert(!path.isClientSlug("ipfs://example.com")) | ||||
|     assert(!path.isClientSlug("http")) | ||||
|     assert(!path.isClientSlug("https")) | ||||
|   }) | ||||
| 
 | ||||
|   test("isCanonicalSlug", () => { | ||||
|     assert(path.isCanonicalSlug("")) | ||||
|     assert(path.isCanonicalSlug("abc")) | ||||
|     assert(path.isCanonicalSlug("notindex")) | ||||
|     assert(path.isCanonicalSlug("notindex/def")) | ||||
| 
 | ||||
|     assert(!path.isCanonicalSlug("//")) | ||||
|     assert(!path.isCanonicalSlug("index")) | ||||
|     assert(!path.isCanonicalSlug("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")) | ||||
|     assert(!path.isSimpleSlug("//")) | ||||
|     assert(!path.isSimpleSlug("index")) | ||||
|     assert(!path.isSimpleSlug("https://example.com")) | ||||
|     assert(!path.isSimpleSlug("/abc")) | ||||
|     assert(!path.isSimpleSlug("abc/index")) | ||||
|     assert(!path.isSimpleSlug("abc#anchor")) | ||||
|     assert(!path.isSimpleSlug("abc?query=1")) | ||||
|     assert(!path.isSimpleSlug("index.md")) | ||||
|     assert(!path.isSimpleSlug("index.html")) | ||||
|   }) | ||||
| 
 | ||||
|   test("isRelativeURL", () => { | ||||
| @ -58,18 +38,18 @@ describe("typeguards", () => { | ||||
|     assert(!path.isRelativeURL("./abc/def.md")) | ||||
|   }) | ||||
| 
 | ||||
|   test("isServerSlug", () => { | ||||
|     assert(path.isServerSlug("index")) | ||||
|     assert(path.isServerSlug("abc/def")) | ||||
|     assert(path.isServerSlug("html.energy")) | ||||
|     assert(path.isServerSlug("test.pdf")) | ||||
|   test("isFullSlug", () => { | ||||
|     assert(path.isFullSlug("index")) | ||||
|     assert(path.isFullSlug("abc/def")) | ||||
|     assert(path.isFullSlug("html.energy")) | ||||
|     assert(path.isFullSlug("test.pdf")) | ||||
| 
 | ||||
|     assert(!path.isServerSlug(".")) | ||||
|     assert(!path.isServerSlug("./abc/def")) | ||||
|     assert(!path.isServerSlug("../abc/def")) | ||||
|     assert(!path.isServerSlug("abc/def#anchor")) | ||||
|     assert(!path.isServerSlug("abc/def?query=1")) | ||||
|     assert(!path.isServerSlug("note with spaces")) | ||||
|     assert(!path.isFullSlug(".")) | ||||
|     assert(!path.isFullSlug("./abc/def")) | ||||
|     assert(!path.isFullSlug("../abc/def")) | ||||
|     assert(!path.isFullSlug("abc/def#anchor")) | ||||
|     assert(!path.isFullSlug("abc/def?query=1")) | ||||
|     assert(!path.isFullSlug("note with spaces")) | ||||
|   }) | ||||
| 
 | ||||
|   test("isFilePath", () => { | ||||
| @ -100,40 +80,17 @@ describe("transforms", () => { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   test("canonicalizeServer", () => { | ||||
|   test("simplifySlug", () => { | ||||
|     asserts( | ||||
|       [ | ||||
|         ["index", ""], | ||||
|         ["abc/index", "abc"], | ||||
|         ["abc", "abc"], | ||||
|         ["abc/index", "abc/"], | ||||
|         ["abc/def", "abc/def"], | ||||
|       ], | ||||
|       path.canonicalizeServer, | ||||
|       path.isServerSlug, | ||||
|       path.isCanonicalSlug, | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   test("canonicalizeClient", () => { | ||||
|     asserts( | ||||
|       [ | ||||
|         ["http://localhost:3000", ""], | ||||
|         ["http://localhost:3000/index", ""], | ||||
|         ["http://localhost:3000/test", "test"], | ||||
|         ["http://example.com", ""], | ||||
|         ["http://example.com/index", ""], | ||||
|         ["http://example.com/index.html", ""], | ||||
|         ["http://example.com/", ""], | ||||
|         ["https://example.com", ""], | ||||
|         ["https://example.com/abc/def", "abc/def"], | ||||
|         ["https://example.com/abc/def/", "abc/def"], | ||||
|         ["https://example.com/abc/def#cool", "abc/def"], | ||||
|         ["https://example.com/abc/def?field=1&another=2", "abc/def"], | ||||
|         ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"], | ||||
|         ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"], | ||||
|       ], | ||||
|       path.canonicalizeClient, | ||||
|       path.isClientSlug, | ||||
|       path.isCanonicalSlug, | ||||
|       path.simplifySlug, | ||||
|       path.isFullSlug, | ||||
|       path.isSimpleSlug, | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
| @ -151,7 +108,7 @@ describe("transforms", () => { | ||||
|       ], | ||||
|       path.slugifyFilePath, | ||||
|       path.isFilePath, | ||||
|       path.isServerSlug, | ||||
|       path.isFullSlug, | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
| @ -186,12 +143,14 @@ describe("transforms", () => { | ||||
|   test("pathToRoot", () => { | ||||
|     asserts( | ||||
|       [ | ||||
|         ["", "."], | ||||
|         ["abc", ".."], | ||||
|         ["abc/def", "../.."], | ||||
|         ["index", "."], | ||||
|         ["abc", "."], | ||||
|         ["abc/def", ".."], | ||||
|         ["abc/def/ghi", "../.."], | ||||
|         ["abc/def/index", "../.."], | ||||
|       ], | ||||
|       path.pathToRoot, | ||||
|       path.isCanonicalSlug, | ||||
|       path.isFullSlug, | ||||
|       path.isRelativeURL, | ||||
|     ) | ||||
|   }) | ||||
| @ -206,7 +165,7 @@ describe("link strategies", () => { | ||||
|     "e/g/h", | ||||
|     "index", | ||||
|     "a/test.png", | ||||
|   ] as ServerSlug[] | ||||
|   ] as FullSlug[] | ||||
| 
 | ||||
|   describe("absolute", () => { | ||||
|     const opts: TransformOptions = { | ||||
| @ -215,28 +174,28 @@ describe("link strategies", () => { | ||||
|     } | ||||
| 
 | ||||
|     test("from a/b/c", () => { | ||||
|       const cur = "a/b/c" as CanonicalSlug | ||||
|       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, "e/f", opts), "../../../e/f") | ||||
|       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.png", opts), "../../../index.png") | ||||
|       assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc") | ||||
|       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/test.png", opts), "../../../a/test.png") | ||||
|       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/index", opts), "../../a/b/") | ||||
|       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, "index", opts), "../../") | ||||
|       assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png") | ||||
|       assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc") | ||||
|       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/test.png", opts), "../../a/test.png") | ||||
|     }) | ||||
| 
 | ||||
|     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", opts), "../../a/b") | ||||
|       assert.strictEqual(path.transformLink(cur, "index", opts), "../../") | ||||
|     }) | ||||
| 
 | ||||
|     test("from index", () => { | ||||
|       const cur = "" as CanonicalSlug | ||||
|       const cur = "index" as FullSlug | ||||
|       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/index", opts), "./a/b/") | ||||
| @ -250,20 +209,20 @@ describe("link strategies", () => { | ||||
|     } | ||||
| 
 | ||||
|     test("from a/b/c", () => { | ||||
|       const cur = "a/b/c" as CanonicalSlug | ||||
|       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, "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#abc", opts), "../../../a/b/#abc") | ||||
|       assert.strictEqual(path.transformLink(cur, "index", opts), "../../../") | ||||
|       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, "index#abc", opts), "../../../#abc") | ||||
|       const cur = "a/b/c" as FullSlug | ||||
|       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, "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#abc", opts), "../../a/b/#abc") | ||||
|       assert.strictEqual(path.transformLink(cur, "index", opts), "../../") | ||||
|       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, "index#abc", opts), "../../#abc") | ||||
|     }) | ||||
| 
 | ||||
|     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, "h", opts), "../../e/g/h") | ||||
|       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/") | ||||
| @ -271,7 +230,7 @@ describe("link strategies", () => { | ||||
|     }) | ||||
| 
 | ||||
|     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, "h", opts), "./e/g/h") | ||||
|       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") | ||||
| @ -286,7 +245,7 @@ describe("link strategies", () => { | ||||
|     } | ||||
| 
 | ||||
|     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, "index", opts), "./") | ||||
|       assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../") | ||||
| @ -303,7 +262,7 @@ describe("link strategies", () => { | ||||
|     }) | ||||
| 
 | ||||
|     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, "../../", opts), "../../") | ||||
|       assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h") | ||||
| @ -311,7 +270,7 @@ describe("link strategies", () => { | ||||
|     }) | ||||
| 
 | ||||
|     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, "a/b/index", opts), "./a/b/") | ||||
|     }) | ||||
|  | ||||
| @ -1,73 +1,35 @@ | ||||
| import { slug } from "github-slugger" | ||||
| // 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" | ||||
| 
 | ||||
| /// Utility type to simulate nominal types in TypeScript
 | ||||
| type SlugLike<T> = string & { __brand: T } | ||||
| 
 | ||||
| /** Client-side slug, usually obtained through `window.location` */ | ||||
| export type ClientSlug = SlugLike<"client"> | ||||
| export function isClientSlug(s: string): s is ClientSlug { | ||||
|   const res = /^https?:\/\/.+/.test(s) | ||||
|   return res | ||||
| /** Cannot be relative and must have a file extension. */ | ||||
| export type FilePath = SlugLike<"filepath"> | ||||
| export function isFilePath(s: string): s is FilePath { | ||||
|   const validStart = !s.startsWith(".") | ||||
|   return validStart && _hasFileExtension(s) | ||||
| } | ||||
| 
 | ||||
| /** Canonical slug, should be used whenever you need to refer to the location of a file/note. | ||||
|  * On the client, this is normally stored in `document.body.dataset.slug` | ||||
|  */ | ||||
| export type CanonicalSlug = SlugLike<"canonical"> | ||||
| export function isCanonicalSlug(s: string): s is CanonicalSlug { | ||||
| /** 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. */ | ||||
| export type FullSlug = SlugLike<"full"> | ||||
| export function isFullSlug(s: string): s is FullSlug { | ||||
|   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) | ||||
| } | ||||
| 
 | ||||
| /** A relative link, can be found on `href`s but can also be constructed for | ||||
|  * client-side navigation (e.g. search and graph) | ||||
|  */ | ||||
| /** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */ | ||||
| export type RelativeURL = SlugLike<"relative"> | ||||
| export function isRelativeURL(s: string): s is RelativeURL { | ||||
|   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) ?? "") | ||||
| } | ||||
| 
 | ||||
| /** A server side slug. This is what Quartz uses to emit files so uses index suffixes */ | ||||
| export type ServerSlug = SlugLike<"server"> | ||||
| export function isServerSlug(s: string): s is ServerSlug { | ||||
|   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 | ||||
| export function getFullSlug(window: Window): FullSlug { | ||||
|   const res = window.document.body.dataset.slug! as FullSlug | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| export function getCanonicalSlug(window: Window): CanonicalSlug { | ||||
|   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 { | ||||
| export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { | ||||
|   fp = _stripSlashes(fp) as FilePath | ||||
|   let ext = _getFileExtension(fp) | ||||
|   const withoutFileExt = fp.replace(new RegExp(ext + "$"), "") | ||||
| @ -133,44 +61,47 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug | ||||
|     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 { | ||||
|   let [fplike, anchor] = splitAnchor(decodeURI(link)) | ||||
| 
 | ||||
|   const folderPath = | ||||
|     fplike.endsWith("index") || | ||||
|     fplike.endsWith("index.md") || | ||||
|     fplike.endsWith("index.html") || | ||||
|     fplike.endsWith("/") | ||||
| 
 | ||||
|   const folderPath = _isFolderPath(fplike) | ||||
|   let segments = fplike.split("/").filter((x) => x.length > 0) | ||||
|   let prefix = segments.filter(_isRelativeSegment).join("/") | ||||
|   let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/") | ||||
|   let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/") | ||||
| 
 | ||||
|   // 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 joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp)) | ||||
|   const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath)) | ||||
|   const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug)) | ||||
|   const trail = folderPath ? "/" : "" | ||||
|   const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| // resolve /a/b/c to ../../..
 | ||||
| export function pathToRoot(slug: CanonicalSlug): RelativeURL { | ||||
| // resolve /a/b/c to ../..
 | ||||
| export function pathToRoot(slug: FullSlug): RelativeURL { | ||||
|   let rootPath = slug | ||||
|     .split("/") | ||||
|     .filter((x) => x !== "") | ||||
|     .slice(0, -1) | ||||
|     .map((_) => "..") | ||||
|     .join("/") | ||||
| 
 | ||||
|   const res = _addRelativeToStart(rootPath) as RelativeURL | ||||
|   return res | ||||
|   if (rootPath.length === 0) { | ||||
|     rootPath = "." | ||||
|   } | ||||
| 
 | ||||
| export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL { | ||||
|   const res = joinSegments(pathToRoot(current), target) as RelativeURL | ||||
|   return rootPath as RelativeURL | ||||
| } | ||||
| 
 | ||||
| export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL { | ||||
|   const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL | ||||
|   return res | ||||
| } | ||||
| 
 | ||||
| @ -206,20 +137,16 @@ export function getAllSegmentPrefixes(tags: string): string[] { | ||||
| 
 | ||||
| export interface TransformOptions { | ||||
|   strategy: "absolute" | "relative" | "shortest" | ||||
|   allSlugs: ServerSlug[] | ||||
|   allSlugs: FullSlug[] | ||||
| } | ||||
| 
 | ||||
| export function transformLink( | ||||
|   src: CanonicalSlug, | ||||
|   target: string, | ||||
|   opts: TransformOptions, | ||||
| ): RelativeURL { | ||||
|   let targetSlug: string = transformInternalLink(target) | ||||
| export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL { | ||||
|   let targetSlug = transformInternalLink(target) | ||||
| 
 | ||||
|   if (opts.strategy === "relative") { | ||||
|     return _addRelativeToStart(targetSlug) as RelativeURL | ||||
|     return targetSlug as RelativeURL | ||||
|   } else { | ||||
|     const folderTail = targetSlug.endsWith("/") ? "/" : "" | ||||
|     const folderTail = _isFolderPath(targetSlug) ? "/" : "" | ||||
|     const canonicalSlug = _stripSlashes(targetSlug.slice(".".length)) | ||||
|     let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug) | ||||
| 
 | ||||
| @ -233,7 +160,7 @@ export function transformLink( | ||||
| 
 | ||||
|       // only match, just use it
 | ||||
|       if (matchingFileNames.length === 1) { | ||||
|         const targetSlug = canonicalizeServer(matchingFileNames[0]) | ||||
|         const targetSlug = matchingFileNames[0] | ||||
|         return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL | ||||
|       } | ||||
|     } | ||||
| @ -243,9 +170,13 @@ export function transformLink( | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function _canonicalize(fp: string): string { | ||||
|   fp = _trimSuffix(fp, "index") | ||||
|   return _stripSlashes(fp) | ||||
| function _isFolderPath(fplike: string): boolean { | ||||
|   return ( | ||||
|     fplike.endsWith("/") || | ||||
|     _endsWith(fplike, "index") || | ||||
|     _endsWith(fplike, "index.md") || | ||||
|     _endsWith(fplike, "index.html") | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function _endsWith(s: string, suffix: string): boolean { | ||||
| @ -275,12 +206,12 @@ function _isRelativeSegment(s: string): boolean { | ||||
|   return /^\.{0,2}$/.test(s) | ||||
| } | ||||
| 
 | ||||
| export function _stripSlashes(s: string): string { | ||||
| export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string { | ||||
|   if (s.startsWith("/")) { | ||||
|     s = s.substring(1) | ||||
|   } | ||||
| 
 | ||||
|   if (s.endsWith("/")) { | ||||
|   if (!onlyStripPrefix && s.endsWith("/")) { | ||||
|     s = s.slice(0, -1) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -2,12 +2,12 @@ import sourceMapSupport from "source-map-support" | ||||
| sourceMapSupport.install(options) | ||||
| import cfg from "../quartz.config" | ||||
| 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 { options } from "./util/sourcemap" | ||||
| 
 | ||||
| // 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 = { | ||||
|     cfg, | ||||
|     argv, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user