mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-20 03:14:06 -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
|
||||
- name: Install cosign
|
||||
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
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
@ -274,7 +274,7 @@ export const ContentPage: QuartzEmitterPlugin = () => {
|
||||
const allFiles = content.map((c) => c[1].data)
|
||||
for (const [tree, file] of content) {
|
||||
const slug = canonicalizeServer(file.data.slug!)
|
||||
const externalResources = pageResources(slug, resources)
|
||||
const externalResources = pageResources(slug, file.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
fileData: file.data,
|
||||
externalResources,
|
||||
|
||||
@ -9,6 +9,7 @@ A backlink for a note is a link from another note to that note. Links in the bac
|
||||
## Customization
|
||||
|
||||
- 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`
|
||||
- Style: `quartz/components/styles/backlinks.scss`
|
||||
- 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?
|
||||
removeTags: [], // what tags to remove from the graph
|
||||
showTags: true, // whether to show tags in the graph
|
||||
enableRadial: false, // whether to constrain the graph, similar to Obsidian
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
@ -49,6 +50,7 @@ Component.Graph({
|
||||
opacityScale: 1,
|
||||
removeTags: [], // what tags to remove from 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
|
||||
- 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)
|
||||
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
||||
- [🌓 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)!
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.8.1",
|
||||
"@floating-ui/dom": "^1.6.12",
|
||||
"@myriaddreamin/rehype-typst": "^0.5.0-rc7",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||
"@napi-rs/simple-git": "0.1.19",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chalk": "^5.4.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"d3": "^7.9.0",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"flexsearch": "0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"globby": "^14.1.0",
|
||||
"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-string": "^3.0.1",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lightningcss": "^1.28.1",
|
||||
"mdast-util-find-and-replace": "^3.0.1",
|
||||
"lightningcss": "^1.29.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"mdast-util-to-hast": "^13.2.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"micromorph": "^0.4.5",
|
||||
"pixi.js": "^8.5.2",
|
||||
"preact": "^10.24.3",
|
||||
"preact-render-to-string": "^6.5.11",
|
||||
"pixi.js": "^8.7.3",
|
||||
"preact": "^10.25.4",
|
||||
"preact-render-to-string": "^6.5.13",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pretty-time": "^1.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
@ -77,17 +76,17 @@
|
||||
"remark": "^15.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-smartypants": "^3.0.2",
|
||||
"rfdc": "^1.4.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"satori": "^0.11.3",
|
||||
"satori": "^0.12.1",
|
||||
"serve-handler": "^6.1.6",
|
||||
"sharp": "^0.33.5",
|
||||
"shiki": "^1.23.1",
|
||||
"shiki": "^1.26.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"to-vfile": "^8.0.0",
|
||||
"toml": "^3.0.0",
|
||||
@ -103,14 +102,14 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"esbuild": "^0.24.0",
|
||||
"prettier": "^3.3.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"prettier": "^3.5.0",
|
||||
"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.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
Component.Explorer(),
|
||||
],
|
||||
right: [
|
||||
Component.Graph(),
|
||||
@ -44,7 +44,7 @@ export const defaultListPageLayout: PageLayout = {
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
Component.Explorer(),
|
||||
],
|
||||
right: [],
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import workerpool from "workerpool"
|
||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
||||
const { parseFiles } = await import(cacheFile)
|
||||
const { parseMarkdown, processHtml } = await import(cacheFile)
|
||||
workerpool.worker({
|
||||
parseFiles,
|
||||
parseMarkdown,
|
||||
processHtml,
|
||||
})
|
||||
|
||||
@ -139,9 +139,9 @@ async function startServing(
|
||||
|
||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
||||
watcher
|
||||
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
|
||||
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
|
||||
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
|
||||
.on("add", (fp) => buildFromEntry(fp as string, "add", clientRefresh, buildData))
|
||||
.on("change", (fp) => buildFromEntry(fp as string, "change", clientRefresh, buildData))
|
||||
.on("unlink", (fp) => buildFromEntry(fp as string, "delete", clientRefresh, buildData))
|
||||
|
||||
return async () => {
|
||||
await watcher.close()
|
||||
|
||||
@ -4,33 +4,49 @@ import { resolveRelative, simplifySlug } from "../util/path"
|
||||
import { i18n } from "../i18n"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
const Backlinks: QuartzComponent = ({
|
||||
fileData,
|
||||
allFiles,
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const slug = simplifySlug(fileData.slug!)
|
||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||
return (
|
||||
<div class={classNames(displayClass, "backlinks")}>
|
||||
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
||||
<ul class="overflow">
|
||||
{backlinkFiles.length > 0 ? (
|
||||
backlinkFiles.map((f) => (
|
||||
<li>
|
||||
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
|
||||
{f.frontmatter?.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
interface BacklinksOptions {
|
||||
hideWhenEmpty: boolean
|
||||
}
|
||||
|
||||
Backlinks.css = style
|
||||
export default (() => Backlinks) satisfies QuartzComponentConstructor
|
||||
const defaultOptions: BacklinksOptions = {
|
||||
hideWhenEmpty: true,
|
||||
}
|
||||
|
||||
export default ((opts?: Partial<BacklinksOptions>) => {
|
||||
const options: BacklinksOptions = { ...defaultOptions, ...opts }
|
||||
|
||||
const Backlinks: QuartzComponent = ({
|
||||
fileData,
|
||||
allFiles,
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const slug = simplifySlug(fileData.slug!)
|
||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||
if (options.hideWhenEmpty && backlinkFiles.length == 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div class={classNames(displayClass, "backlinks")}>
|
||||
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
||||
<ul class="overflow">
|
||||
{backlinkFiles.length > 0 ? (
|
||||
backlinkFiles.map((f) => (
|
||||
<li>
|
||||
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
|
||||
{f.frontmatter?.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Backlinks.css = style
|
||||
|
||||
return Backlinks
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
||||
@ -28,7 +28,8 @@ export default ((opts: Options) => {
|
||||
const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => {
|
||||
// check if comments should be displayed according to frontmatter
|
||||
const disableComment: boolean =
|
||||
!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false"
|
||||
typeof fileData.frontmatter?.comments !== "undefined" &&
|
||||
(!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false")
|
||||
if (disableComment) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { formatDate, getDate } from "./Date"
|
||||
import { Date, getDate } from "./Date"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import readingTime from "reading-time"
|
||||
import { classNames } from "../util/lang"
|
||||
@ -30,7 +30,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
const segments: (string | JSX.Element)[] = []
|
||||
|
||||
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
|
||||
@ -39,14 +39,12 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
||||
minutes: Math.ceil(minutes),
|
||||
})
|
||||
segments.push(displayedTime)
|
||||
segments.push(<span>{displayedTime}</span>)
|
||||
}
|
||||
|
||||
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
|
||||
|
||||
return (
|
||||
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
|
||||
{segmentsElements}
|
||||
{segments}
|
||||
</p>
|
||||
)
|
||||
} else {
|
||||
|
||||
@ -27,5 +27,5 @@ export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
|
||||
}
|
||||
|
||||
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 explorerStyle from "./styles/explorer.scss"
|
||||
import style from "./styles/explorer.scss"
|
||||
|
||||
// @ts-ignore
|
||||
import script from "./scripts/explorer.inline"
|
||||
@ -83,18 +83,46 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
lastBuildId = ctx.buildId
|
||||
constructFileTree(allFiles)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={classNames(displayClass, "explorer")}>
|
||||
<button
|
||||
type="button"
|
||||
id="explorer"
|
||||
id="mobile-explorer"
|
||||
class="collapsed hide-until-loaded"
|
||||
data-behavior={opts.folderClickBehavior}
|
||||
data-collapsed={opts.folderDefaultState}
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
data-mobile={true}
|
||||
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>
|
||||
<svg
|
||||
@ -122,7 +150,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
)
|
||||
}
|
||||
|
||||
Explorer.css = explorerStyle
|
||||
Explorer.css = style
|
||||
Explorer.afterDOMLoaded = script
|
||||
return Explorer
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
||||
@ -18,6 +18,7 @@ export interface D3Config {
|
||||
removeTags: string[]
|
||||
showTags: boolean
|
||||
focusOnHover?: boolean
|
||||
enableRadial?: boolean
|
||||
}
|
||||
|
||||
interface GraphOptions {
|
||||
@ -39,6 +40,7 @@ const defaultOptions: GraphOptions = {
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: false,
|
||||
enableRadial: false,
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
@ -53,10 +55,11 @@ const defaultOptions: GraphOptions = {
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: true,
|
||||
enableRadial: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default ((opts?: GraphOptions) => {
|
||||
export default ((opts?: Partial<GraphOptions>) => {
|
||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||
|
||||
@ -165,6 +165,7 @@ export default (() => {
|
||||
<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" />
|
||||
{/* OG/Twitter meta tags */}
|
||||
<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: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} />
|
||||
|
||||
@ -46,13 +46,9 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
||||
return (
|
||||
<li class="section-li">
|
||||
<div class="section">
|
||||
<div>
|
||||
{page.dates && (
|
||||
<p class="meta">
|
||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p class="meta">
|
||||
{page.dates && <Date date={getDate(cfg, page)!} locale={cfg.locale} />}
|
||||
</p>
|
||||
<div class="desc">
|
||||
<h3>
|
||||
<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 classes = ["popover-hint", ...cssClasses].join(" ")
|
||||
const classes = cssClasses.join(" ")
|
||||
const listProps = {
|
||||
...props,
|
||||
sort: options.sort,
|
||||
@ -84,8 +84,8 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
: htmlToJsx(fileData.filePath!, tree)
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
<article>{content}</article>
|
||||
<div class="popover-hint">
|
||||
<article class={classes}>{content}</article>
|
||||
<div class="page-listing">
|
||||
{options.showFolderCount && (
|
||||
<p>
|
||||
|
||||
@ -38,7 +38,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
? fileData.description
|
||||
: htmlToJsx(fileData.filePath!, tree)
|
||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
||||
const classes = cssClasses.join(" ")
|
||||
if (tag === "/") {
|
||||
const tags = [
|
||||
...new Set(
|
||||
@ -50,8 +50,8 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||
}
|
||||
return (
|
||||
<div class={classes}>
|
||||
<article>
|
||||
<div class="popover-hint">
|
||||
<article class={classes}>
|
||||
<p>{content}</p>
|
||||
</article>
|
||||
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
||||
@ -93,7 +93,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
|
||||
<PageList limit={options.numPages} {...listProps} sort={options?.sort} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -110,11 +110,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
<article>{content}</article>
|
||||
<article class="popover-hint">{content}</article>
|
||||
<div class="page-listing">
|
||||
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
||||
<div>
|
||||
<PageList {...listProps} />
|
||||
<PageList {...listProps} sort={options?.sort} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,10 @@ import { visit } from "unist-util-visit"
|
||||
import { Root, Element, ElementContent } from "hast"
|
||||
import { GlobalConfiguration } from "../cfg"
|
||||
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 {
|
||||
head: QuartzComponent
|
||||
@ -23,12 +27,13 @@ interface RenderComponents {
|
||||
const headerRegex = new RegExp(/h[1-6]/)
|
||||
export function pageResources(
|
||||
baseDir: FullSlug | RelativeURL,
|
||||
fileData: QuartzPluginData,
|
||||
staticResources: StaticResources,
|
||||
): StaticResources {
|
||||
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
||||
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
|
||||
|
||||
return {
|
||||
const resources: StaticResources = {
|
||||
css: [
|
||||
{
|
||||
content: joinSegments(baseDir, "index.css"),
|
||||
@ -48,14 +53,28 @@ export function pageResources(
|
||||
script: contentIndexScript,
|
||||
},
|
||||
...staticResources.js,
|
||||
{
|
||||
src: joinSegments(baseDir, "postscript.js"),
|
||||
loadTime: "afterDOMReady",
|
||||
moduleType: "module",
|
||||
contentType: "external",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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"),
|
||||
loadTime: "afterDOMReady",
|
||||
moduleType: "module",
|
||||
contentType: "external",
|
||||
})
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
export function renderPage(
|
||||
|
||||
@ -27,9 +27,10 @@ document.addEventListener("nav", () => {
|
||||
|
||||
// Darkmode toggle
|
||||
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
||||
themeButton.addEventListener("click", switchTheme)
|
||||
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
||||
|
||||
if (themeButton) {
|
||||
themeButton.addEventListener("click", switchTheme)
|
||||
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
||||
}
|
||||
// Listen for changes in prefers-color-scheme
|
||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { FolderState } from "../ExplorerNode"
|
||||
|
||||
// Current state of folders
|
||||
type MaybeHTMLElement = HTMLElement | undefined
|
||||
let currentExplorerState: FolderState[]
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||
const explorerUl = document.getElementById("explorer-ul")
|
||||
@ -16,23 +18,43 @@ const observer = new IntersectionObserver((entries) => {
|
||||
})
|
||||
|
||||
function toggleExplorer(this: HTMLElement) {
|
||||
// Toggle collapsed state of entire explorer
|
||||
this.classList.toggle("collapsed")
|
||||
|
||||
// Toggle collapsed aria state of entire explorer
|
||||
this.setAttribute(
|
||||
"aria-expanded",
|
||||
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("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) {
|
||||
evt.stopPropagation()
|
||||
|
||||
// Element that was clicked
|
||||
const target = evt.target as MaybeHTMLElement
|
||||
if (!target) return
|
||||
|
||||
// Check if target was svg icon or button
|
||||
const isSvg = target.nodeName === "svg"
|
||||
|
||||
// corresponding <ul> element relative to clicked button/folder
|
||||
const childFolderContainer = (
|
||||
isSvg
|
||||
? target.parentElement?.nextSibling
|
||||
@ -42,10 +64,14 @@ function toggleFolder(evt: MouseEvent) {
|
||||
isSvg ? target.nextElementSibling : target.parentElement
|
||||
) as MaybeHTMLElement
|
||||
if (!(childFolderContainer && currentFolderParent)) return
|
||||
|
||||
// <li> element of folder (stores folder-path dataset)
|
||||
childFolderContainer.classList.toggle("open")
|
||||
|
||||
// Collapse folder container
|
||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||
setFolderState(childFolderContainer, !isCollapsed)
|
||||
|
||||
// Save folder state to localStorage
|
||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
||||
@ -53,57 +79,106 @@ function toggleFolder(evt: MouseEvent) {
|
||||
}
|
||||
|
||||
function setupExplorer() {
|
||||
const explorer = document.getElementById("explorer")
|
||||
if (!explorer) return
|
||||
// Set click handler for collapsing entire explorer
|
||||
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(
|
||||
"folder-button",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
item.addEventListener("click", toggleFolder)
|
||||
}
|
||||
}
|
||||
|
||||
// Add click handler to main explorer
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
explorer.addEventListener("click", toggleExplorer)
|
||||
}
|
||||
|
||||
// Set up click handlers for each folder (click handler on folder "icon")
|
||||
for (const item of document.getElementsByClassName(
|
||||
"folder-button",
|
||||
"folder-icon",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
item.addEventListener("click", toggleFolder)
|
||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||
}
|
||||
|
||||
// Get folder state from local storage
|
||||
const oldExplorerState: FolderState[] =
|
||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
||||
const newExplorerState: FolderState[] = explorer.dataset.tree
|
||||
? JSON.parse(explorer.dataset.tree)
|
||||
: []
|
||||
currentExplorerState = []
|
||||
|
||||
for (const { path, collapsed } of newExplorerState) {
|
||||
currentExplorerState.push({
|
||||
path,
|
||||
collapsed: oldIndex.get(path) ?? collapsed,
|
||||
})
|
||||
}
|
||||
|
||||
currentExplorerState.map((folderState) => {
|
||||
const folderLi = document.querySelector(
|
||||
`[data-folderpath='${folderState.path.replace("'", "-")}']`,
|
||||
) as MaybeHTMLElement
|
||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
||||
if (folderUl) {
|
||||
setFolderState(folderUl, folderState.collapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
explorer.addEventListener("click", toggleExplorer)
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
function toggleExplorerFolders() {
|
||||
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
|
||||
/\/index$/g,
|
||||
"",
|
||||
)
|
||||
const allFolders = document.querySelectorAll(".folder-outer")
|
||||
|
||||
// Set up click handlers for each folder (click handler on folder "icon")
|
||||
for (const item of document.getElementsByClassName(
|
||||
"folder-icon",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
item.addEventListener("click", toggleFolder)
|
||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||
}
|
||||
|
||||
// Get folder state from local storage
|
||||
const storageTree = localStorage.getItem("fileTree")
|
||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||
const oldExplorerState: FolderState[] =
|
||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
||||
const newExplorerState: FolderState[] = explorer.dataset.tree
|
||||
? JSON.parse(explorer.dataset.tree)
|
||||
: []
|
||||
currentExplorerState = []
|
||||
for (const { path, collapsed } of newExplorerState) {
|
||||
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
||||
}
|
||||
|
||||
currentExplorerState.map((folderState) => {
|
||||
const folderLi = document.querySelector(
|
||||
`[data-folderpath='${folderState.path}']`,
|
||||
) as MaybeHTMLElement
|
||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
||||
allFolders.forEach((element) => {
|
||||
const folderUl = Array.from(element.children).find((child) =>
|
||||
child.matches("ul[data-folderul]"),
|
||||
)
|
||||
if (folderUl) {
|
||||
setFolderState(folderUl, folderState.collapsed)
|
||||
if (currentFile.includes(folderUl.getAttribute("data-folderul") ?? "")) {
|
||||
if (!element.classList.contains("open")) {
|
||||
element.classList.add("open")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener("resize", setupExplorer)
|
||||
|
||||
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()
|
||||
|
||||
observer.disconnect()
|
||||
|
||||
// select pseudo element at end of list
|
||||
@ -111,6 +186,12 @@ document.addEventListener("nav", () => {
|
||||
if (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,
|
||||
forceLink,
|
||||
forceCollide,
|
||||
forceRadial,
|
||||
zoomIdentity,
|
||||
select,
|
||||
drag,
|
||||
@ -87,6 +88,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
removeTags,
|
||||
showTags,
|
||||
focusOnHover,
|
||||
enableRadial,
|
||||
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||
|
||||
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
|
||||
// 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)
|
||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||
.force("center", forceCenter().strength(centerForce))
|
||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||
|
||||
const width = graph.offsetWidth
|
||||
const height = Math.max(graph.offsetHeight, 250)
|
||||
if (enableRadial)
|
||||
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
|
||||
const cssVars = [
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { removeAllChildren } from "./util"
|
||||
import mermaid from "mermaid"
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
@ -144,6 +143,7 @@ const cssVars = [
|
||||
"--codeFont",
|
||||
] as const
|
||||
|
||||
let mermaidImport = undefined
|
||||
document.addEventListener("nav", async () => {
|
||||
const center = document.querySelector(".center") as HTMLElement
|
||||
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
|
||||
@ -157,6 +157,12 @@ document.addEventListener("nav", async () => {
|
||||
{} 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"
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||
import { normalizeRelativeURLs } from "../../util/path"
|
||||
import { fetchCanonical } from "./util"
|
||||
|
||||
const p = new DOMParser()
|
||||
async function mouseEnterHandler(
|
||||
@ -37,7 +38,7 @@ async function mouseEnterHandler(
|
||||
targetUrl.hash = ""
|
||||
targetUrl.search = ""
|
||||
|
||||
const response = await fetch(`${targetUrl}`).catch((err) => {
|
||||
const response = await fetchCanonical(targetUrl).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import micromorph from "micromorph"
|
||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||
import { fetchCanonical } from "./util"
|
||||
|
||||
// adapted from `micromorph`
|
||||
// https://github.com/natemoo-re/micromorph
|
||||
@ -42,10 +43,24 @@ function notifyNav(url: FullSlug) {
|
||||
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||
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
|
||||
async function navigate(url: URL, isBack: boolean = false) {
|
||||
startLoading()
|
||||
p = p || new DOMParser()
|
||||
const contents = await fetch(`${url}`)
|
||||
const contents = await fetchCanonical(url)
|
||||
.then((res) => {
|
||||
const contentType = res.headers.get("content-type")
|
||||
if (contentType?.startsWith("text/html")) {
|
||||
@ -104,6 +119,7 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||
if (!isBack) {
|
||||
history.pushState({}, "", url)
|
||||
}
|
||||
|
||||
notifyNav(getFullSlug(window))
|
||||
delete announcer.dataset.persist
|
||||
}
|
||||
|
||||
@ -24,3 +24,22 @@ export function removeAllChildren(node: HTMLElement) {
|
||||
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);
|
||||
|
||||
&[show-comma="true"] {
|
||||
> span:not(:last-child) {
|
||||
> *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
|
||||
&::after {
|
||||
|
||||
@ -1,14 +1,70 @@
|
||||
@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 {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
@media all and not ($mobile) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/*&:after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
@ -23,7 +79,8 @@
|
||||
}*/
|
||||
}
|
||||
|
||||
button#explorer {
|
||||
button#mobile-explorer,
|
||||
button#desktop-explorer {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
@ -68,19 +125,19 @@ button#explorer {
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
max-height: 0px;
|
||||
transition:
|
||||
max-height 0.35s ease,
|
||||
visibility 0s linear 0s;
|
||||
visibility 0s linear 0.35s;
|
||||
margin-top: 0.5rem;
|
||||
visibility: visible;
|
||||
visibility: hidden;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 0;
|
||||
max-height: 100%;
|
||||
transition:
|
||||
max-height 0.35s ease,
|
||||
visibility 0s linear 0.35s;
|
||||
visibility: hidden;
|
||||
visibility 0s linear 0s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
& ul {
|
||||
@ -91,12 +148,14 @@ button#explorer {
|
||||
max-height 0.35s ease,
|
||||
transform 0.35s ease,
|
||||
opacity 0.2s ease;
|
||||
|
||||
& li > a {
|
||||
color: var(--dark);
|
||||
opacity: 0.75;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
> #explorer-ul {
|
||||
max-height: none;
|
||||
}
|
||||
@ -179,3 +238,80 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||
// remove default margin from li
|
||||
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);
|
||||
}
|
||||
|
||||
@media all and not ($tablet) {
|
||||
@media all and not ($mobile) {
|
||||
&[data-preview] {
|
||||
& .result-card > p.preview {
|
||||
display: none;
|
||||
@ -132,7 +132,7 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media all and ($tablet) {
|
||||
@media all and ($mobile) {
|
||||
& > #preview-container {
|
||||
display: none !important;
|
||||
}
|
||||
@ -151,6 +151,7 @@
|
||||
}
|
||||
|
||||
& > #preview-container {
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
|
||||
@ -14,6 +14,7 @@ import uk from "./locales/uk-UA"
|
||||
import ru from "./locales/ru-RU"
|
||||
import ko from "./locales/ko-KR"
|
||||
import zh from "./locales/zh-CN"
|
||||
import zhTw from "./locales/zh-TW"
|
||||
import vi from "./locales/vi-VN"
|
||||
import pt from "./locales/pt-BR"
|
||||
import hu from "./locales/hu-HU"
|
||||
@ -21,6 +22,8 @@ import fa from "./locales/fa-IR"
|
||||
import pl from "./locales/pl-PL"
|
||||
import cs from "./locales/cs-CZ"
|
||||
import tr from "./locales/tr-TR"
|
||||
import th from "./locales/th-TH"
|
||||
import lt from "./locales/lt-LT"
|
||||
|
||||
export const TRANSLATIONS = {
|
||||
"en-US": enUs,
|
||||
@ -59,6 +62,7 @@ export const TRANSLATIONS = {
|
||||
"ru-RU": ru,
|
||||
"ko-KR": ko,
|
||||
"zh-CN": zh,
|
||||
"zh-TW": zhTw,
|
||||
"vi-VN": vi,
|
||||
"pt-BR": pt,
|
||||
"hu-HU": hu,
|
||||
@ -66,6 +70,8 @@ export const TRANSLATIONS = {
|
||||
"pl-PL": pl,
|
||||
"cs-CZ": cs,
|
||||
"tr-TR": tr,
|
||||
"th-TH": th,
|
||||
"lt-LT": lt,
|
||||
} as const
|
||||
|
||||
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 path = url.pathname as FullSlug
|
||||
const externalResources = pageResources(path, resources)
|
||||
const notFound = i18n(cfg.locale).pages.error.title
|
||||
const [tree, vfile] = defaultProcessedContent({
|
||||
slug,
|
||||
@ -45,6 +44,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||
description: notFound,
|
||||
frontmatter: { title: notFound, tags: [] },
|
||||
})
|
||||
const externalResources = pageResources(path, vfile.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
ctx,
|
||||
fileData: vfile.data,
|
||||
|
||||
@ -106,7 +106,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
||||
containsIndex = true
|
||||
}
|
||||
|
||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
||||
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
ctx,
|
||||
fileData: file.data,
|
||||
|
||||
@ -106,8 +106,8 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
||||
|
||||
for (const folder of folders) {
|
||||
const slug = joinSegments(folder, "index") as FullSlug
|
||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
||||
const [tree, file] = folderDescriptions[folder]
|
||||
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
ctx,
|
||||
fileData: file.data,
|
||||
|
||||
@ -105,14 +105,17 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
||||
const tag = slug.slice("tags/".length)
|
||||
if (tags.has(tag)) {
|
||||
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) {
|
||||
const slug = joinSegments("tags", tag) as FullSlug
|
||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
||||
const [tree, file] = tagDescriptions[tag]
|
||||
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
ctx,
|
||||
fileData: file.data,
|
||||
|
||||
@ -73,6 +73,18 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
|
||||
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
|
||||
|
||||
// fill in frontmatter
|
||||
@ -91,6 +103,9 @@ declare module "vfile" {
|
||||
} & Partial<{
|
||||
tags: string[]
|
||||
aliases: string[]
|
||||
modified: string
|
||||
created: string
|
||||
published: string
|
||||
description: string
|
||||
publish: boolean | string
|
||||
draft: boolean | string
|
||||
|
||||
@ -48,11 +48,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
||||
created ||= st.birthtimeMs
|
||||
modified ||= st.mtimeMs
|
||||
} else if (source === "frontmatter" && file.data.frontmatter) {
|
||||
created ||= file.data.frontmatter.date as MaybeDate
|
||||
modified ||= file.data.frontmatter.lastmod as MaybeDate
|
||||
modified ||= file.data.frontmatter.updated as MaybeDate
|
||||
modified ||= file.data.frontmatter["last-modified"] as MaybeDate
|
||||
published ||= file.data.frontmatter.publishDate as MaybeDate
|
||||
created ||= file.data.frontmatter.created as MaybeDate
|
||||
modified ||= file.data.frontmatter.modified as MaybeDate
|
||||
published ||= file.data.frontmatter.published as MaybeDate
|
||||
} else if (source === "git") {
|
||||
if (!repo) {
|
||||
// Get a reference to the main git repo.
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
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 { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
@ -11,13 +19,9 @@ import { JSResource, CSSResource } from "../../util/resources"
|
||||
import calloutScript from "../../components/scripts/callout.inline.ts"
|
||||
// @ts-ignore
|
||||
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 { toHast } from "mdast-util-to-hast"
|
||||
import { toHtml } from "hast-util-to-html"
|
||||
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
|
||||
import { capitalize } from "../../util/lang"
|
||||
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
|
||||
const calloutRegex = new RegExp(/^\[\!([\w-]+)\|?(.+?)?\]([+-]?)/)
|
||||
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 #
|
||||
// (?:[-_\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 "/"
|
||||
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 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.replace(commentRegex, "")
|
||||
src = (src as string).replace(commentRegex, "")
|
||||
}
|
||||
|
||||
// pre-transform blockquotes
|
||||
@ -165,7 +169,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
src = src.toString()
|
||||
}
|
||||
|
||||
src = src.replace(calloutLineRegex, (value) => {
|
||||
src = (src as string).replace(calloutLineRegex, (value) => {
|
||||
// force newline after title of callout
|
||||
return value + "\n> "
|
||||
})
|
||||
@ -178,7 +182,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
}
|
||||
|
||||
// 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
|
||||
return value.replace(tableWikilinkRegex, (_value, raw) => {
|
||||
// const [raw]: (string | undefined)[] = capture
|
||||
@ -192,7 +196,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
})
|
||||
|
||||
// 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 [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
|
||||
@ -513,9 +517,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
if (opts.mermaid) {
|
||||
plugins.push(() => {
|
||||
return (tree: Root, _file) => {
|
||||
return (tree: Root, file) => {
|
||||
visit(tree, "code", (node: Code) => {
|
||||
if (node.lang === "mermaid") {
|
||||
file.data.hasMermaidDiagram = true
|
||||
node.data = {
|
||||
hProperties: {
|
||||
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 }
|
||||
},
|
||||
}
|
||||
@ -835,5 +827,6 @@ declare module "vfile" {
|
||||
interface DataMap {
|
||||
blocks: Record<string, Element>
|
||||
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"
|
||||
|
||||
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 {
|
||||
const root: Parent = { type: "root", children: [] }
|
||||
const root: HtmlRoot = { type: "root", children: [] }
|
||||
const vfile = new VFile("")
|
||||
vfile.data = vfileData
|
||||
return [root, vfile]
|
||||
|
||||
@ -4,18 +4,20 @@ import remarkRehype from "remark-rehype"
|
||||
import { Processor, unified } from "unified"
|
||||
import { Root as MDRoot } from "remark-parse/lib"
|
||||
import { Root as HTMLRoot } from "hast"
|
||||
import { ProcessedContent } from "../plugins/vfile"
|
||||
import { MarkdownContent, ProcessedContent } from "../plugins/vfile"
|
||||
import { PerfTimer } from "../util/perf"
|
||||
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 workerpool, { Promise as WorkerPromise } from "workerpool"
|
||||
import { QuartzLogger } from "../util/log"
|
||||
import { trace } from "../util/trace"
|
||||
import { BuildCtx } from "../util/ctx"
|
||||
|
||||
export type QuartzProcessor = Processor<MDRoot, MDRoot, HTMLRoot>
|
||||
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
||||
export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
|
||||
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
|
||||
|
||||
export function createMdProcessor(ctx: BuildCtx): QuartzMdProcessor {
|
||||
const transformers = ctx.cfg.plugins.transformers
|
||||
|
||||
return (
|
||||
@ -24,14 +26,20 @@ export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
||||
.use(remarkParse)
|
||||
// MD AST -> MD AST transforms
|
||||
.use(
|
||||
transformers
|
||||
.filter((p) => p.markdownPlugins)
|
||||
.flatMap((plugin) => plugin.markdownPlugins!(ctx)),
|
||||
)
|
||||
transformers.flatMap((plugin) => plugin.markdownPlugins?.(ctx) ?? []),
|
||||
) as unknown as QuartzMdProcessor
|
||||
// ^ 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
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
// 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[]) {
|
||||
const { argv, cfg } = ctx
|
||||
return async (processor: QuartzProcessor) => {
|
||||
const res: ProcessedContent[] = []
|
||||
return async (processor: QuartzMdProcessor) => {
|
||||
const res: MarkdownContent[] = []
|
||||
for (const fp of fps) {
|
||||
try {
|
||||
const perf = new PerfTimer()
|
||||
@ -100,10 +108,32 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
||||
res.push([newAst, file])
|
||||
|
||||
if (argv.verbose) {
|
||||
console.log(`[process] ${fp} -> ${file.data.slug} (${perf.timeSince()})`)
|
||||
console.log(`[markdown] ${fp} -> ${file.data.slug} (${perf.timeSince()})`)
|
||||
}
|
||||
} 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) =>
|
||||
Math.min(Math.max(Math.round(num), min), max)
|
||||
|
||||
export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<ProcessedContent[]> {
|
||||
const { argv } = ctx
|
||||
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`)
|
||||
if (concurrency === 1) {
|
||||
try {
|
||||
const processor = createProcessor(ctx)
|
||||
const parse = createFileParser(ctx, fps)
|
||||
res = await parse(processor)
|
||||
const mdRes = await createFileParser(ctx, fps)(createMdProcessor(ctx))
|
||||
res = await createMarkdownParser(ctx, mdRes)(createHtmlProcessor(ctx))
|
||||
} catch (error) {
|
||||
log.end()
|
||||
throw error
|
||||
@ -140,17 +170,27 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
maxWorkers: concurrency,
|
||||
workerType: "thread",
|
||||
})
|
||||
|
||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||
childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
|
||||
const errorHandler = (err: any) => {
|
||||
console.error(`${err}`.replace(/^error:\s*/i, ""))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {
|
||||
const errString = err.toString().slice("Error:".length)
|
||||
console.error(errString)
|
||||
process.exit(1)
|
||||
})
|
||||
const mdPromises: WorkerPromise<[MarkdownContent[], FullSlug[]]>[] = []
|
||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||
mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, argv, chunk]))
|
||||
}
|
||||
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()
|
||||
await pool.terminate()
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "sass:map";
|
||||
|
||||
@use "./variables.scss" as *;
|
||||
@use "./syntax.scss";
|
||||
@use "./callouts.scss";
|
||||
@ -85,7 +87,7 @@ a {
|
||||
line-height: 1.4rem;
|
||||
|
||||
&:has(> img) {
|
||||
background-color: none;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@ -121,7 +123,7 @@ a {
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: calc(#{map-get($breakpoints, desktop)} + 300px);
|
||||
max-width: calc(#{map.get($breakpoints, desktop)} + 300px);
|
||||
margin: 0 auto;
|
||||
& article {
|
||||
& > h1 {
|
||||
@ -151,24 +153,25 @@ a {
|
||||
|
||||
& > #quartz-body {
|
||||
display: grid;
|
||||
grid-template-columns: #{map-get($desktopGrid, templateColumns)};
|
||||
grid-template-rows: #{map-get($desktopGrid, templateRows)};
|
||||
column-gap: #{map-get($desktopGrid, columnGap)};
|
||||
row-gap: #{map-get($desktopGrid, rowGap)};
|
||||
grid-template-areas: #{map-get($desktopGrid, templateAreas)};
|
||||
grid-template-columns: #{map.get($desktopGrid, templateColumns)};
|
||||
grid-template-rows: #{map.get($desktopGrid, templateRows)};
|
||||
column-gap: #{map.get($desktopGrid, columnGap)};
|
||||
row-gap: #{map.get($desktopGrid, rowGap)};
|
||||
grid-template-areas: #{map.get($desktopGrid, templateAreas)};
|
||||
|
||||
@media all and ($tablet) {
|
||||
grid-template-columns: #{map-get($tabletGrid, templateColumns)};
|
||||
grid-template-rows: #{map-get($tabletGrid, templateRows)};
|
||||
column-gap: #{map-get($tabletGrid, columnGap)};
|
||||
row-gap: #{map-get($tabletGrid, rowGap)};
|
||||
grid-template-areas: #{map-get($tabletGrid, templateAreas)};
|
||||
grid-template-columns: #{map.get($tabletGrid, templateColumns)};
|
||||
grid-template-rows: #{map.get($tabletGrid, templateRows)};
|
||||
column-gap: #{map.get($tabletGrid, columnGap)};
|
||||
row-gap: #{map.get($tabletGrid, rowGap)};
|
||||
grid-template-areas: #{map.get($tabletGrid, templateAreas)};
|
||||
}
|
||||
@media all and ($mobile) {
|
||||
grid-template-columns: #{map-get($mobileGrid, templateColumns)};
|
||||
grid-template-rows: #{map-get($mobileGrid, templateRows)};
|
||||
column-gap: #{map-get($mobileGrid, columnGap)};
|
||||
row-gap: #{map-get($mobileGrid, rowGap)};
|
||||
grid-template-areas: #{map-get($mobileGrid, templateAreas)};
|
||||
grid-template-columns: #{map.get($mobileGrid, templateColumns)};
|
||||
grid-template-rows: #{map.get($mobileGrid, templateRows)};
|
||||
column-gap: #{map.get($mobileGrid, columnGap)};
|
||||
row-gap: #{map.get($mobileGrid, rowGap)};
|
||||
grid-template-areas: #{map.get($mobileGrid, templateAreas)};
|
||||
}
|
||||
|
||||
@media all and not ($desktop) {
|
||||
@ -388,7 +391,7 @@ figure[data-rehype-pretty-code-figure] {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border: 1px solid var(--lightgray);
|
||||
width: max-content;
|
||||
width: fit-content;
|
||||
border-radius: 5px;
|
||||
margin-bottom: -0.5rem;
|
||||
color: var(--darkgray);
|
||||
@ -512,6 +515,7 @@ img {
|
||||
max-width: 100%;
|
||||
border-radius: 5px;
|
||||
margin: 1rem 0;
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
p > img + em {
|
||||
@ -587,3 +591,14 @@ iframe.pdf {
|
||||
width: 100%;
|
||||
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
|
||||
* $mobile: screen width below this value will use mobile styles
|
||||
@ -10,11 +12,11 @@ $breakpoints: (
|
||||
desktop: 1200px,
|
||||
);
|
||||
|
||||
$mobile: "(max-width: #{map-get($breakpoints, mobile)})";
|
||||
$tablet: "(min-width: #{map-get($breakpoints, mobile)}) and (max-width: #{map-get($breakpoints, desktop)})";
|
||||
$desktop: "(min-width: #{map-get($breakpoints, desktop)})";
|
||||
$mobile: "(max-width: #{map.get($breakpoints, mobile)})";
|
||||
$tablet: "(min-width: #{map.get($breakpoints, mobile)}) and (max-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;
|
||||
$topSpacing: 6rem;
|
||||
$boldWeight: 700;
|
||||
|
||||
@ -35,7 +35,9 @@ export async function getSatoriFont(headerFontName: string, bodyFontName: string
|
||||
async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> {
|
||||
try {
|
||||
// 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()
|
||||
|
||||
// Extract .ttf url from css file
|
||||
|
||||
@ -158,6 +158,29 @@ describe("transforms", () => {
|
||||
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", () => {
|
||||
|
||||
@ -108,10 +108,10 @@ const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) =>
|
||||
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
||||
}
|
||||
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),
|
||||
)
|
||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
||||
el.querySelectorAll('[src=""], [src^="./"], [src^="../"]').forEach((item) =>
|
||||
_rebaseHtmlElement(item, "src", destination),
|
||||
)
|
||||
}
|
||||
@ -183,10 +183,26 @@ export function slugTag(tag: string) {
|
||||
}
|
||||
|
||||
export function joinSegments(...args: string[]): string {
|
||||
return args
|
||||
.filter((segment) => segment !== "")
|
||||
if (args.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let joined = args
|
||||
.filter((segment) => segment !== "" && segment !== "/")
|
||||
.map((segment) => stripSlashes(segment))
|
||||
.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[] {
|
||||
|
||||
@ -3,23 +3,46 @@ sourceMapSupport.install(options)
|
||||
import cfg from "../quartz.config"
|
||||
import { Argv, BuildCtx } from "./util/ctx"
|
||||
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 { MarkdownContent, ProcessedContent } from "./plugins/vfile"
|
||||
|
||||
// only called from worker thread
|
||||
export async function parseFiles(
|
||||
export async function parseMarkdown(
|
||||
buildId: string,
|
||||
argv: Argv,
|
||||
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 = {
|
||||
buildId,
|
||||
cfg,
|
||||
argv,
|
||||
allSlugs,
|
||||
}
|
||||
const processor = createProcessor(ctx)
|
||||
const parse = createFileParser(ctx, fps)
|
||||
return parse(processor)
|
||||
return [await createFileParser(ctx, fps)(createMdProcessor(ctx)), allSlugs]
|
||||
}
|
||||
|
||||
// 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