Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8c025d6a | ||
|
|
54b4a5567c | ||
|
|
610b04406f | ||
|
|
82bd08d14a | ||
|
|
649090de1b | ||
|
|
b5fec6c87f | ||
|
|
0d314db1f8 | ||
|
|
660aae62e0 | ||
|
|
9a599aebea | ||
|
|
296c1cf83f | ||
|
|
516d9a27e7 | ||
|
|
6a05fa777c | ||
|
|
3f0be7fbe4 | ||
|
|
ea08c0511a | ||
|
|
727b9b5d72 | ||
|
|
50f0ba29a2 | ||
|
|
95b1141b9d | ||
|
|
a26eb59392 | ||
|
|
5befcf4780 | ||
|
|
f861a7c160 | ||
|
|
06426c8f7e | ||
|
|
8fc7b9f4c6 | ||
|
|
2de48b267a | ||
|
|
76f2664277 | ||
|
|
74777118a7 | ||
|
|
8223465bda | ||
|
|
cf6ab9e933 | ||
|
|
74c63e448e | ||
|
|
43d638a6de | ||
|
|
d1551872ff | ||
|
|
275bea3051 | ||
|
|
bc02791734 | ||
|
|
bf603c49c2 | ||
|
|
f67356c3d2 | ||
|
|
5d666d1860 | ||
|
|
22b7cf135e | ||
|
|
50a87d0d86 | ||
|
|
134b6ed582 | ||
|
|
99e8f5944f | ||
|
|
e9f4e28a2d | ||
|
|
2a6b9a9ea0 | ||
|
|
e806c30fa1 | ||
|
|
aac7b7e97d | ||
|
|
101e9946bd | ||
|
|
a62a97c7ab |
@@ -228,7 +228,7 @@ export type QuartzEmitterPluginInstance = {
|
|||||||
|
|
||||||
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
||||||
|
|
||||||
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. It's interface looks something like this:
|
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type EmitCallback = (data: {
|
export type EmitCallback = (data: {
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ For example, here's what the default configuration looks like:
|
|||||||
|
|
||||||
```typescript title="quartz.layout.ts"
|
```typescript title="quartz.layout.ts"
|
||||||
Component.Breadcrumbs({
|
Component.Breadcrumbs({
|
||||||
spacerSymbol: ">", // symbol between crumbs
|
spacerSymbol: "❯", // symbol between crumbs
|
||||||
rootName: "Home", // name of first/root element
|
rootName: "Home", // name of first/root element
|
||||||
resolveFrontmatterTitle: false, // wether to resolve folder names through frontmatter titles (more computationally expensive)
|
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||||
hideOnRoot: true, // wether to hide breadcrumbs on root `index.md` page
|
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||||
|
showCurrentPage: true, // wether to display the current page in the breadcrumbs
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
|
|||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be nested?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can.
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
> > > [!example] You can even use multiple layers of nesting.
|
> > > [!example] You can even use multiple layers of nesting.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ tags:
|
|||||||
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
||||||
|
|
||||||
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
||||||
You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page.
|
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||||
@@ -18,6 +18,7 @@ You can also hide the table of contents on a page by adding `showToc: false` to
|
|||||||
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
||||||
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
||||||
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
||||||
|
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
|
||||||
- Component: `quartz/components/TableOfContents.tsx`
|
- Component: `quartz/components/TableOfContents.tsx`
|
||||||
- Style:
|
- Style:
|
||||||
- Modern (default): `quartz/components/styles/toc.scss`
|
- Modern (default): `quartz/components/styles/toc.scss`
|
||||||
|
|||||||
@@ -166,3 +166,67 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c
|
|||||||
3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project.
|
3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project.
|
||||||
4. Go to the Settings tab and then click Domains in the sidebar
|
4. Go to the Settings tab and then click Domains in the sidebar
|
||||||
5. Enter your subdomain into the field and press Add
|
5. Enter your subdomain into the field and press Add
|
||||||
|
|
||||||
|
## Netlify
|
||||||
|
|
||||||
|
Like Vercel, you can also deploy the site generated by Quartz 4 via Netlify.
|
||||||
|
|
||||||
|
1. Log in to the [Netlify dashboard](https://app.netlify.com/) and click "Add new site".
|
||||||
|
2. Select your Git provider and repository containing your Quartz project.
|
||||||
|
3. Under "Build command", enter `npx quartz build`.
|
||||||
|
4. Under "Publish directory", enter `public`.
|
||||||
|
5. Press Deploy. Once it's live, you'll have a `*.netlify.app` URL to view the page.
|
||||||
|
6. To add a custom domain, check "Domain management" in the left sidebar, just like with Vercel.
|
||||||
|
|
||||||
|
## GitLab Pages
|
||||||
|
|
||||||
|
You can configure GitLab CI to build and deploy a Quartz 4 project.
|
||||||
|
|
||||||
|
In your local Quartz, create a new file `.gitlab-ci.yaml`.
|
||||||
|
|
||||||
|
```yaml title=".gitlab-ci.yaml"
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
variables:
|
||||||
|
NODE_VERSION: "18.14"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||||
|
before_script:
|
||||||
|
- apt-get update -q && apt-get install -y nodejs npm
|
||||||
|
- npm install -g n
|
||||||
|
- n $NODE_VERSION
|
||||||
|
- hash -r
|
||||||
|
- npm ci
|
||||||
|
script:
|
||||||
|
- npx prettier --write .
|
||||||
|
- npm run check
|
||||||
|
- npx quartz build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- ~/.npm/
|
||||||
|
key: "${CI_COMMIT_REF_SLUG}-node-${CI_COMMIT_REF_NAME}"
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
pages:
|
||||||
|
stage: deploy
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||||
|
script:
|
||||||
|
- echo "Deploying to GitLab Pages..."
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
```
|
||||||
|
|
||||||
|
When `.gitlab-ci.yaml` is commited, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy` -> `Pages` in the sidebar.
|
||||||
|
|
||||||
|
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
|
||||||
|
|||||||
@@ -19,5 +19,7 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
||||||
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
||||||
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||||
|
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||||
|
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.0.11",
|
"version": "4.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.0.11",
|
"version": "4.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.6.3",
|
||||||
@@ -85,7 +85,8 @@
|
|||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14"
|
"node": ">=18.14",
|
||||||
|
"npm": ">=9.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clack/core": {
|
"node_modules/@clack/core": {
|
||||||
|
|||||||
@@ -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.1.0",
|
"version": "4.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export const SyncArgv = {
|
|||||||
default: true,
|
default: true,
|
||||||
describe: "create a git commit for your unsaved changes",
|
describe: "create a git commit for your unsaved changes",
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
string: true,
|
||||||
|
alias: ["m"],
|
||||||
|
describe: "option to override the default Quartz commit message",
|
||||||
|
},
|
||||||
push: {
|
push: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: true,
|
default: true,
|
||||||
|
|||||||
@@ -483,8 +483,9 @@ export async function handleSync(argv) {
|
|||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
})
|
})
|
||||||
|
const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
|
||||||
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
||||||
spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
|
spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
|
||||||
|
|
||||||
if (contentStat.isSymbolicLink()) {
|
if (contentStat.isSymbolicLink()) {
|
||||||
// put symlink back
|
// put symlink back
|
||||||
|
|||||||
@@ -25,13 +25,18 @@ interface BreadcrumbOptions {
|
|||||||
* Wether to display breadcrumbs on root `index.md`
|
* Wether to display breadcrumbs on root `index.md`
|
||||||
*/
|
*/
|
||||||
hideOnRoot: boolean
|
hideOnRoot: boolean
|
||||||
|
/**
|
||||||
|
* Wether to display the current page in the breadcrumbs.
|
||||||
|
*/
|
||||||
|
showCurrentPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: BreadcrumbOptions = {
|
const defaultOptions: BreadcrumbOptions = {
|
||||||
spacerSymbol: ">",
|
spacerSymbol: "❯",
|
||||||
rootName: "Home",
|
rootName: "Home",
|
||||||
resolveFrontmatterTitle: false,
|
resolveFrontmatterTitle: true,
|
||||||
hideOnRoot: true,
|
hideOnRoot: true,
|
||||||
|
showCurrentPage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
||||||
@@ -41,25 +46,13 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a folderName (e.g. "features"), search for the corresponding `index.md` file
|
|
||||||
function findCurrentFile(allFiles: QuartzPluginData[], folderName: string) {
|
|
||||||
return allFiles.find((file) => {
|
|
||||||
if (file.slug?.endsWith("index")) {
|
|
||||||
const folderParts = file.filePath?.split("/")
|
|
||||||
if (folderParts) {
|
|
||||||
const name = folderParts[folderParts?.length - 2]
|
|
||||||
if (name === folderName) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||||
// Merge options with defaults
|
// Merge options with defaults
|
||||||
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
|
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
|
// computed index of folder name to its associated file data
|
||||||
|
let folderIndex: Map<string, QuartzPluginData> | undefined
|
||||||
|
|
||||||
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
// Hide crumbs on root if enabled
|
// Hide crumbs on root if enabled
|
||||||
if (options.hideOnRoot && fileData.slug === "index") {
|
if (options.hideOnRoot && fileData.slug === "index") {
|
||||||
@@ -70,36 +63,49 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
||||||
const crumbs: CrumbData[] = [firstEntry]
|
const crumbs: CrumbData[] = [firstEntry]
|
||||||
|
|
||||||
|
if (!folderIndex && options.resolveFrontmatterTitle) {
|
||||||
|
folderIndex = new Map()
|
||||||
|
// construct the index for the first time
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (file.slug?.endsWith("index")) {
|
||||||
|
const folderParts = file.filePath?.split("/")
|
||||||
|
if (folderParts) {
|
||||||
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
|
folderIndex.set(folderName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Split slug into hierarchy/parts
|
// Split slug into hierarchy/parts
|
||||||
const slugParts = fileData.slug?.split("/")
|
const slugParts = fileData.slug?.split("/")
|
||||||
if (slugParts) {
|
if (slugParts) {
|
||||||
// full path until current part
|
// full path until current part
|
||||||
let currentPath = ""
|
let currentPath = ""
|
||||||
for (let i = 0; i < slugParts.length - 1; i++) {
|
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||||
let currentTitle = slugParts[i]
|
let curPathSegment = slugParts[i]
|
||||||
|
|
||||||
// TODO: performance optimizations/memoizing
|
|
||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
if (options?.resolveFrontmatterTitle) {
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
// try to find file for current path
|
if (currentFile) {
|
||||||
const currentFile = findCurrentFile(allFiles, currentTitle)
|
curPathSegment = currentFile.frontmatter!.title
|
||||||
if (currentFile) {
|
|
||||||
currentTitle = currentFile.frontmatter!.title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
currentPath += slugParts[i] + "/"
|
currentPath += slugParts[i] + "/"
|
||||||
|
|
||||||
// Format and add current crumb
|
// Format and add current crumb
|
||||||
const crumb = formatCrumb(currentTitle, fileData.slug!, currentPath as SimpleSlug)
|
const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
|
||||||
crumbs.push(crumb)
|
crumbs.push(crumb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current file to crumb (can directly use frontmatter title)
|
// Add current file to crumb (can directly use frontmatter title)
|
||||||
crumbs.push({
|
if (options.showCurrentPage) {
|
||||||
displayName: fileData.frontmatter!.title,
|
crumbs.push({
|
||||||
path: "",
|
displayName: fileData.frontmatter!.title,
|
||||||
})
|
path: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`toc ${displayClass ?? ""}`}>
|
<div class={`toc ${displayClass ?? ""}`}>
|
||||||
<button type="button" id="toc">
|
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -60,7 +60,7 @@ function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details id="toc" open>
|
<details id="toc" open={!fileData.collapseToc}>
|
||||||
<summary>
|
<summary>
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>
|
<h2>
|
||||||
<a class="internal tag-link" href={`./${tag}`}>
|
<a class="internal tag-link" href={`../tags/${tag}`}>
|
||||||
#{tag}
|
#{tag}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ 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 { FullSlug, RelativeURL, joinSegments } from "../util/path"
|
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { Root, Element } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@@ -49,6 +50,18 @@ export function pageResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
|
||||||
|
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
|
||||||
|
if (!pageIndex) {
|
||||||
|
pageIndex = new Map()
|
||||||
|
for (const file of allFiles) {
|
||||||
|
pageIndex.set(file.slug!, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageIndex
|
||||||
|
}
|
||||||
|
|
||||||
export function renderPage(
|
export function renderPage(
|
||||||
slug: FullSlug,
|
slug: FullSlug,
|
||||||
componentData: QuartzComponentProps,
|
componentData: QuartzComponentProps,
|
||||||
@@ -61,22 +74,83 @@ export function renderPage(
|
|||||||
const classNames = (node.properties?.className ?? []) as string[]
|
const classNames = (node.properties?.className ?? []) as string[]
|
||||||
if (classNames.includes("transclude")) {
|
if (classNames.includes("transclude")) {
|
||||||
const inner = node.children[0] as Element
|
const inner = node.children[0] as Element
|
||||||
const blockSlug = inner.properties?.["data-slug"] as FullSlug
|
const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
|
||||||
const blockRef = node.properties!.dataBlock as string
|
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||||
|
if (!page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: avoid this expensive find operation and construct an index ahead of time
|
let blockRef = node.properties?.dataBlock as string | undefined
|
||||||
let blockNode = componentData.allFiles.find((f) => f.slug === blockSlug)?.blocks?.[blockRef]
|
if (blockRef?.startsWith("#^")) {
|
||||||
if (blockNode) {
|
// block transclude
|
||||||
if (blockNode.tagName === "li") {
|
blockRef = blockRef.slice("#^".length)
|
||||||
blockNode = {
|
let blockNode = page.blocks?.[blockRef]
|
||||||
type: "element",
|
if (blockNode) {
|
||||||
tagName: "ul",
|
if (blockNode.tagName === "li") {
|
||||||
children: [blockNode],
|
blockNode = {
|
||||||
|
type: "element",
|
||||||
|
tagName: "ul",
|
||||||
|
children: [blockNode],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.children = [
|
||||||
|
normalizeHastElement(blockNode, slug, transcludeTarget),
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "a",
|
||||||
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (blockRef?.startsWith("#") && page.htmlAst) {
|
||||||
|
// header transclude
|
||||||
|
blockRef = blockRef.slice(1)
|
||||||
|
let startIdx = undefined
|
||||||
|
let endIdx = undefined
|
||||||
|
for (const [i, el] of page.htmlAst.children.entries()) {
|
||||||
|
if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
|
||||||
|
if (endIdx) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIdx !== undefined) {
|
||||||
|
endIdx = i
|
||||||
|
} else if (el.properties?.id === blockRef) {
|
||||||
|
startIdx = i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startIdx === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
node.children = [
|
node.children = [
|
||||||
blockNode,
|
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "a",
|
||||||
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (page.htmlAst) {
|
||||||
|
// page transclude
|
||||||
|
node.children = [
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "h1",
|
||||||
|
children: [
|
||||||
|
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ function setupExplorer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else if (explorer?.dataset.tree) {
|
||||||
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
||||||
explorerState = JSON.parse(explorer?.dataset.tree as string)
|
explorerState = JSON.parse(explorer.dataset.tree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +130,13 @@ window.addEventListener("resize", setupExplorer)
|
|||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
setupExplorer()
|
setupExplorer()
|
||||||
|
|
||||||
const explorerContent = document.getElementById("explorer-ul")
|
observer.disconnect()
|
||||||
|
|
||||||
// select pseudo element at end of list
|
// select pseudo element at end of list
|
||||||
const lastItem = document.getElementById("explorer-end")
|
const lastItem = document.getElementById("explorer-end")
|
||||||
|
if (lastItem) {
|
||||||
observer.disconnect()
|
observer.observe(lastItem)
|
||||||
observer.observe(lastItem as Element)
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import type { ContentDetails, ContentIndex } 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 { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
@@ -46,20 +46,22 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
showTags,
|
showTags,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = JSON.parse(graph.dataset["cfg"]!)
|
||||||
|
|
||||||
const data = await fetchData
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
|
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
||||||
|
simplifySlug(k as FullSlug),
|
||||||
|
v,
|
||||||
|
]),
|
||||||
|
)
|
||||||
const links: LinkData[] = []
|
const links: LinkData[] = []
|
||||||
const tags: SimpleSlug[] = []
|
const tags: SimpleSlug[] = []
|
||||||
|
|
||||||
const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug)))
|
const validLinks = new Set(data.keys())
|
||||||
|
for (const [source, details] of data.entries()) {
|
||||||
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 (validLinks.has(dest)) {
|
if (validLinks.has(dest)) {
|
||||||
links.push({ source, target: dest })
|
links.push({ source: source, target: dest })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
tags.push(...localTags.filter((tag) => !tags.includes(tag)))
|
tags.push(...localTags.filter((tag) => !tags.includes(tag)))
|
||||||
|
|
||||||
for (const tag of localTags) {
|
for (const tag of localTags) {
|
||||||
links.push({ source, target: tag })
|
links.push({ source: source, target: tag })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,17 +95,17 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
|
validLinks.forEach((id) => neighbourhood.add(id))
|
||||||
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||||
nodes: [...neighbourhood].map((url) => {
|
nodes: [...neighbourhood].map((url) => {
|
||||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url
|
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
|
||||||
return {
|
return {
|
||||||
id: url,
|
id: url,
|
||||||
text: text,
|
text: text,
|
||||||
tags: data[url]?.tags ?? [],
|
tags: data.get(url)?.tags ?? [],
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
||||||
@@ -200,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
})
|
})
|
||||||
.on("mouseover", function (_, d) {
|
.on("mouseover", function (_, d) {
|
||||||
const neighbours: SimpleSlug[] = data[fullSlug].links ?? []
|
const neighbours: SimpleSlug[] = data.get(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))
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||||
|
import { normalizeRelativeURLs } from "../../util/path"
|
||||||
// from micromorph/src/utils.ts
|
|
||||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
|
||||||
export function normalizeRelativeURLs(el: Element | Document, base: string | URL) {
|
|
||||||
const update = (el: Element, attr: string, base: string | URL) => {
|
|
||||||
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
|
|
||||||
|
|
||||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
async function mouseEnterHandler(
|
async function mouseEnterHandler(
|
||||||
@@ -28,8 +17,11 @@ async function mouseEnterHandler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasAlreadyBeenFetched = () =>
|
||||||
|
[...link.children].some((child) => child.classList.contains("popover"))
|
||||||
|
|
||||||
// dont refetch if there's already a popover
|
// dont refetch if there's already a popover
|
||||||
if ([...link.children].some((child) => child.classList.contains("popover"))) {
|
if (hasAlreadyBeenFetched()) {
|
||||||
return setPosition(link.lastChild as HTMLElement)
|
return setPosition(link.lastChild as HTMLElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +32,6 @@ async function mouseEnterHandler(
|
|||||||
const hash = targetUrl.hash
|
const hash = targetUrl.hash
|
||||||
targetUrl.hash = ""
|
targetUrl.hash = ""
|
||||||
targetUrl.search = ""
|
targetUrl.search = ""
|
||||||
// prevent hover of the same page
|
|
||||||
if (thisUrl.toString() === targetUrl.toString()) return
|
|
||||||
|
|
||||||
const contents = await fetch(`${targetUrl}`)
|
const contents = await fetch(`${targetUrl}`)
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
@@ -49,6 +39,11 @@ async function mouseEnterHandler(
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// bailout if another popover exists
|
||||||
|
if (hasAlreadyBeenFetched()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
normalizeRelativeURLs(html, targetUrl)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import micromorph from "micromorph"
|
import micromorph from "micromorph"
|
||||||
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
|
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||||
|
|
||||||
// adapted from `micromorph`
|
// adapted from `micromorph`
|
||||||
// https://github.com/natemoo-re/micromorph
|
// https://github.com/natemoo-re/micromorph
|
||||||
|
|
||||||
const NODE_TYPE_ELEMENT = 1
|
const NODE_TYPE_ELEMENT = 1
|
||||||
let announcer = document.createElement("route-announcer")
|
let announcer = document.createElement("route-announcer")
|
||||||
const isElement = (target: EventTarget | null): target is Element =>
|
const isElement = (target: EventTarget | null): target is Element =>
|
||||||
@@ -18,6 +17,12 @@ const isLocalUrl = (href: string) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSamePage = (url: URL): boolean => {
|
||||||
|
const sameOrigin = url.origin === window.location.origin
|
||||||
|
const samePath = url.pathname === window.location.pathname
|
||||||
|
return sameOrigin && samePath
|
||||||
|
}
|
||||||
|
|
||||||
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
||||||
if (!isElement(target)) return
|
if (!isElement(target)) return
|
||||||
if (target.attributes.getNamedItem("target")?.value === "_blank") return
|
if (target.attributes.getNamedItem("target")?.value === "_blank") return
|
||||||
@@ -38,7 +43,14 @@ let p: DOMParser
|
|||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
const contents = await fetch(`${url}`)
|
const contents = await fetch(`${url}`)
|
||||||
.then((res) => res.text())
|
.then((res) => {
|
||||||
|
const contentType = res.headers.get("content-type")
|
||||||
|
if (contentType?.startsWith("text/html")) {
|
||||||
|
return res.text()
|
||||||
|
} else {
|
||||||
|
window.location.assign(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
window.location.assign(url)
|
window.location.assign(url)
|
||||||
})
|
})
|
||||||
@@ -46,6 +58,8 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
let title = html.querySelector("title")?.textContent
|
let title = html.querySelector("title")?.textContent
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = title
|
document.title = title
|
||||||
@@ -93,8 +107,17 @@ function createRouter() {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("click", async (event) => {
|
window.addEventListener("click", async (event) => {
|
||||||
const { url } = getOpts(event) ?? {}
|
const { url } = getOpts(event) ?? {}
|
||||||
|
// dont hijack behaviour, just let browser act normally
|
||||||
if (!url || event.ctrlKey || event.metaKey) return
|
if (!url || event.ctrlKey || event.metaKey) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (isSamePage(url) && url.hash) {
|
||||||
|
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||||
|
el?.scrollIntoView()
|
||||||
|
history.pushState({}, "", url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigate(url, false)
|
navigate(url, false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -140,6 +163,7 @@ if (!customElements.get("route-announcer")) {
|
|||||||
style:
|
style:
|
||||||
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define(
|
customElements.define(
|
||||||
"route-announcer",
|
"route-announcer",
|
||||||
class RouteAnnouncer extends HTMLElement {
|
class RouteAnnouncer extends HTMLElement {
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ function toggleToc(this: HTMLElement) {
|
|||||||
function setupToc() {
|
function setupToc() {
|
||||||
const toc = document.getElementById("toc")
|
const toc = document.getElementById("toc")
|
||||||
if (toc) {
|
if (toc) {
|
||||||
|
const collapsed = toc.classList.contains("collapsed")
|
||||||
const content = toc.nextElementSibling as HTMLElement
|
const content = toc.nextElementSibling as HTMLElement
|
||||||
content.style.maxHeight = content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
toc.removeEventListener("click", toggleToc)
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ button#toc {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
transition: max-height 0.5s ease;
|
transition: max-height 0.5s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed > .overflow::after {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
@@ -25,7 +25,12 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
slugs.push(permalink as FullSlug)
|
slugs.push(permalink as FullSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slug of slugs) {
|
for (let slug of slugs) {
|
||||||
|
// fix any slugs that have trailing slash
|
||||||
|
if (slug.endsWith("/")) {
|
||||||
|
slug = joinSegments(slug, "index") as FullSlug
|
||||||
|
}
|
||||||
|
|
||||||
const redirUrl = resolveRelative(slug, file.data.slug!)
|
const redirUrl = resolveRelative(slug, file.data.slug!)
|
||||||
const fp = await emit({
|
const fp = await emit({
|
||||||
content: `
|
content: `
|
||||||
|
|||||||
29
quartz/plugins/emitters/cname.ts
Normal file
29
quartz/plugins/emitters/cname.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { FilePath, joinSegments } from "../../util/path"
|
||||||
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
|
import fs from "fs"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
export function extractDomainFromBaseUrl(baseUrl: string) {
|
||||||
|
const url = new URL(`https://${baseUrl}`)
|
||||||
|
return url.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CNAME: QuartzEmitterPlugin = () => ({
|
||||||
|
name: "CNAME",
|
||||||
|
getQuartzComponents() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
|
if (!cfg.configuration.baseUrl) {
|
||||||
|
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const path = joinSegments(argv.output, "CNAME")
|
||||||
|
const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl)
|
||||||
|
if (!content) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path, content)
|
||||||
|
return [path] as FilePath[]
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -59,6 +59,17 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
</item>`
|
</item>`
|
||||||
|
|
||||||
const items = Array.from(idx)
|
const items = Array.from(idx)
|
||||||
|
.sort(([_, f1], [__, f2]) => {
|
||||||
|
if (f1.date && f2.date) {
|
||||||
|
return f2.date.getTime() - f1.date.getTime()
|
||||||
|
} else if (f1.date && !f2.date) {
|
||||||
|
return -1
|
||||||
|
} else if (!f1.date && f2.date) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return f1.title.localeCompare(f2.title)
|
||||||
|
})
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
||||||
.slice(0, limit ?? idx.size)
|
.slice(0, limit ?? idx.size)
|
||||||
.join("")
|
.join("")
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { Assets } from "./assets"
|
|||||||
export { Static } from "./static"
|
export { Static } from "./static"
|
||||||
export { ComponentResources } from "./componentResources"
|
export { ComponentResources } from "./componentResources"
|
||||||
export { NotFoundPage } from "./404"
|
export { NotFoundPage } from "./404"
|
||||||
|
export { CNAME } from "./cname"
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export const Static: QuartzEmitterPlugin = () => ({
|
|||||||
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
const staticPath = joinSegments(QUARTZ, "static")
|
const staticPath = joinSegments(QUARTZ, "static")
|
||||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||||
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { recursive: true })
|
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
|
||||||
|
recursive: true,
|
||||||
|
dereference: true,
|
||||||
|
})
|
||||||
return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[]
|
return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
node.properties.className ??= []
|
node.properties.className ??= []
|
||||||
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
||||||
|
|
||||||
|
// Check if the link has alias text
|
||||||
|
if (
|
||||||
|
node.children.length === 1 &&
|
||||||
|
node.children[0].type === "text" &&
|
||||||
|
node.children[0].value !== dest
|
||||||
|
) {
|
||||||
|
// Add the 'alias' class if the text content is not the same as the href
|
||||||
|
node.properties.className.push("alias")
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.openLinksInNewTab) {
|
if (opts.openLinksInNewTab) {
|
||||||
node.properties.target = "_blank"
|
node.properties.target = "_blank"
|
||||||
}
|
}
|
||||||
@@ -71,14 +81,16 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
||||||
const url = new URL(dest, `https://base.com/${curSlug}`)
|
const url = new URL(dest, `https://base.com/${curSlug}`)
|
||||||
const canonicalDest = url.pathname
|
const canonicalDest = url.pathname
|
||||||
const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
||||||
|
if (destCanonical.endsWith("/")) {
|
||||||
|
destCanonical += "index"
|
||||||
|
}
|
||||||
|
|
||||||
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
|
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
|
||||||
const simple = decodeURIComponent(
|
const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug
|
||||||
simplifySlug(destCanonical as FullSlug),
|
const simple = simplifySlug(full)
|
||||||
) as SimpleSlug
|
|
||||||
outgoing.add(simple)
|
outgoing.add(simple)
|
||||||
node.properties["data-slug"] = simple
|
node.properties["data-slug"] = full
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewrite link internals if prettylinks is on
|
// rewrite link internals if prettylinks is on
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PluggableList } from "unified"
|
import { PluggableList } from "unified"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
|
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
|
||||||
import { Element, Literal } from "hast"
|
import { Element, Literal, Root as HtmlRoot } from "hast"
|
||||||
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||||
import { slug as slugAnchor } from "github-slugger"
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
@@ -110,7 +110,10 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
|||||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
||||||
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
||||||
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
||||||
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
|
export const wikilinkRegex = new RegExp(
|
||||||
|
/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/,
|
||||||
|
"g",
|
||||||
|
)
|
||||||
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
|
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
|
||||||
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
||||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
||||||
@@ -178,8 +181,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
||||||
const [rawFp, rawHeader, rawAlias] = capture
|
const [rawFp, rawHeader, rawAlias] = capture
|
||||||
const fp = rawFp ?? ""
|
const fp = rawFp ?? ""
|
||||||
const anchor = rawHeader?.trim().slice(1)
|
const anchor = rawHeader?.trim().replace(/^#+/, "")
|
||||||
const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : ""
|
const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : ""
|
||||||
|
const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : ""
|
||||||
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
||||||
const embedDisplay = value.startsWith("!") ? "!" : ""
|
const embedDisplay = value.startsWith("!") ? "!" : ""
|
||||||
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
||||||
@@ -236,13 +240,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
value: `<iframe src="${url}"></iframe>`,
|
value: `<iframe src="${url}"></iframe>`,
|
||||||
}
|
}
|
||||||
} else if (ext === "") {
|
} else if (ext === "") {
|
||||||
const block = anchor.slice(1)
|
const block = anchor
|
||||||
return {
|
return {
|
||||||
type: "html",
|
type: "html",
|
||||||
data: { hProperties: { transclude: true } },
|
data: { hProperties: { transclude: true } },
|
||||||
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
|
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
|
||||||
url + anchor
|
url + anchor
|
||||||
}" class="transclude-inner">Transclude of block ${block}</a></blockquote>`,
|
}" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,6 +481,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
file.data.htmlAst = tree
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -524,5 +530,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
blocks: Record<string, Element>
|
blocks: Record<string, Element>
|
||||||
|
htmlAst: HtmlRoot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import { Root } from "mdast"
|
|||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { toString } from "mdast-util-to-string"
|
import { toString } from "mdast-util-to-string"
|
||||||
import Slugger from "github-slugger"
|
import Slugger from "github-slugger"
|
||||||
|
import { wikilinkRegex } from "./ofm"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
||||||
minEntries: 1
|
minEntries: 1
|
||||||
showByDefault: boolean
|
showByDefault: boolean
|
||||||
|
collapseByDefault: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
maxDepth: 3,
|
maxDepth: 3,
|
||||||
minEntries: 1,
|
minEntries: 1,
|
||||||
showByDefault: true,
|
showByDefault: true,
|
||||||
|
collapseByDefault: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TocEntry {
|
interface TocEntry {
|
||||||
@@ -22,6 +25,7 @@ interface TocEntry {
|
|||||||
slug: string // this is just the anchor (#some-slug), not the canonical slug
|
slug: string // this is just the anchor (#some-slug), not the canonical slug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g")
|
||||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
userOpts,
|
userOpts,
|
||||||
) => {
|
) => {
|
||||||
@@ -39,7 +43,16 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
|||||||
let highestDepth: number = opts.maxDepth
|
let highestDepth: number = opts.maxDepth
|
||||||
visit(tree, "heading", (node) => {
|
visit(tree, "heading", (node) => {
|
||||||
if (node.depth <= opts.maxDepth) {
|
if (node.depth <= opts.maxDepth) {
|
||||||
const text = toString(node)
|
let text = toString(node)
|
||||||
|
|
||||||
|
// strip link formatting from toc entries
|
||||||
|
text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => {
|
||||||
|
const fp = rawFp?.trim() ?? ""
|
||||||
|
const alias = rawAlias?.slice(1).trim()
|
||||||
|
return alias ?? fp
|
||||||
|
})
|
||||||
|
text = text.replace(regexMdLinks, "$1")
|
||||||
|
|
||||||
highestDepth = Math.min(highestDepth, node.depth)
|
highestDepth = Math.min(highestDepth, node.depth)
|
||||||
toc.push({
|
toc.push({
|
||||||
depth: node.depth,
|
depth: node.depth,
|
||||||
@@ -54,6 +67,7 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
|||||||
...entry,
|
...entry,
|
||||||
depth: entry.depth - highestDepth,
|
depth: entry.depth - highestDepth,
|
||||||
}))
|
}))
|
||||||
|
file.data.collapseToc = opts.collapseByDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,5 +80,6 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
|||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
toc: TocEntry[]
|
toc: TocEntry[]
|
||||||
|
collapseToc: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ a {
|
|||||||
color: var(--tertiary) !important;
|
color: var(--tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.internal {
|
&.internal:not(:has(> img)) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
@@ -390,23 +390,33 @@ p {
|
|||||||
line-height: 1.6rem;
|
line-height: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.table-container {
|
||||||
margin: 1rem;
|
overflow-x: auto;
|
||||||
padding: 1.5rem;
|
|
||||||
border-collapse: collapse;
|
& > table {
|
||||||
& > * {
|
margin: 1rem;
|
||||||
line-height: 2rem;
|
padding: 1.5rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
min-width: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.4rem 1rem;
|
padding: 0.4rem 0.7rem;
|
||||||
border-bottom: 2px solid var(--gray);
|
border-bottom: 2px solid var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.2rem 1rem;
|
padding: 0.2rem 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import { Node, Root } from "hast"
|
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
|
||||||
import { trace } from "./trace"
|
|
||||||
import { type FilePath } from "./path"
|
|
||||||
|
|
||||||
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
|
||||||
try {
|
|
||||||
// @ts-ignore (preact makes it angry)
|
|
||||||
return toJsxRuntime(tree as Root, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
|
||||||
} catch (e) {
|
|
||||||
trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
quartz/util/jsx.tsx
Normal file
28
quartz/util/jsx.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
import { Node, Root } from "hast"
|
||||||
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
|
import { trace } from "./trace"
|
||||||
|
import { type FilePath } from "./path"
|
||||||
|
|
||||||
|
const customComponents: Components = {
|
||||||
|
table: (props) => (
|
||||||
|
<div class="table-container">
|
||||||
|
<table {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
||||||
|
try {
|
||||||
|
return toJsxRuntime(tree as Root, {
|
||||||
|
Fragment,
|
||||||
|
jsx: jsx as Jsx,
|
||||||
|
jsxs: jsxs as Jsx,
|
||||||
|
elementAttributeNameCase: "html",
|
||||||
|
components: customComponents,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ describe("transforms", () => {
|
|||||||
test("simplifySlug", () => {
|
test("simplifySlug", () => {
|
||||||
asserts(
|
asserts(
|
||||||
[
|
[
|
||||||
["index", ""],
|
["index", "/"],
|
||||||
["abc", "abc"],
|
["abc", "abc"],
|
||||||
["abc/index", "abc/"],
|
["abc/index", "abc/"],
|
||||||
["abc/def", "abc/def"],
|
["abc/def", "abc/def"],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { slug } from "github-slugger"
|
import { slug } from "github-slugger"
|
||||||
|
import type { Element as HastElement } from "hast"
|
||||||
// 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)
|
||||||
|
|
||||||
export const QUARTZ = "quartz"
|
export const QUARTZ = "quartz"
|
||||||
@@ -24,7 +25,7 @@ export function isFullSlug(s: string): s is FullSlug {
|
|||||||
/** 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. */
|
/** 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 type SimpleSlug = SlugLike<"simple">
|
||||||
export function isSimpleSlug(s: string): s is SimpleSlug {
|
export function isSimpleSlug(s: string): s is SimpleSlug {
|
||||||
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
|
||||||
const validEnding = !(s.endsWith("/index") || s === "index")
|
const validEnding = !(s.endsWith("/index") || s === "index")
|
||||||
return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,8 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function simplifySlug(fp: FullSlug): SimpleSlug {
|
export function simplifySlug(fp: FullSlug): SimpleSlug {
|
||||||
return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug
|
const res = _stripSlashes(_trimSuffix(fp, "index"), true)
|
||||||
|
return (res.length === 0 ? "/" : res) as SimpleSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformInternalLink(link: string): RelativeURL {
|
export function transformInternalLink(link: string): RelativeURL {
|
||||||
@@ -84,6 +86,49 @@ export function transformInternalLink(link: string): RelativeURL {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from micromorph/src/utils.ts
|
||||||
|
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||||
|
const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => {
|
||||||
|
const rebased = new URL(el.getAttribute(attr)!, newBase)
|
||||||
|
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
||||||
|
}
|
||||||
|
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
||||||
|
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
||||||
|
_rebaseHtmlElement(item, "href", destination),
|
||||||
|
)
|
||||||
|
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
||||||
|
_rebaseHtmlElement(item, "src", destination),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _rebaseHastElement = (
|
||||||
|
el: HastElement,
|
||||||
|
attr: string,
|
||||||
|
curBase: FullSlug,
|
||||||
|
newBase: FullSlug,
|
||||||
|
) => {
|
||||||
|
if (el.properties?.[attr]) {
|
||||||
|
if (!isRelativeURL(String(el.properties[attr]))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string)
|
||||||
|
el.properties[attr] = rel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
||||||
|
_rebaseHastElement(el, "src", curBase, newBase)
|
||||||
|
_rebaseHastElement(el, "href", curBase, newBase)
|
||||||
|
if (el.children) {
|
||||||
|
el.children = el.children.map((child) =>
|
||||||
|
normalizeHastElement(child as HastElement, curBase, newBase),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
// resolve /a/b/c to ../..
|
// resolve /a/b/c to ../..
|
||||||
export function pathToRoot(slug: FullSlug): RelativeURL {
|
export function pathToRoot(slug: FullSlug): RelativeURL {
|
||||||
let rootPath = slug
|
let rootPath = slug
|
||||||
|
|||||||
Reference in New Issue
Block a user