This commit is contained in:
semanticdata 2024-02-12 08:59:11 -06:00
commit 598b9a1c33
22 changed files with 103 additions and 55 deletions

View File

@ -25,8 +25,9 @@ This part of the configuration concerns anything that can affect the whole site.
- `enablePopovers`: whether to enable [[popover previews]] on your site.
- `analytics`: what to use for analytics on your site. Values can be
- `null`: don't use analytics;
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
- `locale`: used for [[i18n]] and date formatting
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`

8
package-lock.json generated
View File

@ -17,7 +17,7 @@
"chokidar": "^3.5.3",
"cli-spinner": "^0.2.10",
"d3": "^7.8.5",
"esbuild-sass-plugin": "^2.16.0",
"esbuild-sass-plugin": "^2.16.1",
"flexsearch": "0.7.43",
"github-slugger": "^2.0.0",
"globby": "^14.0.0",
@ -2052,9 +2052,9 @@
}
},
"node_modules/esbuild-sass-plugin": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz",
"integrity": "sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz",
"integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==",
"dependencies": {
"resolve": "^1.22.6",
"sass": "^1.7.3"

View File

@ -203,8 +203,9 @@ async function partialRebuildFromEntrypoint(
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
// emmiter may not define a dependency graph. nothing to update if so
if (emitterGraph) {
// only update the graph if the emitter plugin uses the changed file
// eg. Assets plugin ignores md files, so we skip updating the graph
if (emitterGraph?.hasNode(fp)) {
// merge the new dependencies into the dep graph
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}

View File

@ -68,13 +68,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
folderIndex = new Map()
// construct the index for the first time
for (const file of allFiles) {
if (file.slug?.endsWith("index")) {
const folderParts = file.slug?.split("/")
// 2nd last to exclude the /index
const folderName = folderParts?.at(-2)
if (folderName) {
folderIndex.set(folderName, file)
}
const folderParts = file.slug?.split("/")
if (folderParts?.at(-1) === "index") {
folderIndex.set(folderParts.slice(0, -1).join("/"), file)
}
}
}
@ -88,7 +84,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
let curPathSegment = slugParts[i]
// Try to resolve frontmatter folder title
const currentFile = folderIndex?.get(curPathSegment)
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
if (currentFile) {
const title = currentFile.frontmatter!.title
if (title !== "index") {

View File

@ -2,6 +2,7 @@ import { formatDate, getDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
interface ContentMetaOptions {
/**
@ -30,8 +31,11 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
// Display reading time if enabled
if (options.showReadingTime) {
const { text: timeTaken, words: _words } = readingTime(text)
segments.push(timeTaken)
const { minutes, words: _words } = readingTime(text)
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
minutes: Math.ceil(minutes),
})
segments.push(displayedTime)
}
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>

View File

@ -1,5 +1,5 @@
import { i18n } from "../i18n"
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"

View File

@ -3,7 +3,7 @@ import path from "path"
import style from "../styles/listPage.scss"
import { PageList } from "../PageList"
import { _stripSlashes, simplifySlug } from "../../util/path"
import { stripSlashes, simplifySlug } from "../../util/path"
import { Root } from "hast"
import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n"
@ -24,9 +24,9 @@ export default ((opts?: Partial<FolderContentOptions>) => {
function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles, cfg } = props
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
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 folderParts = folderSlug.split(path.posix.sep)
const fileParts = fileSlug.split(path.posix.sep)

View File

@ -53,6 +53,14 @@ export default {
tableOfContents: {
title: "فهرس المحتويات",
},
contentMeta: {
readingTime: ({ minutes }) =>
minutes == 1
? `دقيقة أو أقل للقراءة`
: minutes == 2
? `دقيقتان للقراءة`
: `${minutes} دقائق للقراءة`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Inhaltsverzeichnis",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -55,6 +55,9 @@ export interface Translation {
tableOfContents: {
title: string
}
contentMeta: {
readingTime: (variables: { minutes: number }) => string
}
}
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Table of Contents",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Tabla de Contenidos",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Table des Matières",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "目次",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Inhoudsopgave",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,10 @@ export default {
tableOfContents: {
title: "Cuprins",
},
contentMeta: {
readingTime: ({ minutes }) =>
minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`,
},
},
pages: {
rss: {

View File

@ -53,6 +53,9 @@ export default {
tableOfContents: {
title: "Зміст",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {

View File

@ -4,6 +4,13 @@ import path from "path"
import fs from "fs"
import { glob } from "../../util/glob"
import DepGraph from "../../depgraph"
import { Argv } from "../../util/ctx"
import { QuartzConfig } from "../../cfg"
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
// glob all non MD files in content folder and copy it over
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
}
export const Assets: QuartzEmitterPlugin = () => {
return {
@ -15,7 +22,7 @@ export const Assets: QuartzEmitterPlugin = () => {
const { argv, cfg } = ctx
const graph = new DepGraph<FilePath>()
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
const fps = await filesToCopy(argv, cfg)
for (const fp of fps) {
const ext = path.extname(fp)
@ -30,9 +37,8 @@ export const Assets: QuartzEmitterPlugin = () => {
return graph
},
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
// glob all non MD/MDX/HTML files in content folder and copy it over
const assetsPath = argv.output
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
const fps = await filesToCopy(argv, cfg)
const res: FilePath[] = []
for (const fp of fps) {
const ext = path.extname(fp)

View File

@ -10,7 +10,7 @@ import {
FilePath,
FullSlug,
SimpleSlug,
_stripSlashes,
stripSlashes,
joinSegments,
pathToRoot,
simplifySlug,
@ -38,7 +38,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
},
async getDependencyGraph(ctx, content, _resources) {
async getDependencyGraph(_ctx, _content, _resources) {
// Example graph:
// nested/file.md --> nested/file.html
// \-------> nested/index.html
@ -75,7 +75,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
)
for (const [tree, file] of content) {
const slug = _stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
if (folders.has(slug)) {
folderDescriptions[slug] = [tree, file]
}

View File

@ -4,7 +4,7 @@ import {
RelativeURL,
SimpleSlug,
TransformOptions,
_stripSlashes,
stripSlashes,
simplifySlug,
splitAnchor,
transformLink,
@ -115,7 +115,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
}
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug
const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug
const simple = simplifySlug(full)
outgoing.add(simple)
node.properties["data-slug"] = full

View File

@ -118,7 +118,10 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
// #(...) -> capturing group, tag itself must start with #
// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{Emoji}\d]+)*)/, "gu")
const tagRegex = new RegExp(
/(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/,
"gu",
)
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g")
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)

View File

@ -23,22 +23,22 @@ export type FullSlug = SlugLike<"full">
export function isFullSlug(s: string): s is FullSlug {
const validStart = !(s.startsWith(".") || s.startsWith("/"))
const validEnding = !s.endsWith("/")
return validStart && validEnding && !_containsForbiddenCharacters(s)
return validStart && validEnding && !containsForbiddenCharacters(s)
}
/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
export type SimpleSlug = SlugLike<"simple">
export function isSimpleSlug(s: string): s is SimpleSlug {
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
const validEnding = !(s.endsWith("/index") || s === "index")
return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
const validEnding = !endsWith(s, "index")
return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
}
/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
export type RelativeURL = SlugLike<"relative">
export function isRelativeURL(s: string): s is RelativeURL {
const validStart = /^\.{1,2}/.test(s)
const validEnding = !(s.endsWith("/index") || s === "index")
const validEnding = !endsWith(s, "index")
return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
}
@ -63,7 +63,7 @@ function sluggify(s: string): string {
}
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
fp = _stripSlashes(fp) as FilePath
fp = stripSlashes(fp) as FilePath
let ext = _getFileExtension(fp)
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
if (excludeExt || [".md", ".html", undefined].includes(ext)) {
@ -73,7 +73,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
let slug = sluggify(withoutFileExt)
// treat _index as index
if (_endsWith(slug, "_index")) {
if (endsWith(slug, "_index")) {
slug = slug.replace(/_index$/, "index")
}
@ -81,21 +81,21 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
}
export function simplifySlug(fp: FullSlug): SimpleSlug {
const res = _stripSlashes(_trimSuffix(fp, "index"), true)
const res = stripSlashes(trimSuffix(fp, "index"), true)
return (res.length === 0 ? "/" : res) as SimpleSlug
}
export function transformInternalLink(link: string): RelativeURL {
let [fplike, anchor] = splitAnchor(decodeURI(link))
const folderPath = _isFolderPath(fplike)
const folderPath = isFolderPath(fplike)
let segments = fplike.split("/").filter((x) => x.length > 0)
let prefix = segments.filter(_isRelativeSegment).join("/")
let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/")
let prefix = segments.filter(isRelativeSegment).join("/")
let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/")
// manually add ext here as we want to not strip 'index' if it has an extension
const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug))
const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug))
const trail = folderPath ? "/" : ""
const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
return res
@ -206,8 +206,8 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
if (opts.strategy === "relative") {
return targetSlug as RelativeURL
} else {
const folderTail = _isFolderPath(targetSlug) ? "/" : ""
const canonicalSlug = _stripSlashes(targetSlug.slice(".".length))
const folderTail = isFolderPath(targetSlug) ? "/" : ""
const canonicalSlug = stripSlashes(targetSlug.slice(".".length))
let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
if (opts.strategy === "shortest") {
@ -230,28 +230,29 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
}
}
function _isFolderPath(fplike: string): boolean {
// path helpers
function isFolderPath(fplike: string): boolean {
return (
fplike.endsWith("/") ||
_endsWith(fplike, "index") ||
_endsWith(fplike, "index.md") ||
_endsWith(fplike, "index.html")
endsWith(fplike, "index") ||
endsWith(fplike, "index.md") ||
endsWith(fplike, "index.html")
)
}
function _endsWith(s: string, suffix: string): boolean {
export function endsWith(s: string, suffix: string): boolean {
return s === suffix || s.endsWith("/" + suffix)
}
function _trimSuffix(s: string, suffix: string): string {
if (_endsWith(s, suffix)) {
function trimSuffix(s: string, suffix: string): string {
if (endsWith(s, suffix)) {
s = s.slice(0, -suffix.length)
}
return s
}
function _containsForbiddenCharacters(s: string): boolean {
return s.includes(" ") || s.includes("#") || s.includes("?")
function containsForbiddenCharacters(s: string): boolean {
return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
}
function _hasFileExtension(s: string): boolean {
@ -262,11 +263,11 @@ function _getFileExtension(s: string): string | undefined {
return s.match(/\.[A-Za-z0-9]+$/)?.[0]
}
function _isRelativeSegment(s: string): boolean {
function isRelativeSegment(s: string): boolean {
return /^\.{0,2}$/.test(s)
}
export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string {
export function stripSlashes(s: string, onlyStripPrefix?: boolean): string {
if (s.startsWith("/")) {
s = s.substring(1)
}