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

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: '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: '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(),
...

View File

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

View File

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

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:
- `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

View File

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

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.
- `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`.

View File

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

167
package-lock.json generated
View File

@ -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": {

View File

@ -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 <j.zhao2k19@gmail.com>",
"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",

View File

@ -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<FilePath>()
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<FilePath>()
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()

View File

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

View File

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

View File

@ -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}
</a>
</li>
))}

View File

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

View File

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

View File

@ -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 (
<article class="popover-hint">
<h1>404</h1>

View File

@ -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(" ")

View File

@ -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<FolderContentOptions>) => {
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) => {

View File

@ -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) {
<div>
<h2>
<a class="internal tag-link" href={`../tags/${tag}`}>
#{tag}
{tag}
</a>
</h2>
{content && <p>{content}</p>}

View File

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

View File

@ -44,6 +44,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
opacityScale,
removeTags,
showTags,
focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!)
const data: Map<SimpleSlug, ContentDetails> = 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<HTMLElement, NodeData>(".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<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
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<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 linkNodes = d3
.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", () => {
test("merges when node exists", () => {
// 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 {
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<T> {
// 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:
// 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<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
// Eg. if the graph is A -> B -> C
// D ---^

View File

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

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") {
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) {

View File

@ -73,7 +73,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (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({

View File

@ -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?:\/\/)?(?<domain>([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?<path>[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/,
"g",
)
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
@ -19,22 +26,42 @@ export const Description: QuartzTransformerPlugin<Partial<Options> | 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,
"$<domain>" + "$<path>",
)
text = text.replace(urlRegex, "$<domain>" + "$<path>")
}
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
}
},

View File

@ -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(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
@ -328,7 +328,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
children: [
{
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)
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<Partial<Options>
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<Partial<Options>
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}`,
}
}
}

View File

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

View File

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