mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-21 03:44:05 -06:00
Merge branch 'jackyzha0:v4' into v4
This commit is contained in:
commit
ca05e58313
2
.github/workflows/docker-build-push.yaml
vendored
2
.github/workflows/docker-build-push.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
network=host
|
network=host
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@v3.8.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|||||||
@ -274,7 +274,7 @@ export const ContentPage: QuartzEmitterPlugin = () => {
|
|||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = canonicalizeServer(file.data.slug!)
|
const slug = canonicalizeServer(file.data.slug!)
|
||||||
const externalResources = pageResources(slug, resources)
|
const externalResources = pageResources(slug, file.data, resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
fileData: file.data,
|
fileData: file.data,
|
||||||
externalResources,
|
externalResources,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ A backlink for a note is a link from another note to that note. Links in the bac
|
|||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`.
|
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`.
|
||||||
|
- Hide when empty: hide `Backlinks` if given page doesn't contain any backlinks (default to `true`). To disable this, use `Component.Backlinks({ hideWhenEmpty: false })`.
|
||||||
- Component: `quartz/components/Backlinks.tsx`
|
- Component: `quartz/components/Backlinks.tsx`
|
||||||
- Style: `quartz/components/styles/backlinks.scss`
|
- Style: `quartz/components/styles/backlinks.scss`
|
||||||
- Script: `quartz/components/scripts/search.inline.ts`
|
- Script: `quartz/components/scripts/search.inline.ts`
|
||||||
|
|||||||
@ -36,6 +36,7 @@ Component.Graph({
|
|||||||
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||||
removeTags: [], // what tags to remove from the graph
|
removeTags: [], // what tags to remove from the graph
|
||||||
showTags: true, // whether to show tags in the graph
|
showTags: true, // whether to show tags in the graph
|
||||||
|
enableRadial: false, // whether to constrain the graph, similar to Obsidian
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@ -49,6 +50,7 @@ Component.Graph({
|
|||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
removeTags: [], // what tags to remove from the graph
|
removeTags: [], // what tags to remove from the graph
|
||||||
showTags: true, // whether to show tags in the graph
|
showTags: true, // whether to show tags in the graph
|
||||||
|
enableRadial: true, // whether to constrain the graph, similar to Obsidian
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@ -31,4 +31,4 @@ This plugin accepts the following configuration options:
|
|||||||
|
|
||||||
- Category: Transformer
|
- Category: Transformer
|
||||||
- Function name: `Plugin.ObsidianFlavoredMarkdown()`.
|
- Function name: `Plugin.ObsidianFlavoredMarkdown()`.
|
||||||
- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts).
|
- Source: [`quartz/plugins/transformers/ofm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/ofm.ts)
|
||||||
|
|||||||
@ -30,5 +30,7 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
||||||
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
||||||
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
||||||
|
- [Zen Browser Docs](https://docs.zen-browser.app)
|
||||||
|
- [🪴8cat life](https://8cat.life)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
1427
package-lock.json
generated
1427
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@ -35,35 +35,34 @@
|
|||||||
"quartz": "./quartz/bootstrap-cli.mjs"
|
"quartz": "./quartz/bootstrap-cli.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.8.1",
|
"@clack/prompts": "^0.10.0",
|
||||||
"@floating-ui/dom": "^1.6.12",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@myriaddreamin/rehype-typst": "^0.5.0-rc7",
|
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||||
"@napi-rs/simple-git": "0.1.19",
|
"@napi-rs/simple-git": "0.1.19",
|
||||||
"@tweenjs/tween.js": "^25.0.0",
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.4.1",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"esbuild-sass-plugin": "^3.3.1",
|
"esbuild-sass-plugin": "^3.3.1",
|
||||||
"flexsearch": "0.7.43",
|
"flexsearch": "0.7.43",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.1.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.3",
|
"hast-util-to-html": "^9.0.4",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.2",
|
"hast-util-to-jsx-runtime": "^2.3.2",
|
||||||
"hast-util-to-string": "^3.0.1",
|
"hast-util-to-string": "^3.0.1",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.28.1",
|
"lightningcss": "^1.29.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"mdast-util-find-and-replace": "^3.0.2",
|
||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"mermaid": "^11.4.0",
|
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"pixi.js": "^8.5.2",
|
"pixi.js": "^8.7.3",
|
||||||
"preact": "^10.24.3",
|
"preact": "^10.25.4",
|
||||||
"preact-render-to-string": "^6.5.11",
|
"preact-render-to-string": "^6.5.13",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
@ -77,17 +76,17 @@
|
|||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
"remark-smartypants": "^3.0.2",
|
"remark-smartypants": "^3.0.2",
|
||||||
"rfdc": "^1.4.1",
|
"rfdc": "^1.4.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"satori": "^0.11.3",
|
"satori": "^0.12.1",
|
||||||
"serve-handler": "^6.1.6",
|
"serve-handler": "^6.1.6",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"shiki": "^1.23.1",
|
"shiki": "^1.26.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^8.0.0",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
@ -103,14 +102,14 @@
|
|||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.13.1",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.14",
|
||||||
"@types/yargs": "^17.0.33",
|
"@types/yargs": "^17.0.33",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.25.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.5.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const defaultContentPageLayout: PageLayout = {
|
|||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.Explorer()),
|
Component.Explorer(),
|
||||||
],
|
],
|
||||||
right: [
|
right: [
|
||||||
Component.Graph(),
|
Component.Graph(),
|
||||||
@ -44,7 +44,7 @@ export const defaultListPageLayout: PageLayout = {
|
|||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.Explorer()),
|
Component.Explorer(),
|
||||||
],
|
],
|
||||||
right: [],
|
right: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import workerpool from "workerpool"
|
import workerpool from "workerpool"
|
||||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
||||||
const { parseFiles } = await import(cacheFile)
|
const { parseMarkdown, processHtml } = await import(cacheFile)
|
||||||
workerpool.worker({
|
workerpool.worker({
|
||||||
parseFiles,
|
parseMarkdown,
|
||||||
|
processHtml,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -139,9 +139,9 @@ async function startServing(
|
|||||||
|
|
||||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
||||||
watcher
|
watcher
|
||||||
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
|
.on("add", (fp) => buildFromEntry(fp as string, "add", clientRefresh, buildData))
|
||||||
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
|
.on("change", (fp) => buildFromEntry(fp as string, "change", clientRefresh, buildData))
|
||||||
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
|
.on("unlink", (fp) => buildFromEntry(fp as string, "delete", clientRefresh, buildData))
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await watcher.close()
|
await watcher.close()
|
||||||
|
|||||||
@ -4,14 +4,28 @@ import { resolveRelative, simplifySlug } from "../util/path"
|
|||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
const Backlinks: QuartzComponent = ({
|
interface BacklinksOptions {
|
||||||
|
hideWhenEmpty: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: BacklinksOptions = {
|
||||||
|
hideWhenEmpty: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((opts?: Partial<BacklinksOptions>) => {
|
||||||
|
const options: BacklinksOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
|
const Backlinks: QuartzComponent = ({
|
||||||
fileData,
|
fileData,
|
||||||
allFiles,
|
allFiles,
|
||||||
displayClass,
|
displayClass,
|
||||||
cfg,
|
cfg,
|
||||||
}: QuartzComponentProps) => {
|
}: QuartzComponentProps) => {
|
||||||
const slug = simplifySlug(fileData.slug!)
|
const slug = simplifySlug(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
|
if (options.hideWhenEmpty && backlinkFiles.length == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "backlinks")}>
|
<div class={classNames(displayClass, "backlinks")}>
|
||||||
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
||||||
@ -30,7 +44,9 @@ const Backlinks: QuartzComponent = ({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Backlinks.css = style
|
Backlinks.css = style
|
||||||
export default (() => Backlinks) satisfies QuartzComponentConstructor
|
|
||||||
|
return Backlinks
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@ -28,7 +28,8 @@ export default ((opts: Options) => {
|
|||||||
const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => {
|
const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => {
|
||||||
// check if comments should be displayed according to frontmatter
|
// check if comments should be displayed according to frontmatter
|
||||||
const disableComment: boolean =
|
const disableComment: boolean =
|
||||||
!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false"
|
typeof fileData.frontmatter?.comments !== "undefined" &&
|
||||||
|
(!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false")
|
||||||
if (disableComment) {
|
if (disableComment) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { formatDate, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import readingTime from "reading-time"
|
import readingTime from "reading-time"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
@ -30,7 +30,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
|||||||
const segments: (string | JSX.Element)[] = []
|
const segments: (string | JSX.Element)[] = []
|
||||||
|
|
||||||
if (fileData.dates) {
|
if (fileData.dates) {
|
||||||
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
|
segments.push(<Date date={getDate(cfg, fileData)!} locale={cfg.locale} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display reading time if enabled
|
// Display reading time if enabled
|
||||||
@ -39,14 +39,12 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
|||||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
||||||
minutes: Math.ceil(minutes),
|
minutes: Math.ceil(minutes),
|
||||||
})
|
})
|
||||||
segments.push(displayedTime)
|
segments.push(<span>{displayedTime}</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
|
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
|
||||||
{segmentsElements}
|
{segments}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -27,5 +27,5 @@ export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Date({ date, locale }: Props) {
|
export function Date({ date, locale }: Props) {
|
||||||
return <>{formatDate(date, locale)}</>
|
return <time datetime={date.toISOString()}>{formatDate(date, locale)}</time>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import explorerStyle from "./styles/explorer.scss"
|
import style from "./styles/explorer.scss"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/explorer.inline"
|
import script from "./scripts/explorer.inline"
|
||||||
@ -83,18 +83,46 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
lastBuildId = ctx.buildId
|
lastBuildId = ctx.buildId
|
||||||
constructFileTree(allFiles)
|
constructFileTree(allFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div class={classNames(displayClass, "explorer")}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="explorer"
|
id="mobile-explorer"
|
||||||
|
class="collapsed hide-until-loaded"
|
||||||
data-behavior={opts.folderClickBehavior}
|
data-behavior={opts.folderClickBehavior}
|
||||||
data-collapsed={opts.folderDefaultState}
|
data-collapsed={opts.folderDefaultState}
|
||||||
data-savestate={opts.useSavedState}
|
data-savestate={opts.useSavedState}
|
||||||
data-tree={jsonTree}
|
data-tree={jsonTree}
|
||||||
|
data-mobile={true}
|
||||||
aria-controls="explorer-content"
|
aria-controls="explorer-content"
|
||||||
aria-expanded={opts.folderDefaultState === "open"}
|
aria-expanded={false}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-menu"
|
||||||
|
>
|
||||||
|
<line x1="4" x2="20" y1="12" y2="12" />
|
||||||
|
<line x1="4" x2="20" y1="6" y2="6" />
|
||||||
|
<line x1="4" x2="20" y1="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="desktop-explorer"
|
||||||
|
class="title-button"
|
||||||
|
data-behavior={opts.folderClickBehavior}
|
||||||
|
data-collapsed={opts.folderDefaultState}
|
||||||
|
data-savestate={opts.useSavedState}
|
||||||
|
data-tree={jsonTree}
|
||||||
|
data-mobile={false}
|
||||||
|
aria-controls="explorer-content"
|
||||||
|
aria-expanded={true}
|
||||||
>
|
>
|
||||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||||
<svg
|
<svg
|
||||||
@ -122,7 +150,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = explorerStyle
|
Explorer.css = style
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = script
|
||||||
return Explorer
|
return Explorer
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface D3Config {
|
|||||||
removeTags: string[]
|
removeTags: string[]
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
focusOnHover?: boolean
|
focusOnHover?: boolean
|
||||||
|
enableRadial?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphOptions {
|
interface GraphOptions {
|
||||||
@ -39,6 +40,7 @@ const defaultOptions: GraphOptions = {
|
|||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
focusOnHover: false,
|
focusOnHover: false,
|
||||||
|
enableRadial: false,
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@ -53,10 +55,11 @@ const defaultOptions: GraphOptions = {
|
|||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
focusOnHover: true,
|
focusOnHover: true,
|
||||||
|
enableRadial: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: Partial<GraphOptions>) => {
|
||||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||||
|
|||||||
@ -165,6 +165,7 @@ export default (() => {
|
|||||||
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
|
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossOrigin={"anonymous"} />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
{/* OG/Twitter meta tags */}
|
{/* OG/Twitter meta tags */}
|
||||||
<meta name="og:site_name" content={cfg.pageTitle}></meta>
|
<meta name="og:site_name" content={cfg.pageTitle}></meta>
|
||||||
@ -181,8 +182,6 @@ export default (() => {
|
|||||||
<>
|
<>
|
||||||
<meta property="og:image:width" content={fullOptions.width.toString()} />
|
<meta property="og:image:width" content={fullOptions.width.toString()} />
|
||||||
<meta property="og:image:height" content={fullOptions.height.toString()} />
|
<meta property="og:image:height" content={fullOptions.height.toString()} />
|
||||||
<meta property="og:width" content={fullOptions.width.toString()} />
|
|
||||||
<meta property="og:height" content={fullOptions.height.toString()} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<meta property="og:image:url" content={ogImagePath} />
|
<meta property="og:image:url" content={ogImagePath} />
|
||||||
|
|||||||
@ -46,13 +46,9 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
|||||||
return (
|
return (
|
||||||
<li class="section-li">
|
<li class="section-li">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div>
|
|
||||||
{page.dates && (
|
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
{page.dates && <Date date={getDate(cfg, page)!} locale={cfg.locale} />}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h3>
|
<h3>
|
||||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
const classes = cssClasses.join(" ")
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
sort: options.sort,
|
sort: options.sort,
|
||||||
@ -84,8 +84,8 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article class={classes}>{content}</article>
|
||||||
<div class="page-listing">
|
<div class="page-listing">
|
||||||
{options.showFolderCount && (
|
{options.showFolderCount && (
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
? fileData.description
|
? fileData.description
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
const classes = cssClasses.join(" ")
|
||||||
if (tag === "/") {
|
if (tag === "/") {
|
||||||
const tags = [
|
const tags = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@ -50,8 +50,8 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>
|
<article class={classes}>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
||||||
@ -93,7 +93,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
|
<PageList limit={options.numPages} {...listProps} sort={options?.sort} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -110,11 +110,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class={classes}>
|
||||||
<article>{content}</article>
|
<article class="popover-hint">{content}</article>
|
||||||
<div class="page-listing">
|
<div class="page-listing">
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} sort={options?.sort} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import { visit } from "unist-util-visit"
|
|||||||
import { Root, Element, ElementContent } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
|
// @ts-ignore
|
||||||
|
import mermaidScript from "./scripts/mermaid.inline"
|
||||||
|
import mermaidStyle from "./styles/mermaid.inline.scss"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@ -23,12 +27,13 @@ interface RenderComponents {
|
|||||||
const headerRegex = new RegExp(/h[1-6]/)
|
const headerRegex = new RegExp(/h[1-6]/)
|
||||||
export function pageResources(
|
export function pageResources(
|
||||||
baseDir: FullSlug | RelativeURL,
|
baseDir: FullSlug | RelativeURL,
|
||||||
|
fileData: QuartzPluginData,
|
||||||
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 {
|
const resources: StaticResources = {
|
||||||
css: [
|
css: [
|
||||||
{
|
{
|
||||||
content: joinSegments(baseDir, "index.css"),
|
content: joinSegments(baseDir, "index.css"),
|
||||||
@ -48,14 +53,28 @@ export function pageResources(
|
|||||||
script: contentIndexScript,
|
script: contentIndexScript,
|
||||||
},
|
},
|
||||||
...staticResources.js,
|
...staticResources.js,
|
||||||
{
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileData.hasMermaidDiagram) {
|
||||||
|
resources.js.push({
|
||||||
|
script: mermaidScript,
|
||||||
|
loadTime: "afterDOMReady",
|
||||||
|
moduleType: "module",
|
||||||
|
contentType: "inline",
|
||||||
|
})
|
||||||
|
resources.css.push({ content: mermaidStyle, inline: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: we have to put this last to make sure spa.inline.ts is the last item.
|
||||||
|
resources.js.push({
|
||||||
src: joinSegments(baseDir, "postscript.js"),
|
src: joinSegments(baseDir, "postscript.js"),
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
moduleType: "module",
|
moduleType: "module",
|
||||||
contentType: "external",
|
contentType: "external",
|
||||||
},
|
})
|
||||||
],
|
|
||||||
}
|
return resources
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPage(
|
export function renderPage(
|
||||||
|
|||||||
@ -27,9 +27,10 @@ document.addEventListener("nav", () => {
|
|||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
||||||
|
if (themeButton) {
|
||||||
themeButton.addEventListener("click", switchTheme)
|
themeButton.addEventListener("click", switchTheme)
|
||||||
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
||||||
|
}
|
||||||
// Listen for changes in prefers-color-scheme
|
// Listen for changes in prefers-color-scheme
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { FolderState } from "../ExplorerNode"
|
import { FolderState } from "../ExplorerNode"
|
||||||
|
|
||||||
|
// Current state of folders
|
||||||
type MaybeHTMLElement = HTMLElement | undefined
|
type MaybeHTMLElement = HTMLElement | undefined
|
||||||
let currentExplorerState: FolderState[]
|
let currentExplorerState: FolderState[]
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||||
const explorerUl = document.getElementById("explorer-ul")
|
const explorerUl = document.getElementById("explorer-ul")
|
||||||
@ -16,23 +18,43 @@ const observer = new IntersectionObserver((entries) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(this: HTMLElement) {
|
||||||
|
// Toggle collapsed state of entire explorer
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
|
|
||||||
|
// Toggle collapsed aria state of entire explorer
|
||||||
this.setAttribute(
|
this.setAttribute(
|
||||||
"aria-expanded",
|
"aria-expanded",
|
||||||
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||||
)
|
)
|
||||||
const content = this.nextElementSibling as MaybeHTMLElement
|
|
||||||
if (!content) return
|
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
this.nextElementSibling?.nextElementSibling
|
||||||
|
? this.nextElementSibling.nextElementSibling
|
||||||
|
: this.nextElementSibling
|
||||||
|
) as MaybeHTMLElement
|
||||||
|
if (!content) return
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
|
content.classList.toggle("explorer-viewmode")
|
||||||
|
|
||||||
|
// Prevent scroll under
|
||||||
|
if (document.querySelector("#mobile-explorer")) {
|
||||||
|
// Disable scrolling on the page when the explorer is opened on mobile
|
||||||
|
const bodySelector = document.querySelector("#quartz-body")
|
||||||
|
if (bodySelector) bodySelector.classList.toggle("lock-scroll")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFolder(evt: MouseEvent) {
|
function toggleFolder(evt: MouseEvent) {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
|
|
||||||
|
// Element that was clicked
|
||||||
const target = evt.target as MaybeHTMLElement
|
const target = evt.target as MaybeHTMLElement
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
|
// Check if target was svg icon or button
|
||||||
const isSvg = target.nodeName === "svg"
|
const isSvg = target.nodeName === "svg"
|
||||||
|
|
||||||
|
// corresponding <ul> element relative to clicked button/folder
|
||||||
const childFolderContainer = (
|
const childFolderContainer = (
|
||||||
isSvg
|
isSvg
|
||||||
? target.parentElement?.nextSibling
|
? target.parentElement?.nextSibling
|
||||||
@ -42,10 +64,14 @@ function toggleFolder(evt: MouseEvent) {
|
|||||||
isSvg ? target.nextElementSibling : target.parentElement
|
isSvg ? target.nextElementSibling : target.parentElement
|
||||||
) as MaybeHTMLElement
|
) as MaybeHTMLElement
|
||||||
if (!(childFolderContainer && currentFolderParent)) return
|
if (!(childFolderContainer && currentFolderParent)) return
|
||||||
|
// <li> element of folder (stores folder-path dataset)
|
||||||
childFolderContainer.classList.toggle("open")
|
childFolderContainer.classList.toggle("open")
|
||||||
|
|
||||||
|
// Collapse folder container
|
||||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||||
setFolderState(childFolderContainer, !isCollapsed)
|
setFolderState(childFolderContainer, !isCollapsed)
|
||||||
|
|
||||||
|
// Save folder state to localStorage
|
||||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
||||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
||||||
@ -53,20 +79,34 @@ function toggleFolder(evt: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupExplorer() {
|
function setupExplorer() {
|
||||||
const explorer = document.getElementById("explorer")
|
// Set click handler for collapsing entire explorer
|
||||||
if (!explorer) return
|
const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
|
||||||
|
|
||||||
if (explorer.dataset.behavior === "collapse") {
|
for (const explorer of allExplorers) {
|
||||||
|
// Get folder state from local storage
|
||||||
|
const storageTree = localStorage.getItem("fileTree")
|
||||||
|
|
||||||
|
// Convert to bool
|
||||||
|
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||||
|
|
||||||
|
if (explorer) {
|
||||||
|
// Get config
|
||||||
|
const collapseBehavior = explorer.dataset.behavior
|
||||||
|
|
||||||
|
// Add click handlers for all folders (click handler on folder "label")
|
||||||
|
if (collapseBehavior === "collapse") {
|
||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
"folder-button",
|
"folder-button",
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
) as HTMLCollectionOf<HTMLElement>) {
|
||||||
|
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||||
item.addEventListener("click", toggleFolder)
|
item.addEventListener("click", toggleFolder)
|
||||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
// Add click handler to main explorer
|
||||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||||
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
@ -77,8 +117,6 @@ function setupExplorer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
const storageTree = localStorage.getItem("fileTree")
|
|
||||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
|
||||||
const oldExplorerState: FolderState[] =
|
const oldExplorerState: FolderState[] =
|
||||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
||||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
||||||
@ -86,24 +124,61 @@ function setupExplorer() {
|
|||||||
? JSON.parse(explorer.dataset.tree)
|
? JSON.parse(explorer.dataset.tree)
|
||||||
: []
|
: []
|
||||||
currentExplorerState = []
|
currentExplorerState = []
|
||||||
|
|
||||||
for (const { path, collapsed } of newExplorerState) {
|
for (const { path, collapsed } of newExplorerState) {
|
||||||
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
currentExplorerState.push({
|
||||||
|
path,
|
||||||
|
collapsed: oldIndex.get(path) ?? collapsed,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
currentExplorerState.map((folderState) => {
|
currentExplorerState.map((folderState) => {
|
||||||
const folderLi = document.querySelector(
|
const folderLi = document.querySelector(
|
||||||
`[data-folderpath='${folderState.path}']`,
|
`[data-folderpath='${folderState.path.replace("'", "-")}']`,
|
||||||
) as MaybeHTMLElement
|
) as MaybeHTMLElement
|
||||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
||||||
if (folderUl) {
|
if (folderUl) {
|
||||||
setFolderState(folderUl, folderState.collapsed)
|
setFolderState(folderUl, folderState.collapsed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExplorerFolders() {
|
||||||
|
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
|
||||||
|
/\/index$/g,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
const allFolders = document.querySelectorAll(".folder-outer")
|
||||||
|
|
||||||
|
allFolders.forEach((element) => {
|
||||||
|
const folderUl = Array.from(element.children).find((child) =>
|
||||||
|
child.matches("ul[data-folderul]"),
|
||||||
|
)
|
||||||
|
if (folderUl) {
|
||||||
|
if (currentFile.includes(folderUl.getAttribute("data-folderul") ?? "")) {
|
||||||
|
if (!element.classList.contains("open")) {
|
||||||
|
element.classList.add("open")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", setupExplorer)
|
window.addEventListener("resize", setupExplorer)
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
|
const explorer = document.querySelector("#mobile-explorer")
|
||||||
|
if (explorer) {
|
||||||
|
explorer.classList.add("collapsed")
|
||||||
|
const content = explorer.nextElementSibling?.nextElementSibling as HTMLElement
|
||||||
|
if (content) {
|
||||||
|
content.classList.add("collapsed")
|
||||||
|
content.classList.toggle("explorer-viewmode")
|
||||||
|
}
|
||||||
|
}
|
||||||
setupExplorer()
|
setupExplorer()
|
||||||
|
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
|
|
||||||
// select pseudo element at end of list
|
// select pseudo element at end of list
|
||||||
@ -111,6 +186,12 @@ document.addEventListener("nav", () => {
|
|||||||
if (lastItem) {
|
if (lastItem) {
|
||||||
observer.observe(lastItem)
|
observer.observe(lastItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide explorer on mobile until it is requested
|
||||||
|
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
|
||||||
|
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
|
||||||
|
|
||||||
|
toggleExplorerFolders()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
forceCenter,
|
forceCenter,
|
||||||
forceLink,
|
forceLink,
|
||||||
forceCollide,
|
forceCollide,
|
||||||
|
forceRadial,
|
||||||
zoomIdentity,
|
zoomIdentity,
|
||||||
select,
|
select,
|
||||||
drag,
|
drag,
|
||||||
@ -87,6 +88,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
removeTags,
|
removeTags,
|
||||||
showTags,
|
showTags,
|
||||||
focusOnHover,
|
focusOnHover,
|
||||||
|
enableRadial,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||||
|
|
||||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
@ -161,15 +163,20 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const width = graph.offsetWidth
|
||||||
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
// we virtualize the simulation and use pixi to actually render it
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
|
// Calculate the radius of the container circle
|
||||||
|
const radius = Math.min(width, height) / 2 - 40 // 40px padding
|
||||||
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force("center", forceCenter().strength(centerForce))
|
.force("center", forceCenter().strength(centerForce))
|
||||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
|
|
||||||
const width = graph.offsetWidth
|
if (enableRadial)
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
|
||||||
|
|
||||||
// precompute style prop strings as pixi doesn't support css variables
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
const cssVars = [
|
const cssVars = [
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { removeAllChildren } from "./util"
|
import { removeAllChildren } from "./util"
|
||||||
import mermaid from "mermaid"
|
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
x: number
|
x: number
|
||||||
@ -144,6 +143,7 @@ const cssVars = [
|
|||||||
"--codeFont",
|
"--codeFont",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
let mermaidImport = undefined
|
||||||
document.addEventListener("nav", async () => {
|
document.addEventListener("nav", async () => {
|
||||||
const center = document.querySelector(".center") as HTMLElement
|
const center = document.querySelector(".center") as HTMLElement
|
||||||
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
|
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
|
||||||
@ -157,6 +157,12 @@ document.addEventListener("nav", async () => {
|
|||||||
{} as Record<(typeof cssVars)[number], string>,
|
{} as Record<(typeof cssVars)[number], string>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mermaidImport ||= await import(
|
||||||
|
//@ts-ignore
|
||||||
|
"https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
|
||||||
|
)
|
||||||
|
const mermaid = mermaidImport.default
|
||||||
|
|
||||||
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
|
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||||
import { normalizeRelativeURLs } from "../../util/path"
|
import { normalizeRelativeURLs } from "../../util/path"
|
||||||
|
import { fetchCanonical } from "./util"
|
||||||
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
async function mouseEnterHandler(
|
async function mouseEnterHandler(
|
||||||
@ -37,7 +38,7 @@ async function mouseEnterHandler(
|
|||||||
targetUrl.hash = ""
|
targetUrl.hash = ""
|
||||||
targetUrl.search = ""
|
targetUrl.search = ""
|
||||||
|
|
||||||
const response = await fetch(`${targetUrl}`).catch((err) => {
|
const response = await fetchCanonical(targetUrl).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import micromorph from "micromorph"
|
import micromorph from "micromorph"
|
||||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||||
|
import { fetchCanonical } from "./util"
|
||||||
|
|
||||||
// adapted from `micromorph`
|
// adapted from `micromorph`
|
||||||
// https://github.com/natemoo-re/micromorph
|
// https://github.com/natemoo-re/micromorph
|
||||||
@ -42,10 +43,24 @@ function notifyNav(url: FullSlug) {
|
|||||||
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||||
window.addCleanup = (fn) => cleanupFns.add(fn)
|
window.addCleanup = (fn) => cleanupFns.add(fn)
|
||||||
|
|
||||||
|
function startLoading() {
|
||||||
|
const loadingBar = document.createElement("div")
|
||||||
|
loadingBar.className = "navigation-progress"
|
||||||
|
loadingBar.style.width = "0"
|
||||||
|
if (!document.body.contains(loadingBar)) {
|
||||||
|
document.body.appendChild(loadingBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingBar.style.width = "80%"
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
let p: DOMParser
|
let p: DOMParser
|
||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
|
startLoading()
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
const contents = await fetch(`${url}`)
|
const contents = await fetchCanonical(url)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const contentType = res.headers.get("content-type")
|
const contentType = res.headers.get("content-type")
|
||||||
if (contentType?.startsWith("text/html")) {
|
if (contentType?.startsWith("text/html")) {
|
||||||
@ -104,6 +119,7 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
history.pushState({}, "", url)
|
history.pushState({}, "", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyNav(getFullSlug(window))
|
notifyNav(getFullSlug(window))
|
||||||
delete announcer.dataset.persist
|
delete announcer.dataset.persist
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,3 +24,22 @@ export function removeAllChildren(node: HTMLElement) {
|
|||||||
node.removeChild(node.firstChild)
|
node.removeChild(node.firstChild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AliasRedirect emits HTML redirects which also have the link[rel="canonical"]
|
||||||
|
// containing the URL it's redirecting to.
|
||||||
|
// Extracting it here with regex is _probably_ faster than parsing the entire HTML
|
||||||
|
// with a DOMParser effectively twice (here and later in the SPA code), even if
|
||||||
|
// way less robust - we only care about our own generated redirects after all.
|
||||||
|
const canonicalRegex = /<link rel="canonical" href="([^"]*)">/
|
||||||
|
|
||||||
|
export async function fetchCanonical(url: URL): Promise<Response> {
|
||||||
|
const res = await fetch(`${url}`)
|
||||||
|
if (!res.headers.get("content-type")?.startsWith("text/html")) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
// reading the body can only be done once, so we need to clone the response
|
||||||
|
// to allow the caller to read it if it's was not a redirect
|
||||||
|
const text = await res.clone().text()
|
||||||
|
const [_, redirect] = text.match(canonicalRegex) ?? []
|
||||||
|
return redirect ? fetch(`${new URL(redirect, url)}`) : res
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
|
|
||||||
&[show-comma="true"] {
|
&[show-comma="true"] {
|
||||||
> span:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|||||||
@ -1,14 +1,70 @@
|
|||||||
@use "../../styles/variables.scss" as *;
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
|
@media all and ($mobile) {
|
||||||
|
.page > #quartz-body {
|
||||||
|
// Shift page position when toggling Explorer on mobile.
|
||||||
|
& > :not(.sidebar.left:has(.explorer)) {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
|
||||||
|
transform: translateX(100dvw);
|
||||||
|
transition: transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky top bar (stays in place when scrolling down on mobile).
|
||||||
|
.sidebar.left:has(.explorer) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: sticky;
|
||||||
|
background-color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide Explorer on mobile until done loading.
|
||||||
|
// Prevents ugly animation on page load.
|
||||||
|
.hide-until-loaded ~ #explorer-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.explorer {
|
.explorer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
@media all and ($mobile) {
|
||||||
|
order: -1;
|
||||||
|
height: initial;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#mobile-explorer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#desktop-explorer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and ($mobile) {
|
||||||
|
button#mobile-explorer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#desktop-explorer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.desktop-only {
|
&.desktop-only {
|
||||||
@media all and not ($mobile) {
|
@media all and not ($mobile) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*&:after {
|
/*&:after {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: "";
|
content: "";
|
||||||
@ -23,7 +79,8 @@
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
button#explorer {
|
button#mobile-explorer,
|
||||||
|
button#desktop-explorer {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -68,19 +125,19 @@ button#explorer {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
max-height: 0px;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0.35s;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
transition:
|
transition:
|
||||||
max-height 0.35s ease,
|
max-height 0.35s ease,
|
||||||
visibility 0s linear 0s;
|
visibility 0s linear 0s;
|
||||||
margin-top: 0.5rem;
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
max-height: 0;
|
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
visibility 0s linear 0.35s;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
@ -91,12 +148,14 @@ button#explorer {
|
|||||||
max-height 0.35s ease,
|
max-height 0.35s ease,
|
||||||
transform 0.35s ease,
|
transform 0.35s ease,
|
||||||
opacity 0.2s ease;
|
opacity 0.2s ease;
|
||||||
|
|
||||||
& li > a {
|
& li > a {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> #explorer-ul {
|
> #explorer-ul {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
@ -179,3 +238,80 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
|||||||
// remove default margin from li
|
// remove default margin from li
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
@media all and ($mobile) {
|
||||||
|
#explorer-content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
z-index: 100;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--light);
|
||||||
|
max-width: 100dvw;
|
||||||
|
left: -100dvw;
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 300ms ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: $topSpacing 2rem 2rem;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
margin-top: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&:not(.collapsed) {
|
||||||
|
transform: translateX(100dvw);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.overflow {
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
transform: translateX(0);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-explorer {
|
||||||
|
margin: 5px;
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
&:not(.collapsed) .lucide-menu {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-menu {
|
||||||
|
stroke: var(--darkgray);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
stroke: var(--dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scroll {
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:has(.no-scroll) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and not ($mobile) {
|
||||||
|
.no-scroll {
|
||||||
|
opacity: 1 !important;
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:has(.no-scroll) {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -106,7 +106,7 @@
|
|||||||
flex: 0 0 min(30%, 450px);
|
flex: 0 0 min(30%, 450px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and not ($tablet) {
|
@media all and not ($mobile) {
|
||||||
&[data-preview] {
|
&[data-preview] {
|
||||||
& .result-card > p.preview {
|
& .result-card > p.preview {
|
||||||
display: none;
|
display: none;
|
||||||
@ -132,7 +132,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and ($tablet) {
|
@media all and ($mobile) {
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@ -151,6 +151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
|
flex-grow: 1;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import uk from "./locales/uk-UA"
|
|||||||
import ru from "./locales/ru-RU"
|
import ru from "./locales/ru-RU"
|
||||||
import ko from "./locales/ko-KR"
|
import ko from "./locales/ko-KR"
|
||||||
import zh from "./locales/zh-CN"
|
import zh from "./locales/zh-CN"
|
||||||
|
import zhTw from "./locales/zh-TW"
|
||||||
import vi from "./locales/vi-VN"
|
import vi from "./locales/vi-VN"
|
||||||
import pt from "./locales/pt-BR"
|
import pt from "./locales/pt-BR"
|
||||||
import hu from "./locales/hu-HU"
|
import hu from "./locales/hu-HU"
|
||||||
@ -21,6 +22,8 @@ import fa from "./locales/fa-IR"
|
|||||||
import pl from "./locales/pl-PL"
|
import pl from "./locales/pl-PL"
|
||||||
import cs from "./locales/cs-CZ"
|
import cs from "./locales/cs-CZ"
|
||||||
import tr from "./locales/tr-TR"
|
import tr from "./locales/tr-TR"
|
||||||
|
import th from "./locales/th-TH"
|
||||||
|
import lt from "./locales/lt-LT"
|
||||||
|
|
||||||
export const TRANSLATIONS = {
|
export const TRANSLATIONS = {
|
||||||
"en-US": enUs,
|
"en-US": enUs,
|
||||||
@ -59,6 +62,7 @@ export const TRANSLATIONS = {
|
|||||||
"ru-RU": ru,
|
"ru-RU": ru,
|
||||||
"ko-KR": ko,
|
"ko-KR": ko,
|
||||||
"zh-CN": zh,
|
"zh-CN": zh,
|
||||||
|
"zh-TW": zhTw,
|
||||||
"vi-VN": vi,
|
"vi-VN": vi,
|
||||||
"pt-BR": pt,
|
"pt-BR": pt,
|
||||||
"hu-HU": hu,
|
"hu-HU": hu,
|
||||||
@ -66,6 +70,8 @@ export const TRANSLATIONS = {
|
|||||||
"pl-PL": pl,
|
"pl-PL": pl,
|
||||||
"cs-CZ": cs,
|
"cs-CZ": cs,
|
||||||
"tr-TR": tr,
|
"tr-TR": tr,
|
||||||
|
"th-TH": th,
|
||||||
|
"lt-LT": lt,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const defaultTranslation = "en-US"
|
export const defaultTranslation = "en-US"
|
||||||
|
|||||||
104
quartz/i18n/locales/lt-LT.ts
Normal file
104
quartz/i18n/locales/lt-LT.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Be Pavadinimo",
|
||||||
|
description: "Aprašymas Nepateiktas",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "Pastaba",
|
||||||
|
abstract: "Santrauka",
|
||||||
|
info: "Informacija",
|
||||||
|
todo: "Darbų sąrašas",
|
||||||
|
tip: "Patarimas",
|
||||||
|
success: "Sėkmingas",
|
||||||
|
question: "Klausimas",
|
||||||
|
warning: "Įspėjimas",
|
||||||
|
failure: "Nesėkmingas",
|
||||||
|
danger: "Pavojus",
|
||||||
|
bug: "Klaida",
|
||||||
|
example: "Pavyzdys",
|
||||||
|
quote: "Citata",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "Atgalinės Nuorodos",
|
||||||
|
noBacklinksFound: "Atgalinių Nuorodų Nerasta",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Šviesus Režimas",
|
||||||
|
darkMode: "Tamsus Režimas",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Naršyklė",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Sukurta Su",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Grafiko Vaizdas",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Naujausi Užrašai",
|
||||||
|
seeRemainingMore: ({ remaining }) => `Peržiūrėti dar ${remaining} →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`,
|
||||||
|
linkToOriginal: "Nuoroda į originalą",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Paieška",
|
||||||
|
searchBarPlaceholder: "Ieškoti",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Turinys",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `${minutes} min skaitymo`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Naujausi užrašai",
|
||||||
|
lastFewNotes: ({ count }) =>
|
||||||
|
count === 1
|
||||||
|
? "Paskutinis 1 užrašas"
|
||||||
|
: count < 10
|
||||||
|
? `Paskutiniai ${count} užrašai`
|
||||||
|
: `Paskutiniai ${count} užrašų`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Nerasta",
|
||||||
|
notFound:
|
||||||
|
"Arba šis puslapis yra pasiekiamas tik tam tikriems vartotojams, arba tokio puslapio nėra.",
|
||||||
|
home: "Grįžti į pagrindinį puslapį",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Aplankas",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1
|
||||||
|
? "1 elementas šiame aplanke."
|
||||||
|
: count < 10
|
||||||
|
? `${count} elementai šiame aplanke.`
|
||||||
|
: `${count} elementų šiame aplanke.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Žyma",
|
||||||
|
tagIndex: "Žymų indeksas",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1
|
||||||
|
? "1 elementas su šia žyma."
|
||||||
|
: count < 10
|
||||||
|
? `${count} elementai su šia žyma.`
|
||||||
|
: `${count} elementų su šia žyma.`,
|
||||||
|
showingFirst: ({ count }) =>
|
||||||
|
count < 10 ? `Rodomos pirmosios ${count} žymos.` : `Rodomos pirmosios ${count} žymų.`,
|
||||||
|
totalTags: ({ count }) =>
|
||||||
|
count === 1
|
||||||
|
? "Rasta iš viso 1 žyma."
|
||||||
|
: count < 10
|
||||||
|
? `Rasta iš viso ${count} žymos.`
|
||||||
|
: `Rasta iš viso ${count} žymų.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
82
quartz/i18n/locales/th-TH.ts
Normal file
82
quartz/i18n/locales/th-TH.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "ไม่มีชื่อ",
|
||||||
|
description: "ไม่ได้ระบุคำอธิบายย่อ",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "หมายเหตุ",
|
||||||
|
abstract: "บทคัดย่อ",
|
||||||
|
info: "ข้อมูล",
|
||||||
|
todo: "ต้องทำเพิ่มเติม",
|
||||||
|
tip: "คำแนะนำ",
|
||||||
|
success: "เรียบร้อย",
|
||||||
|
question: "คำถาม",
|
||||||
|
warning: "คำเตือน",
|
||||||
|
failure: "ข้อผิดพลาด",
|
||||||
|
danger: "อันตราย",
|
||||||
|
bug: "บั๊ก",
|
||||||
|
example: "ตัวอย่าง",
|
||||||
|
quote: "คำพูกยกมา",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "หน้าที่กล่าวถึง",
|
||||||
|
noBacklinksFound: "ไม่มีหน้าที่โยงมาหน้านี้",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "โหมดสว่าง",
|
||||||
|
darkMode: "โหมดมืด",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "รายการหน้า",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "สร้างด้วย",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "มุมมองกราฟ",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "บันทึกล่าสุด",
|
||||||
|
seeRemainingMore: ({ remaining }) => `ดูเพิ่มอีก ${remaining} รายการ →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`,
|
||||||
|
linkToOriginal: "ดูหน้าต้นทาง",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "ค้นหา",
|
||||||
|
searchBarPlaceholder: "ค้นหาบางอย่าง",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "สารบัญ",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `อ่านราว ${minutes} นาที`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "บันทึกล่าสุด",
|
||||||
|
lastFewNotes: ({ count }) => `${count} บันทึกล่าสุด`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "ไม่มีหน้านี้",
|
||||||
|
notFound: "หน้านี้อาจตั้งค่าเป็นส่วนตัวหรือยังไม่ถูกสร้าง",
|
||||||
|
home: "กลับหน้าหลัก",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "โฟลเดอร์",
|
||||||
|
itemsUnderFolder: ({ count }) => `มี ${count} รายการในโฟลเดอร์นี้`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "แท็ก",
|
||||||
|
tagIndex: "แท็กทั้งหมด",
|
||||||
|
itemsUnderTag: ({ count }) => `มี ${count} รายการในแท็กนี้`,
|
||||||
|
showingFirst: ({ count }) => `แสดง ${count} แท็กแรก`,
|
||||||
|
totalTags: ({ count }) => `มีทั้งหมด ${count} แท็ก`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
82
quartz/i18n/locales/zh-TW.ts
Normal file
82
quartz/i18n/locales/zh-TW.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "無題",
|
||||||
|
description: "無描述",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "筆記",
|
||||||
|
abstract: "摘要",
|
||||||
|
info: "提示",
|
||||||
|
todo: "待辦",
|
||||||
|
tip: "提示",
|
||||||
|
success: "成功",
|
||||||
|
question: "問題",
|
||||||
|
warning: "警告",
|
||||||
|
failure: "失敗",
|
||||||
|
danger: "危險",
|
||||||
|
bug: "錯誤",
|
||||||
|
example: "範例",
|
||||||
|
quote: "引用",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "反向連結",
|
||||||
|
noBacklinksFound: "無法找到反向連結",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "亮色模式",
|
||||||
|
darkMode: "暗色模式",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "探索",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Created with",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "關係圖譜",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "最近的筆記",
|
||||||
|
seeRemainingMore: ({ remaining }) => `查看更多 ${remaining} 篇筆記 →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`,
|
||||||
|
linkToOriginal: "指向原始筆記的連結",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "搜尋",
|
||||||
|
searchBarPlaceholder: "搜尋些什麼",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "目錄",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `閱讀時間約 ${minutes} 分鐘`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "最近的筆記",
|
||||||
|
lastFewNotes: ({ count }) => `最近的 ${count} 條筆記`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "無法找到",
|
||||||
|
notFound: "私人筆記或筆記不存在。",
|
||||||
|
home: "返回首頁",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "資料夾",
|
||||||
|
itemsUnderFolder: ({ count }) => `此資料夾下有 ${count} 條筆記。`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "標籤",
|
||||||
|
tagIndex: "標籤索引",
|
||||||
|
itemsUnderTag: ({ count }) => `此標籤下有 ${count} 條筆記。`,
|
||||||
|
showingFirst: ({ count }) => `顯示前 ${count} 個標籤。`,
|
||||||
|
totalTags: ({ count }) => `總共有 ${count} 個標籤。`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@ -37,7 +37,6 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
|
|
||||||
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
const path = url.pathname as FullSlug
|
const path = url.pathname as FullSlug
|
||||||
const externalResources = pageResources(path, resources)
|
|
||||||
const notFound = i18n(cfg.locale).pages.error.title
|
const notFound = i18n(cfg.locale).pages.error.title
|
||||||
const [tree, vfile] = defaultProcessedContent({
|
const [tree, vfile] = defaultProcessedContent({
|
||||||
slug,
|
slug,
|
||||||
@ -45,6 +44,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
description: notFound,
|
description: notFound,
|
||||||
frontmatter: { title: notFound, tags: [] },
|
frontmatter: { title: notFound, tags: [] },
|
||||||
})
|
})
|
||||||
|
const externalResources = pageResources(path, vfile.data, resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
ctx,
|
ctx,
|
||||||
fileData: vfile.data,
|
fileData: vfile.data,
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
containsIndex = true
|
containsIndex = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
ctx,
|
ctx,
|
||||||
fileData: file.data,
|
fileData: file.data,
|
||||||
|
|||||||
@ -106,8 +106,8 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
|
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
const slug = joinSegments(folder, "index") as FullSlug
|
const slug = joinSegments(folder, "index") as FullSlug
|
||||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
|
||||||
const [tree, file] = folderDescriptions[folder]
|
const [tree, file] = folderDescriptions[folder]
|
||||||
|
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
ctx,
|
ctx,
|
||||||
fileData: file.data,
|
fileData: file.data,
|
||||||
|
|||||||
@ -105,14 +105,17 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
|||||||
const tag = slug.slice("tags/".length)
|
const tag = slug.slice("tags/".length)
|
||||||
if (tags.has(tag)) {
|
if (tags.has(tag)) {
|
||||||
tagDescriptions[tag] = [tree, file]
|
tagDescriptions[tag] = [tree, file]
|
||||||
|
if (file.data.frontmatter?.title === tag) {
|
||||||
|
file.data.frontmatter.title = `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const slug = joinSegments("tags", tag) as FullSlug
|
const slug = joinSegments("tags", tag) as FullSlug
|
||||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
|
||||||
const [tree, file] = tagDescriptions[tag]
|
const [tree, file] = tagDescriptions[tag]
|
||||||
|
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
ctx,
|
ctx,
|
||||||
fileData: file.data,
|
fileData: file.data,
|
||||||
|
|||||||
@ -73,6 +73,18 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
|
|
||||||
const socialImage = coalesceAliases(data, ["socialImage", "image", "cover"])
|
const socialImage = coalesceAliases(data, ["socialImage", "image", "cover"])
|
||||||
|
|
||||||
|
const created = coalesceAliases(data, ["created", "date"])
|
||||||
|
if (created) data.created = created
|
||||||
|
const modified = coalesceAliases(data, [
|
||||||
|
"modified",
|
||||||
|
"lastmod",
|
||||||
|
"updated",
|
||||||
|
"last-modified",
|
||||||
|
])
|
||||||
|
if (modified) data.modified = modified
|
||||||
|
const published = coalesceAliases(data, ["published", "publishDate", "date"])
|
||||||
|
if (published) data.published = published
|
||||||
|
|
||||||
if (socialImage) data.socialImage = socialImage
|
if (socialImage) data.socialImage = socialImage
|
||||||
|
|
||||||
// fill in frontmatter
|
// fill in frontmatter
|
||||||
@ -91,6 +103,9 @@ declare module "vfile" {
|
|||||||
} & Partial<{
|
} & Partial<{
|
||||||
tags: string[]
|
tags: string[]
|
||||||
aliases: string[]
|
aliases: string[]
|
||||||
|
modified: string
|
||||||
|
created: string
|
||||||
|
published: string
|
||||||
description: string
|
description: string
|
||||||
publish: boolean | string
|
publish: boolean | string
|
||||||
draft: boolean | string
|
draft: boolean | string
|
||||||
|
|||||||
@ -48,11 +48,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
|||||||
created ||= st.birthtimeMs
|
created ||= st.birthtimeMs
|
||||||
modified ||= st.mtimeMs
|
modified ||= st.mtimeMs
|
||||||
} else if (source === "frontmatter" && file.data.frontmatter) {
|
} else if (source === "frontmatter" && file.data.frontmatter) {
|
||||||
created ||= file.data.frontmatter.date as MaybeDate
|
created ||= file.data.frontmatter.created as MaybeDate
|
||||||
modified ||= file.data.frontmatter.lastmod as MaybeDate
|
modified ||= file.data.frontmatter.modified as MaybeDate
|
||||||
modified ||= file.data.frontmatter.updated as MaybeDate
|
published ||= file.data.frontmatter.published as MaybeDate
|
||||||
modified ||= file.data.frontmatter["last-modified"] as MaybeDate
|
|
||||||
published ||= file.data.frontmatter.publishDate as MaybeDate
|
|
||||||
} else if (source === "git") {
|
} else if (source === "git") {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
// Get a reference to the main git repo.
|
// Get a reference to the main git repo.
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
|
import {
|
||||||
|
Root,
|
||||||
|
Html,
|
||||||
|
BlockContent,
|
||||||
|
PhrasingContent,
|
||||||
|
DefinitionContent,
|
||||||
|
Paragraph,
|
||||||
|
Code,
|
||||||
|
} from "mdast"
|
||||||
import { Element, Literal, Root as HtmlRoot } from "hast"
|
import { Element, Literal, Root as HtmlRoot } from "hast"
|
||||||
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
@ -11,13 +19,9 @@ import { JSResource, CSSResource } from "../../util/resources"
|
|||||||
import calloutScript from "../../components/scripts/callout.inline.ts"
|
import calloutScript from "../../components/scripts/callout.inline.ts"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import checkboxScript from "../../components/scripts/checkbox.inline.ts"
|
import checkboxScript from "../../components/scripts/checkbox.inline.ts"
|
||||||
// @ts-ignore
|
|
||||||
import mermaidExtensionScript from "../../components/scripts/mermaid.inline.ts"
|
|
||||||
import mermaidStyle from "../../components/styles/mermaid.inline.scss"
|
|
||||||
import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
|
import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
|
||||||
import { toHast } from "mdast-util-to-hast"
|
import { toHast } from "mdast-util-to-hast"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
|
|
||||||
import { capitalize } from "../../util/lang"
|
import { capitalize } from "../../util/lang"
|
||||||
import { PluggableList } from "unified"
|
import { PluggableList } from "unified"
|
||||||
|
|
||||||
@ -124,12 +128,12 @@ const commentRegex = new RegExp(/%%[\s\S]*?%%/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
|
||||||
const calloutRegex = new RegExp(/^\[\!([\w-]+)\|?(.+?)?\]([+-]?)/)
|
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
|
// (?<=^| ) -> a lookbehind assertion, 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}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, 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}\d\p{Z}]+)*) -> 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(
|
const tagRegex = new RegExp(
|
||||||
/(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu,
|
/(?<=^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu,
|
||||||
)
|
)
|
||||||
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g)
|
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g)
|
||||||
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
||||||
@ -156,7 +160,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
src = src.toString()
|
src = src.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
src = src.replace(commentRegex, "")
|
src = (src as string).replace(commentRegex, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-transform blockquotes
|
// pre-transform blockquotes
|
||||||
@ -165,7 +169,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
src = src.toString()
|
src = src.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
src = src.replace(calloutLineRegex, (value) => {
|
src = (src as string).replace(calloutLineRegex, (value) => {
|
||||||
// force newline after title of callout
|
// force newline after title of callout
|
||||||
return value + "\n> "
|
return value + "\n> "
|
||||||
})
|
})
|
||||||
@ -178,7 +182,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replace all wikilinks inside a table first
|
// replace all wikilinks inside a table first
|
||||||
src = src.replace(tableRegex, (value) => {
|
src = (src as string).replace(tableRegex, (value) => {
|
||||||
// escape all aliases and headers in wikilinks inside a table
|
// escape all aliases and headers in wikilinks inside a table
|
||||||
return value.replace(tableWikilinkRegex, (_value, raw) => {
|
return value.replace(tableWikilinkRegex, (_value, raw) => {
|
||||||
// const [raw]: (string | undefined)[] = capture
|
// const [raw]: (string | undefined)[] = capture
|
||||||
@ -192,7 +196,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
})
|
})
|
||||||
|
|
||||||
// replace all other wikilinks
|
// replace all other wikilinks
|
||||||
src = src.replace(wikilinkRegex, (value, ...capture) => {
|
src = (src as string).replace(wikilinkRegex, (value, ...capture) => {
|
||||||
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
|
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
|
||||||
|
|
||||||
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
|
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
|
||||||
@ -513,9 +517,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
|
|
||||||
if (opts.mermaid) {
|
if (opts.mermaid) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, file) => {
|
||||||
visit(tree, "code", (node: Code) => {
|
visit(tree, "code", (node: Code) => {
|
||||||
if (node.lang === "mermaid") {
|
if (node.lang === "mermaid") {
|
||||||
|
file.data.hasMermaidDiagram = true
|
||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
className: ["mermaid"],
|
className: ["mermaid"],
|
||||||
@ -813,19 +818,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.mermaid) {
|
|
||||||
js.push({
|
|
||||||
script: mermaidExtensionScript,
|
|
||||||
loadTime: "afterDOMReady",
|
|
||||||
moduleType: "module",
|
|
||||||
contentType: "inline",
|
|
||||||
})
|
|
||||||
css.push({
|
|
||||||
content: mermaidStyle,
|
|
||||||
inline: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { js, css }
|
return { js, css }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -835,5 +827,6 @@ declare module "vfile" {
|
|||||||
interface DataMap {
|
interface DataMap {
|
||||||
blocks: Record<string, Element>
|
blocks: Record<string, Element>
|
||||||
htmlAst: HtmlRoot
|
htmlAst: HtmlRoot
|
||||||
|
hasMermaidDiagram: boolean | undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Node, Parent } from "hast"
|
import { Root as HtmlRoot } from "hast"
|
||||||
|
import { Root as MdRoot } from "mdast"
|
||||||
import { Data, VFile } from "vfile"
|
import { Data, VFile } from "vfile"
|
||||||
|
|
||||||
export type QuartzPluginData = Data
|
export type QuartzPluginData = Data
|
||||||
export type ProcessedContent = [Node, VFile]
|
export type MarkdownContent = [MdRoot, VFile]
|
||||||
|
export type ProcessedContent = [HtmlRoot, VFile]
|
||||||
|
|
||||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
||||||
const root: Parent = { type: "root", children: [] }
|
const root: HtmlRoot = { type: "root", children: [] }
|
||||||
const vfile = new VFile("")
|
const vfile = new VFile("")
|
||||||
vfile.data = vfileData
|
vfile.data = vfileData
|
||||||
return [root, vfile]
|
return [root, vfile]
|
||||||
|
|||||||
@ -4,18 +4,20 @@ import remarkRehype from "remark-rehype"
|
|||||||
import { Processor, unified } from "unified"
|
import { Processor, unified } from "unified"
|
||||||
import { Root as MDRoot } from "remark-parse/lib"
|
import { Root as MDRoot } from "remark-parse/lib"
|
||||||
import { Root as HTMLRoot } from "hast"
|
import { Root as HTMLRoot } from "hast"
|
||||||
import { ProcessedContent } from "../plugins/vfile"
|
import { MarkdownContent, ProcessedContent } from "../plugins/vfile"
|
||||||
import { PerfTimer } from "../util/perf"
|
import { PerfTimer } from "../util/perf"
|
||||||
import { read } from "to-vfile"
|
import { read } from "to-vfile"
|
||||||
import { FilePath, QUARTZ, slugifyFilePath } from "../util/path"
|
import { FilePath, FullSlug, QUARTZ, slugifyFilePath } from "../util/path"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
||||||
import { QuartzLogger } from "../util/log"
|
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, MDRoot, HTMLRoot>
|
export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
|
||||||
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
|
||||||
|
|
||||||
|
export function createMdProcessor(ctx: BuildCtx): QuartzMdProcessor {
|
||||||
const transformers = ctx.cfg.plugins.transformers
|
const transformers = ctx.cfg.plugins.transformers
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,14 +26,20 @@ export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
|||||||
.use(remarkParse)
|
.use(remarkParse)
|
||||||
// MD AST -> MD AST transforms
|
// MD AST -> MD AST transforms
|
||||||
.use(
|
.use(
|
||||||
transformers
|
transformers.flatMap((plugin) => plugin.markdownPlugins?.(ctx) ?? []),
|
||||||
.filter((p) => p.markdownPlugins)
|
) as unknown as QuartzMdProcessor
|
||||||
.flatMap((plugin) => plugin.markdownPlugins!(ctx)),
|
// ^ sadly the typing of `use` is not smart enough to infer the correct type from our plugin list
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHtmlProcessor(ctx: BuildCtx): QuartzHtmlProcessor {
|
||||||
|
const transformers = ctx.cfg.plugins.transformers
|
||||||
|
return (
|
||||||
|
unified()
|
||||||
// MD AST -> HTML AST
|
// MD AST -> HTML AST
|
||||||
.use(remarkRehype, { allowDangerousHtml: true })
|
.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
// HTML AST -> HTML AST transforms
|
// HTML AST -> HTML AST transforms
|
||||||
.use(transformers.filter((p) => p.htmlPlugins).flatMap((plugin) => plugin.htmlPlugins!(ctx)))
|
.use(transformers.flatMap((plugin) => plugin.htmlPlugins?.(ctx) ?? []))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +83,8 @@ async function transpileWorkerScript() {
|
|||||||
|
|
||||||
export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
||||||
const { argv, cfg } = ctx
|
const { argv, cfg } = ctx
|
||||||
return async (processor: QuartzProcessor) => {
|
return async (processor: QuartzMdProcessor) => {
|
||||||
const res: ProcessedContent[] = []
|
const res: MarkdownContent[] = []
|
||||||
for (const fp of fps) {
|
for (const fp of fps) {
|
||||||
try {
|
try {
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
@ -100,10 +108,32 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
|||||||
res.push([newAst, file])
|
res.push([newAst, file])
|
||||||
|
|
||||||
if (argv.verbose) {
|
if (argv.verbose) {
|
||||||
console.log(`[process] ${fp} -> ${file.data.slug} (${perf.timeSince()})`)
|
console.log(`[markdown] ${fp} -> ${file.data.slug} (${perf.timeSince()})`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
trace(`\nFailed to process \`${fp}\``, err as Error)
|
trace(`\nFailed to process markdown \`${fp}\``, err as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkdownParser(ctx: BuildCtx, mdContent: MarkdownContent[]) {
|
||||||
|
return async (processor: QuartzHtmlProcessor) => {
|
||||||
|
const res: ProcessedContent[] = []
|
||||||
|
for (const [ast, file] of mdContent) {
|
||||||
|
try {
|
||||||
|
const perf = new PerfTimer()
|
||||||
|
|
||||||
|
const newAst = await processor.run(ast as MDRoot, file)
|
||||||
|
res.push([newAst, file])
|
||||||
|
|
||||||
|
if (ctx.argv.verbose) {
|
||||||
|
console.log(`[html] ${file.data.slug} (${perf.timeSince()})`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
trace(`\nFailed to process html \`${file.data.filePath}\``, err as Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +143,7 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
|||||||
|
|
||||||
const clamp = (num: number, min: number, max: number) =>
|
const clamp = (num: number, min: number, max: number) =>
|
||||||
Math.min(Math.max(Math.round(num), min), max)
|
Math.min(Math.max(Math.round(num), min), max)
|
||||||
|
|
||||||
export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<ProcessedContent[]> {
|
export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<ProcessedContent[]> {
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
@ -126,9 +157,8 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
log.start(`Parsing input files using ${concurrency} threads`)
|
log.start(`Parsing input files using ${concurrency} threads`)
|
||||||
if (concurrency === 1) {
|
if (concurrency === 1) {
|
||||||
try {
|
try {
|
||||||
const processor = createProcessor(ctx)
|
const mdRes = await createFileParser(ctx, fps)(createMdProcessor(ctx))
|
||||||
const parse = createFileParser(ctx, fps)
|
res = await createMarkdownParser(ctx, mdRes)(createHtmlProcessor(ctx))
|
||||||
res = await parse(processor)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.end()
|
log.end()
|
||||||
throw error
|
throw error
|
||||||
@ -140,17 +170,27 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
maxWorkers: concurrency,
|
maxWorkers: concurrency,
|
||||||
workerType: "thread",
|
workerType: "thread",
|
||||||
})
|
})
|
||||||
|
const errorHandler = (err: any) => {
|
||||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
console.error(`${err}`.replace(/^error:\s*/i, ""))
|
||||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
process.exit(1)
|
||||||
childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {
|
const mdPromises: WorkerPromise<[MarkdownContent[], FullSlug[]]>[] = []
|
||||||
const errString = err.toString().slice("Error:".length)
|
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||||
console.error(errString)
|
mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, argv, chunk]))
|
||||||
process.exit(1)
|
}
|
||||||
})
|
const mdResults: [MarkdownContent[], FullSlug[]][] =
|
||||||
|
await WorkerPromise.all(mdPromises).catch(errorHandler)
|
||||||
|
|
||||||
|
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||||
|
for (const [_, extraSlugs] of mdResults) {
|
||||||
|
ctx.allSlugs.push(...extraSlugs)
|
||||||
|
}
|
||||||
|
for (const [mdChunk, _] of mdResults) {
|
||||||
|
childPromises.push(pool.exec("processHtml", [ctx.buildId, argv, mdChunk, ctx.allSlugs]))
|
||||||
|
}
|
||||||
|
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch(errorHandler)
|
||||||
|
|
||||||
res = results.flat()
|
res = results.flat()
|
||||||
await pool.terminate()
|
await pool.terminate()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
@use "sass:map";
|
||||||
|
|
||||||
@use "./variables.scss" as *;
|
@use "./variables.scss" as *;
|
||||||
@use "./syntax.scss";
|
@use "./syntax.scss";
|
||||||
@use "./callouts.scss";
|
@use "./callouts.scss";
|
||||||
@ -85,7 +87,7 @@ a {
|
|||||||
line-height: 1.4rem;
|
line-height: 1.4rem;
|
||||||
|
|
||||||
&:has(> img) {
|
&:has(> img) {
|
||||||
background-color: none;
|
background-color: transparent;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -121,7 +123,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
max-width: calc(#{map-get($breakpoints, desktop)} + 300px);
|
max-width: calc(#{map.get($breakpoints, desktop)} + 300px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
& article {
|
& article {
|
||||||
& > h1 {
|
& > h1 {
|
||||||
@ -151,24 +153,25 @@ a {
|
|||||||
|
|
||||||
& > #quartz-body {
|
& > #quartz-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: #{map-get($desktopGrid, templateColumns)};
|
grid-template-columns: #{map.get($desktopGrid, templateColumns)};
|
||||||
grid-template-rows: #{map-get($desktopGrid, templateRows)};
|
grid-template-rows: #{map.get($desktopGrid, templateRows)};
|
||||||
column-gap: #{map-get($desktopGrid, columnGap)};
|
column-gap: #{map.get($desktopGrid, columnGap)};
|
||||||
row-gap: #{map-get($desktopGrid, rowGap)};
|
row-gap: #{map.get($desktopGrid, rowGap)};
|
||||||
grid-template-areas: #{map-get($desktopGrid, templateAreas)};
|
grid-template-areas: #{map.get($desktopGrid, templateAreas)};
|
||||||
|
|
||||||
@media all and ($tablet) {
|
@media all and ($tablet) {
|
||||||
grid-template-columns: #{map-get($tabletGrid, templateColumns)};
|
grid-template-columns: #{map.get($tabletGrid, templateColumns)};
|
||||||
grid-template-rows: #{map-get($tabletGrid, templateRows)};
|
grid-template-rows: #{map.get($tabletGrid, templateRows)};
|
||||||
column-gap: #{map-get($tabletGrid, columnGap)};
|
column-gap: #{map.get($tabletGrid, columnGap)};
|
||||||
row-gap: #{map-get($tabletGrid, rowGap)};
|
row-gap: #{map.get($tabletGrid, rowGap)};
|
||||||
grid-template-areas: #{map-get($tabletGrid, templateAreas)};
|
grid-template-areas: #{map.get($tabletGrid, templateAreas)};
|
||||||
}
|
}
|
||||||
@media all and ($mobile) {
|
@media all and ($mobile) {
|
||||||
grid-template-columns: #{map-get($mobileGrid, templateColumns)};
|
grid-template-columns: #{map.get($mobileGrid, templateColumns)};
|
||||||
grid-template-rows: #{map-get($mobileGrid, templateRows)};
|
grid-template-rows: #{map.get($mobileGrid, templateRows)};
|
||||||
column-gap: #{map-get($mobileGrid, columnGap)};
|
column-gap: #{map.get($mobileGrid, columnGap)};
|
||||||
row-gap: #{map-get($mobileGrid, rowGap)};
|
row-gap: #{map.get($mobileGrid, rowGap)};
|
||||||
grid-template-areas: #{map-get($mobileGrid, templateAreas)};
|
grid-template-areas: #{map.get($mobileGrid, templateAreas)};
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and not ($desktop) {
|
@media all and not ($desktop) {
|
||||||
@ -388,7 +391,7 @@ figure[data-rehype-pretty-code-figure] {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
width: max-content;
|
width: fit-content;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: -0.5rem;
|
margin-bottom: -0.5rem;
|
||||||
color: var(--darkgray);
|
color: var(--darkgray);
|
||||||
@ -512,6 +515,7 @@ img {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
content-visibility: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p > img + em {
|
p > img + em {
|
||||||
@ -587,3 +591,14 @@ iframe.pdf {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--secondary);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
@use "sass:map";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout breakpoints
|
* Layout breakpoints
|
||||||
* $mobile: screen width below this value will use mobile styles
|
* $mobile: screen width below this value will use mobile styles
|
||||||
@ -10,11 +12,11 @@ $breakpoints: (
|
|||||||
desktop: 1200px,
|
desktop: 1200px,
|
||||||
);
|
);
|
||||||
|
|
||||||
$mobile: "(max-width: #{map-get($breakpoints, mobile)})";
|
$mobile: "(max-width: #{map.get($breakpoints, mobile)})";
|
||||||
$tablet: "(min-width: #{map-get($breakpoints, mobile)}) and (max-width: #{map-get($breakpoints, desktop)})";
|
$tablet: "(min-width: #{map.get($breakpoints, mobile)}) and (max-width: #{map.get($breakpoints, desktop)})";
|
||||||
$desktop: "(min-width: #{map-get($breakpoints, desktop)})";
|
$desktop: "(min-width: #{map.get($breakpoints, desktop)})";
|
||||||
|
|
||||||
$pageWidth: #{map-get($breakpoints, mobile)};
|
$pageWidth: #{map.get($breakpoints, mobile)};
|
||||||
$sidePanelWidth: 320px; //380px;
|
$sidePanelWidth: 320px; //380px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$boldWeight: 700;
|
$boldWeight: 700;
|
||||||
|
|||||||
@ -35,7 +35,9 @@ export async function getSatoriFont(headerFontName: string, bodyFontName: string
|
|||||||
async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> {
|
async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> {
|
||||||
try {
|
try {
|
||||||
// Get css file from google fonts
|
// Get css file from google fonts
|
||||||
const cssResponse = await fetch(`https://fonts.googleapis.com/css?family=${fontName}:${weight}`)
|
const cssResponse = await fetch(
|
||||||
|
`https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`,
|
||||||
|
)
|
||||||
const css = await cssResponse.text()
|
const css = await cssResponse.text()
|
||||||
|
|
||||||
// Extract .ttf url from css file
|
// Extract .ttf url from css file
|
||||||
|
|||||||
@ -158,6 +158,29 @@ describe("transforms", () => {
|
|||||||
path.isRelativeURL,
|
path.isRelativeURL,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("joinSegments", () => {
|
||||||
|
assert.strictEqual(path.joinSegments("a", "b"), "a/b")
|
||||||
|
assert.strictEqual(path.joinSegments("a/", "b"), "a/b")
|
||||||
|
assert.strictEqual(path.joinSegments("a", "b/"), "a/b/")
|
||||||
|
assert.strictEqual(path.joinSegments("a/", "b/"), "a/b/")
|
||||||
|
|
||||||
|
// preserve leading and trailing slashes
|
||||||
|
assert.strictEqual(path.joinSegments("/a", "b"), "/a/b")
|
||||||
|
assert.strictEqual(path.joinSegments("/a/", "b"), "/a/b")
|
||||||
|
assert.strictEqual(path.joinSegments("/a", "b/"), "/a/b/")
|
||||||
|
assert.strictEqual(path.joinSegments("/a/", "b/"), "/a/b/")
|
||||||
|
|
||||||
|
// lone slash
|
||||||
|
assert.strictEqual(path.joinSegments("/a/", "b", "/"), "/a/b/")
|
||||||
|
assert.strictEqual(path.joinSegments("a/", "b" + "/"), "a/b/")
|
||||||
|
|
||||||
|
// works with protocol specifiers
|
||||||
|
assert.strictEqual(path.joinSegments("https://example.com", "a"), "https://example.com/a")
|
||||||
|
assert.strictEqual(path.joinSegments("https://example.com/", "a"), "https://example.com/a")
|
||||||
|
assert.strictEqual(path.joinSegments("https://example.com", "a/"), "https://example.com/a/")
|
||||||
|
assert.strictEqual(path.joinSegments("https://example.com/", "a/"), "https://example.com/a/")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("link strategies", () => {
|
describe("link strategies", () => {
|
||||||
|
|||||||
@ -108,10 +108,10 @@ const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) =>
|
|||||||
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
||||||
}
|
}
|
||||||
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
||||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
el.querySelectorAll('[href=""], [href^="./"], [href^="../"]').forEach((item) =>
|
||||||
_rebaseHtmlElement(item, "href", destination),
|
_rebaseHtmlElement(item, "href", destination),
|
||||||
)
|
)
|
||||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
el.querySelectorAll('[src=""], [src^="./"], [src^="../"]').forEach((item) =>
|
||||||
_rebaseHtmlElement(item, "src", destination),
|
_rebaseHtmlElement(item, "src", destination),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -183,10 +183,26 @@ export function slugTag(tag: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function joinSegments(...args: string[]): string {
|
export function joinSegments(...args: string[]): string {
|
||||||
return args
|
if (args.length === 0) {
|
||||||
.filter((segment) => segment !== "")
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let joined = args
|
||||||
|
.filter((segment) => segment !== "" && segment !== "/")
|
||||||
|
.map((segment) => stripSlashes(segment))
|
||||||
.join("/")
|
.join("/")
|
||||||
.replace(/\/\/+/g, "/")
|
|
||||||
|
// if the first segment starts with a slash, add it back
|
||||||
|
if (args[0].startsWith("/")) {
|
||||||
|
joined = "/" + joined
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the last segment is a folder, add a trailing slash
|
||||||
|
if (args[args.length - 1].endsWith("/")) {
|
||||||
|
joined = joined + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return joined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllSegmentPrefixes(tags: string): string[] {
|
export function getAllSegmentPrefixes(tags: string): string[] {
|
||||||
|
|||||||
@ -3,23 +3,46 @@ sourceMapSupport.install(options)
|
|||||||
import cfg from "../quartz.config"
|
import cfg from "../quartz.config"
|
||||||
import { Argv, BuildCtx } from "./util/ctx"
|
import { Argv, BuildCtx } from "./util/ctx"
|
||||||
import { FilePath, FullSlug } from "./util/path"
|
import { FilePath, FullSlug } from "./util/path"
|
||||||
import { createFileParser, createProcessor } from "./processors/parse"
|
import {
|
||||||
|
createFileParser,
|
||||||
|
createHtmlProcessor,
|
||||||
|
createMarkdownParser,
|
||||||
|
createMdProcessor,
|
||||||
|
} from "./processors/parse"
|
||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
|
import { MarkdownContent, ProcessedContent } from "./plugins/vfile"
|
||||||
|
|
||||||
// only called from worker thread
|
// only called from worker thread
|
||||||
export async function parseFiles(
|
export async function parseMarkdown(
|
||||||
buildId: string,
|
buildId: string,
|
||||||
argv: Argv,
|
argv: Argv,
|
||||||
fps: FilePath[],
|
fps: FilePath[],
|
||||||
allSlugs: FullSlug[],
|
): Promise<[MarkdownContent[], FullSlug[]]> {
|
||||||
) {
|
// this is a hack
|
||||||
|
// we assume markdown parsers can add to `allSlugs`,
|
||||||
|
// but don't actually use them
|
||||||
|
const allSlugs: FullSlug[] = []
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
buildId,
|
buildId,
|
||||||
cfg,
|
cfg,
|
||||||
argv,
|
argv,
|
||||||
allSlugs,
|
allSlugs,
|
||||||
}
|
}
|
||||||
const processor = createProcessor(ctx)
|
return [await createFileParser(ctx, fps)(createMdProcessor(ctx)), allSlugs]
|
||||||
const parse = createFileParser(ctx, fps)
|
}
|
||||||
return parse(processor)
|
|
||||||
|
// only called from worker thread
|
||||||
|
export function processHtml(
|
||||||
|
buildId: string,
|
||||||
|
argv: Argv,
|
||||||
|
mds: MarkdownContent[],
|
||||||
|
allSlugs: FullSlug[],
|
||||||
|
): Promise<ProcessedContent[]> {
|
||||||
|
const ctx: BuildCtx = {
|
||||||
|
buildId,
|
||||||
|
cfg,
|
||||||
|
argv,
|
||||||
|
allSlugs,
|
||||||
|
}
|
||||||
|
return createMarkdownParser(ctx, mds)(createHtmlProcessor(ctx))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user