mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-19 10:54:06 -06:00
Merge 1275fdf1a2 into bacd19c4ea
This commit is contained in:
commit
a6b91a0f99
@ -42,6 +42,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
||||
- 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`.
|
||||
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
||||
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details.
|
||||
- `lowercasePaths`: whether to convert all generated paths (URLs) to lowercase. When set to `true`, a file named `MyFile.md` will be available at `/myfile` instead of `/MyFile`. This also applies to tags but not aliases.
|
||||
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
|
||||
- `theme`: configure how the site looks.
|
||||
- `cdnCaching`: if `true` (default), use Google CDN to cache the fonts. This will generally be faster. Disable (`false`) this if you want Quartz to download the fonts to be self-contained.
|
||||
|
||||
@ -18,6 +18,7 @@ const config: QuartzConfig = {
|
||||
locale: "en-US",
|
||||
baseUrl: "quartz.jzhao.xyz",
|
||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||
lowercasePaths: false,
|
||||
defaultDateType: "modified",
|
||||
theme: {
|
||||
fontOrigin: "googleFonts",
|
||||
|
||||
@ -79,7 +79,9 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
|
||||
const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath)
|
||||
ctx.allFiles = allFiles
|
||||
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||
ctx.allSlugs = allFiles.map((fp) =>
|
||||
slugifyFilePath(fp as FilePath, undefined, ctx.cfg.configuration.lowercasePaths),
|
||||
)
|
||||
|
||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||
const filteredContent = filterContent(ctx, parsedFiles)
|
||||
@ -253,7 +255,9 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
||||
|
||||
// update allFiles and then allSlugs with the consistent view of content map
|
||||
ctx.allFiles = Array.from(contentMap.keys())
|
||||
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||
ctx.allSlugs = ctx.allFiles.map((fp) =>
|
||||
slugifyFilePath(fp as FilePath, undefined, ctx.cfg.configuration.lowercasePaths),
|
||||
)
|
||||
let processedFiles = filterContent(
|
||||
ctx,
|
||||
Array.from(contentMap.values())
|
||||
|
||||
@ -67,6 +67,8 @@ export interface GlobalConfiguration {
|
||||
analytics: Analytics
|
||||
/** Glob patterns to not search */
|
||||
ignorePatterns: string[]
|
||||
/** Whether to convert all generated paths to lowercase (default: false) */
|
||||
lowercasePaths: boolean
|
||||
/** Whether to use created, modified, or published as the default type of date */
|
||||
defaultDateType: ValidDateType
|
||||
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
||||
|
||||
@ -11,10 +11,10 @@ const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
}
|
||||
|
||||
const copyFile = async (argv: Argv, fp: FilePath) => {
|
||||
const copyFile = async (argv: Argv, fp: FilePath, lowercasePaths: boolean) => {
|
||||
const src = joinSegments(argv.directory, fp) as FilePath
|
||||
|
||||
const name = slugifyFilePath(fp)
|
||||
const name = slugifyFilePath(fp, undefined, lowercasePaths)
|
||||
const dest = joinSegments(argv.output, name) as FilePath
|
||||
|
||||
// ensure dir exists
|
||||
@ -31,7 +31,7 @@ export const Assets: QuartzEmitterPlugin = () => {
|
||||
async *emit({ argv, cfg }) {
|
||||
const fps = await filesToCopy(argv, cfg)
|
||||
for (const fp of fps) {
|
||||
yield copyFile(argv, fp)
|
||||
yield copyFile(argv, fp, cfg.configuration.lowercasePaths)
|
||||
}
|
||||
},
|
||||
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
||||
@ -40,9 +40,13 @@ export const Assets: QuartzEmitterPlugin = () => {
|
||||
if (ext === ".md") continue
|
||||
|
||||
if (changeEvent.type === "add" || changeEvent.type === "change") {
|
||||
yield copyFile(ctx.argv, changeEvent.path)
|
||||
yield copyFile(ctx.argv, changeEvent.path, ctx.cfg.configuration.lowercasePaths)
|
||||
} else if (changeEvent.type === "delete") {
|
||||
const name = slugifyFilePath(changeEvent.path)
|
||||
const name = slugifyFilePath(
|
||||
changeEvent.path,
|
||||
undefined,
|
||||
ctx.cfg.configuration.lowercasePaths,
|
||||
)
|
||||
const dest = joinSegments(ctx.argv.output, name) as FilePath
|
||||
await fs.promises.unlink(dest)
|
||||
}
|
||||
|
||||
@ -78,7 +78,12 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
}
|
||||
|
||||
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, cfg.configuration.lowercasePaths)),
|
||||
),
|
||||
]
|
||||
|
||||
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
||||
if (aliases) {
|
||||
|
||||
@ -46,6 +46,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
const transformOptions: TransformOptions = {
|
||||
strategy: opts.markdownLinkResolution,
|
||||
allSlugs: ctx.allSlugs,
|
||||
lowercasePaths: ctx.cfg.configuration.lowercasePaths,
|
||||
}
|
||||
|
||||
visit(tree, "element", (node, _index, _parent) => {
|
||||
|
||||
@ -229,7 +229,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
// embed cases
|
||||
if (value.startsWith("!")) {
|
||||
const ext: string = path.extname(fp).toLowerCase()
|
||||
const url = slugifyFilePath(fp as FilePath)
|
||||
const url = slugifyFilePath(
|
||||
fp as FilePath,
|
||||
undefined,
|
||||
ctx.cfg.configuration.lowercasePaths,
|
||||
)
|
||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
|
||||
const match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
||||
const alt = match?.groups?.alt ?? ""
|
||||
@ -279,7 +283,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
// treat as broken link if slug not in ctx.allSlugs
|
||||
if (opts.disableBrokenWikilinks) {
|
||||
const slug = slugifyFilePath(fp as FilePath)
|
||||
const slug = slugifyFilePath(
|
||||
fp as FilePath,
|
||||
undefined,
|
||||
ctx.cfg.configuration.lowercasePaths,
|
||||
)
|
||||
const exists = ctx.allSlugs && ctx.allSlugs.includes(slug)
|
||||
if (!exists) {
|
||||
return {
|
||||
@ -342,7 +350,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
return false
|
||||
}
|
||||
|
||||
tag = slugTag(tag)
|
||||
tag = slugTag(tag, ctx.cfg.configuration.lowercasePaths)
|
||||
if (file.data.frontmatter) {
|
||||
const noteTags = file.data.frontmatter.tags ?? []
|
||||
file.data.frontmatter.tags = [...new Set([...noteTags, tag])]
|
||||
|
||||
@ -102,7 +102,11 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
||||
// base data properties that plugins may use
|
||||
file.data.filePath = file.path as FilePath
|
||||
file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath
|
||||
file.data.slug = slugifyFilePath(file.data.relativePath)
|
||||
file.data.slug = slugifyFilePath(
|
||||
file.data.relativePath,
|
||||
undefined,
|
||||
cfg.configuration.lowercasePaths,
|
||||
)
|
||||
|
||||
const ast = processor.parse(file)
|
||||
const newAst = await processor.run(ast, file)
|
||||
|
||||
@ -127,6 +127,29 @@ describe("transforms", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("slugifyFilePath with lowercase", () => {
|
||||
asserts(
|
||||
[
|
||||
["content/Index.md", "content/index"],
|
||||
["content/INDEX.html", "content/index"],
|
||||
["content/_Index.md", "content/index"],
|
||||
["/content/Index.md", "content/index"],
|
||||
["content/Cool.png", "content/cool.png"],
|
||||
["Index.md", "index"],
|
||||
["Test.mp4", "test.mp4"],
|
||||
["Note With Spaces.md", "note-with-spaces"],
|
||||
["Notes.With.Dots.md", "notes.with.dots"],
|
||||
["Test/Special Chars?.md", "test/special-chars"],
|
||||
["Test/Special Chars #3.md", "test/special-chars-3"],
|
||||
["Cool/What About R&D?.md", "cool/what-about-r-and-d"],
|
||||
["MixedCase/File.md", "mixedcase/file"],
|
||||
],
|
||||
(fp: path.FilePath) => path.slugifyFilePath(fp, undefined, true),
|
||||
path.isFilePath,
|
||||
path.isFullSlug,
|
||||
)
|
||||
})
|
||||
|
||||
test("transformInternalLink", () => {
|
||||
asserts(
|
||||
[
|
||||
@ -155,6 +178,27 @@ describe("transforms", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("transformInternalLink with lowercase", () => {
|
||||
asserts(
|
||||
[
|
||||
["Content", "./content"],
|
||||
["Content/Test.md", "./content/test"],
|
||||
["Content/Test.pdf", "./content/test.pdf"],
|
||||
["./Content/Test.md", "./content/test"],
|
||||
["../Content/Test.md", "../content/test"],
|
||||
["Tags/", "./tags/"],
|
||||
["/Tags/", "./tags/"],
|
||||
["Content/With Spaces", "./content/with-spaces"],
|
||||
["Content/With Spaces/index", "./content/with-spaces/"],
|
||||
["Content/With Spaces#And Anchor!", "./content/with-spaces#and-anchor"],
|
||||
["MixedCase/File.md", "./mixedcase/file"],
|
||||
],
|
||||
(link: string) => path.transformInternalLink(link, true),
|
||||
(_x: string): _x is string => true,
|
||||
path.isRelativeURL,
|
||||
)
|
||||
})
|
||||
|
||||
test("pathToRoot", () => {
|
||||
asserts(
|
||||
[
|
||||
|
||||
@ -54,8 +54,8 @@ export function getFullSlug(window: Window): FullSlug {
|
||||
return res
|
||||
}
|
||||
|
||||
function sluggify(s: string): string {
|
||||
return s
|
||||
function sluggify(s: string, lowercase?: boolean): string {
|
||||
const result = s
|
||||
.split("/")
|
||||
.map((segment) =>
|
||||
segment
|
||||
@ -67,9 +67,11 @@ function sluggify(s: string): string {
|
||||
)
|
||||
.join("/") // always use / as sep
|
||||
.replace(/\/$/, "")
|
||||
|
||||
return lowercase ? result.toLowerCase() : result
|
||||
}
|
||||
|
||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean, lowercase?: boolean): FullSlug {
|
||||
fp = stripSlashes(fp) as FilePath
|
||||
let ext = getFileExtension(fp)
|
||||
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
||||
@ -77,7 +79,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
ext = ""
|
||||
}
|
||||
|
||||
let slug = sluggify(withoutFileExt)
|
||||
let slug = sluggify(withoutFileExt, lowercase)
|
||||
|
||||
// treat _index as index
|
||||
if (endsWith(slug, "_index")) {
|
||||
@ -92,7 +94,7 @@ export function simplifySlug(fp: FullSlug): SimpleSlug {
|
||||
return (res.length === 0 ? "/" : res) as SimpleSlug
|
||||
}
|
||||
|
||||
export function transformInternalLink(link: string): RelativeURL {
|
||||
export function transformInternalLink(link: string, lowercase?: boolean): RelativeURL {
|
||||
let [fplike, anchor] = splitAnchor(decodeURI(link))
|
||||
|
||||
const folderPath = isFolderPath(fplike)
|
||||
@ -101,7 +103,7 @@ export function transformInternalLink(link: string): RelativeURL {
|
||||
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 simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath, undefined, lowercase))
|
||||
const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug))
|
||||
const trail = folderPath ? "/" : ""
|
||||
const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
|
||||
@ -182,10 +184,10 @@ export function splitAnchor(link: string): [string, string] {
|
||||
return [fp, anchor]
|
||||
}
|
||||
|
||||
export function slugTag(tag: string) {
|
||||
export function slugTag(tag: string, lowercase?: boolean) {
|
||||
return tag
|
||||
.split("/")
|
||||
.map((tagSegment) => sluggify(tagSegment))
|
||||
.map((tagSegment) => sluggify(tagSegment, lowercase))
|
||||
.join("/")
|
||||
}
|
||||
|
||||
@ -224,10 +226,11 @@ export function getAllSegmentPrefixes(tags: string): string[] {
|
||||
export interface TransformOptions {
|
||||
strategy: "absolute" | "relative" | "shortest"
|
||||
allSlugs: FullSlug[]
|
||||
lowercasePaths?: boolean
|
||||
}
|
||||
|
||||
export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
|
||||
let targetSlug = transformInternalLink(target)
|
||||
let targetSlug = transformInternalLink(target, opts.lowercasePaths)
|
||||
|
||||
if (opts.strategy === "relative") {
|
||||
return targetSlug as RelativeURL
|
||||
|
||||
Loading…
Reference in New Issue
Block a user