base path refactor to better support subpath hosting
This commit is contained in:
parent
a3703cbe0b
commit
303a867d60
@ -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…
Reference in New Issue
Block a user