Merge branch 'jackyzha0:v4' into v4

This commit is contained in:
Erdem Özgen 2024-03-08 07:39:50 +03:00 committed by GitHub
commit 3dcd659d8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 470 additions and 176 deletions

View File

@ -46,8 +46,26 @@ jobs:
- name: Ensure Quartz builds, check bundle info - name: Ensure Quartz builds, check bundle info
run: npx quartz build --bundleInfo run: npx quartz build --bundleInfo
- name: Create release tag publish-tag:
uses: Klemensas/action-autotag@stable if: ${{ github.repository == 'jackyzha0/quartz' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with: with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" fetch-depth: 0
tag_prefix: "v" - 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"

View File

@ -28,6 +28,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google 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: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/); - `{ 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)
- `locale`: used for [[i18n]] and date formatting - `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. - `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`. - 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] > [!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. > 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" ```ts title="quartz.config.ts"
transformers: [ filters: [
... ...
Plugin.ExplicitPublish(), Plugin.ExplicitPublish(),
... ...

View File

@ -57,6 +57,15 @@ For example:
- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2 - 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 - 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 ## Customization
Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options. Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options.

View File

@ -61,7 +61,7 @@ export class FileNode {
children: FileNode[] // children of current node children: FileNode[] // children of current node
name: string // last part of slug name: string // last part of slug
displayName: string // what actually should be displayed in the explorer 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 depth: number // depth of current node
... // rest of implementation ... // 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. 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 ### 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`. To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`.

View File

@ -14,6 +14,7 @@ If the frontmatter contains a `description` property, it is used (see [[authorin
This plugin accepts the following configuration options: 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. - `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 ## API

View File

@ -13,6 +13,6 @@ This plugin has no configuration options.
## API ## API
- Category: Emitter - Category: Filter
- Function name: `Plugin.ExplicitPublish()`. - Function name: `Plugin.ExplicitPublish()`.
- Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts). - Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts).

View File

@ -20,7 +20,7 @@ This plugin accepts the following configuration options:
- `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents. - `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents.
- `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks. - `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`. - `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. - `enableVideoEmbed`: If `true` (default), enables the embedding of video files.
- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`. - `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`.

View File

@ -16,5 +16,5 @@ The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`
## API ## API
- Category: Emitter - 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). - Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx).

167
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.2.2", "version": "4.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.2.2", "version": "4.2.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
@ -14,7 +14,7 @@
"@napi-rs/simple-git": "0.1.16", "@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.6.0",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.8.5", "d3": "^7.8.5",
"esbuild-sass-plugin": "^2.16.1", "esbuild-sass-plugin": "^2.16.1",
@ -27,13 +27,13 @@
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.23.0", "lightningcss": "^1.24.0",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.1.0", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.5", "preact": "^10.19.6",
"preact-render-to-string": "^6.3.1", "preact-render-to-string": "^6.4.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
@ -50,11 +50,11 @@
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0", "remark-rehype": "^11.1.0",
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.1.0",
"rfdc": "^1.3.1", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shiki": "^1.1.6", "shiki": "^1.1.7",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
@ -73,7 +73,7 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.19", "@types/node": "^20.11.24",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
@ -743,9 +743,9 @@
} }
}, },
"node_modules/@shikijs/core": { "node_modules/@shikijs/core": {
"version": "1.1.6", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.7.tgz",
"integrity": "sha512-kt9hhvrWTm0EPtRDIsoAZnSsFlIDBVBBI5CQewpA/NZCPin+MOKRXg+JiWc4y+8fZ/v0HzfDhu/UC+OTZGMt7A==" "integrity": "sha512-gTYLUIuD1UbZp/11qozD3fWpUTuMqPSf3svDMMrL0UmlGU7D9dPw/V1FonwAorCUJBltaaESxq90jrSjQyGixg=="
}, },
"node_modules/@sindresorhus/merge-streams": { "node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0", "version": "2.3.0",
@ -1093,9 +1093,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.19", "version": "20.11.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@ -1344,15 +1344,9 @@
} }
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
@ -1365,6 +1359,9 @@
"engines": { "engines": {
"node": ">= 8.10.0" "node": ">= 8.10.0"
}, },
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
@ -3035,9 +3032,9 @@
} }
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.0.tgz",
"integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==", "integrity": "sha512-y36QEEDVx4IM7/yIZNsZJMRREIu26WzTsauIysf5s76YeCmlSbRZS7aC97IGPuoFRnyZ5Wx43OBsQBFB5Ne7ng==",
"dependencies": { "dependencies": {
"detect-libc": "^1.0.3" "detect-libc": "^1.0.3"
}, },
@ -3049,21 +3046,21 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-darwin-arm64": "1.23.0", "lightningcss-darwin-arm64": "1.24.0",
"lightningcss-darwin-x64": "1.23.0", "lightningcss-darwin-x64": "1.24.0",
"lightningcss-freebsd-x64": "1.23.0", "lightningcss-freebsd-x64": "1.24.0",
"lightningcss-linux-arm-gnueabihf": "1.23.0", "lightningcss-linux-arm-gnueabihf": "1.24.0",
"lightningcss-linux-arm64-gnu": "1.23.0", "lightningcss-linux-arm64-gnu": "1.24.0",
"lightningcss-linux-arm64-musl": "1.23.0", "lightningcss-linux-arm64-musl": "1.24.0",
"lightningcss-linux-x64-gnu": "1.23.0", "lightningcss-linux-x64-gnu": "1.24.0",
"lightningcss-linux-x64-musl": "1.23.0", "lightningcss-linux-x64-musl": "1.24.0",
"lightningcss-win32-x64-msvc": "1.23.0" "lightningcss-win32-x64-msvc": "1.24.0"
} }
}, },
"node_modules/lightningcss-darwin-arm64": { "node_modules/lightningcss-darwin-arm64": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.0.tgz",
"integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==", "integrity": "sha512-rTNPkEiynOu4CfGdd5ZfVOQe2gd2idfQd4EfX1l2ZUUwd+2SwSdbb7cG4rlwfnZckbzCAygm85xkpekRE5/wFw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3080,9 +3077,9 @@
} }
}, },
"node_modules/lightningcss-darwin-x64": { "node_modules/lightningcss-darwin-x64": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.0.tgz",
"integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==", "integrity": "sha512-4KCeF2RJjzp9xdGY8zIH68CUtptEg8uz8PfkHvsIdrP4t9t5CIgfDBhiB8AmuO75N6SofdmZexDZIKdy9vA7Ww==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3099,9 +3096,9 @@
} }
}, },
"node_modules/lightningcss-freebsd-x64": { "node_modules/lightningcss-freebsd-x64": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.0.tgz",
"integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==", "integrity": "sha512-FJAYlek1wXuVTsncNU0C6YD41q126dXcIUm97KAccMn9C4s/JfLSqGWT2gIzAblavPFkyGG2gIADTWp3uWfN1g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3118,9 +3115,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm-gnueabihf": { "node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.0.tgz",
"integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==", "integrity": "sha512-N55K6JqzMx7C0hYUu1YmWqhkHwzEJlkQRMA6phY65noO0I1LOAvP4wBIoFWrzRE+O6zL0RmXJ2xppqyTbk3sYw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3137,9 +3134,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-gnu": { "node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.0.tgz",
"integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==", "integrity": "sha512-MqqUB2TpYtFWeBvvf5KExDdClU3YGLW5bHKs50uKKootcvG9KoS7wYwd5UichS+W3mYLc5yXUPGD1DNWbLiYKw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3156,9 +3153,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-musl": { "node_modules/lightningcss-linux-arm64-musl": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.0.tgz",
"integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==", "integrity": "sha512-5wn4d9tFwa5bS1ao9mLexYVJdh3nn09HNIipsII6ZF7z9ZA5J4dOEhMgKoeCl891axTGTUYd8Kxn+Hn3XUSYRQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3175,9 +3172,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-gnu": { "node_modules/lightningcss-linux-x64-gnu": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.0.tgz",
"integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==", "integrity": "sha512-3j5MdTh+LSDF3o6uDwRjRUgw4J+IfDCVtdkUrJvKxL79qBLUujXY7CTe5X3IQDDLKEe/3wu49r8JKgxr0MfjbQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3194,9 +3191,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-musl": { "node_modules/lightningcss-linux-x64-musl": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.0.tgz",
"integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==", "integrity": "sha512-HI+rNnvaLz0o36z6Ki0gyG5igVGrJmzczxA5fznr6eFTj3cHORoR/j2q8ivMzNFR4UKJDkTWUH5LMhacwOHWBA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3213,9 +3210,9 @@
} }
}, },
"node_modules/lightningcss-win32-x64-msvc": { "node_modules/lightningcss-win32-x64-msvc": {
"version": "1.23.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.0.tgz",
"integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==", "integrity": "sha512-oeije/t7OZ5N9vSs6amyW/34wIYoBCpE6HUlsSKcP2SR1CVgx9oKEM00GtQmtqNnYiMIfsSm7+ppMb4NLtD5vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4459,18 +4456,18 @@
} }
}, },
"node_modules/preact": { "node_modules/preact": {
"version": "10.19.5", "version": "10.19.6",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.6.tgz",
"integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==", "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/preact" "url": "https://opencollective.com/preact"
} }
}, },
"node_modules/preact-render-to-string": { "node_modules/preact-render-to-string": {
"version": "6.3.1", "version": "6.4.0",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.3.1.tgz", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.4.0.tgz",
"integrity": "sha512-NQ28WrjLtWY6lKDlTxnFpKHZdpjfF+oE6V4tZ0rTrunHrtZp6Dm0oFrcJalt/5PNeqJz4j1DuZDS0Y6rCBoqDA==", "integrity": "sha512-pzDwezZaLbK371OiJjXDsZJwVOALzFX5M1wEh2Kr0pEApq5AV6bRH/DFbA/zNA7Lck/duyREPQLLvzu2G6hEQQ==",
"dependencies": { "dependencies": {
"pretty-format": "^3.8.0" "pretty-format": "^3.8.0"
}, },
@ -4869,32 +4866,18 @@
} }
}, },
"node_modules/remark-smartypants": { "node_modules/remark-smartypants": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.0.0.tgz", "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.1.0.tgz",
"integrity": "sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA==", "integrity": "sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==",
"dependencies": { "dependencies": {
"retext": "^8.1.0", "retext": "^8.1.0",
"retext-smartypants": "^5.1.0", "retext-smartypants": "^5.2.0",
"unist-util-visit": "^4.1.0" "unist-util-visit": "^5.0.0"
}, },
"engines": { "engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "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": { "node_modules/remark-stringify": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
@ -5327,11 +5310,11 @@
} }
}, },
"node_modules/shiki": { "node_modules/shiki": {
"version": "1.1.6", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.6.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.7.tgz",
"integrity": "sha512-j4pcpvaQWHb42cHeV+W6P+X/VcK7Y2ctvEham6zB8wsuRQroT6cEMIkiUmBU2Nqg2qnHZDH6ZyRdVldcy0l6xw==", "integrity": "sha512-9kUTMjZtcPH3i7vHunA6EraTPpPOITYTdA5uMrvsJRexktqP0s7P3s9HVK80b4pP42FRVe03D7fT3NmJv2yYhw==",
"dependencies": { "dependencies": {
"@shikijs/core": "1.1.6" "@shikijs/core": "1.1.7"
} }
}, },
"node_modules/signal-exit": { "node_modules/signal-exit": {

View File

@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.2.2", "version": "4.2.3",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@ -39,7 +39,7 @@
"@napi-rs/simple-git": "0.1.16", "@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.6.0",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.8.5", "d3": "^7.8.5",
"esbuild-sass-plugin": "^2.16.1", "esbuild-sass-plugin": "^2.16.1",
@ -52,13 +52,13 @@
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.23.0", "lightningcss": "^1.24.0",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.1.0", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.5", "preact": "^10.19.6",
"preact-render-to-string": "^6.3.1", "preact-render-to-string": "^6.4.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
@ -75,11 +75,11 @@
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0", "remark-rehype": "^11.1.0",
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.1.0",
"rfdc": "^1.3.1", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shiki": "^1.1.6", "shiki": "^1.1.7",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
@ -95,7 +95,7 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.19", "@types/node": "^20.11.24",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",

View File

@ -185,9 +185,14 @@ async function partialRebuildFromEntrypoint(
const emitterGraph = const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
// emmiter may not define a dependency graph. nothing to update if so
if (emitterGraph) { 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 break
@ -224,7 +229,6 @@ async function partialRebuildFromEntrypoint(
// EMIT // EMIT
perf.addEvent("rebuild") perf.addEvent("rebuild")
let emittedFiles = 0 let emittedFiles = 0
const destinationsToDelete = new Set<FilePath>()
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
const depGraph = dependencies[emitter.name] const depGraph = dependencies[emitter.name]
@ -264,11 +268,6 @@ async function partialRebuildFromEntrypoint(
// and supply [a.md, b.md] to the emitter // and supply [a.md, b.md] to the emitter
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] 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 const upstreamContent = upstreams
// filter out non-markdown files // filter out non-markdown files
.filter((file) => contentMap.has(file)) .filter((file) => contentMap.has(file))
@ -291,14 +290,24 @@ async function partialRebuildFromEntrypoint(
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
// CLEANUP // CLEANUP
// delete files that are solely dependent on this file const destinationsToDelete = new Set<FilePath>()
await rimraf([...destinationsToDelete])
for (const file of toRemove) { for (const file of toRemove) {
// remove from cache // remove from cache
contentMap.delete(file) contentMap.delete(file)
// remove the node from dependency graphs Object.values(dependencies).forEach((depGraph) => {
Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file)) // 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() toRemove.clear()
release() release()

View File

@ -19,6 +19,12 @@ export type Analytics =
websiteId: string websiteId: string
host?: string host?: string
} }
| {
provider: "goatcounter"
websiteId: string
host?: string
scriptSrc?: string
}
export interface GlobalConfiguration { export interface GlobalConfiguration {
pageTitle: string pageTitle: string

View File

@ -17,6 +17,7 @@ export interface D3Config {
opacityScale: number opacityScale: number
removeTags: string[] removeTags: string[]
showTags: boolean showTags: boolean
focusOnHover?: boolean
} }
interface GraphOptions { interface GraphOptions {
@ -37,6 +38,7 @@ const defaultOptions: GraphOptions = {
opacityScale: 1, opacityScale: 1,
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: false,
}, },
globalGraph: { globalGraph: {
drag: true, drag: true,
@ -50,6 +52,7 @@ const defaultOptions: GraphOptions = {
opacityScale: 1, opacityScale: 1,
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: true,
}, },
} }

View File

@ -63,7 +63,7 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Pr
class="internal tag-link" class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
> >
#{tag} {tag}
</a> </a>
</li> </li>
))} ))}

View File

@ -63,7 +63,7 @@ export default ((userOpts?: Partial<Options>) => {
class="internal tag-link" class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
> >
#{tag} {tag}
</a> </a>
</li> </li>
))} ))}

View File

@ -9,12 +9,11 @@ const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentPro
return ( return (
<ul class={classNames(displayClass, "tags")}> <ul class={classNames(displayClass, "tags")}>
{tags.map((tag) => { {tags.map((tag) => {
const display = `#${tag}`
const linkDest = baseDir + `/tags/${slugTag(tag)}` const linkDest = baseDir + `/tags/${slugTag(tag)}`
return ( return (
<li> <li>
<a href={linkDest} class="internal tag-link"> <a href={linkDest} class="internal tag-link">
{display} {tag}
</a> </a>
</li> </li>
) )

View File

@ -1,7 +1,7 @@
import { i18n } from "../../i18n" 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 ( return (
<article class="popover-hint"> <article class="popover-hint">
<h1>404</h1> <h1>404</h1>

View File

@ -1,7 +1,7 @@
import { htmlToJsx } from "../../util/jsx" 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 content = htmlToJsx(fileData.filePath!, tree)
const classes: string[] = fileData.frontmatter?.cssclasses ?? [] const classes: string[] = fileData.frontmatter?.cssclasses ?? []
const classString = ["popover-hint", ...classes].join(" ") const classString = ["popover-hint", ...classes].join(" ")

View File

@ -1,4 +1,4 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import path from "path" import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
@ -22,7 +22,7 @@ const defaultOptions: FolderContentOptions = {
export default ((opts?: Partial<FolderContentOptions>) => { export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts } const options: FolderContentOptions = { ...defaultOptions, ...opts }
function FolderContent(props: QuartzComponentProps) { const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props const { tree, fileData, allFiles, cfg } = props
const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => { const allPagesInFolder = allFiles.filter((file) => {

View File

@ -1,4 +1,4 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList } from "../PageList" import { PageList } from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
@ -8,7 +8,7 @@ import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
const numPages = 10 const numPages = 10
function TagContent(props: QuartzComponentProps) { const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props const { tree, fileData, allFiles, cfg } = props
const slug = fileData.slug const slug = fileData.slug
@ -58,7 +58,7 @@ function TagContent(props: QuartzComponentProps) {
<div> <div>
<h2> <h2>
<a class="internal tag-link" href={`../tags/${tag}`}> <a class="internal tag-link" href={`../tags/${tag}`}>
#{tag} {tag}
</a> </a>
</h2> </h2>
{content && <p>{content}</p>} {content && <p>{content}</p>}

View File

@ -19,6 +19,7 @@ interface RenderComponents {
footer: QuartzComponent footer: QuartzComponent
} }
const headerRegex = new RegExp(/h[1-6]/)
export function pageResources( export function pageResources(
baseDir: FullSlug | RelativeURL, baseDir: FullSlug | RelativeURL,
staticResources: StaticResources, staticResources: StaticResources,
@ -105,18 +106,23 @@ export function renderPage(
// header transclude // header transclude
blockRef = blockRef.slice(1) blockRef = blockRef.slice(1)
let startIdx = undefined let startIdx = undefined
let startDepth = undefined
let endIdx = undefined let endIdx = undefined
for (const [i, el] of page.htmlAst.children.entries()) { for (const [i, el] of page.htmlAst.children.entries()) {
if (el.type === "element" && el.tagName.match(/h[1-6]/)) { // skip non-headers
if (endIdx) { if (!(el.type === "element" && el.tagName.match(headerRegex))) continue
break const depth = Number(el.tagName.substring(1))
}
if (startIdx !== undefined) { // lookin for our blockref
endIdx = i if (startIdx === undefined || startDepth === undefined) {
} else if (el.properties?.id === blockRef) { // skip until we find the blockref that matches
if (el.properties?.id === blockRef) {
startIdx = i startIdx = i
startDepth = Number(el.tagName.substring(1))
} }
} else if (depth <= startDepth) {
// looking for new header that is same level or higher
endIdx = i
} }
} }

View File

@ -44,6 +44,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
opacityScale, opacityScale,
removeTags, removeTags,
showTags, showTags,
focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!) } = JSON.parse(graph.dataset["cfg"]!)
const data: Map<SimpleSlug, ContentDetails> = new Map( const data: Map<SimpleSlug, ContentDetails> = new Map(
@ -189,6 +190,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
return 2 + Math.sqrt(numLinks) return 2 + Math.sqrt(numLinks)
} }
let connectedNodes: SimpleSlug[] = []
// draw individual nodes // draw individual nodes
const node = graphNode const node = graphNode
.append("circle") .append("circle")
@ -202,17 +205,25 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
window.spaNavigate(new URL(targ, window.location.toString())) window.spaNavigate(new URL(targ, window.location.toString()))
}) })
.on("mouseover", function (_, d) { .on("mouseover", function (_, d) {
const neighbours: SimpleSlug[] = data.get(slug)?.links ?? []
const neighbourNodes = d3
.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => neighbours.includes(d.id))
const currentId = d.id const currentId = d.id
const linkNodes = d3 const linkNodes = d3
.selectAll(".link") .selectAll(".link")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId) .filter((d: any) => d.source.id === currentId || d.target.id === currentId)
// highlight neighbour nodes if (focusOnHover) {
neighbourNodes.transition().duration(200).attr("fill", color) // fade out non-neighbour nodes
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
d3.selectAll<HTMLElement, NodeData>(".link")
.transition()
.duration(200)
.style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.transition()
.duration(200)
.style("opacity", 0.2)
}
// highlight links // highlight links
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) 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") .style("font-size", bigFont + "em")
}) })
.on("mouseleave", function (_, d) { .on("mouseleave", function (_, d) {
if (focusOnHover) {
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
}
const currentId = d.id const currentId = d.id
const linkNodes = d3 const linkNodes = d3
.selectAll(".link") .selectAll(".link")

View File

@ -39,6 +39,28 @@ describe("DepGraph", () => {
}) })
}) })
describe("mergeGraph", () => {
test("merges two graphs", () => {
const graph = new DepGraph<string>()
graph.addEdge("A.md", "A.html")
const other = new DepGraph<string>()
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", () => { describe("updateIncomingEdgesForNode", () => {
test("merges when node exists", () => { test("merges when node exists", () => {
// A.md -> B.md -> B.html // A.md -> B.md -> B.html

View File

@ -39,12 +39,26 @@ export default class DepGraph<T> {
} }
} }
// Remove node and all edges connected to it
removeNode(node: T): void { removeNode(node: T): void {
if (this._graph.has(node)) { 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) 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 { hasEdge(from: T, to: T): boolean {
return Boolean(this._graph.get(from)?.outgoing.has(to)) return Boolean(this._graph.get(from)?.outgoing.has(to))
} }
@ -92,6 +106,15 @@ export default class DepGraph<T> {
// DEPENDENCY ALGORITHMS // DEPENDENCY ALGORITHMS
// Add all nodes and edges from other graph to this graph
mergeGraph(other: DepGraph<T>): void {
other.forEachEdge(([source, target]) => {
this.addNode(source)
this.addNode(target)
this.addEdge(source, target)
})
}
// For the node provided: // For the node provided:
// If node does not exist, add it // If node does not exist, add it
// If an incoming edge was added in other, it is added in this graph // If an incoming edge was added in other, it is added in this graph
@ -112,6 +135,24 @@ export default class DepGraph<T> {
}) })
} }
// 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<T> {
let orphanNodes = new Set<T>()
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 // Get all leaf nodes (i.e. destination paths) reachable from the node provided
// Eg. if the graph is A -> B -> C // Eg. if the graph is A -> B -> C
// D ---^ // D ---^

View File

@ -12,6 +12,7 @@ import uk from "./locales/uk-UA"
import ru from "./locales/ru-RU" import ru from "./locales/ru-RU"
import ko from "./locales/ko-KR" import ko from "./locales/ko-KR"
import zh from "./locales/zh-CN" import zh from "./locales/zh-CN"
import vi from "./locales/vi-VN"
export const TRANSLATIONS = { export const TRANSLATIONS = {
"en-US": en, "en-US": en,
@ -48,6 +49,7 @@ export const TRANSLATIONS = {
"ru-RU": ru, "ru-RU": ru,
"ko-KR": ko, "ko-KR": ko,
"zh-CN": zh, "zh-CN": zh,
"vi-VN": vi,
} as const } as const
export const defaultTranslation = "en-US" export const defaultTranslation = "en-US"

View File

@ -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

View File

@ -120,12 +120,21 @@ function addGlobalPageResources(
} else if (cfg.analytics?.provider === "umami") { } else if (cfg.analytics?.provider === "umami") {
componentResources.afterDOMLoaded.push(` componentResources.afterDOMLoaded.push(`
const umamiScript = document.createElement("script") 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.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
umamiScript.async = true umamiScript.async = true
document.head.appendChild(umamiScript) 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) { if (cfg.enableSPA) {

View File

@ -73,7 +73,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
const title = const title =
tag === "index" tag === "index"
? i18n(cfg.locale).pages.tagContent.tagIndex ? i18n(cfg.locale).pages.tagContent.tagIndex
: `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}` : `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}`
return [ return [
tag, tag,
defaultProcessedContent({ defaultProcessedContent({

View File

@ -5,12 +5,19 @@ import { escapeHTML } from "../../util/escape"
export interface Options { export interface Options {
descriptionLength: number descriptionLength: number
replaceExternalLinks: boolean
} }
const defaultOptions: Options = { const defaultOptions: Options = {
descriptionLength: 150, descriptionLength: 150,
replaceExternalLinks: true,
} }
const urlRegex = new RegExp(
/(https?:\/\/)?(?<domain>([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?<path>[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/,
"g",
)
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
@ -19,22 +26,42 @@ export const Description: QuartzTransformerPlugin<Partial<Options> | undefined>
return [ return [
() => { () => {
return async (tree: HTMLRoot, file) => { return async (tree: HTMLRoot, file) => {
const frontMatterDescription = file.data.frontmatter?.description let frontMatterDescription = file.data.frontmatter?.description
const text = escapeHTML(toString(tree)) let text = escapeHTML(toString(tree))
const desc = frontMatterDescription ?? text if (opts.replaceExternalLinks) {
const sentences = desc.replace(/\s+/g, " ").split(".") frontMatterDescription = frontMatterDescription?.replace(
let finalDesc = "" urlRegex,
let sentenceIdx = 0 "$<domain>" + "$<path>",
const len = opts.descriptionLength )
while (finalDesc.length < len) { text = text.replace(urlRegex, "$<domain>" + "$<path>")
const sentence = sentences[sentenceIdx]
if (!sentence) break
finalDesc += sentence + "."
sentenceIdx++
} }
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 file.data.text = text
} }
}, },

View File

@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types" 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 { Element, Literal, Root as HtmlRoot } from "hast"
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { slug as slugAnchor } from "github-slugger" 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 { PhrasingContent } from "mdast-util-find-and-replace/lib"
import { capitalize } from "../../util/lang" import { capitalize } from "../../util/lang"
import { PluggableList } from "unified" import { PluggableList } from "unified"
import { ValidCallout, i18n } from "../../i18n"
export interface Options { export interface Options {
comments: boolean comments: boolean
@ -124,6 +123,7 @@ const tagRegex = new RegExp(
) )
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g")
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ 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 videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
const wikilinkImageEmbedRegex = new RegExp( const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/, /^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
@ -328,7 +328,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
children: [ children: [
{ {
type: "text", type: "text",
value: `#${tag}`, value: tag,
}, },
], ],
} }
@ -528,12 +528,35 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
last.value = last.value.slice(0, -matches[0].length) last.value = last.value.slice(0, -matches[0].length)
const block = matches[0].slice(1) const block = matches[0].slice(1)
if (!Object.keys(file.data.blocks!).includes(block)) { if (last.value === "") {
node.properties = { // this is an inline block ref but the actual block
...node.properties, // is the previous element above it
id: block, 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<Partial<Options>
if (node.tagName === "img" && typeof node.properties.src === "string") { if (node.tagName === "img" && typeof node.properties.src === "string") {
const match = node.properties.src.match(ytLinkRegex) const match = node.properties.src.match(ytLinkRegex)
const videoId = match && match[2].length == 11 ? match[2] : null const videoId = match && match[2].length == 11 ? match[2] : null
const playlistId = node.properties.src.match(ytPlaylistLinkRegex)?.[1]
if (videoId) { if (videoId) {
// YouTube video (with optional playlist)
node.tagName = "iframe" node.tagName = "iframe"
node.properties = { node.properties = {
class: "external-embed", class: "external-embed",
@ -560,7 +585,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
frameborder: 0, frameborder: 0,
width: "600px", width: "600px",
height: "350px", 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}`,
} }
} }
} }

View File

@ -79,6 +79,11 @@ a {
border-radius: 0; border-radius: 0;
padding: 0; padding: 0;
} }
&.tag-link {
&::before {
content: "#";
}
}
} }
&.external .external-icon { &.external .external-icon {

View File

@ -9,6 +9,11 @@ export interface ColorScheme {
highlight: string highlight: string
} }
interface Colors {
lightMode: ColorScheme
darkMode: ColorScheme
}
export interface Theme { export interface Theme {
typography: { typography: {
header: string header: string
@ -16,12 +21,11 @@ export interface Theme {
code: string code: string
} }
cdnCaching: boolean cdnCaching: boolean
colors: { colors: Colors
lightMode: ColorScheme
darkMode: ColorScheme
}
} }
export type ThemeKey = keyof Colors
const DEFAULT_SANS_SERIF = const DEFAULT_SANS_SERIF =
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace"