chore(i18n): refactor and cleanup (#805)
* checkpoint
* finish
* docs
3 files deleted
5 files added
29 files modified
| | |
| | | allFiles, |
| | | } |
| | | |
| | | const content = renderPage(slug, componentData, opts, externalResources) |
| | | const content = renderPage(cfg, slug, componentData, opts, externalResources) |
| | | const fp = await emit({ |
| | | content, |
| | | slug: file.data.slug!, |
| | |
| | | - `null`: don't use analytics; |
| | | - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or |
| | | - `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics |
| | | - `locale`: used for [[i18n]] and date formatting |
| | | - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. |
| | | - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz` |
| | | - Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it. |
| New file |
| | |
| | | --- |
| | | title: Internationalization |
| | | --- |
| | | |
| | | 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`. |
| | | |
| | | The locale field generally follows a certain format: `{language}-{REGION}` |
| | | |
| | | - `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). |
| | | - `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) |
| | | |
| | | > [!tip] Interested in contributing? |
| | | > We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: |
| | | > |
| | | > 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. |
| | | > 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. |
| | | > 3. Fill in the translations! |
| | | > 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. |
| | |
| | | |
| | | ## 🔧 Features |
| | | |
| | | - [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box |
| | | - [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box |
| | | - Hot-reload for both configuration and content |
| | | - Simple JSX layouts and [[creating components|page components]] |
| | | - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes |
| | |
| | | import { ValidDateType } from "./components/Date" |
| | | import { QuartzComponent } from "./components/types" |
| | | import { ValidLocale } from "./i18n" |
| | | import { PluginTypes } from "./plugins/types" |
| | | import { Theme } from "./util/theme" |
| | | |
| | |
| | | /** |
| | | * Allow to translate the date in the language of your choice. |
| | | * Also used for UI translation (default: en-US) |
| | | * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) |
| | | * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag |
| | | * The first part is the language (en) and the second part is the script/region (US) |
| | | * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes |
| | | * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 |
| | | */ |
| | | locale?: string |
| | | locale: ValidLocale |
| | | } |
| | | |
| | | export interface QuartzConfig { |
| | |
| | | return null |
| | | } |
| | | } |
| | | |
| | | ArticleTitle.css = ` |
| | | .article-title { |
| | | margin: 2rem 0 0 0; |
| | |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | import style from "./styles/backlinks.scss" |
| | | import { resolveRelative, simplifySlug } from "../util/path" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | import { classNames } from "../util/lang" |
| | | |
| | | function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) { |
| | |
| | | const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) |
| | | return ( |
| | | <div class={classNames(displayClass, "backlinks")}> |
| | | <h3>{i18n(cfg.locale, "backlinks.backlinks")}</h3> |
| | | <h3>{i18n(cfg.locale).components.backlinks.title}</h3> |
| | | <ul class="overflow"> |
| | | {backlinkFiles.length > 0 ? ( |
| | | backlinkFiles.map((f) => ( |
| | |
| | | </li> |
| | | )) |
| | | ) : ( |
| | | <li>{i18n(cfg.locale, "backlinks.noBacklinksFound")}</li> |
| | | <li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li> |
| | | )} |
| | | </ul> |
| | | </div> |
| | |
| | | import darkmodeScript from "./scripts/darkmode.inline" |
| | | import styles from "./styles/darkmode.scss" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | import { classNames } from "../util/lang" |
| | | |
| | | function Darkmode({ displayClass, cfg }: QuartzComponentProps) { |
| | |
| | | style="enable-background:new 0 0 35 35" |
| | | xmlSpace="preserve" |
| | | > |
| | | <title>{i18n(cfg.locale, "darkmode.lightMode")}</title> |
| | | <title>{i18n(cfg.locale).components.themeToggle.darkMode}</title> |
| | | <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path> |
| | | </svg> |
| | | </label> |
| | |
| | | style="enable-background:new 0 0 100 100" |
| | | xmlSpace="preserve" |
| | | > |
| | | <title>{i18n(cfg.locale, "darkmode.lightMode")}</title> |
| | | <title>{i18n(cfg.locale).components.themeToggle.lightMode}</title> |
| | | <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path> |
| | | </svg> |
| | | </label> |
| | |
| | | import { GlobalConfiguration } from "../cfg" |
| | | import { ValidLocale } from "../i18n" |
| | | import { QuartzPluginData } from "../plugins/vfile" |
| | | |
| | | interface Props { |
| | | date: Date |
| | | locale?: string |
| | | locale?: ValidLocale |
| | | } |
| | | |
| | | export type ValidDateType = keyof Required<QuartzPluginData>["dates"] |
| | |
| | | return data.dates?.[cfg.defaultDateType] |
| | | } |
| | | |
| | | export function formatDate(d: Date, locale = "en-US"): string { |
| | | export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { |
| | | return d.toLocaleDateString(locale, { |
| | | year: "numeric", |
| | | month: "short", |
| | |
| | | import { ExplorerNode, FileNode, Options } from "./ExplorerNode" |
| | | import { QuartzPluginData } from "../plugins/vfile" |
| | | import { classNames } from "../util/lang" |
| | | import { i18n } from "../i18n" |
| | | |
| | | // Options interface defined in `ExplorerNode` to avoid circular dependency |
| | | const defaultOptions = { |
| | | title: "Explorer", |
| | | folderClickBehavior: "collapse", |
| | | folderDefaultState: "collapsed", |
| | | useSavedState: true, |
| | |
| | | jsonTree = JSON.stringify(folders) |
| | | } |
| | | |
| | | function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { |
| | | function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) { |
| | | constructFileTree(allFiles) |
| | | return ( |
| | | <div class={classNames(displayClass, "explorer")}> |
| | |
| | | data-savestate={opts.useSavedState} |
| | | data-tree={jsonTree} |
| | | > |
| | | <h1>{opts.title}</h1> |
| | | <h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1> |
| | | <svg |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | width="14" |
| | |
| | | type OrderEntries = "sort" | "filter" | "map" |
| | | |
| | | export interface Options { |
| | | title: string |
| | | title?: string |
| | | folderDefaultState: "collapsed" | "open" |
| | | folderClickBehavior: "collapse" | "link" |
| | | useSavedState: boolean |
| | |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | import style from "./styles/footer.scss" |
| | | import { version } from "../../package.json" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | |
| | | interface Options { |
| | | links: Record<string, string> |
| | |
| | | <footer class={`${displayClass ?? ""}`}> |
| | | <hr /> |
| | | <p> |
| | | {i18n(cfg.locale, "footer.createdWith")}{" "} |
| | | <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year} |
| | | {i18n(cfg.locale).components.footer.createdWith}{" "} |
| | | <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year} |
| | | </p> |
| | | <ul> |
| | | {Object.entries(links).map(([text, link]) => ( |
| | |
| | | // @ts-ignore |
| | | import script from "./scripts/graph.inline" |
| | | import style from "./styles/graph.scss" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | import { classNames } from "../util/lang" |
| | | |
| | | export interface D3Config { |
| | |
| | | const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } |
| | | return ( |
| | | <div class={classNames(displayClass, "graph")}> |
| | | <h3>{i18n(cfg.locale, "graph.graphView")}</h3> |
| | | <h3>{i18n(cfg.locale).components.graph.title}</h3> |
| | | <div class="graph-outer"> |
| | | <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> |
| | | <svg |
| | |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path" |
| | | import { JSResourceToScriptElement } from "../util/resources" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | export default (() => { |
| | | function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { |
| | | const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled") |
| | | const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title |
| | | const description = |
| | | fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided") |
| | | fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description |
| | | const { css, js } = externalResources |
| | | |
| | | const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) |
| | |
| | | import { pathToRoot } from "../util/path" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | import { classNames } from "../util/lang" |
| | | import { i18n } from "../i18n" |
| | | |
| | | function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { |
| | | const title = cfg?.pageTitle ?? "Untitled Quartz" |
| | | const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title |
| | | const baseDir = pathToRoot(fileData.slug!) |
| | | return ( |
| | | <h1 class={classNames(displayClass, "page-title")}> |
| | |
| | | import style from "./styles/recentNotes.scss" |
| | | import { Date, getDate } from "./Date" |
| | | import { GlobalConfiguration } from "../cfg" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | import { classNames } from "../util/lang" |
| | | |
| | | interface Options { |
| | | title: string |
| | | title?: string |
| | | limit: number |
| | | linkToMore: SimpleSlug | false |
| | | filter: (f: QuartzPluginData) => boolean |
| | |
| | | } |
| | | |
| | | const defaultOptions = (cfg: GlobalConfiguration): Options => ({ |
| | | title: "Recent Notes", |
| | | limit: 3, |
| | | linkToMore: false, |
| | | filter: () => true, |
| | |
| | | const remaining = Math.max(0, pages.length - opts.limit) |
| | | return ( |
| | | <div class={classNames(displayClass, "recent-notes")}> |
| | | <h3>{opts.title}</h3> |
| | | <h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3> |
| | | <ul class="recent-ul"> |
| | | {pages.slice(0, opts.limit).map((page) => { |
| | | const title = page.frontmatter?.title |
| | | const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title |
| | | const tags = page.frontmatter?.tags ?? [] |
| | | |
| | | return ( |
| | |
| | | {opts.linkToMore && remaining > 0 && ( |
| | | <p> |
| | | <a href={resolveRelative(fileData.slug!, opts.linkToMore)}> |
| | | {" "} |
| | | {i18n(cfg.locale, "recentNotes.seeRemainingMore", { |
| | | remaining: remaining.toString(), |
| | | })}{" "} |
| | | → |
| | | {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })} |
| | | </a> |
| | | </p> |
| | | )} |
| | |
| | | // @ts-ignore |
| | | import script from "./scripts/search.inline" |
| | | import { classNames } from "../util/lang" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | |
| | | export interface SearchOptions { |
| | | enablePreview: boolean |
| | |
| | | export default ((userOpts?: Partial<SearchOptions>) => { |
| | | function Search({ displayClass, cfg }: QuartzComponentProps) { |
| | | const opts = { ...defaultOptions, ...userOpts } |
| | | |
| | | const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder |
| | | return ( |
| | | <div class={classNames(displayClass, "search")}> |
| | | <div id="search-icon"> |
| | | <p>{i18n(cfg.locale, "search")}</p> |
| | | <p>{i18n(cfg.locale).components.search.title}</p> |
| | | <div></div> |
| | | <svg |
| | | tabIndex={0} |
| | |
| | | id="search-bar" |
| | | name="search" |
| | | type="text" |
| | | aria-label="Search for something" |
| | | placeholder="Search for something" |
| | | aria-label={searchPlaceholder} |
| | | placeholder={searchPlaceholder} |
| | | /> |
| | | <div id="search-layout" data-preview={opts.enablePreview}></div> |
| | | </div> |
| | |
| | | |
| | | // @ts-ignore |
| | | import script from "./scripts/toc.inline" |
| | | import { i18n } from "../i18n/i18next" |
| | | import { i18n } from "../i18n" |
| | | |
| | | interface Options { |
| | | layout: "modern" | "legacy" |
| | |
| | | return ( |
| | | <div class={classNames(displayClass, "toc")}> |
| | | <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> |
| | | <h3>{i18n(cfg.locale, "tableOfContent")}</h3> |
| | | <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> |
| | | <svg |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | width="24" |
| | |
| | | return ( |
| | | <details id="toc" open={!fileData.collapseToc}> |
| | | <summary> |
| | | <h3>{i18n(cfg.locale, "tableOfContent")}</h3> |
| | | <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> |
| | | </summary> |
| | | <ul> |
| | | {fileData.toc.map((tocEntry) => ( |
| | |
| | | import { i18n } from "../../i18n/i18next" |
| | | import { i18n } from "../../i18n" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "../types" |
| | | |
| | | function NotFound({ cfg }: QuartzComponentProps) { |
| | | return ( |
| | | <article class="popover-hint"> |
| | | <h1>404</h1> |
| | | <p>{i18n(cfg.locale, "404")}</p> |
| | | <p>{i18n(cfg.locale).pages.error.notFound}</p> |
| | | </article> |
| | | ) |
| | | } |
| | |
| | | import { PageList } from "../PageList" |
| | | import { _stripSlashes, simplifySlug } from "../../util/path" |
| | | import { Root } from "hast" |
| | | import { pluralize } from "../../util/lang" |
| | | import { htmlToJsx } from "../../util/jsx" |
| | | import { i18n } from "../../i18n/i18next" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | interface FolderContentOptions { |
| | | /** |
| | |
| | | <div class="page-listing"> |
| | | {options.showFolderCount && ( |
| | | <p> |
| | | {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "} |
| | | {i18n(cfg.locale, "folderContent.underThisFolder")}. |
| | | {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ |
| | | count: allPagesInFolder.length, |
| | | })} |
| | | </p> |
| | | )} |
| | | <div> |
| | |
| | | import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" |
| | | import { QuartzPluginData } from "../../plugins/vfile" |
| | | import { Root } from "hast" |
| | | import { pluralize } from "../../util/lang" |
| | | import { htmlToJsx } from "../../util/jsx" |
| | | import { i18n } from "../../i18n/i18next" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | const numPages = 10 |
| | | function TagContent(props: QuartzComponentProps) { |
| | |
| | | <article> |
| | | <p>{content}</p> |
| | | </article> |
| | | <p> |
| | | {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "} |
| | | {i18n(cfg.locale, "tagContent.totalTags")}. |
| | | </p> |
| | | <p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p> |
| | | <div> |
| | | {tags.map((tag) => { |
| | | const pages = tagItemMap.get(tag)! |
| | |
| | | {content && <p>{content}</p>} |
| | | <div class="page-listing"> |
| | | <p> |
| | | {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} |
| | | {i18n(cfg.locale, "tagContent.withThisTag")}.{" "} |
| | | {pages.length > numPages && |
| | | `${i18n(cfg.locale, "tagContent.showingFirst")} ${numPages}.`} |
| | | {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} |
| | | {pages.length > numPages && ( |
| | | <span> |
| | | {i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })} |
| | | </span> |
| | | )} |
| | | </p> |
| | | <PageList limit={numPages} {...listProps} /> |
| | | </div> |
| | |
| | | <div class={classes}> |
| | | <article>{content}</article> |
| | | <div class="page-listing"> |
| | | <p> |
| | | {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} |
| | | {i18n(cfg.locale, "tagContent.withThisTag")}. |
| | | </p> |
| | | <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p> |
| | | <div> |
| | | <PageList {...listProps} /> |
| | | </div> |
| | |
| | | import { visit } from "unist-util-visit" |
| | | import { Root, Element, ElementContent } from "hast" |
| | | import { QuartzPluginData } from "../plugins/vfile" |
| | | import { GlobalConfiguration } from "../cfg" |
| | | import { i18n } from "../i18n" |
| | | |
| | | interface RenderComponents { |
| | | head: QuartzComponent |
| | |
| | | } |
| | | |
| | | export function renderPage( |
| | | cfg: GlobalConfiguration, |
| | | slug: FullSlug, |
| | | componentData: QuartzComponentProps, |
| | | components: RenderComponents, |
| | |
| | | type: "element", |
| | | tagName: "a", |
| | | properties: { href: inner.properties?.href, class: ["internal"] }, |
| | | children: [{ type: "text", value: `Link to original` }], |
| | | children: [ |
| | | { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, |
| | | ], |
| | | }, |
| | | ] |
| | | } else if (page.htmlAst) { |
| | |
| | | tagName: "h1", |
| | | properties: {}, |
| | | children: [ |
| | | { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, |
| | | { |
| | | type: "text", |
| | | value: |
| | | page.frontmatter?.title ?? |
| | | i18n(cfg.locale).components.transcludes.transcludeOf({ |
| | | targetSlug: page.slug!, |
| | | }), |
| | | }, |
| | | ], |
| | | }, |
| | | ...(page.htmlAst.children as ElementContent[]).map((child) => |
| | |
| | | type: "element", |
| | | tagName: "a", |
| | | properties: { href: inner.properties?.href, class: ["internal"] }, |
| | | children: [{ type: "text", value: `Link to original` }], |
| | | children: [ |
| | | { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, |
| | | ], |
| | | }, |
| | | ] |
| | | } |
| New file |
| | |
| | | import { Translation } from "./locales/definition" |
| | | import en from "./locales/en-US" |
| | | import fr from "./locales/fr-FR" |
| | | |
| | | export const TRANSLATIONS = { |
| | | "en-US": en, |
| | | "fr-FR": fr, |
| | | } as const |
| | | |
| | | export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale] |
| | | export type ValidLocale = keyof typeof TRANSLATIONS |
| New file |
| | |
| | | import { FullSlug } from "../../util/path" |
| | | |
| | | export interface Translation { |
| | | propertyDefaults: { |
| | | title: string |
| | | description: string |
| | | } |
| | | components: { |
| | | backlinks: { |
| | | title: string |
| | | noBacklinksFound: string |
| | | } |
| | | themeToggle: { |
| | | lightMode: string |
| | | darkMode: string |
| | | } |
| | | explorer: { |
| | | title: string |
| | | } |
| | | footer: { |
| | | createdWith: string |
| | | } |
| | | graph: { |
| | | title: string |
| | | } |
| | | recentNotes: { |
| | | title: string |
| | | seeRemainingMore: (variables: { remaining: number }) => string |
| | | } |
| | | transcludes: { |
| | | transcludeOf: (variables: { targetSlug: FullSlug }) => string |
| | | linkToOriginal: string |
| | | } |
| | | search: { |
| | | title: string |
| | | searchBarPlaceholder: string |
| | | } |
| | | tableOfContents: { |
| | | title: string |
| | | } |
| | | } |
| | | pages: { |
| | | rss: { |
| | | recentNotes: string |
| | | lastFewNotes: (variables: { count: number }) => string |
| | | } |
| | | error: { |
| | | title: string |
| | | notFound: string |
| | | } |
| | | folderContent: { |
| | | folder: string |
| | | itemsUnderFolder: (variables: { count: number }) => string |
| | | } |
| | | tagContent: { |
| | | tag: string |
| | | tagIndex: string |
| | | itemsUnderTag: (variables: { count: number }) => string |
| | | showingFirst: (variables: { count: number }) => string |
| | | totalTags: (variables: { count: number }) => string |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import { Translation } from "./definition" |
| | | |
| | | export default { |
| | | propertyDefaults: { |
| | | title: "Untitled", |
| | | description: "No description provided", |
| | | }, |
| | | components: { |
| | | backlinks: { |
| | | title: "Backlinks", |
| | | noBacklinksFound: "No backlinks found", |
| | | }, |
| | | themeToggle: { |
| | | lightMode: "Light mode", |
| | | darkMode: "Dark mode", |
| | | }, |
| | | explorer: { |
| | | title: "Explorer", |
| | | }, |
| | | footer: { |
| | | createdWith: "Created with", |
| | | }, |
| | | graph: { |
| | | title: "Graph View", |
| | | }, |
| | | recentNotes: { |
| | | title: "Recent Notes", |
| | | seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, |
| | | }, |
| | | transcludes: { |
| | | transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, |
| | | linkToOriginal: "Link to original", |
| | | }, |
| | | search: { |
| | | title: "Search", |
| | | searchBarPlaceholder: "Search for something", |
| | | }, |
| | | tableOfContents: { |
| | | title: "Table of Contents", |
| | | }, |
| | | }, |
| | | pages: { |
| | | rss: { |
| | | recentNotes: "Recent notes", |
| | | lastFewNotes: ({ count }) => `Last ${count} notes`, |
| | | }, |
| | | error: { |
| | | title: "Not Found", |
| | | notFound: "Either this page is private or doesn't exist.", |
| | | }, |
| | | folderContent: { |
| | | folder: "Folder", |
| | | itemsUnderFolder: ({ count }) => |
| | | count === 1 ? "1 item under this folder" : `${count} items under this folder.`, |
| | | }, |
| | | tagContent: { |
| | | tag: "Tag", |
| | | tagIndex: "Tag Index", |
| | | itemsUnderTag: ({ count }) => |
| | | count === 1 ? "1 item with this tag" : `${count} items with this tag.`, |
| | | showingFirst: ({ count }) => `Showing first ${count} tags.`, |
| | | totalTags: ({ count }) => `Found ${count} total tags.`, |
| | | }, |
| | | }, |
| | | } as const satisfies Translation |
| New file |
| | |
| | | import { Translation } from "./definition" |
| | | |
| | | export default { |
| | | propertyDefaults: { |
| | | title: "Sans titre", |
| | | description: "Aucune description fournie", |
| | | }, |
| | | components: { |
| | | backlinks: { |
| | | title: "Liens retour", |
| | | noBacklinksFound: "Aucun lien retour trouvé", |
| | | }, |
| | | themeToggle: { |
| | | lightMode: "Mode clair", |
| | | darkMode: "Mode sombre", |
| | | }, |
| | | explorer: { |
| | | title: "Explorateur", |
| | | }, |
| | | footer: { |
| | | createdWith: "Créé avec", |
| | | }, |
| | | graph: { |
| | | title: "Vue Graphique", |
| | | }, |
| | | recentNotes: { |
| | | title: "Notes Récentes", |
| | | seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`, |
| | | }, |
| | | transcludes: { |
| | | transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`, |
| | | linkToOriginal: "Lien vers l'original", |
| | | }, |
| | | search: { |
| | | title: "Recherche", |
| | | searchBarPlaceholder: "Rechercher quelque chose", |
| | | }, |
| | | tableOfContents: { |
| | | title: "Table des Matières", |
| | | }, |
| | | }, |
| | | pages: { |
| | | rss: { |
| | | recentNotes: "Notes récentes", |
| | | lastFewNotes: ({ count }) => `Les dernières ${count} notes`, |
| | | }, |
| | | error: { |
| | | title: "Pas trouvé", |
| | | notFound: "Cette page est soit privée, soit elle n'existe pas.", |
| | | }, |
| | | folderContent: { |
| | | folder: "Dossier", |
| | | itemsUnderFolder: ({ count }) => |
| | | count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`, |
| | | }, |
| | | tagContent: { |
| | | tag: "Étiquette", |
| | | tagIndex: "Index des étiquettes", |
| | | itemsUnderTag: ({ count }) => |
| | | count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`, |
| | | showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, |
| | | totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, |
| | | }, |
| | | }, |
| | | } as const satisfies Translation |
| | |
| | | import { NotFound } from "../../components" |
| | | import { defaultProcessedContent } from "../vfile" |
| | | import { write } from "./helpers" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | export const NotFoundPage: QuartzEmitterPlugin = () => { |
| | | const opts: FullPageLayout = { |
| | |
| | | const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) |
| | | const path = url.pathname as FullSlug |
| | | const externalResources = pageResources(path, resources) |
| | | const notFound = i18n(cfg.locale).pages.error.title |
| | | const [tree, vfile] = defaultProcessedContent({ |
| | | slug, |
| | | text: "Not Found", |
| | | description: "Not Found", |
| | | frontmatter: { title: "Not Found", tags: [] }, |
| | | text: notFound, |
| | | description: notFound, |
| | | frontmatter: { title: notFound, tags: [] }, |
| | | }) |
| | | const componentData: QuartzComponentProps = { |
| | | fileData: vfile.data, |
| | |
| | | return [ |
| | | await write({ |
| | | ctx, |
| | | content: renderPage(slug, componentData, opts, externalResources), |
| | | content: renderPage(cfg, slug, componentData, opts, externalResources), |
| | | slug, |
| | | ext: ".html", |
| | | }), |
| | |
| | | import { QuartzEmitterPlugin } from "../types" |
| | | import { toHtml } from "hast-util-to-html" |
| | | import { write } from "./helpers" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | export type ContentIndex = Map<FullSlug, ContentDetails> |
| | | export type ContentDetails = { |
| | |
| | | const base = cfg.baseUrl ?? "" |
| | | const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url> |
| | | <loc>https://${joinSegments(base, encodeURI(slug))}</loc> |
| | | <lastmod>${content.date?.toISOString()}</lastmod> |
| | | ${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`} |
| | | </url>` |
| | | const urls = Array.from(idx) |
| | | .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) |
| | |
| | | <channel> |
| | | <title>${escapeHTML(cfg.pageTitle)}</title> |
| | | <link>https://${base}</link> |
| | | <description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML( |
| | | <description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML( |
| | | cfg.pageTitle, |
| | | )}</description> |
| | | <generator>Quartz -- quartz.jzhao.xyz</generator> |
| | |
| | | allFiles, |
| | | } |
| | | |
| | | const content = renderPage(slug, componentData, opts, externalResources) |
| | | const content = renderPage(cfg, slug, componentData, opts, externalResources) |
| | | const fp = await write({ |
| | | ctx, |
| | | content, |
| | |
| | | import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" |
| | | import { FolderContent } from "../../components" |
| | | import { write } from "./helpers" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { |
| | | const opts: FullPageLayout = { |
| | |
| | | folder, |
| | | defaultProcessedContent({ |
| | | slug: joinSegments(folder, "index") as FullSlug, |
| | | frontmatter: { title: `Folder: ${folder}`, tags: [] }, |
| | | frontmatter: { |
| | | title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`, |
| | | tags: [], |
| | | }, |
| | | }), |
| | | ]), |
| | | ) |
| | |
| | | allFiles, |
| | | } |
| | | |
| | | const content = renderPage(slug, componentData, opts, externalResources) |
| | | const content = renderPage(cfg, slug, componentData, opts, externalResources) |
| | | const fp = await write({ |
| | | ctx, |
| | | content, |
| | |
| | | import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" |
| | | import { TagContent } from "../../components" |
| | | import { write } from "./helpers" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { |
| | | const opts: FullPageLayout = { |
| | |
| | | |
| | | const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries( |
| | | [...tags].map((tag) => { |
| | | const title = tag === "index" ? "Tag Index" : `Tag: #${tag}` |
| | | const title = |
| | | tag === "index" |
| | | ? i18n(cfg.locale).pages.tagContent.tagIndex |
| | | : `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}` |
| | | return [ |
| | | tag, |
| | | defaultProcessedContent({ |
| | |
| | | allFiles, |
| | | } |
| | | |
| | | const content = renderPage(slug, componentData, opts, externalResources) |
| | | const content = renderPage(cfg, slug, componentData, opts, externalResources) |
| | | const fp = await write({ |
| | | ctx, |
| | | content, |
| | |
| | | import toml from "toml" |
| | | import { slugTag } from "../../util/path" |
| | | import { QuartzPluginData } from "../vfile" |
| | | import { i18n } from "../../i18n" |
| | | |
| | | export interface Options { |
| | | delims: string | string[] |
| | |
| | | const opts = { ...defaultOptions, ...userOpts } |
| | | return { |
| | | name: "FrontMatter", |
| | | markdownPlugins() { |
| | | markdownPlugins({ cfg }) { |
| | | return [ |
| | | [remarkFrontmatter, ["yaml", "toml"]], |
| | | () => { |
| | |
| | | if (data.title) { |
| | | data.title = data.title.toString() |
| | | } else if (data.title === null || data.title === undefined) { |
| | | data.title = file.stem ?? "Untitled" |
| | | data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title |
| | | } |
| | | |
| | | const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"])) |
| | |
| | | import { visit } from "unist-util-visit" |
| | | import { toString } from "mdast-util-to-string" |
| | | import Slugger from "github-slugger" |
| | | import { wikilinkRegex } from "./ofm" |
| | | |
| | | export interface Options { |
| | | maxDepth: 1 | 2 | 3 | 4 | 5 | 6 |
| | |
| | | slug: string // this is just the anchor (#some-slug), not the canonical slug |
| | | } |
| | | |
| | | const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") |
| | | const slugAnchor = new Slugger() |
| | | export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = ( |
| | | userOpts, |
| | |
| | | let highestDepth: number = opts.maxDepth |
| | | visit(tree, "heading", (node) => { |
| | | if (node.depth <= opts.maxDepth) { |
| | | let text = toString(node) |
| | | |
| | | // strip link formatting from toc entries |
| | | text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => { |
| | | const fp = rawFp?.trim() ?? "" |
| | | const alias = rawAlias?.slice(1).trim() |
| | | return alias ?? fp |
| | | }) |
| | | text = text.replace(regexMdLinks, "$1") |
| | | |
| | | const text = toString(node) |
| | | highestDepth = Math.min(highestDepth, node.depth) |
| | | toc.push({ |
| | | depth: node.depth, |
| | |
| | | export function pluralize(count: number, s: string): string { |
| | | if (count === 1) { |
| | | return `1 ${s}` |
| | | } else { |
| | | return `${count} ${s}s` |
| | | } |
| | | } |
| | | |
| | | export function capitalize(s: string): string { |
| | | return s.substring(0, 1).toUpperCase() + s.substring(1) |
| | | } |