This commit is contained in:
semanticdata 2024-08-14 16:29:22 -05:00
parent 388ab1f228
commit 3dc4a40d91
110 changed files with 1869 additions and 947 deletions

View File

@ -1,24 +1,24 @@
import sourceMapSupport from "source-map-support" import sourceMapSupport from "source-map-support"
sourceMapSupport.install(options) sourceMapSupport.install(options)
import path from "path" import path from "path"
import { PerfTimer } from "./util/perf" import {PerfTimer} from "./util/perf"
import { rimraf } from "rimraf" import {rimraf} from "rimraf"
import { GlobbyFilterFunction, isGitIgnored } from "globby" import {GlobbyFilterFunction, isGitIgnored} from "globby"
import chalk from "chalk" import chalk from "chalk"
import { parseMarkdown } from "./processors/parse" import {parseMarkdown} from "./processors/parse"
import { filterContent } from "./processors/filter" import {filterContent} from "./processors/filter"
import { emitContent } from "./processors/emit" import {emitContent} from "./processors/emit"
import cfg from "../quartz.config" import cfg from "../quartz.config"
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path" import {FilePath, FullSlug, joinSegments, slugifyFilePath} from "./util/path"
import chokidar from "chokidar" import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile" import {ProcessedContent} from "./plugins/vfile"
import { Argv, BuildCtx } from "./util/ctx" import {Argv, BuildCtx} from "./util/ctx"
import { glob, toPosixPath } from "./util/glob" import {glob, toPosixPath} from "./util/glob"
import { trace } from "./util/trace" import {trace} from "./util/trace"
import { options } from "./util/sourcemap" import {options} from "./util/sourcemap"
import { Mutex } from "async-mutex" import {Mutex} from "async-mutex"
import DepGraph from "./depgraph" import DepGraph from "./depgraph"
import { getStaticResourcesFromPlugins } from "./plugins" import {getStaticResourcesFromPlugins} from "./plugins"
type Dependencies = Record<string, DepGraph<FilePath> | null> type Dependencies = Record<string, DepGraph<FilePath> | null>
@ -65,17 +65,25 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const release = await mut.acquire() const release = await mut.acquire()
perf.addEvent("clean") perf.addEvent("clean")
await rimraf(path.join(output, "*"), { glob: true }) 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") 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() const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort()
console.log( console.log(
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, `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)) ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
const parsedFiles = await parseMarkdown(ctx, filePaths) const parsedFiles = await parseMarkdown(ctx, filePaths)
@ -88,12 +96,18 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const staticResources = getStaticResourcesFromPlugins(ctx) const staticResources = getStaticResourcesFromPlugins(ctx)
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
dependencies[emitter.name] = dependencies[emitter.name] =
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null (await emitter.getDependencyGraph?.(
ctx,
filteredContent,
staticResources,
)) ?? null
} }
} }
await emitContent(ctx, filteredContent) 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() release()
if (argv.serve) { if (argv.serve) {
@ -109,7 +123,7 @@ async function startServing(
clientRefresh: () => void, clientRefresh: () => void,
dependencies: Dependencies, // emitter name: dep graph dependencies: Dependencies, // emitter name: dep graph
) { ) {
const { argv } = ctx const {argv} = ctx
// cache file parse results // cache file parse results
const contentMap = new Map<FilePath, ProcessedContent>() const contentMap = new Map<FilePath, ProcessedContent>()
@ -137,11 +151,17 @@ async function startServing(
ignoreInitial: true, ignoreInitial: true,
}) })
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint const buildFromEntry = argv.fastRebuild
? partialRebuildFromEntrypoint
: rebuildFromEntrypoint
watcher watcher
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData)) .on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData)) .on("change", (fp) =>
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData)) buildFromEntry(fp, "change", clientRefresh, buildData),
)
.on("unlink", (fp) =>
buildFromEntry(fp, "delete", clientRefresh, buildData),
)
return async () => { return async () => {
await watcher.close() await watcher.close()
@ -154,8 +174,8 @@ async function partialRebuildFromEntrypoint(
clientRefresh: () => void, clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData buildData: BuildData, // note: this function mutates buildData
) { ) {
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData const {ctx, ignored, dependencies, contentMap, mut, toRemove} = buildData
const { argv, cfg } = ctx const {argv, cfg} = ctx
// don't do anything for gitignored files // don't do anything for gitignored files
if (ignored(filepath)) { if (ignored(filepath)) {
@ -184,12 +204,18 @@ async function partialRebuildFromEntrypoint(
case "add": case "add":
// add to cache when new file is added // add to cache when new file is added
processedFiles = await parseMarkdown(ctx, [fp]) 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 // update the dep graph by asking all emitters whether they depend on this file
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
const emitterGraph = const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null (await emitter.getDependencyGraph?.(
ctx,
processedFiles,
staticResources,
)) ?? null
if (emitterGraph) { if (emitterGraph) {
const existingGraph = dependencies[emitter.name] const existingGraph = dependencies[emitter.name]
@ -205,20 +231,29 @@ async function partialRebuildFromEntrypoint(
case "change": case "change":
// invalidate cache when file is changed // invalidate cache when file is changed
processedFiles = await parseMarkdown(ctx, [fp]) 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 // only content files can have added/removed dependencies because of transclusions
if (path.extname(fp) === ".md") { if (path.extname(fp) === ".md") {
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
// get new dependencies from all emitters for this file // get new dependencies from all emitters for this file
const emitterGraph = 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 // only update the graph if the emitter plugin uses the changed file
// eg. Assets plugin ignores md files, so we skip updating the graph // eg. Assets plugin ignores md files, so we skip updating the graph
if (emitterGraph?.hasNode(fp)) { if (emitterGraph?.hasNode(fp)) {
// merge the new dependencies into the dep graph // 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)) .filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(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) { if (ctx.argv.verbose) {
for (const file of emittedFps) { 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 // CLEANUP
const destinationsToDelete = new Set<FilePath>() const destinationsToDelete = new Set<FilePath>()
@ -328,10 +369,18 @@ async function rebuildFromEntrypoint(
clientRefresh: () => void, clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData buildData: BuildData, // note: this function mutates buildData
) { ) {
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } = const {
buildData ctx,
ignored,
mut,
initialSlugs,
contentMap,
toRebuild,
toRemove,
trackedAssets,
} = buildData
const { argv } = ctx const {argv} = ctx
// don't do anything for gitignored files // don't do anything for gitignored files
if (ignored(fp)) { if (ignored(fp)) {
@ -387,19 +436,25 @@ async function rebuildFromEntrypoint(
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
// re-update slugs // re-update slugs
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] const trackedSlugs = [
...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets]),
]
.filter((fp) => !toRemove.has(fp)) .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])] ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
// TODO: we can probably traverse the link graph to figure out what's safe to delete here // TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything // instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true }) await rimraf(path.join(argv.output, ".*"), {glob: true})
await emitContent(ctx, filteredContent) await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
} catch (err) { } 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) { if (argv.verbose) {
console.log(chalk.red(err)) console.log(chalk.red(err))
} }

View File

@ -84,4 +84,7 @@ export interface FullPageLayout {
} }
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right"> 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"
>

View File

@ -1,7 +1,14 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { classNames } from "../util/lang" 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 const title = fileData.frontmatter?.title
if (title) { if (title) {
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1> return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>

View File

@ -1,8 +1,12 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import style from "./styles/backlinks.scss" import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path" import {resolveRelative, simplifySlug} from "../util/path"
import { i18n } from "../i18n" import {i18n} from "../i18n"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
const Backlinks: QuartzComponent = ({ const Backlinks: QuartzComponent = ({
fileData, fileData,
@ -19,7 +23,9 @@ const Backlinks: QuartzComponent = ({
{backlinkFiles.length > 0 ? ( {backlinkFiles.length > 0 ? (
backlinkFiles.map((f) => ( backlinkFiles.map((f) => (
<li> <li>
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal"> <a
href={resolveRelative(fileData.slug!, f.slug!)}
class="internal">
{f.frontmatter?.title} {f.frontmatter?.title}
</a> </a>
</li> </li>

View File

@ -1,9 +1,13 @@
// @ts-ignore // @ts-ignore
import clipboardScript from "./scripts/clipboard.inline" import clipboardScript from "./scripts/clipboard.inline"
import clipboardStyle from "./styles/clipboard.scss" import clipboardStyle from "./styles/clipboard.scss"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { const Body: QuartzComponent = ({children}: QuartzComponentProps) => {
return <div id="quartz-body">{children}</div> return <div id="quartz-body">{children}</div>
} }

View File

@ -1,8 +1,12 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss" import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" import {FullSlug, SimpleSlug, joinSegments, resolveRelative} from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
type CrumbData = { type CrumbData = {
displayName: string displayName: string
@ -40,7 +44,11 @@ const defaultOptions: BreadcrumbOptions = {
showCurrentPage: true, showCurrentPage: true,
} }
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { function formatCrumb(
displayName: string,
baseSlug: FullSlug,
currentSlug: SimpleSlug,
): CrumbData {
return { return {
displayName: displayName.replaceAll("-", " "), displayName: displayName.replaceAll("-", " "),
path: resolveRelative(baseSlug, currentSlug), path: resolveRelative(baseSlug, currentSlug),
@ -49,7 +57,7 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl
export default ((opts?: Partial<BreadcrumbOptions>) => { export default ((opts?: Partial<BreadcrumbOptions>) => {
// Merge options with defaults // Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts } const options: BreadcrumbOptions = {...defaultOptions, ...opts}
// computed index of folder name to its associated file data // computed index of folder name to its associated file data
let folderIndex: Map<string, QuartzPluginData> | undefined let folderIndex: Map<string, QuartzPluginData> | undefined
@ -65,7 +73,11 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
} }
// Format entry for root element // 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] const crumbs: CrumbData[] = [firstEntry]
if (!folderIndex && options.resolveFrontmatterTitle) { if (!folderIndex && options.resolveFrontmatterTitle) {
@ -92,7 +104,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
let curPathSegment = slugParts[i] let curPathSegment = slugParts[i]
// Try to resolve frontmatter folder title // 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) { if (currentFile) {
const title = currentFile.frontmatter!.title const title = currentFile.frontmatter!.title
if (title !== "index") { if (title !== "index") {
@ -123,11 +137,15 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
} }
return ( return (
<nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs"> <nav
class={classNames(displayClass, "breadcrumb-container")}
aria-label="breadcrumbs">
{crumbs.map((crumb, index) => ( {crumbs.map((crumb, index) => (
<div class="breadcrumb-element"> <div class="breadcrumb-element">
<a href={crumb.path}>{crumb.displayName}</a> <a href={crumb.path}>{crumb.displayName}</a>
{index !== crumbs.length - 1 && <p>{` ${options.spacerSymbol} `}</p>} {index !== crumbs.length - 1 && (
<p>{` ${options.spacerSymbol} `}</p>
)}
</div> </div>
))} ))}
</nav> </nav>

View File

@ -1,5 +1,9 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { classNames } from "../util/lang" QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import {classNames} from "../util/lang"
// @ts-ignore // @ts-ignore
import script from "./scripts/comments.inline" import script from "./scripts/comments.inline"
@ -22,7 +26,10 @@ function boolToStringBool(b: boolean): string {
} }
export default ((opts: Options) => { export default ((opts: Options) => {
const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Comments: QuartzComponent = ({
displayClass,
cfg,
}: QuartzComponentProps) => {
return ( return (
<div <div
class={classNames(displayClass, "giscus")} class={classNames(displayClass, "giscus")}
@ -32,9 +39,10 @@ export default ((opts: Options) => {
data-category-id={opts.options.categoryId} data-category-id={opts.options.categoryId}
data-mapping={opts.options.mapping ?? "url"} data-mapping={opts.options.mapping ?? "url"}
data-strict={boolToStringBool(opts.options.strict ?? true)} data-strict={boolToStringBool(opts.options.strict ?? true)}
data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)} data-reactions-enabled={boolToStringBool(
data-input-position={opts.options.inputPosition ?? "bottom"} opts.options.reactionsEnabled ?? true,
></div> )}
data-input-position={opts.options.inputPosition ?? "bottom"}></div>
) )
} }

View File

@ -1,9 +1,9 @@
import { formatDate, getDate } from "./Date" import {formatDate, getDate} from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import {QuartzComponentConstructor, QuartzComponentProps} from "./types"
import readingTime from "reading-time" import readingTime from "reading-time"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
import { i18n } from "../i18n" import {i18n} from "../i18n"
import { JSX } from "preact" import {JSX} from "preact"
import style from "./styles/contentMeta.scss" import style from "./styles/contentMeta.scss"
interface ContentMetaOptions { interface ContentMetaOptions {
@ -21,9 +21,13 @@ const defaultOptions: ContentMetaOptions = {
export default ((opts?: Partial<ContentMetaOptions>) => { export default ((opts?: Partial<ContentMetaOptions>) => {
// Merge options with defaults // Merge options with defaults
const options: ContentMetaOptions = { ...defaultOptions, ...opts } const options: ContentMetaOptions = {...defaultOptions, ...opts}
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) { function ContentMetadata({
cfg,
fileData,
displayClass,
}: QuartzComponentProps) {
const text = fileData.text const text = fileData.text
if (text) { if (text) {
@ -35,8 +39,10 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
// Display reading time if enabled // Display reading time if enabled
if (options.showReadingTime) { if (options.showReadingTime) {
const { minutes, words: _words } = readingTime(text) 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), minutes: Math.ceil(minutes),
}) })
segments.push(displayedTime) segments.push(displayedTime)
@ -45,7 +51,9 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
const segmentsElements = segments.map((segment) => <span>{segment}</span>) const segmentsElements = segments.map((segment) => <span>{segment}</span>)
return ( return (
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}> <p
show-comma={options.showComma}
class={classNames(displayClass, "content-meta")}>
{segmentsElements} {segmentsElements}
</p> </p>
) )

View File

@ -3,14 +3,26 @@
// see: https://v8.dev/features/modules#defer // see: https://v8.dev/features/modules#defer
import darkmodeScript from "./scripts/darkmode.inline" import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss" import styles from "./styles/darkmode.scss"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { i18n } from "../i18n" QuartzComponent,
import { classNames } from "../util/lang" 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 ( return (
<div class={classNames(displayClass, "darkmode")}> <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}> <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -21,8 +33,7 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
y="0px" y="0px"
viewBox="0 0 35 35" viewBox="0 0 35 35"
style="enable-background:new 0 0 35 35" style="enable-background:new 0 0 35 35"
xmlSpace="preserve" xmlSpace="preserve">
>
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title> <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> <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> </svg>
@ -37,8 +48,7 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
y="0px" y="0px"
viewBox="0 0 100 100" viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100" style="enable-background:new 0 0 100 100"
xmlSpace="preserve" xmlSpace="preserve">
>
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title> <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> <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> </svg>

View File

@ -1,6 +1,6 @@
import { GlobalConfiguration } from "../cfg" import {GlobalConfiguration} from "../cfg"
import { ValidLocale } from "../i18n" import {ValidLocale} from "../i18n"
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
interface Props { interface Props {
date: Date date: Date
@ -9,7 +9,10 @@ interface Props {
export type ValidDateType = keyof Required<QuartzPluginData>["dates"] 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) { if (!cfg.defaultDateType) {
throw new Error( 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.`, `Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`,
@ -26,6 +29,6 @@ export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
}) })
} }
export function Date({ date, locale }: Props) { export function Date({date, locale}: Props) {
return <>{formatDate(date, locale)}</> return <>{formatDate(date, locale)}</>
} }

View File

@ -1,4 +1,8 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
export default ((component?: QuartzComponent) => { export default ((component?: QuartzComponent) => {
if (component) { if (component) {

View File

@ -1,12 +1,16 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import explorerStyle from "./styles/explorer.scss" import explorerStyle from "./styles/explorer.scss"
// @ts-ignore // @ts-ignore
import script from "./scripts/explorer.inline" import script from "./scripts/explorer.inline"
import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import {ExplorerNode, FileNode, Options} from "./ExplorerNode"
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
import { i18n } from "../i18n" import {i18n} from "../i18n"
// Options interface defined in `ExplorerNode` to avoid circular dependency // Options interface defined in `ExplorerNode` to avoid circular dependency
const defaultOptions = { const defaultOptions = {
@ -39,7 +43,7 @@ const defaultOptions = {
export default ((userOpts?: Partial<Options>) => { export default ((userOpts?: Partial<Options>) => {
// Parse config // Parse config
const opts: Options = { ...defaultOptions, ...userOpts } const opts: Options = {...defaultOptions, ...userOpts}
// memoized // memoized
let fileTree: FileNode let fileTree: FileNode
@ -68,7 +72,9 @@ export default ((userOpts?: Partial<Options>) => {
// Get all folders of tree. Initialize with collapsed state // Get all folders of tree. Initialize with collapsed state
// Stringify to pass json tree as data attribute ([data-tree]) // 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) jsonTree = JSON.stringify(folders)
} }
@ -94,8 +100,7 @@ export default ((userOpts?: Partial<Options>) => {
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-tree={jsonTree}
aria-controls="explorer-content" aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"} aria-expanded={opts.folderDefaultState === "open"}>
>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2> <h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -107,8 +112,7 @@ export default ((userOpts?: Partial<Options>) => {
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="fold" class="fold">
>
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>

View File

@ -1,5 +1,5 @@
// @ts-ignore // @ts-ignore
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
import { import {
joinSegments, joinSegments,
resolveRelative, resolveRelative,
@ -32,7 +32,10 @@ export type FolderState = {
collapsed: boolean collapsed: boolean
} }
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined { function getPathSegment(
fp: FilePath | undefined,
idx: number,
): string | undefined {
if (!fp) { if (!fp) {
return undefined return undefined
} }
@ -48,7 +51,12 @@ export class FileNode {
file: QuartzPluginData | null file: QuartzPluginData | null
depth: number depth: number
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) { constructor(
slugSegment: string,
displayName?: string,
file?: QuartzPluginData,
depth?: number,
) {
this.children = [] this.children = []
this.name = slugSegment this.name = slugSegment
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
@ -73,7 +81,9 @@ export class FileNode {
} }
} else { } else {
// direct child // 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 return
@ -99,7 +109,7 @@ export class FileNode {
// Add new file to tree // Add new file to tree
add(file: QuartzPluginData) { add(file: QuartzPluginData) {
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") }) this.insert({file: file, path: simplifySlug(file.slug!).split("/")})
} }
/** /**
@ -133,7 +143,7 @@ export class FileNode {
if (!node.file) { if (!node.file) {
const folderPath = joinSegments(currentPath, node.name) const folderPath = joinSegments(currentPath, node.name)
if (folderPath !== "") { if (folderPath !== "") {
folderPaths.push({ path: folderPath, collapsed }) folderPaths.push({path: folderPath, collapsed})
} }
node.children.forEach((child) => traverse(child, folderPath)) node.children.forEach((child) => traverse(child, folderPath))
@ -162,13 +172,19 @@ type ExplorerNodeProps = {
fullPath?: string fullPath?: string
} }
export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) { export function ExplorerNode({
node,
opts,
fullPath,
fileData,
}: ExplorerNodeProps) {
// Get options // Get options
const folderBehavior = opts.folderClickBehavior const folderBehavior = opts.folderClickBehavior
const isDefaultOpen = opts.folderDefaultState === "open" const isDefaultOpen = opts.folderDefaultState === "open"
// Calculate current folderPath // 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) + "/" const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
return ( return (
@ -176,7 +192,9 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
{node.file ? ( {node.file ? (
// Single file node // Single file node
<li key={node.file.slug}> <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} {node.displayName}
</a> </a>
</li> </li>
@ -196,8 +214,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="folder-icon" class="folder-icon">
>
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */} {/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
@ -215,15 +232,15 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
</div> </div>
)} )}
{/* Recursively render children of folder */} {/* Recursively render children of folder */}
<div class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}> <div
class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}>
<ul <ul
// Inline style for left folder paddings // Inline style for left folder paddings
style={{ style={{
paddingLeft: node.name !== "" ? "1.4rem" : "0", paddingLeft: node.name !== "" ? "1.4rem" : "0",
}} }}
class="content" class="content"
data-folderul={folderPath} data-folderul={folderPath}>
>
{node.children.map((childNode, i) => ( {node.children.map((childNode, i) => (
<ExplorerNode <ExplorerNode
node={childNode} node={childNode}

View File

@ -1,14 +1,21 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import style from "./styles/footer.scss" import style from "./styles/footer.scss"
import { version } from "../../package.json" import {version} from "../../package.json"
import { i18n } from "../i18n" import {i18n} from "../i18n"
interface Options { interface Options {
links: Record<string, string> links: Record<string, string>
} }
export default ((opts?: Options) => { export default ((opts?: Options) => {
const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Footer: QuartzComponent = ({
displayClass,
cfg,
}: QuartzComponentProps) => {
const year = new Date().getFullYear() const year = new Date().getFullYear()
const links = opts?.links ?? [] const links = opts?.links ?? []
return ( return (

View File

@ -1,9 +1,13 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
// @ts-ignore // @ts-ignore
import script from "./scripts/graph.inline" import script from "./scripts/graph.inline"
import style from "./styles/graph.scss" import style from "./styles/graph.scss"
import { i18n } from "../i18n" import {i18n} from "../i18n"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
export interface D3Config { export interface D3Config {
drag: boolean drag: boolean
@ -57,9 +61,12 @@ const defaultOptions: GraphOptions = {
} }
export default ((opts?: GraphOptions) => { export default ((opts?: GraphOptions) => {
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Graph: QuartzComponent = ({
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } displayClass,
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } cfg,
}: QuartzComponentProps) => {
const localGraph = {...defaultOptions.localGraph, ...opts?.localGraph}
const globalGraph = {...defaultOptions.globalGraph, ...opts?.globalGraph}
return ( return (
<div class={classNames(displayClass, "graph")}> <div class={classNames(displayClass, "graph")}>
<h3>{i18n(cfg.locale).components.graph.title}</h3> <h3>{i18n(cfg.locale).components.graph.title}</h3>
@ -74,8 +81,7 @@ export default ((opts?: GraphOptions) => {
y="0px" y="0px"
viewBox="0 0 55 55" viewBox="0 0 55 55"
fill="currentColor" fill="currentColor"
xmlSpace="preserve" xmlSpace="preserve">
>
<path <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 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 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> </svg>
</div> </div>
<div id="global-graph-outer"> <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>
</div> </div>
) )

View File

@ -1,15 +1,25 @@
import { i18n } from "../i18n" import {i18n} from "../i18n"
import { FullSlug, joinSegments, pathToRoot } from "../util/path" import {FullSlug, joinSegments, pathToRoot} from "../util/path"
import { JSResourceToScriptElement } from "../util/resources" import {JSResourceToScriptElement} from "../util/resources"
import { googleFontHref } from "../util/theme" import {googleFontHref} from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
export default (() => { export default (() => {
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { const Head: QuartzComponent = ({
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title cfg,
fileData,
externalResources,
}: QuartzComponentProps) => {
const title =
fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
const description = const description =
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description fileData.description?.trim() ??
const { css, js } = externalResources i18n(cfg.locale).propertyDefaults.description
const {css, js} = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug const path = url.pathname as FullSlug
@ -39,7 +49,13 @@ export default (() => {
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="generator" content="Quartz" /> <meta name="generator" content="Quartz" />
{css.map((href) => ( {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 {js
.filter((resource) => resource.loadTime === "beforeDOMReady") .filter((resource) => resource.loadTime === "beforeDOMReady")

View File

@ -1,6 +1,10 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
const Header: QuartzComponent = ({ children }: QuartzComponentProps) => { const Header: QuartzComponent = ({children}: QuartzComponentProps) => {
return children.length > 0 ? <header>{children}</header> : null return children.length > 0 ? <header>{children}</header> : null
} }

View File

@ -1,4 +1,8 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
export default ((component?: QuartzComponent) => { export default ((component?: QuartzComponent) => {
if (component) { if (component) {

View File

@ -1,8 +1,8 @@
import { FullSlug, resolveRelative } from "../util/path" import {FullSlug, resolveRelative} from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
import { Date, getDate } from "./Date" import {Date, getDate} from "./Date"
import { QuartzComponent, QuartzComponentProps } from "./types" import {QuartzComponent, QuartzComponentProps} from "./types"
import { GlobalConfiguration } from "../cfg" import {GlobalConfiguration} from "../cfg"
export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
@ -30,7 +30,13 @@ type Props = {
sort?: SortFn sort?: SortFn
} & QuartzComponentProps } & 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) const sorter = sort ?? byDateAndAlphabetical(cfg)
let list = allFiles.sort(sorter) let list = allFiles.sort(sorter)
if (limit) { if (limit) {
@ -53,7 +59,9 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
)} )}
<div class="desc"> <div class="desc">
<h3> <h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> <a
href={resolveRelative(fileData.slug!, page.slug!)}
class="internal">
{title} {title}
</a> </a>
</h3> </h3>
@ -63,8 +71,10 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
<li> <li>
<a <a
class="internal tag-link" class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} href={resolveRelative(
> fileData.slug!,
`tags/${tag}` as FullSlug,
)}>
{tag} {tag}
</a> </a>
</li> </li>

View File

@ -1,9 +1,17 @@
import { pathToRoot } from "../util/path" import {pathToRoot} from "../util/path"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { classNames } from "../util/lang" QuartzComponent,
import { i18n } from "../i18n" 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 title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
return ( return (

View File

@ -1,12 +1,16 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" QuartzComponent,
import { QuartzPluginData } from "../plugins/vfile" QuartzComponentConstructor,
import { byDateAndAlphabetical } from "./PageList" QuartzComponentProps,
} from "./types"
import {FullSlug, SimpleSlug, resolveRelative} from "../util/path"
import {QuartzPluginData} from "../plugins/vfile"
import {byDateAndAlphabetical} from "./PageList"
import style from "./styles/recentNotes.scss" import style from "./styles/recentNotes.scss"
import { Date, getDate } from "./Date" import {Date, getDate} from "./Date"
import { GlobalConfiguration } from "../cfg" import {GlobalConfiguration} from "../cfg"
import { i18n } from "../i18n" import {i18n} from "../i18n"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
interface Options { interface Options {
title?: string title?: string
@ -32,7 +36,7 @@ export default ((userOpts?: Partial<Options>) => {
displayClass, displayClass,
cfg, cfg,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
const opts = { ...defaultOptions(cfg), ...userOpts } const opts = {...defaultOptions(cfg), ...userOpts}
const pages = allFiles.filter(opts.filter).sort(opts.sort) const pages = allFiles.filter(opts.filter).sort(opts.sort)
const remaining = Math.max(0, pages.length - opts.limit) const remaining = Math.max(0, pages.length - opts.limit)
return ( return (
@ -40,7 +44,8 @@ export default ((userOpts?: Partial<Options>) => {
<h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3> <h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
<ul class="recent-ul"> <ul class="recent-ul">
{pages.slice(0, opts.limit).map((page) => { {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 ?? [] const tags = page.frontmatter?.tags ?? []
return ( return (
@ -48,7 +53,9 @@ export default ((userOpts?: Partial<Options>) => {
<div class="section"> <div class="section">
<div class="desc"> <div class="desc">
<h3> <h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> <a
href={resolveRelative(fileData.slug!, page.slug!)}
class="internal">
{title} {title}
</a> </a>
</h3> </h3>
@ -64,8 +71,10 @@ export default ((userOpts?: Partial<Options>) => {
<li> <li>
<a <a
class="internal tag-link" class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} href={resolveRelative(
> fileData.slug!,
`tags/${tag}` as FullSlug,
)}>
{tag} {tag}
</a> </a>
</li> </li>
@ -80,7 +89,9 @@ export default ((userOpts?: Partial<Options>) => {
{opts.linkToMore && remaining > 0 && ( {opts.linkToMore && remaining > 0 && (
<p> <p>
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}> <a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })} {i18n(cfg.locale).components.recentNotes.seeRemainingMore({
remaining,
})}
</a> </a>
</p> </p>
)} )}

View File

@ -1,9 +1,13 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import style from "./styles/search.scss" import style from "./styles/search.scss"
// @ts-ignore // @ts-ignore
import script from "./scripts/search.inline" import script from "./scripts/search.inline"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
import { i18n } from "../i18n" import {i18n} from "../i18n"
export interface SearchOptions { export interface SearchOptions {
enablePreview: boolean enablePreview: boolean
@ -14,14 +18,21 @@ const defaultOptions: SearchOptions = {
} }
export default ((userOpts?: Partial<SearchOptions>) => { export default ((userOpts?: Partial<SearchOptions>) => {
const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Search: QuartzComponent = ({
const opts = { ...defaultOptions, ...userOpts } displayClass,
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder cfg,
}: QuartzComponentProps) => {
const opts = {...defaultOptions, ...userOpts}
const searchPlaceholder = i18n(cfg.locale).components.search
.searchBarPlaceholder
return ( return (
<div class={classNames(displayClass, "search")}> <div class={classNames(displayClass, "search")}>
<button class="search-button" id="search-button"> <button class="search-button" id="search-button">
<p>{i18n(cfg.locale).components.search.title}</p> <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> <title>Search</title>
<g class="search-path" fill="none"> <g class="search-path" fill="none">
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />

View File

@ -1,7 +1,7 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import {QuartzComponentConstructor, QuartzComponentProps} from "./types"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
function Spacer({ displayClass }: QuartzComponentProps) { function Spacer({displayClass}: QuartzComponentProps) {
return <div class={classNames(displayClass, "spacer")}></div> return <div class={classNames(displayClass, "spacer")}></div>
} }

View File

@ -1,11 +1,15 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "./types"
import legacyStyle from "./styles/legacyToc.scss" import legacyStyle from "./styles/legacyToc.scss"
import modernStyle from "./styles/toc.scss" import modernStyle from "./styles/toc.scss"
import { classNames } from "../util/lang" import {classNames} from "../util/lang"
// @ts-ignore // @ts-ignore
import script from "./scripts/toc.inline" import script from "./scripts/toc.inline"
import { i18n } from "../i18n" import {i18n} from "../i18n"
interface Options { interface Options {
layout: "modern" | "legacy" layout: "modern" | "legacy"
@ -31,8 +35,7 @@ const TableOfContents: QuartzComponent = ({
id="toc" id="toc"
class={fileData.collapseToc ? "collapsed" : ""} class={fileData.collapseToc ? "collapsed" : ""}
aria-controls="toc-content" aria-controls="toc-content"
aria-expanded={!fileData.collapseToc} aria-expanded={!fileData.collapseToc}>
>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -44,8 +47,7 @@ const TableOfContents: QuartzComponent = ({
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="fold" class="fold">
>
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
@ -66,7 +68,10 @@ const TableOfContents: QuartzComponent = ({
TableOfContents.css = modernStyle TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script TableOfContents.afterDOMLoaded = script
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { const LegacyTableOfContents: QuartzComponent = ({
fileData,
cfg,
}: QuartzComponentProps) => {
if (!fileData.toc) { if (!fileData.toc) {
return null return null
} }

View File

@ -1,8 +1,15 @@
import { pathToRoot, slugTag } from "../util/path" import {pathToRoot, slugTag} from "../util/path"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import {
import { classNames } from "../util/lang" 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 tags = fileData.frontmatter?.tags
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {

View File

@ -1,7 +1,11 @@
import { i18n } from "../../i18n" import {i18n} from "../../i18n"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "../types"
const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => { const NotFound: QuartzComponent = ({cfg}: QuartzComponentProps) => {
// If baseUrl contains a pathname after the domain, use this as the home link // If baseUrl contains a pathname after the domain, use this as the home link
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const baseDir = url.pathname const baseDir = url.pathname

View File

@ -1,7 +1,11 @@
import { htmlToJsx } from "../../util/jsx" 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: QuartzComponent = ({fileData, tree}: QuartzComponentProps) => {
const content = htmlToJsx(fileData.filePath!, tree) const content = htmlToJsx(fileData.filePath!, tree)
const classes: string[] = fileData.frontmatter?.cssclasses ?? [] const classes: string[] = fileData.frontmatter?.cssclasses ?? []
const classString = ["popover-hint", ...classes].join(" ") const classString = ["popover-hint", ...classes].join(" ")

View File

@ -1,12 +1,16 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "../types"
import path from "path" import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList, SortFn } from "../PageList" import {PageList, SortFn} from "../PageList"
import { stripSlashes, simplifySlug } from "../../util/path" import {stripSlashes, simplifySlug} from "../../util/path"
import { Root } from "hast" import {Root} from "hast"
import { htmlToJsx } from "../../util/jsx" import {htmlToJsx} from "../../util/jsx"
import { i18n } from "../../i18n" import {i18n} from "../../i18n"
interface FolderContentOptions { interface FolderContentOptions {
/** /**
@ -21,14 +25,15 @@ const defaultOptions: FolderContentOptions = {
} }
export default ((opts?: Partial<FolderContentOptions>) => { export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts } const options: FolderContentOptions = {...defaultOptions, ...opts}
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props const {tree, fileData, allFiles, cfg} = props
const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => { const allPagesInFolder = allFiles.filter((file) => {
const fileSlug = stripSlashes(simplifySlug(file.slug!)) 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 folderParts = folderSlug.split(path.posix.sep)
const fileParts = fileSlug.split(path.posix.sep) const fileParts = fileSlug.split(path.posix.sep)
const isDirectChild = fileParts.length === folderParts.length + 1 const isDirectChild = fileParts.length === folderParts.length + 1

View File

@ -1,11 +1,15 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "../types"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList, SortFn } from "../PageList" import {PageList, SortFn} from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import {FullSlug, getAllSegmentPrefixes, simplifySlug} from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile" import {QuartzPluginData} from "../../plugins/vfile"
import { Root } from "hast" import {Root} from "hast"
import { htmlToJsx } from "../../util/jsx" import {htmlToJsx} from "../../util/jsx"
import { i18n } from "../../i18n" import {i18n} from "../../i18n"
interface TagContentOptions { interface TagContentOptions {
sort?: SortFn sort?: SortFn
@ -17,20 +21,24 @@ const defaultOptions: TagContentOptions = {
} }
export default ((opts?: Partial<TagContentOptions>) => { export default ((opts?: Partial<TagContentOptions>) => {
const options: TagContentOptions = { ...defaultOptions, ...opts } const options: TagContentOptions = {...defaultOptions, ...opts}
const TagContent: QuartzComponent = (props: QuartzComponentProps) => { const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props const {tree, fileData, allFiles, cfg} = props
const slug = fileData.slug const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) { 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 tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
const allPagesWithTag = (tag: string) => const allPagesWithTag = (tag: string) =>
allFiles.filter((file) => allFiles.filter((file) =>
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), (file.frontmatter?.tags ?? [])
.flatMap(getAllSegmentPrefixes)
.includes(tag),
) )
const content = const content =
@ -42,7 +50,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
if (tag === "/") { if (tag === "/") {
const tags = [ const tags = [
...new Set( ...new Set(
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), allFiles
.flatMap((data) => data.frontmatter?.tags ?? [])
.flatMap(getAllSegmentPrefixes),
), ),
].sort((a, b) => a.localeCompare(b)) ].sort((a, b) => a.localeCompare(b))
const tagItemMap: Map<string, QuartzPluginData[]> = new Map() const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
@ -54,7 +64,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
<article> <article>
<p>{content}</p> <p>{content}</p>
</article> </article>
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p> <p>
{i18n(cfg.locale).pages.tagContent.totalTags({count: tags.length})}
</p>
<div> <div>
{tags.map((tag) => { {tags.map((tag) => {
const pages = tagItemMap.get(tag)! const pages = tagItemMap.get(tag)!
@ -63,7 +75,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
allFiles: pages, 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 root = contentPage?.htmlAst
const content = const content =
@ -81,7 +95,9 @@ export default ((opts?: Partial<TagContentOptions>) => {
{content && <p>{content}</p>} {content && <p>{content}</p>}
<div class="page-listing"> <div class="page-listing">
<p> <p>
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} {i18n(cfg.locale).pages.tagContent.itemsUnderTag({
count: pages.length,
})}
{pages.length > options.numPages && ( {pages.length > options.numPages && (
<> <>
{" "} {" "}
@ -93,7 +109,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
</> </>
)} )}
</p> </p>
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} /> <PageList
limit={options.numPages}
{...listProps}
sort={opts?.sort}
/>
</div> </div>
</div> </div>
) )
@ -112,7 +132,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
<div class={classes}> <div class={classes}>
<article>{content}</article> <article>{content}</article>
<div class="page-listing"> <div class="page-listing">
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p> <p>
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({
count: pages.length,
})}
</p>
<div> <div>
<PageList {...listProps} /> <PageList {...listProps} />
</div> </div>

View File

@ -1,13 +1,19 @@
import { render } from "preact-render-to-string" import {render} from "preact-render-to-string"
import { QuartzComponent, QuartzComponentProps } from "./types" import {QuartzComponent, QuartzComponentProps} from "./types"
import HeaderConstructor from "./Header" import HeaderConstructor from "./Header"
import BodyConstructor from "./Body" import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources" import {JSResourceToScriptElement, StaticResources} from "../util/resources"
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import {
import { visit } from "unist-util-visit" clone,
import { Root, Element, ElementContent } from "hast" FullSlug,
import { GlobalConfiguration } from "../cfg" RelativeURL,
import { i18n } from "../i18n" joinSegments,
normalizeHastElement,
} from "../util/path"
import {visit} from "unist-util-visit"
import {Root, Element, ElementContent} from "hast"
import {GlobalConfiguration} from "../cfg"
import {i18n} from "../i18n"
interface RenderComponents { interface RenderComponents {
head: QuartzComponent head: QuartzComponent
@ -71,7 +77,9 @@ export function renderPage(
if (classNames.includes("transclude")) { if (classNames.includes("transclude")) {
const inner = node.children[0] as Element const inner = node.children[0] as Element
const transcludeTarget = inner.properties["data-slug"] as FullSlug const transcludeTarget = inner.properties["data-slug"] as FullSlug
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) const page = componentData.allFiles.find(
(f) => f.slug === transcludeTarget,
)
if (!page) { if (!page) {
return return
} }
@ -96,9 +104,16 @@ export function renderPage(
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, properties: {
href: inner.properties?.href,
class: ["internal", "transclude-src"],
},
children: [ 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 let endIdx = undefined
for (const [i, el] of page.htmlAst.children.entries()) { for (const [i, el] of page.htmlAst.children.entries()) {
// skip non-headers // 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)) const depth = Number(el.tagName.substring(1))
// lookin for our blockref // lookin for our blockref
@ -133,15 +149,23 @@ export function renderPage(
} }
node.children = [ 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), normalizeHastElement(child as Element, slug, transcludeTarget),
), ),
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, properties: {
href: inner.properties?.href,
class: ["internal", "transclude-src"],
},
children: [ 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", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, properties: {
href: inner.properties?.href,
class: ["internal", "transclude-src"],
},
children: [ 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> </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 = ( const doc = (
<html lang={lang}> <html lang={lang}>
<Head {...componentData} /> <Head {...componentData} />

View File

@ -14,7 +14,9 @@ function toggleCallout(this: HTMLElement) {
} }
const collapsed = parent.classList.contains("is-collapsed") 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" parent.style.maxHeight = height + "px"
current = parent current = parent

View File

@ -1,4 +1,4 @@
import { getFullSlug } from "../../util/path" import {getFullSlug} from "../../util/path"
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}` const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
@ -10,7 +10,9 @@ document.addEventListener("nav", () => {
const elId = checkboxId(index) const elId = checkboxId(index)
const switchState = (e: Event) => { 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) localStorage.setItem(elId, newCheckboxState)
} }

View File

@ -1,6 +1,8 @@
const changeTheme = (e: CustomEventMap["themechange"]) => { const changeTheme = (e: CustomEventMap["themechange"]) => {
const theme = e.detail.theme 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) { if (!iframe) {
return return
} }
@ -49,11 +51,20 @@ document.addEventListener("nav", () => {
giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo) giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo)
giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId) giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId)
giscusScript.setAttribute("data-category", giscusContainer.dataset.category) 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-mapping", giscusContainer.dataset.mapping)
giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict) giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict)
giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled) giscusScript.setAttribute(
giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition) "data-reactions-enabled",
giscusContainer.dataset.reactionsEnabled,
)
giscusScript.setAttribute(
"data-input-position",
giscusContainer.dataset.inputPosition,
)
const theme = document.documentElement.getAttribute("saved-theme") const theme = document.documentElement.getAttribute("saved-theme")
if (theme) { if (theme) {
@ -65,6 +76,6 @@ document.addEventListener("nav", () => {
document.addEventListener("themechange", changeTheme) document.addEventListener("themechange", changeTheme)
// window.addCleanup(() => document.removeEventListener("themechange", changeTheme)) // window.addCleanup(() => document.removeEventListener("themechange", changeTheme))
window.addCleanup(() => window.addCleanup(() =>
document.removeEventListener("themechange", changeTheme as EventListener) document.removeEventListener("themechange", changeTheme as EventListener),
); )
}) })

View File

@ -1,10 +1,12 @@
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 const currentTheme = localStorage.getItem("theme") ?? userPref
document.documentElement.setAttribute("saved-theme", currentTheme) document.documentElement.setAttribute("saved-theme", currentTheme)
const emitThemeChangeEvent = (theme: "light" | "dark") => { const emitThemeChangeEvent = (theme: "light" | "dark") => {
const event: CustomEventMap["themechange"] = new CustomEvent("themechange", { const event: CustomEventMap["themechange"] = new CustomEvent("themechange", {
detail: { theme }, detail: {theme},
}) })
document.dispatchEvent(event) document.dispatchEvent(event)
} }
@ -26,15 +28,23 @@ document.addEventListener("nav", () => {
} }
// Darkmode toggle // Darkmode toggle
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement const toggleSwitch = document.querySelector(
"#darkmode-toggle",
) as HTMLInputElement
toggleSwitch.addEventListener("change", switchTheme) toggleSwitch.addEventListener("change", switchTheme)
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) window.addCleanup(() =>
toggleSwitch.removeEventListener("change", switchTheme),
)
if (currentTheme === "dark") { if (currentTheme === "dark") {
toggleSwitch.checked = true toggleSwitch.checked = true
} }
// Listen for changes in prefers-color-scheme // Listen for changes in prefers-color-scheme
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const colorSchemeMediaQuery = window.matchMedia(
"(prefers-color-scheme: dark)",
)
colorSchemeMediaQuery.addEventListener("change", themeChange) colorSchemeMediaQuery.addEventListener("change", themeChange)
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange)) window.addCleanup(() =>
colorSchemeMediaQuery.removeEventListener("change", themeChange),
)
}) })

View File

@ -1,4 +1,4 @@
import { FolderState } from "../ExplorerNode" import {FolderState} from "../ExplorerNode"
type MaybeHTMLElement = HTMLElement | undefined type MaybeHTMLElement = HTMLElement | undefined
let currentExplorerState: FolderState[] let currentExplorerState: FolderState[]
@ -25,7 +25,8 @@ function toggleExplorer(this: HTMLElement) {
if (!content) return if (!content) return
content.classList.toggle("collapsed") 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) { function toggleFolder(evt: MouseEvent) {
@ -82,20 +83,26 @@ function setupExplorer() {
const useSavedFolderState = explorer?.dataset.savestate === "true" const useSavedFolderState = explorer?.dataset.savestate === "true"
const oldExplorerState: FolderState[] = const oldExplorerState: FolderState[] =
storageTree && useSavedFolderState ? JSON.parse(storageTree) : [] storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed])) const oldIndex = new Map(
oldExplorerState.map((entry) => [entry.path, entry.collapsed]),
)
const newExplorerState: FolderState[] = explorer.dataset.tree const newExplorerState: FolderState[] = explorer.dataset.tree
? JSON.parse(explorer.dataset.tree) ? JSON.parse(explorer.dataset.tree)
: [] : []
currentExplorerState = [] currentExplorerState = []
for (const { path, collapsed } of newExplorerState) { for (const {path, collapsed} of newExplorerState) {
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed }) currentExplorerState.push({
path,
collapsed: oldIndex.get(path) ?? collapsed,
})
} }
currentExplorerState.map((folderState) => { currentExplorerState.map((folderState) => {
const folderLi = document.querySelector( const folderLi = document.querySelector(
`[data-folderpath='${folderState.path}']`, `[data-folderpath='${folderState.path}']`,
) as MaybeHTMLElement ) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement const folderUl = folderLi?.parentElement
?.nextElementSibling as MaybeHTMLElement
if (folderUl) { if (folderUl) {
setFolderState(folderUl, folderState.collapsed) setFolderState(folderUl, folderState.collapsed)
} }
@ -120,7 +127,9 @@ document.addEventListener("nav", () => {
* @param collapsed if folder should be set to collapsed or not * @param collapsed if folder should be set to collapsed or not
*/ */
function setFolderState(folderElement: HTMLElement, collapsed: boolean) { 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")
} }
/** /**

View File

@ -1,7 +1,13 @@
import type { ContentDetails } from "../../plugins/emitters/contentIndex" import type {ContentDetails} from "../../plugins/emitters/contentIndex"
import * as d3 from "d3" import * as d3 from "d3"
import { registerEscapeHandler, removeAllChildren } from "./util" import {registerEscapeHandler, removeAllChildren} from "./util"
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" import {
FullSlug,
SimpleSlug,
getFullSlug,
resolveRelative,
simplifySlug,
} from "../../util/path"
type NodeData = { type NodeData = {
id: SimpleSlug id: SimpleSlug
@ -62,7 +68,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
for (const dest of outgoing) { for (const dest of outgoing) {
if (validLinks.has(dest)) { if (validLinks.has(dest)) {
links.push({ source: source, target: dest }) links.push({source: source, target: dest})
} }
} }
@ -74,7 +80,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
tags.push(...localTags.filter((tag) => !tags.includes(tag))) tags.push(...localTags.filter((tag) => !tags.includes(tag)))
for (const tag of localTags) { for (const tag of localTags) {
links.push({ source: source, target: tag }) links.push({source: source, target: tag})
} }
} }
} }
@ -92,7 +98,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
neighbourhood.add(cur) neighbourhood.add(cur)
const outgoing = links.filter((l) => l.source === cur) const outgoing = links.filter((l) => l.source === cur)
const incoming = links.filter((l) => l.target === 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 { } else {
@ -100,16 +109,20 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
} }
const graphData: { nodes: NodeData[]; links: LinkData[] } = { const graphData: {nodes: NodeData[]; links: LinkData[]} = {
nodes: [...neighbourhood].map((url) => { nodes: [...neighbourhood].map((url) => {
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url) const text = url.startsWith("tags/")
? "#" + url.substring(5)
: data.get(url)?.title ?? url
return { return {
id: url, id: url,
text: text, text: text,
tags: data.get(url)?.tags ?? [], tags: data.get(url)?.tags ?? [],
} }
}), }),
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), links: links.filter(
(l) => neighbourhood.has(l.source) && neighbourhood.has(l.target),
),
} }
const simulation: d3.Simulation<NodeData, LinkData> = d3 const simulation: d3.Simulation<NodeData, LinkData> = d3
@ -132,7 +145,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
.append("svg") .append("svg")
.attr("width", width) .attr("width", width)
.attr("height", height) .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 // draw links between nodes
const link = svg const link = svg
@ -145,7 +163,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
.attr("stroke-width", 1) .attr("stroke-width", 1)
// svg groups // 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 // calculate color
const color = (d: NodeData) => { const color = (d: NodeData) => {
@ -186,7 +209,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
} }
function nodeRadius(d: NodeData) { 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) return 2 + Math.sqrt(numLinks)
} }
@ -208,11 +233,15 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
const currentId = d.id const currentId = d.id
const linkNodes = d3 const linkNodes = d3
.selectAll(".link") .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) { if (focusOnHover) {
// fade out non-neighbour nodes // 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") d3.selectAll<HTMLElement, NodeData>(".link")
.transition() .transition()
@ -238,7 +267,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
} }
// highlight links // 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 const bigFont = fontSize * 1.5
@ -255,19 +288,32 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
}) })
.on("mouseleave", function (_, d) { .on("mouseleave", function (_, d) {
if (focusOnHover) { if (focusOnHover) {
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1) d3.selectAll<HTMLElement, NodeData>(".link")
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1) .transition()
.duration(200)
.style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node")
.transition()
.duration(200)
.style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node") d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id)) .filter((d) => !connectedNodes.includes(d.id))
.nodes() .nodes()
.map((it) => d3.select(it.parentNode as HTMLElement).select("text")) .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 currentId = d.id
const linkNodes = d3 const linkNodes = d3
.selectAll(".link") .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)") linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
@ -313,7 +359,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
[width, height], [width, height],
]) ])
.scaleExtent([0.25, 4]) .scaleExtent([0.25, 4])
.on("zoom", ({ transform }) => { .on("zoom", ({transform}) => {
link.attr("transform", transform) link.attr("transform", transform)
node.attr("transform", transform) node.attr("transform", transform)
const scale = transform.k * opacityScale const scale = transform.k * opacityScale
@ -366,5 +412,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) window.addCleanup(() =>
containerIcon?.removeEventListener("click", renderGlobalGraph),
)
}) })

View File

@ -1,10 +1,10 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom" import {computePosition, flip, inline, shift} from "@floating-ui/dom"
import { normalizeRelativeURLs } from "../../util/path" import {normalizeRelativeURLs} from "../../util/path"
const p = new DOMParser() const p = new DOMParser()
async function mouseEnterHandler( async function mouseEnterHandler(
this: HTMLAnchorElement, this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number }, {clientX, clientY}: {clientX: number; clientY: number},
) { ) {
const link = this const link = this
if (link.dataset.noPopover === "true") { if (link.dataset.noPopover === "true") {
@ -12,8 +12,8 @@ async function mouseEnterHandler(
} }
async function setPosition(popoverElement: HTMLElement) { async function setPosition(popoverElement: HTMLElement) {
const { x, y } = await computePosition(link, popoverElement, { const {x, y} = await computePosition(link, popoverElement, {
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()], middleware: [inline({x: clientX, y: clientY}), shift(), flip()],
}) })
Object.assign(popoverElement.style, { Object.assign(popoverElement.style, {
left: `${x}px`, left: `${x}px`,
@ -94,15 +94,19 @@ async function mouseEnterHandler(
const heading = popoverInner.querySelector(hash) as HTMLElement | null const heading = popoverInner.querySelector(hash) as HTMLElement | null
if (heading) { if (heading) {
// leave ~12px of buffer when scrolling to a heading // leave ~12px of buffer when scrolling to a heading
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" }) popoverInner.scroll({top: heading.offsetTop - 12, behavior: "instant"})
} }
} }
} }
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[] const links = [
...document.getElementsByClassName("internal"),
] as HTMLAnchorElement[]
for (const link of links) { for (const link of links) {
link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseenter", mouseEnterHandler)
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) window.addCleanup(() =>
link.removeEventListener("mouseenter", mouseEnterHandler),
)
} }
}) })

View File

@ -1,7 +1,7 @@
import FlexSearch from "flexsearch" import FlexSearch from "flexsearch"
import { ContentDetails } from "../../plugins/emitters/contentIndex" import {ContentDetails} from "../../plugins/emitters/contentIndex"
import { registerEscapeHandler, removeAllChildren } from "./util" import {registerEscapeHandler, removeAllChildren} from "./util"
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path" import {FullSlug, normalizeRelativeURLs, resolveRelative} from "../../util/path"
interface Item { interface Item {
id: number id: number
@ -15,7 +15,8 @@ interface Item {
type SearchType = "basic" | "tags" type SearchType = "basic" | "tags"
let searchType: SearchType = "basic" let searchType: SearchType = "basic"
let currentSearchTerm: string = "" 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>({ let index = new FlexSearch.Document<Item>({
charset: "latin:extra", charset: "latin:extra",
encode: encoder, encode: encoder,
@ -65,12 +66,18 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
let endIndex = tokenizedText.length - 1 let endIndex = tokenizedText.length - 1
if (trim) { if (trim) {
const includesCheck = (tok: string) => 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) const occurrencesIndices = tokenizedText.map(includesCheck)
let bestSum = 0 let bestSum = 0
let bestIndex = 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 window = occurrencesIndices.slice(i, i + contextWindowWords)
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0) const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
if (windowSum >= bestSum) { if (windowSum >= bestSum) {
@ -80,7 +87,10 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
} }
startIndex = Math.max(bestIndex - contextWindowWords, 0) 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) tokenizedText = tokenizedText.slice(startIndex, endIndex)
} }
@ -124,15 +134,21 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
let lastIndex = 0 let lastIndex = 0
for (const match of matches) { for (const match of matches) {
const matchIndex = nodeText.indexOf(match, lastIndex) 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)) spanContainer.appendChild(createHighlightSpan(match))
lastIndex = matchIndex + match.length lastIndex = matchIndex + match.length
} }
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) spanContainer.appendChild(
document.createTextNode(nodeText.slice(lastIndex)),
)
node.parentNode?.replaceChild(spanContainer, node) node.parentNode?.replaceChild(spanContainer, node)
} else if (node.nodeType === Node.ELEMENT_NODE) { } else if (node.nodeType === Node.ELEMENT_NODE) {
if ((node as HTMLElement).classList.contains("highlight")) return 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 container = document.getElementById("search-container")
const sidebar = container?.closest(".sidebar") as HTMLElement const sidebar = container?.closest(".sidebar") as HTMLElement
const searchButton = document.getElementById("search-button") 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 searchLayout = document.getElementById("search-layout")
const idDataMap = Object.keys(data) as FullSlug[] const idDataMap = Object.keys(data) as FullSlug[]
@ -212,7 +230,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const searchBarOpen = container?.classList.contains("active") const searchBarOpen = container?.classList.contains("active")
searchBarOpen ? hideSearch() : showSearch("basic") searchBarOpen ? hideSearch() : showSearch("basic")
return 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 // Hotkey to open tag search
e.preventDefault() e.preventDefault()
const searchBarOpen = container?.classList.contains("active") const searchBarOpen = container?.classList.contains("active")
@ -237,7 +259,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
await displayPreview(active) await displayPreview(active)
active.click() active.click()
} else { } 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 if (!anchor || anchor?.classList.contains("no-match")) return
await displayPreview(anchor) await displayPreview(anchor)
anchor.click() anchor.click()
@ -249,7 +273,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const currentResult = currentHover const currentResult = currentHover
? currentHover ? currentHover
: (document.activeElement as HTMLInputElement | null) : (document.activeElement as HTMLInputElement | null)
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null const prevResult =
currentResult?.previousElementSibling as HTMLInputElement | null
currentResult?.classList.remove("focus") currentResult?.classList.remove("focus")
prevResult?.focus() prevResult?.focus()
if (prevResult) currentHover = prevResult if (prevResult) currentHover = prevResult
@ -262,8 +287,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
if (document.activeElement === searchBar || currentHover !== null) { if (document.activeElement === searchBar || currentHover !== null) {
const firstResult = currentHover const firstResult = currentHover
? currentHover ? currentHover
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) : (document.getElementsByClassName(
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null "result-card",
)[0] as HTMLInputElement | null)
const secondResult =
firstResult?.nextElementSibling as HTMLInputElement | null
firstResult?.classList.remove("focus") firstResult?.classList.remove("focus")
secondResult?.focus() secondResult?.focus()
if (secondResult) currentHover = secondResult if (secondResult) currentHover = secondResult
@ -277,7 +305,10 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
return { return {
id, id,
slug, 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), content: highlight(term, data[slug].content ?? "", true),
tags: highlightTags(term.substring(1), data[slug].tags), tags: highlightTags(term.substring(1), data[slug].tags),
} }
@ -303,8 +334,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
return new URL(resolveRelative(currentSlug, slug), location.toString()) return new URL(resolveRelative(currentSlug, slug), location.toString())
} }
const resultToHTML = ({ slug, title, content, tags }: Item) => { 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") const itemTile = document.createElement("a")
itemTile.classList.add("result-card") itemTile.classList.add("result-card")
itemTile.id = slug itemTile.id = slug
@ -313,12 +345,14 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>` enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
}` }`
itemTile.addEventListener("click", (event) => { 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() hideSearch()
}) })
const handler = (event: MouseEvent) => { 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() hideSearch()
} }
@ -329,7 +363,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
} }
itemTile.addEventListener("mouseenter", onMouseEnter) itemTile.addEventListener("mouseenter", onMouseEnter)
window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter)) window.addCleanup(() =>
itemTile.removeEventListener("mouseenter", onMouseEnter),
)
itemTile.addEventListener("click", handler) itemTile.addEventListener("click", handler)
window.addCleanup(() => itemTile.removeEventListener("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 if (!searchLayout || !enablePreview || !el || !preview) return
const slug = el.id as FullSlug const slug = el.id as FullSlug
const innerDiv = await fetchContent(slug).then((contents) => 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 = document.createElement("div")
previewInner.classList.add("preview-inner") previewInner.classList.add("preview-inner")
@ -397,7 +435,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const highlights = [...preview.querySelectorAll(".highlight")].sort( const highlights = [...preview.querySelectorAll(".highlight")].sort(
(a, b) => b.innerHTML.length - a.innerHTML.length, (a, b) => b.innerHTML.length - a.innerHTML.length,
) )
highlights[0]?.scrollIntoView({ block: "start" }) highlights[0]?.scrollIntoView({block: "start"})
} }
async function onType(e: HTMLElementEventMap["input"]) { async function onType(e: HTMLElementEventMap["input"]) {
@ -454,14 +492,20 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
...getByField("content"), ...getByField("content"),
...getByField("tags"), ...getByField("tags"),
]) ])
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id)) const finalResults = [...allIds].map((id) =>
formatForDisplay(currentSearchTerm, id),
)
await displayResults(finalResults) await displayResults(finalResults)
} }
document.addEventListener("keydown", shortcutHandler) document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) window.addCleanup(() =>
document.removeEventListener("keydown", shortcutHandler),
)
searchButton?.addEventListener("click", () => showSearch("basic")) searchButton?.addEventListener("click", () => showSearch("basic"))
window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) window.addCleanup(() =>
searchButton?.removeEventListener("click", () => showSearch("basic")),
)
searchBar?.addEventListener("input", onType) searchBar?.addEventListener("input", onType)
window.addCleanup(() => searchBar?.removeEventListener("input", onType)) window.addCleanup(() => searchBar?.removeEventListener("input", onType))
@ -474,7 +518,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
* @param index index to fill * @param index index to fill
* @param data data to fill index with * @param data data to fill index with
*/ */
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { async function fillDocument(data: {[key: FullSlug]: ContentDetails}) {
let id = 0 let id = 0
const promises: Array<Promise<unknown>> = [] const promises: Array<Promise<unknown>> = []
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {

View File

@ -1,5 +1,10 @@
import micromorph from "micromorph" import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" import {
FullSlug,
RelativeURL,
getFullSlug,
normalizeRelativeURLs,
} from "../../util/path"
// adapted from `micromorph` // adapted from `micromorph`
// https://github.com/natemoo-re/micromorph // https://github.com/natemoo-re/micromorph
@ -23,19 +28,22 @@ const isSamePage = (url: URL): boolean => {
return sameOrigin && samePath return sameOrigin && samePath
} }
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => { const getOpts = ({target}: Event): {url: URL; scroll?: boolean} | undefined => {
if (!isElement(target)) return if (!isElement(target)) return
if (target.attributes.getNamedItem("target")?.value === "_blank") return if (target.attributes.getNamedItem("target")?.value === "_blank") return
const a = target.closest("a") const a = target.closest("a")
if (!a) return if (!a) return
if ("routerIgnore" in a.dataset) return if ("routerIgnore" in a.dataset) return
const { href } = a const {href} = a
if (!isLocalUrl(href)) return 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) { function notifyNav(url: FullSlug) {
const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } }) const event: CustomEventMap["nav"] = new CustomEvent("nav", {detail: {url}})
document.dispatchEvent(event) document.dispatchEvent(event)
} }
@ -86,15 +94,19 @@ async function navigate(url: URL, isBack: boolean = false) {
// scroll into place and add history // scroll into place and add history
if (!isBack) { if (!isBack) {
if (url.hash) { if (url.hash) {
const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) const el = document.getElementById(
decodeURIComponent(url.hash.substring(1)),
)
el?.scrollIntoView() el?.scrollIntoView()
} else { } else {
window.scrollTo({ top: 0 }) window.scrollTo({top: 0})
} }
} }
// now, patch head // now, patch head
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") const elementsToRemove = document.head.querySelectorAll(
":not([spa-preserve])",
)
elementsToRemove.forEach((el) => el.remove()) elementsToRemove.forEach((el) => el.remove())
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
elementsToAdd.forEach((el) => document.head.appendChild(el)) elementsToAdd.forEach((el) => document.head.appendChild(el))
@ -113,13 +125,15 @@ window.spaNavigate = navigate
function createRouter() { function createRouter() {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.addEventListener("click", async (event) => { window.addEventListener("click", async (event) => {
const { url } = getOpts(event) ?? {} const {url} = getOpts(event) ?? {}
// dont hijack behaviour, just let browser act normally // dont hijack behaviour, just let browser act normally
if (!url || event.ctrlKey || event.metaKey) return if (!url || event.ctrlKey || event.metaKey) return
event.preventDefault() event.preventDefault()
if (isSamePage(url) && url.hash) { if (isSamePage(url) && url.hash) {
const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) const el = document.getElementById(
decodeURIComponent(url.hash.substring(1)),
)
el?.scrollIntoView() el?.scrollIntoView()
history.pushState({}, "", url) history.pushState({}, "", url)
return return
@ -133,8 +147,9 @@ function createRouter() {
}) })
window.addEventListener("popstate", (event) => { window.addEventListener("popstate", (event) => {
const { url } = getOpts(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 { try {
navigate(new URL(window.location.toString()), true) navigate(new URL(window.location.toString()), true)
} catch (e) { } catch (e) {
@ -167,7 +182,7 @@ if (!customElements.get("route-announcer")) {
const attrs = { const attrs = {
"aria-live": "assertive", "aria-live": "assertive",
"aria-atomic": "true", "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", "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",
} }

View File

@ -23,7 +23,8 @@ function toggleToc(this: HTMLElement) {
const content = this.nextElementSibling as HTMLElement | undefined const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.classList.toggle("collapsed") 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() { function setupToc() {
@ -44,6 +45,8 @@ document.addEventListener("nav", () => {
// update toc entry highlighting // update toc entry highlighting
observer.disconnect() 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)) headers.forEach((header) => observer.observe(header))
}) })

View File

@ -1,4 +1,7 @@
export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) { export function registerEscapeHandler(
outsideContainer: HTMLElement | null,
cb: () => void,
) {
if (!outsideContainer) return if (!outsideContainer) return
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
if (e.target !== this) return if (e.target !== this) return

View File

@ -143,7 +143,11 @@
} }
& .highlight { & .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; border-radius: 5px;
scroll-margin-top: 2rem; scroll-margin-top: 2rem;
} }

View File

@ -1,9 +1,9 @@
import { ComponentType, JSX } from "preact" import {ComponentType, JSX} from "preact"
import { StaticResources } from "../util/resources" import {StaticResources} from "../util/resources"
import { QuartzPluginData } from "../plugins/vfile" import {QuartzPluginData} from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg" import {GlobalConfiguration} from "../cfg"
import { Node } from "hast" import {Node} from "hast"
import { BuildCtx } from "../util/ctx" import {BuildCtx} from "../util/ctx"
export type QuartzComponentProps = { export type QuartzComponentProps = {
ctx: BuildCtx ctx: BuildCtx
@ -24,6 +24,6 @@ export type QuartzComponent = ComponentType<QuartzComponentProps> & {
afterDOMLoaded?: string afterDOMLoaded?: string
} }
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = ( export type QuartzComponentConstructor<
opts: Options, Options extends object | undefined = undefined,
) => QuartzComponent > = (opts: Options) => QuartzComponent

View File

@ -1,4 +1,4 @@
import { Translation, CalloutTranslation } from "./locales/definition" import {Translation, CalloutTranslation} from "./locales/definition"
import enUs from "./locales/en-US" import enUs from "./locales/en-US"
import enGb from "./locales/en-GB" import enGb from "./locales/en-GB"
import fr from "./locales/fr-FR" import fr from "./locales/fr-FR"
@ -65,6 +65,7 @@ export const TRANSLATIONS = {
} as const } as const
export const defaultTranslation = "en-US" 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 ValidLocale = keyof typeof TRANSLATIONS
export type ValidCallout = keyof CalloutTranslation export type ValidCallout = keyof CalloutTranslation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "آخر الملاحظات", title: "آخر الملاحظات",
seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`, seeRemainingMore: ({remaining}) => `تصفح ${remaining} أكثر →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`, transcludeOf: ({targetSlug}) => `مقتبس من ${targetSlug}`,
linkToOriginal: "وصلة للملاحظة الرئيسة", linkToOriginal: "وصلة للملاحظة الرئيسة",
}, },
search: { search: {
@ -54,7 +54,7 @@ export default {
title: "فهرس المحتويات", title: "فهرس المحتويات",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => readingTime: ({minutes}) =>
minutes == 1 minutes == 1
? `دقيقة أو أقل للقراءة` ? `دقيقة أو أقل للقراءة`
: minutes == 2 : minutes == 2
@ -65,7 +65,7 @@ export default {
pages: { pages: {
rss: { rss: {
recentNotes: "آخر الملاحظات", recentNotes: "آخر الملاحظات",
lastFewNotes: ({ count }) => `آخر ${count} ملاحظة`, lastFewNotes: ({count}) => `آخر ${count} ملاحظة`,
}, },
error: { error: {
title: "غير موجود", title: "غير موجود",
@ -74,16 +74,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "مجلد", folder: "مجلد",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`, count === 1
? "يوجد عنصر واحد فقط تحت هذا المجلد"
: `يوجد ${count} عناصر تحت هذا المجلد.`,
}, },
tagContent: { tagContent: {
tag: "الوسم", tag: "الوسم",
tagIndex: "مؤشر الوسم", tagIndex: "مؤشر الوسم",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`, count === 1
showingFirst: ({ count }) => `إظهار أول ${count} أوسمة.`, ? "يوجد عنصر واحد فقط تحت هذا الوسم"
totalTags: ({ count }) => `يوجد ${count} أوسمة.`, : `يوجد ${count} عناصر تحت هذا الوسم.`,
showingFirst: ({count}) => `إظهار أول ${count} أوسمة.`,
totalTags: ({count}) => `يوجد ${count} أوسمة.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Notes Recents", title: "Notes Recents",
seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`, seeRemainingMore: ({remaining}) => `Vegi ${remaining} més →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transcluit de ${targetSlug}`,
linkToOriginal: "Enllaç a l'original", linkToOriginal: "Enllaç a l'original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Taula de Continguts", title: "Taula de Continguts",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `Es llegeix en ${minutes} min`, readingTime: ({minutes}) => `Es llegeix en ${minutes} min`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Notes recents", recentNotes: "Notes recents",
lastFewNotes: ({ count }) => `Últimes ${count} notes`, lastFewNotes: ({count}) => `Últimes ${count} notes`,
}, },
error: { error: {
title: "No s'ha trobat.", title: "No s'ha trobat.",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Carpeta", folder: "Carpeta",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Etiqueta", tag: "Etiqueta",
tagIndex: "índex d'Etiquetes", tagIndex: "índex d'Etiquetes",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 article amb aquesta etiqueta." : `${count} article amb aquesta etiqueta.`, count === 1
showingFirst: ({ count }) => `Mostrant les primeres ${count} etiquetes.`, ? "1 article amb aquesta etiqueta."
totalTags: ({ count }) => `S'han trobat ${count} etiquetes en total.`, : `${count} article amb aquesta etiqueta.`,
showingFirst: ({count}) => `Mostrant les primeres ${count} etiquetes.`,
totalTags: ({count}) => `S'han trobat ${count} etiquetes en total.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Zuletzt bearbeitete Seiten", title: "Zuletzt bearbeitete Seiten",
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`, seeRemainingMore: ({remaining}) => `${remaining} weitere ansehen →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transklusion von ${targetSlug}`,
linkToOriginal: "Link zum Original", linkToOriginal: "Link zum Original",
}, },
search: { search: {
@ -54,31 +54,36 @@ export default {
title: "Inhaltsverzeichnis", title: "Inhaltsverzeichnis",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`, readingTime: ({minutes}) => `${minutes} min read`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Zuletzt bearbeitete Seiten", recentNotes: "Zuletzt bearbeitete Seiten",
lastFewNotes: ({ count }) => `Letzte ${count} Seiten`, lastFewNotes: ({count}) => `Letzte ${count} Seiten`,
}, },
error: { error: {
title: "Nicht gefunden", 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", home: "Return to Homepage",
}, },
folderContent: { folderContent: {
folder: "Ordner", folder: "Ordner",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Tag", tag: "Tag",
tagIndex: "Tag-Übersicht", tagIndex: "Tag-Übersicht",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`, count === 1
showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`, ? "1 Datei mit diesem Tag."
totalTags: ({ count }) => `${count} Tags insgesamt.`, : `${count} Dateien mit diesem Tag.`,
showingFirst: ({count}) => `Die ersten ${count} Tags werden angezeigt.`,
totalTags: ({count}) => `${count} Tags insgesamt.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { FullSlug } from "../../util/path" import {FullSlug} from "../../util/path"
export interface CalloutTranslation { export interface CalloutTranslation {
note: string note: string
@ -42,10 +42,10 @@ export interface Translation {
} }
recentNotes: { recentNotes: {
title: string title: string
seeRemainingMore: (variables: { remaining: number }) => string seeRemainingMore: (variables: {remaining: number}) => string
} }
transcludes: { transcludes: {
transcludeOf: (variables: { targetSlug: FullSlug }) => string transcludeOf: (variables: {targetSlug: FullSlug}) => string
linkToOriginal: string linkToOriginal: string
} }
search: { search: {
@ -56,13 +56,13 @@ export interface Translation {
title: string title: string
} }
contentMeta: { contentMeta: {
readingTime: (variables: { minutes: number }) => string readingTime: (variables: {minutes: number}) => string
} }
} }
pages: { pages: {
rss: { rss: {
recentNotes: string recentNotes: string
lastFewNotes: (variables: { count: number }) => string lastFewNotes: (variables: {count: number}) => string
} }
error: { error: {
title: string title: string
@ -71,14 +71,14 @@ export interface Translation {
} }
folderContent: { folderContent: {
folder: string folder: string
itemsUnderFolder: (variables: { count: number }) => string itemsUnderFolder: (variables: {count: number}) => string
} }
tagContent: { tagContent: {
tag: string tag: string
tagIndex: string tagIndex: string
itemsUnderTag: (variables: { count: number }) => string itemsUnderTag: (variables: {count: number}) => string
showingFirst: (variables: { count: number }) => string showingFirst: (variables: {count: number}) => string
totalTags: (variables: { count: number }) => string totalTags: (variables: {count: number}) => string
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Recent Notes", title: "Recent Notes",
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, seeRemainingMore: ({remaining}) => `See ${remaining} more →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transclude of ${targetSlug}`,
linkToOriginal: "Link to original", linkToOriginal: "Link to original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Table of Contents", title: "Table of Contents",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`, readingTime: ({minutes}) => `${minutes} min read`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Recent notes", recentNotes: "Recent notes",
lastFewNotes: ({ count }) => `Last ${count} notes`, lastFewNotes: ({count}) => `Last ${count} notes`,
}, },
error: { error: {
title: "Not Found", title: "Not Found",
@ -69,16 +69,18 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Folder", folder: "Folder",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Tag", tag: "Tag",
tagIndex: "Tag Index", tagIndex: "Tag Index",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 item with this tag." : `${count} items with this tag.`, count === 1 ? "1 item with this tag." : `${count} items with this tag.`,
showingFirst: ({ count }) => `Showing first ${count} tags.`, showingFirst: ({count}) => `Showing first ${count} tags.`,
totalTags: ({ count }) => `Found ${count} total tags.`, totalTags: ({count}) => `Found ${count} total tags.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Recent Notes", title: "Recent Notes",
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, seeRemainingMore: ({remaining}) => `See ${remaining} more →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transclude of ${targetSlug}`,
linkToOriginal: "Link to original", linkToOriginal: "Link to original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Table of Contents", title: "Table of Contents",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`, readingTime: ({minutes}) => `${minutes} min read`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Recent notes", recentNotes: "Recent notes",
lastFewNotes: ({ count }) => `Last ${count} notes`, lastFewNotes: ({count}) => `Last ${count} notes`,
}, },
error: { error: {
title: "Not Found", title: "Not Found",
@ -69,16 +69,18 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Folder", folder: "Folder",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Tag", tag: "Tag",
tagIndex: "Tag Index", tagIndex: "Tag Index",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 item with this tag." : `${count} items with this tag.`, count === 1 ? "1 item with this tag." : `${count} items with this tag.`,
showingFirst: ({ count }) => `Showing first ${count} tags.`, showingFirst: ({count}) => `Showing first ${count} tags.`,
totalTags: ({ count }) => `Found ${count} total tags.`, totalTags: ({count}) => `Found ${count} total tags.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Notas Recientes", title: "Notas Recientes",
seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`, seeRemainingMore: ({remaining}) => `Vea ${remaining} más →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transcluido de ${targetSlug}`,
linkToOriginal: "Enlace al original", linkToOriginal: "Enlace al original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Tabla de Contenidos", title: "Tabla de Contenidos",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `Se lee en ${minutes} min`, readingTime: ({minutes}) => `Se lee en ${minutes} min`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Notas recientes", recentNotes: "Notas recientes",
lastFewNotes: ({ count }) => `Últimas ${count} notas`, lastFewNotes: ({count}) => `Últimas ${count} notas`,
}, },
error: { error: {
title: "No se ha encontrado.", title: "No se ha encontrado.",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Carpeta", folder: "Carpeta",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Etiqueta", tag: "Etiqueta",
tagIndex: "Índice de Etiquetas", tagIndex: "Índice de Etiquetas",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`, count === 1
showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`, ? "1 artículo con esta etiqueta."
totalTags: ({ count }) => `Se han encontrado ${count} etiquetas en total.`, : `${count} artículos con esta etiqueta.`,
showingFirst: ({count}) => `Mostrando las primeras ${count} etiquetas.`,
totalTags: ({count}) => `Se han encontrado ${count} etiquetas en total.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "یادداشت‌های اخیر", title: "یادداشت‌های اخیر",
seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`, seeRemainingMore: ({remaining}) => `${remaining} یادداشت دیگر →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`, transcludeOf: ({targetSlug}) => `از ${targetSlug}`,
linkToOriginal: "پیوند به اصلی", linkToOriginal: "پیوند به اصلی",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "فهرست", title: "فهرست",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `زمان تقریبی مطالعه: ${minutes} دقیقه`, readingTime: ({minutes}) => `زمان تقریبی مطالعه: ${minutes} دقیقه`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "یادداشت‌های اخیر", recentNotes: "یادداشت‌های اخیر",
lastFewNotes: ({ count }) => `${count} یادداشت اخیر`, lastFewNotes: ({count}) => `${count} یادداشت اخیر`,
}, },
error: { error: {
title: "یافت نشد", title: "یافت نشد",
@ -69,16 +69,18 @@ export default {
}, },
folderContent: { folderContent: {
folder: "پوشه", folder: "پوشه",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`, count === 1
? ".یک مطلب در این پوشه است"
: `${count} مطلب در این پوشه است.`,
}, },
tagContent: { tagContent: {
tag: "برچسب", tag: "برچسب",
tagIndex: "فهرست برچسب‌ها", tagIndex: "فهرست برچسب‌ها",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`, count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`,
showingFirst: ({ count }) => `در حال نمایش ${count} برچسب.`, showingFirst: ({count}) => `در حال نمایش ${count} برچسب.`,
totalTags: ({ count }) => `${count} برچسب یافت شد.`, totalTags: ({count}) => `${count} برچسب یافت شد.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Notes Récentes", title: "Notes Récentes",
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`, seeRemainingMore: ({remaining}) => `Voir ${remaining} de plus →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transclusion de ${targetSlug}`,
linkToOriginal: "Lien vers l'original", linkToOriginal: "Lien vers l'original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Table des Matières", title: "Table des Matières",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min de lecture`, readingTime: ({minutes}) => `${minutes} min de lecture`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Notes récentes", recentNotes: "Notes récentes",
lastFewNotes: ({ count }) => `Les dernières ${count} notes`, lastFewNotes: ({count}) => `Les dernières ${count} notes`,
}, },
error: { error: {
title: "Introuvable", title: "Introuvable",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Dossier", folder: "Dossier",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Étiquette", tag: "Étiquette",
tagIndex: "Index des étiquettes", tagIndex: "Index des étiquettes",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`, count === 1
showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, ? "1 élément avec cette étiquette."
totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, : `${count} éléments avec cette étiquette.`,
showingFirst: ({count}) => `Affichage des premières ${count} étiquettes.`,
totalTags: ({count}) => `Trouvé ${count} étiquettes au total.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Legutóbbi jegyzetek", title: "Legutóbbi jegyzetek",
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`, seeRemainingMore: ({remaining}) => `${remaining} további megtekintése →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`, transcludeOf: ({targetSlug}) => `${targetSlug} áthivatkozása`,
linkToOriginal: "Hivatkozás az eredetire", linkToOriginal: "Hivatkozás az eredetire",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Tartalomjegyzék", title: "Tartalomjegyzék",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} perces olvasás`, readingTime: ({minutes}) => `${minutes} perces olvasás`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Legutóbbi jegyzetek", recentNotes: "Legutóbbi jegyzetek",
lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`, lastFewNotes: ({count}) => `Legutóbbi ${count} jegyzet`,
}, },
error: { error: {
title: "Nem található", title: "Nem található",
@ -69,14 +69,15 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Mappa", folder: "Mappa",
itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`, itemsUnderFolder: ({count}) =>
`Ebben a mappában ${count} elem található.`,
}, },
tagContent: { tagContent: {
tag: "Címke", tag: "Címke",
tagIndex: "Címke index", tagIndex: "Címke index",
itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`, itemsUnderTag: ({count}) => `${count} elem található ezzel a címkével.`,
showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`, showingFirst: ({count}) => `Első ${count} címke megjelenítve.`,
totalTags: ({ count }) => `Összesen ${count} címke található.`, totalTags: ({count}) => `Összesen ${count} címke található.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Note recenti", title: "Note recenti",
seeRemainingMore: ({ remaining }) => `Vedi ${remaining} altro →`, seeRemainingMore: ({remaining}) => `Vedi ${remaining} altro →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transclusione di ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transclusione di ${targetSlug}`,
linkToOriginal: "Link all'originale", linkToOriginal: "Link all'originale",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Tabella dei contenuti", title: "Tabella dei contenuti",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} minuti`, readingTime: ({minutes}) => `${minutes} minuti`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Note recenti", recentNotes: "Note recenti",
lastFewNotes: ({ count }) => `Ultime ${count} note`, lastFewNotes: ({count}) => `Ultime ${count} note`,
}, },
error: { error: {
title: "Non trovato", title: "Non trovato",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Cartella", folder: "Cartella",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Etichetta", tag: "Etichetta",
tagIndex: "Indice etichette", tagIndex: "Indice etichette",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`, count === 1
showingFirst: ({ count }) => `Prime ${count} etichette.`, ? "1 oggetto con questa etichetta."
totalTags: ({ count }) => `Trovate ${count} etichette totali.`, : `${count} oggetti con questa etichetta.`,
showingFirst: ({count}) => `Prime ${count} etichette.`,
totalTags: ({count}) => `Trovate ${count} etichette totali.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "最近の記事", title: "最近の記事",
seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`, seeRemainingMore: ({remaining}) => `さらに${remaining}件 →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`, transcludeOf: ({targetSlug}) => `${targetSlug}のまとめ`,
linkToOriginal: "元記事へのリンク", linkToOriginal: "元記事へのリンク",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "目次", title: "目次",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`, readingTime: ({minutes}) => `${minutes} min read`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "最近の記事", recentNotes: "最近の記事",
lastFewNotes: ({ count }) => `最新の${count}`, lastFewNotes: ({count}) => `最新の${count}`,
}, },
error: { error: {
title: "Not Found", title: "Not Found",
@ -69,14 +69,14 @@ export default {
}, },
folderContent: { folderContent: {
folder: "フォルダ", folder: "フォルダ",
itemsUnderFolder: ({ count }) => `${count}件のページ`, itemsUnderFolder: ({count}) => `${count}件のページ`,
}, },
tagContent: { tagContent: {
tag: "タグ", tag: "タグ",
tagIndex: "タグ一覧", tagIndex: "タグ一覧",
itemsUnderTag: ({ count }) => `${count}件のページ`, itemsUnderTag: ({count}) => `${count}件のページ`,
showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`, showingFirst: ({count}) => `のうち最初の${count}件を表示しています`,
totalTags: ({ count }) => `${count}個のタグを表示中`, totalTags: ({count}) => `${count}個のタグを表示中`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "최근 게시글", title: "최근 게시글",
seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`, seeRemainingMore: ({remaining}) => `${remaining}건 더보기 →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`, transcludeOf: ({targetSlug}) => `${targetSlug}의 포함`,
linkToOriginal: "원본 링크", linkToOriginal: "원본 링크",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "목차", title: "목차",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`, readingTime: ({minutes}) => `${minutes} min read`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "최근 게시글", recentNotes: "최근 게시글",
lastFewNotes: ({ count }) => `최근 ${count}`, lastFewNotes: ({count}) => `최근 ${count}`,
}, },
error: { error: {
title: "Not Found", title: "Not Found",
@ -69,14 +69,14 @@ export default {
}, },
folderContent: { folderContent: {
folder: "폴더", folder: "폴더",
itemsUnderFolder: ({ count }) => `${count}건의 항목`, itemsUnderFolder: ({count}) => `${count}건의 항목`,
}, },
tagContent: { tagContent: {
tag: "태그", tag: "태그",
tagIndex: "태그 목록", tagIndex: "태그 목록",
itemsUnderTag: ({ count }) => `${count}건의 항목`, itemsUnderTag: ({count}) => `${count}건의 항목`,
showingFirst: ({ count }) => `처음 ${count}개의 태그`, showingFirst: ({count}) => `처음 ${count}개의 태그`,
totalTags: ({ count }) => `${count}개의 태그를 찾았습니다.`, totalTags: ({count}) => `${count}개의 태그를 찾았습니다.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Recente notities", title: "Recente notities",
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`, seeRemainingMore: ({remaining}) => `Zie ${remaining} meer →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`, transcludeOf: ({targetSlug}) => `Invoeging van ${targetSlug}`,
linkToOriginal: "Link naar origineel", linkToOriginal: "Link naar origineel",
}, },
search: { search: {
@ -54,14 +54,14 @@ export default {
title: "Inhoudsopgave", title: "Inhoudsopgave",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => readingTime: ({minutes}) =>
minutes === 1 ? "1 minuut leestijd" : `${minutes} minuten leestijd`, minutes === 1 ? "1 minuut leestijd" : `${minutes} minuten leestijd`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Recente notities", recentNotes: "Recente notities",
lastFewNotes: ({ count }) => `Laatste ${count} notities`, lastFewNotes: ({count}) => `Laatste ${count} notities`,
}, },
error: { error: {
title: "Niet gevonden", title: "Niet gevonden",
@ -70,17 +70,17 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Map", folder: "Map",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
count === 1 ? "1 item in deze map." : `${count} items in deze map.`, count === 1 ? "1 item in deze map." : `${count} items in deze map.`,
}, },
tagContent: { tagContent: {
tag: "Label", tag: "Label",
tagIndex: "Label-index", tagIndex: "Label-index",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 item met dit label." : `${count} items met dit label.`, count === 1 ? "1 item met dit label." : `${count} items met dit label.`,
showingFirst: ({ count }) => showingFirst: ({count}) =>
count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`, count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`,
totalTags: ({ count }) => `${count} labels gevonden.`, totalTags: ({count}) => `${count} labels gevonden.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Najnowsze notatki", title: "Najnowsze notatki",
seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`, seeRemainingMore: ({remaining}) => `Zobacz ${remaining} nastepnych →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`, transcludeOf: ({targetSlug}) => `Osadzone ${targetSlug}`,
linkToOriginal: "Łącze do oryginału", linkToOriginal: "Łącze do oryginału",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Spis treści", title: "Spis treści",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} min. czytania `, readingTime: ({minutes}) => `${minutes} min. czytania `,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Najnowsze notatki", recentNotes: "Najnowsze notatki",
lastFewNotes: ({ count }) => `Ostatnie ${count} notatek`, lastFewNotes: ({count}) => `Ostatnie ${count} notatek`,
}, },
error: { error: {
title: "Nie znaleziono", title: "Nie znaleziono",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Folder", folder: "Folder",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Znacznik", tag: "Znacznik",
tagIndex: "Spis znaczników", tagIndex: "Spis znaczników",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`, count === 1
showingFirst: ({ count }) => `Pokazuje ${count} pierwszych znaczników.`, ? "Oznaczony 1 element."
totalTags: ({ count }) => `Znalezionych wszystkich znaczników: ${count}.`, : `Elementów z tym znacznikiem: ${count}.`,
showingFirst: ({count}) => `Pokazuje ${count} pierwszych znaczników.`,
totalTags: ({count}) => `Znalezionych wszystkich znaczników: ${count}.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Notas recentes", title: "Notas recentes",
seeRemainingMore: ({ remaining }) => `Veja mais ${remaining}`, seeRemainingMore: ({remaining}) => `Veja mais ${remaining}`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`, transcludeOf: ({targetSlug}) => `Transcrever de ${targetSlug}`,
linkToOriginal: "Link ao original", linkToOriginal: "Link ao original",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Sumário", title: "Sumário",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `Leitura de ${minutes} min`, readingTime: ({minutes}) => `Leitura de ${minutes} min`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Notas recentes", recentNotes: "Notas recentes",
lastFewNotes: ({ count }) => `Últimas ${count} notas`, lastFewNotes: ({count}) => `Últimas ${count} notas`,
}, },
error: { error: {
title: "Não encontrado", title: "Não encontrado",
@ -69,16 +69,16 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Arquivo", folder: "Arquivo",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
count === 1 ? "1 item neste arquivo." : `${count} items neste arquivo.`, count === 1 ? "1 item neste arquivo." : `${count} items neste arquivo.`,
}, },
tagContent: { tagContent: {
tag: "Tag", tag: "Tag",
tagIndex: "Sumário de Tags", tagIndex: "Sumário de Tags",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`, count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`,
showingFirst: ({ count }) => `Mostrando as ${count} primeiras tags.`, showingFirst: ({count}) => `Mostrando as ${count} primeiras tags.`,
totalTags: ({ count }) => `Encontradas ${count} tags.`, totalTags: ({count}) => `Encontradas ${count} tags.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Notițe recente", title: "Notițe recente",
seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining}`, seeRemainingMore: ({remaining}) => `Vezi încă ${remaining}`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`, transcludeOf: ({targetSlug}) => `Extras din ${targetSlug}`,
linkToOriginal: "Legătură către original", linkToOriginal: "Legătură către original",
}, },
search: { search: {
@ -54,14 +54,14 @@ export default {
title: "Cuprins", title: "Cuprins",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => readingTime: ({minutes}) =>
minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`, minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Notițe recente", recentNotes: "Notițe recente",
lastFewNotes: ({ count }) => `Ultimele ${count} notițe`, lastFewNotes: ({count}) => `Ultimele ${count} notițe`,
}, },
error: { error: {
title: "Pagina nu a fost găsită", title: "Pagina nu a fost găsită",
@ -70,16 +70,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Dosar", folder: "Dosar",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Etichetă", tag: "Etichetă",
tagIndex: "Indexul etichetelor", tagIndex: "Indexul etichetelor",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 articol cu această etichetă." : `${count} articole cu această etichetă.`, count === 1
showingFirst: ({ count }) => `Se afișează primele ${count} etichete.`, ? "1 articol cu această etichetă."
totalTags: ({ count }) => `Au fost găsite ${count} etichete în total.`, : `${count} articole cu această etichetă.`,
showingFirst: ({count}) => `Se afișează primele ${count} etichete.`,
totalTags: ({count}) => `Au fost găsite ${count} etichete în total.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,11 +40,11 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Недавние заметки", title: "Недавние заметки",
seeRemainingMore: ({ remaining }) => seeRemainingMore: ({remaining}) =>
`Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining}`, `Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining}`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`, transcludeOf: ({targetSlug}) => `Переход из ${targetSlug}`,
linkToOriginal: "Ссылка на оригинал", linkToOriginal: "Ссылка на оригинал",
}, },
search: { search: {
@ -55,13 +55,13 @@ export default {
title: "Оглавление", title: "Оглавление",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `время чтения ~${minutes} мин.`, readingTime: ({minutes}) => `время чтения ~${minutes} мин.`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Недавние заметки", recentNotes: "Недавние заметки",
lastFewNotes: ({ count }) => lastFewNotes: ({count}) =>
`Последн${getForm(count, "яя", "ие", "ие")} ${count} замет${getForm(count, "ка", "ки", "ок")}`, `Последн${getForm(count, "яя", "ие", "ие")} ${count} замет${getForm(count, "ка", "ки", "ок")}`,
}, },
error: { error: {
@ -71,21 +71,28 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Папка", folder: "Папка",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
`в этой папке ${count} элемент${getForm(count, "", "а", "ов")}`, `в этой папке ${count} элемент${getForm(count, "", "а", "ов")}`,
}, },
tagContent: { tagContent: {
tag: "Тег", tag: "Тег",
tagIndex: "Индекс тегов", tagIndex: "Индекс тегов",
itemsUnderTag: ({ count }) => `с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`, itemsUnderTag: ({count}) =>
showingFirst: ({ count }) => `с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`,
showingFirst: ({count}) =>
`Показыва${getForm(count, "ется", "ются", "ются")} ${count} тег${getForm(count, "", "а", "ов")}`, `Показыва${getForm(count, "ется", "ются", "ются")} ${count} тег${getForm(count, "", "а", "ов")}`,
totalTags: ({ count }) => `Всего ${count} тег${getForm(count, "", "а", "ов")}`, totalTags: ({count}) =>
`Всего ${count} тег${getForm(count, "", "а", "ов")}`,
}, },
}, },
} as const satisfies Translation } 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 remainder100 = number % 100
const remainder10 = remainder100 % 10 const remainder10 = remainder100 % 10

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Останні нотатки", title: "Останні нотатки",
seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining}`, seeRemainingMore: ({remaining}) => `Переглянути ще ${remaining}`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`, transcludeOf: ({targetSlug}) => `Видобуто з ${targetSlug}`,
linkToOriginal: "Посилання на оригінал", linkToOriginal: "Посилання на оригінал",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Зміст", title: "Зміст",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes} хв читання`, readingTime: ({minutes}) => `${minutes} хв читання`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Останні нотатки", recentNotes: "Останні нотатки",
lastFewNotes: ({ count }) => `Останні нотатки: ${count}`, lastFewNotes: ({count}) => `Останні нотатки: ${count}`,
}, },
error: { error: {
title: "Не знайдено", title: "Не знайдено",
@ -69,16 +69,20 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Тека", folder: "Тека",
itemsUnderFolder: ({ count }) => itemsUnderFolder: ({count}) =>
count === 1 ? "У цій теці 1 елемент." : `Елементів у цій теці: ${count}.`, count === 1
? "У цій теці 1 елемент."
: `Елементів у цій теці: ${count}.`,
}, },
tagContent: { tagContent: {
tag: "Мітка", tag: "Мітка",
tagIndex: "Індекс мітки", tagIndex: "Індекс мітки",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 елемент з цією міткою." : `Елементів з цією міткою: ${count}.`, count === 1
showingFirst: ({ count }) => `Показ перших ${count} міток.`, ? "1 елемент з цією міткою."
totalTags: ({ count }) => `Всього знайдено міток: ${count}.`, : `Елементів з цією міткою: ${count}.`,
showingFirst: ({count}) => `Показ перших ${count} міток.`,
totalTags: ({count}) => `Всього знайдено міток: ${count}.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "Bài viết gần đây", title: "Bài viết gần đây",
seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`, seeRemainingMore: ({remaining}) => `Xem ${remaining} thêm →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`, transcludeOf: ({targetSlug}) => `Bao gồm ${targetSlug}`,
linkToOriginal: "Liên Kết Gốc", linkToOriginal: "Liên Kết Gốc",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "Bảng Nội Dung", title: "Bảng Nội Dung",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `đọc ${minutes} phút`, readingTime: ({minutes}) => `đọc ${minutes} phút`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "Những bài gần đây", recentNotes: "Những bài gần đây",
lastFewNotes: ({ count }) => `${count} Bài gần đây`, lastFewNotes: ({count}) => `${count} Bài gần đây`,
}, },
error: { error: {
title: "Không Tìm Thấy", title: "Không Tìm Thấy",
@ -69,16 +69,18 @@ export default {
}, },
folderContent: { folderContent: {
folder: "Thư Mục", folder: "Thư Mục",
itemsUnderFolder: ({ count }) => 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: { tagContent: {
tag: "Thẻ", tag: "Thẻ",
tagIndex: "Thẻ Mục Lục", tagIndex: "Thẻ Mục Lục",
itemsUnderTag: ({ count }) => itemsUnderTag: ({count}) =>
count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`, count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`,
showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`, showingFirst: ({count}) => `Hiển thị trước ${count} thẻ.`,
totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`, totalTags: ({count}) => `Tìm thấy ${count} thẻ tổng cộng.`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,4 +1,4 @@
import { Translation } from "./definition" import {Translation} from "./definition"
export default { export default {
propertyDefaults: { propertyDefaults: {
@ -40,10 +40,10 @@ export default {
}, },
recentNotes: { recentNotes: {
title: "最近的笔记", title: "最近的笔记",
seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`, seeRemainingMore: ({remaining}) => `查看更多${remaining}篇笔记 →`,
}, },
transcludes: { transcludes: {
transcludeOf: ({ targetSlug }) => `包含${targetSlug}`, transcludeOf: ({targetSlug}) => `包含${targetSlug}`,
linkToOriginal: "指向原始笔记的链接", linkToOriginal: "指向原始笔记的链接",
}, },
search: { search: {
@ -54,13 +54,13 @@ export default {
title: "目录", title: "目录",
}, },
contentMeta: { contentMeta: {
readingTime: ({ minutes }) => `${minutes}分钟阅读`, readingTime: ({minutes}) => `${minutes}分钟阅读`,
}, },
}, },
pages: { pages: {
rss: { rss: {
recentNotes: "最近的笔记", recentNotes: "最近的笔记",
lastFewNotes: ({ count }) => `最近的${count}条笔记`, lastFewNotes: ({count}) => `最近的${count}条笔记`,
}, },
error: { error: {
title: "无法找到", title: "无法找到",
@ -69,14 +69,14 @@ export default {
}, },
folderContent: { folderContent: {
folder: "文件夹", folder: "文件夹",
itemsUnderFolder: ({ count }) => `此文件夹下有${count}条笔记。`, itemsUnderFolder: ({count}) => `此文件夹下有${count}条笔记。`,
}, },
tagContent: { tagContent: {
tag: "标签", tag: "标签",
tagIndex: "标签索引", tagIndex: "标签索引",
itemsUnderTag: ({ count }) => `此标签下有${count}条笔记。`, itemsUnderTag: ({count}) => `此标签下有${count}条笔记。`,
showingFirst: ({ count }) => `显示前${count}个标签。`, showingFirst: ({count}) => `显示前${count}个标签。`,
totalTags: ({ count }) => `总共有${count}个标签。`, totalTags: ({count}) => `总共有${count}个标签。`,
}, },
}, },
} as const satisfies Translation } as const satisfies Translation

View File

@ -1,14 +1,14 @@
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import { QuartzComponentProps } from "../../components/types" import {QuartzComponentProps} from "../../components/types"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import {pageResources, renderPage} from "../../components/renderPage"
import { FullPageLayout } from "../../cfg" import {FullPageLayout} from "../../cfg"
import { FilePath, FullSlug } from "../../util/path" import {FilePath, FullSlug} from "../../util/path"
import { sharedPageComponents } from "../../../quartz.layout" import {sharedPageComponents} from "../../../quartz.layout"
import { NotFound } from "../../components" import {NotFound} from "../../components"
import { defaultProcessedContent } from "../vfile" import {defaultProcessedContent} from "../vfile"
import { write } from "./helpers" import {write} from "./helpers"
import { i18n } from "../../i18n" import {i18n} from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export const NotFoundPage: QuartzEmitterPlugin = () => { export const NotFoundPage: QuartzEmitterPlugin = () => {
@ -20,7 +20,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
right: [], right: [],
} }
const { head: Head, pageBody, footer: Footer } = opts const {head: Head, pageBody, footer: Footer} = opts
const Body = BodyConstructor() const Body = BodyConstructor()
return { return {
@ -43,7 +43,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
slug, slug,
text: notFound, text: notFound,
description: notFound, description: notFound,
frontmatter: { title: notFound, tags: [] }, frontmatter: {title: notFound, tags: []},
}) })
const componentData: QuartzComponentProps = { const componentData: QuartzComponentProps = {
ctx, ctx,
@ -58,7 +58,13 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
return [ return [
await write({ await write({
ctx, ctx,
content: renderPage(cfg, slug, componentData, opts, externalResources), content: renderPage(
cfg,
slug,
componentData,
opts,
externalResources,
),
slug, slug,
ext: ".html", ext: ".html",
}), }),

View File

@ -1,7 +1,13 @@
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path" import {
import { QuartzEmitterPlugin } from "../types" FilePath,
FullSlug,
joinSegments,
resolveRelative,
simplifySlug,
} from "../../util/path"
import {QuartzEmitterPlugin} from "../types"
import path from "path" import path from "path"
import { write } from "./helpers" import {write} from "./helpers"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export const AliasRedirects: QuartzEmitterPlugin = () => ({ export const AliasRedirects: QuartzEmitterPlugin = () => ({
@ -12,11 +18,16 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
async getDependencyGraph(ctx, content, _resources) { async getDependencyGraph(ctx, content, _resources) {
const graph = new DepGraph<FilePath>() const graph = new DepGraph<FilePath>()
const { argv } = ctx const {argv} = ctx
for (const [_tree, file] of content) { 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 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 const permalink = file.data.frontmatter?.permalink
if (typeof permalink === "string") { if (typeof permalink === "string") {
slugs.push(permalink as FullSlug) slugs.push(permalink as FullSlug)
@ -28,21 +39,29 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
slug = joinSegments(slug, "index") as FullSlug 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,
)
} }
} }
return graph return graph
}, },
async emit(ctx, content, _resources): Promise<FilePath[]> { async emit(ctx, content, _resources): Promise<FilePath[]> {
const { argv } = ctx const {argv} = ctx
const fps: FilePath[] = [] const fps: FilePath[] = []
for (const [_tree, file] of content) { for (const [_tree, file] of content) {
const ogSlug = simplifySlug(file.data.slug!) 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 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 const permalink = file.data.frontmatter?.permalink
if (typeof permalink === "string") { if (typeof permalink === "string") {
slugs.push(permalink as FullSlug) slugs.push(permalink as FullSlug)

View File

@ -1,15 +1,18 @@
import { FilePath, joinSegments, slugifyFilePath } from "../../util/path" import {FilePath, joinSegments, slugifyFilePath} from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import path from "path" import path from "path"
import fs from "fs" import fs from "fs"
import { glob } from "../../util/glob" import {glob} from "../../util/glob"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
import { Argv } from "../../util/ctx" import {Argv} from "../../util/ctx"
import { QuartzConfig } from "../../cfg" import {QuartzConfig} from "../../cfg"
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => { const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
// glob all non MD files in content folder and copy it over // 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 = () => { export const Assets: QuartzEmitterPlugin = () => {
@ -19,7 +22,7 @@ export const Assets: QuartzEmitterPlugin = () => {
return [] return []
}, },
async getDependencyGraph(ctx, _content, _resources) { async getDependencyGraph(ctx, _content, _resources) {
const { argv, cfg } = ctx const {argv, cfg} = ctx
const graph = new DepGraph<FilePath>() const graph = new DepGraph<FilePath>()
const fps = await filesToCopy(argv, cfg) const fps = await filesToCopy(argv, cfg)
@ -36,7 +39,7 @@ export const Assets: QuartzEmitterPlugin = () => {
return graph return graph
}, },
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> { async emit({argv, cfg}, _content, _resources): Promise<FilePath[]> {
const assetsPath = argv.output const assetsPath = argv.output
const fps = await filesToCopy(argv, cfg) const fps = await filesToCopy(argv, cfg)
const res: FilePath[] = [] const res: FilePath[] = []
@ -47,7 +50,7 @@ export const Assets: QuartzEmitterPlugin = () => {
const dest = joinSegments(assetsPath, name) as FilePath const dest = joinSegments(assetsPath, name) as FilePath
const dir = path.dirname(dest) as FilePath const dir = path.dirname(dest) as FilePath
await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists await fs.promises.mkdir(dir, {recursive: true}) // ensure dir exists
await fs.promises.copyFile(src, dest) await fs.promises.copyFile(src, dest)
res.push(dest) res.push(dest)
} }

View File

@ -1,5 +1,5 @@
import { FilePath, joinSegments } from "../../util/path" import {FilePath, joinSegments} from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import fs from "fs" import fs from "fs"
import chalk from "chalk" import chalk from "chalk"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
@ -17,9 +17,13 @@ export const CNAME: QuartzEmitterPlugin = () => ({
async getDependencyGraph(_ctx, _content, _resources) { async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>() return new DepGraph<FilePath>()
}, },
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> { async emit({argv, cfg}, _content, _resources): Promise<FilePath[]> {
if (!cfg.configuration.baseUrl) { 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 [] return []
} }
const path = joinSegments(argv.output, "CNAME") const path = joinSegments(argv.output, "CNAME")

View File

@ -1,5 +1,5 @@
import { FilePath, FullSlug, joinSegments } from "../../util/path" import {FilePath, FullSlug, joinSegments} from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
// @ts-ignore // @ts-ignore
import spaRouterScript from "../../components/scripts/spa.inline" import spaRouterScript from "../../components/scripts/spa.inline"
@ -7,12 +7,12 @@ import spaRouterScript from "../../components/scripts/spa.inline"
import popoverScript from "../../components/scripts/popover.inline" import popoverScript from "../../components/scripts/popover.inline"
import styles from "../../styles/custom.scss" import styles from "../../styles/custom.scss"
import popoverStyle from "../../components/styles/popover.scss" import popoverStyle from "../../components/styles/popover.scss"
import { BuildCtx } from "../../util/ctx" import {BuildCtx} from "../../util/ctx"
import { QuartzComponent } from "../../components/types" import {QuartzComponent} from "../../components/types"
import { googleFontHref, joinStyles } from "../../util/theme" import {googleFontHref, joinStyles} from "../../util/theme"
import { Features, transform } from "lightningcss" import {Features, transform} from "lightningcss"
import { transform as transpile } from "esbuild" import {transform as transpile} from "esbuild"
import { write } from "./helpers" import {write} from "./helpers"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
type ComponentResources = { type ComponentResources = {
@ -37,7 +37,7 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
} }
for (const component of allComponents) { for (const component of allComponents) {
const { css, beforeDOMLoaded, afterDOMLoaded } = component const {css, beforeDOMLoaded, afterDOMLoaded} = component
if (css) { if (css) {
componentResources.css.add(css) componentResources.css.add(css)
} }
@ -58,7 +58,9 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
async function joinScripts(scripts: string[]): Promise<string> { async function joinScripts(scripts: string[]): Promise<string> {
// wrap with iife to prevent scope collision // 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 // minify with esbuild
const res = await transpile(script, { const res = await transpile(script, {
@ -68,7 +70,10 @@ async function joinScripts(scripts: string[]): Promise<string> {
return res.code return res.code
} }
function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) { function addGlobalPageResources(
ctx: BuildCtx,
componentResources: ComponentResources,
) {
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration
// popovers // popovers
@ -185,11 +190,15 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
let googleFontsStyleSheet = "" let googleFontsStyleSheet = ""
if (cfg.theme.fontOrigin === "local") { if (cfg.theme.fontOrigin === "local") {
// let the user do it themselves in css // 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 // when cdnCaching is true, we link to google fonts in Head.tsx
let match 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 ( googleFontsStyleSheet = await (
await fetch(googleFontHref(ctx.cfg.configuration.theme)) await fetch(googleFontHref(ctx.cfg.configuration.theme))

View File

@ -1,12 +1,18 @@
import { Root } from "hast" import {Root} from "hast"
import { GlobalConfiguration } from "../../cfg" import {GlobalConfiguration} from "../../cfg"
import { getDate } from "../../components/Date" import {getDate} from "../../components/Date"
import { escapeHTML } from "../../util/escape" import {escapeHTML} from "../../util/escape"
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" import {
import { QuartzEmitterPlugin } from "../types" FilePath,
import { toHtml } from "hast-util-to-html" FullSlug,
import { write } from "./helpers" SimpleSlug,
import { i18n } from "../../i18n" joinSegments,
simplifySlug,
} from "../../util/path"
import {QuartzEmitterPlugin} from "../types"
import {toHtml} from "hast-util-to-html"
import {write} from "./helpers"
import {i18n} from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export type ContentIndex = Map<FullSlug, ContentDetails> export type ContentIndex = Map<FullSlug, ContentDetails>
@ -38,7 +44,10 @@ const defaultOptions: Options = {
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
const base = cfg.baseUrl ?? "" const base = cfg.baseUrl ?? ""
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url> const createURLEntry = (
slug: SimpleSlug,
content: ContentDetails,
): string => `<url>
<loc>https://${joinSegments(base, encodeURI(slug))}</loc> <loc>https://${joinSegments(base, encodeURI(slug))}</loc>
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`} ${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
</url>` </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>` 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 base = cfg.baseUrl ?? ""
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item> const createURLEntry = (
slug: SimpleSlug,
content: ContentDetails,
): string => `<item>
<title>${escapeHTML(content.title)}</title> <title>${escapeHTML(content.title)}</title>
<link>https://${joinSegments(base, encodeURI(slug))}</link> <link>https://${joinSegments(base, encodeURI(slug))}</link>
<guid>https://${joinSegments(base, encodeURI(slug))}</guid> <guid>https://${joinSegments(base, encodeURI(slug))}</guid>
@ -80,7 +96,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
<channel> <channel>
<title>${escapeHTML(cfg.pageTitle)}</title> <title>${escapeHTML(cfg.pageTitle)}</title>
<link>https://${base}</link> <link>https://${base}</link>
<description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML( <description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({count: limit}) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
cfg.pageTitle, cfg.pageTitle,
)}</description> )}</description>
<generator>Quartz -- quartz.jzhao.xyz</generator> <generator>Quartz -- quartz.jzhao.xyz</generator>
@ -90,7 +106,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
} }
export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
opts = { ...defaultOptions, ...opts } opts = {...defaultOptions, ...opts}
return { return {
name: "ContentIndex", name: "ContentIndex",
async getDependencyGraph(ctx, content, _resources) { async getDependencyGraph(ctx, content, _resources) {
@ -104,10 +120,16 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath, joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath,
) )
if (opts?.enableSiteMap) { 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) { 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,14 +142,17 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
for (const [tree, file] of content) { for (const [tree, file] of content) {
const slug = file.data.slug! const slug = file.data.slug!
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() 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, { linkIndex.set(slug, {
title: file.data.frontmatter?.title!, title: file.data.frontmatter?.title!,
links: file.data.links ?? [], links: file.data.links ?? [],
tags: file.data.frontmatter?.tags ?? [], tags: file.data.frontmatter?.tags ?? [],
content: file.data.text ?? "", content: file.data.text ?? "",
richContent: opts?.rssFullHtml richContent: opts?.rssFullHtml
? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) ? escapeHTML(toHtml(tree as Root, {allowDangerousHtml: true}))
: undefined, : undefined,
date: date, date: date,
description: file.data.description ?? "", description: file.data.description ?? "",

View File

@ -1,19 +1,27 @@
import path from "path" import path from "path"
import { visit } from "unist-util-visit" import {visit} from "unist-util-visit"
import { Root } from "hast" import {Root} from "hast"
import { VFile } from "vfile" import {VFile} from "vfile"
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import { QuartzComponentProps } from "../../components/types" import {QuartzComponentProps} from "../../components/types"
import HeaderConstructor from "../../components/Header" import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import {pageResources, renderPage} from "../../components/renderPage"
import { FullPageLayout } from "../../cfg" import {FullPageLayout} from "../../cfg"
import { Argv } from "../../util/ctx" import {Argv} from "../../util/ctx"
import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path" import {
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" FilePath,
import { Content } from "../../components" isRelativeURL,
joinSegments,
pathToRoot,
} from "../../util/path"
import {
defaultContentPageLayout,
sharedPageComponents,
} from "../../../quartz.layout"
import {Content} from "../../components"
import chalk from "chalk" import chalk from "chalk"
import { write } from "./helpers" import {write} from "./helpers"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
// get all the dependencies for the markdown file // get all the dependencies for the markdown file
@ -25,7 +33,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
let ref: string | null = null let ref: string | null = null
if ( if (
["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) && ["script", "img", "audio", "video", "source", "iframe"].includes(
elem.tagName,
) &&
elem?.properties?.src elem?.properties?.src
) { ) {
ref = elem.properties.src.toString() ref = elem.properties.src.toString()
@ -40,7 +50,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
return 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 // markdown files have the .md extension stripped in hrefs, add it back here
if (!fp.split("/").pop()?.includes(".")) { if (!fp.split("/").pop()?.includes(".")) {
fp += ".md" fp += ".md"
@ -51,7 +63,9 @@ const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
return dependencies return dependencies
} }
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (
userOpts,
) => {
const opts: FullPageLayout = { const opts: FullPageLayout = {
...sharedPageComponents, ...sharedPageComponents,
...defaultContentPageLayout, ...defaultContentPageLayout,
@ -59,7 +73,16 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
...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 Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
@ -85,7 +108,10 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
for (const [tree, file] of content) { for (const [tree, file] of content) {
const sourcePath = file.data.filePath! const sourcePath = file.data.filePath!
const slug = file.data.slug! 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) => { parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => {
graph.addEdge(dep as FilePath, sourcePath) graph.addEdge(dep as FilePath, sourcePath)
@ -117,7 +143,13 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
allFiles, allFiles,
} }
const content = renderPage(cfg, slug, componentData, opts, externalResources) const content = renderPage(
cfg,
slug,
componentData,
opts,
externalResources,
)
const fp = await write({ const fp = await write({
ctx, ctx,
content, content,

View File

@ -1,10 +1,14 @@
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import { QuartzComponentProps } from "../../components/types" import {QuartzComponentProps} from "../../components/types"
import HeaderConstructor from "../../components/Header" import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import {pageResources, renderPage} from "../../components/renderPage"
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import {
import { FullPageLayout } from "../../cfg" ProcessedContent,
QuartzPluginData,
defaultProcessedContent,
} from "../vfile"
import {FullPageLayout} from "../../cfg"
import path from "path" import path from "path"
import { import {
FilePath, FilePath,
@ -15,25 +19,39 @@ import {
pathToRoot, pathToRoot,
simplifySlug, simplifySlug,
} from "../../util/path" } from "../../util/path"
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" import {
import { FolderContent } from "../../components" defaultListPageLayout,
import { write } from "./helpers" sharedPageComponents,
import { i18n } from "../../i18n" } from "../../../quartz.layout"
import {FolderContent} from "../../components"
import {write} from "./helpers"
import {i18n} from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
interface FolderPageOptions extends FullPageLayout { interface FolderPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
} }
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (userOpts) => { export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (
userOpts,
) => {
const opts: FullPageLayout = { const opts: FullPageLayout = {
...sharedPageComponents, ...sharedPageComponents,
...defaultListPageLayout, ...defaultListPageLayout,
pageBody: FolderContent({ sort: userOpts?.sort }), pageBody: FolderContent({sort: userOpts?.sort}),
...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 Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
@ -63,7 +81,10 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
const slug = vfile.data.slug const slug = vfile.data.slug
const folderName = path.dirname(slug ?? "") as SimpleSlug const folderName = path.dirname(slug ?? "") as SimpleSlug
if (slug && folderName !== "." && folderName !== "tags") { 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) => [ [...folders].map((folder) => [
folder, folder,
defaultProcessedContent({ defaultProcessedContent({
@ -119,7 +141,13 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
allFiles, allFiles,
} }
const content = renderPage(cfg, slug, componentData, opts, externalResources) const content = renderPage(
cfg,
slug,
componentData,
opts,
externalResources,
)
const fp = await write({ const fp = await write({
ctx, ctx,
content, content,

View File

@ -1,7 +1,7 @@
import path from "path" import path from "path"
import fs from "fs" import fs from "fs"
import { BuildCtx } from "../../util/ctx" import {BuildCtx} from "../../util/ctx"
import { FilePath, FullSlug, joinSegments } from "../../util/path" import {FilePath, FullSlug, joinSegments} from "../../util/path"
type WriteOptions = { type WriteOptions = {
ctx: BuildCtx ctx: BuildCtx
@ -10,10 +10,15 @@ type WriteOptions = {
content: string | Buffer 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 pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
const dir = path.dirname(pathToPage) const dir = path.dirname(pathToPage)
await fs.promises.mkdir(dir, { recursive: true }) await fs.promises.mkdir(dir, {recursive: true})
await fs.promises.writeFile(pathToPage, content) await fs.promises.writeFile(pathToPage, content)
return pathToPage return pathToPage
} }

View File

@ -1,10 +1,10 @@
export { ContentPage } from "./contentPage" export {ContentPage} from "./contentPage"
export { TagPage } from "./tagPage" export {TagPage} from "./tagPage"
export { FolderPage } from "./folderPage" export {FolderPage} from "./folderPage"
export { ContentIndex } from "./contentIndex" export {ContentIndex} from "./contentIndex"
export { AliasRedirects } from "./aliases" export {AliasRedirects} from "./aliases"
export { Assets } from "./assets" export {Assets} from "./assets"
export { Static } from "./static" export {Static} from "./static"
export { ComponentResources } from "./componentResources" export {ComponentResources} from "./componentResources"
export { NotFoundPage } from "./404" export {NotFoundPage} from "./404"
export { CNAME } from "./cname" export {CNAME} from "./cname"

View File

@ -1,7 +1,7 @@
import { FilePath, QUARTZ, joinSegments } from "../../util/path" import {FilePath, QUARTZ, joinSegments} from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import fs from "fs" import fs from "fs"
import { glob } from "../../util/glob" import {glob} from "../../util/glob"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export const Static: QuartzEmitterPlugin = () => ({ export const Static: QuartzEmitterPlugin = () => ({
@ -9,7 +9,7 @@ export const Static: QuartzEmitterPlugin = () => ({
getQuartzComponents() { getQuartzComponents() {
return [] return []
}, },
async getDependencyGraph({ argv, cfg }, _content, _resources) { async getDependencyGraph({argv, cfg}, _content, _resources) {
const graph = new DepGraph<FilePath>() const graph = new DepGraph<FilePath>()
const staticPath = joinSegments(QUARTZ, "static") const staticPath = joinSegments(QUARTZ, "static")
@ -23,13 +23,15 @@ export const Static: QuartzEmitterPlugin = () => ({
return graph return graph
}, },
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> { async emit({argv, cfg}, _content, _resources): Promise<FilePath[]> {
const staticPath = joinSegments(QUARTZ, "static") const staticPath = joinSegments(QUARTZ, "static")
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
recursive: true, recursive: true,
dereference: 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[]
}, },
}) })

View File

@ -1,10 +1,14 @@
import { QuartzEmitterPlugin } from "../types" import {QuartzEmitterPlugin} from "../types"
import { QuartzComponentProps } from "../../components/types" import {QuartzComponentProps} from "../../components/types"
import HeaderConstructor from "../../components/Header" import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import {pageResources, renderPage} from "../../components/renderPage"
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import {
import { FullPageLayout } from "../../cfg" ProcessedContent,
QuartzPluginData,
defaultProcessedContent,
} from "../vfile"
import {FullPageLayout} from "../../cfg"
import { import {
FilePath, FilePath,
FullSlug, FullSlug,
@ -12,25 +16,39 @@ import {
joinSegments, joinSegments,
pathToRoot, pathToRoot,
} from "../../util/path" } from "../../util/path"
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" import {
import { TagContent } from "../../components" defaultListPageLayout,
import { write } from "./helpers" sharedPageComponents,
import { i18n } from "../../i18n" } from "../../../quartz.layout"
import {TagContent} from "../../components"
import {write} from "./helpers"
import {i18n} from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
interface TagPageOptions extends FullPageLayout { interface TagPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
} }
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts) => { export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (
userOpts,
) => {
const opts: FullPageLayout = { const opts: FullPageLayout = {
...sharedPageComponents, ...sharedPageComponents,
...defaultListPageLayout, ...defaultListPageLayout,
pageBody: TagContent({ sort: userOpts?.sort }), pageBody: TagContent({sort: userOpts?.sort}),
...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 Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
@ -55,7 +73,9 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
for (const [_tree, file] of content) { for (const [_tree, file] of content) {
const sourcePath = file.data.filePath! 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 the file has at least one tag, it is used in the tag index page
if (tags.length > 0) { if (tags.length > 0) {
tags.push("index") tags.push("index")
@ -77,13 +97,16 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration
const tags: Set<string> = new Set( const tags: Set<string> = new Set(
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), allFiles
.flatMap((data) => data.frontmatter?.tags ?? [])
.flatMap(getAllSegmentPrefixes),
) )
// add base tag // add base tag
tags.add("index") tags.add("index")
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries( const tagDescriptions: Record<string, ProcessedContent> =
Object.fromEntries(
[...tags].map((tag) => { [...tags].map((tag) => {
const title = const title =
tag === "index" tag === "index"
@ -93,7 +116,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
tag, tag,
defaultProcessedContent({ defaultProcessedContent({
slug: joinSegments("tags", tag) as FullSlug, slug: joinSegments("tags", tag) as FullSlug,
frontmatter: { title, tags: [] }, frontmatter: {title, tags: []},
}), }),
] ]
}), }),
@ -123,7 +146,13 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
allFiles, allFiles,
} }
const content = renderPage(cfg, slug, componentData, opts, externalResources) const content = renderPage(
cfg,
slug,
componentData,
opts,
externalResources,
)
const fp = await write({ const fp = await write({
ctx, ctx,
content, content,

View File

@ -1,4 +1,4 @@
import { QuartzFilterPlugin } from "../types" import {QuartzFilterPlugin} from "../types"
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
name: "RemoveDrafts", name: "RemoveDrafts",

View File

@ -1,4 +1,4 @@
import { QuartzFilterPlugin } from "../types" import {QuartzFilterPlugin} from "../types"
export const ExplicitPublish: QuartzFilterPlugin = () => ({ export const ExplicitPublish: QuartzFilterPlugin = () => ({
name: "ExplicitPublish", name: "ExplicitPublish",

View File

@ -1,2 +1,2 @@
export { RemoveDrafts } from "./draft" export {RemoveDrafts} from "./draft"
export { ExplicitPublish } from "./explicit" export {ExplicitPublish} from "./explicit"

View File

@ -1,6 +1,6 @@
import { StaticResources } from "../util/resources" import {StaticResources} from "../util/resources"
import { FilePath, FullSlug } from "../util/path" import {FilePath, FullSlug} from "../util/path"
import { BuildCtx } from "../util/ctx" import {BuildCtx} from "../util/ctx"
export function getStaticResourcesFromPlugins(ctx: BuildCtx) { export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
const staticResources: StaticResources = { const staticResources: StaticResources = {
@ -9,7 +9,9 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
} }
for (const transformer of ctx.cfg.plugins.transformers) { 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) { if (res?.js) {
staticResources.js.push(...res.js) staticResources.js.push(...res.js)
} }

View File

@ -1,7 +1,7 @@
import rehypeCitation from "rehype-citation" import rehypeCitation from "rehype-citation"
import { PluggableList } from "unified" import {PluggableList} from "unified"
import { visit } from "unist-util-visit" import {visit} from "unist-util-visit"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
export interface Options { export interface Options {
bibliographyFile: string bibliographyFile: string
@ -17,8 +17,10 @@ const defaultOptions: Options = {
csl: "apa", csl: "apa",
} }
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const Citations: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "Citations", name: "Citations",
htmlPlugins() { htmlPlugins() {
@ -39,7 +41,10 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
plugins.push(() => { plugins.push(() => {
return (tree, _file) => { return (tree, _file) => {
visit(tree, "element", (node, _index, _parent) => { 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 node.properties["data-no-popover"] = true
} }
}) })

View File

@ -1,7 +1,7 @@
import { Root as HTMLRoot } from "hast" import {Root as HTMLRoot} from "hast"
import { toString } from "hast-util-to-string" import {toString} from "hast-util-to-string"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import { escapeHTML } from "../../util/escape" import {escapeHTML} from "../../util/escape"
export interface Options { export interface Options {
descriptionLength: number descriptionLength: number
@ -18,8 +18,10 @@ const urlRegex = new RegExp(
"g", "g",
) )
export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const Description: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "Description", name: "Description",
htmlPlugins() { htmlPlugins() {
@ -58,7 +60,9 @@ export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
while (currentDescriptionLength < len) { while (currentDescriptionLength < len) {
const sentence = sentences[sentenceIdx] const sentence = sentences[sentenceIdx]
if (!sentence) break if (!sentence) break
const currentSentence = sentence.endsWith(".") ? sentence : sentence + "." const currentSentence = sentence.endsWith(".")
? sentence
: sentence + "."
finalDesc.push(currentSentence) finalDesc.push(currentSentence)
currentDescriptionLength += currentSentence.length currentDescriptionLength += currentSentence.length
sentenceIdx++ sentenceIdx++

View File

@ -1,11 +1,11 @@
import matter from "gray-matter" import matter from "gray-matter"
import remarkFrontmatter from "remark-frontmatter" import remarkFrontmatter from "remark-frontmatter"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import yaml from "js-yaml" import yaml from "js-yaml"
import toml from "toml" import toml from "toml"
import { slugTag } from "../../util/path" import {slugTag} from "../../util/path"
import { QuartzPluginData } from "../vfile" import {QuartzPluginData} from "../vfile"
import { i18n } from "../../i18n" import {i18n} from "../../i18n"
export interface Options { export interface Options {
delimiters: string | [string, string] delimiters: string | [string, string]
@ -17,7 +17,7 @@ const defaultOptions: Options = {
language: "yaml", language: "yaml",
} }
function coalesceAliases(data: { [key: string]: any }, aliases: string[]) { function coalesceAliases(data: {[key: string]: any}, aliases: string[]) {
for (const alias of aliases) { for (const alias of aliases) {
if (data[alias] !== undefined && data[alias] !== null) return data[alias] if (data[alias] !== undefined && data[alias] !== null) return data[alias]
} }
@ -36,23 +36,27 @@ function coerceToArray(input: string | string[]): string[] | undefined {
// remove all non-strings // remove all non-strings
return input 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()) .map((tag: string | number) => tag.toString())
} }
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "FrontMatter", name: "FrontMatter",
markdownPlugins({ cfg }) { markdownPlugins({cfg}) {
return [ return [
[remarkFrontmatter, ["yaml", "toml"]], [remarkFrontmatter, ["yaml", "toml"]],
() => { () => {
return (_, file) => { return (_, file) => {
const { data } = matter(Buffer.from(file.value), { const {data} = matter(Buffer.from(file.value), {
...opts, ...opts,
engines: { engines: {
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, yaml: (s) => yaml.load(s, {schema: yaml.JSON_SCHEMA}) as object,
toml: (s) => toml.parse(s) as object, toml: (s) => toml.parse(s) as object,
}, },
}) })
@ -60,15 +64,22 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
if (data.title != null && data.title.toString() !== "") { if (data.title != null && data.title.toString() !== "") {
data.title = data.title.toString() data.title = data.title.toString()
} else { } 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"])) 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 if (aliases) data.aliases = aliases
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"])) const cssclasses = coerceToArray(
coalesceAliases(data, ["cssclasses", "cssclass"]),
)
if (cssclasses) data.cssclasses = cssclasses if (cssclasses) data.cssclasses = cssclasses
// fill in frontmatter // fill in frontmatter
@ -82,7 +93,7 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
declare module "vfile" { declare module "vfile" {
interface DataMap { interface DataMap {
frontmatter: { [key: string]: unknown } & { frontmatter: {[key: string]: unknown} & {
title: string title: string
} & Partial<{ } & Partial<{
tags: string[] tags: string[]

View File

@ -1,6 +1,6 @@
import remarkGfm from "remark-gfm" import remarkGfm from "remark-gfm"
import smartypants from "remark-smartypants" import smartypants from "remark-smartypants"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import rehypeSlug from "rehype-slug" import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings" import rehypeAutolinkHeadings from "rehype-autolink-headings"
@ -14,8 +14,10 @@ const defaultOptions: Options = {
linkHeadings: true, linkHeadings: true,
} }
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<
const opts = { ...defaultOptions, ...userOpts } Partial<Options>
> = (userOpts) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "GitHubFlavoredMarkdown", name: "GitHubFlavoredMarkdown",
markdownPlugins() { markdownPlugins() {
@ -30,20 +32,20 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> =
{ {
behavior: "append", behavior: "append",
properties: { properties: {
role: "anchor", "role": "anchor",
ariaHidden: true, "ariaHidden": true,
tabIndex: -1, "tabIndex": -1,
"data-no-popover": true, "data-no-popover": true,
}, },
content: { content: {
type: "element", type: "element",
tagName: "svg", tagName: "svg",
properties: { properties: {
width: 18, "width": 18,
height: 18, "height": 18,
viewBox: "0 0 24 24", "viewBox": "0 0 24 24",
fill: "none", "fill": "none",
stroke: "currentColor", "stroke": "currentColor",
"stroke-width": "2", "stroke-width": "2",
"stroke-linecap": "round", "stroke-linecap": "round",
"stroke-linejoin": "round", "stroke-linejoin": "round",

View File

@ -1,12 +1,12 @@
export { FrontMatter } from "./frontmatter" export {FrontMatter} from "./frontmatter"
export { GitHubFlavoredMarkdown } from "./gfm" export {GitHubFlavoredMarkdown} from "./gfm"
export { Citations } from "./citations" export {Citations} from "./citations"
export { CreatedModifiedDate } from "./lastmod" export {CreatedModifiedDate} from "./lastmod"
export { Latex } from "./latex" export {Latex} from "./latex"
export { Description } from "./description" export {Description} from "./description"
export { CrawlLinks } from "./links" export {CrawlLinks} from "./links"
export { ObsidianFlavoredMarkdown } from "./ofm" export {ObsidianFlavoredMarkdown} from "./ofm"
export { OxHugoFlavouredMarkdown } from "./oxhugofm" export {OxHugoFlavouredMarkdown} from "./oxhugofm"
export { SyntaxHighlighting } from "./syntax" export {SyntaxHighlighting} from "./syntax"
export { TableOfContents } from "./toc" export {TableOfContents} from "./toc"
export { HardLineBreaks } from "./linebreaks" export {HardLineBreaks} from "./linebreaks"

View File

@ -1,7 +1,7 @@
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import { Repository } from "@napi-rs/simple-git" import {Repository} from "@napi-rs/simple-git"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import chalk from "chalk" import chalk from "chalk"
export interface Options { export interface Options {
@ -27,8 +27,10 @@ function coerceDate(fp: string, d: any): Date {
} }
type MaybeDate = undefined | string | number type MaybeDate = undefined | string | number
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "CreatedModifiedDate", name: "CreatedModifiedDate",
markdownPlugins() { markdownPlugins() {
@ -41,7 +43,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
let published: MaybeDate = undefined let published: MaybeDate = undefined
const fp = file.data.filePath! 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) { for (const source of opts.priority) {
if (source === "filesystem") { if (source === "filesystem") {
const st = await fs.promises.stat(fullFp) const st = await fs.promises.stat(fullFp)
@ -62,7 +66,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
} }
try { try {
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!) modified ||= await repo.getFileLatestModifiedDateAsync(
file.data.filePath!,
)
} catch { } catch {
console.log( console.log(
chalk.yellow( chalk.yellow(

View File

@ -1,7 +1,7 @@
import remarkMath from "remark-math" import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex" import rehypeKatex from "rehype-katex"
import rehypeMathjax from "rehype-mathjax/svg" import rehypeMathjax from "rehype-mathjax/svg"
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
interface Options { interface Options {
renderEngine: "katex" | "mathjax" renderEngine: "katex" | "mathjax"
@ -22,9 +22,9 @@ export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
}, },
htmlPlugins() { htmlPlugins() {
if (engine === "katex") { if (engine === "katex") {
return [[rehypeKatex, { output: "html", macros }]] return [[rehypeKatex, {output: "html", macros}]]
} else { } else {
return [[rehypeMathjax, { macros }]] return [[rehypeMathjax, {macros}]]
} }
}, },
externalResources() { externalResources() {

View File

@ -1,4 +1,4 @@
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import remarkBreaks from "remark-breaks" import remarkBreaks from "remark-breaks"
export const HardLineBreaks: QuartzTransformerPlugin = () => { export const HardLineBreaks: QuartzTransformerPlugin = () => {

View File

@ -1,4 +1,4 @@
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import { import {
FullSlug, FullSlug,
RelativeURL, RelativeURL,
@ -10,9 +10,9 @@ import {
transformLink, transformLink,
} from "../../util/path" } from "../../util/path"
import path from "path" import path from "path"
import { visit } from "unist-util-visit" import {visit} from "unist-util-visit"
import isAbsoluteUrl from "is-absolute-url" import isAbsoluteUrl from "is-absolute-url"
import { Root } from "hast" import {Root} from "hast"
interface Options { interface Options {
/** How to resolve Markdown paths */ /** How to resolve Markdown paths */
@ -32,8 +32,10 @@ const defaultOptions: Options = {
externalLinkIcon: true, externalLinkIcon: true,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "LinkProcessing", name: "LinkProcessing",
htmlPlugins(ctx) { htmlPlugins(ctx) {
@ -66,8 +68,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
tagName: "svg", tagName: "svg",
properties: { properties: {
"aria-hidden": "true", "aria-hidden": "true",
class: "external-icon", "class": "external-icon",
viewBox: "0 0 512 512", "viewBox": "0 0 512 512",
}, },
children: [ children: [
{ {
@ -98,7 +100,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
} }
// don't process external links or intra-document anchors // don't process external links or intra-document anchors
const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) const isInternal = !(
isAbsoluteUrl(dest) || dest.startsWith("#")
)
if (isInternal) { if (isInternal) {
dest = node.properties.href = transformLink( dest = node.properties.href = transformLink(
file.data.slug!, file.data.slug!,
@ -108,7 +112,10 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
// url.resolve is considered legacy // url.resolve is considered legacy
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true)) const url = new URL(
dest,
"https://base.com/" + stripSlashes(curSlug, true),
)
const canonicalDest = url.pathname const canonicalDest = url.pathname
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
if (destCanonical.endsWith("/")) { if (destCanonical.endsWith("/")) {
@ -116,7 +123,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
} }
// need to decodeURIComponent here as WHATWG URL percent-encodes everything // 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) const simple = simplifySlug(full)
outgoing.add(simple) outgoing.add(simple)
node.properties["data-slug"] = full node.properties["data-slug"] = full

View File

@ -1,22 +1,32 @@
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" import {
import { Element, Literal, Root as HtmlRoot } from "hast" Root,
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" 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 rehypeRaw from "rehype-raw" import rehypeRaw from "rehype-raw"
import { SKIP, visit } from "unist-util-visit" import {SKIP, visit} from "unist-util-visit"
import path from "path" import path from "path"
import { splitAnchor } from "../../util/path" import {splitAnchor} from "../../util/path"
import { JSResource } from "../../util/resources" import {JSResource} from "../../util/resources"
// @ts-ignore // @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts" import calloutScript from "../../components/scripts/callout.inline.ts"
// @ts-ignore // @ts-ignore
import checkboxScript from "../../components/scripts/checkbox.inline.ts" import checkboxScript from "../../components/scripts/checkbox.inline.ts"
import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" import {FilePath, pathToRoot, slugTag, slugifyFilePath} from "../../util/path"
import { toHast } from "mdast-util-to-hast" import {toHast} from "mdast-util-to-hast"
import { toHtml } from "hast-util-to-html" import {toHtml} from "hast-util-to-html"
import { PhrasingContent } from "mdast-util-find-and-replace/lib" import {PhrasingContent} from "mdast-util-find-and-replace/lib"
import { capitalize } from "../../util/lang" import {capitalize} from "../../util/lang"
import { PluggableList } from "unified" import {PluggableList} from "unified"
export interface Options { export interface Options {
comments: boolean comments: boolean
@ -90,7 +100,8 @@ const arrowMapping: Record<string, string> = {
} }
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { 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 // if callout is not recognized, make it a custom one
return calloutMapping[normalizedCallout] ?? calloutName return calloutMapping[normalizedCallout] ?? calloutName
} }
@ -111,7 +122,9 @@ export const wikilinkRegex = new RegExp(
// ^\|([^\n])+\|\n(\|) -> matches the header row // ^\|([^\n])+\|\n(\|) -> matches the header row
// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator // ( ?:?-{3,}:? ?\|)+ -> matches the header row separator
// (\|([^\n])+\|\n)+ -> matches the body rows // (\|([^\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 // matches any wikilink, only used for escaping wikilinks inside tables
export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g) export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g)
@ -129,19 +142,24 @@ const tagRegex = new RegExp(
/(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu, /(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu,
) )
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g) const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g)
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ const ytLinkRegex =
/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ 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( const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/, /^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
) )
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<
const opts = { ...defaultOptions, ...userOpts } Partial<Options>
> = (userOpts) => {
const opts = {...defaultOptions, ...userOpts}
const mdastToHtml = (ast: PhrasingContent | Paragraph) => { const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
const hast = toHast(ast, { allowDangerousHtml: true })! const hast = toHast(ast, {allowDangerousHtml: true})!
return toHtml(hast, { allowDangerousHtml: true }) return toHtml(hast, {allowDangerousHtml: true})
} }
return { return {
@ -194,7 +212,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : "" 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 displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
const embedDisplay = value.startsWith("!") ? "!" : "" const embedDisplay = value.startsWith("!") ? "!" : ""
@ -230,7 +250,17 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (value.startsWith("!")) { if (value.startsWith("!")) {
const ext: string = path.extname(fp).toLowerCase() const ext: string = path.extname(fp).toLowerCase()
const url = slugifyFilePath(fp as FilePath) 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 match = wikilinkImageEmbedRegex.exec(alias ?? "")
const alt = match?.groups?.alt ?? "" const alt = match?.groups?.alt ?? ""
const width = match?.groups?.width ?? "auto" 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 { return {
type: "html", type: "html",
value: `<video src="${url}" controls></video>`, value: `<video src="${url}" controls></video>`,
} }
} else if ( } else if (
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext) [
".mp3",
".webm",
".wav",
".m4a",
".ogg",
".3gp",
".flac",
].includes(ext)
) { ) {
return { return {
type: "html", type: "html",
@ -267,7 +307,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
const block = anchor const block = anchor
return { return {
type: "html", type: "html",
data: { hProperties: { transclude: true } }, data: {hProperties: {transclude: true}},
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}" data-embed-alias="${alias}"><a href="${ value: `<blockquote class="transclude" data-url="${url}" data-block="${block}" data-embed-alias="${alias}"><a href="${
url + anchor url + anchor
}" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`, }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
@ -360,18 +400,24 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (typeof replace === "string") { if (typeof replace === "string") {
node.value = node.value.replace(regex, replace) node.value = node.value.replace(regex, replace)
} else { } else {
node.value = node.value.replace(regex, (substring: string, ...args) => { node.value = node.value.replace(
regex,
(substring: string, ...args) => {
const replaceValue = replace(substring, ...args) const replaceValue = replace(substring, ...args)
if (typeof replaceValue === "string") { if (typeof replaceValue === "string") {
return replaceValue return replaceValue
} else if (Array.isArray(replaceValue)) { } else if (Array.isArray(replaceValue)) {
return replaceValue.map(mdastToHtml).join("") return replaceValue.map(mdastToHtml).join("")
} else if (typeof replaceValue === "object" && replaceValue !== null) { } else if (
typeof replaceValue === "object" &&
replaceValue !== null
) {
return mdastToHtml(replaceValue) return mdastToHtml(replaceValue)
} else { } else {
return substring return substring
} }
}) },
)
} }
} }
}) })
@ -384,7 +430,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
plugins.push(() => { plugins.push(() => {
return (tree: Root, _file) => { return (tree: Root, _file) => {
visit(tree, "image", (node, index, parent) => { 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 = { const newNode: Html = {
type: "html", type: "html",
value: `<video controls src="${node.url}"></video>`, value: `<video controls src="${node.url}"></video>`,
@ -408,7 +458,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
// find first line and callout content // find first line and callout content
const [firstChild, ...calloutContent] = node.children const [firstChild, ...calloutContent] = node.children
if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") { if (
firstChild.type !== "paragraph" ||
firstChild.children[0]?.type !== "text"
) {
return return
} }
@ -419,18 +472,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
const match = firstLine.match(calloutRegex) const match = firstLine.match(calloutRegex)
if (match && match.input) { if (match && match.input) {
const [calloutDirective, typeString, calloutMetaData, collapseChar] = match const [
const calloutType = canonicalizeCallout(typeString.toLowerCase()) calloutDirective,
typeString,
calloutMetaData,
collapseChar,
] = match
const calloutType = canonicalizeCallout(
typeString.toLowerCase(),
)
const collapse = collapseChar === "+" || collapseChar === "-" const collapse = collapseChar === "+" || collapseChar === "-"
const defaultState = collapseChar === "-" ? "collapsed" : "expanded" const defaultState =
const titleContent = match.input.slice(calloutDirective.length).trim() collapseChar === "-" ? "collapsed" : "expanded"
const useDefaultTitle = titleContent === "" && restOfTitle.length === 0 const titleContent = match.input
.slice(calloutDirective.length)
.trim()
const useDefaultTitle =
titleContent === "" && restOfTitle.length === 0
const titleNode: Paragraph = { const titleNode: Paragraph = {
type: "paragraph", type: "paragraph",
children: [ children: [
{ {
type: "text", type: "text",
value: useDefaultTitle ? capitalize(typeString) : titleContent + " ", value: useDefaultTitle
? capitalize(typeString)
: titleContent + " ",
}, },
...restOfTitle, ...restOfTitle,
], ],
@ -450,7 +516,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
</div>`, </div>`,
} }
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleHtml] const blockquoteContent: (BlockContent | DefinitionContent)[] =
[titleHtml]
if (remainingText.length > 0) { if (remainingText.length > 0) {
blockquoteContent.push({ blockquoteContent.push({
type: "paragraph", type: "paragraph",
@ -478,7 +545,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
node.data = { node.data = {
hProperties: { hProperties: {
...(node.data?.hProperties ?? {}), ...(node.data?.hProperties ?? {}),
className: classNames.join(" "), "className": classNames.join(" "),
"data-callout": calloutType, "data-callout": calloutType,
"data-callout-fold": collapse, "data-callout-fold": collapse,
"data-callout-metadata": calloutMetaData, "data-callout-metadata": calloutMetaData,
@ -606,10 +673,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
plugins.push(() => { plugins.push(() => {
return (tree: HtmlRoot) => { return (tree: HtmlRoot) => {
visit(tree, "element", (node) => { 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 match = node.properties.src.match(ytLinkRegex)
const videoId = match && match[2].length == 11 ? match[2] : null 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) { if (videoId) {
// YouTube video (with optional playlist) // YouTube video (with optional playlist)
node.tagName = "iframe" node.tagName = "iframe"
@ -643,7 +714,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
plugins.push(() => { plugins.push(() => {
return (tree: HtmlRoot, _file) => { return (tree: HtmlRoot, _file) => {
visit(tree, "element", (node) => { 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 const isChecked = node.properties?.checked ?? false
node.properties = { node.properties = {
type: "checkbox", type: "checkbox",
@ -705,7 +779,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
}) })
} }
return { js } return {js}
}, },
} }
} }

View File

@ -1,4 +1,4 @@
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
export interface Options { export interface Options {
/** Replace {{ relref }} with quartz wikilinks []() */ /** Replace {{ relref }} with quartz wikilinks []() */
@ -22,7 +22,10 @@ const defaultOptions: Options = {
replaceOrgLatex: true, replaceOrgLatex: true,
} }
const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g") const relrefRegex = new RegExp(
/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/,
"g",
)
const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g") const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g")
const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g") const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g")
const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g") const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g")
@ -47,8 +50,10 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
* markdown to make it compatible with quartz but the list of changes applied it * markdown to make it compatible with quartz but the list of changes applied it
* is not exhaustive. * is not exhaustive.
* */ * */
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<
const opts = { ...defaultOptions, ...userOpts } Partial<Options>
> = (userOpts) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "OxHugoFlavouredMarkdown", name: "OxHugoFlavouredMarkdown",
textTransform(_ctx, src) { textTransform(_ctx, src) {

View File

@ -1,5 +1,8 @@
import { QuartzTransformerPlugin } from "../types" 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> { interface Theme extends Record<string, CodeTheme> {
light: CodeTheme light: CodeTheme
@ -19,8 +22,10 @@ const defaultOptions: Options = {
keepBackground: false, keepBackground: false,
} }
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (
const opts: CodeOptions = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts: CodeOptions = {...defaultOptions, ...userOpts}
return { return {
name: "SyntaxHighlighting", name: "SyntaxHighlighting",

View File

@ -1,7 +1,7 @@
import { QuartzTransformerPlugin } from "../types" import {QuartzTransformerPlugin} from "../types"
import { Root } from "mdast" import {Root} from "mdast"
import { visit } from "unist-util-visit" import {visit} from "unist-util-visit"
import { toString } from "mdast-util-to-string" import {toString} from "mdast-util-to-string"
import Slugger from "github-slugger" import Slugger from "github-slugger"
export interface Options { export interface Options {
@ -25,15 +25,18 @@ interface TocEntry {
} }
const slugAnchor = new Slugger() const slugAnchor = new Slugger()
export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (
const opts = { ...defaultOptions, ...userOpts } userOpts,
) => {
const opts = {...defaultOptions, ...userOpts}
return { return {
name: "TableOfContents", name: "TableOfContents",
markdownPlugins() { markdownPlugins() {
return [ return [
() => { () => {
return async (tree: Root, file) => { return async (tree: Root, file) => {
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault const display =
file.data.frontmatter?.enableToc ?? opts.showByDefault
if (display) { if (display) {
slugAnchor.reset() slugAnchor.reset()
const toc: TocEntry[] = [] const toc: TocEntry[] = []

View File

@ -1,9 +1,9 @@
import { PluggableList } from "unified" import {PluggableList} from "unified"
import { StaticResources } from "../util/resources" import {StaticResources} from "../util/resources"
import { ProcessedContent } from "./vfile" import {ProcessedContent} from "./vfile"
import { QuartzComponent } from "../components/types" import {QuartzComponent} from "../components/types"
import { FilePath } from "../util/path" import {FilePath} from "../util/path"
import { BuildCtx } from "../util/ctx" import {BuildCtx} from "../util/ctx"
import DepGraph from "../depgraph" import DepGraph from "../depgraph"
export interface PluginTypes { export interface PluginTypes {
@ -37,7 +37,11 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
) => QuartzEmitterPluginInstance ) => QuartzEmitterPluginInstance
export type QuartzEmitterPluginInstance = { export type QuartzEmitterPluginInstance = {
name: string name: string
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]> emit(
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
): Promise<FilePath[]>
getQuartzComponents(ctx: BuildCtx): QuartzComponent[] getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
getDependencyGraph?( getDependencyGraph?(
ctx: BuildCtx, ctx: BuildCtx,

View File

@ -1,11 +1,13 @@
import { Node, Parent } from "hast" import {Node, Parent} from "hast"
import { Data, VFile } from "vfile" import {Data, VFile} from "vfile"
export type QuartzPluginData = Data export type QuartzPluginData = Data
export type ProcessedContent = [Node, VFile] export type ProcessedContent = [Node, VFile]
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent { export function defaultProcessedContent(
const root: Parent = { type: "root", children: [] } vfileData: Partial<QuartzPluginData>,
): ProcessedContent {
const root: Parent = {type: "root", children: []}
const vfile = new VFile("") const vfile = new VFile("")
vfile.data = vfileData vfile.data = vfileData
return [root, vfile] return [root, vfile]

View File

@ -158,7 +158,9 @@ export async function parseMarkdown(
const childPromises: WorkerPromise<ProcessedContent[]>[] = [] const childPromises: WorkerPromise<ProcessedContent[]>[] = []
for (const chunk of chunks(fps, CHUNK_SIZE)) { 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( const results: ProcessedContent[][] = await WorkerPromise.all(

View File

@ -1,5 +1,5 @@
import { QuartzConfig } from "../cfg" import {QuartzConfig} from "../cfg"
import { FullSlug } from "./path" import {FullSlug} from "./path"
export interface Argv { export interface Argv {
directory: string directory: string

View File

@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { FilePath } from "./path" import {FilePath} from "./path"
import { globby } from "globby" import {globby} from "globby"
export function toPosixPath(fp: string): string { export function toPosixPath(fp: string): string {
return fp.split(path.sep).join("/") return fp.split(path.sep).join("/")

View File

@ -1,8 +1,8 @@
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime" import {Components, Jsx, toJsxRuntime} from "hast-util-to-jsx-runtime"
import { Node, Root } from "hast" import {Node, Root} from "hast"
import { Fragment, jsx, jsxs } from "preact/jsx-runtime" import {Fragment, jsx, jsxs} from "preact/jsx-runtime"
import { trace } from "./trace" import {trace} from "./trace"
import { type FilePath } from "./path" import {type FilePath} from "./path"
const customComponents: Components = { const customComponents: Components = {
table: (props) => ( table: (props) => (

Some files were not shown because too many files have changed in this diff Show More