├── .npmrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yaml ├── content ├── null.md ├── Idempotency.md ├── OOP.md ├── Stack overflow.md ├── Stack.md ├── OOM.md ├── Silver bullet.md ├── ASCII.md ├── hjkl keys in vim │ └── adm-3a-hjkl-keyboard.jpg ├── psa.md ├── Algebraic data types.md ├── ADT.md ├── N+1 query problem.md ├── SOLID.md ├── Off-by-one error.md ├── gatekeeping.md ├── DRY.md ├── Footgun.md ├── IEEE 754.md ├── Conway's Law.md ├── Unix philosophy.md ├── Convention over configuration.md ├── Cache invalidation.md ├── Accidentally quadratic.md ├── Principle.md ├── hjkl keys in vim.md ├── Abstract data type.md ├── B-Tree.md ├── ORM.md ├── bikeshedding.md ├── i-n variables.md ├── Yak Shaving.md └── index.md ├── docs ├── plugins │ ├── index.md │ ├── NotFoundPage.md │ ├── ContentPage.md │ ├── CNAME.md │ ├── RemoveDrafts.md │ ├── ExplicitPublish.md │ ├── Assets.md │ ├── HardLineBreaks.md │ ├── ComponentResources.md │ ├── TagPage.md │ ├── Latex.md │ ├── FolderPage.md │ ├── AliasRedirects.md │ ├── Static.md │ ├── Frontmatter.md │ ├── SyntaxHighlighting.md │ ├── GitHubFlavoredMarkdown.md │ ├── CreatedModifiedDate.md │ ├── TableOfContents.md │ ├── Description.md │ ├── ContentIndex.md │ ├── OxHugoFlavoredMarkdown.md │ ├── CrawlLinks.md │ └── ObsidianFlavoredMarkdown.md ├── tags │ ├── plugin.md │ └── component.md ├── advanced │ └── index.md ├── features │ ├── index.md │ ├── Docker Support.md │ ├── RSS Feed.md │ ├── backlinks.md │ ├── SPA Routing.md │ ├── upcoming features.md │ ├── darkmode.md │ ├── table of contents.md │ ├── i18n.md │ ├── popover previews.md │ ├── Obsidian compatibility.md │ ├── Mermaid diagrams.md │ ├── OxHugo compatibility.md │ ├── wikilinks.md │ ├── recent notes.md │ ├── breadcrumbs.md │ ├── full-text search.md │ ├── folder and tag listings.md │ ├── private pages.md │ └── callouts.md ├── images │ ├── dns records.png │ ├── quartz layout.png │ ├── github-quick-setup.png │ ├── github-init-repo-options.png │ └── quartz transform pipeline.png ├── build.md ├── upgrading.md ├── showcase.md └── setting up your GitHub repository.md ├── .prettierignore ├── quartz ├── styles │ ├── custom.scss │ ├── variables.scss │ └── syntax.scss ├── static │ ├── icon.png │ └── og-image.png ├── plugins │ ├── filters │ │ ├── index.ts │ │ ├── explicit.ts │ │ └── draft.ts │ ├── transformers │ │ ├── linebreaks.ts │ │ ├── index.ts │ │ ├── syntax.ts │ │ ├── latex.ts │ │ ├── citations.ts │ │ ├── toc.ts │ │ └── gfm.ts │ ├── vfile.ts │ ├── emitters │ │ ├── index.ts │ │ ├── helpers.ts │ │ ├── cname.ts │ │ ├── static.ts │ │ ├── assets.ts │ │ └── 404.tsx │ ├── index.ts │ └── types.ts ├── bootstrap-worker.mjs ├── util │ ├── escape.ts │ ├── lang.ts │ ├── ctx.ts │ ├── perf.ts │ ├── glob.ts │ ├── sourcemap.ts │ ├── log.ts │ ├── jsx.tsx │ ├── trace.ts │ ├── resources.tsx │ └── theme.ts ├── components │ ├── styles │ │ ├── contentMeta.scss │ │ ├── footer.scss │ │ ├── backlinks.scss │ │ ├── breadcrumbs.scss │ │ ├── recentNotes.scss │ │ ├── legacyToc.scss │ │ ├── clipboard.scss │ │ ├── listPage.scss │ │ ├── darkmode.scss │ │ ├── toc.scss │ │ ├── graph.scss │ │ └── popover.scss │ ├── Spacer.tsx │ ├── Body.tsx │ ├── Header.tsx │ ├── pages │ │ ├── Content.tsx │ │ ├── 404.tsx │ │ └── FolderContent.tsx │ ├── ArticleTitle.tsx │ ├── MobileOnly.tsx │ ├── DesktopOnly.tsx │ ├── PageTitle.tsx │ ├── scripts │ │ ├── checkbox.inline.ts │ │ ├── util.ts │ │ ├── callout.inline.ts │ │ ├── toc.inline.ts │ │ ├── darkmode.inline.ts │ │ └── clipboard.inline.ts │ ├── types.ts │ ├── Date.tsx │ ├── Footer.tsx │ ├── index.ts │ ├── Backlinks.tsx │ ├── TagList.tsx │ ├── ContentMeta.tsx │ ├── Search.tsx │ └── Head.tsx ├── cli │ ├── constants.js │ └── helpers.js ├── worker.ts ├── processors │ ├── filter.ts │ └── emit.ts ├── bootstrap-cli.mjs └── i18n │ ├── index.ts │ └── locales │ ├── zh-CN.ts │ ├── definition.ts │ ├── ja-JP.ts │ ├── ko-KR.ts │ ├── fa-IR.ts │ ├── hu-HU.ts │ ├── en-US.ts │ ├── pt-BR.ts │ ├── vi-VN.ts │ ├── uk-UA.ts │ ├── it-IT.ts │ ├── de-DE.ts │ ├── es-ES.ts │ ├── pl-PL.ts │ ├── fr-FR.ts │ └── ar-SA.ts ├── .prettierrc ├── .gitignore ├── Dockerfile ├── index.d.ts ├── globals.d.ts ├── tsconfig.json ├── TODO.md ├── LICENSE.txt └── quartz.layout.ts /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jackyzha0] 2 | -------------------------------------------------------------------------------- /content/null.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | -------------------------------------------------------------------------------- /docs/plugins/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | --- 4 | -------------------------------------------------------------------------------- /docs/tags/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | --- 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public 2 | node_modules 3 | .quartz-cache 4 | -------------------------------------------------------------------------------- /docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Advanced" 3 | --- 4 | -------------------------------------------------------------------------------- /docs/features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Feature List 3 | --- 4 | -------------------------------------------------------------------------------- /content/Idempotency.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | Double billing -------------------------------------------------------------------------------- /content/OOP.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: [acronym, paradigm] 4 | --- 5 | -------------------------------------------------------------------------------- /quartz/styles/custom.scss: -------------------------------------------------------------------------------- 1 | @use "./base.scss"; 2 | 3 | // put your custom CSS here! 4 | -------------------------------------------------------------------------------- /content/Stack overflow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Stack overflow" 3 | draft: true 4 | tags: [error] 5 | --- 6 | -------------------------------------------------------------------------------- /quartz/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/quartz/static/icon.png -------------------------------------------------------------------------------- /content/Stack.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Stack" 3 | draft: true 4 | --- 5 | 6 | LIFO, stack of plates, linked list 7 | -------------------------------------------------------------------------------- /docs/images/dns records.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/docs/images/dns records.png -------------------------------------------------------------------------------- /quartz/static/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/quartz/static/og-image.png -------------------------------------------------------------------------------- /docs/images/quartz layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/docs/images/quartz layout.png -------------------------------------------------------------------------------- /content/OOM.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OOM" 3 | draft: false 4 | tags: [acronym, error] 5 | --- 6 | 7 | Out Of Memory error 8 | -------------------------------------------------------------------------------- /quartz/plugins/filters/index.ts: -------------------------------------------------------------------------------- 1 | export { RemoveDrafts } from "./draft" 2 | export { ExplicitPublish } from "./explicit" 3 | -------------------------------------------------------------------------------- /content/Silver bullet.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | https://worrydream.com/refs/Brooks_1986_-_No_Silver_Bullet.pdf 6 | -------------------------------------------------------------------------------- /docs/images/github-quick-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/docs/images/github-quick-setup.png -------------------------------------------------------------------------------- /docs/images/github-init-repo-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/docs/images/github-init-repo-options.png -------------------------------------------------------------------------------- /docs/images/quartz transform pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/docs/images/quartz transform pipeline.png -------------------------------------------------------------------------------- /content/ASCII.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ASCII" 3 | date: 2024-06-18T00:00:00+00:00 4 | draft: true 5 | tags: [legacy, acronym] 6 | year: 1963 7 | --- 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "quoteProps": "as-needed", 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /content/hjkl keys in vim/adm-3a-hjkl-keyboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/dev.wtf/main/content/hjkl keys in vim/adm-3a-hjkl-keyboard.jpg -------------------------------------------------------------------------------- /content/psa.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PSA" 3 | date: 2019-08-03T09:01:40+02:00 4 | draft: false 5 | tags: acronym 6 | --- 7 | 8 | Public Service Announcement 9 | -------------------------------------------------------------------------------- /content/Algebraic data types.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | [combinatorial species](https://bergeron.math.uqam.ca/wp-content/uploads/2013/11/book.pdf) 6 | -------------------------------------------------------------------------------- /content/ADT.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: [acronym] 4 | --- 5 | 6 | Ambiguous terminology, see: 7 | 8 | - [[Abstract data type]] 9 | - [[Algebraic data types]] 10 | -------------------------------------------------------------------------------- /content/N+1 query problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: easy-to-make-mistake 4 | --- 5 | 6 | Can be viewed as variation of [[Accidentally quadratic]]. 7 | 8 | see [[ORM]] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitignore 3 | node_modules 4 | public 5 | prof 6 | tsconfig.tsbuildinfo 7 | .obsidian 8 | .quartz-cache 9 | private/ 10 | .replit 11 | replit.nix 12 | -------------------------------------------------------------------------------- /docs/tags/component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Components 3 | --- 4 | 5 | Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information. 6 | -------------------------------------------------------------------------------- /content/SOLID.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: [acronym, principle] 4 | --- 5 | 6 | - Single responsibility 7 | - Open–closed 8 | - Liskov substitution 9 | - Interface segregation 10 | - Dependency inversion 11 | -------------------------------------------------------------------------------- /quartz/bootstrap-worker.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import workerpool from "workerpool" 3 | const cacheFile = "./.quartz-cache/transpiled-worker.mjs" 4 | const { parseFiles } = await import(cacheFile) 5 | workerpool.worker({ 6 | parseFiles, 7 | }) 8 | -------------------------------------------------------------------------------- /content/Off-by-one error.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: error 4 | --- 5 | 6 | > There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors. 7 | > 8 | > -- [Leon Bambrick](https://x.com/secretGeek/status/7269997868) 9 | -------------------------------------------------------------------------------- /quartz/util/escape.ts: -------------------------------------------------------------------------------- 1 | export const escapeHTML = (unsafe: string) => { 2 | return unsafe 3 | .replaceAll("&", "&") 4 | .replaceAll("<", "<") 5 | .replaceAll(">", ">") 6 | .replaceAll('"', """) 7 | .replaceAll("'", "'") 8 | } 9 | -------------------------------------------------------------------------------- /docs/features/Docker Support.md: -------------------------------------------------------------------------------- 1 | Quartz comes shipped with a Docker image that will allow you to preview your Quartz locally without installing Node. 2 | 3 | You can run the below one-liner to run Quartz in Docker. 4 | 5 | ```sh 6 | docker run --rm -itp 8080:8080 $(docker build -q .) 7 | ``` 8 | -------------------------------------------------------------------------------- /quartz/plugins/filters/explicit.ts: -------------------------------------------------------------------------------- 1 | import { QuartzFilterPlugin } from "../types" 2 | 3 | export const ExplicitPublish: QuartzFilterPlugin = () => ({ 4 | name: "ExplicitPublish", 5 | shouldPublish(_ctx, [_tree, vfile]) { 6 | return vfile.data?.frontmatter?.publish ?? false 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /quartz/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $pageWidth: 690px; 2 | $mobileBreakpoint: 600px; 3 | $tabletBreakpoint: 1000px; 4 | $sidePanelWidth: 340px; 5 | $topSpacing: 6rem; 6 | $fullPageWidth: $pageWidth + 2 * $sidePanelWidth; 7 | $boldWeight: 700; 8 | $semiBoldWeight: 600; 9 | $normalWeight: 400; 10 | -------------------------------------------------------------------------------- /content/gatekeeping.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Gatekeeping" 3 | date: 2019-08-03T09:13:03+02:00 4 | draft: false 5 | tags: slang 6 | --- 7 | 8 | When someone takes it upon themselves to decide who does or does not have access or rights to a community or identity. 9 | 10 | Source: urbandictionary 11 | -------------------------------------------------------------------------------- /quartz/components/styles/contentMeta.scss: -------------------------------------------------------------------------------- 1 | .content-meta { 2 | margin-top: 0; 3 | color: var(--gray); 4 | 5 | &[show-comma="true"] { 6 | > span:not(:last-child) { 7 | margin-right: 8px; 8 | 9 | &::after { 10 | content: ","; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim as builder 2 | WORKDIR /usr/src/app 3 | COPY package.json . 4 | COPY package-lock.json* . 5 | RUN npm ci 6 | 7 | FROM node:20-slim 8 | WORKDIR /usr/src/app 9 | COPY --from=builder /usr/src/app/ /usr/src/app/ 10 | COPY . . 11 | CMD ["npx", "quartz", "build", "--serve"] 12 | -------------------------------------------------------------------------------- /quartz/components/styles/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | text-align: left; 3 | margin-bottom: 4rem; 4 | opacity: 0.7; 5 | 6 | & ul { 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | display: flex; 11 | flex-direction: row; 12 | gap: 1rem; 13 | margin-top: -1rem; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss" { 2 | const content: string 3 | export = content 4 | } 5 | 6 | // dom custom event 7 | interface CustomEventMap { 8 | nav: CustomEvent<{ url: FullSlug }> 9 | themechange: CustomEvent<{ theme: "light" | "dark" }> 10 | } 11 | 12 | declare const fetchData: Promise 13 | -------------------------------------------------------------------------------- /content/DRY.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: [acronym, principle] 4 | --- 5 | 6 | > Duplication is far cheaper than the wrong abstraction. 7 | > 8 | > -- [Sandi Metz](http://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction) 9 | 10 | - http://jamie-wong.com/2013/07/12/grep-test/ 11 | - https://www.youtube.com/watch?v=XpDsk374LDE -------------------------------------------------------------------------------- /content/Footgun.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Footgun" 3 | date: 2024-06-19T00:00:00+00:00 4 | draft: false 5 | tags: slang 6 | --- 7 | 8 | Feature likely to lead to the programmer [shooting themselves in the foot](https://en.wiktionary.org/wiki/shoot_oneself_in_the_foot). 9 | 10 | Source: [wiktionary](https://en.wiktionary.org/wiki/footgun) 11 | -------------------------------------------------------------------------------- /quartz/plugins/filters/draft.ts: -------------------------------------------------------------------------------- 1 | import { QuartzFilterPlugin } from "../types" 2 | 3 | export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ 4 | name: "RemoveDrafts", 5 | shouldPublish(_ctx, [_tree, vfile]) { 6 | const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false 7 | return !draftFlag 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/linebreaks.ts: -------------------------------------------------------------------------------- 1 | import { QuartzTransformerPlugin } from "../types" 2 | import remarkBreaks from "remark-breaks" 3 | 4 | export const HardLineBreaks: QuartzTransformerPlugin = () => { 5 | return { 6 | name: "HardLineBreaks", 7 | markdownPlugins() { 8 | return [remarkBreaks] 9 | }, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /content/IEEE 754.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | 0.1 + 0.2 = 0.30000000000000004 6 | 7 | https://0.30000000000000004.com/ 8 | 9 | [Posits: An Alternative to Floating Point Calculations](https://repository.rit.edu/cgi/viewcontent.cgi?article=11516&context=theses) 10 | 11 | https://cloud.google.com/tpu/docs/bfloat16 12 | 13 | https://gmplib.org/ 14 | -------------------------------------------------------------------------------- /quartz/components/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | import { classNames } from "../util/lang" 3 | 4 | function Spacer({ displayClass }: QuartzComponentProps) { 5 | return
6 | } 7 | 8 | export default (() => Spacer) satisfies QuartzComponentConstructor 9 | -------------------------------------------------------------------------------- /quartz/components/styles/backlinks.scss: -------------------------------------------------------------------------------- 1 | .backlinks { 2 | position: relative; 3 | 4 | & > h3 { 5 | font-size: 1rem; 6 | margin: 0; 7 | } 8 | 9 | & > ul { 10 | list-style: none; 11 | padding: 0; 12 | margin: 0.5rem 0; 13 | 14 | & > li { 15 | & > a { 16 | background-color: transparent; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /quartz/util/lang.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(s: string): string { 2 | return s.substring(0, 1).toUpperCase() + s.substring(1) 3 | } 4 | 5 | export function classNames( 6 | displayClass?: "mobile-only" | "desktop-only", 7 | ...classes: string[] 8 | ): string { 9 | if (displayClass) { 10 | classes.push(displayClass) 11 | } 12 | return classes.join(" ") 13 | } 14 | -------------------------------------------------------------------------------- /content/Conway's Law.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Conway's Law" 3 | date: 2019-08-24T23:27:21+02:00 4 | draft: false 5 | tags: [adage] 6 | --- 7 | 8 | > organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations. 9 | > 10 | > -- Melvin Edward Conway 11 | 12 | **Note**: Author of Game of Life is John Horton Conway. 13 | -------------------------------------------------------------------------------- /docs/features/RSS Feed.md: -------------------------------------------------------------------------------- 1 | Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly. 2 | 3 | ## Configuration 4 | 5 | This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options. 6 | -------------------------------------------------------------------------------- /quartz/plugins/vfile.ts: -------------------------------------------------------------------------------- 1 | import { Node, Parent } from "hast" 2 | import { Data, VFile } from "vfile" 3 | 4 | export type QuartzPluginData = Data 5 | export type ProcessedContent = [Node, VFile] 6 | 7 | export function defaultProcessedContent(vfileData: Partial): ProcessedContent { 8 | const root: Parent = { type: "root", children: [] } 9 | const vfile = new VFile("") 10 | vfile.data = vfileData 11 | return [root, vfile] 12 | } 13 | -------------------------------------------------------------------------------- /quartz/styles/syntax.scss: -------------------------------------------------------------------------------- 1 | code[data-theme*=" "] { 2 | color: var(--shiki-light); 3 | background-color: var(--shiki-light-bg); 4 | } 5 | 6 | code[data-theme*=" "] span { 7 | color: var(--shiki-light); 8 | } 9 | 10 | [saved-theme="dark"] code[data-theme*=" "] { 11 | color: var(--shiki-dark); 12 | background-color: var(--shiki-dark-bg); 13 | } 14 | 15 | [saved-theme="dark"] code[data-theme*=" "] span { 16 | color: var(--shiki-dark); 17 | } 18 | -------------------------------------------------------------------------------- /content/Unix philosophy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Unix philosophy" 3 | date: 2024-06-17T00:00:00+00:00 4 | draft: false 5 | tags: [principle] 6 | --- 7 | 8 | > This is the Unix philosophy: **Write programs that do one thing and do it well. Write programs to work together. Write programs to handle** text streams, because that is a **universal interface**. 9 | > 10 | > -- [Richard M. Stallman. The GNU Manifesto](http://www.catb.org/esr/writings/taoup/html/ch01s06.html) 11 | -------------------------------------------------------------------------------- /quartz/util/ctx.ts: -------------------------------------------------------------------------------- 1 | import { QuartzConfig } from "../cfg" 2 | import { FullSlug } from "./path" 3 | 4 | export interface Argv { 5 | directory: string 6 | verbose: boolean 7 | output: string 8 | serve: boolean 9 | fastRebuild: boolean 10 | port: number 11 | wsPort: number 12 | remoteDevHost?: string 13 | concurrency?: number 14 | } 15 | 16 | export interface BuildCtx { 17 | argv: Argv 18 | cfg: QuartzConfig 19 | allSlugs: FullSlug[] 20 | } 21 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | export declare global { 2 | interface Document { 3 | addEventListener( 4 | type: K, 5 | listener: (this: Document, ev: CustomEventMap[K]) => void, 6 | ): void 7 | dispatchEvent(ev: CustomEventMap[K] | UIEvent): void 8 | } 9 | interface Window { 10 | spaNavigate(url: URL, isBack: boolean = false) 11 | addCleanup(fn: (...args: any[]) => void) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/index.ts: -------------------------------------------------------------------------------- 1 | export { ContentPage } from "./contentPage" 2 | export { TagPage } from "./tagPage" 3 | export { FolderPage } from "./folderPage" 4 | export { ContentIndex } from "./contentIndex" 5 | export { AliasRedirects } from "./aliases" 6 | export { Assets } from "./assets" 7 | export { Static } from "./static" 8 | export { ComponentResources } from "./componentResources" 9 | export { NotFoundPage } from "./404" 10 | export { CNAME } from "./cname" 11 | -------------------------------------------------------------------------------- /content/Convention over configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Convention Over Configuration" 3 | date: 2024-06-19T00:00:00+00:00 4 | draft: false 5 | tags: principle 6 | --- 7 | 8 | Convention Over Configuration is a software design principle that emphasizes the use of predefined conventions to simplify the development process, reducing the need for excessive configuration. 9 | 10 | See also: [Ruby on Rails doctrine](https://rubyonrails.org/doctrine#convention-over-configuration). 11 | -------------------------------------------------------------------------------- /quartz/components/styles/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb-container { 2 | margin: 0; 3 | margin-top: 0.75rem; 4 | padding: 0; 5 | display: flex; 6 | flex-direction: row; 7 | flex-wrap: wrap; 8 | gap: 0.5rem; 9 | } 10 | 11 | .breadcrumb-element { 12 | p { 13 | margin: 0; 14 | margin-left: 0.5rem; 15 | padding: 0; 16 | line-height: normal; 17 | } 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | -------------------------------------------------------------------------------- /quartz/util/perf.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import pretty from "pretty-time" 3 | 4 | export class PerfTimer { 5 | evts: { [key: string]: [number, number] } 6 | 7 | constructor() { 8 | this.evts = {} 9 | this.addEvent("start") 10 | } 11 | 12 | addEvent(evtName: string) { 13 | this.evts[evtName] = process.hrtime() 14 | } 15 | 16 | timeSince(evtName?: string): string { 17 | return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"]))) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /content/Cache invalidation.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | > There are only two hard things in Computer Science: cache invalidation and naming things. 6 | > 7 | > -- Phil Karlton 8 | 9 | 10 | -------------------------------------------------------------------------------- /quartz/components/styles/recentNotes.scss: -------------------------------------------------------------------------------- 1 | .recent-notes { 2 | & > h3 { 3 | margin: 0.5rem 0 0 0; 4 | font-size: 1rem; 5 | } 6 | 7 | & > ul.recent-ul { 8 | list-style: none; 9 | margin-top: 1rem; 10 | padding-left: 0; 11 | 12 | & > li { 13 | margin: 1rem 0; 14 | .section > .desc > h3 > a { 15 | background-color: transparent; 16 | } 17 | 18 | .section > .meta { 19 | margin: 0 0 0.5rem 0; 20 | opacity: 0.6; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /quartz/components/styles/legacyToc.scss: -------------------------------------------------------------------------------- 1 | details#toc { 2 | & summary { 3 | cursor: pointer; 4 | 5 | &::marker { 6 | color: var(--dark); 7 | } 8 | 9 | & > * { 10 | padding-left: 0.25rem; 11 | display: inline-block; 12 | margin: 0; 13 | } 14 | } 15 | 16 | & ul { 17 | list-style: none; 18 | margin: 0.5rem 1.25rem; 19 | padding: 0; 20 | } 21 | 22 | @for $i from 1 through 6 { 23 | & .depth-#{$i} { 24 | padding-left: calc(1rem * #{$i}); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quartz/components/Body.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import clipboardScript from "./scripts/clipboard.inline" 3 | import clipboardStyle from "./styles/clipboard.scss" 4 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 5 | 6 | const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { 7 | return
{children}
8 | } 9 | 10 | Body.afterDOMLoaded = clipboardScript 11 | Body.css = clipboardStyle 12 | 13 | export default (() => Body) satisfies QuartzComponentConstructor 14 | -------------------------------------------------------------------------------- /docs/features/backlinks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Backlinks 3 | tags: 4 | - component 5 | --- 6 | 7 | A backlink for a note is a link from another note to that note. Links in the backlink pane also feature rich [[popover previews]] if you have that feature enabled. 8 | 9 | ## Customization 10 | 11 | - Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`. 12 | - Component: `quartz/components/Backlinks.tsx` 13 | - Style: `quartz/components/styles/backlinks.scss` 14 | - Script: `quartz/components/scripts/search.inline.ts` 15 | -------------------------------------------------------------------------------- /quartz/util/glob.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { FilePath } from "./path" 3 | import { globby } from "globby" 4 | 5 | export function toPosixPath(fp: string): string { 6 | return fp.split(path.sep).join("/") 7 | } 8 | 9 | export async function glob( 10 | pattern: string, 11 | cwd: string, 12 | ignorePatterns: string[], 13 | ): Promise { 14 | const fps = ( 15 | await globby(pattern, { 16 | cwd, 17 | ignore: ignorePatterns, 18 | gitignore: true, 19 | }) 20 | ).map(toPosixPath) 21 | return fps as FilePath[] 22 | } 23 | -------------------------------------------------------------------------------- /quartz/util/sourcemap.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import sourceMapSupport from "source-map-support" 3 | import { fileURLToPath } from "url" 4 | 5 | export const options: sourceMapSupport.Options = { 6 | // source map hack to get around query param 7 | // import cache busting 8 | retrieveSourceMap(source) { 9 | if (source.includes(".quartz-cache")) { 10 | let realSource = fileURLToPath(source.split("?", 2)[0] + ".map") 11 | return { 12 | map: fs.readFileSync(realSource, "utf8"), 13 | } 14 | } else { 15 | return null 16 | } 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /docs/plugins/NotFoundPage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NotFoundPage 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits a 404 (Not Found) page for broken or non-existent URLs. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Emitter 17 | - Function name: `Plugin.NotFoundPage()`. 18 | - Source: [`quartz/plugins/emitters/404.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/404.tsx). 19 | -------------------------------------------------------------------------------- /quartz/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | 3 | const Header: QuartzComponent = ({ children }: QuartzComponentProps) => { 4 | return children.length > 0 ?
{children}
: null 5 | } 6 | 7 | Header.css = ` 8 | header { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | margin: 2rem 0; 13 | gap: 1.5rem; 14 | } 15 | 16 | header h1 { 17 | margin: 0; 18 | flex: auto; 19 | } 20 | ` 21 | 22 | export default (() => Header) satisfies QuartzComponentConstructor 23 | -------------------------------------------------------------------------------- /quartz/components/pages/Content.tsx: -------------------------------------------------------------------------------- 1 | import { htmlToJsx } from "../../util/jsx" 2 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" 3 | 4 | const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { 5 | const content = htmlToJsx(fileData.filePath!, tree) 6 | const classes: string[] = fileData.frontmatter?.cssclasses ?? [] 7 | const classString = ["popover-hint", ...classes].join(" ") 8 | return
{content}
9 | } 10 | 11 | export default (() => Content) satisfies QuartzComponentConstructor 12 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export { FrontMatter } from "./frontmatter" 2 | export { GitHubFlavoredMarkdown } from "./gfm" 3 | export { Citations } from "./citations" 4 | export { CreatedModifiedDate } from "./lastmod" 5 | export { Latex } from "./latex" 6 | export { Description } from "./description" 7 | export { CrawlLinks } from "./links" 8 | export { ObsidianFlavoredMarkdown } from "./ofm" 9 | export { OxHugoFlavouredMarkdown } from "./oxhugofm" 10 | export { SyntaxHighlighting } from "./syntax" 11 | export { TableOfContents } from "./toc" 12 | export { HardLineBreaks } from "./linebreaks" 13 | -------------------------------------------------------------------------------- /content/Accidentally quadratic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Accidentally quadratic" 3 | date: 2024-06-19T00:00:00+00:00 4 | draft: false 5 | tags: easy-to-make-mistake 6 | --- 7 | 8 | Alogrithm that can be implemented in linear time (or space) $0(n)$ instead implemented as quadratic $0(n^2)$. Often happens by accident or due to inattentiveness. 9 | 10 | It is very easy to do this mistake by accident. For example, you have function that runs in linear time and you call this function inside the loop - and you have quadratic algorithm. 11 | 12 | Terminology taken [here](https://www.tumblr.com/accidentallyquadratic). 13 | -------------------------------------------------------------------------------- /content/Principle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Principle" 3 | date: 2024-06-29T00:00:00+00:00 4 | --- 5 | 6 | > A wizard's staff poked out. The chieftain saw the knob on the end. 7 | > "Now, then," he said, pleasantly. "I know the rules. Wizards aren't allowed to use magic against civilians except in genuine life-threatening situa-" 8 | > There was a burst of octarine light. 9 | > "Actually, it's not a rule," said Ridcully. "It's more a guideline." 10 | > 11 | > -- Lords and Ladies, Novel by Terry Pratchett 12 | 13 | There are a lot of programming principles such as DRY, SOLID, KISS etc. Just keep in mind they are not rules, they are more guidelines. 14 | -------------------------------------------------------------------------------- /quartz/components/ArticleTitle.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | import { classNames } from "../util/lang" 3 | 4 | const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { 5 | const title = fileData.frontmatter?.title 6 | if (title) { 7 | return

{title}

8 | } else { 9 | return null 10 | } 11 | } 12 | 13 | ArticleTitle.css = ` 14 | .article-title { 15 | margin: 2rem 0 0 0; 16 | } 17 | ` 18 | 19 | export default (() => ArticleTitle) satisfies QuartzComponentConstructor 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext", "DOM", "DOM.Iterable"], 4 | "experimentalDecorators": true, 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "incremental": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "preact", 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx", "./package.json"], 19 | "exclude": ["build/**/*.d.ts"], 20 | } 21 | -------------------------------------------------------------------------------- /docs/features/SPA Routing.md: -------------------------------------------------------------------------------- 1 | Single-page-app style rendering. This prevents flashes of unstyled content and improves the smoothness of Quartz. 2 | 3 | Under the hood, this is done by hijacking page navigations and instead fetching the HTML via a `GET` request and then diffing and selectively replacing parts of the page using [micromorph](https://github.com/natemoo-re/micromorph). This allows us to change the content of the page without fully refreshing the page, reducing the amount of content that the browser needs to load. 4 | 5 | ## Configuration 6 | 7 | - Disable SPA Routing: set the `enableSPA` field of the [[configuration]] in `quartz.config.ts` to be `false`. 8 | -------------------------------------------------------------------------------- /quartz/cli/constants.js: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { readFileSync } from "fs" 3 | 4 | /** 5 | * All constants relating to helpers or handlers 6 | */ 7 | export const ORIGIN_NAME = "origin" 8 | export const UPSTREAM_NAME = "upstream" 9 | export const QUARTZ_SOURCE_BRANCH = "v4" 10 | export const cwd = process.cwd() 11 | export const cacheDir = path.join(cwd, ".quartz-cache") 12 | export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs" 13 | export const fp = "./quartz/build.ts" 14 | export const { version } = JSON.parse(readFileSync("./package.json").toString()) 15 | export const contentCacheFolder = path.join(cacheDir, "content-cache") 16 | -------------------------------------------------------------------------------- /quartz/util/log.ts: -------------------------------------------------------------------------------- 1 | import { Spinner } from "cli-spinner" 2 | 3 | export class QuartzLogger { 4 | verbose: boolean 5 | spinner: Spinner | undefined 6 | constructor(verbose: boolean) { 7 | this.verbose = verbose 8 | } 9 | 10 | start(text: string) { 11 | if (this.verbose) { 12 | console.log(text) 13 | } else { 14 | this.spinner = new Spinner(`%s ${text}`) 15 | this.spinner.setSpinnerString(18) 16 | this.spinner.start() 17 | } 18 | } 19 | 20 | end(text?: string) { 21 | if (!this.verbose) { 22 | this.spinner!.stop(true) 23 | } 24 | if (text) { 25 | console.log(text) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /content/hjkl keys in vim.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "hjkl keys in vim" 3 | date: 2024-06-18T00:00:00+00:00 4 | draft: false 5 | tags: legacy 6 | year: 1976 7 | --- 8 | 9 | Have you ever wondered why vim (vi) uses `hjkl` for moving the cursor? 10 | 11 | [Bill Joy](http://en.wikipedia.org/wiki/Bill_Joy) created the vi text editor when he used the [ADM-3A](http://en.wikipedia.org/wiki/ADM-3A) terminal, which had the arrows on `hjkl` keys. 12 | 13 | ![ADM-3A keyboard](./hjkl%20keys%20in%20vim/adm-3a-hjkl-keyboard.jpg) 14 | 15 | It is also believed that UNIX convention for using tilde (`~`) for "home" folder also comes from this keyboard. 16 | 17 | **Related**: Larry Tesler aka [nomodes](https://www.nomodes.com/). 18 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/helpers.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import fs from "fs" 3 | import { BuildCtx } from "../../util/ctx" 4 | import { FilePath, FullSlug, joinSegments } from "../../util/path" 5 | 6 | type WriteOptions = { 7 | ctx: BuildCtx 8 | slug: FullSlug 9 | ext: `.${string}` | "" 10 | content: string | Buffer 11 | } 12 | 13 | export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise => { 14 | const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath 15 | const dir = path.dirname(pathToPage) 16 | await fs.promises.mkdir(dir, { recursive: true }) 17 | await fs.promises.writeFile(pathToPage, content) 18 | return pathToPage 19 | } 20 | -------------------------------------------------------------------------------- /content/Abstract data type.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | Abstract Data Type (ADT) is a data type, where only behavior is defined, but not implementation. 6 | 7 | - https://xlinux.nist.gov/dads/HTML/abstractDataType.html 8 | - https://web.cecs.pdx.edu/~sheard/course/Cs163/Doc/AbstractDataTypes.html 9 | - https://web.mit.edu/6.031/www/sp19/classes/10-abstract-data-types/ 10 | - https://eng.libretexts.org/Bookshelves/Computer_Science/Databases_and_Data_Structures/Book%3A_Data_Structure_and_Algorithms_(Njoroge)/03%3A_Basic_Data_Structures_and_Abstract_Data_Types/3.02%3A_Activity_2_-_Abstract_Data_Type 11 | 12 | Related: 13 | 14 | - interfaces in languages like Java and TypeScript 15 | - classes in Haskel 16 | -------------------------------------------------------------------------------- /quartz/worker.ts: -------------------------------------------------------------------------------- 1 | import sourceMapSupport from "source-map-support" 2 | sourceMapSupport.install(options) 3 | import cfg from "../quartz.config" 4 | import { Argv, BuildCtx } from "./util/ctx" 5 | import { FilePath, FullSlug } from "./util/path" 6 | import { createFileParser, createProcessor } from "./processors/parse" 7 | import { options } from "./util/sourcemap" 8 | 9 | // only called from worker thread 10 | export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) { 11 | const ctx: BuildCtx = { 12 | cfg, 13 | argv, 14 | allSlugs, 15 | } 16 | const processor = createProcessor(ctx) 17 | const parse = createFileParser(ctx, fps) 18 | return parse(processor) 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or improvement for Quartz 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /docs/plugins/ContentPage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ContentPage 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin is a core component of the Quartz framework. It generates the HTML pages for each piece of Markdown content. It emits the full-page [[layout]], including headers, footers, and body content, among others. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Emitter 17 | - Function name: `Plugin.ContentPage()`. 18 | - Source: [`quartz/plugins/emitters/contentPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentPage.tsx). 19 | -------------------------------------------------------------------------------- /quartz/components/styles/clipboard.scss: -------------------------------------------------------------------------------- 1 | .clipboard-button { 2 | position: absolute; 3 | display: flex; 4 | float: right; 5 | right: 0; 6 | padding: 0.4rem; 7 | margin: 0.3rem; 8 | color: var(--gray); 9 | border-color: var(--dark); 10 | background-color: var(--light); 11 | border: 1px solid; 12 | border-radius: 5px; 13 | opacity: 0; 14 | transition: 0.2s; 15 | 16 | & > svg { 17 | fill: var(--light); 18 | filter: contrast(0.3); 19 | } 20 | 21 | &:hover { 22 | cursor: pointer; 23 | border-color: var(--secondary); 24 | } 25 | 26 | &:focus { 27 | outline: 0; 28 | } 29 | } 30 | 31 | pre { 32 | &:hover > .clipboard-button { 33 | opacity: 1; 34 | transition: 0.2s; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /quartz/components/MobileOnly.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | 3 | export default ((component?: QuartzComponent) => { 4 | if (component) { 5 | const Component = component 6 | const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { 7 | return 8 | } 9 | 10 | MobileOnly.displayName = component.displayName 11 | MobileOnly.afterDOMLoaded = component?.afterDOMLoaded 12 | MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded 13 | MobileOnly.css = component?.css 14 | return MobileOnly 15 | } else { 16 | return () => <> 17 | } 18 | }) satisfies QuartzComponentConstructor 19 | -------------------------------------------------------------------------------- /quartz/components/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { i18n } from "../../i18n" 2 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" 3 | 4 | const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => { 5 | // If baseUrl contains a pathname after the domain, use this as the home link 6 | const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) 7 | const baseDir = url.pathname 8 | 9 | return ( 10 | 15 | ) 16 | } 17 | 18 | export default (() => NotFound) satisfies QuartzComponentConstructor 19 | -------------------------------------------------------------------------------- /quartz/components/DesktopOnly.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | 3 | export default ((component?: QuartzComponent) => { 4 | if (component) { 5 | const Component = component 6 | const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { 7 | return 8 | } 9 | 10 | DesktopOnly.displayName = component.displayName 11 | DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded 12 | DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded 13 | DesktopOnly.css = component?.css 14 | return DesktopOnly 15 | } else { 16 | return () => <> 17 | } 18 | }) satisfies QuartzComponentConstructor 19 | -------------------------------------------------------------------------------- /docs/plugins/CNAME.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CNAME 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits a `CNAME` record that points your subdomain to the default domain of your site. 8 | 9 | If you want to use a custom domain name like `quartz.example.com` for the site, then this is needed. 10 | 11 | See [[hosting|Hosting]] for more information. 12 | 13 | > [!note] 14 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 15 | 16 | This plugin has no configuration options. 17 | 18 | ## API 19 | 20 | - Category: Emitter 21 | - Function name: `Plugin.CNAME()`. 22 | - Source: [`quartz/plugins/emitters/cname.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/cname.ts). 23 | -------------------------------------------------------------------------------- /docs/plugins/RemoveDrafts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RemoveDrafts 3 | tags: 4 | - plugin/filter 5 | --- 6 | 7 | This plugin filters out content from your vault, so that only finalized content is made available. This prevents [[private pages]] from being published. By default, it filters out all pages with `draft: true` in the frontmatter and leaves all other pages intact. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Filter 17 | - Function name: `Plugin.RemoveDrafts()`. 18 | - Source: [`quartz/plugins/filters/draft.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/draft.ts). 19 | -------------------------------------------------------------------------------- /docs/plugins/ExplicitPublish.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ExplicitPublish 3 | tags: 4 | - plugin/filter 5 | --- 6 | 7 | This plugin filters content based on an explicit `publish` flag in the frontmatter, allowing only content that is explicitly marked for publication to pass through. It's the opt-in version of [[RemoveDrafts]]. See [[private pages]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Filter 17 | - Function name: `Plugin.ExplicitPublish()`. 18 | - Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts). 19 | -------------------------------------------------------------------------------- /quartz/components/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import { pathToRoot } from "../util/path" 2 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 | import { classNames } from "../util/lang" 4 | import { i18n } from "../i18n" 5 | 6 | const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => { 7 | const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title 8 | const baseDir = pathToRoot(fileData.slug!) 9 | return ( 10 |

11 | {title} 12 |

13 | ) 14 | } 15 | 16 | PageTitle.css = ` 17 | .page-title { 18 | margin: 0; 19 | } 20 | ` 21 | 22 | export default (() => PageTitle) satisfies QuartzComponentConstructor 23 | -------------------------------------------------------------------------------- /docs/features/upcoming features.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | ## high priority backlog 6 | 7 | - static dead link detection 8 | - block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note 9 | - note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files 10 | - docker support 11 | 12 | ## misc backlog 13 | 14 | - breadcrumbs component 15 | - cursor chat extension 16 | - https://giscus.app/ extension 17 | - sidenotes? https://github.com/capnfabs/paperesque 18 | - direct match in search using double quotes 19 | - https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI 20 | - audio/video embed styling 21 | - Canvas 22 | - parse all images in page: use this for page lists if applicable? 23 | - CV mode? with print stylesheet 24 | -------------------------------------------------------------------------------- /docs/plugins/Assets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Assets 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` in the global [[configuration]]. 8 | 9 | Note that all static assets will then be accessible through its path on your generated site, i.e: `host.me/path/to/static.pdf` 10 | 11 | > [!note] 12 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 13 | 14 | This plugin has no configuration options. 15 | 16 | ## API 17 | 18 | - Category: Emitter 19 | - Function name: `Plugin.Assets()`. 20 | - Source: [`quartz/plugins/emitters/assets.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/assets.ts). 21 | -------------------------------------------------------------------------------- /docs/plugins/HardLineBreaks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HardLineBreaks 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin automatically converts single line breaks in Markdown text into hard line breaks in the HTML output. This plugin is not enabled by default as this doesn't follow the semantics of actual Markdown but you may enable it if you'd like parity with [[Obsidian compatibility|Obsidian]]. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Transformer 17 | - Function name: `Plugin.HardLineBreaks()`. 18 | - Source: [`quartz/plugins/transformers/linebreaks.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/linebreaks.ts). 19 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [ ] https://github.com/paulirish/lite-youtube-embed 4 | - [ ] show recently changed files at the home page 5 | - [ ] show list of file on the tag page with content, like `tags/internet-slang` 6 | - [ ] "edit on github" link 7 | - [ ] last updated based on git 8 | 9 | nobody cares, but `acronym` technically is wrong name for this, it is rather `initialism` 10 | 11 | | Abbreviations | Acronyms | Initialisms | 12 | | ------------------- | ----------------------------------------------------- | ------------------------------ | 13 | | inc. (incorporated) | scuba (self-contained underwater breathing apparatus) | NFL (National Football League) | 14 | | est. (established) | POTUS (President of the United States) | idk (I don't know) | 15 | -------------------------------------------------------------------------------- /docs/plugins/ComponentResources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ComponentResources 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin manages and emits the static resources required for the Quartz framework. This includes CSS stylesheets and JavaScript scripts that enhance the functionality and aesthetics of the generated site. See also the `cdnCaching` option in the `theme` section of the [[configuration]]. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | ## API 15 | 16 | - Category: Emitter 17 | - Function name: `Plugin.ComponentResources()`. 18 | - Source: [`quartz/plugins/emitters/componentResources.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/componentResources.ts). 19 | -------------------------------------------------------------------------------- /quartz/util/jsx.tsx: -------------------------------------------------------------------------------- 1 | import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime" 2 | import { Node, Root } from "hast" 3 | import { Fragment, jsx, jsxs } from "preact/jsx-runtime" 4 | import { trace } from "./trace" 5 | import { type FilePath } from "./path" 6 | 7 | const customComponents: Components = { 8 | table: (props) => ( 9 |
10 | 11 | 12 | ), 13 | } 14 | 15 | export function htmlToJsx(fp: FilePath, tree: Node) { 16 | try { 17 | return toJsxRuntime(tree as Root, { 18 | Fragment, 19 | jsx: jsx as Jsx, 20 | jsxs: jsxs as Jsx, 21 | elementAttributeNameCase: "html", 22 | components: customComponents, 23 | }) 24 | } catch (e) { 25 | trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quartz/components/styles/listPage.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/variables.scss" as *; 2 | 3 | ul.section-ul { 4 | list-style: none; 5 | margin-top: 2em; 6 | padding-left: 0; 7 | } 8 | 9 | li.section-li { 10 | margin-bottom: 1em; 11 | 12 | & > .section { 13 | display: grid; 14 | grid-template-columns: 6em 3fr 1fr; 15 | 16 | @media all and (max-width: $mobileBreakpoint) { 17 | & > .tags { 18 | display: none; 19 | } 20 | } 21 | 22 | & > .desc > h3 > a { 23 | background-color: transparent; 24 | } 25 | 26 | & > .meta { 27 | margin: 0; 28 | flex-basis: 6em; 29 | opacity: 0.6; 30 | } 31 | } 32 | } 33 | 34 | // modifications in popover context 35 | .popover .section { 36 | grid-template-columns: 6em 1fr !important; 37 | & > .tags { 38 | display: none; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/plugins/TagPage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TagPage 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits dedicated pages for each tag used in the content. See [[folder and tag listings]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin has no configuration options. 13 | 14 | The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`). 15 | 16 | ## API 17 | 18 | - Category: Emitter 19 | - Function name: `Plugin.TagPage()`. 20 | - Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx). 21 | -------------------------------------------------------------------------------- /docs/plugins/Latex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Latex" 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX. 15 | 16 | ## API 17 | 18 | - Category: Transformer 19 | - Function name: `Plugin.Latex()`. 20 | - Source: [`quartz/plugins/transformers/latex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/latex.ts). 21 | -------------------------------------------------------------------------------- /quartz/components/scripts/checkbox.inline.ts: -------------------------------------------------------------------------------- 1 | import { getFullSlug } from "../../util/path" 2 | 3 | const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}` 4 | 5 | document.addEventListener("nav", () => { 6 | const checkboxes = document.querySelectorAll( 7 | "input.checkbox-toggle", 8 | ) as NodeListOf 9 | checkboxes.forEach((el, index) => { 10 | const elId = checkboxId(index) 11 | 12 | const switchState = (e: Event) => { 13 | const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false" 14 | localStorage.setItem(elId, newCheckboxState) 15 | } 16 | 17 | el.addEventListener("change", switchState) 18 | window.addCleanup(() => el.removeEventListener("change", switchState)) 19 | if (localStorage.getItem(elId) === "true") { 20 | el.checked = true 21 | } 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /quartz/components/scripts/util.ts: -------------------------------------------------------------------------------- 1 | export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) { 2 | if (!outsideContainer) return 3 | function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { 4 | if (e.target !== this) return 5 | e.preventDefault() 6 | cb() 7 | } 8 | 9 | function esc(e: HTMLElementEventMap["keydown"]) { 10 | if (!e.key.startsWith("Esc")) return 11 | e.preventDefault() 12 | cb() 13 | } 14 | 15 | outsideContainer?.addEventListener("click", click) 16 | window.addCleanup(() => outsideContainer?.removeEventListener("click", click)) 17 | document.addEventListener("keydown", esc) 18 | window.addCleanup(() => document.removeEventListener("keydown", esc)) 19 | } 20 | 21 | export function removeAllChildren(node: HTMLElement) { 22 | while (node.firstChild) { 23 | node.removeChild(node.firstChild) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/syntax.ts: -------------------------------------------------------------------------------- 1 | import { QuartzTransformerPlugin } from "../types" 2 | import rehypePrettyCode, { Options as CodeOptions, Theme as CodeTheme } from "rehype-pretty-code" 3 | 4 | interface Theme extends Record { 5 | light: CodeTheme 6 | dark: CodeTheme 7 | } 8 | 9 | interface Options { 10 | theme?: Theme 11 | keepBackground?: boolean 12 | } 13 | 14 | const defaultOptions: Options = { 15 | theme: { 16 | light: "github-light", 17 | dark: "github-dark", 18 | }, 19 | keepBackground: false, 20 | } 21 | 22 | export const SyntaxHighlighting: QuartzTransformerPlugin = ( 23 | userOpts?: Partial, 24 | ) => { 25 | const opts: Partial = { ...defaultOptions, ...userOpts } 26 | 27 | return { 28 | name: "SyntaxHighlighting", 29 | htmlPlugins() { 30 | return [[rehypePrettyCode, opts]] 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/features/darkmode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Darkmode" 3 | tags: 4 | - component 5 | --- 6 | 7 | Quartz supports darkmode out of the box that respects the user's theme preference. Any future manual toggles of the darkmode switch will be saved in the browser's local storage so it can be persisted across future page loads. 8 | 9 | ## Customization 10 | 11 | - Removing darkmode: delete all usages of `Component.Darkmode()` from `quartz.layout.ts`. 12 | - Component: `quartz/components/Darkmode.tsx` 13 | - Style: `quartz/components/styles/darkmode.scss` 14 | - Script: `quartz/components/scripts/darkmode.inline.ts` 15 | 16 | You can also listen to the `themechange` event to perform any custom logic when the theme changes. 17 | 18 | ```js 19 | document.addEventListener("themechange", (e) => { 20 | console.log("Theme changed to " + e.detail.theme) // either "light" or "dark" 21 | // your logic here 22 | }) 23 | ``` 24 | -------------------------------------------------------------------------------- /content/B-Tree.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "B-Tree" 3 | date: 2024-06-28T00:00:00+02:00 4 | draft: false 5 | tags: data-structure 6 | --- 7 | 8 | B-Tree stands for balanced tree. B-Tree refers to the specific data structure and family of data-structures: 9 | 10 | - B-Tree (1971) 11 | - B+Tree (1973) 12 | - B\*Tree (1977) 13 | - B-link-Tree (1981) 14 | - B-epsilon-Tree (2003) 15 | - Bw-Tree (2013) 16 | 17 | 18 | 19 | Related: 20 | 21 | - [Topics in database research: indexes](https://w6113.github.io/papers#indexes) 22 | - [Modern B-Tree Techniques](https://w6113.github.io/files/papers/btreesurvey-graefe.pdf) 23 | -------------------------------------------------------------------------------- /content/ORM.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | tags: [pattern, acronym] 4 | --- 5 | 6 | Object-Relational Metadata Mapping Patterns: [Metadata Mapping](https://www.martinfowler.com/eaaCatalog/metadataMapping.html), [Query Object](https://www.martinfowler.com/eaaCatalog/queryObject.html), [Repository](https://www.martinfowler.com/eaaCatalog/repository.html). 7 | 8 | Data Source Architectural Patterns: [Table Data Gateway](https://www.martinfowler.com/eaaCatalog/tableDataGateway.html), [Row Data Gateway](https://www.martinfowler.com/eaaCatalog/rowDataGateway.html), [Active Record](https://www.martinfowler.com/eaaCatalog/activeRecord.html), [Data Mapper](https://www.martinfowler.com/eaaCatalog/dataMapper.html). 9 | 10 | > It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system. 11 | > 12 | > -- [Active Record Basics](https://guides.rubyonrails.org/active_record_basics.html) 13 | -------------------------------------------------------------------------------- /quartz/processors/filter.ts: -------------------------------------------------------------------------------- 1 | import { BuildCtx } from "../util/ctx" 2 | import { PerfTimer } from "../util/perf" 3 | import { ProcessedContent } from "../plugins/vfile" 4 | 5 | export function filterContent(ctx: BuildCtx, content: ProcessedContent[]): ProcessedContent[] { 6 | const { cfg, argv } = ctx 7 | const perf = new PerfTimer() 8 | const initialLength = content.length 9 | for (const plugin of cfg.plugins.filters) { 10 | const updatedContent = content.filter((item) => plugin.shouldPublish(ctx, item)) 11 | 12 | if (argv.verbose) { 13 | const diff = content.filter((x) => !updatedContent.includes(x)) 14 | for (const file of diff) { 15 | console.log(`[filter:${plugin.name}] ${file[1].data.slug}`) 16 | } 17 | } 18 | 19 | content = updatedContent 20 | } 21 | 22 | console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`) 23 | return content 24 | } 25 | -------------------------------------------------------------------------------- /quartz/components/styles/darkmode.scss: -------------------------------------------------------------------------------- 1 | .darkmode { 2 | position: relative; 3 | width: 20px; 4 | height: 20px; 5 | margin: 0 10px; 6 | 7 | & > .toggle { 8 | display: none; 9 | box-sizing: border-box; 10 | } 11 | 12 | & svg { 13 | cursor: pointer; 14 | opacity: 0; 15 | position: absolute; 16 | width: 20px; 17 | height: 20px; 18 | top: calc(50% - 10px); 19 | fill: var(--darkgray); 20 | transition: opacity 0.1s ease; 21 | } 22 | } 23 | 24 | :root[saved-theme="dark"] { 25 | color-scheme: dark; 26 | } 27 | 28 | :root[saved-theme="light"] { 29 | color-scheme: light; 30 | } 31 | 32 | :root[saved-theme="dark"] .toggle ~ label { 33 | & > #dayIcon { 34 | opacity: 0; 35 | } 36 | & > #nightIcon { 37 | opacity: 1; 38 | } 39 | } 40 | 41 | :root .toggle ~ label { 42 | & > #dayIcon { 43 | opacity: 1; 44 | } 45 | & > #nightIcon { 46 | opacity: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /content/bikeshedding.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bikeshedding" 3 | date: 2019-08-03T09:19:46+02:00 4 | draft: false 5 | tags: slang 6 | --- 7 | 8 | The term was coined as a metaphor to illuminate Parkinson’s Law of Triviality. Parkinson observed that a committee whose job is to approve plans for a nuclear power plant may spend the majority of its time on relatively unimportant but easy-to-grasp issues, such as what materials to use for the staff bikeshed, while neglecting the design of the power plant itself, which is far more important but also far more difficult to criticize constructively. It was popularized in the Berkeley Software Distribution community by Poul-Henning Kamp and has spread from there to the software industry at large. 9 | 10 | Source: [wiktionary](https://en.wiktionary.org/wiki/bikeshedding) 11 | 12 | ### Related 13 | 14 | - [Useful tech terms: Yak Shaving, Technical Debt, Bikeshedding](https://phinze.blog/2014/05/24/useful-tech-terms-part-1.html) 15 | -------------------------------------------------------------------------------- /docs/plugins/FolderPage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FolderPage 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin generates index pages for folders, creating a listing page for each folder that contains multiple content files. See [[folder and tag listings]] for more information. 8 | 9 | Example: [[advanced/|Advanced]] 10 | 11 | > [!note] 12 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 13 | 14 | This plugin has no configuration options. 15 | 16 | The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`). 17 | 18 | ## API 19 | 20 | - Category: Emitter 21 | - Function name: `Plugin.FolderPage()`. 22 | - Source: [`quartz/plugins/emitters/folderPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/folderPage.tsx). 23 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Building your Quartz" 3 | --- 4 | 5 | Once you've [[index#🪴 Get Started|initialized]] Quartz, let's see what it looks like locally: 6 | 7 | ```bash 8 | npx quartz build --serve 9 | ``` 10 | 11 | This will start a local web server to run your Quartz on your computer. Open a web browser and visit `http://localhost:8080/` to view it. 12 | 13 | > [!hint] Flags and options 14 | > For full help options, you can run `npx quartz build --help`. 15 | > 16 | > Most of these have sensible defaults but you can override them if you have a custom setup: 17 | > 18 | > - `-d` or `--directory`: the content folder. This is normally just `content` 19 | > - `-v` or `--verbose`: print out extra logging information 20 | > - `-o` or `--output`: the output folder. This is normally just `public` 21 | > - `--serve`: run a local hot-reloading server to preview your Quartz 22 | > - `--port`: what port to run the local preview server on 23 | > - `--concurrency`: how many threads to use to parse notes 24 | -------------------------------------------------------------------------------- /docs/plugins/AliasRedirects.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AliasRedirects 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files. 8 | 9 | For example, A `foo.md` has the following frontmatter 10 | 11 | ```md title="foo.md" 12 | --- 13 | title: "Foo" 14 | alias: 15 | - "bar" 16 | --- 17 | ``` 18 | 19 | The target `host.me/bar` will be redirected to `host.me/foo` 20 | 21 | Note that these are permanent redirect. 22 | 23 | The emitter supports the following aliases: 24 | 25 | - `aliases` 26 | - `alias` 27 | 28 | > [!note] 29 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 30 | 31 | This plugin has no configuration options. 32 | 33 | ## API 34 | 35 | - Category: Emitter 36 | - Function name: `Plugin.AliasRedirects()`. 37 | - Source: [`quartz/plugins/emitters/aliases.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/aliases.ts). 38 | -------------------------------------------------------------------------------- /quartz/components/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType, JSX } from "preact" 2 | import { StaticResources } from "../util/resources" 3 | import { QuartzPluginData } from "../plugins/vfile" 4 | import { GlobalConfiguration } from "../cfg" 5 | import { Node } from "hast" 6 | import { BuildCtx } from "../util/ctx" 7 | 8 | export type QuartzComponentProps = { 9 | ctx: BuildCtx 10 | externalResources: StaticResources 11 | fileData: QuartzPluginData 12 | cfg: GlobalConfiguration 13 | children: (QuartzComponent | JSX.Element)[] 14 | tree: Node 15 | allFiles: QuartzPluginData[] 16 | displayClass?: "mobile-only" | "desktop-only" 17 | } & JSX.IntrinsicAttributes & { 18 | [key: string]: any 19 | } 20 | 21 | export type QuartzComponent = ComponentType & { 22 | css?: string 23 | beforeDOMLoaded?: string 24 | afterDOMLoaded?: string 25 | } 26 | 27 | export type QuartzComponentConstructor = ( 28 | opts: Options, 29 | ) => QuartzComponent 30 | -------------------------------------------------------------------------------- /content/i-n variables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "i-n variables" 3 | date: 2024-06-18T00:00:00+00:00 4 | draft: false 5 | tags: legacy 6 | year: 1954 7 | --- 8 | 9 | Have you ever wonder why typically people use `i`, `j`, `k` variables in "for loops"? Example from [wikipedia article](https://en.wikipedia.org/wiki/For_loop): 10 | 11 | ```c 12 | for (i = 0; i < 100; i++) { 13 | for (j = i; j < 10; j++) { 14 | some_function(i, j); 15 | } 16 | } 17 | ``` 18 | 19 | It is believed that this tradition comes from [Fortran, 1954](https://archive.computerhistory.org/resources/text/Fortran/102679231.05.01.acc.pdf). There were only two types of variables: fixed point (integer) and floating point (real). All variables starting with `i` - `n` were integers. It is not specified in manual, but I think that `i` - `n` were selected as mnemonic technique - it is two first letters in the word i-n-teger. 20 | 21 | Here we are 70 years later still using this convention, even so many modern developer haven't even heard about Fortran. 22 | -------------------------------------------------------------------------------- /docs/plugins/Static.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Static 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits all static resources needed by Quartz. This is used, for example, for fonts and images that need a stable position, such as banners and icons. The plugin respects the `ignorePatterns` in the global [[configuration]]. 8 | 9 | > [!important] 10 | > This is different from [[Assets]]. The resources from the [[Static]] plugin are located under `quartz/static`, whereas [[Assets]] renders all static resources under `content` and is used for images, videos, audio, etc. that are directly referenced by your markdown content. 11 | 12 | > [!note] 13 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 14 | 15 | This plugin has no configuration options. 16 | 17 | ## API 18 | 19 | - Category: Emitter 20 | - Function name: `Plugin.Static()`. 21 | - Source: [`quartz/plugins/emitters/static.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/static.ts). 22 | -------------------------------------------------------------------------------- /docs/features/table of contents.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Table of Contents" 3 | tags: 4 | - component 5 | - feature/transformer 6 | --- 7 | 8 | Quartz can automatically generate a table of contents (TOC) from a list of headings on each page. It will also show you your current scrolling position on the page by highlighting headings you've scrolled through with a different color. 9 | 10 | You can hide the TOC on a page by adding `enableToc: false` to the frontmatter for that page. 11 | 12 | By default, the TOC shows all headings from H1 (`# Title`) to H3 (`### Title`) and is only displayed if there is more than one heading on the page. 13 | 14 | ## Customization 15 | 16 | The table of contents is a functionality of the [[TableOfContents]] plugin. See the plugin page for more customization options. 17 | 18 | It also needs the `TableOfContents` component, which is displayed in the right sidebar by default. You can change this by customizing the [[layout]]. The TOC component can be configured with the `layout` parameter, which can either be `modern` (default) or `legacy`. 19 | -------------------------------------------------------------------------------- /quartz/components/Date.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalConfiguration } from "../cfg" 2 | import { ValidLocale } from "../i18n" 3 | import { QuartzPluginData } from "../plugins/vfile" 4 | 5 | interface Props { 6 | date: Date 7 | locale?: ValidLocale 8 | } 9 | 10 | export type ValidDateType = keyof Required["dates"] 11 | 12 | export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined { 13 | if (!cfg.defaultDateType) { 14 | throw new Error( 15 | `Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`, 16 | ) 17 | } 18 | return data.dates?.[cfg.defaultDateType] 19 | } 20 | 21 | export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { 22 | return d.toLocaleDateString(locale, { 23 | year: "numeric", 24 | month: "short", 25 | day: "2-digit", 26 | }) 27 | } 28 | 29 | export function Date({ date, locale }: Props) { 30 | return <>{formatDate(date, locale)} 31 | } 32 | -------------------------------------------------------------------------------- /docs/features/i18n.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Internationalization 3 | --- 4 | 5 | Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`. 6 | 7 | The locale field generally follows a certain format: `{language}-{REGION}` 8 | 9 | - `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). 10 | - `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 11 | 12 | > [!tip] Interested in contributing? 13 | > We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: 14 | > 15 | > 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. 16 | > 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. 17 | > 3. Fill in the translations! 18 | > 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something about Quartz isn't working the way you expect 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots and Source** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | You can help speed up fixing the problem by either 27 | 28 | 1. providing a simple reproduction 29 | 2. linking to your Quartz repository where the problem can be observed 30 | 31 | **Desktop (please complete the following information):** 32 | 33 | - Quartz Version: [e.g. v4.1.2] 34 | - `node` Version: [e.g. v18.16] 35 | - `npm` version: [e.g. v10.1.0] 36 | - OS: [e.g. iOS] 37 | - Browser [e.g. chrome, safari] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /docs/features/popover previews.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Popover Previews 3 | --- 4 | 5 | Like Wikipedia, when you hover over a link in Quartz, there is a popup of a page preview that you can scroll to see the entire content. Links to headers will also scroll the popup to show that specific header in view. 6 | 7 | By default, Quartz only fetches previews for pages inside your vault due to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). It does this by selecting all HTML elements with the `popover-hint` class. For most pages, this includes the page title, page metadata like words and time to read, tags, and the actual page content. 8 | 9 | When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover. 10 | 11 | Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups. 12 | 13 | ## Configuration 14 | 15 | - Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`. 16 | - Style: `quartz/components/styles/popover.scss` 17 | - Script: `quartz/components/scripts/popover.inline.ts` 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jackyzha0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /quartz/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | import style from "./styles/footer.scss" 3 | import { version } from "../../package.json" 4 | import { i18n } from "../i18n" 5 | 6 | interface Options { 7 | links: Record 8 | } 9 | 10 | export default ((opts?: Options) => { 11 | const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { 12 | const year = new Date().getFullYear() 13 | const links = opts?.links ?? [] 14 | return ( 15 |
16 |
17 |

18 | {i18n(cfg.locale).components.footer.createdWith}{" "} 19 | Quartz v{version} © {year} 20 |

21 |
    22 | {Object.entries(links).map(([text, link]) => ( 23 |
  • 24 | {text} 25 |
  • 26 | ))} 27 |
28 |
29 | ) 30 | } 31 | 32 | Footer.css = style 33 | return Footer 34 | }) satisfies QuartzComponentConstructor 35 | -------------------------------------------------------------------------------- /quartz/util/trace.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import process from "process" 3 | import { isMainThread } from "workerpool" 4 | 5 | const rootFile = /.*at file:/ 6 | export function trace(msg: string, err: Error) { 7 | let stack = err.stack ?? "" 8 | 9 | const lines: string[] = [] 10 | 11 | lines.push("") 12 | lines.push( 13 | "\n" + 14 | chalk.bgRed.black.bold(" ERROR ") + 15 | "\n\n" + 16 | chalk.red(` ${msg}`) + 17 | (err.message.length > 0 ? `: ${err.message}` : ""), 18 | ) 19 | 20 | let reachedEndOfLegibleTrace = false 21 | for (const line of stack.split("\n").slice(1)) { 22 | if (reachedEndOfLegibleTrace) { 23 | break 24 | } 25 | 26 | if (!line.includes("node_modules")) { 27 | lines.push(` ${line}`) 28 | if (rootFile.test(line)) { 29 | reachedEndOfLegibleTrace = true 30 | } 31 | } 32 | } 33 | 34 | const traceMsg = lines.join("\n") 35 | if (!isMainThread) { 36 | // gather lines and throw 37 | throw new Error(traceMsg) 38 | } else { 39 | // print and exit 40 | console.error(traceMsg) 41 | process.exit(1) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/cname.ts: -------------------------------------------------------------------------------- 1 | import { FilePath, joinSegments } from "../../util/path" 2 | import { QuartzEmitterPlugin } from "../types" 3 | import fs from "fs" 4 | import chalk from "chalk" 5 | import DepGraph from "../../depgraph" 6 | 7 | export function extractDomainFromBaseUrl(baseUrl: string) { 8 | const url = new URL(`https://${baseUrl}`) 9 | return url.hostname 10 | } 11 | 12 | export const CNAME: QuartzEmitterPlugin = () => ({ 13 | name: "CNAME", 14 | getQuartzComponents() { 15 | return [] 16 | }, 17 | async getDependencyGraph(_ctx, _content, _resources) { 18 | return new DepGraph() 19 | }, 20 | async emit({ argv, cfg }, _content, _resources): Promise { 21 | if (!cfg.configuration.baseUrl) { 22 | console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration")) 23 | return [] 24 | } 25 | const path = joinSegments(argv.output, "CNAME") 26 | const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl) 27 | if (!content) { 28 | return [] 29 | } 30 | fs.writeFileSync(path, content) 31 | return [path] as FilePath[] 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /docs/plugins/Frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Frontmatter" 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin parses the frontmatter of the page using the [gray-matter](https://github.com/jonschlinkert/gray-matter) library. See [[authoring content#Syntax]], [[Obsidian compatibility]] and [[OxHugo compatibility]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `delimiters`: the delimiters to use for the frontmatter. Can have one value (e.g. `"---"`) or separate values for opening and closing delimiters (e.g. `["---", "~~~"]`). Defaults to `"---"`. 15 | - `language`: the language to use for parsing the frontmatter. Can be `yaml` (default) or `toml`. 16 | 17 | > [!warning] 18 | > This plugin must not be removed, otherwise Quartz will break. 19 | 20 | ## API 21 | 22 | - Category: Transformer 23 | - Function name: `Plugin.Frontmatter()`. 24 | - Source: [`quartz/plugins/transformers/frontmatter.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/frontmatter.ts). 25 | -------------------------------------------------------------------------------- /docs/plugins/SyntaxHighlighting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SyntaxHighlighting" 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin is used to add syntax highlighting to code blocks in Quartz. See [[syntax highlighting]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `theme`: a separate id of one of the [themes bundled with Shikiji](https://shikiji.netlify.app/themes). One for light mode and one for dark mode. Defaults to `theme: { light: "github-light", dark: "github-dark" }`. 15 | - `keepBackground`: If set to `true`, the background of the Shikiji theme will be used. With `false` (default) the Quartz theme color for background will be used instead. 16 | 17 | In addition, you can further override the colours in the `quartz/styles/syntax.scss` file. 18 | 19 | ## API 20 | 21 | - Category: Transformer 22 | - Function name: `Plugin.SyntaxHighlighting()`. 23 | - Source: [`quartz/plugins/transformers/syntax.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/syntax.ts). 24 | -------------------------------------------------------------------------------- /docs/features/Obsidian compatibility.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Obsidian Compatibility" 3 | tags: 4 | - feature/transformer 5 | --- 6 | 7 | Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian. 8 | 9 | By default, Quartz ships with the [[ObsidianFlavoredMarkdown]] plugin, which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]]. 10 | 11 | It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the [[Frontmatter]] transformer plugin. 12 | 13 | Finally, Quartz also provides [[CrawlLinks]] plugin, which allows you to customize Quartz's link resolution behaviour to match Obsidian. 14 | 15 | ## Configuration 16 | 17 | This functionality is provided by the [[ObsidianFlavoredMarkdown]], [[Frontmatter]] and [[CrawlLinks]] plugins. See the plugin pages for customization options. 18 | -------------------------------------------------------------------------------- /docs/plugins/GitHubFlavoredMarkdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHubFlavoredMarkdown 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin enhances Markdown processing to support GitHub Flavored Markdown (GFM) which adds features like autolink literals, footnotes, strikethrough, tables and tasklists. 8 | 9 | In addition, this plugin adds optional features for typographic refinement (such as converting straight quotes to curly quotes, dashes to en-dashes/em-dashes, and ellipses) and automatic heading links as a symbol that appears next to the heading on hover. 10 | 11 | > [!note] 12 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 13 | 14 | This plugin accepts the following configuration options: 15 | 16 | - `enableSmartyPants`: When true, enables typographic enhancements. Default is true. 17 | - `linkHeadings`: When true, automatically adds links to headings. Default is true. 18 | 19 | ## API 20 | 21 | - Category: Transformer 22 | - Function name: `Plugin.GitHubFlavoredMarkdown()`. 23 | - Source: [`quartz/plugins/transformers/gfm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/gfm.ts). 24 | -------------------------------------------------------------------------------- /quartz/util/resources.tsx: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "crypto" 2 | import { JSX } from "preact/jsx-runtime" 3 | 4 | export type JSResource = { 5 | loadTime: "beforeDOMReady" | "afterDOMReady" 6 | moduleType?: "module" 7 | spaPreserve?: boolean 8 | } & ( 9 | | { 10 | src: string 11 | contentType: "external" 12 | } 13 | | { 14 | script: string 15 | contentType: "inline" 16 | } 17 | ) 18 | 19 | export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element { 20 | const scriptType = resource.moduleType ?? "application/javascript" 21 | const spaPreserve = preserve ?? resource.spaPreserve 22 | if (resource.contentType === "external") { 23 | return ( 24 | 35 | ) 36 | } 37 | } 38 | 39 | export interface StaticResources { 40 | css: string[] 41 | js: JSResource[] 42 | } 43 | -------------------------------------------------------------------------------- /quartz/processors/emit.ts: -------------------------------------------------------------------------------- 1 | import { PerfTimer } from "../util/perf" 2 | import { getStaticResourcesFromPlugins } from "../plugins" 3 | import { ProcessedContent } from "../plugins/vfile" 4 | import { QuartzLogger } from "../util/log" 5 | import { trace } from "../util/trace" 6 | import { BuildCtx } from "../util/ctx" 7 | 8 | export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { 9 | const { argv, cfg } = ctx 10 | const perf = new PerfTimer() 11 | const log = new QuartzLogger(ctx.argv.verbose) 12 | 13 | log.start(`Emitting output files`) 14 | 15 | let emittedFiles = 0 16 | const staticResources = getStaticResourcesFromPlugins(ctx) 17 | for (const emitter of cfg.plugins.emitters) { 18 | try { 19 | const emitted = await emitter.emit(ctx, content, staticResources) 20 | emittedFiles += emitted.length 21 | 22 | if (ctx.argv.verbose) { 23 | for (const file of emitted) { 24 | console.log(`[emit:${emitter.name}] ${file}`) 25 | } 26 | } 27 | } catch (err) { 28 | trace(`Failed to emit from plugin \`${emitter.name}\``, err as Error) 29 | } 30 | } 31 | 32 | log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`) 33 | } 34 | -------------------------------------------------------------------------------- /quartz/components/styles/toc.scss: -------------------------------------------------------------------------------- 1 | button#toc { 2 | background-color: transparent; 3 | border: none; 4 | text-align: left; 5 | cursor: pointer; 6 | padding: 0; 7 | color: var(--dark); 8 | display: flex; 9 | align-items: center; 10 | 11 | & h3 { 12 | font-size: 1rem; 13 | display: inline-block; 14 | margin: 0; 15 | } 16 | 17 | & .fold { 18 | margin-left: 0.5rem; 19 | transition: transform 0.3s ease; 20 | opacity: 0.8; 21 | } 22 | 23 | &.collapsed .fold { 24 | transform: rotateZ(-90deg); 25 | } 26 | } 27 | 28 | #toc-content { 29 | list-style: none; 30 | overflow: hidden; 31 | max-height: none; 32 | transition: max-height 0.5s ease; 33 | position: relative; 34 | 35 | &.collapsed > .overflow::after { 36 | opacity: 0; 37 | } 38 | 39 | & ul { 40 | list-style: none; 41 | margin: 0.5rem 0; 42 | padding: 0; 43 | & > li > a { 44 | color: var(--dark); 45 | opacity: 0.35; 46 | transition: 47 | 0.5s ease opacity, 48 | 0.3s ease color; 49 | &.in-view { 50 | opacity: 0.75; 51 | } 52 | } 53 | } 54 | 55 | @for $i from 0 through 6 { 56 | & .depth-#{$i} { 57 | padding-left: calc(1rem * #{$i}); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /quartz/components/index.ts: -------------------------------------------------------------------------------- 1 | import Content from "./pages/Content" 2 | import TagContent from "./pages/TagContent" 3 | import FolderContent from "./pages/FolderContent" 4 | import NotFound from "./pages/404" 5 | import ArticleTitle from "./ArticleTitle" 6 | import Darkmode from "./Darkmode" 7 | import Head from "./Head" 8 | import PageTitle from "./PageTitle" 9 | import ContentMeta from "./ContentMeta" 10 | import Spacer from "./Spacer" 11 | import TableOfContents from "./TableOfContents" 12 | import Explorer from "./Explorer" 13 | import TagList from "./TagList" 14 | import Graph from "./Graph" 15 | import Backlinks from "./Backlinks" 16 | import Search from "./Search" 17 | import Footer from "./Footer" 18 | import DesktopOnly from "./DesktopOnly" 19 | import MobileOnly from "./MobileOnly" 20 | import RecentNotes from "./RecentNotes" 21 | import Breadcrumbs from "./Breadcrumbs" 22 | 23 | export { 24 | ArticleTitle, 25 | Content, 26 | TagContent, 27 | FolderContent, 28 | Darkmode, 29 | Head, 30 | PageTitle, 31 | ContentMeta, 32 | Spacer, 33 | TableOfContents, 34 | Explorer, 35 | TagList, 36 | Graph, 37 | Backlinks, 38 | Search, 39 | Footer, 40 | DesktopOnly, 41 | MobileOnly, 42 | RecentNotes, 43 | NotFound, 44 | Breadcrumbs, 45 | } 46 | -------------------------------------------------------------------------------- /content/Yak Shaving.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Yak Shaving" 3 | date: 2019-08-03T09:01:40+02:00 4 | draft: false 5 | tags: slang 6 | --- 7 | 8 | The term was coined by Carlin J. Vieri, a Ph.D. at MIT back in the 90s. 9 | 10 | > Yak shaving is what you are doing when you're doing some stupid, fiddly little task that bears no obvious relationship to what you're supposed to be working on, but yet a chain of twelve causal relations links what you're doing to the original meta-task. 11 | 12 | Source: [Yak Shaving Defined - I'll get that done, as soon as I shave this yak.](https://www.hanselman.com/blog/YakShavingDefinedIllGetThatDoneAsSoonAsIShaveThisYak.aspx) 13 | 14 | Example: Hal fixing a light bulb (from Malcolm in the Middle S03E06 - Health Scare) 15 | 16 | 17 | 18 | ### Related 19 | 20 | - [Donald Knuth - The Patron Saint of Yak Shaves](https://yakshav.es/the-patron-saint-of-yakshaves/) 21 | - [Rabbit Hole](https://www.urbandictionary.com/define.php?term=Rabbit%20Hole) 22 | - [Useful tech terms: Yak Shaving, Technical Debt, Bikeshedding](https://phinze.blog/2014/05/24/useful-tech-terms-part-1.html) 23 | -------------------------------------------------------------------------------- /quartz/components/Backlinks.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | import style from "./styles/backlinks.scss" 3 | import { resolveRelative, simplifySlug } from "../util/path" 4 | import { i18n } from "../i18n" 5 | import { classNames } from "../util/lang" 6 | 7 | const Backlinks: QuartzComponent = ({ 8 | fileData, 9 | allFiles, 10 | displayClass, 11 | cfg, 12 | }: QuartzComponentProps) => { 13 | const slug = simplifySlug(fileData.slug!) 14 | const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) 15 | return ( 16 |
17 |

{i18n(cfg.locale).components.backlinks.title}

18 |
    19 | {backlinkFiles.length > 0 ? ( 20 | backlinkFiles.map((f) => ( 21 |
  • 22 | 23 | {f.frontmatter?.title} 24 | 25 |
  • 26 | )) 27 | ) : ( 28 |
  • {i18n(cfg.locale).components.backlinks.noBacklinksFound}
  • 29 | )} 30 |
31 |
32 | ) 33 | } 34 | 35 | Backlinks.css = style 36 | export default (() => Backlinks) satisfies QuartzComponentConstructor 37 | -------------------------------------------------------------------------------- /quartz/bootstrap-cli.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import yargs from "yargs" 3 | import { hideBin } from "yargs/helpers" 4 | import { 5 | handleBuild, 6 | handleCreate, 7 | handleUpdate, 8 | handleRestore, 9 | handleSync, 10 | } from "./cli/handlers.js" 11 | import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js" 12 | import { version } from "./cli/constants.js" 13 | 14 | yargs(hideBin(process.argv)) 15 | .scriptName("quartz") 16 | .version(version) 17 | .usage("$0 [args]") 18 | .command("create", "Initialize Quartz", CreateArgv, async (argv) => { 19 | await handleCreate(argv) 20 | }) 21 | .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => { 22 | await handleUpdate(argv) 23 | }) 24 | .command( 25 | "restore", 26 | "Try to restore your content folder from the cache", 27 | CommonArgv, 28 | async (argv) => { 29 | await handleRestore(argv) 30 | }, 31 | ) 32 | .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => { 33 | await handleSync(argv) 34 | }) 35 | .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => { 36 | await handleBuild(argv) 37 | }) 38 | .showHelpOnFail(false) 39 | .help() 40 | .strict() 41 | .demandCommand().argv 42 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/static.ts: -------------------------------------------------------------------------------- 1 | import { FilePath, QUARTZ, joinSegments } from "../../util/path" 2 | import { QuartzEmitterPlugin } from "../types" 3 | import fs from "fs" 4 | import { glob } from "../../util/glob" 5 | import DepGraph from "../../depgraph" 6 | 7 | export const Static: QuartzEmitterPlugin = () => ({ 8 | name: "Static", 9 | getQuartzComponents() { 10 | return [] 11 | }, 12 | async getDependencyGraph({ argv, cfg }, _content, _resources) { 13 | const graph = new DepGraph() 14 | 15 | const staticPath = joinSegments(QUARTZ, "static") 16 | const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) 17 | for (const fp of fps) { 18 | graph.addEdge( 19 | joinSegments("static", fp) as FilePath, 20 | joinSegments(argv.output, "static", fp) as FilePath, 21 | ) 22 | } 23 | 24 | return graph 25 | }, 26 | async emit({ argv, cfg }, _content, _resources): Promise { 27 | const staticPath = joinSegments(QUARTZ, "static") 28 | const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) 29 | await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { 30 | recursive: true, 31 | dereference: true, 32 | }) 33 | return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[] 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /docs/plugins/CreatedModifiedDate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CreatedModifiedDate" 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin determines the created, modified, and published dates for a document using three potential data sources: frontmatter metadata, Git history, and the filesystem. See [[authoring content#Syntax]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `priority`: The data sources to consult for date information. Highest priority first. Possible values are `"frontmatter"`, `"git"`, and `"filesystem"`. Defaults to `"frontmatter", "git", "filesystem"]`. 15 | 16 | > [!warning] 17 | > If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in `quartz.config.ts`. 18 | > 19 | > Depending on how you [[hosting|host]] your Quartz, the `filesystem` dates of your local files may not match the final dates. In these cases, it may be better to use `git` or `frontmatter` to guarantee correct dates. 20 | 21 | ## API 22 | 23 | - Category: Transformer 24 | - Function name: `Plugin.CreatedModifiedDate()`. 25 | - Source: [`quartz/plugins/transformers/lastmod.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/lastmod.ts). 26 | -------------------------------------------------------------------------------- /docs/plugins/TableOfContents.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TableOfContents 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin generates a table of contents (TOC) for Markdown documents. See [[table of contents]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `maxDepth`: Limits the depth of headings included in the TOC, ranging from `1` (top level headings only) to `6` (all heading levels). Default is `3`. 15 | - `minEntries`: The minimum number of heading entries required for the TOC to be displayed. Default is `1`. 16 | - `showByDefault`: If `true` (default), the TOC should be displayed by default. Can be overridden by frontmatter settings. 17 | - `collapseByDefault`: If `true`, the TOC will start in a collapsed state. Default is `false`. 18 | 19 | > [!warning] 20 | > This plugin needs the `Component.TableOfContents` component in `quartz.layout.ts` to determine where to display the TOC. Without it, nothing will be displayed. They should always be added or removed together. 21 | 22 | ## API 23 | 24 | - Category: Transformer 25 | - Function name: `Plugin.TableOfContents()`. 26 | - Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts). 27 | -------------------------------------------------------------------------------- /docs/features/Mermaid diagrams.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mermaid Diagrams" 3 | tags: 4 | - feature/transformer 5 | --- 6 | 7 | Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. 8 | 9 | By default, Quartz will render Mermaid diagrams to match the site theme. 10 | 11 | > [!warning] 12 | > Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]]. 13 | 14 | ## Syntax 15 | 16 | To add a Mermaid diagram, create a mermaid code block. 17 | 18 | ```` 19 | ```mermaid 20 | sequenceDiagram 21 | Alice->>+John: Hello John, how are you? 22 | Alice->>+John: John, can you hear me? 23 | John-->>-Alice: Hi Alice, I can hear you! 24 | John-->>-Alice: I feel great! 25 | ``` 26 | ```` 27 | 28 | ```mermaid 29 | sequenceDiagram 30 | Alice->>+John: Hello John, how are you? 31 | Alice->>+John: John, can you hear me? 32 | John-->>-Alice: Hi Alice, I can hear you! 33 | John-->>-Alice: I feel great! 34 | ``` 35 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/latex.ts: -------------------------------------------------------------------------------- 1 | import remarkMath from "remark-math" 2 | import rehypeKatex from "rehype-katex" 3 | import rehypeMathjax from "rehype-mathjax/svg" 4 | import { QuartzTransformerPlugin } from "../types" 5 | 6 | interface Options { 7 | renderEngine: "katex" | "mathjax" 8 | } 9 | 10 | export const Latex: QuartzTransformerPlugin = (opts?: Options) => { 11 | const engine = opts?.renderEngine ?? "katex" 12 | return { 13 | name: "Latex", 14 | markdownPlugins() { 15 | return [remarkMath] 16 | }, 17 | htmlPlugins() { 18 | if (engine === "katex") { 19 | return [[rehypeKatex, { output: "html" }]] 20 | } else { 21 | return [rehypeMathjax] 22 | } 23 | }, 24 | externalResources() { 25 | if (engine === "katex") { 26 | return { 27 | css: [ 28 | // base css 29 | "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css", 30 | ], 31 | js: [ 32 | { 33 | // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md 34 | src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js", 35 | loadTime: "afterDOMReady", 36 | contentType: "external", 37 | }, 38 | ], 39 | } 40 | } else { 41 | return {} 42 | } 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Upgrading Quartz" 3 | --- 4 | 5 | > [!note] 6 | > This is specifically a guide for upgrading Quartz 4 version to a more recent update. If you are coming from Quartz 3, check out the [[migrating from Quartz 3|migration guide]] for more info. 7 | 8 | To fetch the latest Quartz updates, simply run 9 | 10 | ```bash 11 | npx quartz update 12 | ``` 13 | 14 | As Quartz uses [git](https://git-scm.com/) under the hood for versioning, updating effectively 'pulls' in the updates from the official Quartz GitHub repository. If you have local changes that might conflict with the updates, you may need to resolve these manually yourself (or, pull manually using `git pull origin upstream`). 15 | 16 | > [!hint] 17 | > Quartz will try to cache your content before updating to try and prevent merge conflicts. If you get a conflict mid-merge, you can stop the merge and then run `npx quartz restore` to restore your content from the cache. 18 | 19 | If you have the [GitHub desktop app](https://desktop.github.com/), this will automatically open to help you resolve the conflicts. Otherwise, you will need to resolve this in a text editor like VSCode. For more help on resolving conflicts manually, check out the [GitHub guide on resolving merge conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line#competing-line-change-merge-conflicts). 20 | -------------------------------------------------------------------------------- /docs/plugins/Description.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Description 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing. 8 | 9 | If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length. 10 | 11 | > [!note] 12 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 13 | 14 | This plugin accepts the following configuration options: 15 | 16 | - `descriptionLength`: the maximum length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length. 17 | - `replaceExternalLinks`: If `true` (default), replace external links with their domain and path in the description (e.g. `https://domain.tld/some_page/another_page?query=hello&target=world` is replaced with `domain.tld/some_page/another_page`). 18 | 19 | ## API 20 | 21 | - Category: Transformer 22 | - Function name: `Plugin.Description()`. 23 | - Source: [`quartz/plugins/transformers/description.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/description.ts). 24 | -------------------------------------------------------------------------------- /docs/features/OxHugo compatibility.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OxHugo Compatibility" 3 | tags: 4 | - feature/transformer 5 | --- 6 | 7 | [org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. 8 | 9 | Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by the [[OxHugoFlavoredMarkdown]] plugin. Even though this plugin was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown. 10 | 11 | ```typescript title="quartz.config.ts" 12 | plugins: { 13 | transformers: [ 14 | Plugin.FrontMatter({ delims: "+++", language: "toml" }), // if toml frontmatter 15 | // ... 16 | Plugin.OxHugoFlavouredMarkdown(), 17 | Plugin.GitHubFlavoredMarkdown(), 18 | // ... 19 | ], 20 | }, 21 | ``` 22 | 23 | ## Usage 24 | 25 | Quartz by default doesn't understand `org-roam` files as they aren't Markdown. You're responsible for using an external tool like `ox-hugo` to export the `org-roam` files as Markdown content to Quartz and managing the static assets so that they're available in the final output. 26 | 27 | ## Configuration 28 | 29 | This functionality is provided by the [[OxHugoFlavoredMarkdown]] plugin. See the plugin page for customization options. 30 | -------------------------------------------------------------------------------- /docs/features/wikilinks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wikilinks 3 | --- 4 | 5 | Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time. 6 | 7 | Quartz supports Wikilinks by default and these links are resolved by Quartz using the [[CrawlLinks]] plugin. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax. 8 | 9 | This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. 10 | 11 | ## Syntax 12 | 13 | - `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file` 14 | - `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override` 15 | - `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md` 16 | - `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md` 17 | 18 | ### Embeds 19 | 20 | - `![[Path to image]]`: embeds an image into the page 21 | - `![[Path to image|100x145]]`: embeds an image into the page with dimensions 100px by 145px 22 | - `![[Path to file]]`: transclude an entire page 23 | - `![[Path to file#Anchor]]`: transclude everything under the header `Anchor` 24 | - `![[Path to file#^b15695]]`: transclude block with ID `^b15695` 25 | -------------------------------------------------------------------------------- /docs/features/recent notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recent Notes 3 | tags: component 4 | --- 5 | 6 | Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`. 7 | 8 | ## Customization 9 | 10 | - Changing the title from "Recent notes": pass in an additional parameter to `Component.RecentNotes({ title: "Recent writing" })` 11 | - Changing the number of recent notes: pass in an additional parameter to `Component.RecentNotes({ limit: 5 })` 12 | - Display the note's tags (defaults to true): `Component.RecentNotes({ showTags: false })` 13 | - Show a 'see more' link: pass in an additional parameter to `Component.RecentNotes({ linkToMore: "tags/components" })`. This field should be a full slug to a page that exists. 14 | - Customize filtering: pass in an additional parameter to `Component.RecentNotes({ filter: someFilterFunction })`. The filter function should be a function that has the signature `(f: QuartzPluginData) => boolean`. 15 | - Customize sorting: pass in an additional parameter to `Component.RecentNotes({ sort: someSortFunction })`. By default, Quartz will sort by date and then tie break lexographically. The sort function should be a function that has the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. See `byDateAndAlphabetical` in `quartz/components/PageList.tsx` for an example. 16 | - Component: `quartz/components/RecentNotes.tsx` 17 | - Style: `quartz/components/styles/recentNotes.scss` 18 | -------------------------------------------------------------------------------- /quartz/components/TagList.tsx: -------------------------------------------------------------------------------- 1 | import { pathToRoot, slugTag } from "../util/path" 2 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 | import { classNames } from "../util/lang" 4 | 5 | const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { 6 | const tags = fileData.frontmatter?.tags 7 | const baseDir = pathToRoot(fileData.slug!) 8 | if (tags && tags.length > 0) { 9 | return ( 10 |
    11 | {tags.map((tag) => { 12 | const linkDest = baseDir + `/tags/${slugTag(tag)}` 13 | return ( 14 |
  • 15 | 16 | {tag} 17 | 18 |
  • 19 | ) 20 | })} 21 |
22 | ) 23 | } else { 24 | return null 25 | } 26 | } 27 | 28 | TagList.css = ` 29 | .tags { 30 | list-style: none; 31 | display: flex; 32 | padding-left: 0; 33 | gap: 0.4rem; 34 | margin: 1rem 0; 35 | flex-wrap: wrap; 36 | justify-self: end; 37 | } 38 | 39 | .section-li > .section > .tags { 40 | justify-content: flex-end; 41 | } 42 | 43 | .tags > li { 44 | display: inline-block; 45 | white-space: nowrap; 46 | margin: 0; 47 | overflow-wrap: normal; 48 | } 49 | 50 | a.internal.tag-link { 51 | border-radius: 8px; 52 | background-color: var(--highlight); 53 | padding: 0.2rem 0.4rem; 54 | margin: 0 0.1rem; 55 | } 56 | ` 57 | 58 | export default (() => TagList) satisfies QuartzComponentConstructor 59 | -------------------------------------------------------------------------------- /docs/features/breadcrumbs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Breadcrumbs" 3 | tags: 4 | - component 5 | --- 6 | 7 | Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders. 8 | 9 | By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!). 10 | 11 | ## Customization 12 | 13 | Most configuration can be done by passing in options to `Component.Breadcrumbs()`. 14 | 15 | For example, here's what the default configuration looks like: 16 | 17 | ```typescript title="quartz.layout.ts" 18 | Component.Breadcrumbs({ 19 | spacerSymbol: "❯", // symbol between crumbs 20 | rootName: "Home", // name of first/root element 21 | resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles 22 | hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page 23 | showCurrentPage: true, // whether to display the current page in the breadcrumbs 24 | }) 25 | ``` 26 | 27 | When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. 28 | 29 | You can also adjust where the breadcrumbs will be displayed by adjusting the [[layout]] (moving `Component.Breadcrumbs()` up or down) 30 | 31 | Want to customize it even more? 32 | 33 | - Removing breadcrumbs: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`. 34 | - Component: `quartz/components/Breadcrumbs.tsx` 35 | - Style: `quartz/components/styles/breadcrumbs.scss` 36 | - Script: inline at `quartz/components/Breadcrumbs.tsx` 37 | -------------------------------------------------------------------------------- /docs/plugins/ContentIndex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ContentIndex 3 | tags: 4 | - plugin/emitter 5 | --- 6 | 7 | This plugin emits both RSS and an XML sitemap for your site. The [[RSS Feed]] allows users to subscribe to content on your site and the sitemap allows search engines to better index your site. The plugin also emits a `contentIndex.json` file which is used by dynamic frontend components like search and graph. 8 | 9 | This plugin emits a comprehensive index of the site's content, generating additional resources such as a sitemap, an RSS feed, and a 10 | 11 | > [!note] 12 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 13 | 14 | This plugin accepts the following configuration options: 15 | 16 | - `enableSiteMap`: If `true` (default), generates a sitemap XML file (`sitemap.xml`) listing all site URLs for search engines in content discovery. 17 | - `enableRSS`: If `true` (default), produces an RSS feed (`index.xml`) with recent content updates. 18 | - `rssLimit`: Defines the maximum number of entries to include in the RSS feed, helping to focus on the most recent or relevant content. Defaults to `10`. 19 | - `rssFullHtml`: If `true`, the RSS feed includes full HTML content. Otherwise it includes just summaries. 20 | - `includeEmptyFiles`: If `true` (default), content files with no body text are included in the generated index and resources. 21 | 22 | ## API 23 | 24 | - Category: Emitter 25 | - Function name: `Plugin.ContentIndex()`. 26 | - Source: [`quartz/plugins/emitters/contentIndex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentIndex.ts). 27 | -------------------------------------------------------------------------------- /docs/plugins/OxHugoFlavoredMarkdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: OxHugoFlavoredMarkdown 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin provides support for [ox-hugo](https://github.com/kaushalmodi/ox-hugo) compatibility. See [[OxHugo compatibility]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `wikilinks`: If `true` (default), converts Hugo `{{ relref }}` shortcodes to Quartz [[wikilinks]]. 15 | - `removePredefinedAnchor`: If `true` (default), strips predefined anchors from headings. 16 | - `removeHugoShortcode`: If `true` (default), removes Hugo shortcode syntax (`{{}}`) from the content. 17 | - `replaceFigureWithMdImg`: If `true` (default), replaces `
` with `![]()`. 18 | - `replaceOrgLatex`: If `true` (default), converts Org-mode [[features/Latex|Latex]] fragments to Quartz-compatible LaTeX wrapped in `$` (for inline) and `$$` (for block equations). 19 | 20 | > [!warning] 21 | > While you can use this together with [[ObsidianFlavoredMarkdown]], it's not recommended because it might mutate the file in unexpected ways. Use with caution. 22 | > 23 | > If you use `toml` frontmatter, make sure to configure the [[Frontmatter]] plugin accordingly. See [[OxHugo compatibility]] for an example. 24 | 25 | ## API 26 | 27 | - Category: Transformer 28 | - Function name: `Plugin.OxHugoFlavoredMarkdown()`. 29 | - Source: [`quartz/plugins/transformers/oxhugofm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/oxhugofm.ts). 30 | -------------------------------------------------------------------------------- /quartz/components/scripts/callout.inline.ts: -------------------------------------------------------------------------------- 1 | function toggleCallout(this: HTMLElement) { 2 | const outerBlock = this.parentElement! 3 | outerBlock.classList.toggle("is-collapsed") 4 | const collapsed = outerBlock.classList.contains("is-collapsed") 5 | const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight 6 | outerBlock.style.maxHeight = height + "px" 7 | 8 | // walk and adjust height of all parents 9 | let current = outerBlock 10 | let parent = outerBlock.parentElement 11 | while (parent) { 12 | if (!parent.classList.contains("callout")) { 13 | return 14 | } 15 | 16 | const collapsed = parent.classList.contains("is-collapsed") 17 | const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight 18 | parent.style.maxHeight = height + "px" 19 | 20 | current = parent 21 | parent = parent.parentElement 22 | } 23 | } 24 | 25 | function setupCallout() { 26 | const collapsible = document.getElementsByClassName( 27 | `callout is-collapsible`, 28 | ) as HTMLCollectionOf 29 | for (const div of collapsible) { 30 | const title = div.firstElementChild 31 | 32 | if (title) { 33 | title.addEventListener("click", toggleCallout) 34 | window.addCleanup(() => title.removeEventListener("click", toggleCallout)) 35 | 36 | const collapsed = div.classList.contains("is-collapsed") 37 | const height = collapsed ? title.scrollHeight : div.scrollHeight 38 | div.style.maxHeight = height + "px" 39 | } 40 | } 41 | } 42 | 43 | document.addEventListener("nav", setupCallout) 44 | window.addEventListener("resize", setupCallout) 45 | -------------------------------------------------------------------------------- /quartz/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { StaticResources } from "../util/resources" 2 | import { FilePath, FullSlug } from "../util/path" 3 | import { BuildCtx } from "../util/ctx" 4 | 5 | export function getStaticResourcesFromPlugins(ctx: BuildCtx) { 6 | const staticResources: StaticResources = { 7 | css: [], 8 | js: [], 9 | } 10 | 11 | for (const transformer of ctx.cfg.plugins.transformers) { 12 | const res = transformer.externalResources ? transformer.externalResources(ctx) : {} 13 | if (res?.js) { 14 | staticResources.js.push(...res.js) 15 | } 16 | if (res?.css) { 17 | staticResources.css.push(...res.css) 18 | } 19 | } 20 | 21 | // if serving locally, listen for rebuilds and reload the page 22 | if (ctx.argv.serve) { 23 | const wsUrl = ctx.argv.remoteDevHost 24 | ? `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}` 25 | : `ws://localhost:${ctx.argv.wsPort}` 26 | 27 | staticResources.js.push({ 28 | loadTime: "afterDOMReady", 29 | contentType: "inline", 30 | script: ` 31 | const socket = new WebSocket('${wsUrl}') 32 | // reload(true) ensures resources like images and scripts are fetched again in firefox 33 | socket.addEventListener('message', () => document.location.reload(true)) 34 | `, 35 | }) 36 | } 37 | 38 | return staticResources 39 | } 40 | 41 | export * from "./transformers" 42 | export * from "./filters" 43 | export * from "./emitters" 44 | 45 | declare module "vfile" { 46 | // inserted in processors.ts 47 | interface DataMap { 48 | slug: FullSlug 49 | filePath: FilePath 50 | relativePath: FilePath 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /quartz/components/styles/graph.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/variables.scss" as *; 2 | 3 | .graph { 4 | & > h3 { 5 | font-size: 1rem; 6 | margin: 0; 7 | } 8 | 9 | & > .graph-outer { 10 | border-radius: 5px; 11 | border: 1px solid var(--lightgray); 12 | box-sizing: border-box; 13 | height: 250px; 14 | margin: 0.5em 0; 15 | position: relative; 16 | overflow: hidden; 17 | 18 | & > #global-graph-icon { 19 | color: var(--dark); 20 | opacity: 0.5; 21 | width: 18px; 22 | height: 18px; 23 | position: absolute; 24 | padding: 0.2rem; 25 | margin: 0.3rem; 26 | top: 0; 27 | right: 0; 28 | border-radius: 4px; 29 | background-color: transparent; 30 | transition: background-color 0.5s ease; 31 | cursor: pointer; 32 | &:hover { 33 | background-color: var(--lightgray); 34 | } 35 | } 36 | } 37 | 38 | & > #global-graph-outer { 39 | position: fixed; 40 | z-index: 9999; 41 | left: 0; 42 | top: 0; 43 | width: 100vw; 44 | height: 100%; 45 | backdrop-filter: blur(4px); 46 | display: none; 47 | overflow: hidden; 48 | 49 | &.active { 50 | display: inline-block; 51 | } 52 | 53 | & > #global-graph-container { 54 | border: 1px solid var(--lightgray); 55 | background-color: var(--light); 56 | border-radius: 5px; 57 | box-sizing: border-box; 58 | position: fixed; 59 | top: 50%; 60 | left: 50%; 61 | transform: translate(-50%, -50%); 62 | height: 60vh; 63 | width: 50vw; 64 | 65 | @media all and (max-width: $fullPageWidth) { 66 | width: 90%; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /quartz.layout.ts: -------------------------------------------------------------------------------- 1 | import { PageLayout, SharedLayout } from "./quartz/cfg" 2 | import * as Component from "./quartz/components" 3 | 4 | // components shared across all pages 5 | export const sharedPageComponents: SharedLayout = { 6 | head: Component.Head(), 7 | header: [], 8 | footer: Component.Footer({ 9 | links: { 10 | GitHub: "https://github.com/stereobooster/dev.wtf", 11 | "stereobooster.com": "https://stereobooster.com", 12 | }, 13 | }), 14 | } 15 | 16 | // components for pages that display a single page (e.g. a single note) 17 | export const defaultContentPageLayout: PageLayout = { 18 | beforeBody: [ 19 | Component.Breadcrumbs(), 20 | Component.ArticleTitle(), 21 | Component.ContentMeta(), 22 | Component.TagList(), 23 | ], 24 | left: [ 25 | Component.PageTitle(), 26 | Component.MobileOnly(Component.Spacer()), 27 | Component.Search(), 28 | Component.Darkmode(), 29 | Component.DesktopOnly(Component.Explorer()), 30 | ], 31 | right: [ 32 | Component.Graph(), 33 | Component.DesktopOnly(Component.TableOfContents()), 34 | Component.Backlinks(), 35 | ], 36 | } 37 | 38 | // components for pages that display lists of pages (e.g. tags or folders) 39 | export const defaultListPageLayout: PageLayout = { 40 | beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], 41 | left: [ 42 | Component.PageTitle(), 43 | Component.MobileOnly(Component.Spacer()), 44 | Component.Search(), 45 | Component.Darkmode(), 46 | Component.DesktopOnly(Component.Explorer()), 47 | ], 48 | right: [ 49 | Component.DesktopOnly(Component.RecentNotes({ title: "Recently Updated", showTags: false })), 50 | ], 51 | } 52 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/citations.ts: -------------------------------------------------------------------------------- 1 | import rehypeCitation from "rehype-citation" 2 | import { PluggableList } from "unified" 3 | import { visit } from "unist-util-visit" 4 | import { QuartzTransformerPlugin } from "../types" 5 | 6 | export interface Options { 7 | bibliographyFile: string 8 | suppressBibliography: boolean 9 | linkCitations: boolean 10 | csl: string 11 | } 12 | 13 | const defaultOptions: Options = { 14 | bibliographyFile: "./bibliography.bib", 15 | suppressBibliography: false, 16 | linkCitations: false, 17 | csl: "apa", 18 | } 19 | 20 | export const Citations: QuartzTransformerPlugin | undefined> = (userOpts) => { 21 | const opts = { ...defaultOptions, ...userOpts } 22 | return { 23 | name: "Citations", 24 | htmlPlugins() { 25 | const plugins: PluggableList = [] 26 | 27 | // Add rehype-citation to the list of plugins 28 | plugins.push([ 29 | rehypeCitation, 30 | { 31 | bibliography: opts.bibliographyFile, 32 | suppressBibliography: opts.suppressBibliography, 33 | linkCitations: opts.linkCitations, 34 | }, 35 | ]) 36 | 37 | // Transform the HTML of the citattions; add data-no-popover property to the citation links 38 | // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links 39 | plugins.push(() => { 40 | return (tree, _file) => { 41 | visit(tree, "element", (node, index, parent) => { 42 | if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { 43 | node.properties["data-no-popover"] = true 44 | } 45 | }) 46 | } 47 | }) 48 | 49 | return plugins 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /quartz/components/scripts/toc.inline.ts: -------------------------------------------------------------------------------- 1 | const bufferPx = 150 2 | const observer = new IntersectionObserver((entries) => { 3 | for (const entry of entries) { 4 | const slug = entry.target.id 5 | const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`) 6 | const windowHeight = entry.rootBounds?.height 7 | if (windowHeight && tocEntryElement) { 8 | if (entry.boundingClientRect.y < windowHeight) { 9 | tocEntryElement.classList.add("in-view") 10 | } else { 11 | tocEntryElement.classList.remove("in-view") 12 | } 13 | } 14 | } 15 | }) 16 | 17 | function toggleToc(this: HTMLElement) { 18 | this.classList.toggle("collapsed") 19 | const content = this.nextElementSibling as HTMLElement | undefined 20 | if (!content) return 21 | content.classList.toggle("collapsed") 22 | content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" 23 | } 24 | 25 | function setupToc() { 26 | const toc = document.getElementById("toc") 27 | if (toc) { 28 | const collapsed = toc.classList.contains("collapsed") 29 | const content = toc.nextElementSibling as HTMLElement | undefined 30 | if (!content) return 31 | content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px" 32 | toc.addEventListener("click", toggleToc) 33 | window.addCleanup(() => toc.removeEventListener("click", toggleToc)) 34 | } 35 | } 36 | 37 | window.addEventListener("resize", setupToc) 38 | document.addEventListener("nav", () => { 39 | setupToc() 40 | 41 | // update toc entry highlighting 42 | observer.disconnect() 43 | const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]") 44 | headers.forEach((header) => observer.observe(header)) 45 | }) 46 | -------------------------------------------------------------------------------- /quartz/components/scripts/darkmode.inline.ts: -------------------------------------------------------------------------------- 1 | const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark" 2 | const currentTheme = localStorage.getItem("theme") ?? userPref 3 | document.documentElement.setAttribute("saved-theme", currentTheme) 4 | 5 | const emitThemeChangeEvent = (theme: "light" | "dark") => { 6 | const event: CustomEventMap["themechange"] = new CustomEvent("themechange", { 7 | detail: { theme }, 8 | }) 9 | document.dispatchEvent(event) 10 | } 11 | 12 | document.addEventListener("nav", () => { 13 | const switchTheme = (e: Event) => { 14 | const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" 15 | document.documentElement.setAttribute("saved-theme", newTheme) 16 | localStorage.setItem("theme", newTheme) 17 | emitThemeChangeEvent(newTheme) 18 | } 19 | 20 | const themeChange = (e: MediaQueryListEvent) => { 21 | const newTheme = e.matches ? "dark" : "light" 22 | document.documentElement.setAttribute("saved-theme", newTheme) 23 | localStorage.setItem("theme", newTheme) 24 | toggleSwitch.checked = e.matches 25 | emitThemeChangeEvent(newTheme) 26 | } 27 | 28 | // Darkmode toggle 29 | const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement 30 | toggleSwitch.addEventListener("change", switchTheme) 31 | window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) 32 | if (currentTheme === "dark") { 33 | toggleSwitch.checked = true 34 | } 35 | 36 | // Listen for changes in prefers-color-scheme 37 | const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") 38 | colorSchemeMediaQuery.addEventListener("change", themeChange) 39 | window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange)) 40 | }) 41 | -------------------------------------------------------------------------------- /docs/plugins/CrawlLinks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CrawlLinks 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin parses links and processes them to point to the right places. It is also needed for embedded links (like images). See [[Obsidian compatibility]] for more information. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `markdownLinkResolution`: Sets the strategy for resolving Markdown paths, can be `"absolute"` (default), `"relative"` or `"shortest"`. You should use the same setting here as in [[Obsidian compatibility|Obsidian]]. 15 | - `absolute`: Path relative to the root of the content folder. 16 | - `relative`: Path relative to the file you are linking from. 17 | - `shortest`: Name of the file. If this isn't enough to identify the file, use the full absolute path. 18 | - `prettyLinks`: If `true` (default), simplifies links by removing folder paths, making them more user friendly (e.g. `folder/deeply/nested/note` becomes `note`). 19 | - `openLinksInNewTab`: If `true`, configures external links to open in a new tab. Defaults to `false`. 20 | - `lazyLoad`: If `true`, adds lazy loading to resource elements (`img`, `video`, etc.) to improve page load performance. Defaults to `false`. 21 | - `externalLinkIcon`: Adds an icon next to external links when `true` (default) to visually distinguishing them from internal links. 22 | 23 | > [!warning] 24 | > Removing this plugin is _not_ recommended and will likely break the page. 25 | 26 | ## API 27 | 28 | - Category: Transformer 29 | - Function name: `Plugin.CrawlLinks()`. 30 | - Source: [`quartz/plugins/transformers/links.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/links.ts). 31 | -------------------------------------------------------------------------------- /docs/features/full-text search.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Full-text Search 3 | tags: 4 | - component 5 | --- 6 | 7 | Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words. 8 | 9 | It can be opened by either clicking on the search bar or pressing `⌘`/`ctrl` + `K`. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. 10 | 11 | To search content by tags, you can either press `⌘`/`ctrl` + `shift` + `K` or start your query with `#` (e.g. `#components`). 12 | 13 | This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). You are also able to navigate search results using `ArrowUp` and `ArrowDown`. 14 | 15 | > [!info] 16 | > Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. 17 | 18 | ### Indexing Behaviour 19 | 20 | By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed. 21 | 22 | It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title, content and tags, weighing title matches above content matches. 23 | 24 | ## Customization 25 | 26 | - Removing search: delete all usages of `Component.Search()` from `quartz.layout.ts`. 27 | - Component: `quartz/components/Search.tsx` 28 | - Style: `quartz/components/styles/search.scss` 29 | - Script: `quartz/components/scripts/search.inline.ts` 30 | - You can edit `contextWindowWords`, `numSearchResults` or `numTagResults` to suit your needs 31 | -------------------------------------------------------------------------------- /quartz/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { Translation, CalloutTranslation } from "./locales/definition" 2 | import en from "./locales/en-US" 3 | import fr from "./locales/fr-FR" 4 | import it from "./locales/it-IT" 5 | import ja from "./locales/ja-JP" 6 | import de from "./locales/de-DE" 7 | import nl from "./locales/nl-NL" 8 | import ro from "./locales/ro-RO" 9 | import es from "./locales/es-ES" 10 | import ar from "./locales/ar-SA" 11 | import uk from "./locales/uk-UA" 12 | import ru from "./locales/ru-RU" 13 | import ko from "./locales/ko-KR" 14 | import zh from "./locales/zh-CN" 15 | import vi from "./locales/vi-VN" 16 | import pt from "./locales/pt-BR" 17 | import hu from "./locales/hu-HU" 18 | import fa from "./locales/fa-IR" 19 | import pl from "./locales/pl-PL" 20 | 21 | export const TRANSLATIONS = { 22 | "en-US": en, 23 | "fr-FR": fr, 24 | "it-IT": it, 25 | "ja-JP": ja, 26 | "de-DE": de, 27 | "nl-NL": nl, 28 | "nl-BE": nl, 29 | "ro-RO": ro, 30 | "ro-MD": ro, 31 | "es-ES": es, 32 | "ar-SA": ar, 33 | "ar-AE": ar, 34 | "ar-QA": ar, 35 | "ar-BH": ar, 36 | "ar-KW": ar, 37 | "ar-OM": ar, 38 | "ar-YE": ar, 39 | "ar-IR": ar, 40 | "ar-SY": ar, 41 | "ar-IQ": ar, 42 | "ar-JO": ar, 43 | "ar-PL": ar, 44 | "ar-LB": ar, 45 | "ar-EG": ar, 46 | "ar-SD": ar, 47 | "ar-LY": ar, 48 | "ar-MA": ar, 49 | "ar-TN": ar, 50 | "ar-DZ": ar, 51 | "ar-MR": ar, 52 | "uk-UA": uk, 53 | "ru-RU": ru, 54 | "ko-KR": ko, 55 | "zh-CN": zh, 56 | "vi-VN": vi, 57 | "pt-BR": pt, 58 | "hu-HU": hu, 59 | "fa-IR": fa, 60 | "pl-PL": pl, 61 | } as const 62 | 63 | export const defaultTranslation = "en-US" 64 | export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation] 65 | export type ValidLocale = keyof typeof TRANSLATIONS 66 | export type ValidCallout = keyof CalloutTranslation 67 | -------------------------------------------------------------------------------- /quartz/components/styles/popover.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/variables.scss" as *; 2 | 3 | @keyframes dropin { 4 | 0% { 5 | opacity: 0; 6 | visibility: hidden; 7 | } 8 | 1% { 9 | opacity: 0; 10 | } 11 | 100% { 12 | opacity: 1; 13 | visibility: visible; 14 | } 15 | } 16 | 17 | .popover { 18 | z-index: 999; 19 | position: absolute; 20 | overflow: visible; 21 | padding: 1rem; 22 | 23 | & > .popover-inner { 24 | position: relative; 25 | width: 30rem; 26 | max-height: 20rem; 27 | padding: 0 1rem 1rem 1rem; 28 | font-weight: initial; 29 | font-style: initial; 30 | line-height: normal; 31 | font-size: initial; 32 | font-family: var(--bodyFont); 33 | border: 1px solid var(--lightgray); 34 | background-color: var(--light); 35 | border-radius: 5px; 36 | box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25); 37 | overflow: auto; 38 | white-space: normal; 39 | } 40 | 41 | & > .popover-inner[data-content-type] { 42 | &[data-content-type*="pdf"], 43 | &[data-content-type*="image"] { 44 | padding: 0; 45 | max-height: 100%; 46 | } 47 | 48 | &[data-content-type*="image"] { 49 | img { 50 | margin: 0; 51 | border-radius: 0; 52 | display: block; 53 | } 54 | } 55 | 56 | &[data-content-type*="pdf"] { 57 | iframe { 58 | width: 100%; 59 | } 60 | } 61 | } 62 | 63 | h1 { 64 | font-size: 1.5rem; 65 | } 66 | 67 | visibility: hidden; 68 | opacity: 0; 69 | transition: 70 | opacity 0.3s ease, 71 | visibility 0.3s ease; 72 | 73 | @media all and (max-width: $mobileBreakpoint) { 74 | display: none !important; 75 | } 76 | } 77 | 78 | a:hover .popover, 79 | .popover:hover { 80 | animation: dropin 0.3s ease; 81 | animation-fill-mode: forwards; 82 | animation-delay: 0.2s; 83 | } 84 | -------------------------------------------------------------------------------- /docs/showcase.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Quartz Showcase" 3 | --- 4 | 5 | Want to see what Quartz can do? Here are some cool community gardens: 6 | 7 | - [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/) 8 | - [Jacky Zhao's Garden](https://jzhao.xyz/) 9 | - [Socratica Toolbox](https://toolbox.socratica.info/) 10 | - [oldwinter の数字花园](https://garden.oldwinter.top/) 11 | - [Aaron Pham's Garden](https://aarnphm.xyz/) 12 | - [The Quantum Garden](https://quantumgardener.blog/) 13 | - [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/) 14 | - [Matt Dunn's Second Brain](https://mattdunn.info/) 15 | - [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) 16 | - [Vince Imbat's Talahardin](https://vinceimbat.com/) 17 | - [🧠🌳 Chad's Mind Garden](https://www.chadly.net/) 18 | - [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/) 19 | - [Mau Camargo's Notkesto](https://notes.camargomau.com/) 20 | - [Caicai's Novels](https://imoko.cc/blog/caicai/) 21 | - [🌊 Collapsed Wave](https://collapsedwave.com/) 22 | - [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/) 23 | - [Mike's AI Garden 🤖🪴](https://mwalton.me/) 24 | - [Brandon Boswell's Garden](https://brandonkboswell.com) 25 | - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) 26 | - [Data Dictionary 🧠](https://glossary.airbyte.com/) 27 | - [sspaeti.com's Second Brain](https://brain.sspaeti.com/) 28 | - [🪴Aster's notebook](https://notes.asterhu.com) 29 | - [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja) 30 | - [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/) 31 | - [Gatekeeper Wiki](https://www.gatekeeper.wiki) 32 | 33 | If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! 34 | -------------------------------------------------------------------------------- /content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About this dictionary" 3 | date: 2019-06-16T16:32:44+02:00 4 | draft: false 5 | --- 6 | 7 | Read more here: 8 | 9 | - https://stereobooster.com/posts/dev.wtf/ 10 | - https://stereobooster.com/posts/how-little-we-know-about-software-development/ 11 | 12 | ## Alternatives 13 | 14 | There are a lot of wikis, enciclopedias, dictionaries on the subject: 15 | 16 | - [WikiWikiWeb](https://wiki.c2.com/) 17 | - [Dictionary of Algorithms and Data Structures](https://xlinux.nist.gov/dads/) 18 | - [BABEL: A Glossary of Computer Oriented Abbreviations and Acronyms](https://www.arcelect.com/babel99.htm) 19 | - [The Jargon Lexicon](http://www.catb.org/~esr/jargon/html/go01.html) 20 | - [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) obviously 21 | - [MDN](https://developer.mozilla.org/en-US/) 22 | - [From Nand to Tetris](https://www.nand2tetris.org/) 23 | - [Paradigms of Computer Programming – Fundamentals](https://www.edx.org/learn/computer-programming/universite-catholique-de-louvain-paradigms-of-computer-programming-fundamentals) 24 | - [Finite of Sense and Infinite of Thought](https://pron.github.io/posts/computation-logic-algebra-pt1) 25 | - [Parsing: a timeline](https://jeffreykegler.github.io/personal/timeline_v3) 26 | - [The Early Development of Programming Languages](http://bitsavers.trailing-edge.com/pdf/stanford/cs_techReports/STAN-CS-76-562_EarlyDevelPgmgLang_Aug76.pdf), by Knuth 27 | - [Awesome Cold Showers](https://github.com/hwayne/awesome-cold-showers) 28 | - etc. 29 | 30 | Not public domain: 31 | 32 | - [TechDictionary](https://www.techopedia.com/it-terms/a) 33 | - [DevX Technology Glossary](https://www.devx.com/terms/) 34 | 35 | ## Not NPOV 36 | 37 | Wikipedia enforces its entries to adopt an NPOV – a neutral point of view. This is appropriate for an encyclopedia. But it is not required for this wiki. See [nPOV](https://ncatlab.org/nlab/show/nPOV). 38 | -------------------------------------------------------------------------------- /quartz/plugins/types.ts: -------------------------------------------------------------------------------- 1 | import { PluggableList } from "unified" 2 | import { StaticResources } from "../util/resources" 3 | import { ProcessedContent } from "./vfile" 4 | import { QuartzComponent } from "../components/types" 5 | import { FilePath } from "../util/path" 6 | import { BuildCtx } from "../util/ctx" 7 | import DepGraph from "../depgraph" 8 | 9 | export interface PluginTypes { 10 | transformers: QuartzTransformerPluginInstance[] 11 | filters: QuartzFilterPluginInstance[] 12 | emitters: QuartzEmitterPluginInstance[] 13 | } 14 | 15 | type OptionType = object | undefined 16 | export type QuartzTransformerPlugin = ( 17 | opts?: Options, 18 | ) => QuartzTransformerPluginInstance 19 | export type QuartzTransformerPluginInstance = { 20 | name: string 21 | textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer 22 | markdownPlugins?: (ctx: BuildCtx) => PluggableList 23 | htmlPlugins?: (ctx: BuildCtx) => PluggableList 24 | externalResources?: (ctx: BuildCtx) => Partial 25 | } 26 | 27 | export type QuartzFilterPlugin = ( 28 | opts?: Options, 29 | ) => QuartzFilterPluginInstance 30 | export type QuartzFilterPluginInstance = { 31 | name: string 32 | shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean 33 | } 34 | 35 | export type QuartzEmitterPlugin = ( 36 | opts?: Options, 37 | ) => QuartzEmitterPluginInstance 38 | export type QuartzEmitterPluginInstance = { 39 | name: string 40 | emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise 41 | getQuartzComponents(ctx: BuildCtx): QuartzComponent[] 42 | getDependencyGraph?( 43 | ctx: BuildCtx, 44 | content: ProcessedContent[], 45 | resources: StaticResources, 46 | ): Promise> 47 | } 48 | -------------------------------------------------------------------------------- /quartz/cli/helpers.js: -------------------------------------------------------------------------------- 1 | import { isCancel, outro } from "@clack/prompts" 2 | import chalk from "chalk" 3 | import { contentCacheFolder } from "./constants.js" 4 | import { spawnSync } from "child_process" 5 | import fs from "fs" 6 | 7 | export function escapePath(fp) { 8 | return fp 9 | .replace(/\\ /g, " ") // unescape spaces 10 | .replace(/^".*"$/, "$1") 11 | .replace(/^'.*"$/, "$1") 12 | .trim() 13 | } 14 | 15 | export function exitIfCancel(val) { 16 | if (isCancel(val)) { 17 | outro(chalk.red("Exiting")) 18 | process.exit(0) 19 | } else { 20 | return val 21 | } 22 | } 23 | 24 | export async function stashContentFolder(contentFolder) { 25 | await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) 26 | await fs.promises.cp(contentFolder, contentCacheFolder, { 27 | force: true, 28 | recursive: true, 29 | verbatimSymlinks: true, 30 | preserveTimestamps: true, 31 | }) 32 | await fs.promises.rm(contentFolder, { force: true, recursive: true }) 33 | } 34 | 35 | export function gitPull(origin, branch) { 36 | const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"] 37 | const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) 38 | if (out.stderr) { 39 | throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`)) 40 | } else if (out.status !== 0) { 41 | throw new Error(chalk.red("Error while pulling updates")) 42 | } 43 | } 44 | 45 | export async function popContentFolder(contentFolder) { 46 | await fs.promises.rm(contentFolder, { force: true, recursive: true }) 47 | await fs.promises.cp(contentCacheFolder, contentFolder, { 48 | force: true, 49 | recursive: true, 50 | verbatimSymlinks: true, 51 | preserveTimestamps: true, 52 | }) 53 | await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) 54 | } 55 | -------------------------------------------------------------------------------- /quartz/components/ContentMeta.tsx: -------------------------------------------------------------------------------- 1 | import { formatDate, getDate } from "./Date" 2 | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 | import readingTime from "reading-time" 4 | import { classNames } from "../util/lang" 5 | import { i18n } from "../i18n" 6 | import { JSX } from "preact" 7 | import style from "./styles/contentMeta.scss" 8 | 9 | interface ContentMetaOptions { 10 | /** 11 | * Whether to display reading time 12 | */ 13 | showReadingTime: boolean 14 | showComma: boolean 15 | } 16 | 17 | const defaultOptions: ContentMetaOptions = { 18 | showReadingTime: true, 19 | showComma: true, 20 | } 21 | 22 | export default ((opts?: Partial) => { 23 | // Merge options with defaults 24 | const options: ContentMetaOptions = { ...defaultOptions, ...opts } 25 | 26 | function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) { 27 | const text = fileData.text 28 | 29 | if (text) { 30 | const segments: (string | JSX.Element)[] = [] 31 | 32 | if (fileData.dates) { 33 | segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale)) 34 | } 35 | 36 | // Display reading time if enabled 37 | if (options.showReadingTime) { 38 | const { minutes, words: _words } = readingTime(text) 39 | const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({ 40 | minutes: Math.ceil(minutes), 41 | }) 42 | segments.push(displayedTime) 43 | } 44 | 45 | const segmentsElements = segments.map((segment) => {segment}) 46 | 47 | return ( 48 |

49 | {segmentsElements} 50 |

51 | ) 52 | } else { 53 | return null 54 | } 55 | } 56 | 57 | ContentMetadata.css = style 58 | 59 | return ContentMetadata 60 | }) satisfies QuartzComponentConstructor 61 | -------------------------------------------------------------------------------- /docs/plugins/ObsidianFlavoredMarkdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ObsidianFlavoredMarkdown 3 | tags: 4 | - plugin/transformer 5 | --- 6 | 7 | This plugin provides support for [[Obsidian compatibility]]. 8 | 9 | > [!note] 10 | > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. 11 | 12 | This plugin accepts the following configuration options: 13 | 14 | - `comments`: If `true` (default), enables parsing of `%%` style Obsidian comment blocks. 15 | - `highlight`: If `true` (default), enables parsing of `==` style highlights within content. 16 | - `wikilinks`:If `true` (default), turns [[wikilinks]] into regular links. 17 | - `callouts`: If `true` (default), adds support for [[callouts|callout]] blocks for emphasizing content. 18 | - `mermaid`: If `true` (default), enables [[Mermaid diagrams|Mermaid diagram]] rendering within Markdown files. 19 | - `parseTags`: If `true` (default), parses and links tags within the content. 20 | - `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents. 21 | - `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks. 22 | - `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`. 23 | - `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax. 24 | - `enableVideoEmbed`: If `true` (default), enables the embedding of video files. 25 | - `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`. 26 | 27 | > [!warning] 28 | > Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content! 29 | 30 | ## API 31 | 32 | - Category: Transformer 33 | - Function name: `Plugin.ObsidianFlavoredMarkdown()`. 34 | - Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts). 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - v4 7 | push: 8 | branches: 9 | - v4 10 | 11 | jobs: 12 | build-and-test: 13 | if: ${{ github.repository == 'jackyzha0/quartz' }} 14 | strategy: 15 | matrix: 16 | os: [windows-latest, macos-latest, ubuntu-latest] 17 | runs-on: ${{ matrix.os }} 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | 30 | - name: Cache dependencies 31 | uses: actions/cache@v3 32 | with: 33 | path: ~/.npm 34 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: | 36 | ${{ runner.os }}-node- 37 | 38 | - run: npm ci 39 | 40 | - name: Check types and style 41 | run: npm run check 42 | 43 | - name: Test 44 | run: npm test 45 | 46 | - name: Ensure Quartz builds, check bundle info 47 | run: npx quartz build --bundleInfo 48 | 49 | publish-tag: 50 | if: ${{ github.repository == 'jackyzha0/quartz' }} 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: write 54 | steps: 55 | - uses: actions/checkout@v3 56 | with: 57 | fetch-depth: 0 58 | - name: Setup Node 59 | uses: actions/setup-node@v3 60 | with: 61 | node-version: 18 62 | - name: Get package version 63 | run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV 64 | - name: Create release tag 65 | uses: pkgdeps/git-tag-action@v2 66 | with: 67 | github_token: ${{ secrets.GITHUB_TOKEN }} 68 | github_repo: ${{ github.repository }} 69 | version: ${{ env.PACKAGE_VERSION }} 70 | git_commit_sha: ${{ github.sha }} 71 | git_tag_prefix: "v" 72 | -------------------------------------------------------------------------------- /quartz/i18n/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "无题", 6 | description: "无描述", 7 | }, 8 | components: { 9 | callout: { 10 | note: "笔记", 11 | abstract: "摘要", 12 | info: "提示", 13 | todo: "待办", 14 | tip: "提示", 15 | success: "成功", 16 | question: "问题", 17 | warning: "警告", 18 | failure: "失败", 19 | danger: "危险", 20 | bug: "错误", 21 | example: "示例", 22 | quote: "引用", 23 | }, 24 | backlinks: { 25 | title: "反向链接", 26 | noBacklinksFound: "无法找到反向链接", 27 | }, 28 | themeToggle: { 29 | lightMode: "亮色模式", 30 | darkMode: "暗色模式", 31 | }, 32 | explorer: { 33 | title: "探索", 34 | }, 35 | footer: { 36 | createdWith: "Created with", 37 | }, 38 | graph: { 39 | title: "关系图谱", 40 | }, 41 | recentNotes: { 42 | title: "最近的笔记", 43 | seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `包含${targetSlug}`, 47 | linkToOriginal: "指向原始笔记的链接", 48 | }, 49 | search: { 50 | title: "搜索", 51 | searchBarPlaceholder: "搜索些什么", 52 | }, 53 | tableOfContents: { 54 | title: "目录", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes}分钟阅读`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "最近的笔记", 63 | lastFewNotes: ({ count }) => `最近的${count}条笔记`, 64 | }, 65 | error: { 66 | title: "无法找到", 67 | notFound: "私有笔记或笔记不存在。", 68 | home: "返回首页", 69 | }, 70 | folderContent: { 71 | folder: "文件夹", 72 | itemsUnderFolder: ({ count }) => `此文件夹下有${count}条笔记。`, 73 | }, 74 | tagContent: { 75 | tag: "标签", 76 | tagIndex: "标签索引", 77 | itemsUnderTag: ({ count }) => `此标签下有${count}条笔记。`, 78 | showingFirst: ({ count }) => `显示前${count}个标签。`, 79 | totalTags: ({ count }) => `总共有${count}个标签。`, 80 | }, 81 | }, 82 | } as const satisfies Translation 83 | -------------------------------------------------------------------------------- /quartz/i18n/locales/definition.ts: -------------------------------------------------------------------------------- 1 | import { FullSlug } from "../../util/path" 2 | 3 | export interface CalloutTranslation { 4 | note: string 5 | abstract: string 6 | info: string 7 | todo: string 8 | tip: string 9 | success: string 10 | question: string 11 | warning: string 12 | failure: string 13 | danger: string 14 | bug: string 15 | example: string 16 | quote: string 17 | } 18 | 19 | export interface Translation { 20 | propertyDefaults: { 21 | title: string 22 | description: string 23 | } 24 | components: { 25 | callout: CalloutTranslation 26 | backlinks: { 27 | title: string 28 | noBacklinksFound: string 29 | } 30 | themeToggle: { 31 | lightMode: string 32 | darkMode: string 33 | } 34 | explorer: { 35 | title: string 36 | } 37 | footer: { 38 | createdWith: string 39 | } 40 | graph: { 41 | title: string 42 | } 43 | recentNotes: { 44 | title: string 45 | seeRemainingMore: (variables: { remaining: number }) => string 46 | } 47 | transcludes: { 48 | transcludeOf: (variables: { targetSlug: FullSlug }) => string 49 | linkToOriginal: string 50 | } 51 | search: { 52 | title: string 53 | searchBarPlaceholder: string 54 | } 55 | tableOfContents: { 56 | title: string 57 | } 58 | contentMeta: { 59 | readingTime: (variables: { minutes: number }) => string 60 | } 61 | } 62 | pages: { 63 | rss: { 64 | recentNotes: string 65 | lastFewNotes: (variables: { count: number }) => string 66 | } 67 | error: { 68 | title: string 69 | notFound: string 70 | home: string 71 | } 72 | folderContent: { 73 | folder: string 74 | itemsUnderFolder: (variables: { count: number }) => string 75 | } 76 | tagContent: { 77 | tag: string 78 | tagIndex: string 79 | itemsUnderTag: (variables: { count: number }) => string 80 | showingFirst: (variables: { count: number }) => string 81 | totalTags: (variables: { count: number }) => string 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /quartz/components/scripts/clipboard.inline.ts: -------------------------------------------------------------------------------- 1 | const svgCopy = 2 | '' 3 | const svgCheck = 4 | '' 5 | 6 | document.addEventListener("nav", () => { 7 | const els = document.getElementsByTagName("pre") 8 | for (let i = 0; i < els.length; i++) { 9 | const codeBlock = els[i].getElementsByTagName("code")[0] 10 | if (codeBlock) { 11 | const source = codeBlock.innerText.replace(/\n\n/g, "\n") 12 | const button = document.createElement("button") 13 | button.className = "clipboard-button" 14 | button.type = "button" 15 | button.innerHTML = svgCopy 16 | button.ariaLabel = "Copy source" 17 | function onClick() { 18 | navigator.clipboard.writeText(source).then( 19 | () => { 20 | button.blur() 21 | button.innerHTML = svgCheck 22 | setTimeout(() => { 23 | button.innerHTML = svgCopy 24 | button.style.borderColor = "" 25 | }, 2000) 26 | }, 27 | (error) => console.error(error), 28 | ) 29 | } 30 | button.addEventListener("click", onClick) 31 | window.addCleanup(() => button.removeEventListener("click", onClick)) 32 | els[i].prepend(button) 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /quartz/i18n/locales/ja-JP.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "無題", 6 | description: "説明なし", 7 | }, 8 | components: { 9 | callout: { 10 | note: "ノート", 11 | abstract: "抄録", 12 | info: "情報", 13 | todo: "やるべきこと", 14 | tip: "ヒント", 15 | success: "成功", 16 | question: "質問", 17 | warning: "警告", 18 | failure: "失敗", 19 | danger: "危険", 20 | bug: "バグ", 21 | example: "例", 22 | quote: "引用", 23 | }, 24 | backlinks: { 25 | title: "バックリンク", 26 | noBacklinksFound: "バックリンクはありません", 27 | }, 28 | themeToggle: { 29 | lightMode: "ライトモード", 30 | darkMode: "ダークモード", 31 | }, 32 | explorer: { 33 | title: "エクスプローラー", 34 | }, 35 | footer: { 36 | createdWith: "作成", 37 | }, 38 | graph: { 39 | title: "グラフビュー", 40 | }, 41 | recentNotes: { 42 | title: "最近の記事", 43 | seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`, 47 | linkToOriginal: "元記事へのリンク", 48 | }, 49 | search: { 50 | title: "検索", 51 | searchBarPlaceholder: "検索ワードを入力", 52 | }, 53 | tableOfContents: { 54 | title: "目次", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "最近の記事", 63 | lastFewNotes: ({ count }) => `最新の${count}件`, 64 | }, 65 | error: { 66 | title: "Not Found", 67 | notFound: "ページが存在しないか、非公開設定になっています。", 68 | home: "ホームページに戻る", 69 | }, 70 | folderContent: { 71 | folder: "フォルダ", 72 | itemsUnderFolder: ({ count }) => `${count}件のページ`, 73 | }, 74 | tagContent: { 75 | tag: "タグ", 76 | tagIndex: "タグ一覧", 77 | itemsUnderTag: ({ count }) => `${count}件のページ`, 78 | showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`, 79 | totalTags: ({ count }) => `全${count}個のタグを表示中`, 80 | }, 81 | }, 82 | } as const satisfies Translation 83 | -------------------------------------------------------------------------------- /quartz/i18n/locales/ko-KR.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "제목 없음", 6 | description: "설명 없음", 7 | }, 8 | components: { 9 | callout: { 10 | note: "노트", 11 | abstract: "개요", 12 | info: "정보", 13 | todo: "할일", 14 | tip: "팁", 15 | success: "성공", 16 | question: "질문", 17 | warning: "주의", 18 | failure: "실패", 19 | danger: "위험", 20 | bug: "버그", 21 | example: "예시", 22 | quote: "인용", 23 | }, 24 | backlinks: { 25 | title: "백링크", 26 | noBacklinksFound: "백링크가 없습니다.", 27 | }, 28 | themeToggle: { 29 | lightMode: "라이트 모드", 30 | darkMode: "다크 모드", 31 | }, 32 | explorer: { 33 | title: "탐색기", 34 | }, 35 | footer: { 36 | createdWith: "Created with", 37 | }, 38 | graph: { 39 | title: "그래프 뷰", 40 | }, 41 | recentNotes: { 42 | title: "최근 게시글", 43 | seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`, 47 | linkToOriginal: "원본 링크", 48 | }, 49 | search: { 50 | title: "검색", 51 | searchBarPlaceholder: "검색어를 입력하세요", 52 | }, 53 | tableOfContents: { 54 | title: "목차", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "최근 게시글", 63 | lastFewNotes: ({ count }) => `최근 ${count} 건`, 64 | }, 65 | error: { 66 | title: "Not Found", 67 | notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.", 68 | home: "홈페이지로 돌아가기", 69 | }, 70 | folderContent: { 71 | folder: "폴더", 72 | itemsUnderFolder: ({ count }) => `${count}건의 항목`, 73 | }, 74 | tagContent: { 75 | tag: "태그", 76 | tagIndex: "태그 목록", 77 | itemsUnderTag: ({ count }) => `${count}건의 항목`, 78 | showingFirst: ({ count }) => `처음 ${count}개의 태그`, 79 | totalTags: ({ count }) => `총 ${count}개의 태그를 찾았습니다.`, 80 | }, 81 | }, 82 | } as const satisfies Translation 83 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/assets.ts: -------------------------------------------------------------------------------- 1 | import { FilePath, joinSegments, slugifyFilePath } from "../../util/path" 2 | import { QuartzEmitterPlugin } from "../types" 3 | import path from "path" 4 | import fs from "fs" 5 | import { glob } from "../../util/glob" 6 | import DepGraph from "../../depgraph" 7 | import { Argv } from "../../util/ctx" 8 | import { QuartzConfig } from "../../cfg" 9 | 10 | const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => { 11 | // glob all non MD files in content folder and copy it over 12 | return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns]) 13 | } 14 | 15 | export const Assets: QuartzEmitterPlugin = () => { 16 | return { 17 | name: "Assets", 18 | getQuartzComponents() { 19 | return [] 20 | }, 21 | async getDependencyGraph(ctx, _content, _resources) { 22 | const { argv, cfg } = ctx 23 | const graph = new DepGraph() 24 | 25 | const fps = await filesToCopy(argv, cfg) 26 | 27 | for (const fp of fps) { 28 | const ext = path.extname(fp) 29 | const src = joinSegments(argv.directory, fp) as FilePath 30 | const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath 31 | 32 | const dest = joinSegments(argv.output, name) as FilePath 33 | 34 | graph.addEdge(src, dest) 35 | } 36 | 37 | return graph 38 | }, 39 | async emit({ argv, cfg }, _content, _resources): Promise { 40 | const assetsPath = argv.output 41 | const fps = await filesToCopy(argv, cfg) 42 | const res: FilePath[] = [] 43 | for (const fp of fps) { 44 | const ext = path.extname(fp) 45 | const src = joinSegments(argv.directory, fp) as FilePath 46 | const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath 47 | 48 | const dest = joinSegments(assetsPath, name) as FilePath 49 | const dir = path.dirname(dest) as FilePath 50 | await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists 51 | await fs.promises.copyFile(src, dest) 52 | res.push(dest) 53 | } 54 | 55 | return res 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /quartz/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 | import style from "./styles/search.scss" 3 | // @ts-ignore 4 | import script from "./scripts/search.inline" 5 | import { classNames } from "../util/lang" 6 | import { i18n } from "../i18n" 7 | 8 | export interface SearchOptions { 9 | enablePreview: boolean 10 | } 11 | 12 | const defaultOptions: SearchOptions = { 13 | enablePreview: true, 14 | } 15 | 16 | export default ((userOpts?: Partial) => { 17 | const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { 18 | const opts = { ...defaultOptions, ...userOpts } 19 | const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder 20 | return ( 21 |
22 |
23 |

{i18n(cfg.locale).components.search.title}

24 |
25 | 32 | Search 33 | Search 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | 50 |
51 |
52 |
53 |
54 | ) 55 | } 56 | 57 | Search.afterDOMLoaded = script 58 | Search.css = style 59 | 60 | return Search 61 | }) satisfies QuartzComponentConstructor 62 | -------------------------------------------------------------------------------- /docs/features/folder and tag listings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Folder and Tag Listings 3 | tags: 4 | - feature/emitter 5 | --- 6 | 7 | Quartz emits listing pages for any folders and tags you have. 8 | 9 | ## Folder Listings 10 | 11 | Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep. 12 | 13 | Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`. 14 | 15 | You can link to the folder listing by referencing its name, plus a trailing slash, like this: `[[advanced/]]` (results in [[advanced/]]). 16 | 17 | By default, Quartz will title the page `Folder: ` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the folder description. 18 | 19 | For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it. 20 | 21 | ## Tag Listings 22 | 23 | Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag. 24 | 25 | Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each level of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz. 26 | 27 | You can link to the tag listing by referencing its name with a `tag/` prefix, like this: `[[tags/plugin]]` (results in [[tags/plugin]]). 28 | 29 | As with folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description. 30 | 31 | ## Customization 32 | 33 | The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options. 34 | -------------------------------------------------------------------------------- /docs/setting up your GitHub repository.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting up your GitHub repository 3 | --- 4 | 5 | First, make sure you have Quartz [[index#🪴 Get Started|cloned and setup locally]]. 6 | 7 | Then, create a new repository on GitHub.com. Do **not** initialize the new repository with `README`, license, or `gitignore` files. 8 | 9 | ![[github-init-repo-options.png]] 10 | 11 | At the top of your repository on GitHub.com's Quick Setup page, click the clipboard to copy the remote repository URL. 12 | 13 | ![[github-quick-setup.png]] 14 | 15 | In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step. 16 | 17 | ```bash 18 | # list all the repositories that are tracked 19 | git remote -v 20 | 21 | # if the origin doesn't match your own repository, set your repository as the origin 22 | git remote set-url origin REMOTE-URL 23 | 24 | # if you don't have upstream as a remote, add it so updates work 25 | git remote add upstream https://github.com/jackyzha0/quartz.git 26 | ``` 27 | 28 | Then, you can sync the content to upload it to your repository. This is a helper command that will do the initial push of your content to your repository. 29 | 30 | ```bash 31 | npx quartz sync --no-pull 32 | ``` 33 | 34 | > [!warning]- `fatal: --[no-]autostash option is only valid with --rebase` 35 | > You may have an outdated version of `git`. Updating `git` should fix this issue. 36 | 37 | In future updates, you can simply run `npx quartz sync` every time you want to push updates to your repository. 38 | 39 | > [!hint] Flags and options 40 | > For full help options, you can run `npx quartz sync --help`. 41 | > 42 | > Most of these have sensible defaults but you can override them if you have a custom setup: 43 | > 44 | > - `-d` or `--directory`: the content folder. This is normally just `content` 45 | > - `-v` or `--verbose`: print out extra logging information 46 | > - `--commit` or `--no-commit`: whether to make a `git` commit for your changes 47 | > - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz 48 | > - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing 49 | -------------------------------------------------------------------------------- /docs/features/private pages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Private Pages 3 | tags: 4 | - feature/filter 5 | --- 6 | 7 | There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction: 8 | 9 | ## Filter Plugins 10 | 11 | [[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the [[RemoveDrafts]] plugin which filters out any note that has `draft: true` in the frontmatter. 12 | 13 | If you'd like to only publish a select number of notes, you can instead use [[ExplicitPublish]] which will filter out all notes except for any that have `publish: true` in the frontmatter. 14 | 15 | > [!warning] 16 | > Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array. 17 | > 18 | > `"!(PublicMedia)**/!(*.md)", "!(*.md)"` 19 | 20 | ## `ignorePatterns` 21 | 22 | This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [fast-glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here. 23 | 24 | > [!note] 25 | > Bash's glob syntax is slightly different from fast-glob's and using bash's syntax may lead to unexpected results. 26 | 27 | Common examples include: 28 | 29 | - `some/folder`: exclude the entire of `some/folder` 30 | - `*.md`: exclude all files with a `.md` extension 31 | - `!*.md` exclude all files that _don't_ have a `.md` extension 32 | - `**/private`: exclude any files or folders named `private` at any level of nesting 33 | 34 | > [!warning] 35 | > Marking something as private via either a plugin or through the `ignorePatterns` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information. 36 | -------------------------------------------------------------------------------- /quartz/util/theme.ts: -------------------------------------------------------------------------------- 1 | export interface ColorScheme { 2 | light: string 3 | lightgray: string 4 | gray: string 5 | darkgray: string 6 | dark: string 7 | secondary: string 8 | tertiary: string 9 | highlight: string 10 | } 11 | 12 | interface Colors { 13 | lightMode: ColorScheme 14 | darkMode: ColorScheme 15 | } 16 | 17 | export interface Theme { 18 | typography: { 19 | header: string 20 | body: string 21 | code: string 22 | } 23 | cdnCaching: boolean 24 | colors: Colors 25 | fontOrigin: "googleFonts" | "local" 26 | } 27 | 28 | export type ThemeKey = keyof Colors 29 | 30 | const DEFAULT_SANS_SERIF = 31 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' 32 | const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" 33 | 34 | export function googleFontHref(theme: Theme) { 35 | const { code, header, body } = theme.typography 36 | return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap` 37 | } 38 | 39 | export function joinStyles(theme: Theme, ...stylesheet: string[]) { 40 | return ` 41 | ${stylesheet.join("\n\n")} 42 | 43 | :root { 44 | --light: ${theme.colors.lightMode.light}; 45 | --lightgray: ${theme.colors.lightMode.lightgray}; 46 | --gray: ${theme.colors.lightMode.gray}; 47 | --darkgray: ${theme.colors.lightMode.darkgray}; 48 | --dark: ${theme.colors.lightMode.dark}; 49 | --secondary: ${theme.colors.lightMode.secondary}; 50 | --tertiary: ${theme.colors.lightMode.tertiary}; 51 | --highlight: ${theme.colors.lightMode.highlight}; 52 | 53 | --headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF}; 54 | --bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF}; 55 | --codeFont: "${theme.typography.code}", ${DEFAULT_MONO}; 56 | } 57 | 58 | :root[saved-theme="dark"] { 59 | --light: ${theme.colors.darkMode.light}; 60 | --lightgray: ${theme.colors.darkMode.lightgray}; 61 | --gray: ${theme.colors.darkMode.gray}; 62 | --darkgray: ${theme.colors.darkMode.darkgray}; 63 | --dark: ${theme.colors.darkMode.dark}; 64 | --secondary: ${theme.colors.darkMode.secondary}; 65 | --tertiary: ${theme.colors.darkMode.tertiary}; 66 | --highlight: ${theme.colors.darkMode.highlight}; 67 | } 68 | ` 69 | } 70 | -------------------------------------------------------------------------------- /quartz/plugins/emitters/404.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzEmitterPlugin } from "../types" 2 | import { QuartzComponentProps } from "../../components/types" 3 | import BodyConstructor from "../../components/Body" 4 | import { pageResources, renderPage } from "../../components/renderPage" 5 | import { FullPageLayout } from "../../cfg" 6 | import { FilePath, FullSlug } from "../../util/path" 7 | import { sharedPageComponents } from "../../../quartz.layout" 8 | import { NotFound } from "../../components" 9 | import { defaultProcessedContent } from "../vfile" 10 | import { write } from "./helpers" 11 | import { i18n } from "../../i18n" 12 | import DepGraph from "../../depgraph" 13 | 14 | export const NotFoundPage: QuartzEmitterPlugin = () => { 15 | const opts: FullPageLayout = { 16 | ...sharedPageComponents, 17 | pageBody: NotFound(), 18 | beforeBody: [], 19 | left: [], 20 | right: [], 21 | } 22 | 23 | const { head: Head, pageBody, footer: Footer } = opts 24 | const Body = BodyConstructor() 25 | 26 | return { 27 | name: "404Page", 28 | getQuartzComponents() { 29 | return [Head, Body, pageBody, Footer] 30 | }, 31 | async getDependencyGraph(_ctx, _content, _resources) { 32 | return new DepGraph() 33 | }, 34 | async emit(ctx, _content, resources): Promise { 35 | const cfg = ctx.cfg.configuration 36 | const slug = "404" as FullSlug 37 | 38 | const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) 39 | const path = url.pathname as FullSlug 40 | const externalResources = pageResources(path, resources) 41 | const notFound = i18n(cfg.locale).pages.error.title 42 | const [tree, vfile] = defaultProcessedContent({ 43 | slug, 44 | text: notFound, 45 | description: notFound, 46 | frontmatter: { title: notFound, tags: [] }, 47 | }) 48 | const componentData: QuartzComponentProps = { 49 | ctx, 50 | fileData: vfile.data, 51 | externalResources, 52 | cfg, 53 | children: [], 54 | tree, 55 | allFiles: [], 56 | } 57 | 58 | return [ 59 | await write({ 60 | ctx, 61 | content: renderPage(cfg, slug, componentData, opts, externalResources), 62 | slug, 63 | ext: ".html", 64 | }), 65 | ] 66 | }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/toc.ts: -------------------------------------------------------------------------------- 1 | import { QuartzTransformerPlugin } from "../types" 2 | import { Root } from "mdast" 3 | import { visit } from "unist-util-visit" 4 | import { toString } from "mdast-util-to-string" 5 | import Slugger from "github-slugger" 6 | 7 | export interface Options { 8 | maxDepth: 1 | 2 | 3 | 4 | 5 | 6 9 | minEntries: number 10 | showByDefault: boolean 11 | collapseByDefault: boolean 12 | } 13 | 14 | const defaultOptions: Options = { 15 | maxDepth: 3, 16 | minEntries: 1, 17 | showByDefault: true, 18 | collapseByDefault: false, 19 | } 20 | 21 | interface TocEntry { 22 | depth: number 23 | text: string 24 | slug: string // this is just the anchor (#some-slug), not the canonical slug 25 | } 26 | 27 | const slugAnchor = new Slugger() 28 | export const TableOfContents: QuartzTransformerPlugin | undefined> = ( 29 | userOpts, 30 | ) => { 31 | const opts = { ...defaultOptions, ...userOpts } 32 | return { 33 | name: "TableOfContents", 34 | markdownPlugins() { 35 | return [ 36 | () => { 37 | return async (tree: Root, file) => { 38 | const display = file.data.frontmatter?.enableToc ?? opts.showByDefault 39 | if (display) { 40 | slugAnchor.reset() 41 | const toc: TocEntry[] = [] 42 | let highestDepth: number = opts.maxDepth 43 | visit(tree, "heading", (node) => { 44 | if (node.depth <= opts.maxDepth) { 45 | const text = toString(node) 46 | highestDepth = Math.min(highestDepth, node.depth) 47 | toc.push({ 48 | depth: node.depth, 49 | text, 50 | slug: slugAnchor.slug(text), 51 | }) 52 | } 53 | }) 54 | 55 | if (toc.length > 0 && toc.length > opts.minEntries) { 56 | file.data.toc = toc.map((entry) => ({ 57 | ...entry, 58 | depth: entry.depth - highestDepth, 59 | })) 60 | file.data.collapseToc = opts.collapseByDefault 61 | } 62 | } 63 | } 64 | }, 65 | ] 66 | }, 67 | } 68 | } 69 | 70 | declare module "vfile" { 71 | interface DataMap { 72 | toc: TocEntry[] 73 | collapseToc: boolean 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /quartz/i18n/locales/fa-IR.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "بدون عنوان", 6 | description: "توضیح خاصی اضافه نشده است", 7 | }, 8 | components: { 9 | callout: { 10 | note: "یادداشت", 11 | abstract: "چکیده", 12 | info: "اطلاعات", 13 | todo: "اقدام", 14 | tip: "نکته", 15 | success: "تیک", 16 | question: "سؤال", 17 | warning: "هشدار", 18 | failure: "شکست", 19 | danger: "خطر", 20 | bug: "باگ", 21 | example: "مثال", 22 | quote: "نقل قول", 23 | }, 24 | backlinks: { 25 | title: "بک‌لینک‌ها", 26 | noBacklinksFound: "بدون بک‌لینک", 27 | }, 28 | themeToggle: { 29 | lightMode: "حالت روشن", 30 | darkMode: "حالت تاریک", 31 | }, 32 | explorer: { 33 | title: "مطالب", 34 | }, 35 | footer: { 36 | createdWith: "ساخته شده با", 37 | }, 38 | graph: { 39 | title: "نمای گراف", 40 | }, 41 | recentNotes: { 42 | title: "یادداشت‌های اخیر", 43 | seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `از ${targetSlug}`, 47 | linkToOriginal: "پیوند به اصلی", 48 | }, 49 | search: { 50 | title: "جستجو", 51 | searchBarPlaceholder: "مطلبی را جستجو کنید", 52 | }, 53 | tableOfContents: { 54 | title: "فهرست", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `زمان تقریبی مطالعه: ${minutes} دقیقه`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "یادداشت‌های اخیر", 63 | lastFewNotes: ({ count }) => `${count} یادداشت اخیر`, 64 | }, 65 | error: { 66 | title: "یافت نشد", 67 | notFound: "این صفحه یا خصوصی است یا وجود ندارد", 68 | home: "بازگشت به صفحه اصلی", 69 | }, 70 | folderContent: { 71 | folder: "پوشه", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`, 74 | }, 75 | tagContent: { 76 | tag: "برچسب", 77 | tagIndex: "فهرست برچسب‌ها", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`, 80 | showingFirst: ({ count }) => `در حال نمایش ${count} برچسب.`, 81 | totalTags: ({ count }) => `${count} برچسب یافت شد.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import { i18n } from "../i18n" 2 | import { FullSlug, joinSegments, pathToRoot } from "../util/path" 3 | import { JSResourceToScriptElement } from "../util/resources" 4 | import { googleFontHref } from "../util/theme" 5 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 6 | 7 | export default (() => { 8 | const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { 9 | const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title 10 | const description = 11 | fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description 12 | const { css, js } = externalResources 13 | 14 | const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) 15 | const path = url.pathname as FullSlug 16 | const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) 17 | 18 | const iconPath = joinSegments(baseDir, "static/icon.png") 19 | const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` 20 | 21 | return ( 22 | 23 | {title} 24 | 25 | {cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && ( 26 | <> 27 | 28 | 29 | 30 | 31 | )} 32 | 33 | 34 | 35 | {cfg.baseUrl && } 36 | 37 | 38 | 39 | 40 | 41 | {css.map((href) => ( 42 | 43 | ))} 44 | {js 45 | .filter((resource) => resource.loadTime === "beforeDOMReady") 46 | .map((res) => JSResourceToScriptElement(res, true))} 47 | 48 | ) 49 | } 50 | 51 | return Head 52 | }) satisfies QuartzComponentConstructor 53 | -------------------------------------------------------------------------------- /quartz/i18n/locales/hu-HU.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Névtelen", 6 | description: "Nincs leírás", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Jegyzet", 11 | abstract: "Abstract", 12 | info: "Információ", 13 | todo: "Tennivaló", 14 | tip: "Tipp", 15 | success: "Siker", 16 | question: "Kérdés", 17 | warning: "Figyelmeztetés", 18 | failure: "Hiba", 19 | danger: "Veszély", 20 | bug: "Bug", 21 | example: "Példa", 22 | quote: "Idézet", 23 | }, 24 | backlinks: { 25 | title: "Visszautalások", 26 | noBacklinksFound: "Nincs visszautalás", 27 | }, 28 | themeToggle: { 29 | lightMode: "Világos mód", 30 | darkMode: "Sötét mód", 31 | }, 32 | explorer: { 33 | title: "Fájlböngésző", 34 | }, 35 | footer: { 36 | createdWith: "Készítve ezzel:", 37 | }, 38 | graph: { 39 | title: "Grafikonnézet", 40 | }, 41 | recentNotes: { 42 | title: "Legutóbbi jegyzetek", 43 | seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`, 47 | linkToOriginal: "Hivatkozás az eredetire", 48 | }, 49 | search: { 50 | title: "Keresés", 51 | searchBarPlaceholder: "Keress valamire", 52 | }, 53 | tableOfContents: { 54 | title: "Tartalomjegyzék", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} perces olvasás`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Legutóbbi jegyzetek", 63 | lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`, 64 | }, 65 | error: { 66 | title: "Nem található", 67 | notFound: "Ez a lap vagy privát vagy nem létezik.", 68 | home: "Vissza a kezdőlapra", 69 | }, 70 | folderContent: { 71 | folder: "Mappa", 72 | itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`, 73 | }, 74 | tagContent: { 75 | tag: "Címke", 76 | tagIndex: "Címke index", 77 | itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`, 78 | showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`, 79 | totalTags: ({ count }) => `Összesen ${count} címke található.`, 80 | }, 81 | }, 82 | } as const satisfies Translation 83 | -------------------------------------------------------------------------------- /quartz/i18n/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Untitled", 6 | description: "No description provided", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Note", 11 | abstract: "Abstract", 12 | info: "Info", 13 | todo: "Todo", 14 | tip: "Tip", 15 | success: "Success", 16 | question: "Question", 17 | warning: "Warning", 18 | failure: "Failure", 19 | danger: "Danger", 20 | bug: "Bug", 21 | example: "Example", 22 | quote: "Quote", 23 | }, 24 | backlinks: { 25 | title: "Backlinks", 26 | noBacklinksFound: "No backlinks found", 27 | }, 28 | themeToggle: { 29 | lightMode: "Light mode", 30 | darkMode: "Dark mode", 31 | }, 32 | explorer: { 33 | title: "Explorer", 34 | }, 35 | footer: { 36 | createdWith: "Created with", 37 | }, 38 | graph: { 39 | title: "Graph View", 40 | }, 41 | recentNotes: { 42 | title: "Recent Notes", 43 | seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, 47 | linkToOriginal: "Link to original", 48 | }, 49 | search: { 50 | title: "Search", 51 | searchBarPlaceholder: "Search for something", 52 | }, 53 | tableOfContents: { 54 | title: "Table of Contents", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Recent notes", 63 | lastFewNotes: ({ count }) => `Last ${count} notes`, 64 | }, 65 | error: { 66 | title: "Not Found", 67 | notFound: "Either this page is private or doesn't exist.", 68 | home: "Return to Homepage", 69 | }, 70 | folderContent: { 71 | folder: "Folder", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 item under this folder." : `${count} items under this folder.`, 74 | }, 75 | tagContent: { 76 | tag: "Tag", 77 | tagIndex: "Tag Index", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 item with this tag." : `${count} items with this tag.`, 80 | showingFirst: ({ count }) => `Showing first ${count} tags.`, 81 | totalTags: ({ count }) => `Found ${count} total tags.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/components/pages/FolderContent.tsx: -------------------------------------------------------------------------------- 1 | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" 2 | import path from "path" 3 | 4 | import style from "../styles/listPage.scss" 5 | import { PageList } from "../PageList" 6 | import { stripSlashes, simplifySlug } from "../../util/path" 7 | import { Root } from "hast" 8 | import { htmlToJsx } from "../../util/jsx" 9 | import { i18n } from "../../i18n" 10 | 11 | interface FolderContentOptions { 12 | /** 13 | * Whether to display number of folders 14 | */ 15 | showFolderCount: boolean 16 | } 17 | 18 | const defaultOptions: FolderContentOptions = { 19 | showFolderCount: true, 20 | } 21 | 22 | export default ((opts?: Partial) => { 23 | const options: FolderContentOptions = { ...defaultOptions, ...opts } 24 | 25 | const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { 26 | const { tree, fileData, allFiles, cfg } = props 27 | const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) 28 | const allPagesInFolder = allFiles.filter((file) => { 29 | const fileSlug = stripSlashes(simplifySlug(file.slug!)) 30 | const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug 31 | const folderParts = folderSlug.split(path.posix.sep) 32 | const fileParts = fileSlug.split(path.posix.sep) 33 | const isDirectChild = fileParts.length === folderParts.length + 1 34 | return prefixed && isDirectChild 35 | }) 36 | const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] 37 | const classes = ["popover-hint", ...cssClasses].join(" ") 38 | const listProps = { 39 | ...props, 40 | allFiles: allPagesInFolder, 41 | } 42 | 43 | const content = 44 | (tree as Root).children.length === 0 45 | ? fileData.description 46 | : htmlToJsx(fileData.filePath!, tree) 47 | 48 | return ( 49 |
50 |
{content}
51 |
52 | {options.showFolderCount && ( 53 |

54 | {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ 55 | count: allPagesInFolder.length, 56 | })} 57 |

58 | )} 59 |
60 | 61 |
62 |
63 |
64 | ) 65 | } 66 | 67 | FolderContent.css = style + PageList.css 68 | return FolderContent 69 | }) satisfies QuartzComponentConstructor 70 | -------------------------------------------------------------------------------- /quartz/i18n/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Sem título", 6 | description: "Sem descrição", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Nota", 11 | abstract: "Abstrato", 12 | info: "Info", 13 | todo: "Pendência", 14 | tip: "Dica", 15 | success: "Sucesso", 16 | question: "Pergunta", 17 | warning: "Aviso", 18 | failure: "Falha", 19 | danger: "Perigo", 20 | bug: "Bug", 21 | example: "Exemplo", 22 | quote: "Citação", 23 | }, 24 | backlinks: { 25 | title: "Backlinks", 26 | noBacklinksFound: "Sem backlinks encontrados", 27 | }, 28 | themeToggle: { 29 | lightMode: "Tema claro", 30 | darkMode: "Tema escuro", 31 | }, 32 | explorer: { 33 | title: "Explorador", 34 | }, 35 | footer: { 36 | createdWith: "Criado com", 37 | }, 38 | graph: { 39 | title: "Visão de gráfico", 40 | }, 41 | recentNotes: { 42 | title: "Notas recentes", 43 | seeRemainingMore: ({ remaining }) => `Veja mais ${remaining} →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`, 47 | linkToOriginal: "Link ao original", 48 | }, 49 | search: { 50 | title: "Pesquisar", 51 | searchBarPlaceholder: "Pesquisar por algo", 52 | }, 53 | tableOfContents: { 54 | title: "Sumário", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `Leitura de ${minutes} min`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Notas recentes", 63 | lastFewNotes: ({ count }) => `Últimas ${count} notas`, 64 | }, 65 | error: { 66 | title: "Não encontrado", 67 | notFound: "Esta página é privada ou não existe.", 68 | home: "Retornar a página inicial", 69 | }, 70 | folderContent: { 71 | folder: "Arquivo", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 item neste arquivo." : `${count} items neste arquivo.`, 74 | }, 75 | tagContent: { 76 | tag: "Tag", 77 | tagIndex: "Sumário de Tags", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`, 80 | showingFirst: ({ count }) => `Mostrando as ${count} primeiras tags.`, 81 | totalTags: ({ count }) => `Encontradas ${count} tags.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/vi-VN.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Không có tiêu đề", 6 | description: "Không có mô tả được cung cấp", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Ghi Chú", 11 | abstract: "Tóm Tắt", 12 | info: "Thông tin", 13 | todo: "Cần Làm", 14 | tip: "Gợi Ý", 15 | success: "Thành Công", 16 | question: "Nghi Vấn", 17 | warning: "Cảnh Báo", 18 | failure: "Thất Bại", 19 | danger: "Nguy Hiểm", 20 | bug: "Lỗi", 21 | example: "Ví Dụ", 22 | quote: "Trích Dẫn", 23 | }, 24 | backlinks: { 25 | title: "Liên Kết Ngược", 26 | noBacklinksFound: "Không có liên kết ngược được tìm thấy", 27 | }, 28 | themeToggle: { 29 | lightMode: "Sáng", 30 | darkMode: "Tối", 31 | }, 32 | explorer: { 33 | title: "Trong bài này", 34 | }, 35 | footer: { 36 | createdWith: "Được tạo bởi", 37 | }, 38 | graph: { 39 | title: "Biểu Đồ", 40 | }, 41 | recentNotes: { 42 | title: "Bài viết gần đây", 43 | seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`, 47 | linkToOriginal: "Liên Kết Gốc", 48 | }, 49 | search: { 50 | title: "Tìm Kiếm", 51 | searchBarPlaceholder: "Tìm kiếm thông tin", 52 | }, 53 | tableOfContents: { 54 | title: "Bảng Nội Dung", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `đọc ${minutes} phút`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Những bài gần đây", 63 | lastFewNotes: ({ count }) => `${count} Bài gần đây`, 64 | }, 65 | error: { 66 | title: "Không Tìm Thấy", 67 | notFound: "Trang này được bảo mật hoặc không tồn tại.", 68 | home: "Trở về trang chủ", 69 | }, 70 | folderContent: { 71 | folder: "Thư Mục", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`, 74 | }, 75 | tagContent: { 76 | tag: "Thẻ", 77 | tagIndex: "Thẻ Mục Lục", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`, 80 | showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`, 81 | totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/uk-UA.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Без назви", 6 | description: "Опис не надано", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Примітка", 11 | abstract: "Абстракт", 12 | info: "Інформація", 13 | todo: "Завдання", 14 | tip: "Порада", 15 | success: "Успіх", 16 | question: "Питання", 17 | warning: "Попередження", 18 | failure: "Невдача", 19 | danger: "Небезпека", 20 | bug: "Баг", 21 | example: "Приклад", 22 | quote: "Цитата", 23 | }, 24 | backlinks: { 25 | title: "Зворотні посилання", 26 | noBacklinksFound: "Зворотних посилань не знайдено", 27 | }, 28 | themeToggle: { 29 | lightMode: "Світлий режим", 30 | darkMode: "Темний режим", 31 | }, 32 | explorer: { 33 | title: "Провідник", 34 | }, 35 | footer: { 36 | createdWith: "Створено за допомогою", 37 | }, 38 | graph: { 39 | title: "Вигляд графа", 40 | }, 41 | recentNotes: { 42 | title: "Останні нотатки", 43 | seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining} →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`, 47 | linkToOriginal: "Посилання на оригінал", 48 | }, 49 | search: { 50 | title: "Пошук", 51 | searchBarPlaceholder: "Шукати щось", 52 | }, 53 | tableOfContents: { 54 | title: "Зміст", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Останні нотатки", 63 | lastFewNotes: ({ count }) => `Останні нотатки: ${count}`, 64 | }, 65 | error: { 66 | title: "Не знайдено", 67 | notFound: "Ця сторінка або приватна, або не існує.", 68 | home: "Повернутися на головну сторінку", 69 | }, 70 | folderContent: { 71 | folder: "Папка", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "У цій папці 1 елемент." : `Елементів у цій папці: ${count}.`, 74 | }, 75 | tagContent: { 76 | tag: "Тег", 77 | tagIndex: "Індекс тегу", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 елемент з цим тегом." : `Елементів з цим тегом: ${count}.`, 80 | showingFirst: ({ count }) => `Показ перших ${count} тегів.`, 81 | totalTags: ({ count }) => `Всього знайдено тегів: ${count}.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/it-IT.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Senza titolo", 6 | description: "Nessuna descrizione", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Nota", 11 | abstract: "Astratto", 12 | info: "Info", 13 | todo: "Da fare", 14 | tip: "Consiglio", 15 | success: "Completato", 16 | question: "Domanda", 17 | warning: "Attenzione", 18 | failure: "Errore", 19 | danger: "Pericolo", 20 | bug: "Bug", 21 | example: "Esempio", 22 | quote: "Citazione", 23 | }, 24 | backlinks: { 25 | title: "Link entranti", 26 | noBacklinksFound: "Nessun link entrante", 27 | }, 28 | themeToggle: { 29 | lightMode: "Tema chiaro", 30 | darkMode: "Tema scuro", 31 | }, 32 | explorer: { 33 | title: "Esplora", 34 | }, 35 | footer: { 36 | createdWith: "Creato con", 37 | }, 38 | graph: { 39 | title: "Vista grafico", 40 | }, 41 | recentNotes: { 42 | title: "Note recenti", 43 | seeRemainingMore: ({ remaining }) => `Vedi ${remaining} altro →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transclusione di ${targetSlug}`, 47 | linkToOriginal: "Link all'originale", 48 | }, 49 | search: { 50 | title: "Cerca", 51 | searchBarPlaceholder: "Cerca qualcosa", 52 | }, 53 | tableOfContents: { 54 | title: "Tabella dei contenuti", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} minuti`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Note recenti", 63 | lastFewNotes: ({ count }) => `Ultime ${count} note`, 64 | }, 65 | error: { 66 | title: "Non trovato", 67 | notFound: "Questa pagina è privata o non esiste.", 68 | home: "Ritorna alla home page", 69 | }, 70 | folderContent: { 71 | folder: "Cartella", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 oggetto in questa cartella." : `${count} oggetti in questa cartella.`, 74 | }, 75 | tagContent: { 76 | tag: "Etichetta", 77 | tagIndex: "Indice etichette", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`, 80 | showingFirst: ({ count }) => `Prime ${count} etichette.`, 81 | totalTags: ({ count }) => `Trovate ${count} etichette totali.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /docs/features/callouts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Callouts 3 | tags: 4 | - feature/transformer 5 | --- 6 | 7 | Quartz supports the same Admonition-callout syntax as Obsidian. 8 | 9 | This includes 10 | 11 | - 12 Distinct callout types (each with several aliases) 12 | - Collapsable callouts 13 | 14 | ``` 15 | > [!info] Title 16 | > This is a callout! 17 | ``` 18 | 19 | See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts). 20 | 21 | > [!warning] 22 | > Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]]. 23 | 24 | ## Customization 25 | 26 | The callouts are a functionality of the [[ObsidianFlavoredMarkdown]] plugin. See the plugin page for how to enable or disable them. 27 | 28 | You can edit the icons by customizing `quartz/styles/callouts.scss`. 29 | 30 | ### Add custom callouts 31 | 32 | By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`. 33 | 34 | ```scss title="quartz/styles/custom.scss" 35 | .callout { 36 | &[data-callout="custom"] { 37 | --color: #customcolor; 38 | --border: #custombordercolor; 39 | --bg: #custombg; 40 | --callout-icon: url("data:image/svg+xml; utf8, "); //SVG icon code 41 | } 42 | } 43 | ``` 44 | 45 | > [!warning] 46 | > Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that. 47 | 48 | ## Showcase 49 | 50 | > [!info] 51 | > Default title 52 | 53 | > [!question]+ Can callouts be _nested_? 54 | > 55 | > > [!todo]- Yes!, they can. And collapsed! 56 | > > 57 | > > > [!example] You can even use multiple layers of nesting. 58 | 59 | > [!note] 60 | > Aliases: "note" 61 | 62 | > [!abstract] 63 | > Aliases: "abstract", "summary", "tldr" 64 | 65 | > [!info] 66 | > Aliases: "info" 67 | 68 | > [!todo] 69 | > Aliases: "todo" 70 | 71 | > [!tip] 72 | > Aliases: "tip", "hint", "important" 73 | 74 | > [!success] 75 | > Aliases: "success", "check", "done" 76 | 77 | > [!question] 78 | > Aliases: "question", "help", "faq" 79 | 80 | > [!warning] 81 | > Aliases: "warning", "attention", "caution" 82 | 83 | > [!failure] 84 | > Aliases: "failure", "missing", "fail" 85 | 86 | > [!danger] 87 | > Aliases: "danger", "error" 88 | 89 | > [!bug] 90 | > Aliases: "bug" 91 | 92 | > [!example] 93 | > Aliases: "example" 94 | 95 | > [!quote] 96 | > Aliases: "quote", "cite" 97 | -------------------------------------------------------------------------------- /quartz/i18n/locales/de-DE.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Unbenannt", 6 | description: "Keine Beschreibung angegeben", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Hinweis", 11 | abstract: "Zusammenfassung", 12 | info: "Info", 13 | todo: "Zu erledigen", 14 | tip: "Tipp", 15 | success: "Erfolg", 16 | question: "Frage", 17 | warning: "Warnung", 18 | failure: "Misserfolg", 19 | danger: "Gefahr", 20 | bug: "Fehler", 21 | example: "Beispiel", 22 | quote: "Zitat", 23 | }, 24 | backlinks: { 25 | title: "Backlinks", 26 | noBacklinksFound: "Keine Backlinks gefunden", 27 | }, 28 | themeToggle: { 29 | lightMode: "Light Mode", 30 | darkMode: "Dark Mode", 31 | }, 32 | explorer: { 33 | title: "Explorer", 34 | }, 35 | footer: { 36 | createdWith: "Erstellt mit", 37 | }, 38 | graph: { 39 | title: "Graphansicht", 40 | }, 41 | recentNotes: { 42 | title: "Zuletzt bearbeitete Seiten", 43 | seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`, 47 | linkToOriginal: "Link zum Original", 48 | }, 49 | search: { 50 | title: "Suche", 51 | searchBarPlaceholder: "Suche nach etwas", 52 | }, 53 | tableOfContents: { 54 | title: "Inhaltsverzeichnis", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Zuletzt bearbeitete Seiten", 63 | lastFewNotes: ({ count }) => `Letzte ${count} Seiten`, 64 | }, 65 | error: { 66 | title: "Nicht gefunden", 67 | notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.", 68 | home: "Return to Homepage", 69 | }, 70 | folderContent: { 71 | folder: "Ordner", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 Datei in diesem Ordner." : `${count} Dateien in diesem Ordner.`, 74 | }, 75 | tagContent: { 76 | tag: "Tag", 77 | tagIndex: "Tag-Übersicht", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`, 80 | showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`, 81 | totalTags: ({ count }) => `${count} Tags insgesamt.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/plugins/transformers/gfm.ts: -------------------------------------------------------------------------------- 1 | import remarkGfm from "remark-gfm" 2 | import smartypants from "remark-smartypants" 3 | import { QuartzTransformerPlugin } from "../types" 4 | import rehypeSlug from "rehype-slug" 5 | import rehypeAutolinkHeadings from "rehype-autolink-headings" 6 | 7 | export interface Options { 8 | enableSmartyPants: boolean 9 | linkHeadings: boolean 10 | } 11 | 12 | const defaultOptions: Options = { 13 | enableSmartyPants: true, 14 | linkHeadings: true, 15 | } 16 | 17 | export const GitHubFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( 18 | userOpts, 19 | ) => { 20 | const opts = { ...defaultOptions, ...userOpts } 21 | return { 22 | name: "GitHubFlavoredMarkdown", 23 | markdownPlugins() { 24 | return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm] 25 | }, 26 | htmlPlugins() { 27 | if (opts.linkHeadings) { 28 | return [ 29 | rehypeSlug, 30 | [ 31 | rehypeAutolinkHeadings, 32 | { 33 | behavior: "append", 34 | properties: { 35 | role: "anchor", 36 | ariaHidden: true, 37 | tabIndex: -1, 38 | "data-no-popover": true, 39 | }, 40 | content: { 41 | type: "element", 42 | tagName: "svg", 43 | properties: { 44 | width: 18, 45 | height: 18, 46 | viewBox: "0 0 24 24", 47 | fill: "none", 48 | stroke: "currentColor", 49 | "stroke-width": "2", 50 | "stroke-linecap": "round", 51 | "stroke-linejoin": "round", 52 | }, 53 | children: [ 54 | { 55 | type: "element", 56 | tagName: "path", 57 | properties: { 58 | d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", 59 | }, 60 | children: [], 61 | }, 62 | { 63 | type: "element", 64 | tagName: "path", 65 | properties: { 66 | d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", 67 | }, 68 | children: [], 69 | }, 70 | ], 71 | }, 72 | }, 73 | ], 74 | ] 75 | } else { 76 | return [] 77 | } 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /quartz/i18n/locales/es-ES.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Sin título", 6 | description: "Sin descripción", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Nota", 11 | abstract: "Resumen", 12 | info: "Información", 13 | todo: "Por hacer", 14 | tip: "Consejo", 15 | success: "Éxito", 16 | question: "Pregunta", 17 | warning: "Advertencia", 18 | failure: "Fallo", 19 | danger: "Peligro", 20 | bug: "Error", 21 | example: "Ejemplo", 22 | quote: "Cita", 23 | }, 24 | backlinks: { 25 | title: "Enlaces de Retroceso", 26 | noBacklinksFound: "No se han encontrado enlaces traseros", 27 | }, 28 | themeToggle: { 29 | lightMode: "Modo claro", 30 | darkMode: "Modo oscuro", 31 | }, 32 | explorer: { 33 | title: "Explorador", 34 | }, 35 | footer: { 36 | createdWith: "Creado con", 37 | }, 38 | graph: { 39 | title: "Vista Gráfica", 40 | }, 41 | recentNotes: { 42 | title: "Notas Recientes", 43 | seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`, 47 | linkToOriginal: "Enlace al original", 48 | }, 49 | search: { 50 | title: "Buscar", 51 | searchBarPlaceholder: "Busca algo", 52 | }, 53 | tableOfContents: { 54 | title: "Tabla de Contenidos", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min read`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Notas recientes", 63 | lastFewNotes: ({ count }) => `Últimás ${count} notas`, 64 | }, 65 | error: { 66 | title: "No se encontró.", 67 | notFound: "Esta página es privada o no existe.", 68 | home: "Regresar a la página principal", 69 | }, 70 | folderContent: { 71 | folder: "Carpeta", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 artículo en esta carpeta." : `${count} artículos en esta carpeta.`, 74 | }, 75 | tagContent: { 76 | tag: "Etiqueta", 77 | tagIndex: "Índice de Etiquetas", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`, 80 | showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`, 81 | totalTags: ({ count }) => `Se encontraron ${count} etiquetas en total.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/pl-PL.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Bez nazwy", 6 | description: "Brak opisu", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Notatka", 11 | abstract: "Streszczenie", 12 | info: "informacja", 13 | todo: "Do zrobienia", 14 | tip: "Wskazówka", 15 | success: "Zrobione", 16 | question: "Pytanie", 17 | warning: "Ostrzeżenie", 18 | failure: "Usterka", 19 | danger: "Niebiezpieczeństwo", 20 | bug: "Błąd w kodzie", 21 | example: "Przykład", 22 | quote: "Cytat", 23 | }, 24 | backlinks: { 25 | title: "Odnośniki zwrotne", 26 | noBacklinksFound: "Brak połączeń zwrotnych", 27 | }, 28 | themeToggle: { 29 | lightMode: "Trzyb jasny", 30 | darkMode: "Tryb ciemny", 31 | }, 32 | explorer: { 33 | title: "Przeglądaj", 34 | }, 35 | footer: { 36 | createdWith: "Stworzone z użyciem", 37 | }, 38 | graph: { 39 | title: "Graf", 40 | }, 41 | recentNotes: { 42 | title: "Najnowsze notatki", 43 | seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`, 47 | linkToOriginal: "Łącze do oryginału", 48 | }, 49 | search: { 50 | title: "Szukaj", 51 | searchBarPlaceholder: "Search for something", 52 | }, 53 | tableOfContents: { 54 | title: "Spis treści", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min. czytania `, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Najnowsze notatki", 63 | lastFewNotes: ({ count }) => `Ostatnie ${count} notatek`, 64 | }, 65 | error: { 66 | title: "Nie znaleziono", 67 | notFound: "Ta strona jest prywatna lub nie istnieje.", 68 | home: "Powrót do strony głównej", 69 | }, 70 | folderContent: { 71 | folder: "Folder", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "W tym folderze jest 1 element." : `Elementów w folderze: ${count}.`, 74 | }, 75 | tagContent: { 76 | tag: "Znacznik", 77 | tagIndex: "Spis znaczników", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`, 80 | showingFirst: ({ count }) => `Pokazuje ${count} pierwszych znaczników.`, 81 | totalTags: ({ count }) => `Znalezionych wszystkich znaczników: ${count}.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "Sans titre", 6 | description: "Aucune description fournie", 7 | }, 8 | components: { 9 | callout: { 10 | note: "Note", 11 | abstract: "Résumé", 12 | info: "Info", 13 | todo: "À faire", 14 | tip: "Conseil", 15 | success: "Succès", 16 | question: "Question", 17 | warning: "Avertissement", 18 | failure: "Échec", 19 | danger: "Danger", 20 | bug: "Bogue", 21 | example: "Exemple", 22 | quote: "Citation", 23 | }, 24 | backlinks: { 25 | title: "Liens retour", 26 | noBacklinksFound: "Aucun lien retour trouvé", 27 | }, 28 | themeToggle: { 29 | lightMode: "Mode clair", 30 | darkMode: "Mode sombre", 31 | }, 32 | explorer: { 33 | title: "Explorateur", 34 | }, 35 | footer: { 36 | createdWith: "Créé avec", 37 | }, 38 | graph: { 39 | title: "Vue Graphique", 40 | }, 41 | recentNotes: { 42 | title: "Notes Récentes", 43 | seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`, 47 | linkToOriginal: "Lien vers l'original", 48 | }, 49 | search: { 50 | title: "Recherche", 51 | searchBarPlaceholder: "Rechercher quelque chose", 52 | }, 53 | tableOfContents: { 54 | title: "Table des Matières", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => `${minutes} min de lecture`, 58 | }, 59 | }, 60 | pages: { 61 | rss: { 62 | recentNotes: "Notes récentes", 63 | lastFewNotes: ({ count }) => `Les dernières ${count} notes`, 64 | }, 65 | error: { 66 | title: "Introuvable", 67 | notFound: "Cette page est soit privée, soit elle n'existe pas.", 68 | home: "Retour à la page d'accueil", 69 | }, 70 | folderContent: { 71 | folder: "Dossier", 72 | itemsUnderFolder: ({ count }) => 73 | count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`, 74 | }, 75 | tagContent: { 76 | tag: "Étiquette", 77 | tagIndex: "Index des étiquettes", 78 | itemsUnderTag: ({ count }) => 79 | count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`, 80 | showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, 81 | totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, 82 | }, 83 | }, 84 | } as const satisfies Translation 85 | -------------------------------------------------------------------------------- /quartz/i18n/locales/ar-SA.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from "./definition" 2 | 3 | export default { 4 | propertyDefaults: { 5 | title: "غير معنون", 6 | description: "لم يتم تقديم أي وصف", 7 | }, 8 | components: { 9 | callout: { 10 | note: "ملاحظة", 11 | abstract: "ملخص", 12 | info: "معلومات", 13 | todo: "للقيام", 14 | tip: "نصيحة", 15 | success: "نجاح", 16 | question: "سؤال", 17 | warning: "تحذير", 18 | failure: "فشل", 19 | danger: "خطر", 20 | bug: "خلل", 21 | example: "مثال", 22 | quote: "اقتباس", 23 | }, 24 | backlinks: { 25 | title: "وصلات العودة", 26 | noBacklinksFound: "لا يوجد وصلات عودة", 27 | }, 28 | themeToggle: { 29 | lightMode: "الوضع النهاري", 30 | darkMode: "الوضع الليلي", 31 | }, 32 | explorer: { 33 | title: "المستعرض", 34 | }, 35 | footer: { 36 | createdWith: "أُنشئ باستخدام", 37 | }, 38 | graph: { 39 | title: "التمثيل التفاعلي", 40 | }, 41 | recentNotes: { 42 | title: "آخر الملاحظات", 43 | seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`, 44 | }, 45 | transcludes: { 46 | transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`, 47 | linkToOriginal: "وصلة للملاحظة الرئيسة", 48 | }, 49 | search: { 50 | title: "بحث", 51 | searchBarPlaceholder: "ابحث عن شيء ما", 52 | }, 53 | tableOfContents: { 54 | title: "فهرس المحتويات", 55 | }, 56 | contentMeta: { 57 | readingTime: ({ minutes }) => 58 | minutes == 1 59 | ? `دقيقة أو أقل للقراءة` 60 | : minutes == 2 61 | ? `دقيقتان للقراءة` 62 | : `${minutes} دقائق للقراءة`, 63 | }, 64 | }, 65 | pages: { 66 | rss: { 67 | recentNotes: "آخر الملاحظات", 68 | lastFewNotes: ({ count }) => `آخر ${count} ملاحظة`, 69 | }, 70 | error: { 71 | title: "غير موجود", 72 | notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.", 73 | home: "العوده للصفحة الرئيسية", 74 | }, 75 | folderContent: { 76 | folder: "مجلد", 77 | itemsUnderFolder: ({ count }) => 78 | count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`, 79 | }, 80 | tagContent: { 81 | tag: "الوسم", 82 | tagIndex: "مؤشر الوسم", 83 | itemsUnderTag: ({ count }) => 84 | count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`, 85 | showingFirst: ({ count }) => `إظهار أول ${count} أوسمة.`, 86 | totalTags: ({ count }) => `يوجد ${count} أوسمة.`, 87 | }, 88 | }, 89 | } as const satisfies Translation 90 | --------------------------------------------------------------------------------