mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 15:05:42 -05:00
format
This commit is contained in:
parent
388ab1f228
commit
3dc4a40d91
@ -66,16 +66,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
const release = await mut.acquire()
|
||||
perf.addEvent("clean")
|
||||
await rimraf(path.join(output, "*"), {glob: true})
|
||||
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
||||
console.log(
|
||||
`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`,
|
||||
)
|
||||
|
||||
perf.addEvent("glob")
|
||||
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
||||
const allFiles = await glob(
|
||||
"**/*.*",
|
||||
argv.directory,
|
||||
cfg.configuration.ignorePatterns,
|
||||
)
|
||||
const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort()
|
||||
console.log(
|
||||
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
||||
)
|
||||
|
||||
const filePaths = fps.map((fp) => joinSegments(argv.directory, fp) as FilePath)
|
||||
const filePaths = fps.map(
|
||||
(fp) => joinSegments(argv.directory, fp) as FilePath,
|
||||
)
|
||||
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||
|
||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||
@ -88,12 +96,18 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
dependencies[emitter.name] =
|
||||
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
|
||||
(await emitter.getDependencyGraph?.(
|
||||
ctx,
|
||||
filteredContent,
|
||||
staticResources,
|
||||
)) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
await emitContent(ctx, filteredContent)
|
||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||
console.log(
|
||||
chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`),
|
||||
)
|
||||
release()
|
||||
|
||||
if (argv.serve) {
|
||||
@ -137,11 +151,17 @@ async function startServing(
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
||||
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("change", (fp) =>
|
||||
buildFromEntry(fp, "change", clientRefresh, buildData),
|
||||
)
|
||||
.on("unlink", (fp) =>
|
||||
buildFromEntry(fp, "delete", clientRefresh, buildData),
|
||||
)
|
||||
|
||||
return async () => {
|
||||
await watcher.close()
|
||||
@ -184,12 +204,18 @@ async function partialRebuildFromEntrypoint(
|
||||
case "add":
|
||||
// add to cache when new file is added
|
||||
processedFiles = await parseMarkdown(ctx, [fp])
|
||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
||||
processedFiles.forEach(([tree, vfile]) =>
|
||||
contentMap.set(vfile.data.filePath!, [tree, vfile]),
|
||||
)
|
||||
|
||||
// update the dep graph by asking all emitters whether they depend on this file
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
const emitterGraph =
|
||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||
(await emitter.getDependencyGraph?.(
|
||||
ctx,
|
||||
processedFiles,
|
||||
staticResources,
|
||||
)) ?? null
|
||||
|
||||
if (emitterGraph) {
|
||||
const existingGraph = dependencies[emitter.name]
|
||||
@ -205,20 +231,29 @@ async function partialRebuildFromEntrypoint(
|
||||
case "change":
|
||||
// invalidate cache when file is changed
|
||||
processedFiles = await parseMarkdown(ctx, [fp])
|
||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
||||
processedFiles.forEach(([tree, vfile]) =>
|
||||
contentMap.set(vfile.data.filePath!, [tree, vfile]),
|
||||
)
|
||||
|
||||
// only content files can have added/removed dependencies because of transclusions
|
||||
if (path.extname(fp) === ".md") {
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
// get new dependencies from all emitters for this file
|
||||
const emitterGraph =
|
||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||
(await emitter.getDependencyGraph?.(
|
||||
ctx,
|
||||
processedFiles,
|
||||
staticResources,
|
||||
)) ?? null
|
||||
|
||||
// only update the graph if the emitter plugin uses the changed file
|
||||
// eg. Assets plugin ignores md files, so we skip updating the graph
|
||||
if (emitterGraph?.hasNode(fp)) {
|
||||
// merge the new dependencies into the dep graph
|
||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
|
||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(
|
||||
emitterGraph,
|
||||
fp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,7 +316,11 @@ async function partialRebuildFromEntrypoint(
|
||||
.filter((file) => !toRemove.has(file))
|
||||
.map((file) => contentMap.get(file)!)
|
||||
|
||||
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
|
||||
const emittedFps = await emitter.emit(
|
||||
ctx,
|
||||
upstreamContent,
|
||||
staticResources,
|
||||
)
|
||||
|
||||
if (ctx.argv.verbose) {
|
||||
for (const file of emittedFps) {
|
||||
@ -293,7 +332,9 @@ async function partialRebuildFromEntrypoint(
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
||||
console.log(
|
||||
`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`,
|
||||
)
|
||||
|
||||
// CLEANUP
|
||||
const destinationsToDelete = new Set<FilePath>()
|
||||
@ -328,8 +369,16 @@ async function rebuildFromEntrypoint(
|
||||
clientRefresh: () => void,
|
||||
buildData: BuildData, // note: this function mutates buildData
|
||||
) {
|
||||
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
|
||||
buildData
|
||||
const {
|
||||
ctx,
|
||||
ignored,
|
||||
mut,
|
||||
initialSlugs,
|
||||
contentMap,
|
||||
toRebuild,
|
||||
toRemove,
|
||||
trackedAssets,
|
||||
} = buildData
|
||||
|
||||
const {argv} = ctx
|
||||
|
||||
@ -387,9 +436,13 @@ async function rebuildFromEntrypoint(
|
||||
const filteredContent = filterContent(ctx, parsedFiles)
|
||||
|
||||
// re-update slugs
|
||||
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
||||
const trackedSlugs = [
|
||||
...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets]),
|
||||
]
|
||||
.filter((fp) => !toRemove.has(fp))
|
||||
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
||||
.map((fp) =>
|
||||
slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath),
|
||||
)
|
||||
|
||||
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
||||
|
||||
@ -399,7 +452,9 @@ async function rebuildFromEntrypoint(
|
||||
await emitContent(ctx, filteredContent)
|
||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||
} catch (err) {
|
||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
||||
console.log(
|
||||
chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`),
|
||||
)
|
||||
if (argv.verbose) {
|
||||
console.log(chalk.red(err))
|
||||
}
|
||||
|
||||
@ -84,4 +84,7 @@ export interface FullPageLayout {
|
||||
}
|
||||
|
||||
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
||||
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer" | "afterBody">
|
||||
export type SharedLayout = Pick<
|
||||
FullPageLayout,
|
||||
"head" | "header" | "footer" | "afterBody"
|
||||
>
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {classNames} from "../util/lang"
|
||||
|
||||
const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
||||
const ArticleTitle: QuartzComponent = ({
|
||||
fileData,
|
||||
displayClass,
|
||||
}: QuartzComponentProps) => {
|
||||
const title = fileData.frontmatter?.title
|
||||
if (title) {
|
||||
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import style from "./styles/backlinks.scss"
|
||||
import {resolveRelative, simplifySlug} from "../util/path"
|
||||
import {i18n} from "../i18n"
|
||||
@ -19,7 +23,9 @@ const Backlinks: QuartzComponent = ({
|
||||
{backlinkFiles.length > 0 ? (
|
||||
backlinkFiles.map((f) => (
|
||||
<li>
|
||||
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
|
||||
<a
|
||||
href={resolveRelative(fileData.slug!, f.slug!)}
|
||||
class="internal">
|
||||
{f.frontmatter?.title}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
// @ts-ignore
|
||||
import clipboardScript from "./scripts/clipboard.inline"
|
||||
import clipboardStyle from "./styles/clipboard.scss"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
|
||||
const Body: QuartzComponent = ({children}: QuartzComponentProps) => {
|
||||
return <div id="quartz-body">{children}</div>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||
import {FullSlug, SimpleSlug, joinSegments, resolveRelative} from "../util/path"
|
||||
import {QuartzPluginData} from "../plugins/vfile"
|
||||
@ -40,7 +44,11 @@ const defaultOptions: BreadcrumbOptions = {
|
||||
showCurrentPage: true,
|
||||
}
|
||||
|
||||
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
||||
function formatCrumb(
|
||||
displayName: string,
|
||||
baseSlug: FullSlug,
|
||||
currentSlug: SimpleSlug,
|
||||
): CrumbData {
|
||||
return {
|
||||
displayName: displayName.replaceAll("-", " "),
|
||||
path: resolveRelative(baseSlug, currentSlug),
|
||||
@ -65,7 +73,11 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
}
|
||||
|
||||
// Format entry for root element
|
||||
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
||||
const firstEntry = formatCrumb(
|
||||
options.rootName,
|
||||
fileData.slug!,
|
||||
"/" as SimpleSlug,
|
||||
)
|
||||
const crumbs: CrumbData[] = [firstEntry]
|
||||
|
||||
if (!folderIndex && options.resolveFrontmatterTitle) {
|
||||
@ -92,7 +104,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
let curPathSegment = slugParts[i]
|
||||
|
||||
// Try to resolve frontmatter folder title
|
||||
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
||||
const currentFile = folderIndex?.get(
|
||||
slugParts.slice(0, i + 1).join("/"),
|
||||
)
|
||||
if (currentFile) {
|
||||
const title = currentFile.frontmatter!.title
|
||||
if (title !== "index") {
|
||||
@ -123,11 +137,15 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs">
|
||||
<nav
|
||||
class={classNames(displayClass, "breadcrumb-container")}
|
||||
aria-label="breadcrumbs">
|
||||
{crumbs.map((crumb, index) => (
|
||||
<div class="breadcrumb-element">
|
||||
<a href={crumb.path}>{crumb.displayName}</a>
|
||||
{index !== crumbs.length - 1 && <p>{` ${options.spacerSymbol} `}</p>}
|
||||
{index !== crumbs.length - 1 && (
|
||||
<p>{` ${options.spacerSymbol} `}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {classNames} from "../util/lang"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/comments.inline"
|
||||
@ -22,7 +26,10 @@ function boolToStringBool(b: boolean): string {
|
||||
}
|
||||
|
||||
export default ((opts: Options) => {
|
||||
const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const Comments: QuartzComponent = ({
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
return (
|
||||
<div
|
||||
class={classNames(displayClass, "giscus")}
|
||||
@ -32,9 +39,10 @@ export default ((opts: Options) => {
|
||||
data-category-id={opts.options.categoryId}
|
||||
data-mapping={opts.options.mapping ?? "url"}
|
||||
data-strict={boolToStringBool(opts.options.strict ?? true)}
|
||||
data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)}
|
||||
data-input-position={opts.options.inputPosition ?? "bottom"}
|
||||
></div>
|
||||
data-reactions-enabled={boolToStringBool(
|
||||
opts.options.reactionsEnabled ?? true,
|
||||
)}
|
||||
data-input-position={opts.options.inputPosition ?? "bottom"}></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,11 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
// Merge options with defaults
|
||||
const options: ContentMetaOptions = {...defaultOptions, ...opts}
|
||||
|
||||
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
||||
function ContentMetadata({
|
||||
cfg,
|
||||
fileData,
|
||||
displayClass,
|
||||
}: QuartzComponentProps) {
|
||||
const text = fileData.text
|
||||
|
||||
if (text) {
|
||||
@ -36,7 +40,9 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
// Display reading time if enabled
|
||||
if (options.showReadingTime) {
|
||||
const {minutes, words: _words} = readingTime(text)
|
||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
||||
const displayedTime = i18n(
|
||||
cfg.locale,
|
||||
).components.contentMeta.readingTime({
|
||||
minutes: Math.ceil(minutes),
|
||||
})
|
||||
segments.push(displayedTime)
|
||||
@ -45,7 +51,9 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
|
||||
|
||||
return (
|
||||
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
|
||||
<p
|
||||
show-comma={options.showComma}
|
||||
class={classNames(displayClass, "content-meta")}>
|
||||
{segmentsElements}
|
||||
</p>
|
||||
)
|
||||
|
||||
@ -3,14 +3,26 @@
|
||||
// see: https://v8.dev/features/modules#defer
|
||||
import darkmodeScript from "./scripts/darkmode.inline"
|
||||
import styles from "./styles/darkmode.scss"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {i18n} from "../i18n"
|
||||
import {classNames} from "../util/lang"
|
||||
|
||||
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const Darkmode: QuartzComponent = ({
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
return (
|
||||
<div class={classNames(displayClass, "darkmode")}>
|
||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||
<input
|
||||
class="toggle"
|
||||
id="darkmode-toggle"
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -21,8 +33,7 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
||||
y="0px"
|
||||
viewBox="0 0 35 35"
|
||||
style="enable-background:new 0 0 35 35"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
xmlSpace="preserve">
|
||||
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
|
||||
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
||||
</svg>
|
||||
@ -37,8 +48,7 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
||||
y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
xmlSpace="preserve">
|
||||
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
|
||||
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
|
||||
</svg>
|
||||
|
||||
@ -9,7 +9,10 @@ interface Props {
|
||||
|
||||
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
||||
|
||||
export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined {
|
||||
export function getDate(
|
||||
cfg: GlobalConfiguration,
|
||||
data: QuartzPluginData,
|
||||
): Date | undefined {
|
||||
if (!cfg.defaultDateType) {
|
||||
throw new Error(
|
||||
`Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`,
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
|
||||
export default ((component?: QuartzComponent) => {
|
||||
if (component) {
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import explorerStyle from "./styles/explorer.scss"
|
||||
|
||||
// @ts-ignore
|
||||
@ -68,7 +72,9 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
|
||||
// Get all folders of tree. Initialize with collapsed state
|
||||
// Stringify to pass json tree as data attribute ([data-tree])
|
||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||
const folders = fileTree.getFolderPaths(
|
||||
opts.folderDefaultState === "collapsed",
|
||||
)
|
||||
jsonTree = JSON.stringify(folders)
|
||||
}
|
||||
|
||||
@ -94,8 +100,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
aria-controls="explorer-content"
|
||||
aria-expanded={opts.folderDefaultState === "open"}
|
||||
>
|
||||
aria-expanded={opts.folderDefaultState === "open"}>
|
||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -107,8 +112,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="fold"
|
||||
>
|
||||
class="fold">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@ -32,7 +32,10 @@ export type FolderState = {
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
|
||||
function getPathSegment(
|
||||
fp: FilePath | undefined,
|
||||
idx: number,
|
||||
): string | undefined {
|
||||
if (!fp) {
|
||||
return undefined
|
||||
}
|
||||
@ -48,7 +51,12 @@ export class FileNode {
|
||||
file: QuartzPluginData | null
|
||||
depth: number
|
||||
|
||||
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
||||
constructor(
|
||||
slugSegment: string,
|
||||
displayName?: string,
|
||||
file?: QuartzPluginData,
|
||||
depth?: number,
|
||||
) {
|
||||
this.children = []
|
||||
this.name = slugSegment
|
||||
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
||||
@ -73,7 +81,9 @@ export class FileNode {
|
||||
}
|
||||
} else {
|
||||
// direct child
|
||||
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
||||
this.children.push(
|
||||
new FileNode(nextSegment, undefined, fileData.file, this.depth + 1),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
@ -162,13 +172,19 @@ type ExplorerNodeProps = {
|
||||
fullPath?: string
|
||||
}
|
||||
|
||||
export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) {
|
||||
export function ExplorerNode({
|
||||
node,
|
||||
opts,
|
||||
fullPath,
|
||||
fileData,
|
||||
}: ExplorerNodeProps) {
|
||||
// Get options
|
||||
const folderBehavior = opts.folderClickBehavior
|
||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||
|
||||
// Calculate current folderPath
|
||||
const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
|
||||
const folderPath =
|
||||
node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
|
||||
const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
|
||||
|
||||
return (
|
||||
@ -176,7 +192,9 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
{node.file ? (
|
||||
// Single file node
|
||||
<li key={node.file.slug}>
|
||||
<a href={resolveRelative(fileData.slug!, node.file.slug!)} data-for={node.file.slug}>
|
||||
<a
|
||||
href={resolveRelative(fileData.slug!, node.file.slug!)}
|
||||
data-for={node.file.slug}>
|
||||
{node.displayName}
|
||||
</a>
|
||||
</li>
|
||||
@ -196,8 +214,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="folder-icon"
|
||||
>
|
||||
class="folder-icon">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||
@ -215,15 +232,15 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
</div>
|
||||
)}
|
||||
{/* Recursively render children of folder */}
|
||||
<div class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}>
|
||||
<div
|
||||
class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}>
|
||||
<ul
|
||||
// Inline style for left folder paddings
|
||||
style={{
|
||||
paddingLeft: node.name !== "" ? "1.4rem" : "0",
|
||||
}}
|
||||
class="content"
|
||||
data-folderul={folderPath}
|
||||
>
|
||||
data-folderul={folderPath}>
|
||||
{node.children.map((childNode, i) => (
|
||||
<ExplorerNode
|
||||
node={childNode}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import style from "./styles/footer.scss"
|
||||
import {version} from "../../package.json"
|
||||
import {i18n} from "../i18n"
|
||||
@ -8,7 +12,10 @@ interface Options {
|
||||
}
|
||||
|
||||
export default ((opts?: Options) => {
|
||||
const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const Footer: QuartzComponent = ({
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const year = new Date().getFullYear()
|
||||
const links = opts?.links ?? []
|
||||
return (
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/graph.inline"
|
||||
import style from "./styles/graph.scss"
|
||||
@ -57,7 +61,10 @@ const defaultOptions: GraphOptions = {
|
||||
}
|
||||
|
||||
export default ((opts?: GraphOptions) => {
|
||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const Graph: QuartzComponent = ({
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const localGraph = {...defaultOptions.localGraph, ...opts?.localGraph}
|
||||
const globalGraph = {...defaultOptions.globalGraph, ...opts?.globalGraph}
|
||||
return (
|
||||
@ -74,8 +81,7 @@ export default ((opts?: GraphOptions) => {
|
||||
y="0px"
|
||||
viewBox="0 0 55 55"
|
||||
fill="currentColor"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
xmlSpace="preserve">
|
||||
<path
|
||||
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
|
||||
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
|
||||
@ -92,7 +98,9 @@ export default ((opts?: GraphOptions) => {
|
||||
</svg>
|
||||
</div>
|
||||
<div id="global-graph-outer">
|
||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||
<div
|
||||
id="global-graph-container"
|
||||
data-cfg={JSON.stringify(globalGraph)}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,13 +2,23 @@ import { i18n } from "../i18n"
|
||||
import {FullSlug, joinSegments, pathToRoot} from "../util/path"
|
||||
import {JSResourceToScriptElement} from "../util/resources"
|
||||
import {googleFontHref} from "../util/theme"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
|
||||
export default (() => {
|
||||
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
|
||||
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const Head: QuartzComponent = ({
|
||||
cfg,
|
||||
fileData,
|
||||
externalResources,
|
||||
}: QuartzComponentProps) => {
|
||||
const title =
|
||||
fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const description =
|
||||
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
||||
fileData.description?.trim() ??
|
||||
i18n(cfg.locale).propertyDefaults.description
|
||||
const {css, js} = externalResources
|
||||
|
||||
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||
@ -39,7 +49,13 @@ export default (() => {
|
||||
<meta name="description" content={description} />
|
||||
<meta name="generator" content="Quartz" />
|
||||
{css.map((href) => (
|
||||
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
|
||||
<link
|
||||
key={href}
|
||||
href={href}
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
spa-preserve
|
||||
/>
|
||||
))}
|
||||
{js
|
||||
.filter((resource) => resource.loadTime === "beforeDOMReady")
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
|
||||
const Header: QuartzComponent = ({children}: QuartzComponentProps) => {
|
||||
return children.length > 0 ? <header>{children}</header> : null
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
|
||||
export default ((component?: QuartzComponent) => {
|
||||
if (component) {
|
||||
|
||||
@ -30,7 +30,13 @@ type Props = {
|
||||
sort?: SortFn
|
||||
} & QuartzComponentProps
|
||||
|
||||
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
|
||||
export const PageList: QuartzComponent = ({
|
||||
cfg,
|
||||
fileData,
|
||||
allFiles,
|
||||
limit,
|
||||
sort,
|
||||
}: Props) => {
|
||||
const sorter = sort ?? byDateAndAlphabetical(cfg)
|
||||
let list = allFiles.sort(sorter)
|
||||
if (limit) {
|
||||
@ -53,7 +59,9 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
||||
)}
|
||||
<div class="desc">
|
||||
<h3>
|
||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
||||
<a
|
||||
href={resolveRelative(fileData.slug!, page.slug!)}
|
||||
class="internal">
|
||||
{title}
|
||||
</a>
|
||||
</h3>
|
||||
@ -63,8 +71,10 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
||||
<li>
|
||||
<a
|
||||
class="internal tag-link"
|
||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
||||
>
|
||||
href={resolveRelative(
|
||||
fileData.slug!,
|
||||
`tags/${tag}` as FullSlug,
|
||||
)}>
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import {pathToRoot} from "../util/path"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {classNames} from "../util/lang"
|
||||
import {i18n} from "../i18n"
|
||||
|
||||
const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => {
|
||||
const PageTitle: QuartzComponent = ({
|
||||
fileData,
|
||||
cfg,
|
||||
displayClass,
|
||||
}: QuartzComponentProps) => {
|
||||
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const baseDir = pathToRoot(fileData.slug!)
|
||||
return (
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {FullSlug, SimpleSlug, resolveRelative} from "../util/path"
|
||||
import {QuartzPluginData} from "../plugins/vfile"
|
||||
import {byDateAndAlphabetical} from "./PageList"
|
||||
@ -40,7 +44,8 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
<h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
|
||||
<ul class="recent-ul">
|
||||
{pages.slice(0, opts.limit).map((page) => {
|
||||
const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const title =
|
||||
page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const tags = page.frontmatter?.tags ?? []
|
||||
|
||||
return (
|
||||
@ -48,7 +53,9 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
<div class="section">
|
||||
<div class="desc">
|
||||
<h3>
|
||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
||||
<a
|
||||
href={resolveRelative(fileData.slug!, page.slug!)}
|
||||
class="internal">
|
||||
{title}
|
||||
</a>
|
||||
</h3>
|
||||
@ -64,8 +71,10 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
<li>
|
||||
<a
|
||||
class="internal tag-link"
|
||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
||||
>
|
||||
href={resolveRelative(
|
||||
fileData.slug!,
|
||||
`tags/${tag}` as FullSlug,
|
||||
)}>
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
@ -80,7 +89,9 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
{opts.linkToMore && remaining > 0 && (
|
||||
<p>
|
||||
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
|
||||
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
|
||||
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({
|
||||
remaining,
|
||||
})}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import style from "./styles/search.scss"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/search.inline"
|
||||
@ -14,14 +18,21 @@ const defaultOptions: SearchOptions = {
|
||||
}
|
||||
|
||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
||||
const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const Search: QuartzComponent = ({
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
||||
const searchPlaceholder = i18n(cfg.locale).components.search
|
||||
.searchBarPlaceholder
|
||||
return (
|
||||
<div class={classNames(displayClass, "search")}>
|
||||
<button class="search-button" id="search-button">
|
||||
<p>{i18n(cfg.locale).components.search.title}</p>
|
||||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
|
||||
<svg
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 19.9 19.7">
|
||||
<title>Search</title>
|
||||
<g class="search-path" fill="none">
|
||||
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import legacyStyle from "./styles/legacyToc.scss"
|
||||
import modernStyle from "./styles/toc.scss"
|
||||
import {classNames} from "../util/lang"
|
||||
@ -31,8 +35,7 @@ const TableOfContents: QuartzComponent = ({
|
||||
id="toc"
|
||||
class={fileData.collapseToc ? "collapsed" : ""}
|
||||
aria-controls="toc-content"
|
||||
aria-expanded={!fileData.collapseToc}
|
||||
>
|
||||
aria-expanded={!fileData.collapseToc}>
|
||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -44,8 +47,7 @@ const TableOfContents: QuartzComponent = ({
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="fold"
|
||||
>
|
||||
class="fold">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
@ -66,7 +68,10 @@ const TableOfContents: QuartzComponent = ({
|
||||
TableOfContents.css = modernStyle
|
||||
TableOfContents.afterDOMLoaded = script
|
||||
|
||||
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
||||
const LegacyTableOfContents: QuartzComponent = ({
|
||||
fileData,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
if (!fileData.toc) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import {pathToRoot, slugTag} from "../util/path"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "./types"
|
||||
import {classNames} from "../util/lang"
|
||||
|
||||
const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
||||
const TagList: QuartzComponent = ({
|
||||
fileData,
|
||||
displayClass,
|
||||
}: QuartzComponentProps) => {
|
||||
const tags = fileData.frontmatter?.tags
|
||||
const baseDir = pathToRoot(fileData.slug!)
|
||||
if (tags && tags.length > 0) {
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import {i18n} from "../../i18n"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "../types"
|
||||
|
||||
const NotFound: QuartzComponent = ({cfg}: QuartzComponentProps) => {
|
||||
// If baseUrl contains a pathname after the domain, use this as the home link
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import {htmlToJsx} from "../../util/jsx"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "../types"
|
||||
|
||||
const Content: QuartzComponent = ({fileData, tree}: QuartzComponentProps) => {
|
||||
const content = htmlToJsx(fileData.filePath!, tree)
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "../types"
|
||||
import path from "path"
|
||||
|
||||
import style from "../styles/listPage.scss"
|
||||
@ -28,7 +32,8 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
||||
const allPagesInFolder = allFiles.filter((file) => {
|
||||
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
||||
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||
const prefixed =
|
||||
fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||
const folderParts = folderSlug.split(path.posix.sep)
|
||||
const fileParts = fileSlug.split(path.posix.sep)
|
||||
const isDirectChild = fileParts.length === folderParts.length + 1
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
} from "../types"
|
||||
import style from "../styles/listPage.scss"
|
||||
import {PageList, SortFn} from "../PageList"
|
||||
import {FullSlug, getAllSegmentPrefixes, simplifySlug} from "../../util/path"
|
||||
@ -24,13 +28,17 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
const slug = fileData.slug
|
||||
|
||||
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
||||
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
|
||||
throw new Error(
|
||||
`Component "TagContent" tried to render a non-tag page: ${slug}`,
|
||||
)
|
||||
}
|
||||
|
||||
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
|
||||
const allPagesWithTag = (tag: string) =>
|
||||
allFiles.filter((file) =>
|
||||
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
|
||||
(file.frontmatter?.tags ?? [])
|
||||
.flatMap(getAllSegmentPrefixes)
|
||||
.includes(tag),
|
||||
)
|
||||
|
||||
const content =
|
||||
@ -42,7 +50,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
if (tag === "/") {
|
||||
const tags = [
|
||||
...new Set(
|
||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||
allFiles
|
||||
.flatMap((data) => data.frontmatter?.tags ?? [])
|
||||
.flatMap(getAllSegmentPrefixes),
|
||||
),
|
||||
].sort((a, b) => a.localeCompare(b))
|
||||
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
||||
@ -54,7 +64,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
<article>
|
||||
<p>{content}</p>
|
||||
</article>
|
||||
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
||||
<p>
|
||||
{i18n(cfg.locale).pages.tagContent.totalTags({count: tags.length})}
|
||||
</p>
|
||||
<div>
|
||||
{tags.map((tag) => {
|
||||
const pages = tagItemMap.get(tag)!
|
||||
@ -63,7 +75,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
allFiles: pages,
|
||||
}
|
||||
|
||||
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
|
||||
const contentPage = allFiles
|
||||
.filter((file) => file.slug === `tags/${tag}`)
|
||||
.at(0)
|
||||
|
||||
const root = contentPage?.htmlAst
|
||||
const content =
|
||||
@ -81,7 +95,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
{content && <p>{content}</p>}
|
||||
<div class="page-listing">
|
||||
<p>
|
||||
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
||||
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({
|
||||
count: pages.length,
|
||||
})}
|
||||
{pages.length > options.numPages && (
|
||||
<>
|
||||
{" "}
|
||||
@ -93,7 +109,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
|
||||
<PageList
|
||||
limit={options.numPages}
|
||||
{...listProps}
|
||||
sort={opts?.sort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -112,7 +132,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
||||
<div class={classes}>
|
||||
<article>{content}</article>
|
||||
<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>
|
||||
<PageList {...listProps} />
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,13 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||
import HeaderConstructor from "./Header"
|
||||
import BodyConstructor from "./Body"
|
||||
import {JSResourceToScriptElement, StaticResources} from "../util/resources"
|
||||
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||
import {
|
||||
clone,
|
||||
FullSlug,
|
||||
RelativeURL,
|
||||
joinSegments,
|
||||
normalizeHastElement,
|
||||
} from "../util/path"
|
||||
import {visit} from "unist-util-visit"
|
||||
import {Root, Element, ElementContent} from "hast"
|
||||
import {GlobalConfiguration} from "../cfg"
|
||||
@ -71,7 +77,9 @@ export function renderPage(
|
||||
if (classNames.includes("transclude")) {
|
||||
const inner = node.children[0] as Element
|
||||
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
||||
const page = componentData.allFiles.find(
|
||||
(f) => f.slug === transcludeTarget,
|
||||
)
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
@ -96,9 +104,16 @@ export function renderPage(
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
properties: {
|
||||
href: inner.properties?.href,
|
||||
class: ["internal", "transclude-src"],
|
||||
},
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
{
|
||||
type: "text",
|
||||
value: i18n(cfg.locale).components.transcludes
|
||||
.linkToOriginal,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@ -111,7 +126,8 @@ export function renderPage(
|
||||
let endIdx = undefined
|
||||
for (const [i, el] of page.htmlAst.children.entries()) {
|
||||
// skip non-headers
|
||||
if (!(el.type === "element" && el.tagName.match(headerRegex))) continue
|
||||
if (!(el.type === "element" && el.tagName.match(headerRegex)))
|
||||
continue
|
||||
const depth = Number(el.tagName.substring(1))
|
||||
|
||||
// lookin for our blockref
|
||||
@ -133,15 +149,23 @@ export function renderPage(
|
||||
}
|
||||
|
||||
node.children = [
|
||||
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
|
||||
...(
|
||||
page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]
|
||||
).map((child) =>
|
||||
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||
),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
properties: {
|
||||
href: inner.properties?.href,
|
||||
class: ["internal", "transclude-src"],
|
||||
},
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
{
|
||||
type: "text",
|
||||
value: i18n(cfg.locale).components.transcludes.linkToOriginal,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@ -169,9 +193,15 @@ export function renderPage(
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
properties: {
|
||||
href: inner.properties?.href,
|
||||
class: ["internal", "transclude-src"],
|
||||
},
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
{
|
||||
type: "text",
|
||||
value: i18n(cfg.locale).components.transcludes.linkToOriginal,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@ -212,7 +242,10 @@ export function renderPage(
|
||||
</div>
|
||||
)
|
||||
|
||||
const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
|
||||
const lang =
|
||||
componentData.fileData.frontmatter?.lang ??
|
||||
cfg.locale?.split("-")[0] ??
|
||||
"en"
|
||||
const doc = (
|
||||
<html lang={lang}>
|
||||
<Head {...componentData} />
|
||||
|
||||
@ -14,7 +14,9 @@ function toggleCallout(this: HTMLElement) {
|
||||
}
|
||||
|
||||
const collapsed = parent.classList.contains("is-collapsed")
|
||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||
const height = collapsed
|
||||
? parent.scrollHeight
|
||||
: parent.scrollHeight + current.scrollHeight
|
||||
parent.style.maxHeight = height + "px"
|
||||
|
||||
current = parent
|
||||
|
||||
@ -10,7 +10,9 @@ document.addEventListener("nav", () => {
|
||||
const elId = checkboxId(index)
|
||||
|
||||
const switchState = (e: Event) => {
|
||||
const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false"
|
||||
const newCheckboxState = (e.target as HTMLInputElement)?.checked
|
||||
? "true"
|
||||
: "false"
|
||||
localStorage.setItem(elId, newCheckboxState)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
const changeTheme = (e: CustomEventMap["themechange"]) => {
|
||||
const theme = e.detail.theme
|
||||
const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement
|
||||
const iframe = document.querySelector(
|
||||
"iframe.giscus-frame",
|
||||
) as HTMLIFrameElement
|
||||
if (!iframe) {
|
||||
return
|
||||
}
|
||||
@ -49,11 +51,20 @@ document.addEventListener("nav", () => {
|
||||
giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo)
|
||||
giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId)
|
||||
giscusScript.setAttribute("data-category", giscusContainer.dataset.category)
|
||||
giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId)
|
||||
giscusScript.setAttribute(
|
||||
"data-category-id",
|
||||
giscusContainer.dataset.categoryId,
|
||||
)
|
||||
giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping)
|
||||
giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict)
|
||||
giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled)
|
||||
giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition)
|
||||
giscusScript.setAttribute(
|
||||
"data-reactions-enabled",
|
||||
giscusContainer.dataset.reactionsEnabled,
|
||||
)
|
||||
giscusScript.setAttribute(
|
||||
"data-input-position",
|
||||
giscusContainer.dataset.inputPosition,
|
||||
)
|
||||
|
||||
const theme = document.documentElement.getAttribute("saved-theme")
|
||||
if (theme) {
|
||||
@ -65,6 +76,6 @@ document.addEventListener("nav", () => {
|
||||
document.addEventListener("themechange", changeTheme)
|
||||
// window.addCleanup(() => document.removeEventListener("themechange", changeTheme))
|
||||
window.addCleanup(() =>
|
||||
document.removeEventListener("themechange", changeTheme as EventListener)
|
||||
);
|
||||
document.removeEventListener("themechange", changeTheme as EventListener),
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"
|
||||
const userPref = window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
? "light"
|
||||
: "dark"
|
||||
const currentTheme = localStorage.getItem("theme") ?? userPref
|
||||
document.documentElement.setAttribute("saved-theme", currentTheme)
|
||||
|
||||
@ -26,15 +28,23 @@ document.addEventListener("nav", () => {
|
||||
}
|
||||
|
||||
// Darkmode toggle
|
||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||
const toggleSwitch = document.querySelector(
|
||||
"#darkmode-toggle",
|
||||
) as HTMLInputElement
|
||||
toggleSwitch.addEventListener("change", switchTheme)
|
||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||
window.addCleanup(() =>
|
||||
toggleSwitch.removeEventListener("change", switchTheme),
|
||||
)
|
||||
if (currentTheme === "dark") {
|
||||
toggleSwitch.checked = true
|
||||
}
|
||||
|
||||
// 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)
|
||||
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
|
||||
window.addCleanup(() =>
|
||||
colorSchemeMediaQuery.removeEventListener("change", themeChange),
|
||||
)
|
||||
})
|
||||
|
||||
@ -25,7 +25,8 @@ function toggleExplorer(this: HTMLElement) {
|
||||
if (!content) return
|
||||
|
||||
content.classList.toggle("collapsed")
|
||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||
content.style.maxHeight =
|
||||
content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||
}
|
||||
|
||||
function toggleFolder(evt: MouseEvent) {
|
||||
@ -82,20 +83,26 @@ function setupExplorer() {
|
||||
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 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.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
|
||||
const folderUl = folderLi?.parentElement
|
||||
?.nextElementSibling as MaybeHTMLElement
|
||||
if (folderUl) {
|
||||
setFolderState(folderUl, folderState.collapsed)
|
||||
}
|
||||
@ -120,7 +127,9 @@ document.addEventListener("nav", () => {
|
||||
* @param collapsed if folder should be set to collapsed or not
|
||||
*/
|
||||
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
||||
return collapsed
|
||||
? folderElement.classList.remove("open")
|
||||
: folderElement.classList.add("open")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import type {ContentDetails} from "../../plugins/emitters/contentIndex"
|
||||
import * as d3 from "d3"
|
||||
import {registerEscapeHandler, removeAllChildren} from "./util"
|
||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||
import {
|
||||
FullSlug,
|
||||
SimpleSlug,
|
||||
getFullSlug,
|
||||
resolveRelative,
|
||||
simplifySlug,
|
||||
} from "../../util/path"
|
||||
|
||||
type NodeData = {
|
||||
id: SimpleSlug
|
||||
@ -92,7 +98,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
neighbourhood.add(cur)
|
||||
const outgoing = links.filter((l) => l.source === cur)
|
||||
const incoming = links.filter((l) => l.target === cur)
|
||||
wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
|
||||
wl.push(
|
||||
...outgoing.map((l) => l.target),
|
||||
...incoming.map((l) => l.source),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -102,14 +111,18 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
|
||||
const graphData: {nodes: NodeData[]; links: LinkData[]} = {
|
||||
nodes: [...neighbourhood].map((url) => {
|
||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
|
||||
const text = url.startsWith("tags/")
|
||||
? "#" + url.substring(5)
|
||||
: data.get(url)?.title ?? url
|
||||
return {
|
||||
id: url,
|
||||
text: text,
|
||||
tags: data.get(url)?.tags ?? [],
|
||||
}
|
||||
}),
|
||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
||||
links: links.filter(
|
||||
(l) => neighbourhood.has(l.source) && neighbourhood.has(l.target),
|
||||
),
|
||||
}
|
||||
|
||||
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
||||
@ -132,7 +145,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
||||
.attr("viewBox", [
|
||||
-width / 2 / scale,
|
||||
-height / 2 / scale,
|
||||
width / scale,
|
||||
height / scale,
|
||||
])
|
||||
|
||||
// draw links between nodes
|
||||
const link = svg
|
||||
@ -145,7 +163,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
.attr("stroke-width", 1)
|
||||
|
||||
// svg groups
|
||||
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
|
||||
const graphNode = svg
|
||||
.append("g")
|
||||
.selectAll("g")
|
||||
.data(graphData.nodes)
|
||||
.enter()
|
||||
.append("g")
|
||||
|
||||
// calculate color
|
||||
const color = (d: NodeData) => {
|
||||
@ -186,7 +209,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
}
|
||||
|
||||
function nodeRadius(d: NodeData) {
|
||||
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length
|
||||
const numLinks = links.filter(
|
||||
(l: any) => l.source.id === d.id || l.target.id === d.id,
|
||||
).length
|
||||
return 2 + Math.sqrt(numLinks)
|
||||
}
|
||||
|
||||
@ -208,11 +233,15 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
const currentId = d.id
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
||||
.filter(
|
||||
(d: any) => d.source.id === currentId || d.target.id === currentId,
|
||||
)
|
||||
|
||||
if (focusOnHover) {
|
||||
// fade out non-neighbour nodes
|
||||
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
|
||||
connectedNodes = linkNodes
|
||||
.data()
|
||||
.flatMap((d: any) => [d.source.id, d.target.id])
|
||||
|
||||
d3.selectAll<HTMLElement, NodeData>(".link")
|
||||
.transition()
|
||||
@ -238,7 +267,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
}
|
||||
|
||||
// highlight links
|
||||
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
|
||||
linkNodes
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("stroke", "var(--gray)")
|
||||
.attr("stroke-width", 1)
|
||||
|
||||
const bigFont = fontSize * 1.5
|
||||
|
||||
@ -255,19 +288,32 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
})
|
||||
.on("mouseleave", function (_, d) {
|
||||
if (focusOnHover) {
|
||||
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
|
||||
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
|
||||
d3.selectAll<HTMLElement, NodeData>(".link")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1)
|
||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1)
|
||||
|
||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
||||
.filter((d) => !connectedNodes.includes(d.id))
|
||||
.nodes()
|
||||
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
|
||||
.forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld")))
|
||||
.forEach((it) =>
|
||||
it
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("opacity", it.attr("opacityOld")),
|
||||
)
|
||||
}
|
||||
const currentId = d.id
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
||||
.filter(
|
||||
(d: any) => d.source.id === currentId || d.target.id === currentId,
|
||||
)
|
||||
|
||||
linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
|
||||
|
||||
@ -366,5 +412,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
|
||||
const containerIcon = document.getElementById("global-graph-icon")
|
||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||
window.addCleanup(() =>
|
||||
containerIcon?.removeEventListener("click", renderGlobalGraph),
|
||||
)
|
||||
})
|
||||
|
||||
@ -100,9 +100,13 @@ async function mouseEnterHandler(
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
|
||||
const links = [
|
||||
...document.getElementsByClassName("internal"),
|
||||
] as HTMLAnchorElement[]
|
||||
for (const link of links) {
|
||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||
window.addCleanup(() =>
|
||||
link.removeEventListener("mouseenter", mouseEnterHandler),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@ -15,7 +15,8 @@ interface Item {
|
||||
type SearchType = "basic" | "tags"
|
||||
let searchType: SearchType = "basic"
|
||||
let currentSearchTerm: string = ""
|
||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
const encoder = (str: string) =>
|
||||
str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
let index = new FlexSearch.Document<Item>({
|
||||
charset: "latin:extra",
|
||||
encode: encoder,
|
||||
@ -65,12 +66,18 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||
let endIndex = tokenizedText.length - 1
|
||||
if (trim) {
|
||||
const includesCheck = (tok: string) =>
|
||||
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
||||
tokenizedTerms.some((term) =>
|
||||
tok.toLowerCase().startsWith(term.toLowerCase()),
|
||||
)
|
||||
const occurrencesIndices = tokenizedText.map(includesCheck)
|
||||
|
||||
let bestSum = 0
|
||||
let bestIndex = 0
|
||||
for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.max(tokenizedText.length - contextWindowWords, 0);
|
||||
i++
|
||||
) {
|
||||
const window = occurrencesIndices.slice(i, i + contextWindowWords)
|
||||
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
|
||||
if (windowSum >= bestSum) {
|
||||
@ -80,7 +87,10 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||
}
|
||||
|
||||
startIndex = Math.max(bestIndex - contextWindowWords, 0)
|
||||
endIndex = Math.min(startIndex + 2 * contextWindowWords, tokenizedText.length - 1)
|
||||
endIndex = Math.min(
|
||||
startIndex + 2 * contextWindowWords,
|
||||
tokenizedText.length - 1,
|
||||
)
|
||||
tokenizedText = tokenizedText.slice(startIndex, endIndex)
|
||||
}
|
||||
|
||||
@ -124,15 +134,21 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||
let lastIndex = 0
|
||||
for (const match of matches) {
|
||||
const matchIndex = nodeText.indexOf(match, lastIndex)
|
||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex)))
|
||||
spanContainer.appendChild(
|
||||
document.createTextNode(nodeText.slice(lastIndex, matchIndex)),
|
||||
)
|
||||
spanContainer.appendChild(createHighlightSpan(match))
|
||||
lastIndex = matchIndex + match.length
|
||||
}
|
||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex)))
|
||||
spanContainer.appendChild(
|
||||
document.createTextNode(nodeText.slice(lastIndex)),
|
||||
)
|
||||
node.parentNode?.replaceChild(spanContainer, node)
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if ((node as HTMLElement).classList.contains("highlight")) return
|
||||
Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term))
|
||||
Array.from(node.childNodes).forEach((child) =>
|
||||
highlightTextNodes(child, term),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +165,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
const container = document.getElementById("search-container")
|
||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||
const searchButton = document.getElementById("search-button")
|
||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||
const searchBar = document.getElementById(
|
||||
"search-bar",
|
||||
) as HTMLInputElement | null
|
||||
const searchLayout = document.getElementById("search-layout")
|
||||
const idDataMap = Object.keys(data) as FullSlug[]
|
||||
|
||||
@ -212,7 +230,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
const searchBarOpen = container?.classList.contains("active")
|
||||
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||
return
|
||||
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||
} else if (
|
||||
e.shiftKey &&
|
||||
(e.ctrlKey || e.metaKey) &&
|
||||
e.key.toLowerCase() === "k"
|
||||
) {
|
||||
// Hotkey to open tag search
|
||||
e.preventDefault()
|
||||
const searchBarOpen = container?.classList.contains("active")
|
||||
@ -237,7 +259,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
await displayPreview(active)
|
||||
active.click()
|
||||
} else {
|
||||
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||
const anchor = document.getElementsByClassName(
|
||||
"result-card",
|
||||
)[0] as HTMLInputElement | null
|
||||
if (!anchor || anchor?.classList.contains("no-match")) return
|
||||
await displayPreview(anchor)
|
||||
anchor.click()
|
||||
@ -249,7 +273,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
const currentResult = currentHover
|
||||
? currentHover
|
||||
: (document.activeElement as HTMLInputElement | null)
|
||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
||||
const prevResult =
|
||||
currentResult?.previousElementSibling as HTMLInputElement | null
|
||||
currentResult?.classList.remove("focus")
|
||||
prevResult?.focus()
|
||||
if (prevResult) currentHover = prevResult
|
||||
@ -262,8 +287,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
if (document.activeElement === searchBar || currentHover !== null) {
|
||||
const firstResult = currentHover
|
||||
? currentHover
|
||||
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
||||
: (document.getElementsByClassName(
|
||||
"result-card",
|
||||
)[0] as HTMLInputElement | null)
|
||||
const secondResult =
|
||||
firstResult?.nextElementSibling as HTMLInputElement | null
|
||||
firstResult?.classList.remove("focus")
|
||||
secondResult?.focus()
|
||||
if (secondResult) currentHover = secondResult
|
||||
@ -277,7 +305,10 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
||||
title:
|
||||
searchType === "tags"
|
||||
? data[slug].title
|
||||
: highlight(term, data[slug].title ?? ""),
|
||||
content: highlight(term, data[slug].content ?? "", true),
|
||||
tags: highlightTags(term.substring(1), data[slug].tags),
|
||||
}
|
||||
@ -304,7 +335,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
}
|
||||
|
||||
const resultToHTML = ({slug, title, content, tags}: Item) => {
|
||||
const htmlTags = tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : ``
|
||||
const htmlTags =
|
||||
tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : ``
|
||||
const itemTile = document.createElement("a")
|
||||
itemTile.classList.add("result-card")
|
||||
itemTile.id = slug
|
||||
@ -313,12 +345,14 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
|
||||
}`
|
||||
itemTile.addEventListener("click", (event) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
||||
return
|
||||
hideSearch()
|
||||
})
|
||||
|
||||
const handler = (event: MouseEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
||||
return
|
||||
hideSearch()
|
||||
}
|
||||
|
||||
@ -329,7 +363,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
}
|
||||
|
||||
itemTile.addEventListener("mouseenter", onMouseEnter)
|
||||
window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter))
|
||||
window.addCleanup(() =>
|
||||
itemTile.removeEventListener("mouseenter", onMouseEnter),
|
||||
)
|
||||
itemTile.addEventListener("click", handler)
|
||||
window.addCleanup(() => itemTile.removeEventListener("click", handler))
|
||||
|
||||
@ -386,7 +422,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
if (!searchLayout || !enablePreview || !el || !preview) return
|
||||
const slug = el.id as FullSlug
|
||||
const innerDiv = await fetchContent(slug).then((contents) =>
|
||||
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
||||
contents.flatMap((el) => [
|
||||
...highlightHTML(currentSearchTerm, el as HTMLElement).children,
|
||||
]),
|
||||
)
|
||||
previewInner = document.createElement("div")
|
||||
previewInner.classList.add("preview-inner")
|
||||
@ -454,14 +492,20 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
...getByField("content"),
|
||||
...getByField("tags"),
|
||||
])
|
||||
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
|
||||
const finalResults = [...allIds].map((id) =>
|
||||
formatForDisplay(currentSearchTerm, id),
|
||||
)
|
||||
await displayResults(finalResults)
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", shortcutHandler)
|
||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||
window.addCleanup(() =>
|
||||
document.removeEventListener("keydown", shortcutHandler),
|
||||
)
|
||||
searchButton?.addEventListener("click", () => showSearch("basic"))
|
||||
window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic")))
|
||||
window.addCleanup(() =>
|
||||
searchButton?.removeEventListener("click", () => showSearch("basic")),
|
||||
)
|
||||
searchBar?.addEventListener("input", onType)
|
||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import micromorph from "micromorph"
|
||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||
import {
|
||||
FullSlug,
|
||||
RelativeURL,
|
||||
getFullSlug,
|
||||
normalizeRelativeURLs,
|
||||
} from "../../util/path"
|
||||
|
||||
// adapted from `micromorph`
|
||||
// https://github.com/natemoo-re/micromorph
|
||||
@ -31,7 +36,10 @@ const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined
|
||||
if ("routerIgnore" in a.dataset) return
|
||||
const {href} = a
|
||||
if (!isLocalUrl(href)) return
|
||||
return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
|
||||
return {
|
||||
url: new URL(href),
|
||||
scroll: "routerNoscroll" in a.dataset ? false : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function notifyNav(url: FullSlug) {
|
||||
@ -86,7 +94,9 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||
// scroll into place and add history
|
||||
if (!isBack) {
|
||||
if (url.hash) {
|
||||
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||
const el = document.getElementById(
|
||||
decodeURIComponent(url.hash.substring(1)),
|
||||
)
|
||||
el?.scrollIntoView()
|
||||
} else {
|
||||
window.scrollTo({top: 0})
|
||||
@ -94,7 +104,9 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||
}
|
||||
|
||||
// now, patch head
|
||||
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
|
||||
const elementsToRemove = document.head.querySelectorAll(
|
||||
":not([spa-preserve])",
|
||||
)
|
||||
elementsToRemove.forEach((el) => el.remove())
|
||||
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
|
||||
elementsToAdd.forEach((el) => document.head.appendChild(el))
|
||||
@ -119,7 +131,9 @@ function createRouter() {
|
||||
event.preventDefault()
|
||||
|
||||
if (isSamePage(url) && url.hash) {
|
||||
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||
const el = document.getElementById(
|
||||
decodeURIComponent(url.hash.substring(1)),
|
||||
)
|
||||
el?.scrollIntoView()
|
||||
history.pushState({}, "", url)
|
||||
return
|
||||
@ -134,7 +148,8 @@ function createRouter() {
|
||||
|
||||
window.addEventListener("popstate", (event) => {
|
||||
const {url} = getOpts(event) ?? {}
|
||||
if (window.location.hash && window.location.pathname === url?.pathname) return
|
||||
if (window.location.hash && window.location.pathname === url?.pathname)
|
||||
return
|
||||
try {
|
||||
navigate(new URL(window.location.toString()), true)
|
||||
} catch (e) {
|
||||
@ -167,7 +182,7 @@ if (!customElements.get("route-announcer")) {
|
||||
const attrs = {
|
||||
"aria-live": "assertive",
|
||||
"aria-atomic": "true",
|
||||
style:
|
||||
"style":
|
||||
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,8 @@ function toggleToc(this: HTMLElement) {
|
||||
const content = this.nextElementSibling as HTMLElement | undefined
|
||||
if (!content) return
|
||||
content.classList.toggle("collapsed")
|
||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||
content.style.maxHeight =
|
||||
content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||
}
|
||||
|
||||
function setupToc() {
|
||||
@ -44,6 +45,8 @@ document.addEventListener("nav", () => {
|
||||
|
||||
// update toc entry highlighting
|
||||
observer.disconnect()
|
||||
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
|
||||
const headers = document.querySelectorAll(
|
||||
"h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]",
|
||||
)
|
||||
headers.forEach((header) => observer.observe(header))
|
||||
})
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) {
|
||||
export function registerEscapeHandler(
|
||||
outsideContainer: HTMLElement | null,
|
||||
cb: () => void,
|
||||
) {
|
||||
if (!outsideContainer) return
|
||||
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
|
||||
if (e.target !== this) return
|
||||
|
||||
@ -143,7 +143,11 @@
|
||||
}
|
||||
|
||||
& .highlight {
|
||||
background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0));
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--tertiary) 60%,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
border-radius: 5px;
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
|
||||
@ -24,6 +24,6 @@ export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||
afterDOMLoaded?: string
|
||||
}
|
||||
|
||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
||||
opts: Options,
|
||||
) => QuartzComponent
|
||||
export type QuartzComponentConstructor<
|
||||
Options extends object | undefined = undefined,
|
||||
> = (opts: Options) => QuartzComponent
|
||||
|
||||
@ -65,6 +65,7 @@ export const TRANSLATIONS = {
|
||||
} as const
|
||||
|
||||
export const defaultTranslation = "en-US"
|
||||
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation]
|
||||
export const i18n = (locale: ValidLocale): Translation =>
|
||||
TRANSLATIONS[locale ?? defaultTranslation]
|
||||
export type ValidLocale = keyof typeof TRANSLATIONS
|
||||
export type ValidCallout = keyof CalloutTranslation
|
||||
|
||||
@ -75,13 +75,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "مجلد",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`,
|
||||
count === 1
|
||||
? "يوجد عنصر واحد فقط تحت هذا المجلد"
|
||||
: `يوجد ${count} عناصر تحت هذا المجلد.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "الوسم",
|
||||
tagIndex: "مؤشر الوسم",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`,
|
||||
count === 1
|
||||
? "يوجد عنصر واحد فقط تحت هذا الوسم"
|
||||
: `يوجد ${count} عناصر تحت هذا الوسم.`,
|
||||
showingFirst: ({count}) => `إظهار أول ${count} أوسمة.`,
|
||||
totalTags: ({count}) => `يوجد ${count} أوسمة.`,
|
||||
},
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Carpeta",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 article en aquesta carpeta." : `${count} articles en esta carpeta.`,
|
||||
count === 1
|
||||
? "1 article en aquesta carpeta."
|
||||
: `${count} articles en esta carpeta.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etiqueta",
|
||||
tagIndex: "índex d'Etiquetes",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 article amb aquesta etiqueta." : `${count} article amb aquesta etiqueta.`,
|
||||
count === 1
|
||||
? "1 article amb aquesta etiqueta."
|
||||
: `${count} article amb aquesta etiqueta.`,
|
||||
showingFirst: ({count}) => `Mostrant les primeres ${count} etiquetes.`,
|
||||
totalTags: ({count}) => `S'han trobat ${count} etiquetes en total.`,
|
||||
},
|
||||
|
||||
@ -64,19 +64,24 @@ export default {
|
||||
},
|
||||
error: {
|
||||
title: "Nicht gefunden",
|
||||
notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
|
||||
notFound:
|
||||
"Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
|
||||
home: "Return to Homepage",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Ordner",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 Datei in diesem Ordner." : `${count} Dateien in diesem Ordner.`,
|
||||
count === 1
|
||||
? "1 Datei in diesem Ordner."
|
||||
: `${count} Dateien in diesem Ordner.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Tag",
|
||||
tagIndex: "Tag-Übersicht",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`,
|
||||
count === 1
|
||||
? "1 Datei mit diesem Tag."
|
||||
: `${count} Dateien mit diesem Tag.`,
|
||||
showingFirst: ({count}) => `Die ersten ${count} Tags werden angezeigt.`,
|
||||
totalTags: ({count}) => `${count} Tags insgesamt.`,
|
||||
},
|
||||
|
||||
@ -70,7 +70,9 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Folder",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 item under this folder." : `${count} items under this folder.`,
|
||||
count === 1
|
||||
? "1 item under this folder."
|
||||
: `${count} items under this folder.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Tag",
|
||||
|
||||
@ -70,7 +70,9 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Folder",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 item under this folder." : `${count} items under this folder.`,
|
||||
count === 1
|
||||
? "1 item under this folder."
|
||||
: `${count} items under this folder.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Tag",
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Carpeta",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 artículo en esta carpeta." : `${count} artículos en esta carpeta.`,
|
||||
count === 1
|
||||
? "1 artículo en esta carpeta."
|
||||
: `${count} artículos en esta carpeta.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etiqueta",
|
||||
tagIndex: "Índice de Etiquetas",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`,
|
||||
count === 1
|
||||
? "1 artículo con esta etiqueta."
|
||||
: `${count} artículos con esta etiqueta.`,
|
||||
showingFirst: ({count}) => `Mostrando las primeras ${count} etiquetas.`,
|
||||
totalTags: ({count}) => `Se han encontrado ${count} etiquetas en total.`,
|
||||
},
|
||||
|
||||
@ -70,7 +70,9 @@ export default {
|
||||
folderContent: {
|
||||
folder: "پوشه",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`,
|
||||
count === 1
|
||||
? ".یک مطلب در این پوشه است"
|
||||
: `${count} مطلب در این پوشه است.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "برچسب",
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Dossier",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`,
|
||||
count === 1
|
||||
? "1 élément sous ce dossier."
|
||||
: `${count} éléments sous ce dossier.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Étiquette",
|
||||
tagIndex: "Index des étiquettes",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`,
|
||||
count === 1
|
||||
? "1 élément avec cette étiquette."
|
||||
: `${count} éléments avec cette étiquette.`,
|
||||
showingFirst: ({count}) => `Affichage des premières ${count} étiquettes.`,
|
||||
totalTags: ({count}) => `Trouvé ${count} étiquettes au total.`,
|
||||
},
|
||||
|
||||
@ -69,7 +69,8 @@ export default {
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Mappa",
|
||||
itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`,
|
||||
itemsUnderFolder: ({count}) =>
|
||||
`Ebben a mappában ${count} elem található.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Címke",
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Cartella",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 oggetto in questa cartella." : `${count} oggetti in questa cartella.`,
|
||||
count === 1
|
||||
? "1 oggetto in questa cartella."
|
||||
: `${count} oggetti in questa cartella.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etichetta",
|
||||
tagIndex: "Indice etichette",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`,
|
||||
count === 1
|
||||
? "1 oggetto con questa etichetta."
|
||||
: `${count} oggetti con questa etichetta.`,
|
||||
showingFirst: ({count}) => `Prime ${count} etichette.`,
|
||||
totalTags: ({count}) => `Trovate ${count} etichette totali.`,
|
||||
},
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Folder",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "W tym folderze jest 1 element." : `Elementów w folderze: ${count}.`,
|
||||
count === 1
|
||||
? "W tym folderze jest 1 element."
|
||||
: `Elementów w folderze: ${count}.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Znacznik",
|
||||
tagIndex: "Spis znaczników",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`,
|
||||
count === 1
|
||||
? "Oznaczony 1 element."
|
||||
: `Elementów z tym znacznikiem: ${count}.`,
|
||||
showingFirst: ({count}) => `Pokazuje ${count} pierwszych znaczników.`,
|
||||
totalTags: ({count}) => `Znalezionych wszystkich znaczników: ${count}.`,
|
||||
},
|
||||
|
||||
@ -71,13 +71,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Dosar",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 articol în acest dosar." : `${count} elemente în acest dosar.`,
|
||||
count === 1
|
||||
? "1 articol în acest dosar."
|
||||
: `${count} elemente în acest dosar.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etichetă",
|
||||
tagIndex: "Indexul etichetelor",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 articol cu această etichetă." : `${count} articole cu această etichetă.`,
|
||||
count === 1
|
||||
? "1 articol cu această etichetă."
|
||||
: `${count} articole cu această etichetă.`,
|
||||
showingFirst: ({count}) => `Se afișează primele ${count} etichete.`,
|
||||
totalTags: ({count}) => `Au fost găsite ${count} etichete în total.`,
|
||||
},
|
||||
|
||||
@ -77,15 +77,22 @@ export default {
|
||||
tagContent: {
|
||||
tag: "Тег",
|
||||
tagIndex: "Индекс тегов",
|
||||
itemsUnderTag: ({ count }) => `с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`,
|
||||
itemsUnderTag: ({count}) =>
|
||||
`с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`,
|
||||
showingFirst: ({count}) =>
|
||||
`Показыва${getForm(count, "ется", "ются", "ются")} ${count} тег${getForm(count, "", "а", "ов")}`,
|
||||
totalTags: ({ count }) => `Всего ${count} тег${getForm(count, "", "а", "ов")}`,
|
||||
totalTags: ({count}) =>
|
||||
`Всего ${count} тег${getForm(count, "", "а", "ов")}`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
||||
|
||||
function getForm(number: number, form1: string, form2: string, form5: string): string {
|
||||
function getForm(
|
||||
number: number,
|
||||
form1: string,
|
||||
form2: string,
|
||||
form5: string,
|
||||
): string {
|
||||
const remainder100 = number % 100
|
||||
const remainder10 = remainder100 % 10
|
||||
|
||||
|
||||
@ -70,13 +70,17 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Тека",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "У цій теці 1 елемент." : `Елементів у цій теці: ${count}.`,
|
||||
count === 1
|
||||
? "У цій теці 1 елемент."
|
||||
: `Елементів у цій теці: ${count}.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Мітка",
|
||||
tagIndex: "Індекс мітки",
|
||||
itemsUnderTag: ({count}) =>
|
||||
count === 1 ? "1 елемент з цією міткою." : `Елементів з цією міткою: ${count}.`,
|
||||
count === 1
|
||||
? "1 елемент з цією міткою."
|
||||
: `Елементів з цією міткою: ${count}.`,
|
||||
showingFirst: ({count}) => `Показ перших ${count} міток.`,
|
||||
totalTags: ({count}) => `Всього знайдено міток: ${count}.`,
|
||||
},
|
||||
|
||||
@ -70,7 +70,9 @@ export default {
|
||||
folderContent: {
|
||||
folder: "Thư Mục",
|
||||
itemsUnderFolder: ({count}) =>
|
||||
count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`,
|
||||
count === 1
|
||||
? "1 mục trong thư mục này."
|
||||
: `${count} mục trong thư mục này.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Thẻ",
|
||||
|
||||
@ -58,7 +58,13 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||
return [
|
||||
await write({
|
||||
ctx,
|
||||
content: renderPage(cfg, slug, componentData, opts, externalResources),
|
||||
content: renderPage(
|
||||
cfg,
|
||||
slug,
|
||||
componentData,
|
||||
opts,
|
||||
externalResources,
|
||||
),
|
||||
slug,
|
||||
ext: ".html",
|
||||
}),
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
||||
import {
|
||||
FilePath,
|
||||
FullSlug,
|
||||
joinSegments,
|
||||
resolveRelative,
|
||||
simplifySlug,
|
||||
} from "../../util/path"
|
||||
import {QuartzEmitterPlugin} from "../types"
|
||||
import path from "path"
|
||||
import {write} from "./helpers"
|
||||
@ -14,9 +20,14 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||
|
||||
const {argv} = ctx
|
||||
for (const [_tree, file] of content) {
|
||||
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
||||
const dir = path.posix.relative(
|
||||
argv.directory,
|
||||
path.dirname(file.data.filePath!),
|
||||
)
|
||||
const aliases = file.data.frontmatter?.aliases ?? []
|
||||
const slugs = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
|
||||
const slugs = aliases.map(
|
||||
(alias) => path.posix.join(dir, alias) as FullSlug,
|
||||
)
|
||||
const permalink = file.data.frontmatter?.permalink
|
||||
if (typeof permalink === "string") {
|
||||
slugs.push(permalink as FullSlug)
|
||||
@ -28,7 +39,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||
slug = joinSegments(slug, "index") as FullSlug
|
||||
}
|
||||
|
||||
graph.addEdge(file.data.filePath!, joinSegments(argv.output, slug + ".html") as FilePath)
|
||||
graph.addEdge(
|
||||
file.data.filePath!,
|
||||
joinSegments(argv.output, slug + ".html") as FilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,9 +54,14 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||
|
||||
for (const [_tree, file] of content) {
|
||||
const ogSlug = simplifySlug(file.data.slug!)
|
||||
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
||||
const dir = path.posix.relative(
|
||||
argv.directory,
|
||||
path.dirname(file.data.filePath!),
|
||||
)
|
||||
const aliases = file.data.frontmatter?.aliases ?? []
|
||||
const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
|
||||
const slugs: FullSlug[] = aliases.map(
|
||||
(alias) => path.posix.join(dir, alias) as FullSlug,
|
||||
)
|
||||
const permalink = file.data.frontmatter?.permalink
|
||||
if (typeof permalink === "string") {
|
||||
slugs.push(permalink as FullSlug)
|
||||
|
||||
@ -9,7 +9,10 @@ import { QuartzConfig } from "../../cfg"
|
||||
|
||||
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
||||
// glob all non MD files in content folder and copy it over
|
||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
return await glob("**", argv.directory, [
|
||||
"**/*.md",
|
||||
...cfg.configuration.ignorePatterns,
|
||||
])
|
||||
}
|
||||
|
||||
export const Assets: QuartzEmitterPlugin = () => {
|
||||
|
||||
@ -19,7 +19,11 @@ export const CNAME: QuartzEmitterPlugin = () => ({
|
||||
},
|
||||
async emit({argv, cfg}, _content, _resources): Promise<FilePath[]> {
|
||||
if (!cfg.configuration.baseUrl) {
|
||||
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
"CNAME emitter requires `baseUrl` to be set in your configuration",
|
||||
),
|
||||
)
|
||||
return []
|
||||
}
|
||||
const path = joinSegments(argv.output, "CNAME")
|
||||
|
||||
@ -58,7 +58,9 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
||||
|
||||
async function joinScripts(scripts: string[]): Promise<string> {
|
||||
// wrap with iife to prevent scope collision
|
||||
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
|
||||
const script = scripts
|
||||
.map((script) => `(function () {${script}})();`)
|
||||
.join("\n")
|
||||
|
||||
// minify with esbuild
|
||||
const res = await transpile(script, {
|
||||
@ -68,7 +70,10 @@ async function joinScripts(scripts: string[]): Promise<string> {
|
||||
return res.code
|
||||
}
|
||||
|
||||
function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) {
|
||||
function addGlobalPageResources(
|
||||
ctx: BuildCtx,
|
||||
componentResources: ComponentResources,
|
||||
) {
|
||||
const cfg = ctx.cfg.configuration
|
||||
|
||||
// popovers
|
||||
@ -185,11 +190,15 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
|
||||
let googleFontsStyleSheet = ""
|
||||
if (cfg.theme.fontOrigin === "local") {
|
||||
// let the user do it themselves in css
|
||||
} else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) {
|
||||
} else if (
|
||||
cfg.theme.fontOrigin === "googleFonts" &&
|
||||
!cfg.theme.cdnCaching
|
||||
) {
|
||||
// when cdnCaching is true, we link to google fonts in Head.tsx
|
||||
let match
|
||||
|
||||
const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
|
||||
const fontSourceRegex =
|
||||
/url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
|
||||
|
||||
googleFontsStyleSheet = await (
|
||||
await fetch(googleFontHref(ctx.cfg.configuration.theme))
|
||||
|
||||
@ -2,7 +2,13 @@ import { Root } from "hast"
|
||||
import {GlobalConfiguration} from "../../cfg"
|
||||
import {getDate} from "../../components/Date"
|
||||
import {escapeHTML} from "../../util/escape"
|
||||
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||
import {
|
||||
FilePath,
|
||||
FullSlug,
|
||||
SimpleSlug,
|
||||
joinSegments,
|
||||
simplifySlug,
|
||||
} from "../../util/path"
|
||||
import {QuartzEmitterPlugin} from "../types"
|
||||
import {toHtml} from "hast-util-to-html"
|
||||
import {write} from "./helpers"
|
||||
@ -38,7 +44,10 @@ const defaultOptions: Options = {
|
||||
|
||||
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||
const base = cfg.baseUrl ?? ""
|
||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
|
||||
const createURLEntry = (
|
||||
slug: SimpleSlug,
|
||||
content: ContentDetails,
|
||||
): string => `<url>
|
||||
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
|
||||
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
|
||||
</url>`
|
||||
@ -48,10 +57,17 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
||||
}
|
||||
|
||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string {
|
||||
function generateRSSFeed(
|
||||
cfg: GlobalConfiguration,
|
||||
idx: ContentIndex,
|
||||
limit?: number,
|
||||
): string {
|
||||
const base = cfg.baseUrl ?? ""
|
||||
|
||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
||||
const createURLEntry = (
|
||||
slug: SimpleSlug,
|
||||
content: ContentDetails,
|
||||
): string => `<item>
|
||||
<title>${escapeHTML(content.title)}</title>
|
||||
<link>https://${joinSegments(base, encodeURI(slug))}</link>
|
||||
<guid>https://${joinSegments(base, encodeURI(slug))}</guid>
|
||||
@ -104,10 +120,16 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||
joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath,
|
||||
)
|
||||
if (opts?.enableSiteMap) {
|
||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath)
|
||||
graph.addEdge(
|
||||
sourcePath,
|
||||
joinSegments(ctx.argv.output, "sitemap.xml") as FilePath,
|
||||
)
|
||||
}
|
||||
if (opts?.enableRSS) {
|
||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath)
|
||||
graph.addEdge(
|
||||
sourcePath,
|
||||
joinSegments(ctx.argv.output, "index.xml") as FilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +142,10 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||
for (const [tree, file] of content) {
|
||||
const slug = file.data.slug!
|
||||
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
|
||||
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
||||
if (
|
||||
opts?.includeEmptyFiles ||
|
||||
(file.data.text && file.data.text !== "")
|
||||
) {
|
||||
linkIndex.set(slug, {
|
||||
title: file.data.frontmatter?.title!,
|
||||
links: file.data.links ?? [],
|
||||
|
||||
@ -9,8 +9,16 @@ import BodyConstructor from "../../components/Body"
|
||||
import {pageResources, renderPage} from "../../components/renderPage"
|
||||
import {FullPageLayout} from "../../cfg"
|
||||
import {Argv} from "../../util/ctx"
|
||||
import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path"
|
||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import {
|
||||
FilePath,
|
||||
isRelativeURL,
|
||||
joinSegments,
|
||||
pathToRoot,
|
||||
} from "../../util/path"
|
||||
import {
|
||||
defaultContentPageLayout,
|
||||
sharedPageComponents,
|
||||
} from "../../../quartz.layout"
|
||||
import {Content} from "../../components"
|
||||
import chalk from "chalk"
|
||||
import {write} from "./helpers"
|
||||
@ -25,7 +33,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
|
||||
let ref: string | null = null
|
||||
|
||||
if (
|
||||
["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) &&
|
||||
["script", "img", "audio", "video", "source", "iframe"].includes(
|
||||
elem.tagName,
|
||||
) &&
|
||||
elem?.properties?.src
|
||||
) {
|
||||
ref = elem.properties.src.toString()
|
||||
@ -40,7 +50,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
|
||||
return
|
||||
}
|
||||
|
||||
let fp = path.join(file.data.filePath!, path.relative(argv.directory, ref)).replace(/\\/g, "/")
|
||||
let fp = path
|
||||
.join(file.data.filePath!, path.relative(argv.directory, ref))
|
||||
.replace(/\\/g, "/")
|
||||
// markdown files have the .md extension stripped in hrefs, add it back here
|
||||
if (!fp.split("/").pop()?.includes(".")) {
|
||||
fp += ".md"
|
||||
@ -51,7 +63,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
|
||||
return dependencies
|
||||
}
|
||||
|
||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts: FullPageLayout = {
|
||||
...sharedPageComponents,
|
||||
...defaultContentPageLayout,
|
||||
@ -59,7 +73,16 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
||||
...userOpts,
|
||||
}
|
||||
|
||||
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||
const {
|
||||
head: Head,
|
||||
header,
|
||||
beforeBody,
|
||||
pageBody,
|
||||
afterBody,
|
||||
left,
|
||||
right,
|
||||
footer: Footer,
|
||||
} = opts
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
@ -85,7 +108,10 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
||||
for (const [tree, file] of content) {
|
||||
const sourcePath = file.data.filePath!
|
||||
const slug = file.data.slug!
|
||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
|
||||
graph.addEdge(
|
||||
sourcePath,
|
||||
joinSegments(ctx.argv.output, slug + ".html") as FilePath,
|
||||
)
|
||||
|
||||
parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => {
|
||||
graph.addEdge(dep as FilePath, sourcePath)
|
||||
@ -117,7 +143,13 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const content = renderPage(
|
||||
cfg,
|
||||
slug,
|
||||
componentData,
|
||||
opts,
|
||||
externalResources,
|
||||
)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
||||
@ -3,7 +3,11 @@ import { QuartzComponentProps } from "../../components/types"
|
||||
import HeaderConstructor from "../../components/Header"
|
||||
import BodyConstructor from "../../components/Body"
|
||||
import {pageResources, renderPage} from "../../components/renderPage"
|
||||
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
|
||||
import {
|
||||
ProcessedContent,
|
||||
QuartzPluginData,
|
||||
defaultProcessedContent,
|
||||
} from "../vfile"
|
||||
import {FullPageLayout} from "../../cfg"
|
||||
import path from "path"
|
||||
import {
|
||||
@ -15,7 +19,10 @@ import {
|
||||
pathToRoot,
|
||||
simplifySlug,
|
||||
} from "../../util/path"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import {
|
||||
defaultListPageLayout,
|
||||
sharedPageComponents,
|
||||
} from "../../../quartz.layout"
|
||||
import {FolderContent} from "../../components"
|
||||
import {write} from "./helpers"
|
||||
import {i18n} from "../../i18n"
|
||||
@ -25,7 +32,9 @@ interface FolderPageOptions extends FullPageLayout {
|
||||
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||
}
|
||||
|
||||
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (userOpts) => {
|
||||
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts: FullPageLayout = {
|
||||
...sharedPageComponents,
|
||||
...defaultListPageLayout,
|
||||
@ -33,7 +42,16 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
||||
...userOpts,
|
||||
}
|
||||
|
||||
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||
const {
|
||||
head: Head,
|
||||
header,
|
||||
beforeBody,
|
||||
pageBody,
|
||||
afterBody,
|
||||
left,
|
||||
right,
|
||||
footer: Footer,
|
||||
} = opts
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
@ -63,7 +81,10 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
||||
const slug = vfile.data.slug
|
||||
const folderName = path.dirname(slug ?? "") as SimpleSlug
|
||||
if (slug && folderName !== "." && folderName !== "tags") {
|
||||
graph.addEdge(vfile.data.filePath!, joinSegments(folderName, "index.html") as FilePath)
|
||||
graph.addEdge(
|
||||
vfile.data.filePath!,
|
||||
joinSegments(folderName, "index.html") as FilePath,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@ -85,7 +106,8 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
||||
}),
|
||||
)
|
||||
|
||||
const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||
const folderDescriptions: Record<string, ProcessedContent> =
|
||||
Object.fromEntries(
|
||||
[...folders].map((folder) => [
|
||||
folder,
|
||||
defaultProcessedContent({
|
||||
@ -119,7 +141,13 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const content = renderPage(
|
||||
cfg,
|
||||
slug,
|
||||
componentData,
|
||||
opts,
|
||||
externalResources,
|
||||
)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
||||
@ -10,7 +10,12 @@ type WriteOptions = {
|
||||
content: string | Buffer
|
||||
}
|
||||
|
||||
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
|
||||
export const write = async ({
|
||||
ctx,
|
||||
slug,
|
||||
ext,
|
||||
content,
|
||||
}: WriteOptions): Promise<FilePath> => {
|
||||
const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
|
||||
const dir = path.dirname(pathToPage)
|
||||
await fs.promises.mkdir(dir, {recursive: true})
|
||||
|
||||
@ -30,6 +30,8 @@ export const Static: QuartzEmitterPlugin = () => ({
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
})
|
||||
return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[]
|
||||
return fps.map((fp) =>
|
||||
joinSegments(argv.output, "static", fp),
|
||||
) as FilePath[]
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,7 +3,11 @@ import { QuartzComponentProps } from "../../components/types"
|
||||
import HeaderConstructor from "../../components/Header"
|
||||
import BodyConstructor from "../../components/Body"
|
||||
import {pageResources, renderPage} from "../../components/renderPage"
|
||||
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
|
||||
import {
|
||||
ProcessedContent,
|
||||
QuartzPluginData,
|
||||
defaultProcessedContent,
|
||||
} from "../vfile"
|
||||
import {FullPageLayout} from "../../cfg"
|
||||
import {
|
||||
FilePath,
|
||||
@ -12,7 +16,10 @@ import {
|
||||
joinSegments,
|
||||
pathToRoot,
|
||||
} from "../../util/path"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import {
|
||||
defaultListPageLayout,
|
||||
sharedPageComponents,
|
||||
} from "../../../quartz.layout"
|
||||
import {TagContent} from "../../components"
|
||||
import {write} from "./helpers"
|
||||
import {i18n} from "../../i18n"
|
||||
@ -22,7 +29,9 @@ interface TagPageOptions extends FullPageLayout {
|
||||
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||
}
|
||||
|
||||
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts) => {
|
||||
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts: FullPageLayout = {
|
||||
...sharedPageComponents,
|
||||
...defaultListPageLayout,
|
||||
@ -30,7 +39,16 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
||||
...userOpts,
|
||||
}
|
||||
|
||||
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||
const {
|
||||
head: Head,
|
||||
header,
|
||||
beforeBody,
|
||||
pageBody,
|
||||
afterBody,
|
||||
left,
|
||||
right,
|
||||
footer: Footer,
|
||||
} = opts
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
@ -55,7 +73,9 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
||||
|
||||
for (const [_tree, file] of content) {
|
||||
const sourcePath = file.data.filePath!
|
||||
const tags = (file.data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
const tags = (file.data.frontmatter?.tags ?? []).flatMap(
|
||||
getAllSegmentPrefixes,
|
||||
)
|
||||
// if the file has at least one tag, it is used in the tag index page
|
||||
if (tags.length > 0) {
|
||||
tags.push("index")
|
||||
@ -77,13 +97,16 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
||||
const cfg = ctx.cfg.configuration
|
||||
|
||||
const tags: Set<string> = new Set(
|
||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||
allFiles
|
||||
.flatMap((data) => data.frontmatter?.tags ?? [])
|
||||
.flatMap(getAllSegmentPrefixes),
|
||||
)
|
||||
|
||||
// add base tag
|
||||
tags.add("index")
|
||||
|
||||
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||
const tagDescriptions: Record<string, ProcessedContent> =
|
||||
Object.fromEntries(
|
||||
[...tags].map((tag) => {
|
||||
const title =
|
||||
tag === "index"
|
||||
@ -123,7 +146,13 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const content = renderPage(
|
||||
cfg,
|
||||
slug,
|
||||
componentData,
|
||||
opts,
|
||||
externalResources,
|
||||
)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
||||
@ -9,7 +9,9 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
|
||||
}
|
||||
|
||||
for (const transformer of ctx.cfg.plugins.transformers) {
|
||||
const res = transformer.externalResources ? transformer.externalResources(ctx) : {}
|
||||
const res = transformer.externalResources
|
||||
? transformer.externalResources(ctx)
|
||||
: {}
|
||||
if (res?.js) {
|
||||
staticResources.js.push(...res.js)
|
||||
}
|
||||
|
||||
@ -17,7 +17,9 @@ const defaultOptions: Options = {
|
||||
csl: "apa",
|
||||
}
|
||||
|
||||
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "Citations",
|
||||
@ -39,7 +41,10 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
|
||||
plugins.push(() => {
|
||||
return (tree, _file) => {
|
||||
visit(tree, "element", (node, _index, _parent) => {
|
||||
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
|
||||
if (
|
||||
node.tagName === "a" &&
|
||||
node.properties?.href?.startsWith("#bib")
|
||||
) {
|
||||
node.properties["data-no-popover"] = true
|
||||
}
|
||||
})
|
||||
|
||||
@ -18,7 +18,9 @@ const urlRegex = new RegExp(
|
||||
"g",
|
||||
)
|
||||
|
||||
export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const Description: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "Description",
|
||||
@ -58,7 +60,9 @@ export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
while (currentDescriptionLength < len) {
|
||||
const sentence = sentences[sentenceIdx]
|
||||
if (!sentence) break
|
||||
const currentSentence = sentence.endsWith(".") ? sentence : sentence + "."
|
||||
const currentSentence = sentence.endsWith(".")
|
||||
? sentence
|
||||
: sentence + "."
|
||||
finalDesc.push(currentSentence)
|
||||
currentDescriptionLength += currentSentence.length
|
||||
sentenceIdx++
|
||||
|
||||
@ -36,11 +36,15 @@ function coerceToArray(input: string | string[]): string[] | undefined {
|
||||
|
||||
// remove all non-strings
|
||||
return input
|
||||
.filter((tag: unknown) => typeof tag === "string" || typeof tag === "number")
|
||||
.filter(
|
||||
(tag: unknown) => typeof tag === "string" || typeof tag === "number",
|
||||
)
|
||||
.map((tag: string | number) => tag.toString())
|
||||
}
|
||||
|
||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "FrontMatter",
|
||||
@ -60,15 +64,22 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
if (data.title != null && data.title.toString() !== "") {
|
||||
data.title = data.title.toString()
|
||||
} else {
|
||||
data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title
|
||||
data.title =
|
||||
file.stem ??
|
||||
i18n(cfg.configuration.locale).propertyDefaults.title
|
||||
}
|
||||
|
||||
const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
|
||||
if (tags) data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))]
|
||||
if (tags)
|
||||
data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))]
|
||||
|
||||
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
||||
const aliases = coerceToArray(
|
||||
coalesceAliases(data, ["aliases", "alias"]),
|
||||
)
|
||||
if (aliases) data.aliases = aliases
|
||||
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
||||
const cssclasses = coerceToArray(
|
||||
coalesceAliases(data, ["cssclasses", "cssclass"]),
|
||||
)
|
||||
if (cssclasses) data.cssclasses = cssclasses
|
||||
|
||||
// fill in frontmatter
|
||||
|
||||
@ -14,7 +14,9 @@ const defaultOptions: Options = {
|
||||
linkHeadings: true,
|
||||
}
|
||||
|
||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<
|
||||
Partial<Options>
|
||||
> = (userOpts) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "GitHubFlavoredMarkdown",
|
||||
@ -30,20 +32,20 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> =
|
||||
{
|
||||
behavior: "append",
|
||||
properties: {
|
||||
role: "anchor",
|
||||
ariaHidden: true,
|
||||
tabIndex: -1,
|
||||
"role": "anchor",
|
||||
"ariaHidden": true,
|
||||
"tabIndex": -1,
|
||||
"data-no-popover": true,
|
||||
},
|
||||
content: {
|
||||
type: "element",
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
"width": 18,
|
||||
"height": 18,
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
|
||||
@ -27,7 +27,9 @@ function coerceDate(fp: string, d: any): Date {
|
||||
}
|
||||
|
||||
type MaybeDate = undefined | string | number
|
||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "CreatedModifiedDate",
|
||||
@ -41,7 +43,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
||||
let published: MaybeDate = undefined
|
||||
|
||||
const fp = file.data.filePath!
|
||||
const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp)
|
||||
const fullFp = path.isAbsolute(fp)
|
||||
? fp
|
||||
: path.posix.join(file.cwd, fp)
|
||||
for (const source of opts.priority) {
|
||||
if (source === "filesystem") {
|
||||
const st = await fs.promises.stat(fullFp)
|
||||
@ -62,7 +66,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
|
||||
}
|
||||
|
||||
try {
|
||||
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
|
||||
modified ||= await repo.getFileLatestModifiedDateAsync(
|
||||
file.data.filePath!,
|
||||
)
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
|
||||
@ -32,7 +32,9 @@ const defaultOptions: Options = {
|
||||
externalLinkIcon: true,
|
||||
}
|
||||
|
||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "LinkProcessing",
|
||||
@ -66,8 +68,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
"aria-hidden": "true",
|
||||
class: "external-icon",
|
||||
viewBox: "0 0 512 512",
|
||||
"class": "external-icon",
|
||||
"viewBox": "0 0 512 512",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@ -98,7 +100,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
}
|
||||
|
||||
// don't process external links or intra-document anchors
|
||||
const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#"))
|
||||
const isInternal = !(
|
||||
isAbsoluteUrl(dest) || dest.startsWith("#")
|
||||
)
|
||||
if (isInternal) {
|
||||
dest = node.properties.href = transformLink(
|
||||
file.data.slug!,
|
||||
@ -108,7 +112,10 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
|
||||
// url.resolve is considered legacy
|
||||
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
||||
const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true))
|
||||
const url = new URL(
|
||||
dest,
|
||||
"https://base.com/" + stripSlashes(curSlug, true),
|
||||
)
|
||||
const canonicalDest = url.pathname
|
||||
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
||||
if (destCanonical.endsWith("/")) {
|
||||
@ -116,7 +123,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
}
|
||||
|
||||
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
|
||||
const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug
|
||||
const full = decodeURIComponent(
|
||||
stripSlashes(destCanonical, true),
|
||||
) as FullSlug
|
||||
const simple = simplifySlug(full)
|
||||
outgoing.add(simple)
|
||||
node.properties["data-slug"] = full
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import {QuartzTransformerPlugin} from "../types"
|
||||
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
|
||||
import {
|
||||
Root,
|
||||
Html,
|
||||
BlockContent,
|
||||
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 {
|
||||
ReplaceFunction,
|
||||
findAndReplace as mdastFindReplace,
|
||||
} from "mdast-util-find-and-replace"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import {SKIP, visit} from "unist-util-visit"
|
||||
import path from "path"
|
||||
@ -90,7 +100,8 @@ const arrowMapping: Record<string, string> = {
|
||||
}
|
||||
|
||||
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
|
||||
const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||
const normalizedCallout =
|
||||
calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||
// if callout is not recognized, make it a custom one
|
||||
return calloutMapping[normalizedCallout] ?? calloutName
|
||||
}
|
||||
@ -111,7 +122,9 @@ export const wikilinkRegex = new RegExp(
|
||||
// ^\|([^\n])+\|\n(\|) -> matches the header row
|
||||
// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator
|
||||
// (\|([^\n])+\|\n)+ -> matches the body rows
|
||||
export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm)
|
||||
export const tableRegex = new RegExp(
|
||||
/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm,
|
||||
)
|
||||
|
||||
// matches any wikilink, only used for escaping wikilinks inside tables
|
||||
export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g)
|
||||
@ -129,14 +142,19 @@ const tagRegex = new RegExp(
|
||||
/(?:^| )#((?:[-_\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=)([^#\&\?]*).*/
|
||||
const ytLinkRegex =
|
||||
/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
||||
const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/
|
||||
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
|
||||
const videoExtensionRegex = new RegExp(
|
||||
/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/,
|
||||
)
|
||||
const wikilinkImageEmbedRegex = new RegExp(
|
||||
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
|
||||
)
|
||||
|
||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<
|
||||
Partial<Options>
|
||||
> = (userOpts) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
|
||||
const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
|
||||
@ -194,7 +212,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
|
||||
const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : ""
|
||||
const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : ""
|
||||
const displayAnchor = anchor
|
||||
? `#${blockRef}${anchor.trim().replace(/^#+/, "")}`
|
||||
: ""
|
||||
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
||||
const embedDisplay = value.startsWith("!") ? "!" : ""
|
||||
|
||||
@ -230,7 +250,17 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
if (value.startsWith("!")) {
|
||||
const ext: string = path.extname(fp).toLowerCase()
|
||||
const url = slugifyFilePath(fp as FilePath)
|
||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
|
||||
if (
|
||||
[
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".svg",
|
||||
".webp",
|
||||
].includes(ext)
|
||||
) {
|
||||
const match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
||||
const alt = match?.groups?.alt ?? ""
|
||||
const width = match?.groups?.width ?? "auto"
|
||||
@ -246,13 +276,23 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
|
||||
} else if (
|
||||
[".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)
|
||||
) {
|
||||
return {
|
||||
type: "html",
|
||||
value: `<video src="${url}" controls></video>`,
|
||||
}
|
||||
} else if (
|
||||
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
|
||||
[
|
||||
".mp3",
|
||||
".webm",
|
||||
".wav",
|
||||
".m4a",
|
||||
".ogg",
|
||||
".3gp",
|
||||
".flac",
|
||||
].includes(ext)
|
||||
) {
|
||||
return {
|
||||
type: "html",
|
||||
@ -360,18 +400,24 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
if (typeof replace === "string") {
|
||||
node.value = node.value.replace(regex, replace)
|
||||
} else {
|
||||
node.value = node.value.replace(regex, (substring: string, ...args) => {
|
||||
node.value = node.value.replace(
|
||||
regex,
|
||||
(substring: string, ...args) => {
|
||||
const replaceValue = replace(substring, ...args)
|
||||
if (typeof replaceValue === "string") {
|
||||
return replaceValue
|
||||
} else if (Array.isArray(replaceValue)) {
|
||||
return replaceValue.map(mdastToHtml).join("")
|
||||
} else if (typeof replaceValue === "object" && replaceValue !== null) {
|
||||
} else if (
|
||||
typeof replaceValue === "object" &&
|
||||
replaceValue !== null
|
||||
) {
|
||||
return mdastToHtml(replaceValue)
|
||||
} else {
|
||||
return substring
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -384,7 +430,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
plugins.push(() => {
|
||||
return (tree: Root, _file) => {
|
||||
visit(tree, "image", (node, index, parent) => {
|
||||
if (parent && index != undefined && videoExtensionRegex.test(node.url)) {
|
||||
if (
|
||||
parent &&
|
||||
index != undefined &&
|
||||
videoExtensionRegex.test(node.url)
|
||||
) {
|
||||
const newNode: Html = {
|
||||
type: "html",
|
||||
value: `<video controls src="${node.url}"></video>`,
|
||||
@ -408,7 +458,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
// find first line and callout content
|
||||
const [firstChild, ...calloutContent] = node.children
|
||||
if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") {
|
||||
if (
|
||||
firstChild.type !== "paragraph" ||
|
||||
firstChild.children[0]?.type !== "text"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -419,18 +472,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
const match = firstLine.match(calloutRegex)
|
||||
if (match && match.input) {
|
||||
const [calloutDirective, typeString, calloutMetaData, collapseChar] = match
|
||||
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
||||
const [
|
||||
calloutDirective,
|
||||
typeString,
|
||||
calloutMetaData,
|
||||
collapseChar,
|
||||
] = match
|
||||
const calloutType = canonicalizeCallout(
|
||||
typeString.toLowerCase(),
|
||||
)
|
||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||
const titleContent = match.input.slice(calloutDirective.length).trim()
|
||||
const useDefaultTitle = titleContent === "" && restOfTitle.length === 0
|
||||
const defaultState =
|
||||
collapseChar === "-" ? "collapsed" : "expanded"
|
||||
const titleContent = match.input
|
||||
.slice(calloutDirective.length)
|
||||
.trim()
|
||||
const useDefaultTitle =
|
||||
titleContent === "" && restOfTitle.length === 0
|
||||
const titleNode: Paragraph = {
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: useDefaultTitle ? capitalize(typeString) : titleContent + " ",
|
||||
value: useDefaultTitle
|
||||
? capitalize(typeString)
|
||||
: titleContent + " ",
|
||||
},
|
||||
...restOfTitle,
|
||||
],
|
||||
@ -450,7 +516,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
</div>`,
|
||||
}
|
||||
|
||||
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleHtml]
|
||||
const blockquoteContent: (BlockContent | DefinitionContent)[] =
|
||||
[titleHtml]
|
||||
if (remainingText.length > 0) {
|
||||
blockquoteContent.push({
|
||||
type: "paragraph",
|
||||
@ -478,7 +545,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
node.data = {
|
||||
hProperties: {
|
||||
...(node.data?.hProperties ?? {}),
|
||||
className: classNames.join(" "),
|
||||
"className": classNames.join(" "),
|
||||
"data-callout": calloutType,
|
||||
"data-callout-fold": collapse,
|
||||
"data-callout-metadata": calloutMetaData,
|
||||
@ -606,10 +673,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
plugins.push(() => {
|
||||
return (tree: HtmlRoot) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName === "img" && typeof node.properties.src === "string") {
|
||||
if (
|
||||
node.tagName === "img" &&
|
||||
typeof node.properties.src === "string"
|
||||
) {
|
||||
const match = node.properties.src.match(ytLinkRegex)
|
||||
const videoId = match && match[2].length == 11 ? match[2] : null
|
||||
const playlistId = node.properties.src.match(ytPlaylistLinkRegex)?.[1]
|
||||
const playlistId =
|
||||
node.properties.src.match(ytPlaylistLinkRegex)?.[1]
|
||||
if (videoId) {
|
||||
// YouTube video (with optional playlist)
|
||||
node.tagName = "iframe"
|
||||
@ -643,7 +714,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
plugins.push(() => {
|
||||
return (tree: HtmlRoot, _file) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName === "input" && node.properties.type === "checkbox") {
|
||||
if (
|
||||
node.tagName === "input" &&
|
||||
node.properties.type === "checkbox"
|
||||
) {
|
||||
const isChecked = node.properties?.checked ?? false
|
||||
node.properties = {
|
||||
type: "checkbox",
|
||||
|
||||
@ -22,7 +22,10 @@ const defaultOptions: Options = {
|
||||
replaceOrgLatex: true,
|
||||
}
|
||||
|
||||
const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g")
|
||||
const relrefRegex = new RegExp(
|
||||
/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/,
|
||||
"g",
|
||||
)
|
||||
const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g")
|
||||
const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g")
|
||||
const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g")
|
||||
@ -47,7 +50,9 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
|
||||
* markdown to make it compatible with quartz but the list of changes applied it
|
||||
* is not exhaustive.
|
||||
* */
|
||||
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<
|
||||
Partial<Options>
|
||||
> = (userOpts) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "OxHugoFlavouredMarkdown",
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import {QuartzTransformerPlugin} from "../types"
|
||||
import rehypePrettyCode, { Options as CodeOptions, Theme as CodeTheme } from "rehype-pretty-code"
|
||||
import rehypePrettyCode, {
|
||||
Options as CodeOptions,
|
||||
Theme as CodeTheme,
|
||||
} from "rehype-pretty-code"
|
||||
|
||||
interface Theme extends Record<string, CodeTheme> {
|
||||
light: CodeTheme
|
||||
@ -19,7 +22,9 @@ const defaultOptions: Options = {
|
||||
keepBackground: false,
|
||||
}
|
||||
|
||||
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts: CodeOptions = {...defaultOptions, ...userOpts}
|
||||
|
||||
return {
|
||||
|
||||
@ -25,7 +25,9 @@ interface TocEntry {
|
||||
}
|
||||
|
||||
const slugAnchor = new Slugger()
|
||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = {...defaultOptions, ...userOpts}
|
||||
return {
|
||||
name: "TableOfContents",
|
||||
@ -33,7 +35,8 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userO
|
||||
return [
|
||||
() => {
|
||||
return async (tree: Root, file) => {
|
||||
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
|
||||
const display =
|
||||
file.data.frontmatter?.enableToc ?? opts.showByDefault
|
||||
if (display) {
|
||||
slugAnchor.reset()
|
||||
const toc: TocEntry[] = []
|
||||
|
||||
@ -37,7 +37,11 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
||||
) => QuartzEmitterPluginInstance
|
||||
export type QuartzEmitterPluginInstance = {
|
||||
name: string
|
||||
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
||||
emit(
|
||||
ctx: BuildCtx,
|
||||
content: ProcessedContent[],
|
||||
resources: StaticResources,
|
||||
): Promise<FilePath[]>
|
||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||
getDependencyGraph?(
|
||||
ctx: BuildCtx,
|
||||
|
||||
@ -4,7 +4,9 @@ import { Data, VFile } from "vfile"
|
||||
export type QuartzPluginData = Data
|
||||
export type ProcessedContent = [Node, VFile]
|
||||
|
||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
||||
export function defaultProcessedContent(
|
||||
vfileData: Partial<QuartzPluginData>,
|
||||
): ProcessedContent {
|
||||
const root: Parent = {type: "root", children: []}
|
||||
const vfile = new VFile("")
|
||||
vfile.data = vfileData
|
||||
|
||||
@ -158,7 +158,9 @@ export async function parseMarkdown(
|
||||
|
||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||
childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
|
||||
childPromises.push(
|
||||
pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]),
|
||||
)
|
||||
}
|
||||
|
||||
const results: ProcessedContent[][] = await WorkerPromise.all(
|
||||
|
||||
@ -180,15 +180,33 @@ describe("link strategies", () => {
|
||||
test("from a/b/c", () => {
|
||||
const cur = "a/b/c" as FullSlug
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/index", opts),
|
||||
"../../a/b/",
|
||||
)
|
||||
assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../e/f")
|
||||
assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../e/g/h")
|
||||
assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
|
||||
assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
|
||||
assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
|
||||
assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../tag/test")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../a/b/c#test")
|
||||
assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../a/test.png")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "index.png", opts),
|
||||
"../../index.png",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "index#abc", opts),
|
||||
"../../#abc",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "tag/test", opts),
|
||||
"../../tag/test",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/c#test", opts),
|
||||
"../../a/b/c#test",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/test.png", opts),
|
||||
"../../a/test.png",
|
||||
)
|
||||
})
|
||||
|
||||
test("from a/b/index", () => {
|
||||
@ -216,20 +234,41 @@ describe("link strategies", () => {
|
||||
const cur = "a/b/c" as FullSlug
|
||||
assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
|
||||
assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../a/b/index.png")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../a/b/#abc")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/index", opts),
|
||||
"../../a/b/",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/index.png", opts),
|
||||
"../../a/b/index.png",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/index#abc", opts),
|
||||
"../../a/b/#abc",
|
||||
)
|
||||
assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
|
||||
assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
|
||||
assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../a/test.png")
|
||||
assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "index.png", opts),
|
||||
"../../index.png",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "test.png", opts),
|
||||
"../../a/test.png",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "index#abc", opts),
|
||||
"../../#abc",
|
||||
)
|
||||
})
|
||||
|
||||
test("from a/b/index", () => {
|
||||
const cur = "a/b/index" as FullSlug
|
||||
assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
|
||||
assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
|
||||
assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "a/b/index", opts),
|
||||
"../../a/b/",
|
||||
)
|
||||
assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
|
||||
})
|
||||
|
||||
@ -252,24 +291,48 @@ describe("link strategies", () => {
|
||||
const cur = "a/b/c" as FullSlug
|
||||
assert.strictEqual(path.transformLink(cur, "d", opts), "./d")
|
||||
assert.strictEqual(path.transformLink(cur, "index", opts), "./")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../index.png", opts), "../../../index.png")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../index#abc", opts), "../../../#abc")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../", opts), "../../../")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../index", opts),
|
||||
"../../../",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../index.png", opts),
|
||||
"../../../index.png",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../index#abc", opts),
|
||||
"../../../#abc",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../", opts),
|
||||
"../../../",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../a/test.png", opts),
|
||||
"../../../a/test.png",
|
||||
)
|
||||
assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h")
|
||||
assert.strictEqual(path.transformLink(cur, "../../../e/g/h#abc", opts), "../../../e/g/h#abc")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../e/g/h", opts),
|
||||
"../../../e/g/h",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../e/g/h", opts),
|
||||
"../../../e/g/h",
|
||||
)
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../../e/g/h#abc", opts),
|
||||
"../../../e/g/h#abc",
|
||||
)
|
||||
})
|
||||
|
||||
test("from a/b/index", () => {
|
||||
const cur = "a/b/index" as FullSlug
|
||||
assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../")
|
||||
assert.strictEqual(path.transformLink(cur, "../../", opts), "../../")
|
||||
assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h")
|
||||
assert.strictEqual(
|
||||
path.transformLink(cur, "../../e/g/h", opts),
|
||||
"../../e/g/h",
|
||||
)
|
||||
assert.strictEqual(path.transformLink(cur, "c", opts), "./c")
|
||||
})
|
||||
|
||||
|
||||
@ -31,7 +31,12 @@ export type SimpleSlug = SlugLike<"simple">
|
||||
export function isSimpleSlug(s: string): s is SimpleSlug {
|
||||
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
|
||||
const validEnding = !endsWith(s, "index")
|
||||
return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
||||
return (
|
||||
validStart &&
|
||||
!containsForbiddenCharacters(s) &&
|
||||
validEnding &&
|
||||
!_hasFileExtension(s)
|
||||
)
|
||||
}
|
||||
|
||||
/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
|
||||
@ -39,7 +44,11 @@ export type RelativeURL = SlugLike<"relative">
|
||||
export function isRelativeURL(s: string): s is RelativeURL {
|
||||
const validStart = /^\.{1,2}/.test(s)
|
||||
const validEnding = !endsWith(s, "index")
|
||||
return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
|
||||
return (
|
||||
validStart &&
|
||||
validEnding &&
|
||||
![".md", ".html"].includes(_getFileExtension(s) ?? "")
|
||||
)
|
||||
}
|
||||
|
||||
export function getFullSlug(window: Window): FullSlug {
|
||||
@ -91,7 +100,9 @@ export function transformInternalLink(link: string): RelativeURL {
|
||||
const folderPath = isFolderPath(fplike)
|
||||
let segments = fplike.split("/").filter((x) => x.length > 0)
|
||||
let prefix = segments.filter(isRelativeSegment).join("/")
|
||||
let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/")
|
||||
let fp = segments
|
||||
.filter((seg) => !isRelativeSegment(seg) && seg !== "")
|
||||
.join("/")
|
||||
|
||||
// manually add ext here as we want to not strip 'index' if it has an extension
|
||||
const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
|
||||
@ -103,11 +114,18 @@ export function transformInternalLink(link: string): RelativeURL {
|
||||
|
||||
// from micromorph/src/utils.ts
|
||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||
const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => {
|
||||
const _rebaseHtmlElement = (
|
||||
el: Element,
|
||||
attr: string,
|
||||
newBase: string | URL,
|
||||
) => {
|
||||
const rebased = new URL(el.getAttribute(attr)!, newBase)
|
||||
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
||||
}
|
||||
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
||||
export function normalizeRelativeURLs(
|
||||
el: Element | Document,
|
||||
destination: string | URL,
|
||||
) {
|
||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
||||
_rebaseHtmlElement(item, "href", destination),
|
||||
)
|
||||
@ -127,12 +145,20 @@ const _rebaseHastElement = (
|
||||
return
|
||||
}
|
||||
|
||||
const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string)
|
||||
const rel = joinSegments(
|
||||
resolveRelative(curBase, newBase),
|
||||
"..",
|
||||
el.properties[attr] as string,
|
||||
)
|
||||
el.properties[attr] = rel
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
||||
export function normalizeHastElement(
|
||||
rawEl: HastElement,
|
||||
curBase: FullSlug,
|
||||
newBase: FullSlug,
|
||||
) {
|
||||
const el = clone(rawEl) // clone so we dont modify the original page
|
||||
_rebaseHastElement(el, "src", curBase, newBase)
|
||||
_rebaseHastElement(el, "href", curBase, newBase)
|
||||
@ -161,8 +187,14 @@ export function pathToRoot(slug: FullSlug): RelativeURL {
|
||||
return rootPath as RelativeURL
|
||||
}
|
||||
|
||||
export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
|
||||
const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL
|
||||
export function resolveRelative(
|
||||
current: FullSlug,
|
||||
target: FullSlug | SimpleSlug,
|
||||
): RelativeURL {
|
||||
const res = joinSegments(
|
||||
pathToRoot(current),
|
||||
simplifySlug(target as FullSlug),
|
||||
) as RelativeURL
|
||||
return res
|
||||
}
|
||||
|
||||
@ -203,7 +235,11 @@ export interface TransformOptions {
|
||||
allSlugs: FullSlug[]
|
||||
}
|
||||
|
||||
export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
|
||||
export function transformLink(
|
||||
src: FullSlug,
|
||||
target: string,
|
||||
opts: TransformOptions,
|
||||
): RelativeURL {
|
||||
let targetSlug = transformInternalLink(target)
|
||||
|
||||
if (opts.strategy === "relative") {
|
||||
@ -229,7 +265,8 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
|
||||
}
|
||||
|
||||
// if it's not unique, then it's the absolute path from the vault root
|
||||
return (joinSegments(pathToRoot(src), canonicalSlug) + folderTail) as RelativeURL
|
||||
return (joinSegments(pathToRoot(src), canonicalSlug) +
|
||||
folderTail) as RelativeURL
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +292,9 @@ function trimSuffix(s: string, suffix: string): string {
|
||||
}
|
||||
|
||||
function containsForbiddenCharacters(s: string): boolean {
|
||||
return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
|
||||
return (
|
||||
s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
|
||||
)
|
||||
}
|
||||
|
||||
function _hasFileExtension(s: string): boolean {
|
||||
|
||||
@ -16,12 +16,20 @@ export type JSResource = {
|
||||
}
|
||||
)
|
||||
|
||||
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
||||
export function JSResourceToScriptElement(
|
||||
resource: JSResource,
|
||||
preserve?: boolean,
|
||||
): JSX.Element {
|
||||
const scriptType = resource.moduleType ?? "application/javascript"
|
||||
const spaPreserve = preserve ?? resource.spaPreserve
|
||||
if (resource.contentType === "external") {
|
||||
return (
|
||||
<script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
|
||||
<script
|
||||
key={resource.src}
|
||||
src={resource.src}
|
||||
type={scriptType}
|
||||
spa-preserve={spaPreserve}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
const content = resource.script
|
||||
@ -30,8 +38,7 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole
|
||||
key={randomUUID()}
|
||||
type={scriptType}
|
||||
spa-preserve={spaPreserve}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
></script>
|
||||
dangerouslySetInnerHTML={{__html: content}}></script>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user