bump to v4
This commit is contained in:
parent
5c199ae6ef
commit
08ac0c3f8c
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -20,12 +20,19 @@ Steps to reproduce the behavior:
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots and Source**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
You can help speed up fixing the problem by either
|
||||||
|
|
||||||
|
1. providing a simple reproduction
|
||||||
|
2. linking to your Quartz repository where the problem can be observed
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
|
|
||||||
- Device: [e.g. iPhone6]
|
- Quartz Version: [e.g. v4.1.2]
|
||||||
|
- `node` Version: [e.g. v18.16]
|
||||||
|
- `npm` version: [e.g. v10.1.0]
|
||||||
- OS: [e.g. iOS]
|
- OS: [e.g. iOS]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
|
|
||||||
|
3711
package-lock.json
generated
3711
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
package.json
101
package.json
@ -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.1",
|
"version": "4.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -34,76 +34,77 @@
|
|||||||
"quartz": "./quartz/bootstrap-cli.mjs"
|
"quartz": "./quartz/bootstrap-cli.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.4.0",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@napi-rs/simple-git": "0.1.9",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"esbuild-sass-plugin": "^2.12.0",
|
"esbuild-sass-plugin": "^2.16.0",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^14.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^8.0.4",
|
"hast-util-to-html": "^9.0.0",
|
||||||
"hast-util-to-jsx-runtime": "^1.2.0",
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "1.21.7",
|
"lightningcss": "^1.22.1",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"mdast-util-to-hast": "^12.3.0",
|
"mdast-util-to-hast": "^13.0.2",
|
||||||
"mdast-util-to-string": "^3.2.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"plausible-tracker": "^0.3.8",
|
"preact": "^10.19.3",
|
||||||
"preact": "^10.14.1",
|
"preact-render-to-string": "^6.3.1",
|
||||||
"preact-render-to-string": "^6.0.3",
|
"pretty-bytes": "^6.1.1",
|
||||||
"pretty-bytes": "^6.1.0",
|
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^7.0.0",
|
||||||
"rehype-mathjax": "^4.0.3",
|
"rehype-mathjax": "^5.0.0",
|
||||||
"rehype-pretty-code": "^0.10.0",
|
"rehype-pretty-code": "^0.12.3",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-slug": "^5.1.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark": "^14.0.2",
|
"remark": "^15.0.1",
|
||||||
"remark-breaks": "^3.0.3",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^10.0.1",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^10.1.0",
|
"remark-rehype": "^11.0.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"rimraf": "^5.0.1",
|
"rfdc": "^1.3.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
|
"shikiji": "^0.9.9",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^7.2.4",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"unified": "^10.1.2",
|
"unified": "^11.0.4",
|
||||||
"unist-util-visit": "^4.1.2",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^5.3.7",
|
"vfile": "^6.0.1",
|
||||||
"workerpool": "^6.4.0",
|
"workerpool": "^8.0.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.15.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.1",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.0",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/hast": "^2.3.4",
|
"@types/hast": "^3.0.3",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.1.2",
|
"@types/node": "^20.1.2",
|
||||||
"@types/pretty-time": "^1.1.2",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.6",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/workerpool": "^6.4.0",
|
"@types/workerpool": "^6.4.7",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.10",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.32",
|
||||||
"esbuild": "0.19.2",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.1.1",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^4.6.2",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,11 +49,11 @@ const config: QuartzConfig = {
|
|||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ["frontmatter", "git", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
priority: ["frontmatter", "git", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
}),
|
||||||
|
Plugin.Latex({ renderEngine: "katex" }),
|
||||||
Plugin.SyntaxHighlighting(),
|
Plugin.SyntaxHighlighting(),
|
||||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||||
Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
Plugin.Latex({ renderEngine: "katex" }),
|
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
],
|
],
|
||||||
filters: [Plugin.RemoveDrafts()],
|
filters: [Plugin.RemoveDrafts()],
|
||||||
|
@ -147,14 +147,17 @@ async function startServing(
|
|||||||
await rimraf(argv.output)
|
await rimraf(argv.output)
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||||
} catch {
|
} catch (err) {
|
||||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(chalk.red(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
release()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
const watcher = chokidar.watch(".", {
|
||||||
|
@ -7,6 +7,7 @@ export type Analytics =
|
|||||||
| null
|
| null
|
||||||
| {
|
| {
|
||||||
provider: "plausible"
|
provider: "plausible"
|
||||||
|
host?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
provider: "google"
|
provider: "google"
|
||||||
|
@ -196,6 +196,11 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
)
|
)
|
||||||
await fs.promises.writeFile(configFilePath, configContent)
|
await fs.promises.writeFile(configFilePath, configContent)
|
||||||
|
|
||||||
|
// setup remote
|
||||||
|
execSync(
|
||||||
|
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||||
|
)
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
||||||
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
||||||
@ -438,11 +443,23 @@ export async function handleUpdate(argv) {
|
|||||||
console.log(
|
console.log(
|
||||||
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
)
|
)
|
||||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
|
||||||
|
try {
|
||||||
|
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
console.log("Ensuring dependencies are up to date")
|
console.log("Ensuring dependencies are up to date")
|
||||||
spawnSync("npm", ["i"], { stdio: "inherit" })
|
const res = spawnSync("npm", ["i"], { stdio: "inherit" })
|
||||||
console.log(chalk.green("Done!"))
|
if (res.status === 0) {
|
||||||
|
console.log(chalk.green("Done!"))
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red("An error occurred above while installing dependencies."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -499,13 +516,25 @@ export async function handleSync(argv) {
|
|||||||
console.log(
|
console.log(
|
||||||
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
)
|
)
|
||||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
try {
|
||||||
|
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
if (argv.push) {
|
if (argv.push) {
|
||||||
console.log("Pushing your changes")
|
console.log("Pushing your changes")
|
||||||
spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], {
|
||||||
|
stdio: "inherit",
|
||||||
|
})
|
||||||
|
if (res.status !== 0) {
|
||||||
|
console.log(chalk.red(`An error occurred above while pushing to remote ${ORIGIN_NAME}.`))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.green("Done!"))
|
console.log(chalk.green("Done!"))
|
||||||
|
@ -36,7 +36,9 @@ export function gitPull(origin, branch) {
|
|||||||
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
||||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||||
if (out.stderr) {
|
if (out.stderr) {
|
||||||
throw new Error(`Error while pulling updates: ${out.stderr}`)
|
throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`))
|
||||||
|
} else if (out.status !== 0) {
|
||||||
|
throw new Error(chalk.red("Error while pulling updates"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ 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 = {
|
||||||
@ -32,6 +36,7 @@ const defaultOptions: BreadcrumbOptions = {
|
|||||||
rootName: "Home",
|
rootName: "Home",
|
||||||
resolveFrontmatterTitle: true,
|
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 {
|
||||||
@ -63,8 +68,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// construct the index for the first time
|
// construct the index for the first time
|
||||||
for (const file of allFiles) {
|
for (const file of allFiles) {
|
||||||
if (file.slug?.endsWith("index")) {
|
if (file.slug?.endsWith("index")) {
|
||||||
const folderParts = file.filePath?.split("/")
|
const folderParts = file.slug?.split("/")
|
||||||
if (folderParts) {
|
if (folderParts) {
|
||||||
|
// 2nd last to exclude the /index
|
||||||
const folderName = folderParts[folderParts?.length - 2]
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
folderIndex.set(folderName, file)
|
folderIndex.set(folderName, file)
|
||||||
}
|
}
|
||||||
@ -83,7 +89,10 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
const currentFile = folderIndex?.get(curPathSegment)
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
curPathSegment = currentFile.frontmatter!.title
|
const title = currentFile.frontmatter!.title
|
||||||
|
if (title !== "index") {
|
||||||
|
curPathSegment = title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
@ -95,10 +104,12 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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">
|
||||||
|
@ -18,7 +18,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 35 35"
|
viewBox="0 0 35 35"
|
||||||
style="enable-background:new 0 0 35 35;"
|
style="enable-background:new 0 0 35 35"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>Light mode</title>
|
<title>Light mode</title>
|
||||||
@ -34,7 +34,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
style="enable-background='new 0 0 100 100'"
|
style="enable-background:new 0 0 100 100"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>Dark mode</title>
|
<title>Dark mode</title>
|
||||||
|
@ -12,6 +12,9 @@ const defaultOptions = {
|
|||||||
folderClickBehavior: "collapse",
|
folderClickBehavior: "collapse",
|
||||||
folderDefaultState: "collapsed",
|
folderDefaultState: "collapsed",
|
||||||
useSavedState: true,
|
useSavedState: true,
|
||||||
|
mapFn: (node) => {
|
||||||
|
return node
|
||||||
|
},
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
@ -22,6 +25,7 @@ const defaultOptions = {
|
|||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.file && !b.file) {
|
if (a.file && !b.file) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
@ -41,46 +45,34 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
let jsonTree: string
|
let jsonTree: string
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
if (!fileTree) {
|
if (fileTree) {
|
||||||
// Construct tree from allFiles
|
return
|
||||||
fileTree = new FileNode("")
|
}
|
||||||
allFiles.forEach((file) => fileTree.add(file, 1))
|
|
||||||
|
|
||||||
/**
|
// Construct tree from allFiles
|
||||||
* Keys of this object must match corresponding function name of `FileNode`,
|
fileTree = new FileNode("")
|
||||||
* while values must be the argument that will be passed to the function.
|
allFiles.forEach((file) => fileTree.add(file))
|
||||||
*
|
|
||||||
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
|
||||||
*/
|
|
||||||
const functions = {
|
|
||||||
map: opts.mapFn,
|
|
||||||
sort: opts.sortFn,
|
|
||||||
filter: opts.filterFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||||
if (opts.order) {
|
if (opts.order) {
|
||||||
// Order is important, use loop with index instead of order.map()
|
// Order is important, use loop with index instead of order.map()
|
||||||
for (let i = 0; i < opts.order.length; i++) {
|
for (let i = 0; i < opts.order.length; i++) {
|
||||||
const functionName = opts.order[i]
|
const functionName = opts.order[i]
|
||||||
if (functions[functionName]) {
|
if (functionName === "map") {
|
||||||
// for every entry in order, call matching function in FileNode and pass matching argument
|
fileTree.map(opts.mapFn)
|
||||||
// e.g. i = 0; functionName = "filter"
|
} else if (functionName === "sort") {
|
||||||
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
fileTree.sort(opts.sortFn)
|
||||||
|
} else if (functionName === "filter") {
|
||||||
// @ts-ignore
|
fileTree.filter(opts.filterFn)
|
||||||
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
|
||||||
fileTree[functionName].call(fileTree, functions[functionName])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all folders of tree. Initialize with collapsed state
|
|
||||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
|
||||||
|
|
||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
|
||||||
jsonTree = JSON.stringify(folders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all folders of tree. Initialize with collapsed state
|
||||||
|
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||||
|
|
||||||
|
// Stringify to pass json tree as data attribute ([data-tree])
|
||||||
|
jsonTree = JSON.stringify(folders)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||||
@ -120,6 +112,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = explorerStyle
|
Explorer.css = explorerStyle
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = script
|
||||||
return Explorer
|
return Explorer
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { resolveRelative } from "../util/path"
|
import {
|
||||||
|
joinSegments,
|
||||||
|
resolveRelative,
|
||||||
|
clone,
|
||||||
|
simplifySlug,
|
||||||
|
SimpleSlug,
|
||||||
|
FilePath,
|
||||||
|
} from "../util/path"
|
||||||
|
|
||||||
type OrderEntries = "sort" | "filter" | "map"
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
@ -10,9 +17,9 @@ export interface Options {
|
|||||||
folderClickBehavior: "collapse" | "link"
|
folderClickBehavior: "collapse" | "link"
|
||||||
useSavedState: boolean
|
useSavedState: boolean
|
||||||
sortFn: (a: FileNode, b: FileNode) => number
|
sortFn: (a: FileNode, b: FileNode) => number
|
||||||
filterFn?: (node: FileNode) => boolean
|
filterFn: (node: FileNode) => boolean
|
||||||
mapFn?: (node: FileNode) => void
|
mapFn: (node: FileNode) => void
|
||||||
order?: OrderEntries[]
|
order: OrderEntries[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataWrapper = {
|
type DataWrapper = {
|
||||||
@ -25,59 +32,74 @@ export type FolderState = {
|
|||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
|
||||||
|
if (!fp) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp.split("/").at(idx)
|
||||||
|
}
|
||||||
|
|
||||||
// Structure to add all files into a tree
|
// Structure to add all files into a tree
|
||||||
export class FileNode {
|
export class FileNode {
|
||||||
children: FileNode[]
|
children: Array<FileNode>
|
||||||
name: string
|
name: string // this is the slug segment
|
||||||
displayName: string
|
displayName: string
|
||||||
file: QuartzPluginData | null
|
file: QuartzPluginData | null
|
||||||
depth: number
|
depth: number
|
||||||
|
|
||||||
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
||||||
this.children = []
|
this.children = []
|
||||||
this.name = name
|
this.name = slugSegment
|
||||||
this.displayName = name
|
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
||||||
this.file = file ? structuredClone(file) : null
|
this.file = file ? clone(file) : null
|
||||||
this.depth = depth ?? 0
|
this.depth = depth ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private insert(file: DataWrapper) {
|
private insert(fileData: DataWrapper) {
|
||||||
if (file.path.length === 1) {
|
if (fileData.path.length === 0) {
|
||||||
if (file.path[0] !== "index.md") {
|
return
|
||||||
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
}
|
||||||
} else {
|
|
||||||
const title = file.file.frontmatter?.title
|
const nextSegment = fileData.path[0]
|
||||||
if (title && title !== "index" && file.path[0] === "index.md") {
|
|
||||||
|
// base case, insert here
|
||||||
|
if (fileData.path.length === 1) {
|
||||||
|
if (nextSegment === "") {
|
||||||
|
// index case (we are the root and we just found index.md), set our data appropriately
|
||||||
|
const title = fileData.file.frontmatter?.title
|
||||||
|
if (title && title !== "index") {
|
||||||
this.displayName = title
|
this.displayName = title
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
// direct child
|
||||||
const next = file.path[0]
|
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
||||||
file.path = file.path.splice(1)
|
|
||||||
for (const child of this.children) {
|
|
||||||
if (child.name === next) {
|
|
||||||
child.insert(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChild = new FileNode(next, undefined, this.depth + 1)
|
return
|
||||||
newChild.insert(file)
|
|
||||||
this.children.push(newChild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find the right child to insert into
|
||||||
|
fileData.path = fileData.path.splice(1)
|
||||||
|
const child = this.children.find((c) => c.name === nextSegment)
|
||||||
|
if (child) {
|
||||||
|
child.insert(fileData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChild = new FileNode(
|
||||||
|
nextSegment,
|
||||||
|
getPathSegment(fileData.file.relativePath, this.depth),
|
||||||
|
undefined,
|
||||||
|
this.depth + 1,
|
||||||
|
)
|
||||||
|
newChild.insert(fileData)
|
||||||
|
this.children.push(newChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new file to tree
|
// Add new file to tree
|
||||||
add(file: QuartzPluginData, splice: number = 0) {
|
add(file: QuartzPluginData) {
|
||||||
this.insert({ file, path: file.filePath!.split("/").splice(splice) })
|
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
|
||||||
}
|
|
||||||
|
|
||||||
// Print tree structure (for debugging)
|
|
||||||
print(depth: number = 0) {
|
|
||||||
let folderChar = ""
|
|
||||||
if (!this.file) folderChar = "|"
|
|
||||||
console.log("-".repeat(depth), folderChar, this.name, this.depth)
|
|
||||||
this.children.forEach((e) => e.print(depth + 1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,7 +117,6 @@ export class FileNode {
|
|||||||
*/
|
*/
|
||||||
map(mapFn: (node: FileNode) => void) {
|
map(mapFn: (node: FileNode) => void) {
|
||||||
mapFn(this)
|
mapFn(this)
|
||||||
|
|
||||||
this.children.forEach((child) => child.map(mapFn))
|
this.children.forEach((child) => child.map(mapFn))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,16 +131,16 @@ export class FileNode {
|
|||||||
|
|
||||||
const traverse = (node: FileNode, currentPath: string) => {
|
const traverse = (node: FileNode, currentPath: string) => {
|
||||||
if (!node.file) {
|
if (!node.file) {
|
||||||
const folderPath = currentPath + (currentPath ? "/" : "") + node.name
|
const folderPath = joinSegments(currentPath, node.name)
|
||||||
if (folderPath !== "") {
|
if (folderPath !== "") {
|
||||||
folderPaths.push({ path: folderPath, collapsed })
|
folderPaths.push({ path: folderPath, collapsed })
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children.forEach((child) => traverse(child, folderPath))
|
node.children.forEach((child) => traverse(child, folderPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(this, "")
|
traverse(this, "")
|
||||||
|
|
||||||
return folderPaths
|
return folderPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,14 +168,13 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||||
|
|
||||||
// Calculate current folderPath
|
// Calculate current folderPath
|
||||||
let pathOld = fullPath ? fullPath : ""
|
|
||||||
let folderPath = ""
|
let folderPath = ""
|
||||||
if (node.name !== "") {
|
if (node.name !== "") {
|
||||||
folderPath = `${pathOld}/${node.name}`
|
folderPath = joinSegments(fullPath ?? "", node.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<>
|
||||||
{node.file ? (
|
{node.file ? (
|
||||||
// Single file node
|
// Single file node
|
||||||
<li key={node.file.slug}>
|
<li key={node.file.slug}>
|
||||||
@ -163,7 +183,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<li>
|
||||||
{node.name !== "" && (
|
{node.name !== "" && (
|
||||||
// Node with entire folder
|
// Node with entire folder
|
||||||
// Render svg button + folder name, then children
|
// Render svg button + folder name, then children
|
||||||
@ -185,12 +205,16 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||||
<div key={node.name} data-folderpath={folderPath}>
|
<div key={node.name} data-folderpath={folderPath}>
|
||||||
{folderBehavior === "link" ? (
|
{folderBehavior === "link" ? (
|
||||||
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
<a
|
||||||
|
href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)}
|
||||||
|
data-for={node.name}
|
||||||
|
class="folder-title"
|
||||||
|
>
|
||||||
{node.displayName}
|
{node.displayName}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button class="folder-button">
|
<button class="folder-button">
|
||||||
<p class="folder-title">{node.displayName}</p>
|
<span class="folder-title">{node.displayName}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -217,8 +241,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
)}
|
)}
|
||||||
</li>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,12 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
? fileData.description
|
? fileData.description
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
if (tag === "") {
|
if (tag === "/") {
|
||||||
const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
|
const tags = [
|
||||||
|
...new Set(
|
||||||
|
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||||
|
),
|
||||||
|
].sort((a, b) => a.localeCompare(b))
|
||||||
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
|
@ -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, ElementContent } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@ -22,7 +23,7 @@ export function pageResources(
|
|||||||
staticResources: StaticResources,
|
staticResources: StaticResources,
|
||||||
): StaticResources {
|
): StaticResources {
|
||||||
const contentIndexPath = joinSegments(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: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
||||||
@ -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,30 +74,29 @@ 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 transcludeTarget = inner.properties?.["data-slug"] as FullSlug
|
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||||
|
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||||
// TODO: avoid this expensive find operation and construct an index ahead of time
|
|
||||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let blockRef = node.properties?.dataBlock as string | undefined
|
let blockRef = node.properties.dataBlock as string | undefined
|
||||||
if (blockRef?.startsWith("^")) {
|
if (blockRef?.startsWith("#^")) {
|
||||||
// block transclude
|
// block transclude
|
||||||
blockRef = blockRef.slice(1)
|
blockRef = blockRef.slice("#^".length)
|
||||||
let blockNode = page.blocks?.[blockRef]
|
let blockNode = page.blocks?.[blockRef]
|
||||||
if (blockNode) {
|
if (blockNode) {
|
||||||
if (blockNode.tagName === "li") {
|
if (blockNode.tagName === "li") {
|
||||||
blockNode = {
|
blockNode = {
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "ul",
|
tagName: "ul",
|
||||||
|
properties: {},
|
||||||
children: [blockNode],
|
children: [blockNode],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children = [
|
node.children = [
|
||||||
blockNode,
|
normalizeHastElement(blockNode, slug, transcludeTarget),
|
||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
@ -104,7 +116,7 @@ export function renderPage(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startIdx) {
|
if (startIdx !== undefined) {
|
||||||
endIdx = i
|
endIdx = i
|
||||||
} else if (el.properties?.id === blockRef) {
|
} else if (el.properties?.id === blockRef) {
|
||||||
startIdx = i
|
startIdx = i
|
||||||
@ -112,12 +124,14 @@ export function renderPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!startIdx) {
|
if (startIdx === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children = [
|
node.children = [
|
||||||
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]),
|
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
@ -131,11 +145,14 @@ export function renderPage(
|
|||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "h1",
|
tagName: "h1",
|
||||||
|
properties: {},
|
||||||
children: [
|
children: [
|
||||||
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...(page.htmlAst.children as ElementContent[]),
|
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
|
@ -59,8 +59,7 @@ function toggleFolder(evt: MouseEvent) {
|
|||||||
// Save folder state to localStorage
|
// Save folder state to localStorage
|
||||||
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
|
|
||||||
// Remove leading "/"
|
const fullFolderPath = clickFolderPath
|
||||||
const fullFolderPath = clickFolderPath.substring(1)
|
|
||||||
toggleCollapsedByPath(explorerState, fullFolderPath)
|
toggleCollapsedByPath(explorerState, fullFolderPath)
|
||||||
|
|
||||||
const stringifiedFileTree = JSON.stringify(explorerState)
|
const stringifiedFileTree = JSON.stringify(explorerState)
|
||||||
@ -108,9 +107,7 @@ function setupExplorer() {
|
|||||||
explorerState = JSON.parse(storageTree)
|
explorerState = JSON.parse(storageTree)
|
||||||
explorerState.map((folderUl) => {
|
explorerState.map((folderUl) => {
|
||||||
// grab <li> element for matching folder path
|
// grab <li> element for matching folder path
|
||||||
const folderLi = document.querySelector(
|
const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
|
||||||
`[data-folderpath='/${folderUl.path}']`,
|
|
||||||
) as HTMLElement
|
|
||||||
|
|
||||||
// Get corresponding content <ul> tag and set state
|
// Get corresponding content <ul> tag and set state
|
||||||
if (folderLi) {
|
if (folderLi) {
|
||||||
@ -120,9 +117,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 +127,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,3 +0,0 @@
|
|||||||
import Plausible from "plausible-tracker"
|
|
||||||
const { trackPageview } = Plausible()
|
|
||||||
document.addEventListener("nav", () => trackPageview())
|
|
@ -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(
|
||||||
@ -18,6 +7,10 @@ async function mouseEnterHandler(
|
|||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
) {
|
) {
|
||||||
const link = this
|
const link = this
|
||||||
|
if (link.dataset.noPopover === "true") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async function setPosition(popoverElement: HTMLElement) {
|
async function setPosition(popoverElement: HTMLElement) {
|
||||||
const { x, y } = await computePosition(link, popoverElement, {
|
const { x, y } = await computePosition(link, popoverElement, {
|
||||||
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
|
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
|
||||||
@ -43,8 +36,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())
|
||||||
|
@ -1,10 +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"
|
||||||
import { normalizeRelativeURLs } from "./popover.inline"
|
|
||||||
|
|
||||||
// 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 =>
|
||||||
@ -45,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)
|
||||||
})
|
})
|
||||||
@ -109,6 +114,7 @@ function createRouter() {
|
|||||||
if (isSamePage(url) && url.hash) {
|
if (isSamePage(url) && url.hash) {
|
||||||
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||||
el?.scrollIntoView()
|
el?.scrollIntoView()
|
||||||
|
history.pushState({}, "", url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
float: right;
|
float: right;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
margin: -0.2rem 0.3rem;
|
margin: 0.3rem;
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
border-color: var(--dark);
|
border-color: var(--dark);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
|
@ -106,7 +106,7 @@ svg {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
|
|
||||||
& p {
|
& span {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
|
@ -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;
|
||||||
|
@ -9,7 +9,7 @@ export type QuartzComponentProps = {
|
|||||||
fileData: QuartzPluginData
|
fileData: QuartzPluginData
|
||||||
cfg: GlobalConfiguration
|
cfg: GlobalConfiguration
|
||||||
children: (QuartzComponent | JSX.Element)[]
|
children: (QuartzComponent | JSX.Element)[]
|
||||||
tree: Node<QuartzPluginData>
|
tree: Node
|
||||||
allFiles: QuartzPluginData[]
|
allFiles: QuartzPluginData[]
|
||||||
displayClass?: "mobile-only" | "desktop-only"
|
displayClass?: "mobile-only" | "desktop-only"
|
||||||
} & JSX.IntrinsicAttributes & {
|
} & JSX.IntrinsicAttributes & {
|
||||||
|
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[]
|
||||||
|
},
|
||||||
|
})
|
@ -4,8 +4,6 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import spaRouterScript from "../../components/scripts/spa.inline"
|
import spaRouterScript from "../../components/scripts/spa.inline"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import plausibleScript from "../../components/scripts/plausible.inline"
|
|
||||||
// @ts-ignore
|
|
||||||
import popoverScript from "../../components/scripts/popover.inline"
|
import popoverScript from "../../components/scripts/popover.inline"
|
||||||
import styles from "../../styles/custom.scss"
|
import styles from "../../styles/custom.scss"
|
||||||
import popoverStyle from "../../components/styles/popover.scss"
|
import popoverStyle from "../../components/styles/popover.scss"
|
||||||
@ -14,6 +12,7 @@ import { StaticResources } from "../../util/resources"
|
|||||||
import { QuartzComponent } from "../../components/types"
|
import { QuartzComponent } from "../../components/types"
|
||||||
import { googleFontHref, joinStyles } from "../../util/theme"
|
import { googleFontHref, joinStyles } from "../../util/theme"
|
||||||
import { Features, transform } from "lightningcss"
|
import { Features, transform } from "lightningcss"
|
||||||
|
import { transform as transpile } from "esbuild"
|
||||||
|
|
||||||
type ComponentResources = {
|
type ComponentResources = {
|
||||||
css: string[]
|
css: string[]
|
||||||
@ -56,9 +55,16 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinScripts(scripts: string[]): string {
|
async function joinScripts(scripts: string[]): Promise<string> {
|
||||||
// wrap with iife to prevent scope collision
|
// wrap with iife to prevent scope collision
|
||||||
return scripts.map((script) => `(function () {${script}})();`).join("\n")
|
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
|
||||||
|
|
||||||
|
// minify with esbuild
|
||||||
|
const res = await transpile(script, {
|
||||||
|
minify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.code
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGlobalPageResources(
|
function addGlobalPageResources(
|
||||||
@ -85,17 +91,30 @@ function addGlobalPageResources(
|
|||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag() { dataLayer.push(arguments); }
|
function gtag() { dataLayer.push(arguments); }
|
||||||
gtag(\`js\`, new Date());
|
gtag("js", new Date());
|
||||||
gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
|
gtag("config", "${tagId}", { send_page_view: false });
|
||||||
|
|
||||||
document.addEventListener(\`nav\`, () => {
|
document.addEventListener("nav", () => {
|
||||||
gtag(\`event\`, \`page_view\`, {
|
gtag("event", "page_view", {
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
page_location: location.href,
|
page_location: location.href,
|
||||||
});
|
});
|
||||||
});`)
|
});`)
|
||||||
} else if (cfg.analytics?.provider === "plausible") {
|
} else if (cfg.analytics?.provider === "plausible") {
|
||||||
componentResources.afterDOMLoaded.push(plausibleScript)
|
const plausibleHost = cfg.analytics.host ?? "https://plausible.io"
|
||||||
|
componentResources.afterDOMLoaded.push(`
|
||||||
|
const plausibleScript = document.createElement("script")
|
||||||
|
plausibleScript.src = "${plausibleHost}/js/script.manual.js"
|
||||||
|
plausibleScript.setAttribute("data-domain", location.hostname)
|
||||||
|
plausibleScript.defer = true
|
||||||
|
document.head.appendChild(plausibleScript)
|
||||||
|
|
||||||
|
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
|
||||||
|
|
||||||
|
document.addEventListener("nav", () => {
|
||||||
|
plausible("pageview")
|
||||||
|
})
|
||||||
|
`)
|
||||||
} else if (cfg.analytics?.provider === "umami") {
|
} else if (cfg.analytics?.provider === "umami") {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
const umamiScript = document.createElement("script")
|
const umamiScript = document.createElement("script")
|
||||||
@ -165,8 +184,11 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
|
|||||||
addGlobalPageResources(ctx, resources, componentResources)
|
addGlobalPageResources(ctx, resources, componentResources)
|
||||||
|
|
||||||
const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
|
const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
|
||||||
const prescript = joinScripts(componentResources.beforeDOMLoaded)
|
const [prescript, postscript] = await Promise.all([
|
||||||
const postscript = joinScripts(componentResources.afterDOMLoaded)
|
joinScripts(componentResources.beforeDOMLoaded),
|
||||||
|
joinScripts(componentResources.afterDOMLoaded),
|
||||||
|
])
|
||||||
|
|
||||||
const fps = await Promise.all([
|
const fps = await Promise.all([
|
||||||
emit({
|
emit({
|
||||||
slug: "index" as FullSlug,
|
slug: "index" as FullSlug,
|
||||||
|
@ -2,7 +2,7 @@ import { Root } from "hast"
|
|||||||
import { GlobalConfiguration } from "../../cfg"
|
import { GlobalConfiguration } from "../../cfg"
|
||||||
import { getDate } from "../../components/Date"
|
import { getDate } from "../../components/Date"
|
||||||
import { escapeHTML } from "../../util/escape"
|
import { escapeHTML } from "../../util/escape"
|
||||||
import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@ -37,7 +37,7 @@ 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: SimpleSlug, content: ContentDetails): string => `<url>
|
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
|
||||||
<loc>https://${base}/${encodeURI(slug)}</loc>
|
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
|
||||||
<lastmod>${content.date?.toISOString()}</lastmod>
|
<lastmod>${content.date?.toISOString()}</lastmod>
|
||||||
</url>`
|
</url>`
|
||||||
const urls = Array.from(idx)
|
const urls = Array.from(idx)
|
||||||
@ -52,8 +52,8 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
|
|
||||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
||||||
<title>${escapeHTML(content.title)}</title>
|
<title>${escapeHTML(content.title)}</title>
|
||||||
<link>${root}/${encodeURI(slug)}</link>
|
<link>${joinSegments(root, encodeURI(slug))}</link>
|
||||||
<guid>${root}/${encodeURI(slug)}</guid>
|
<guid>${joinSegments(root, encodeURI(slug))}</guid>
|
||||||
<description>${content.richContent ?? content.description}</description>
|
<description>${content.richContent ?? content.description}</description>
|
||||||
<pubDate>${content.date?.toUTCString()}</pubDate>
|
<pubDate>${content.date?.toUTCString()}</pubDate>
|
||||||
</item>`
|
</item>`
|
||||||
|
@ -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[]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -40,12 +40,13 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
const tags: Set<string> = new Set(
|
const tags: Set<string> = new Set(
|
||||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||||
)
|
)
|
||||||
|
|
||||||
// add base tag
|
// add base tag
|
||||||
tags.add("index")
|
tags.add("index")
|
||||||
|
|
||||||
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||||
[...tags].map((tag) => {
|
[...tags].map((tag) => {
|
||||||
const title = tag === "" ? "Tag Index" : `Tag: #${tag}`
|
const title = tag === "index" ? "Tag Index" : `Tag: #${tag}`
|
||||||
return [
|
return [
|
||||||
tag,
|
tag,
|
||||||
defaultProcessedContent({
|
defaultProcessedContent({
|
||||||
|
@ -30,5 +30,6 @@ declare module "vfile" {
|
|||||||
interface DataMap {
|
interface DataMap {
|
||||||
slug: FullSlug
|
slug: FullSlug
|
||||||
filePath: FilePath
|
filePath: FilePath
|
||||||
|
relativePath: FilePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,18 @@ import { QuartzTransformerPlugin } from "../types"
|
|||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
import toml from "toml"
|
import toml from "toml"
|
||||||
import { slugTag } from "../../util/path"
|
import { slugTag } from "../../util/path"
|
||||||
|
import { QuartzPluginData } from "../vfile"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
delims: string | string[]
|
delims: string | string[]
|
||||||
language: "yaml" | "toml"
|
language: "yaml" | "toml"
|
||||||
|
oneLineTagDelim: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
delims: "---",
|
delims: "---",
|
||||||
language: "yaml",
|
language: "yaml",
|
||||||
|
oneLineTagDelim: ",",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@ -20,11 +23,13 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
return {
|
return {
|
||||||
name: "FrontMatter",
|
name: "FrontMatter",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
|
const { oneLineTagDelim } = opts
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[remarkFrontmatter, ["yaml", "toml"]],
|
[remarkFrontmatter, ["yaml", "toml"]],
|
||||||
() => {
|
() => {
|
||||||
return (_, file) => {
|
return (_, file) => {
|
||||||
const { data } = matter(file.value, {
|
const { data } = matter(Buffer.from(file.value), {
|
||||||
...opts,
|
...opts,
|
||||||
engines: {
|
engines: {
|
||||||
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
||||||
@ -40,24 +45,30 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
// coerce title to string
|
// coerce title to string
|
||||||
if (data.title) {
|
if (data.title) {
|
||||||
data.title = data.title.toString()
|
data.title = data.title.toString()
|
||||||
|
} else if (data.title === null || data.title === undefined) {
|
||||||
|
data.title = file.stem ?? "Untitled"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.tags && !Array.isArray(data.tags)) {
|
if (data.tags) {
|
||||||
|
// coerce to array
|
||||||
|
if (!Array.isArray(data.tags)) {
|
||||||
|
data.tags = data.tags
|
||||||
|
.toString()
|
||||||
|
.split(oneLineTagDelim)
|
||||||
|
.map((tag: string) => tag.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all non-string tags
|
||||||
data.tags = data.tags
|
data.tags = data.tags
|
||||||
.toString()
|
.filter((tag: unknown) => typeof tag === "string" || typeof tag === "number")
|
||||||
.split(",")
|
.map((tag: string | number) => tag.toString())
|
||||||
.map((tag: string) => tag.trim())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// slug them all!!
|
// slug them all!!
|
||||||
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))] ?? []
|
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))]
|
||||||
|
|
||||||
// fill in frontmatter
|
// fill in frontmatter
|
||||||
file.data.frontmatter = {
|
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
||||||
title: file.stem ?? "Untitled",
|
|
||||||
tags: [],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -31,6 +31,11 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
|||||||
rehypeAutolinkHeadings,
|
rehypeAutolinkHeadings,
|
||||||
{
|
{
|
||||||
behavior: "append",
|
behavior: "append",
|
||||||
|
properties: {
|
||||||
|
ariaHidden: true,
|
||||||
|
tabIndex: -1,
|
||||||
|
"data-no-popover": true,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "text",
|
type: "text",
|
||||||
value: " §",
|
value: " §",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
import rehypeKatex from "rehype-katex"
|
import rehypeKatex from "rehype-katex"
|
||||||
import rehypeMathjax from "rehype-mathjax/svg.js"
|
import rehypeMathjax from "rehype-mathjax/svg"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import isAbsoluteUrl from "is-absolute-url"
|
import isAbsoluteUrl from "is-absolute-url"
|
||||||
|
import { Root } from "hast"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
/** How to resolve Markdown paths */
|
/** How to resolve Markdown paths */
|
||||||
@ -19,12 +20,14 @@ interface Options {
|
|||||||
/** Strips folders from a link so that it looks nice */
|
/** Strips folders from a link so that it looks nice */
|
||||||
prettyLinks: boolean
|
prettyLinks: boolean
|
||||||
openLinksInNewTab: boolean
|
openLinksInNewTab: boolean
|
||||||
|
lazyLoad: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
markdownLinkResolution: "absolute",
|
markdownLinkResolution: "absolute",
|
||||||
prettyLinks: true,
|
prettyLinks: true,
|
||||||
openLinksInNewTab: false,
|
openLinksInNewTab: false,
|
||||||
|
lazyLoad: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@ -34,7 +37,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
htmlPlugins(ctx) {
|
htmlPlugins(ctx) {
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
return (tree, file) => {
|
return (tree: Root, file) => {
|
||||||
const curSlug = simplifySlug(file.data.slug!)
|
const curSlug = simplifySlug(file.data.slug!)
|
||||||
const outgoing: Set<SimpleSlug> = new Set()
|
const outgoing: Set<SimpleSlug> = new Set()
|
||||||
|
|
||||||
@ -51,8 +54,19 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
typeof node.properties.href === "string"
|
typeof node.properties.href === "string"
|
||||||
) {
|
) {
|
||||||
let dest = node.properties.href as RelativeURL
|
let dest = node.properties.href as RelativeURL
|
||||||
node.properties.className ??= []
|
const classes = (node.properties.className ?? []) as string[]
|
||||||
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
classes.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
|
||||||
|
classes.push("alias")
|
||||||
|
}
|
||||||
|
node.properties.className = classes
|
||||||
|
|
||||||
if (opts.openLinksInNewTab) {
|
if (opts.openLinksInNewTab) {
|
||||||
node.properties.target = "_blank"
|
node.properties.target = "_blank"
|
||||||
@ -71,14 +85,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
|
||||||
@ -99,6 +115,10 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
node.properties &&
|
node.properties &&
|
||||||
typeof node.properties.src === "string"
|
typeof node.properties.src === "string"
|
||||||
) {
|
) {
|
||||||
|
if (opts.lazyLoad) {
|
||||||
|
node.properties.loading = "lazy"
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
dest = node.properties.src = transformLink(
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
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, Paragraph, Code } from "mdast"
|
||||||
import { Element, Literal, Root as HtmlRoot } from "hast"
|
import { Element, Literal, Root as HtmlRoot } from "hast"
|
||||||
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
import { ReplaceFunction, 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"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
@ -15,6 +14,7 @@ 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"
|
||||||
import { capitalize } from "../../util/lang"
|
import { capitalize } from "../../util/lang"
|
||||||
|
import { PluggableList } from "unified"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
comments: boolean
|
comments: boolean
|
||||||
@ -105,12 +105,17 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
|||||||
return calloutMapping[callout] ?? "note"
|
return calloutMapping[callout] ?? "note"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const externalLinkRegex = /^https?:\/\//i
|
||||||
|
|
||||||
// !? -> optional embedding
|
// !? -> optional embedding
|
||||||
// \[\[ -> open brace
|
// \[\[ -> open brace
|
||||||
// ([^\[\]\|\#]+) -> 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
|
||||||
@ -118,8 +123,8 @@ const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
|||||||
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
||||||
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
|
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
|
||||||
// #(...) -> capturing group, tag itself must start with #
|
// #(...) -> capturing group, tag itself must start with #
|
||||||
// (?:[-_\p{L}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores
|
// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
|
||||||
// (?:\/[-_\p{L}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
|
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
|
||||||
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
|
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
|
||||||
const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
|
const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
|
||||||
|
|
||||||
@ -132,39 +137,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
const hast = toHast(ast, { allowDangerousHtml: true })!
|
const hast = toHast(ast, { allowDangerousHtml: true })!
|
||||||
return toHtml(hast, { allowDangerousHtml: true })
|
return toHtml(hast, { allowDangerousHtml: true })
|
||||||
}
|
}
|
||||||
const findAndReplace = opts.enableInHtmlEmbed
|
|
||||||
? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => {
|
|
||||||
if (replace) {
|
|
||||||
visit(tree, "html", (node: HTML) => {
|
|
||||||
if (typeof replace === "string") {
|
|
||||||
node.value = node.value.replace(regex, replace)
|
|
||||||
} else {
|
|
||||||
node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
|
|
||||||
const replaceValue = replace(substring, ...args)
|
|
||||||
if (typeof replaceValue === "string") {
|
|
||||||
return replaceValue
|
|
||||||
} else if (Array.isArray(replaceValue)) {
|
|
||||||
return replaceValue.map(mdastToHtml).join("")
|
|
||||||
} else if (typeof replaceValue === "object" && replaceValue !== null) {
|
|
||||||
return mdastToHtml(replaceValue)
|
|
||||||
} else {
|
|
||||||
return substring
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
mdastFindReplace(tree, regex, replace)
|
|
||||||
}
|
|
||||||
: mdastFindReplace
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "ObsidianFlavoredMarkdown",
|
name: "ObsidianFlavoredMarkdown",
|
||||||
textTransform(_ctx, src) {
|
textTransform(_ctx, src) {
|
||||||
// pre-transform blockquotes
|
// pre-transform blockquotes
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
src = src.toString()
|
if (src instanceof Buffer) {
|
||||||
|
src = src.toString()
|
||||||
|
}
|
||||||
|
|
||||||
src = src.replaceAll(calloutLineRegex, (value) => {
|
src = src.replaceAll(calloutLineRegex, (value) => {
|
||||||
// force newline after title of callout
|
// force newline after title of callout
|
||||||
return value + "\n> "
|
return value + "\n> "
|
||||||
@ -173,14 +155,24 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
|
|
||||||
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
|
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
|
||||||
if (opts.wikilinks) {
|
if (opts.wikilinks) {
|
||||||
src = src.toString()
|
if (src instanceof Buffer) {
|
||||||
|
src = src.toString()
|
||||||
|
}
|
||||||
|
|
||||||
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
||||||
const [rawFp, rawHeader, rawAlias] = capture
|
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = 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("!") ? "!" : ""
|
||||||
|
|
||||||
|
if (rawFp?.match(externalLinkRegex)) {
|
||||||
|
return `${embedDisplay}[${displayAlias.replace(/^\|/, "")}](${rawFp})`
|
||||||
|
}
|
||||||
|
|
||||||
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -189,108 +181,172 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
},
|
},
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
const plugins: PluggableList = []
|
const plugins: PluggableList = []
|
||||||
if (opts.wikilinks) {
|
|
||||||
plugins.push(() => {
|
|
||||||
return (tree: Root, _file) => {
|
|
||||||
findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => {
|
|
||||||
let [rawFp, rawHeader, rawAlias] = capture
|
|
||||||
const fp = rawFp?.trim() ?? ""
|
|
||||||
const anchor = rawHeader?.trim() ?? ""
|
|
||||||
const alias = rawAlias?.slice(1).trim()
|
|
||||||
|
|
||||||
// embed cases
|
// regex replacements
|
||||||
if (value.startsWith("!")) {
|
plugins.push(() => {
|
||||||
const ext: string = path.extname(fp).toLowerCase()
|
return (tree: Root, file) => {
|
||||||
const url = slugifyFilePath(fp as FilePath)
|
const replacements: [RegExp, string | ReplaceFunction][] = []
|
||||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
|
const base = pathToRoot(file.data.slug!)
|
||||||
const dims = alias ?? ""
|
|
||||||
let [width, height] = dims.split("x", 2)
|
if (opts.wikilinks) {
|
||||||
width ||= "auto"
|
replacements.push([
|
||||||
height ||= "auto"
|
wikilinkRegex,
|
||||||
return {
|
(value: string, ...capture: string[]) => {
|
||||||
type: "image",
|
let [rawFp, rawHeader, rawAlias] = capture
|
||||||
url,
|
const fp = rawFp?.trim() ?? ""
|
||||||
data: {
|
const anchor = rawHeader?.trim() ?? ""
|
||||||
hProperties: {
|
const alias = rawAlias?.slice(1).trim()
|
||||||
width,
|
|
||||||
height,
|
// embed cases
|
||||||
|
if (value.startsWith("!")) {
|
||||||
|
const ext: string = path.extname(fp).toLowerCase()
|
||||||
|
const url = slugifyFilePath(fp as FilePath)
|
||||||
|
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
|
||||||
|
const dims = alias ?? ""
|
||||||
|
let [width, height] = dims.split("x", 2)
|
||||||
|
width ||= "auto"
|
||||||
|
height ||= "auto"
|
||||||
|
return {
|
||||||
|
type: "image",
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
hProperties: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
|
||||||
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
|
return {
|
||||||
return {
|
type: "html",
|
||||||
type: "html",
|
value: `<video src="${url}" controls></video>`,
|
||||||
value: `<video src="${url}" controls></video>`,
|
}
|
||||||
}
|
} else if (
|
||||||
} else if (
|
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
|
||||||
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
|
) {
|
||||||
) {
|
return {
|
||||||
return {
|
type: "html",
|
||||||
type: "html",
|
value: `<audio src="${url}" controls></audio>`,
|
||||||
value: `<audio src="${url}" controls></audio>`,
|
}
|
||||||
}
|
} else if ([".pdf"].includes(ext)) {
|
||||||
} else if ([".pdf"].includes(ext)) {
|
return {
|
||||||
return {
|
type: "html",
|
||||||
type: "html",
|
value: `<iframe src="${url}"></iframe>`,
|
||||||
value: `<iframe src="${url}"></iframe>`,
|
}
|
||||||
}
|
} else if (ext === "") {
|
||||||
} else if (ext === "") {
|
const block = anchor
|
||||||
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 ${url}${block}</a></blockquote>`,
|
||||||
}" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// otherwise, fall through to regular link
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, fall through to regular link
|
// internal link
|
||||||
}
|
const url = fp + anchor
|
||||||
|
return {
|
||||||
|
type: "link",
|
||||||
|
url,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
value: alias ?? fp,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
// internal link
|
if (opts.highlight) {
|
||||||
const url = fp + anchor
|
replacements.push([
|
||||||
return {
|
highlightRegex,
|
||||||
type: "link",
|
(_value: string, ...capture: string[]) => {
|
||||||
url,
|
const [inner] = capture
|
||||||
children: [
|
return {
|
||||||
{
|
type: "html",
|
||||||
type: "text",
|
value: `<span class="text-highlight">${inner}</span>`,
|
||||||
value: alias ?? fp,
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.comments) {
|
||||||
|
replacements.push([
|
||||||
|
commentRegex,
|
||||||
|
(_value: string, ..._capture: string[]) => {
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
value: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.parseTags) {
|
||||||
|
replacements.push([
|
||||||
|
tagRegex,
|
||||||
|
(_value: string, tag: string) => {
|
||||||
|
// Check if the tag only includes numbers
|
||||||
|
if (/^\d+$/.test(tag)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = slugTag(tag)
|
||||||
|
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
||||||
|
file.data.frontmatter.tags.push(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "link",
|
||||||
|
url: base + `/tags/${tag}`,
|
||||||
|
data: {
|
||||||
|
hProperties: {
|
||||||
|
className: ["tag-link"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
children: [
|
||||||
}
|
{
|
||||||
})
|
type: "text",
|
||||||
|
value: `#${tag}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.highlight) {
|
if (opts.enableInHtmlEmbed) {
|
||||||
plugins.push(() => {
|
visit(tree, "html", (node: Html) => {
|
||||||
return (tree: Root, _file) => {
|
for (const [regex, replace] of replacements) {
|
||||||
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
if (typeof replace === "string") {
|
||||||
const [inner] = capture
|
node.value = node.value.replace(regex, replace)
|
||||||
return {
|
} else {
|
||||||
type: "html",
|
node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
|
||||||
value: `<span class="text-highlight">${inner}</span>`,
|
const replaceValue = replace(substring, ...args)
|
||||||
|
if (typeof replaceValue === "string") {
|
||||||
|
return replaceValue
|
||||||
|
} else if (Array.isArray(replaceValue)) {
|
||||||
|
return replaceValue.map(mdastToHtml).join("")
|
||||||
|
} else if (typeof replaceValue === "object" && replaceValue !== null) {
|
||||||
|
return mdastToHtml(replaceValue)
|
||||||
|
} else {
|
||||||
|
return substring
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.comments) {
|
mdastFindReplace(tree, replacements)
|
||||||
plugins.push(() => {
|
}
|
||||||
return (tree: Root, _file) => {
|
})
|
||||||
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
|
|
||||||
return {
|
|
||||||
type: "text",
|
|
||||||
value: "",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
@ -331,7 +387,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
const titleHtml: HTML = {
|
const titleHtml: Html = {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<div
|
value: `<div
|
||||||
class="callout-title"
|
class="callout-title"
|
||||||
@ -391,51 +447,17 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.parseTags) {
|
|
||||||
plugins.push(() => {
|
|
||||||
return (tree: Root, file) => {
|
|
||||||
const base = pathToRoot(file.data.slug!)
|
|
||||||
findAndReplace(tree, tagRegex, (_value: string, tag: string) => {
|
|
||||||
// Check if the tag only includes numbers
|
|
||||||
if (/^\d+$/.test(tag)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tag = slugTag(tag)
|
|
||||||
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
|
||||||
file.data.frontmatter.tags.push(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "link",
|
|
||||||
url: base + `/tags/${tag}`,
|
|
||||||
data: {
|
|
||||||
hProperties: {
|
|
||||||
className: ["tag-link"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
value: `#${tag}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return plugins
|
return plugins
|
||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
const plugins = [rehypeRaw]
|
const plugins: PluggableList = [rehypeRaw]
|
||||||
|
|
||||||
if (opts.parseBlockReferences) {
|
if (opts.parseBlockReferences) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
const inlineTagTypes = new Set(["p", "li"])
|
const inlineTagTypes = new Set(["p", "li"])
|
||||||
const blockTagTypes = new Set(["blockquote"])
|
const blockTagTypes = new Set(["blockquote"])
|
||||||
return (tree, file) => {
|
return (tree: HtmlRoot, file) => {
|
||||||
file.data.blocks = {}
|
file.data.blocks = {}
|
||||||
file.data.htmlAst = tree
|
|
||||||
|
|
||||||
visit(tree, "element", (node, index, parent) => {
|
visit(tree, "element", (node, index, parent) => {
|
||||||
if (blockTagTypes.has(node.tagName)) {
|
if (blockTagTypes.has(node.tagName)) {
|
||||||
@ -477,6 +499,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
file.data.htmlAst = tree
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,11 @@ export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
|
|||||||
[
|
[
|
||||||
rehypePrettyCode,
|
rehypePrettyCode,
|
||||||
{
|
{
|
||||||
theme: "css-variables",
|
keepBackground: false,
|
||||||
|
theme: {
|
||||||
|
dark: "github-dark",
|
||||||
|
light: "github-light",
|
||||||
|
},
|
||||||
} satisfies Partial<CodeOptions>,
|
} satisfies Partial<CodeOptions>,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
@ -3,6 +3,7 @@ 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
|
||||||
@ -24,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,
|
||||||
) => {
|
) => {
|
||||||
@ -41,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,
|
||||||
|
@ -2,7 +2,7 @@ import { Node, Parent } from "hast"
|
|||||||
import { Data, VFile } from "vfile"
|
import { Data, VFile } from "vfile"
|
||||||
|
|
||||||
export type QuartzPluginData = Data
|
export type QuartzPluginData = Data
|
||||||
export type ProcessedContent = [Node<QuartzPluginData>, VFile]
|
export type ProcessedContent = [Node, VFile]
|
||||||
|
|
||||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
||||||
const root: Parent = { type: "root", children: [] }
|
const root: Parent = { type: "root", children: [] }
|
||||||
|
@ -14,27 +14,25 @@ import { QuartzLogger } from "../util/log"
|
|||||||
import { trace } from "../util/trace"
|
import { trace } from "../util/trace"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
|
||||||
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
export type QuartzProcessor = Processor<MDRoot, MDRoot, HTMLRoot>
|
||||||
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
||||||
const transformers = ctx.cfg.plugins.transformers
|
const transformers = ctx.cfg.plugins.transformers
|
||||||
|
|
||||||
// base Markdown -> MD AST
|
return (
|
||||||
let processor = unified().use(remarkParse)
|
unified()
|
||||||
|
// base Markdown -> MD AST
|
||||||
// MD AST -> MD AST transforms
|
.use(remarkParse)
|
||||||
for (const plugin of transformers.filter((p) => p.markdownPlugins)) {
|
// MD AST -> MD AST transforms
|
||||||
processor = processor.use(plugin.markdownPlugins!(ctx))
|
.use(
|
||||||
}
|
transformers
|
||||||
|
.filter((p) => p.markdownPlugins)
|
||||||
// MD AST -> HTML AST
|
.flatMap((plugin) => plugin.markdownPlugins!(ctx)),
|
||||||
processor = processor.use(remarkRehype, { allowDangerousHtml: true })
|
)
|
||||||
|
// MD AST -> HTML AST
|
||||||
// HTML AST -> HTML AST transforms
|
.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
for (const plugin of transformers.filter((p) => p.htmlPlugins)) {
|
// HTML AST -> HTML AST transforms
|
||||||
processor = processor.use(plugin.htmlPlugins!(ctx))
|
.use(transformers.filter((p) => p.htmlPlugins).flatMap((plugin) => plugin.htmlPlugins!(ctx)))
|
||||||
}
|
)
|
||||||
|
|
||||||
return processor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function* chunks<T>(arr: T[], n: number) {
|
function* chunks<T>(arr: T[], n: number) {
|
||||||
@ -89,12 +87,13 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
|||||||
|
|
||||||
// Text -> Text transforms
|
// Text -> Text transforms
|
||||||
for (const plugin of cfg.plugins.transformers.filter((p) => p.textTransform)) {
|
for (const plugin of cfg.plugins.transformers.filter((p) => p.textTransform)) {
|
||||||
file.value = plugin.textTransform!(ctx, file.value)
|
file.value = plugin.textTransform!(ctx, file.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// base data properties that plugins may use
|
// base data properties that plugins may use
|
||||||
file.data.slug = slugifyFilePath(path.posix.relative(argv.directory, file.path) as FilePath)
|
file.data.filePath = file.path as FilePath
|
||||||
file.data.filePath = fp
|
file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath
|
||||||
|
file.data.slug = slugifyFilePath(file.data.relativePath)
|
||||||
|
|
||||||
const ast = processor.parse(file)
|
const ast = processor.parse(file)
|
||||||
const newAst = await processor.run(ast, file)
|
const newAst = await processor.run(ast, file)
|
||||||
|
@ -63,11 +63,17 @@ a {
|
|||||||
color: var(--tertiary) !important;
|
color: var(--tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.internal:not(:has(> img)) {
|
&.internal {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&:has(> img) {
|
||||||
|
background-color: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,8 +99,6 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& article {
|
& article {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& > h1 {
|
& > h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
@ -299,11 +303,13 @@ h6 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[data-rehype-pretty-code-fragment] {
|
figure[data-rehype-pretty-code-figure] {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
line-height: 1.6rem;
|
line-height: 1.6rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > div[data-rehype-pretty-code-title] {
|
& > [data-rehype-pretty-code-title] {
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
@ -315,13 +321,13 @@ div[data-rehype-pretty-code-fragment] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > pre {
|
& > pre {
|
||||||
padding: 0.5rem 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
padding: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
@ -337,6 +343,7 @@ pre {
|
|||||||
counter-reset: line;
|
counter-reset: line;
|
||||||
counter-increment: line 0;
|
counter-increment: line 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
& [data-highlighted-chars] {
|
& [data-highlighted-chars] {
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
|
@ -1,29 +1,17 @@
|
|||||||
// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-light.json
|
code[data-theme*=" "] {
|
||||||
:root {
|
color: var(--shiki-light);
|
||||||
--shiki-color-text: #24292e;
|
background-color: var(--shiki-light-bg);
|
||||||
--shiki-color-background: #f8f8f8;
|
|
||||||
--shiki-token-constant: #005cc5;
|
|
||||||
--shiki-token-string: #032f62;
|
|
||||||
--shiki-token-comment: #6a737d;
|
|
||||||
--shiki-token-keyword: #d73a49;
|
|
||||||
--shiki-token-parameter: #24292e;
|
|
||||||
--shiki-token-function: #24292e;
|
|
||||||
--shiki-token-string-expression: #22863a;
|
|
||||||
--shiki-token-punctuation: #24292e;
|
|
||||||
--shiki-token-link: #24292e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-dark.json
|
code[data-theme*=" "] span {
|
||||||
[saved-theme="dark"] {
|
color: var(--shiki-light);
|
||||||
--shiki-color-text: #e1e4e8 !important;
|
}
|
||||||
--shiki-color-background: #24292e !important;
|
|
||||||
--shiki-token-constant: #79b8ff !important;
|
[saved-theme="dark"] code[data-theme*=" "] {
|
||||||
--shiki-token-string: #9ecbff !important;
|
color: var(--shiki-dark);
|
||||||
--shiki-token-comment: #6a737d !important;
|
background-color: var(--shiki-dark-bg);
|
||||||
--shiki-token-keyword: #f97583 !important;
|
}
|
||||||
--shiki-token-parameter: #e1e4e8 !important;
|
|
||||||
--shiki-token-function: #e1e4e8 !important;
|
[saved-theme="dark"] code[data-theme*=" "] span {
|
||||||
--shiki-token-string-expression: #85e89d !important;
|
color: var(--shiki-dark);
|
||||||
--shiki-token-punctuation: #e1e4e8 !important;
|
|
||||||
--shiki-token-link: #e1e4e8 !important;
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import { Node, Root } from "hast"
|
import { Node, Root } from "hast"
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
import { trace } from "./trace"
|
import { trace } from "./trace"
|
||||||
@ -13,7 +12,7 @@ const customComponents: Components = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
export function htmlToJsx(fp: FilePath, tree: Node) {
|
||||||
try {
|
try {
|
||||||
return toJsxRuntime(tree as Root, {
|
return toJsxRuntime(tree as Root, {
|
||||||
Fragment,
|
Fragment,
|
||||||
|
@ -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,9 @@
|
|||||||
import { slug } from "github-slugger"
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
|
import type { Element as HastElement } from "hast"
|
||||||
|
import rfdc from "rfdc"
|
||||||
|
|
||||||
|
export const clone = rfdc()
|
||||||
|
|
||||||
// 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 +29,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)
|
||||||
}
|
}
|
||||||
@ -42,6 +47,14 @@ export function getFullSlug(window: Window): FullSlug {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sluggify(s: string): string {
|
||||||
|
return s
|
||||||
|
.split("/")
|
||||||
|
.map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments
|
||||||
|
.join("/") // always use / as sep
|
||||||
|
.replace(/\/$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||||
fp = _stripSlashes(fp) as FilePath
|
fp = _stripSlashes(fp) as FilePath
|
||||||
let ext = _getFileExtension(fp)
|
let ext = _getFileExtension(fp)
|
||||||
@ -50,11 +63,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
|||||||
ext = ""
|
ext = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let slug = withoutFileExt
|
let slug = sluggify(withoutFileExt)
|
||||||
.split("/")
|
|
||||||
.map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments
|
|
||||||
.join("/") // always use / as sep
|
|
||||||
.replace(/\/$/, "") // remove trailing slash
|
|
||||||
|
|
||||||
// treat _index as index
|
// treat _index as index
|
||||||
if (_endsWith(slug, "_index")) {
|
if (_endsWith(slug, "_index")) {
|
||||||
@ -65,7 +74,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 +94,50 @@ 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(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
||||||
|
const el = clone(rawEl) // clone so we dont modify the original page
|
||||||
|
_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
|
||||||
@ -111,14 +165,10 @@ export function splitAnchor(link: string): [string, string] {
|
|||||||
return [fp, anchor]
|
return [fp, anchor]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugAnchor(anchor: string) {
|
|
||||||
return slug(anchor)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function slugTag(tag: string) {
|
export function slugTag(tag: string) {
|
||||||
return tag
|
return tag
|
||||||
.split("/")
|
.split("/")
|
||||||
.map((tagSegment) => slug(tagSegment))
|
.map((tagSegment) => sluggify(tagSegment))
|
||||||
.join("/")
|
.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +26,12 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole
|
|||||||
} else {
|
} else {
|
||||||
const content = resource.script
|
const content = resource.script
|
||||||
return (
|
return (
|
||||||
<script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>
|
<script
|
||||||
{content}
|
key={randomUUID()}
|
||||||
</script>
|
type={scriptType}
|
||||||
|
spa-preserve={spaPreserve}
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
></script>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user