diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b1622cb8..56107cf49 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,8 +46,26 @@ jobs: - name: Ensure Quartz builds, check bundle info run: npx quartz build --bundleInfo - - name: Create release tag - uses: Klemensas/action-autotag@stable + publish-tag: + if: ${{ github.repository == 'jackyzha0/quartz' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 with: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - tag_prefix: "v" + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Get package version + run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV + - name: Create release tag + uses: pkgdeps/git-tag-action@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + github_repo: ${{ github.repository }} + version: ${{ env.PACKAGE_VERSION }} + git_commit_sha: ${{ github.sha }} + git_tag_prefix: "v" diff --git a/docs/configuration.md b/docs/configuration.md index 6c7665492..64968fbb4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,6 +28,7 @@ This part of the configuration concerns anything that can affect the whole site. - `{ provider: 'google', tagId: '' }`: use Google Analytics; - `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '' }` (self-hosted): use [Plausible](https://plausible.io/); - `{ provider: 'umami', host: '', websiteId: '' }`: 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) - `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`. @@ -73,10 +74,10 @@ You can customize the behaviour of Quartz by adding, removing and reordering plu > [!note] > Each node is modified by every transformer _in order_. Some transformers are position sensitive, so you may need to pay particular attention to whether they need to come before or after certain other plugins. -You should take care to add the plugin to the right entry corresponding to its plugin type. For example, to add the [[ExplicitPublish]] plugin (a [[tags/plugin/transformer|Transformer]], you would add the following line: +You should take care to add the plugin to the right entry corresponding to its plugin type. For example, to add the [[ExplicitPublish]] plugin (a [[tags/plugin/filter|Filter]]), you would add the following line: ```ts title="quartz.config.ts" -transformers: [ +filters: [ ... Plugin.ExplicitPublish(), ... diff --git a/docs/features/Latex.md b/docs/features/Latex.md index e019190e1..b2bdb2dfb 100644 --- a/docs/features/Latex.md +++ b/docs/features/Latex.md @@ -57,6 +57,15 @@ For example: - Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2 - Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2 +### Using mhchem + +Add the following import to the top of `quartz/plugins/transformers/latex.ts` (before all the other +imports): + +```ts title="quartz/plugins/transformers/latex.ts" +import "katex/contrib/mhchem" +``` + ## Customization Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options. diff --git a/docs/features/explorer.md b/docs/features/explorer.md index a2acb861a..95878f787 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -61,7 +61,7 @@ export class FileNode { children: FileNode[] // children of current node name: string // last part of slug displayName: string // what actually should be displayed in the explorer - file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail + file: QuartzPluginData | null // if node is a file, this is the file's metadata. See `QuartzPluginData` for more detail depth: number // depth of current node ... // rest of implementation @@ -167,6 +167,19 @@ Component.Explorer({ You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove. +### Remove files by tag + +You can access the frontmatter of a file by `node.file?.frontmatter?`. This allows you to filter out files based on their frontmatter, for example by their tags. + +```ts title="quartz.layout.ts" +Component.Explorer({ + filterFn: (node) => { + // exclude files with the tag "explorerexclude" + return node.file?.frontmatter?.tags?.includes("explorerexclude") !== true + }, +}) +``` + ### Show every element in explorer To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`. diff --git a/docs/plugins/Description.md b/docs/plugins/Description.md index 6849b3ca4..526bb0175 100644 --- a/docs/plugins/Description.md +++ b/docs/plugins/Description.md @@ -14,6 +14,7 @@ If the frontmatter contains a `description` property, it is used (see [[authorin This plugin accepts the following configuration options: - `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. +- `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`). ## API diff --git a/docs/plugins/ExplicitPublish.md b/docs/plugins/ExplicitPublish.md index 7ef2dd4d0..7c7aceb65 100644 --- a/docs/plugins/ExplicitPublish.md +++ b/docs/plugins/ExplicitPublish.md @@ -13,6 +13,6 @@ This plugin has no configuration options. ## API -- Category: Emitter +- Category: Filter - Function name: `Plugin.ExplicitPublish()`. - Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts). diff --git a/docs/plugins/ObsidianFlavoredMarkdown.md b/docs/plugins/ObsidianFlavoredMarkdown.md index 7700a5cfb..c4240865f 100644 --- a/docs/plugins/ObsidianFlavoredMarkdown.md +++ b/docs/plugins/ObsidianFlavoredMarkdown.md @@ -20,7 +20,7 @@ This plugin accepts the following configuration options: - `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents. - `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks. - `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`. -- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos using external image Markdown syntax. +- `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`. diff --git a/docs/plugins/TagPage.md b/docs/plugins/TagPage.md index 4dabfd06c..cd7dee609 100644 --- a/docs/plugins/TagPage.md +++ b/docs/plugins/TagPage.md @@ -16,5 +16,5 @@ The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts` ## API - Category: Emitter -- Function name: `Plugin.AliasRedirects()`. +- Function name: `Plugin.TagPage()`. - Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx). diff --git a/package-lock.json b/package-lock.json index dc020fc83..b3e7eced9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.2.2", + "version": "4.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.2.2", + "version": "4.2.3", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", @@ -14,7 +14,7 @@ "@napi-rs/simple-git": "0.1.16", "async-mutex": "^0.4.1", "chalk": "^5.3.0", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "cli-spinner": "^0.2.10", "d3": "^7.8.5", "esbuild-sass-plugin": "^2.16.1", @@ -27,13 +27,13 @@ "hast-util-to-string": "^3.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.23.0", + "lightningcss": "^1.24.0", "mdast-util-find-and-replace": "^3.0.1", "mdast-util-to-hast": "^13.1.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.5", - "preact-render-to-string": "^6.3.1", + "preact": "^10.19.6", + "preact-render-to-string": "^6.4.0", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", "reading-time": "^1.5.0", @@ -50,11 +50,11 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", - "remark-smartypants": "^2.0.0", + "remark-smartypants": "^2.1.0", "rfdc": "^1.3.1", "rimraf": "^5.0.5", "serve-handler": "^6.1.5", - "shiki": "^1.1.6", + "shiki": "^1.1.7", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", @@ -73,7 +73,7 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.19", + "@types/node": "^20.11.24", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.5.10", @@ -743,9 +743,9 @@ } }, "node_modules/@shikijs/core": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.6.tgz", - "integrity": "sha512-kt9hhvrWTm0EPtRDIsoAZnSsFlIDBVBBI5CQewpA/NZCPin+MOKRXg+JiWc4y+8fZ/v0HzfDhu/UC+OTZGMt7A==" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.7.tgz", + "integrity": "sha512-gTYLUIuD1UbZp/11qozD3fWpUTuMqPSf3svDMMrL0UmlGU7D9dPw/V1FonwAorCUJBltaaESxq90jrSjQyGixg==" }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", @@ -1093,9 +1093,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1344,15 +1344,9 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1365,6 +1359,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -3035,9 +3032,9 @@ } }, "node_modules/lightningcss": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz", - "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.0.tgz", + "integrity": "sha512-y36QEEDVx4IM7/yIZNsZJMRREIu26WzTsauIysf5s76YeCmlSbRZS7aC97IGPuoFRnyZ5Wx43OBsQBFB5Ne7ng==", "dependencies": { "detect-libc": "^1.0.3" }, @@ -3049,21 +3046,21 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.23.0", - "lightningcss-darwin-x64": "1.23.0", - "lightningcss-freebsd-x64": "1.23.0", - "lightningcss-linux-arm-gnueabihf": "1.23.0", - "lightningcss-linux-arm64-gnu": "1.23.0", - "lightningcss-linux-arm64-musl": "1.23.0", - "lightningcss-linux-x64-gnu": "1.23.0", - "lightningcss-linux-x64-musl": "1.23.0", - "lightningcss-win32-x64-msvc": "1.23.0" + "lightningcss-darwin-arm64": "1.24.0", + "lightningcss-darwin-x64": "1.24.0", + "lightningcss-freebsd-x64": "1.24.0", + "lightningcss-linux-arm-gnueabihf": "1.24.0", + "lightningcss-linux-arm64-gnu": "1.24.0", + "lightningcss-linux-arm64-musl": "1.24.0", + "lightningcss-linux-x64-gnu": "1.24.0", + "lightningcss-linux-x64-musl": "1.24.0", + "lightningcss-win32-x64-msvc": "1.24.0" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz", - "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.0.tgz", + "integrity": "sha512-rTNPkEiynOu4CfGdd5ZfVOQe2gd2idfQd4EfX1l2ZUUwd+2SwSdbb7cG4rlwfnZckbzCAygm85xkpekRE5/wFw==", "cpu": [ "arm64" ], @@ -3080,9 +3077,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz", - "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.0.tgz", + "integrity": "sha512-4KCeF2RJjzp9xdGY8zIH68CUtptEg8uz8PfkHvsIdrP4t9t5CIgfDBhiB8AmuO75N6SofdmZexDZIKdy9vA7Ww==", "cpu": [ "x64" ], @@ -3099,9 +3096,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz", - "integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.0.tgz", + "integrity": "sha512-FJAYlek1wXuVTsncNU0C6YD41q126dXcIUm97KAccMn9C4s/JfLSqGWT2gIzAblavPFkyGG2gIADTWp3uWfN1g==", "cpu": [ "x64" ], @@ -3118,9 +3115,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz", - "integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.0.tgz", + "integrity": "sha512-N55K6JqzMx7C0hYUu1YmWqhkHwzEJlkQRMA6phY65noO0I1LOAvP4wBIoFWrzRE+O6zL0RmXJ2xppqyTbk3sYw==", "cpu": [ "arm" ], @@ -3137,9 +3134,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz", - "integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.0.tgz", + "integrity": "sha512-MqqUB2TpYtFWeBvvf5KExDdClU3YGLW5bHKs50uKKootcvG9KoS7wYwd5UichS+W3mYLc5yXUPGD1DNWbLiYKw==", "cpu": [ "arm64" ], @@ -3156,9 +3153,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz", - "integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.0.tgz", + "integrity": "sha512-5wn4d9tFwa5bS1ao9mLexYVJdh3nn09HNIipsII6ZF7z9ZA5J4dOEhMgKoeCl891axTGTUYd8Kxn+Hn3XUSYRQ==", "cpu": [ "arm64" ], @@ -3175,9 +3172,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz", - "integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.0.tgz", + "integrity": "sha512-3j5MdTh+LSDF3o6uDwRjRUgw4J+IfDCVtdkUrJvKxL79qBLUujXY7CTe5X3IQDDLKEe/3wu49r8JKgxr0MfjbQ==", "cpu": [ "x64" ], @@ -3194,9 +3191,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz", - "integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.0.tgz", + "integrity": "sha512-HI+rNnvaLz0o36z6Ki0gyG5igVGrJmzczxA5fznr6eFTj3cHORoR/j2q8ivMzNFR4UKJDkTWUH5LMhacwOHWBA==", "cpu": [ "x64" ], @@ -3213,9 +3210,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz", - "integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.0.tgz", + "integrity": "sha512-oeije/t7OZ5N9vSs6amyW/34wIYoBCpE6HUlsSKcP2SR1CVgx9oKEM00GtQmtqNnYiMIfsSm7+ppMb4NLtD5vg==", "cpu": [ "x64" ], @@ -4459,18 +4456,18 @@ } }, "node_modules/preact": { - "version": "10.19.5", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz", - "integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==", + "version": "10.19.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.6.tgz", + "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, "node_modules/preact-render-to-string": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.3.1.tgz", - "integrity": "sha512-NQ28WrjLtWY6lKDlTxnFpKHZdpjfF+oE6V4tZ0rTrunHrtZp6Dm0oFrcJalt/5PNeqJz4j1DuZDS0Y6rCBoqDA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.4.0.tgz", + "integrity": "sha512-pzDwezZaLbK371OiJjXDsZJwVOALzFX5M1wEh2Kr0pEApq5AV6bRH/DFbA/zNA7Lck/duyREPQLLvzu2G6hEQQ==", "dependencies": { "pretty-format": "^3.8.0" }, @@ -4869,32 +4866,18 @@ } }, "node_modules/remark-smartypants": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.0.0.tgz", - "integrity": "sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.1.0.tgz", + "integrity": "sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==", "dependencies": { "retext": "^8.1.0", - "retext-smartypants": "^5.1.0", - "unist-util-visit": "^4.1.0" + "retext-smartypants": "^5.2.0", + "unist-util-visit": "^5.0.0" }, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/remark-smartypants/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -5327,11 +5310,11 @@ } }, "node_modules/shiki": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.6.tgz", - "integrity": "sha512-j4pcpvaQWHb42cHeV+W6P+X/VcK7Y2ctvEham6zB8wsuRQroT6cEMIkiUmBU2Nqg2qnHZDH6ZyRdVldcy0l6xw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.7.tgz", + "integrity": "sha512-9kUTMjZtcPH3i7vHunA6EraTPpPOITYTdA5uMrvsJRexktqP0s7P3s9HVK80b4pP42FRVe03D7fT3NmJv2yYhw==", "dependencies": { - "@shikijs/core": "1.1.6" + "@shikijs/core": "1.1.7" } }, "node_modules/signal-exit": { diff --git a/package.json b/package.json index 094e62ae4..a0feb9670 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.2.2", + "version": "4.2.3", "type": "module", "author": "jackyzha0 ", "license": "MIT", @@ -39,7 +39,7 @@ "@napi-rs/simple-git": "0.1.16", "async-mutex": "^0.4.1", "chalk": "^5.3.0", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "cli-spinner": "^0.2.10", "d3": "^7.8.5", "esbuild-sass-plugin": "^2.16.1", @@ -52,13 +52,13 @@ "hast-util-to-string": "^3.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.23.0", + "lightningcss": "^1.24.0", "mdast-util-find-and-replace": "^3.0.1", "mdast-util-to-hast": "^13.1.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.5", - "preact-render-to-string": "^6.3.1", + "preact": "^10.19.6", + "preact-render-to-string": "^6.4.0", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", "reading-time": "^1.5.0", @@ -75,11 +75,11 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", - "remark-smartypants": "^2.0.0", + "remark-smartypants": "^2.1.0", "rfdc": "^1.3.1", "rimraf": "^5.0.5", "serve-handler": "^6.1.5", - "shiki": "^1.1.6", + "shiki": "^1.1.7", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", @@ -95,7 +95,7 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.19", + "@types/node": "^20.11.24", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.5.10", diff --git a/quartz/build.ts b/quartz/build.ts index 3d95f315f..d72b8ddf4 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -185,9 +185,14 @@ async function partialRebuildFromEntrypoint( const emitterGraph = (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null - // emmiter may not define a dependency graph. nothing to update if so if (emitterGraph) { - dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + const existingGraph = dependencies[emitter.name] + if (existingGraph !== null) { + existingGraph.mergeGraph(emitterGraph) + } else { + // might be the first time we're adding a mardown file + dependencies[emitter.name] = emitterGraph + } } } break @@ -224,7 +229,6 @@ async function partialRebuildFromEntrypoint( // EMIT perf.addEvent("rebuild") let emittedFiles = 0 - const destinationsToDelete = new Set() for (const emitter of cfg.plugins.emitters) { const depGraph = dependencies[emitter.name] @@ -264,11 +268,6 @@ async function partialRebuildFromEntrypoint( // and supply [a.md, b.md] to the emitter const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] - if (action === "delete" && upstreams.length === 1) { - // if there's only one upstream, the destination is solely dependent on this file - destinationsToDelete.add(upstreams[0]) - } - const upstreamContent = upstreams // filter out non-markdown files .filter((file) => contentMap.has(file)) @@ -291,14 +290,24 @@ async function partialRebuildFromEntrypoint( console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) // CLEANUP - // delete files that are solely dependent on this file - await rimraf([...destinationsToDelete]) + const destinationsToDelete = new Set() for (const file of toRemove) { // remove from cache contentMap.delete(file) - // remove the node from dependency graphs - Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file)) + Object.values(dependencies).forEach((depGraph) => { + // remove the node from dependency graphs + depGraph?.removeNode(file) + // remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed + const orphanNodes = depGraph?.removeOrphanNodes() + orphanNodes?.forEach((node) => { + // only delete files that are in the output directory + if (node.startsWith(argv.output)) { + destinationsToDelete.add(node) + } + }) + }) } + await rimraf([...destinationsToDelete]) toRemove.clear() release() diff --git a/quartz/cfg.ts b/quartz/cfg.ts index a477db057..2e32b1f80 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -19,6 +19,12 @@ export type Analytics = websiteId: string host?: string } + | { + provider: "goatcounter" + websiteId: string + host?: string + scriptSrc?: string + } export interface GlobalConfiguration { pageTitle: string diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 40ab43a2d..f7ebcc9a2 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -17,6 +17,7 @@ export interface D3Config { opacityScale: number removeTags: string[] showTags: boolean + focusOnHover?: boolean } interface GraphOptions { @@ -37,6 +38,7 @@ const defaultOptions: GraphOptions = { opacityScale: 1, showTags: true, removeTags: [], + focusOnHover: false, }, globalGraph: { drag: true, @@ -50,6 +52,7 @@ const defaultOptions: GraphOptions = { opacityScale: 1, showTags: true, removeTags: [], + focusOnHover: true, }, } diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index 62b77b17b..1e5d232df 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -63,7 +63,7 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Pr class="internal tag-link" href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} > - #{tag} + {tag} ))} diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 549b025d3..d99878db9 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -63,7 +63,7 @@ export default ((userOpts?: Partial) => { class="internal tag-link" href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} > - #{tag} + {tag} ))} diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index 04a483b6c..ba48098bd 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -9,12 +9,11 @@ const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentPro return (
    {tags.map((tag) => { - const display = `#${tag}` const linkDest = baseDir + `/tags/${slugTag(tag)}` return (
  • - {display} + {tag}
  • ) diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx index 276d5e561..4ef1b912c 100644 --- a/quartz/components/pages/404.tsx +++ b/quartz/components/pages/404.tsx @@ -1,7 +1,7 @@ import { i18n } from "../../i18n" -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" -function NotFound({ cfg }: QuartzComponentProps) { +const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => { return (

    404

    diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx index 2e6416f66..8222d786e 100644 --- a/quartz/components/pages/Content.tsx +++ b/quartz/components/pages/Content.tsx @@ -1,7 +1,7 @@ import { htmlToJsx } from "../../util/jsx" -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" -function Content({ fileData, tree }: QuartzComponentProps) { +const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { const content = htmlToJsx(fileData.filePath!, tree) const classes: string[] = fileData.frontmatter?.cssclasses ?? [] const classString = ["popover-hint", ...classes].join(" ") diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index d3f28ddf1..55f1e427d 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import path from "path" import style from "../styles/listPage.scss" @@ -22,7 +22,7 @@ const defaultOptions: FolderContentOptions = { export default ((opts?: Partial) => { const options: FolderContentOptions = { ...defaultOptions, ...opts } - function FolderContent(props: QuartzComponentProps) { + const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const { tree, fileData, allFiles, cfg } = props const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const allPagesInFolder = allFiles.filter((file) => { diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index 22f6bb25d..692585c2b 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import style from "../styles/listPage.scss" import { PageList } from "../PageList" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" @@ -8,7 +8,7 @@ import { htmlToJsx } from "../../util/jsx" import { i18n } from "../../i18n" const numPages = 10 -function TagContent(props: QuartzComponentProps) { +const TagContent: QuartzComponent = (props: QuartzComponentProps) => { const { tree, fileData, allFiles, cfg } = props const slug = fileData.slug @@ -58,7 +58,7 @@ function TagContent(props: QuartzComponentProps) {

    - #{tag} + {tag}

    {content &&

    {content}

    } diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 5394d6f4c..7a97edbc2 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -19,6 +19,7 @@ interface RenderComponents { footer: QuartzComponent } +const headerRegex = new RegExp(/h[1-6]/) export function pageResources( baseDir: FullSlug | RelativeURL, staticResources: StaticResources, @@ -105,18 +106,23 @@ export function renderPage( // header transclude blockRef = blockRef.slice(1) let startIdx = undefined + let startDepth = undefined let endIdx = undefined for (const [i, el] of page.htmlAst.children.entries()) { - if (el.type === "element" && el.tagName.match(/h[1-6]/)) { - if (endIdx) { - break - } + // skip non-headers + if (!(el.type === "element" && el.tagName.match(headerRegex))) continue + const depth = Number(el.tagName.substring(1)) - if (startIdx !== undefined) { - endIdx = i - } else if (el.properties?.id === blockRef) { + // lookin for our blockref + if (startIdx === undefined || startDepth === undefined) { + // skip until we find the blockref that matches + if (el.properties?.id === blockRef) { startIdx = i + startDepth = Number(el.tagName.substring(1)) } + } else if (depth <= startDepth) { + // looking for new header that is same level or higher + endIdx = i } } diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index c991e163e..1c9bb5d64 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -44,6 +44,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { opacityScale, removeTags, showTags, + focusOnHover, } = JSON.parse(graph.dataset["cfg"]!) const data: Map = new Map( @@ -189,6 +190,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) { return 2 + Math.sqrt(numLinks) } + let connectedNodes: SimpleSlug[] = [] + // draw individual nodes const node = graphNode .append("circle") @@ -202,17 +205,25 @@ async function renderGraph(container: string, fullSlug: FullSlug) { window.spaNavigate(new URL(targ, window.location.toString())) }) .on("mouseover", function (_, d) { - const neighbours: SimpleSlug[] = data.get(slug)?.links ?? [] - const neighbourNodes = d3 - .selectAll(".node") - .filter((d) => neighbours.includes(d.id)) const currentId = d.id const linkNodes = d3 .selectAll(".link") .filter((d: any) => d.source.id === currentId || d.target.id === currentId) - // highlight neighbour nodes - neighbourNodes.transition().duration(200).attr("fill", color) + if (focusOnHover) { + // fade out non-neighbour nodes + connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id]) + + d3.selectAll(".link") + .transition() + .duration(200) + .style("opacity", 0.2) + d3.selectAll(".node") + .filter((d) => !connectedNodes.includes(d.id)) + .transition() + .duration(200) + .style("opacity", 0.2) + } // highlight links linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) @@ -231,6 +242,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) { .style("font-size", bigFont + "em") }) .on("mouseleave", function (_, d) { + if (focusOnHover) { + d3.selectAll(".link").transition().duration(200).style("opacity", 1) + d3.selectAll(".node").transition().duration(200).style("opacity", 1) + } const currentId = d.id const linkNodes = d3 .selectAll(".link") diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts index 43eb4024f..062f13e35 100644 --- a/quartz/depgraph.test.ts +++ b/quartz/depgraph.test.ts @@ -39,6 +39,28 @@ describe("DepGraph", () => { }) }) + describe("mergeGraph", () => { + test("merges two graphs", () => { + const graph = new DepGraph() + graph.addEdge("A.md", "A.html") + + const other = new DepGraph() + other.addEdge("B.md", "B.html") + + graph.mergeGraph(other) + + const expected = { + nodes: ["A.md", "A.html", "B.md", "B.html"], + edges: [ + ["A.md", "A.html"], + ["B.md", "B.html"], + ], + } + + assert.deepStrictEqual(graph.export(), expected) + }) + }) + describe("updateIncomingEdgesForNode", () => { test("merges when node exists", () => { // A.md -> B.md -> B.html diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts index 1efad0770..3d048cd83 100644 --- a/quartz/depgraph.ts +++ b/quartz/depgraph.ts @@ -39,12 +39,26 @@ export default class DepGraph { } } + // Remove node and all edges connected to it removeNode(node: T): void { if (this._graph.has(node)) { + // first remove all edges so other nodes don't have references to this node + for (const target of this._graph.get(node)!.outgoing) { + this.removeEdge(node, target) + } + for (const source of this._graph.get(node)!.incoming) { + this.removeEdge(source, node) + } this._graph.delete(node) } } + forEachNode(callback: (node: T) => void): void { + for (const node of this._graph.keys()) { + callback(node) + } + } + hasEdge(from: T, to: T): boolean { return Boolean(this._graph.get(from)?.outgoing.has(to)) } @@ -92,6 +106,15 @@ export default class DepGraph { // DEPENDENCY ALGORITHMS + // Add all nodes and edges from other graph to this graph + mergeGraph(other: DepGraph): void { + other.forEachEdge(([source, target]) => { + this.addNode(source) + this.addNode(target) + this.addEdge(source, target) + }) + } + // For the node provided: // If node does not exist, add it // If an incoming edge was added in other, it is added in this graph @@ -112,6 +135,24 @@ export default class DepGraph { }) } + // Remove all nodes that do not have any incoming or outgoing edges + // A node may be orphaned if the only node pointing to it was removed + removeOrphanNodes(): Set { + let orphanNodes = new Set() + + this.forEachNode((node) => { + if (this.inDegree(node) === 0 && this.outDegree(node) === 0) { + orphanNodes.add(node) + } + }) + + orphanNodes.forEach((node) => { + this.removeNode(node) + }) + + return orphanNodes + } + // Get all leaf nodes (i.e. destination paths) reachable from the node provided // Eg. if the graph is A -> B -> C // D ---^ diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts index 38d356280..b97368d96 100644 --- a/quartz/i18n/index.ts +++ b/quartz/i18n/index.ts @@ -12,6 +12,7 @@ import uk from "./locales/uk-UA" import ru from "./locales/ru-RU" import ko from "./locales/ko-KR" import zh from "./locales/zh-CN" +import vi from "./locales/vi-VN" export const TRANSLATIONS = { "en-US": en, @@ -48,6 +49,7 @@ export const TRANSLATIONS = { "ru-RU": ru, "ko-KR": ko, "zh-CN": zh, + "vi-VN": vi, } as const export const defaultTranslation = "en-US" diff --git a/quartz/i18n/locales/vi-VN.ts b/quartz/i18n/locales/vi-VN.ts new file mode 100644 index 000000000..b72ced4ac --- /dev/null +++ b/quartz/i18n/locales/vi-VN.ts @@ -0,0 +1,83 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Không có tiêu đề", + description: "Không có mô tả được cung cấp", + }, + components: { + callout: { + note: "Ghi Chú", + abstract: "Tóm Tắt", + info: "Thông tin", + todo: "Cần Làm", + tip: "Gợi Ý", + success: "Thành Công", + question: "Nghi Vấn", + warning: "Cảnh Báo", + failure: "Thất Bại", + danger: "Nguy Hiểm", + bug: "Lỗi", + example: "Ví Dụ", + quote: "Trích Dẫn", + }, + backlinks: { + title: "Liên Kết Ngược", + noBacklinksFound: "Không có liên kết ngược được tìm thấy", + }, + themeToggle: { + lightMode: "Sáng", + darkMode: "Tối", + }, + explorer: { + title: "Trong bài này", + }, + footer: { + createdWith: "Được tạo bởi", + }, + graph: { + title: "Biểu Đồ", + }, + recentNotes: { + title: "Bài viết gần đây", + seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`, + linkToOriginal: "Liên Kết Gốc", + }, + search: { + title: "Tìm Kiếm", + searchBarPlaceholder: "Tìm kiếm thông tin", + }, + tableOfContents: { + title: "Bảng Nội Dung", + }, + contentMeta: { + readingTime: ({ minutes }) => `đọc ${minutes} phút`, + }, + }, + pages: { + rss: { + recentNotes: "Những bài gần đây", + lastFewNotes: ({ count }) => `${count} Bài gần đây`, + }, + error: { + title: "Không Tìm Thấy", + notFound: "Trang này được bảo mật hoặc không tồn tại.", + }, + folderContent: { + folder: "Thư Mục", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`, + }, + tagContent: { + tag: "Thẻ", + tagIndex: "Thẻ Mục Lục", + itemsUnderTag: ({ count }) => + count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`, + showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`, + totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 046841689..7d1a01d20 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -120,12 +120,21 @@ function addGlobalPageResources( } else if (cfg.analytics?.provider === "umami") { componentResources.afterDOMLoaded.push(` const umamiScript = document.createElement("script") - umamiScript.src = "${cfg.analytics.host}" ?? "https://analytics.umami.is/script.js" + umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js" umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") umamiScript.async = true document.head.appendChild(umamiScript) `) + } else if (cfg.analytics?.provider === "goatcounter") { + componentResources.afterDOMLoaded.push(` + const goatcounterScript = document.createElement("script") + goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}" + goatcounterScript.async = true + goatcounterScript.setAttribute("data-goatcounter", + "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count") + document.head.appendChild(goatcounterScript) + `) } if (cfg.enableSPA) { diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 6f65ae477..d88d0722a 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -73,7 +73,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) const title = tag === "index" ? i18n(cfg.locale).pages.tagContent.tagIndex - : `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}` + : `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}` return [ tag, defaultProcessedContent({ diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index 884d5b189..b582feea6 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -5,12 +5,19 @@ import { escapeHTML } from "../../util/escape" export interface Options { descriptionLength: number + replaceExternalLinks: boolean } const defaultOptions: Options = { descriptionLength: 150, + replaceExternalLinks: true, } +const urlRegex = new RegExp( + /(https?:\/\/)?(?([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/, + "g", +) + export const Description: QuartzTransformerPlugin | undefined> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { @@ -19,22 +26,42 @@ export const Description: QuartzTransformerPlugin | undefined> return [ () => { return async (tree: HTMLRoot, file) => { - const frontMatterDescription = file.data.frontmatter?.description - const text = escapeHTML(toString(tree)) + let frontMatterDescription = file.data.frontmatter?.description + let text = escapeHTML(toString(tree)) - const desc = frontMatterDescription ?? text - const sentences = desc.replace(/\s+/g, " ").split(".") - let finalDesc = "" - let sentenceIdx = 0 - const len = opts.descriptionLength - while (finalDesc.length < len) { - const sentence = sentences[sentenceIdx] - if (!sentence) break - finalDesc += sentence + "." - sentenceIdx++ + if (opts.replaceExternalLinks) { + frontMatterDescription = frontMatterDescription?.replace( + urlRegex, + "$" + "$", + ) + text = text.replace(urlRegex, "$" + "$") } - file.data.description = finalDesc + const desc = frontMatterDescription ?? text + const sentences = desc.replace(/\s+/g, " ").split(/\.\s/) + const finalDesc: string[] = [] + const len = opts.descriptionLength + let sentenceIdx = 0 + + if (sentences[0] !== undefined && sentences[0].length >= len) { + const firstSentence = sentences[0].split(" ") + while (finalDesc.length < len) { + const sentence = firstSentence[sentenceIdx] + if (!sentence) break + finalDesc.push(sentence) + sentenceIdx++ + } + finalDesc.push("...") + } else { + while (finalDesc.length < len) { + const sentence = sentences[sentenceIdx] + if (!sentence) break + finalDesc.push(sentence.endsWith(".") ? sentence : sentence + ".") + sentenceIdx++ + } + } + + file.data.description = finalDesc.join(" ") file.data.text = text } }, diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index fab7cf894..5058c8b35 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -1,5 +1,5 @@ import { QuartzTransformerPlugin } from "../types" -import { Blockquote, Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" +import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { slug as slugAnchor } from "github-slugger" @@ -17,7 +17,6 @@ import { toHtml } from "hast-util-to-html" import { PhrasingContent } from "mdast-util-find-and-replace/lib" import { capitalize } from "../../util/lang" import { PluggableList } from "unified" -import { ValidCallout, i18n } from "../../i18n" export interface Options { comments: boolean @@ -124,6 +123,7 @@ const tagRegex = new RegExp( ) const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ +const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, @@ -328,7 +328,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin children: [ { type: "text", - value: `#${tag}`, + value: tag, }, ], } @@ -528,12 +528,35 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin last.value = last.value.slice(0, -matches[0].length) const block = matches[0].slice(1) - if (!Object.keys(file.data.blocks!).includes(block)) { - node.properties = { - ...node.properties, - id: block, + if (last.value === "") { + // this is an inline block ref but the actual block + // is the previous element above it + let idx = (index ?? 1) - 1 + while (idx >= 0) { + const element = parent?.children.at(idx) + if (!element) break + if (element.type !== "element") { + idx -= 1 + } else { + if (!Object.keys(file.data.blocks!).includes(block)) { + element.properties = { + ...element.properties, + id: block, + } + file.data.blocks![block] = element + } + return + } + } + } else { + // normal paragraph transclude + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node } - file.data.blocks![block] = node } } } @@ -552,7 +575,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin if (node.tagName === "img" && typeof node.properties.src === "string") { const match = node.properties.src.match(ytLinkRegex) const videoId = match && match[2].length == 11 ? match[2] : null + const playlistId = node.properties.src.match(ytPlaylistLinkRegex)?.[1] if (videoId) { + // YouTube video (with optional playlist) node.tagName = "iframe" node.properties = { class: "external-embed", @@ -560,7 +585,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin frameborder: 0, width: "600px", height: "350px", - src: `https://www.youtube.com/embed/${videoId}`, + src: playlistId + ? `https://www.youtube.com/embed/${videoId}?list=${playlistId}` + : `https://www.youtube.com/embed/${videoId}`, + } + } else if (playlistId) { + // YouTube playlist only. + node.tagName = "iframe" + node.properties = { + class: "external-embed", + allow: "fullscreen", + frameborder: 0, + width: "600px", + height: "350px", + src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`, } } } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 92798a2da..868dfdc79 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -79,6 +79,11 @@ a { border-radius: 0; padding: 0; } + &.tag-link { + &::before { + content: "#"; + } + } } &.external .external-icon { diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts index bd0da5fb0..49cc9cce8 100644 --- a/quartz/util/theme.ts +++ b/quartz/util/theme.ts @@ -9,6 +9,11 @@ export interface ColorScheme { highlight: string } +interface Colors { + lightMode: ColorScheme + darkMode: ColorScheme +} + export interface Theme { typography: { header: string @@ -16,12 +21,11 @@ export interface Theme { code: string } cdnCaching: boolean - colors: { - lightMode: ColorScheme - darkMode: ColorScheme - } + colors: Colors } +export type ThemeKey = keyof Colors + const DEFAULT_SANS_SERIF = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace"