Compare commits

..

22 Commits

Author SHA1 Message Date
e85546134b Merge branch 'v4' of ssh://git.mak-sim.ru:32222/maksim/quartz into v4 2025-07-01 13:54:25 +03:00
112ed153f2 Merge branch 'v4' of https://github.com/jackyzha0/quartz into v4 2025-07-01 13:52:56 +03:00
Emile Bangma
52a5196f38 chore: converted Emoji version 16 (#2018) 2025-06-18 01:18:57 -04:00
Jiangnan Li
cdd0be2aa8 feat(giscus): expose language option for Comments component (#2012)
* add language support for Comment

* docs: let users know they can customize their language preference
2025-06-08 11:23:01 +02:00
Jacky Zhao
9de370ae14 watcher: manually ignore .git changes 2025-06-04 15:00:41 -07:00
Jacky Zhao
ae0466eedb css: unapply redundant bg on section 2025-06-03 10:57:16 -07:00
Emile Bangma
499adf4a4e fix(ios): mobile explorer overscroll behavior (#2008) 2025-06-03 13:51:38 -04:00
Jacky Zhao
cd13ce3c47 docs: clarify parens on ignorePatterns (closes #2009) 2025-06-02 12:39:01 -07:00
Emile Bangma
b48aa91728 fix(flex): respect DesktopOnly and MobileOnly components (#1971)
* fix(flex): respect DesktopOnly and MobileOnly components

* Use classNames util function

* fix(ofm): allow wikilink alias to be empty (#1984)

This is in line with Obsidian's behavior.

* fix(style): Katex adding scrollbars on non-overflowing content (#1989)

* feat(i18n): Bahasa Indonesia translations (#1981)

* fix(a11y): increased content-meta text contrast (#1980)

* fix(analytics): streamline posthog script loading and event capturing (#1974)

* css: adjust color blend for search bg

* feat(links): added ofm option to style unresolved or broken links differently (#1992)

* feat: add option to disable broken wikilinks

* fix(style): update hover color for broken links and introduce new class

* feat: add "disableBrokenWikilinks" option to ObsidianFlavoredMarkdown

* chore(deps): replace `chalk` and `rimraf` with builtin functions (#1879)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore(deps): bump the production-dependencies group across 1 directory with 9 updates (#1996)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Node 22 (#1997)

* docs: showcase housekeeping

* docs: fix explorernode references (closes #1985)

* fix: tz-less date parse in local tz instead of utc (closes #1615)

* docs: added note to not forget to add https:// to the plausible-host (for #1337) (#2000)

* docs: added note to not forget to add https:// to the plausible-host (for #1337)

* Update docs/configuration.md

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Updated documentation

---------

Co-authored-by: Nizav <106657905+Ni-zav@users.noreply.github.com>
Co-authored-by: Aswanth <aswanth366@gmail.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
Co-authored-by: Keisuke ANDO <g.kei0429@gmail.com>
Co-authored-by: fl0werpowers <47599466+fl0werpowers@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sebastian Moser <64004956+c2vi@users.noreply.github.com>
2025-06-02 18:36:57 +02:00
Sebastian Moser
7b052e8844 docs: added note to not forget to add https:// to the plausible-host (for #1337) (#2000)
* docs: added note to not forget to add https:// to the plausible-host (for #1337)

* Update docs/configuration.md

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-06-01 22:56:51 -07:00
Jacky Zhao
1cfc87652c fix: tz-less date parse in local tz instead of utc (closes #1615) 2025-06-01 22:42:37 -07:00
Jacky Zhao
096ef220dc docs: fix explorernode references (closes #1985) 2025-05-28 16:36:25 -07:00
Jacky Zhao
bc16822d14 docs: showcase housekeeping 2025-05-28 16:27:17 -07:00
Emile Bangma
7fa9253abc Node 22 (#1997) 2025-05-28 16:20:59 -07:00
dependabot[bot]
996d8d51fa chore(deps): bump the production-dependencies group across 1 directory with 9 updates (#1996)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 09:21:56 -04:00
fl0werpowers
951d1dec24 chore(deps): replace chalk and rimraf with builtin functions (#1879)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-28 10:40:51 +02:00
Keisuke ANDO
51b43a2115 feat(links): added ofm option to style unresolved or broken links differently (#1992)
* feat: add option to disable broken wikilinks

* fix(style): update hover color for broken links and introduce new class

* feat: add "disableBrokenWikilinks" option to ObsidianFlavoredMarkdown
2025-05-27 21:26:17 +02:00
Jacky Zhao
c9349457ed css: adjust color blend for search bg 2025-05-27 10:00:19 -07:00
Aswanth
6d49d97559 fix(analytics): streamline posthog script loading and event capturing (#1974) 2025-05-24 00:55:07 -04:00
Emile Bangma
c9741d00f3 fix(a11y): increased content-meta text contrast (#1980) 2025-05-23 22:09:48 -04:00
Nizav
73983cfb0e feat(i18n): Bahasa Indonesia translations (#1981) 2025-05-23 22:07:29 -04:00
Emile Bangma
52344cd816 fix(style): Katex adding scrollbars on non-overflowing content (#1989) 2025-05-23 22:05:41 -04:00
37 changed files with 3237 additions and 2839 deletions

View File

@@ -1 +1 @@
v20.9.0
v22.16.0

View File

@@ -27,7 +27,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `analytics`: what to use for analytics on your site. Values can be
- `null`: don't use analytics;
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: 'https://<your-plausible-host>' }` (self-hosted, make sure to include the `https://` protocol prefix): use [Plausible](https://plausible.io/);
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
- `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id' }` (managed) or `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id', host: 'my-goatcounter-domain.com', scriptSrc: 'https://my-url.to/counter.js' }` (self-hosted) use [GoatCounter](https://goatcounter.com);
- `{ provider: 'posthog', apiKey: '<your-posthog-project-apiKey>', host: '<your-posthog-host>' }`: use [Posthog](https://posthog.com/);

View File

@@ -45,6 +45,8 @@ afterBody: [
category: 'Announcements',
// from data-category-id
categoryId: 'DIC_kwDOFxRnmM4B-Xg6',
// from data-lang
lang: 'en'
}
}),
],
@@ -90,6 +92,10 @@ type Options = {
// where to put the comment input box relative to the comments
// defaults to 'bottom'
inputPosition?: "top" | "bottom"
// set your preference language here
// defaults to 'en'
lang?: string
}
}
```

View File

@@ -43,9 +43,7 @@ Want to customize it even more?
- Removing explorer: remove `Component.Explorer()` from `quartz.layout.ts`
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
- Component:
- Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx`
- Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx`
- Component: `quartz/components/Explorer.tsx`
- Style: `quartz/components/styles/explorer.scss`
- Script: `quartz/components/scripts/explorer.inline.ts`
@@ -188,7 +186,7 @@ Component.Explorer({
> and passing it in.
>
> ```ts title="quartz.layout.ts"
> import { Options } from "./quartz/components/ExplorerNode"
> import { Options } from "./quartz/components/Explorer"
>
> export const mapFn: Options["mapFn"] = (node) => {
> // implement your function here

View File

@@ -13,9 +13,7 @@ There may be some notes you want to avoid publishing as a website. Quartz suppor
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.
> [!warning]
> 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.
>
> `"!(PublicMedia)**/!(*.md)", "!(*.md)"`
> 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.
## `ignorePatterns`
@@ -28,7 +26,7 @@ Common examples include:
- `some/folder`: exclude the entire of `some/folder`
- `*.md`: exclude all files with a `.md` extension
- `!*.md` exclude all files that _don't_ have a `.md` extension
- `!(*.md)` exclude all files that _don't_ have a `.md` extension. Note that negations _must_ parenthesize the rest of the pattern!
- `**/private`: exclude any files or folders named `private` at any level of nesting
> [!warning]

View File

@@ -189,7 +189,7 @@ stages:
- build
- deploy
image: node:20
image: node:22
cache: # Cache modules in between jobs
key: $CI_COMMIT_REF_SLUG
paths:

View File

@@ -6,7 +6,7 @@ Quartz is a fast, batteries-included static-site generator that transforms Markd
## 🪴 Get Started
Quartz requires **at least [Node](https://nodejs.org/) v20** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
Quartz requires **at least [Node](https://nodejs.org/) v22** and `npm` v10.9.2 to function correctly. Ensure you have this installed on your machine before continuing.
Then, in your terminal of choice, enter the following commands line by line:

View File

@@ -41,6 +41,15 @@ Component.Flex({
})
```
> [!note] Overriding behavior
> Components inside `Flex` get an additional CSS class `flex-component` that add the `display: flex` property. If you want to override this behavior, you can add a `display` property to the component's CSS class in your custom CSS file.
>
> ```scss
> .flex-component {
> display: block; // or any other display type
> }
> ```
## `MobileOnly` Component
The `MobileOnly` component is a wrapper that makes its child component only visible on mobile devices. This is useful for creating responsive layouts where certain components should only appear on smaller screens.

View File

@@ -23,6 +23,7 @@ This plugin accepts the following configuration options:
- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax.
- `enableVideoEmbed`: If `true` (default), enables the embedding of video files.
- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`.
- `disableBrokenWikilinks`: If `true`, replaces links to non-existent notes with a dimmed, disabled link. Defaults to `false`.
> [!warning]
> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content!

View File

@@ -6,29 +6,18 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
- [Jacky Zhao's Garden](https://jzhao.xyz/)
- [Socratica Toolbox](https://toolbox.socratica.info/)
- [Morrowind Modding Wiki](https://morrowind-modding.github.io/)
- [Aaron Pham's Garden](https://aarnphm.xyz/)
- [The Pond](https://turntrout.com/welcome)
- [Pelayo Arbues' Notes](https://pelayoarbues.com/)
- [Stanford CME 302 Numerical Linear Algebra](https://ericdarve.github.io/NLA/)
- [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/)
- [oldwinter の数字花园](https://garden.oldwinter.top/)
- [Eilleen's Everything Notebook](https://quartz.eilleeenz.com/)
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
- [Morrowind Modding Wiki](https://morrowind-modding.github.io/)
- [Stanford CME 302 Numerical Linear Algebra](https://ericdarve.github.io/NLA/)
- [Socratica Toolbox](https://toolbox.socratica.info/)
- [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/)
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
- [Brandon Boswell's Garden](https://brandonkboswell.com)
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
- [Simon's Second Brain: Crafted, Curated, Connected, Compounded](https://brain.ssp.sh/)
- [Data Engineering Vault: A Second Brain Knowledge Network](https://vault.ssp.sh/)
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [🪴Aster's notebook](https://notes.asterhu.com)
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [Ellie's Notes](https://ellie.wtf)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
- [Eledah's Crystalline](https://blog.eledah.ir/)
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
- [Zen Browser Docs](https://docs.zen-browser.app)
- [🪴8cat life](https://8cat.life)

1069
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website",
"private": true,
"version": "4.5.0",
"version": "4.5.1",
"type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT",
@@ -20,8 +20,8 @@
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
},
"engines": {
"npm": ">=9.3.1",
"node": ">=20"
"npm": ">=10.9.2",
"node": ">=22"
},
"keywords": [
"site generator",
@@ -35,15 +35,14 @@
"quartz": "./quartz/bootstrap-cli.mjs"
},
"dependencies": {
"@clack/prompts": "^0.10.1",
"@clack/prompts": "^0.11.0",
"@floating-ui/dom": "^1.7.0",
"@myriaddreamin/rehype-typst": "^0.6.0",
"@napi-rs/simple-git": "0.1.19",
"@tweenjs/tween.js": "^25.0.0",
"@webgpu/types": "^0.1.60",
"@webgpu/types": "^0.1.61",
"ansi-truncate": "^1.2.0",
"async-mutex": "^0.5.0",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"cli-spinner": "^0.2.10",
"d3": "^7.9.0",
@@ -57,14 +56,14 @@
"hast-util-to-string": "^3.0.1",
"is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0",
"lightningcss": "^1.29.3",
"lightningcss": "^1.30.1",
"mdast-util-find-and-replace": "^3.0.2",
"mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5",
"minimatch": "^10.0.1",
"pixi.js": "^8.9.2",
"preact": "^10.26.5",
"preact": "^10.26.7",
"preact-render-to-string": "^6.5.13",
"pretty-bytes": "^7.0.0",
"pretty-time": "^1.1.0",
@@ -85,10 +84,9 @@
"remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2",
"rfdc": "^1.4.1",
"rimraf": "^6.0.1",
"satori": "^0.12.2",
"satori": "^0.13.1",
"serve-handler": "^6.1.6",
"sharp": "^0.34.1",
"sharp": "^0.34.2",
"shiki": "^1.26.2",
"source-map-support": "^0.5.21",
"to-vfile": "^8.0.0",
@@ -98,18 +96,18 @@
"vfile": "^6.0.3",
"workerpool": "^9.2.0",
"ws": "^8.18.2",
"yargs": "^17.7.2"
"yargs": "^18.0.0"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.15.7",
"@types/node": "^22.15.23",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1",
"@types/yargs": "^17.0.33",
"esbuild": "^0.25.3",
"esbuild": "^0.25.5",
"prettier": "^3.5.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3"

View File

@@ -2,9 +2,9 @@ import sourceMapSupport from "source-map-support"
sourceMapSupport.install(options)
import path from "path"
import { PerfTimer } from "./util/perf"
import { rimraf } from "rimraf"
import { rm } from "fs/promises"
import { GlobbyFilterFunction, isGitIgnored } from "globby"
import chalk from "chalk"
import { styleText } from "util"
import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit"
@@ -67,7 +67,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const release = await mut.acquire()
perf.addEvent("clean")
await rimraf(path.join(output, "*"), { glob: true })
await rm(output, { recursive: true, force: true })
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
perf.addEvent("glob")
@@ -85,7 +85,9 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const filteredContent = filterContent(ctx, parsedFiles)
await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${markdownPaths.length} files in ${perf.timeSince()}`))
console.log(
styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
)
release()
if (argv.watch) {
@@ -123,9 +125,10 @@ async function startWatching(
ctx,
mut,
contentMap,
ignored: (path) => {
if (gitIgnoredMatcher(path)) return true
const pathStr = path.toString()
ignored: (fp) => {
const pathStr = toPosixPath(fp.toString())
if (pathStr.startsWith(".git/")) return true
if (gitIgnoredMatcher(pathStr)) return true
for (const pattern of cfg.configuration.ignorePatterns) {
if (minimatch(pathStr, pattern)) {
return true
@@ -186,7 +189,7 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
const perf = new PerfTimer()
perf.addEvent("rebuild")
console.log(chalk.yellow("Detected change, rebuilding..."))
console.log(styleText("yellow", "Detected change, rebuilding..."))
// update changesSinceLastBuild
for (const change of changes) {
@@ -281,7 +284,7 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
}
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
console.log(styleText("green", `Done rebuilding in ${perf.timeSince()}`))
changes.splice(0, numChangesInBuild)
clientRefresh()
release()

View File

@@ -1,11 +1,11 @@
import { promises } from "fs"
import path from "path"
import esbuild from "esbuild"
import chalk from "chalk"
import { styleText } from "util"
import { sassPlugin } from "esbuild-sass-plugin"
import fs from "fs"
import { intro, outro, select, text } from "@clack/prompts"
import { rimraf } from "rimraf"
import { rm } from "fs/promises"
import chokidar from "chokidar"
import prettyBytes from "pretty-bytes"
import { execSync, spawnSync } from "child_process"
@@ -48,7 +48,7 @@ function resolveContentPath(contentPath) {
*/
export async function handleCreate(argv) {
console.log()
intro(chalk.bgGreen.black(` Quartz v${version} `))
intro(styleText(["bgGreen", "black"], ` Quartz v${version} `))
const contentFolder = resolveContentPath(argv.directory)
let setupStrategy = argv.strategy?.toLowerCase()
let linkResolutionStrategy = argv.links?.toLowerCase()
@@ -61,12 +61,16 @@ export async function handleCreate(argv) {
// Error handling
if (!sourceDirectory) {
outro(
chalk.red(
`Setup strategies (arg '${chalk.yellow(
styleText(
"red",
`Setup strategies (arg '${styleText(
"yellow",
`-${CreateArgv.strategy.alias[0]}`,
)}') other than '${chalk.yellow(
)}') other than '${styleText(
"yellow",
"new",
)}' require content folder argument ('${chalk.yellow(
)}' require content folder argument ('${styleText(
"yellow",
`-${CreateArgv.source.alias[0]}`,
)}') to be set`,
),
@@ -75,19 +79,23 @@ export async function handleCreate(argv) {
} else {
if (!fs.existsSync(sourceDirectory)) {
outro(
chalk.red(
`Input directory to copy/symlink 'content' from not found ('${chalk.yellow(
styleText(
"red",
`Input directory to copy/symlink 'content' from not found ('${styleText(
"yellow",
sourceDirectory,
)}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`,
)}', invalid argument "${styleText("yellow", `-${CreateArgv.source.alias[0]}`)})`,
),
)
process.exit(1)
} else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
outro(
chalk.red(
`Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow(
styleText(
"red",
`Source directory to copy/symlink 'content' from is not a directory (found file at '${styleText(
"yellow",
sourceDirectory,
)}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`,
)}', invalid argument ${styleText("yellow", `-${CreateArgv.source.alias[0]}`)}")`,
),
)
process.exit(1)
@@ -119,7 +127,7 @@ export async function handleCreate(argv) {
if (contentStat.isSymbolicLink()) {
await fs.promises.unlink(contentFolder)
} else {
await rimraf(contentFolder)
await rm(contentFolder, { recursive: true, force: true })
}
}
@@ -229,7 +237,7 @@ export async function handleBuild(argv) {
argv.watch = true
}
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
const ctx = await esbuild.context({
entryPoints: [fp],
outfile: cacheFile,
@@ -304,13 +312,13 @@ export async function handleBuild(argv) {
}
if (cleanupBuild) {
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
console.log(styleText("yellow", "Detected a source code change, doing a hard rebuild..."))
await cleanupBuild()
}
const result = await ctx.rebuild().catch((err) => {
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${chalk.grey(err)}`)
console.error(`${styleText("red", "Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${styleText("grey", err)}`)
process.exit(1)
})
release()
@@ -348,7 +356,8 @@ export async function handleBuild(argv) {
const server = http.createServer(async (req, res) => {
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
console.log(
chalk.red(
styleText(
"red",
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
),
)
@@ -383,8 +392,10 @@ export async function handleBuild(argv) {
})
const status = res.statusCode
const statusString =
status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`))
status >= 200 && status < 300
? styleText("green", `[${status}]`)
: styleText("red", `[${status}]`)
console.log(statusString + styleText("grey", ` ${argv.baseDir}${req.url}`))
release()
}
@@ -393,7 +404,10 @@ export async function handleBuild(argv) {
res.writeHead(302, {
Location: newFp,
})
console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`))
console.log(
styleText("yellow", "[302]") +
styleText("grey", ` ${argv.baseDir}${req.url} -> ${newFp}`),
)
res.end()
}
@@ -443,7 +457,8 @@ export async function handleBuild(argv) {
const wss = new WebSocketServer({ port: argv.wsPort })
wss.on("connection", (ws) => connections.push(ws))
console.log(
chalk.cyan(
styleText(
"cyan",
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
),
)
@@ -467,7 +482,7 @@ export async function handleBuild(argv) {
.on("change", () => build(clientRefresh))
.on("unlink", () => build(clientRefresh))
console.log(chalk.grey("hint: exit with ctrl+c"))
console.log(styleText("grey", "hint: exit with ctrl+c"))
}
}
@@ -477,7 +492,7 @@ export async function handleBuild(argv) {
*/
export async function handleUpdate(argv) {
const contentFolder = resolveContentPath(argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
console.log("Backing up your content")
execSync(
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
@@ -490,7 +505,7 @@ export async function handleUpdate(argv) {
try {
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
} catch {
console.log(chalk.red("An error occurred above while pulling updates."))
console.log(styleText("red", "An error occurred above while pulling updates."))
await popContentFolder(contentFolder)
return
}
@@ -517,9 +532,9 @@ export async function handleUpdate(argv) {
const res = spawnSync("npm", ["i"], opts)
if (res.status === 0) {
console.log(chalk.green("Done!"))
console.log(styleText("green", "Done!"))
} else {
console.log(chalk.red("An error occurred above while installing dependencies."))
console.log(styleText("red", "An error occurred above while installing dependencies."))
}
}
@@ -538,14 +553,14 @@ export async function handleRestore(argv) {
*/
export async function handleSync(argv) {
const contentFolder = resolveContentPath(argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)}\n`)
console.log("Backing up your content")
if (argv.commit) {
const contentStat = await fs.promises.lstat(contentFolder)
if (contentStat.isSymbolicLink()) {
const linkTarg = await fs.promises.readlink(contentFolder)
console.log(chalk.yellow("Detected symlink, trying to dereference before committing"))
console.log(styleText("yellow", "Detected symlink, trying to dereference before committing"))
// stash symlink file
await stashContentFolder(contentFolder)
@@ -580,7 +595,7 @@ export async function handleSync(argv) {
try {
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
} catch {
console.log(chalk.red("An error occurred above while pulling updates."))
console.log(styleText("red", "An error occurred above while pulling updates."))
await popContentFolder(contentFolder)
return
}
@@ -594,10 +609,12 @@ export async function handleSync(argv) {
stdio: "inherit",
})
if (res.status !== 0) {
console.log(chalk.red(`An error occurred above while pushing to remote ${ORIGIN_NAME}.`))
console.log(
styleText("red", `An error occurred above while pushing to remote ${ORIGIN_NAME}.`),
)
return
}
}
console.log(chalk.green("Done!"))
console.log(styleText("green", "Done!"))
}

View File

@@ -1,5 +1,5 @@
import { isCancel, outro } from "@clack/prompts"
import chalk from "chalk"
import { styleText } from "util"
import { contentCacheFolder } from "./constants.js"
import { spawnSync } from "child_process"
import fs from "fs"
@@ -14,7 +14,7 @@ export function escapePath(fp) {
export function exitIfCancel(val) {
if (isCancel(val)) {
outro(chalk.red("Exiting"))
outro(styleText("red", "Exiting"))
process.exit(0)
} else {
return val
@@ -36,9 +36,9 @@ export function gitPull(origin, branch) {
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
if (out.stderr) {
throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`))
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
} else if (out.status !== 0) {
throw new Error(chalk.red("Error while pulling updates"))
throw new Error(styleText("red", "Error while pulling updates"))
}
}

View File

@@ -17,6 +17,7 @@ type Options = {
strict?: boolean
reactionsEnabled?: boolean
inputPosition?: "top" | "bottom"
lang?: string
}
}
@@ -50,6 +51,7 @@ export default ((opts: Options) => {
data-theme-url={
opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`
}
data-lang={opts.options.lang ?? "en"}
></div>
)
}

View File

@@ -1,4 +1,5 @@
import { concatenateResources } from "../util/resources"
import { classNames } from "../util/lang"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type FlexConfig = {
@@ -23,7 +24,10 @@ export default ((config: FlexConfig) => {
const gap = config.gap ?? "1rem"
return (
<div style={`display: flex; flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}>
<div
class={classNames(props.displayClass, "flex-component")}
style={`flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}
>
{config.components.map((c) => {
const grow = c.grow ? 1 : 0
const shrink = (c.shrink ?? true) ? 1 : 0

View File

@@ -55,6 +55,7 @@ type GiscusElement = Omit<HTMLElement, "dataset"> & {
strict: string
reactionsEnabled: string
inputPosition: "top" | "bottom"
lang: string
}
}
@@ -78,7 +79,7 @@ document.addEventListener("nav", () => {
giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict)
giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled)
giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition)
giscusScript.setAttribute("data-lang", giscusContainer.dataset.lang)
const theme = document.documentElement.getAttribute("saved-theme")
if (theme) {
giscusScript.setAttribute("data-theme", getThemeUrl(getThemeName(theme)))

View File

@@ -1,6 +1,6 @@
.content-meta {
margin-top: 0;
color: var(--gray);
color: var(--darkgray);
&[show-comma="true"] {
> *:not(:last-child) {

View File

@@ -239,7 +239,7 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
margin-top: 0;
background-color: var(--light);
max-width: 100vw;
width: 100%;
width: 100vw;
transform: translateX(-100vw);
transition:
transform 200ms ease,
@@ -265,6 +265,6 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
.mobile-no-scroll {
@media all and ($mobile) {
overflow: hidden;
overscroll-behavior: none;
}
}

View File

@@ -8,7 +8,7 @@
}
& > .search-button {
background-color: var(--lightgray);
background-color: color-mix(in srgb, var(--lightgray) 60%, var(--light));
border: none;
border-radius: 4px;
font-family: inherit;

View File

@@ -26,6 +26,7 @@ import th from "./locales/th-TH"
import lt from "./locales/lt-LT"
import fi from "./locales/fi-FI"
import no from "./locales/nb-NO"
import id from "./locales/id-ID"
export const TRANSLATIONS = {
"en-US": enUs,
@@ -76,6 +77,7 @@ export const TRANSLATIONS = {
"lt-LT": lt,
"fi-FI": fi,
"nb-NO": no,
"id-ID": id,
} as const
export const defaultTranslation = "en-US"

View File

@@ -0,0 +1,87 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Tanpa Judul",
description: "Tidak ada deskripsi",
},
components: {
callout: {
note: "Catatan",
abstract: "Abstrak",
info: "Info",
todo: "Daftar Tugas",
tip: "Tips",
success: "Berhasil",
question: "Pertanyaan",
warning: "Peringatan",
failure: "Gagal",
danger: "Bahaya",
bug: "Bug",
example: "Contoh",
quote: "Kutipan",
},
backlinks: {
title: "Tautan Balik",
noBacklinksFound: "Tidak ada tautan balik ditemukan",
},
themeToggle: {
lightMode: "Mode Terang",
darkMode: "Mode Gelap",
},
readerMode: {
title: "Mode Pembaca",
},
explorer: {
title: "Penjelajah",
},
footer: {
createdWith: "Dibuat dengan",
},
graph: {
title: "Tampilan Grafik",
},
recentNotes: {
title: "Catatan Terbaru",
seeRemainingMore: ({ remaining }) => `Lihat ${remaining} lagi →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transklusi dari ${targetSlug}`,
linkToOriginal: "Tautan ke asli",
},
search: {
title: "Cari",
searchBarPlaceholder: "Cari sesuatu",
},
tableOfContents: {
title: "Daftar Isi",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} menit baca`,
},
},
pages: {
rss: {
recentNotes: "Catatan terbaru",
lastFewNotes: ({ count }) => `${count} catatan terakhir`,
},
error: {
title: "Tidak Ditemukan",
notFound: "Halaman ini bersifat privat atau tidak ada.",
home: "Kembali ke Beranda",
},
folderContent: {
folder: "Folder",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 item di bawah folder ini." : `${count} item di bawah folder ini.`,
},
tagContent: {
tag: "Tag",
tagIndex: "Indeks Tag",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 item dengan tag ini." : `${count} item dengan tag ini.`,
showingFirst: ({ count }) => `Menampilkan ${count} tag pertama.`,
totalTags: ({ count }) => `Ditemukan total ${count} tag.`,
},
},
} as const satisfies Translation

View File

@@ -1,7 +1,7 @@
import { FilePath, joinSegments } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import fs from "fs"
import chalk from "chalk"
import { styleText } from "util"
export function extractDomainFromBaseUrl(baseUrl: string) {
const url = new URL(`https://${baseUrl}`)
@@ -12,7 +12,9 @@ export const CNAME: QuartzEmitterPlugin = () => ({
name: "CNAME",
async emit({ argv, cfg }) {
if (!cfg.configuration.baseUrl) {
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
console.warn(
styleText("yellow", "CNAME emitter requires `baseUrl` to be set in your configuration"),
)
return []
}
const path = joinSegments(argv.output, "CNAME")

View File

@@ -159,14 +159,10 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
posthog.init('${cfg.analytics.apiKey}', {
api_host: '${cfg.analytics.host ?? "https://app.posthog.com"}',
capture_pageview: false,
})\`
posthogScript.onload = () => {
});
document.addEventListener('nav', () => {
posthog.capture('$pageview', { path: location.pathname });
document.addEventListener('nav', () => {
posthog.capture('$pageview', { path: location.pathname });
});
};
})\`
document.head.appendChild(posthogScript);
`)

View File

@@ -8,7 +8,7 @@ import { FullPageLayout } from "../../cfg"
import { pathToRoot } from "../../util/path"
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { Content } from "../../components"
import chalk from "chalk"
import { styleText } from "util"
import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { Node } from "unist"
@@ -90,7 +90,8 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
if (!containsIndex) {
console.log(
chalk.yellow(
styleText(
"yellow",
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder (\`${path.join(ctx.argv.directory, "index.md")} does not exist\`). This may cause errors when deploying.`,
),
)

View File

@@ -11,7 +11,7 @@ import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { QuartzPluginData } from "../vfile"
import fs from "node:fs/promises"
import chalk from "chalk"
import { styleText } from "util"
const defaultOptions: SocialImageOptions = {
colorScheme: "lightMode",
@@ -36,7 +36,7 @@ async function generateSocialImage(
const iconData = await fs.readFile(iconPath)
iconBase64 = `data:image/png;base64,${iconData.toString("base64")}`
} catch (err) {
console.warn(chalk.yellow(`Warning: Could not find icon at ${iconPath}`))
console.warn(styleText("yellow", `Warning: Could not find icon at ${iconPath}`))
}
const imageComponent = userOpts.imageStructure({

View File

@@ -101,7 +101,11 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
const socialImage = coalesceAliases(data, ["socialImage", "image", "cover"])
const created = coalesceAliases(data, ["created", "date"])
if (created) data.created = created
if (created) {
data.created = created
data.modified ||= created // if modified is not set, use created
}
const modified = coalesceAliases(data, [
"modified",
"lastmod",

View File

@@ -1,8 +1,8 @@
import fs from "fs"
import { Repository } from "@napi-rs/simple-git"
import { QuartzTransformerPlugin } from "../types"
import chalk from "chalk"
import path from "path"
import { styleText } from "util"
export interface Options {
priority: ("frontmatter" | "git" | "filesystem")[]
@@ -12,12 +12,23 @@ const defaultOptions: Options = {
priority: ["frontmatter", "git", "filesystem"],
}
// YYYY-MM-DD
const iso8601DateOnlyRegex = /^\d{4}-\d{2}-\d{2}$/
function coerceDate(fp: string, d: any): Date {
// check ISO8601 date-only format
// we treat this one as local midnight as the normal
// js date ctor treats YYYY-MM-DD as UTC midnight
if (typeof d === "string" && iso8601DateOnlyRegex.test(d)) {
d = `${d}T00:00:00`
}
const dt = new Date(d)
const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0
if (invalidDate && d !== undefined) {
console.log(
chalk.yellow(
styleText(
"yellow",
`\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`,
),
)
@@ -42,7 +53,10 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
repositoryWorkdir = repo.workdir() ?? ctx.argv.directory
} catch (e) {
console.log(
chalk.yellow(`\nWarning: couldn't find git repository for ${ctx.argv.directory}`),
styleText(
"yellow",
`\nWarning: couldn't find git repository for ${ctx.argv.directory}`,
),
)
}
}
@@ -69,7 +83,8 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (u
modified ||= await repo.getFileLatestModifiedDateAsync(relativePath)
} catch {
console.log(
chalk.yellow(
styleText(
"yellow",
`\nWarning: ${file.data.filePath!} isn't yet tracked by git, dates will be inaccurate`,
),
)

View File

@@ -41,6 +41,7 @@ export interface Options {
enableYouTubeEmbed: boolean
enableVideoEmbed: boolean
enableCheckbox: boolean
disableBrokenWikilinks: boolean
}
const defaultOptions: Options = {
@@ -56,6 +57,7 @@ const defaultOptions: Options = {
enableYouTubeEmbed: true,
enableVideoEmbed: true,
enableCheckbox: false,
disableBrokenWikilinks: false,
}
const calloutMapping = {
@@ -206,7 +208,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
return src
},
markdownPlugins(_ctx) {
markdownPlugins(ctx) {
const plugins: PluggableList = []
// regex replacements
@@ -275,6 +277,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
// otherwise, fall through to regular link
}
// treat as broken link if slug not in ctx.allSlugs
if (opts.disableBrokenWikilinks) {
const slug = slugifyFilePath(fp as FilePath)
const exists = ctx.allSlugs && ctx.allSlugs.includes(slug)
if (!exists) {
return {
type: "html",
value: `<a class=\"internal broken\">${alias ?? fp}</a>`,
}
}
}
// internal link
const url = fp + anchor

View File

@@ -4,7 +4,7 @@ import { ProcessedContent } from "../plugins/vfile"
import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace"
import { BuildCtx } from "../util/ctx"
import chalk from "chalk"
import { styleText } from "util"
export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
const { argv, cfg } = ctx
@@ -26,7 +26,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
if (ctx.argv.verbose) {
console.log(`[emit:${emitter.name}] ${file}`)
} else {
log.updateText(`${emitter.name} -> ${chalk.gray(file)}`)
log.updateText(`${emitter.name} -> ${styleText("gray", file)}`)
}
}
} else {
@@ -36,7 +36,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
if (ctx.argv.verbose) {
console.log(`[emit:${emitter.name}] ${file}`)
} else {
log.updateText(`${emitter.name} -> ${chalk.gray(file)}`)
log.updateText(`${emitter.name} -> ${styleText("gray", file)}`)
}
}
}

View File

@@ -13,7 +13,7 @@ import workerpool, { Promise as WorkerPromise } from "workerpool"
import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace"
import { BuildCtx, WorkerSerializableBuildCtx } from "../util/ctx"
import chalk from "chalk"
import { styleText } from "util"
export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
@@ -194,7 +194,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
textToMarkdownPromises.map(async (promise) => {
const result = await promise
processedFiles += result.length
log.updateText(`text->markdown ${chalk.gray(`${processedFiles}/${fps.length}`)}`)
log.updateText(`text->markdown ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
return result
}),
).catch(errorHandler)
@@ -208,7 +208,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
markdownToHtmlPromises.map(async (promise) => {
const result = await promise
processedFiles += result.length
log.updateText(`markdown->html ${chalk.gray(`${processedFiles}/${fps.length}`)}`)
log.updateText(`markdown->html ${styleText("gray", `${processedFiles}/${fps.length}`)}`)
return result
}),
).catch(errorHandler)

View File

@@ -11,8 +11,7 @@ html {
width: 100vw;
}
body,
section {
body {
margin: 0;
box-sizing: border-box;
background-color: var(--light);
@@ -91,7 +90,7 @@ a {
color: var(--secondary);
&:hover {
color: var(--tertiary) !important;
color: var(--tertiary);
}
&.internal {
@@ -101,6 +100,15 @@ a {
border-radius: 5px;
line-height: 1.4rem;
&.broken {
color: var(--secondary);
opacity: 0.5;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
}
&:has(> img) {
background-color: transparent;
border-radius: 0;
@@ -123,16 +131,32 @@ a {
}
}
.flex-component {
display: flex;
}
.desktop-only {
display: initial;
&.flex-component {
display: flex;
}
@media all and ($mobile) {
&.flex-component {
display: none;
}
display: none;
}
}
.mobile-only {
display: none;
&.flex-component {
display: none;
}
@media all and ($mobile) {
&.flex-component {
display: flex;
}
display: initial;
}
}
@@ -594,6 +618,7 @@ ol.overflow {
}
.katex-display {
display: initial;
overflow-x: auto;
overflow-y: hidden;
}

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,7 @@ import { QUARTZ } from "./path"
import { formatDate, getDate } from "../components/Date"
import readingTime from "reading-time"
import { i18n } from "../i18n"
import chalk from "chalk"
import { styleText } from "util"
const defaultHeaderWeight = [700]
const defaultBodyWeight = [400]
@@ -100,7 +100,8 @@ export async function fetchTtf(
if (!match) {
console.log(
chalk.yellow(
styleText(
"yellow",
`\nWarning: Failed to fetch font ${rawFontName} with weight ${weight}, got ${cssResponse.statusText}`,
),
)

View File

@@ -1,5 +1,5 @@
import chalk from "chalk"
import pretty from "pretty-time"
import { styleText } from "util"
export class PerfTimer {
evts: { [key: string]: [number, number] }
@@ -14,6 +14,6 @@ export class PerfTimer {
}
timeSince(evtName?: string): string {
return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"])))
return styleText("yellow", pretty(process.hrtime(this.evts[evtName ?? "start"])))
}
}

View File

@@ -1,4 +1,4 @@
import chalk from "chalk"
import { styleText } from "util"
import process from "process"
import { isMainThread } from "workerpool"
@@ -11,9 +11,9 @@ export function trace(msg: string, err: Error) {
lines.push("")
lines.push(
"\n" +
chalk.bgRed.black.bold(" ERROR ") +
styleText(["bgRed", "black", "bold"], " ERROR ") +
"\n\n" +
chalk.red(` ${msg}`) +
styleText("red", ` ${msg}`) +
(err.message.length > 0 ? `: ${err.message}` : ""),
)