This commit is contained in:
wych 2024-04-03 14:27:24 -05:00
commit df0fbec117
39 changed files with 950 additions and 268 deletions

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

@ -39,6 +39,17 @@ a & b & c
\end{bmatrix}
$$
$$
\begin{array}{rll}
E \psi &= H\psi & \text{Expanding the Hamiltonian Operator} \\
&= -\frac{\hbar^2}{2m}\frac{\partial^2}{\partial x^2} \psi + \frac{1}{2}m\omega x^2 \psi & \text{Using the ansatz $\psi(x) = e^{-kx^2}f(x)$, hoping to cancel the $x^2$ term} \\
&= -\frac{\hbar^2}{2m} [4k^2x^2f(x)+2(-2kx)f'(x) + f''(x)]e^{-kx^2} + \frac{1}{2}m\omega x^2 f(x)e^{-kx^2} &\text{Removing the $e^{-kx^2}$ term from both sides} \\
& \Downarrow \\
Ef(x) &= -\frac{\hbar^2}{2m} [4k^2x^2f(x)-4kxf'(x) + f''(x)] + \frac{1}{2}m\omega x^2 f(x) & \text{Choosing $k=\frac{im}{2}\sqrt{\frac{\omega}{\hbar}}$ to cancel the $x^2$ term, via $-\frac{\hbar^2}{2m}4k^2=\frac{1}{2}m \omega$} \\
&= -\frac{\hbar^2}{2m} [-4kxf'(x) + f''(x)] \\
\end{array}
$$
> [!warn]
> Due to limitations in the [underlying parsing library](https://github.com/remarkjs/remark-math), block math in Quartz requires the `$$` delimiters to be on newlines like above.

View File

@ -250,3 +250,21 @@ server {
}
}
```
### Using Caddy
Here's and example of how to do this with Caddy:
```caddy title="Caddyfile"
example.com {
root * /path/to/quartz/public
try_files {path} {path}.html {path}/ =404
file_server
encode gzip
handle_errors {
rewrite * /{err.status_code}.html
file_server
}
}
```

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

@ -26,5 +26,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
- [🪴Aster's notebook](https://notes.asterhu.com)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

435
package-lock.json generated
View File

@ -12,7 +12,7 @@
"@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.3",
"@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1",
"async-mutex": "^0.5.0",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"cli-spinner": "^0.2.10",
@ -27,17 +27,18 @@
"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.1",
"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.6",
"preact-render-to-string": "^6.3.1",
"preact": "^10.20.1",
"preact-render-to-string": "^6.4.0",
"pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-citation": "^2.0.0",
"rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.13.0",
@ -50,11 +51,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.2.3",
"source-map-support": "^0.5.21",
"to-vfile": "^8.0.0",
"toml": "^3.0.0",
@ -73,7 +74,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.29",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10",
@ -81,7 +82,7 @@
"esbuild": "^0.19.9",
"prettier": "^3.2.4",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
},
"engines": {
"node": ">=18.14",
@ -98,6 +99,82 @@
"is-potential-custom-element-name": "^1.0.1"
}
},
"node_modules/@citation-js/core": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.9.tgz",
"integrity": "sha512-fSbkB32JayDChZnAYC/kB+sWHRvxxL7ibVetyBOyzOc+5aCnjb6UVsbcfhnkOIEyAMoRRvWDyFmakEoTtA5ttQ==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2",
"fetch-ponyfill": "^7.1.0",
"sync-fetch": "^0.4.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@citation-js/date": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz",
"integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@citation-js/name": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz",
"integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/@citation-js/plugin-bibjson": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.9.tgz",
"integrity": "sha512-YNCWIrkhqZ3cZKewHkLBixABo2PvOWnU+8dBx6KfN47ysdECR76xENe86YYpJ0ska2D5ZnTP0jKZIrUHQoxYfQ==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@citation-js/core": "^0.7.0"
}
},
"node_modules/@citation-js/plugin-bibtex": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.9.tgz",
"integrity": "sha512-gIJpCd6vmmTOcRfDrSOjtoNhw2Mi94UwFxmgJ7GwkXyTYcNheW5VlMMo1tlqjakJGARQ0eOsKcI57gSPqJSS2g==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2",
"moo": "^0.5.1"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@citation-js/core": "^0.7.0"
}
},
"node_modules/@citation-js/plugin-csl": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.9.tgz",
"integrity": "sha512-mbD7CnUiPOuVnjeJwo+d0RGUcY0PE8n01gHyjq0qpTeS42EGmQ9+LzqfsTUVWWBndTwc6zLRuIF1qFAUHKE4oA==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"citeproc": "^2.4.6"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@citation-js/core": "^0.7.0"
}
},
"node_modules/@clack/core": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.3.tgz",
@ -743,9 +820,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.2.3",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.2.3.tgz",
"integrity": "sha512-SM+aiQVaEK2P53dEcsvhq9+LJPr0rzwezHbMQhHaSrPN4OlOB4vp1qTdhVEKfMg6atdq8s9ZotWW/CSCzWftwg=="
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
@ -1093,9 +1170,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.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.29.tgz",
"integrity": "sha512-P99thMkD/1YkCvAtOd6/zGedKNA0p2fj4ZpjCzcNiSCBWgm3cNRTBfa/qjFnsKkkojxu4vVLtWpesnZ9+ap+gA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@ -1213,9 +1290,9 @@
}
},
"node_modules/async-mutex": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
"integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"dependencies": {
"tslib": "^2.4.0"
}
@ -1239,6 +1316,25 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@ -1274,6 +1370,29 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -1366,6 +1485,11 @@
"fsevents": "~2.3.2"
}
},
"node_modules/citeproc": {
"version": "2.4.63",
"resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz",
"integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="
},
"node_modules/cli-spinner": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz",
@ -1497,6 +1621,14 @@
"node": ">= 0.6"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2172,6 +2304,52 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/fetch-ponyfill": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz",
"integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==",
"dependencies": {
"node-fetch": "~2.6.1"
}
},
"node_modules/fetch-ponyfill/node_modules/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/fetch-ponyfill/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/fetch-ponyfill/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/fetch-ponyfill/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -2748,6 +2926,25 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@ -3032,9 +3229,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.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.1.tgz",
"integrity": "sha512-kUpHOLiH5GB0ERSv4pxqlL0RYKnOXtgGtVe7shDGfhS0AZ4D1ouKFYAcLcZhql8aMspDNzaUCumGHZ78tb2fTg==",
"dependencies": {
"detect-libc": "^1.0.3"
},
@ -3046,21 +3243,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.1",
"lightningcss-darwin-x64": "1.24.1",
"lightningcss-freebsd-x64": "1.24.1",
"lightningcss-linux-arm-gnueabihf": "1.24.1",
"lightningcss-linux-arm64-gnu": "1.24.1",
"lightningcss-linux-arm64-musl": "1.24.1",
"lightningcss-linux-x64-gnu": "1.24.1",
"lightningcss-linux-x64-musl": "1.24.1",
"lightningcss-win32-x64-msvc": "1.24.1"
}
},
"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.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.1.tgz",
"integrity": "sha512-1jQ12jBy+AE/73uGQWGSafK5GoWgmSiIQOGhSEXiFJSZxzV+OXIx+a9h2EYHxdJfX864M+2TAxWPWb0Vv+8y4w==",
"cpu": [
"arm64"
],
@ -3077,9 +3274,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.1.tgz",
"integrity": "sha512-R4R1d7VVdq2mG4igMU+Di8GPf0b64ZLnYVkubYnGG0Qxq1KaXQtAzcLI43EkpnoWvB/kUg8JKCWH4S13NfiLcQ==",
"cpu": [
"x64"
],
@ -3096,9 +3293,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.1.tgz",
"integrity": "sha512-z6NberUUw5ALES6Ixn2shmjRRrM1cmEn1ZQPiM5IrZ6xHHL5a1lPin9pRv+w6eWfcrEo+qGG6R9XfJrpuY3e4g==",
"cpu": [
"x64"
],
@ -3115,9 +3312,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.1.tgz",
"integrity": "sha512-NLQLnBQW/0sSg74qLNI8F8QKQXkNg4/ukSTa+XhtkO7v3BnK19TS1MfCbDHt+TTdSgNEBv0tubRuapcKho2EWw==",
"cpu": [
"arm"
],
@ -3134,9 +3331,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.1.tgz",
"integrity": "sha512-AQxWU8c9E9JAjAi4Qw9CvX2tDIPjgzCTrZCSXKELfs4mCwzxRkHh2RCxX8sFK19RyJoJAjA/Kw8+LMNRHS5qEg==",
"cpu": [
"arm64"
],
@ -3153,9 +3350,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.1.tgz",
"integrity": "sha512-JCgH/SrNrhqsguUA0uJUM1PvN5+dVuzPIlXcoWDHSv2OU/BWlj2dUYr3XNzEw748SmNZPfl2NjQrAdzaPOn1lA==",
"cpu": [
"arm64"
],
@ -3172,9 +3369,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.1.tgz",
"integrity": "sha512-TYdEsC63bHV0h47aNRGN3RiK7aIeco3/keN4NkoSQ5T8xk09KHuBdySltWAvKLgT8JvR+ayzq8ZHnL1wKWY0rw==",
"cpu": [
"x64"
],
@ -3191,9 +3388,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.1.tgz",
"integrity": "sha512-HLfzVik3RToot6pQ2Rgc3JhfZkGi01hFetHt40HrUMoeKitLoqUUT5owM6yTZPTytTUW9ukLBJ1pc3XNMSvlLw==",
"cpu": [
"x64"
],
@ -3210,9 +3407,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.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.1.tgz",
"integrity": "sha512-joEupPjYJ7PjZtDsS5lzALtlAudAbgIBMGJPNeFe5HfdmJXFd13ECmEM+5rXNxYVMRHua2w8132R6ab5Z6K9Ow==",
"cpu": [
"x64"
],
@ -4316,6 +4513,11 @@
"resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
"integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -4333,6 +4535,44 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -4456,18 +4696,18 @@
}
},
"node_modules/preact": {
"version": "10.19.6",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.6.tgz",
"integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==",
"version": "10.20.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.20.1.tgz",
"integrity": "sha512-JIFjgFg9B2qnOoGiYMVBtrcFxHqn+dNXbq76bVmcaHYJFYR4lW67AOcXgAYQQTDYXDOg/kTZrKPNCdRgJ2UJmw==",
"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"
},
@ -4598,6 +4838,27 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-citation": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/rehype-citation/-/rehype-citation-2.0.0.tgz",
"integrity": "sha512-rGawTBI8SJA1Y4IRyROvpYF6oXBVNFXlJYHIJ2jJH3HgeuCbAC9AO8wE/NMPLDOPQ8+Q8QkZm93fKsnUNbvwZA==",
"dependencies": {
"@citation-js/core": "^0.7.1",
"@citation-js/date": "^0.5.1",
"@citation-js/name": "^0.4.2",
"@citation-js/plugin-bibjson": "^0.7.2",
"@citation-js/plugin-bibtex": "^0.7.2",
"@citation-js/plugin-csl": "^0.7.2",
"citeproc": "^2.4.63",
"cross-fetch": "^4.0.0",
"hast-util-from-dom": "^5.0.0",
"hast-util-from-parse5": "^8.0.1",
"js-yaml": "^4.1.0",
"parse5": "^7.1.2",
"unified": "^11.0.0",
"unist-util-visit": "^5.0.0"
}
},
"node_modules/rehype-katex": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.0.tgz",
@ -4866,32 +5127,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",
@ -5324,11 +5571,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.2.3",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.2.3.tgz",
"integrity": "sha512-+v7lO5cJMeV2N2ySK4l+51YX3wTh5I49SLjAOs1ch1DbUfeEytU1Ac9KaZPoZJCVBGycDZ09OBQN5nbcPFc5FQ==",
"dependencies": {
"@shikijs/core": "1.1.6"
"@shikijs/core": "1.2.3"
}
},
"node_modules/signal-exit": {
@ -5551,6 +5798,18 @@
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"node_modules/sync-fetch": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz",
"integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==",
"dependencies": {
"buffer": "^5.7.1",
"node-fetch": "^2.6.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -5663,9 +5922,9 @@
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@ -37,7 +37,7 @@
"@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.3",
"@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1",
"async-mutex": "^0.5.0",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"cli-spinner": "^0.2.10",
@ -52,17 +52,18 @@
"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.1",
"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.6",
"preact-render-to-string": "^6.3.1",
"preact": "^10.20.1",
"preact-render-to-string": "^6.4.0",
"pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-citation": "^2.0.0",
"rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.13.0",
@ -75,11 +76,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.2.3",
"source-map-support": "^0.5.21",
"to-vfile": "^8.0.0",
"toml": "^3.0.0",
@ -95,7 +96,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.29",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10",
@ -103,6 +104,6 @@
"esbuild": "^0.19.9",
"prettier": "^3.2.4",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
}
}

View File

@ -19,6 +19,7 @@ const config: QuartzConfig = {
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created",
theme: {
fontOrigin: "googleFonts",
cdnCaching: true,
typography: {
header: "Schibsted Grotesk",
@ -72,7 +73,7 @@ const config: QuartzConfig = {
filters: [Plugin.RemoveDrafts()],
emitters: [
Plugin.AliasRedirects(),
Plugin.ComponentResources({ fontOrigin: "googleFonts" }),
Plugin.ComponentResources(),
Plugin.ContentPage(),
Plugin.FolderPage(),
Plugin.TagPage(),

View File

@ -309,6 +309,8 @@ async function partialRebuildFromEntrypoint(
}
await rimraf([...destinationsToDelete])
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
toRemove.clear()
release()
clientRefresh()

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

@ -3,16 +3,20 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
import { JSX } from "preact"
import style from "./styles/contentMeta.scss"
interface ContentMetaOptions {
/**
* Whether to display reading time
*/
showReadingTime: boolean
showComma: boolean
}
const defaultOptions: ContentMetaOptions = {
showReadingTime: true,
showComma: true,
}
export default ((opts?: Partial<ContentMetaOptions>) => {
@ -23,7 +27,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
const text = fileData.text
if (text) {
const segments: string[] = []
const segments: (string | JSX.Element)[] = []
if (fileData.dates) {
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
@ -38,17 +42,19 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
segments.push(displayedTime)
}
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
return (
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
{segmentsElements}
</p>
)
} else {
return null
}
}
ContentMetadata.css = `
.content-meta {
margin-top: 0;
color: var(--gray);
}
`
ContentMetadata.css = style
return ContentMetadata
}) satisfies QuartzComponentConstructor

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

@ -1,6 +1,7 @@
import { i18n } from "../i18n"
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources"
import { googleFontHref } from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default (() => {
@ -21,10 +22,11 @@ export default (() => {
<head>
<title>{title}</title>
<meta charSet="utf-8" />
{cfg.theme.cdnCaching && (
{cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
</>
)}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

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

@ -47,9 +47,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
return (
<div class={classes}>
<article>
<p>{content}</p>
</article>
<article>{content}</article>
<div class="page-listing">
{options.showFolderCount && (
<p>

View File

@ -52,13 +52,19 @@ const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
allFiles: pages,
}
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`)[0]
const content = contentPage?.description
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
const root = contentPage?.htmlAst
const content =
!root || root?.children.length === 0
? contentPage?.description
: htmlToJsx(contentPage.filePath!, root)
return (
<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,24 @@ 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 = depth
}
} else if (depth <= startDepth) {
// looking for new header that is same level or higher
endIdx = i
break
}
}
@ -203,7 +210,7 @@ export function renderPage(
</div>
)
const lang = componentData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
const doc = (
<html lang={lang}>
<Head {...componentData} />

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

@ -21,6 +21,7 @@ let index = new FlexSearch.Document<Item>({
encode: encoder,
document: {
id: "id",
tag: "tags",
index: [
{
field: "title",
@ -405,11 +406,33 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
if (searchType === "tags") {
searchResults = await index.searchAsync({
query: currentSearchTerm.substring(1),
limit: numSearchResults,
index: ["tags"],
})
currentSearchTerm = currentSearchTerm.substring(1).trim()
const separatorIndex = currentSearchTerm.indexOf(" ")
if (separatorIndex != -1) {
// search by title and content index and then filter by tag (implemented in flexsearch)
const tag = currentSearchTerm.substring(0, separatorIndex)
const query = currentSearchTerm.substring(separatorIndex + 1).trim()
searchResults = await index.searchAsync({
query: query,
// return at least 10000 documents, so it is enough to filter them by tag (implemented in flexsearch)
limit: Math.max(numSearchResults, 10000),
index: ["title", "content"],
tag: tag,
})
for (let searchResult of searchResults) {
searchResult.result = searchResult.result.slice(0, numSearchResults)
}
// set search type to basic and remove tag from term for proper highlightning and scroll
searchType = "basic"
currentSearchTerm = query
} else {
// default search by tags index
searchResults = await index.searchAsync({
query: currentSearchTerm,
limit: numSearchResults,
index: ["tags"],
})
}
} else if (searchType === "basic") {
searchResults = await index.searchAsync({
query: currentSearchTerm,

View File

@ -0,0 +1,14 @@
.content-meta {
margin-top: 0;
color: var(--gray);
&[show-comma="true"] {
> span:not(:last-child) {
margin-right: 8px;
&::after {
content: ",";
}
}
}
}

View File

@ -13,6 +13,8 @@ import ru from "./locales/ru-RU"
import ko from "./locales/ko-KR"
import zh from "./locales/zh-CN"
import vi from "./locales/vi-VN"
import pt from "./locales/pt-BR"
import hu from "./locales/hu-HU"
export const TRANSLATIONS = {
"en-US": en,
@ -50,6 +52,8 @@ export const TRANSLATIONS = {
"ko-KR": ko,
"zh-CN": zh,
"vi-VN": vi,
"pt-BR": pt,
"hu-HU": hu,
} as const
export const defaultTranslation = "en-US"

View File

@ -54,7 +54,7 @@ export default {
title: "Table des Matières",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
readingTime: ({ minutes }) => `${minutes} min de lecture`,
},
},
pages: {
@ -63,7 +63,7 @@ export default {
lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
},
error: {
title: "Pas trouvé",
title: "Introuvable",
notFound: "Cette page est soit privée, soit elle n'existe pas.",
},
folderContent: {

View File

@ -0,0 +1,81 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Névtelen",
description: "Nincs leírás",
},
components: {
callout: {
note: "Jegyzet",
abstract: "Abstract",
info: "Információ",
todo: "Tennivaló",
tip: "Tipp",
success: "Siker",
question: "Kérdés",
warning: "Figyelmeztetés",
failure: "Hiba",
danger: "Veszély",
bug: "Bug",
example: "Példa",
quote: "Idézet",
},
backlinks: {
title: "Visszautalások",
noBacklinksFound: "Nincs visszautalás",
},
themeToggle: {
lightMode: "Világos mód",
darkMode: "Sötét mód",
},
explorer: {
title: "Fájlböngésző",
},
footer: {
createdWith: "Készítve ezzel:",
},
graph: {
title: "Grafikonnézet",
},
recentNotes: {
title: "Legutóbbi jegyzetek",
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
linkToOriginal: "Hivatkozás az eredetire",
},
search: {
title: "Keresés",
searchBarPlaceholder: "Keress valamire",
},
tableOfContents: {
title: "Tartalomjegyzék",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} perces olvasás`,
},
},
pages: {
rss: {
recentNotes: "Legutóbbi jegyzetek",
lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`,
},
error: {
title: "Nem található",
notFound: "Ez a lap vagy privát vagy nem létezik.",
},
folderContent: {
folder: "Mappa",
itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`,
},
tagContent: {
tag: "Címke",
tagIndex: "Címke index",
itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`,
showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`,
totalTags: ({ count }) => `Összesen ${count} címke található.`,
},
},
} as const satisfies Translation

View File

@ -0,0 +1,83 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Sem título",
description: "Sem descrição",
},
components: {
callout: {
note: "Nota",
abstract: "Abstrato",
info: "Info",
todo: "Pendência",
tip: "Dica",
success: "Sucesso",
question: "Pergunta",
warning: "Aviso",
failure: "Falha",
danger: "Perigo",
bug: "Bug",
example: "Exemplo",
quote: "Citação",
},
backlinks: {
title: "Backlinks",
noBacklinksFound: "Sem backlinks encontrados",
},
themeToggle: {
lightMode: "Tema claro",
darkMode: "Tema escuro",
},
explorer: {
title: "Explorador",
},
footer: {
createdWith: "Criado com",
},
graph: {
title: "Visão de gráfico",
},
recentNotes: {
title: "Notas recentes",
seeRemainingMore: ({ remaining }) => `Veja mais ${remaining}`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`,
linkToOriginal: "Link ao original",
},
search: {
title: "Pesquisar",
searchBarPlaceholder: "Pesquisar por algo",
},
tableOfContents: {
title: "Sumário",
},
contentMeta: {
readingTime: ({ minutes }) => `Leitura de ${minutes} min`,
},
},
pages: {
rss: {
recentNotes: "Notas recentes",
lastFewNotes: ({ count }) => `Últimas ${count} notas`,
},
error: {
title: "Não encontrado",
notFound: "Esta página é privada ou não existe.",
},
folderContent: {
folder: "Arquivo",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 item mneste arquivo." : `${count} items neste arquivo.`,
},
tagContent: {
tag: "Tag",
tagIndex: "Sumário de Tags",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`,
showingFirst: ({ count }) => `Mostrando as ${count} primeiras tags.`,
totalTags: ({ count }) => `Encontradas ${count} tags.`,
},
},
} as const satisfies Translation

View File

@ -8,7 +8,6 @@ import popoverScript from "../../components/scripts/popover.inline"
import styles from "../../styles/custom.scss"
import popoverStyle from "../../components/styles/popover.scss"
import { BuildCtx } from "../../util/ctx"
import { StaticResources } from "../../util/resources"
import { QuartzComponent } from "../../components/types"
import { googleFontHref, joinStyles } from "../../util/theme"
import { Features, transform } from "lightningcss"
@ -69,13 +68,8 @@ async function joinScripts(scripts: string[]): Promise<string> {
return res.code
}
function addGlobalPageResources(
ctx: BuildCtx,
staticResources: StaticResources,
componentResources: ComponentResources,
) {
function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) {
const cfg = ctx.cfg.configuration
const reloadScript = ctx.argv.serve
// popovers
if (cfg.enablePopovers) {
@ -85,12 +79,12 @@ function addGlobalPageResources(
if (cfg.analytics?.provider === "google") {
const tagId = cfg.analytics.tagId
staticResources.js.push({
src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
contentType: "external",
loadTime: "afterDOMReady",
})
componentResources.afterDOMLoaded.push(`
const gtagScript = document.createElement("script")
gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}"
gtagScript.async = true
document.head.appendChild(gtagScript)
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("js", new Date());
@ -126,6 +120,15 @@ function addGlobalPageResources(
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) {
@ -138,115 +141,72 @@ function addGlobalPageResources(
document.dispatchEvent(event)
`)
}
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
if (ctx.argv.remoteDevHost) {
wsUrl = `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}`
}
if (reloadScript) {
staticResources.js.push({
loadTime: "afterDOMReady",
contentType: "inline",
script: `
const socket = new WebSocket('${wsUrl}')
// reload(true) ensures resources like images and scripts are fetched again in firefox
socket.addEventListener('message', () => document.location.reload(true))
`,
})
}
}
interface Options {
fontOrigin: "googleFonts" | "local"
}
const defaultOptions: Options = {
fontOrigin: "googleFonts",
}
export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<Options>) => {
const { fontOrigin } = { ...defaultOptions, ...opts }
// This emitter should not update the `resources` parameter. If it does, partial
// rebuilds may not work as expected.
export const ComponentResources: QuartzEmitterPlugin = () => {
return {
name: "ComponentResources",
getQuartzComponents() {
return []
},
async getDependencyGraph(ctx, content, _resources) {
// This emitter adds static resources to the `resources` parameter. One
// important resource this emitter adds is the code to start a websocket
// connection and listen to rebuild messages, which triggers a page reload.
// The resources parameter with the reload logic is later used by the
// ContentPage emitter while creating the final html page. In order for
// the reload logic to be included, and so for partial rebuilds to work,
// we need to run this emitter for all markdown files.
const graph = new DepGraph<FilePath>()
for (const [_tree, file] of content) {
const sourcePath = file.data.filePath!
const slug = file.data.slug!
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
}
return graph
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},
async emit(ctx, _content, resources): Promise<FilePath[]> {
async emit(ctx, _content, _resources): Promise<FilePath[]> {
const promises: Promise<FilePath>[] = []
const cfg = ctx.cfg.configuration
// component specific scripts and styles
const componentResources = getComponentResources(ctx)
let googleFontsStyleSheet = ""
if (fontOrigin === "local") {
if (cfg.theme.fontOrigin === "local") {
// let the user do it themselves in css
} else if (fontOrigin === "googleFonts") {
if (cfg.theme.cdnCaching) {
resources.css.push(googleFontHref(cfg.theme))
} else {
let match
} else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) {
// when cdnCaching is true, we link to google fonts in Head.tsx
let match
const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
googleFontsStyleSheet = await (
await fetch(googleFontHref(ctx.cfg.configuration.theme))
).text()
googleFontsStyleSheet = await (
await fetch(googleFontHref(ctx.cfg.configuration.theme))
).text()
while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) {
// match[0] is the `url(path)`, match[1] is the `path`
const url = match[1]
// the static name of this file.
const [filename, ext] = url.split("/").pop()!.split(".")
while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) {
// match[0] is the `url(path)`, match[1] is the `path`
const url = match[1]
// the static name of this file.
const [filename, ext] = url.split("/").pop()!.split(".")
googleFontsStyleSheet = googleFontsStyleSheet.replace(
url,
`/static/fonts/${filename}.ttf`,
)
googleFontsStyleSheet = googleFontsStyleSheet.replace(
url,
`https://${cfg.baseUrl}/static/fonts/${filename}.ttf`,
)
promises.push(
fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to fetch font`)
}
return res.arrayBuffer()
})
.then((buf) =>
write({
ctx,
slug: joinSegments("static", "fonts", filename) as FullSlug,
ext: `.${ext}`,
content: Buffer.from(buf),
}),
),
)
}
promises.push(
fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to fetch font`)
}
return res.arrayBuffer()
})
.then((buf) =>
write({
ctx,
slug: joinSegments("static", "fonts", filename) as FullSlug,
ext: `.${ext}`,
content: Buffer.from(buf),
}),
),
)
}
}
// important that this goes *after* component scripts
// as the "nav" event gets triggered here and we should make sure
// that everyone else had the chance to register a listener for it
addGlobalPageResources(ctx, resources, componentResources)
addGlobalPageResources(ctx, componentResources)
const stylesheet = joinStyles(
ctx.cfg.configuration.theme,

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

@ -18,6 +18,23 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
}
}
// if serving locally, listen for rebuilds and reload the page
if (ctx.argv.serve) {
const wsUrl = ctx.argv.remoteDevHost
? `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}`
: `ws://localhost:${ctx.argv.wsPort}`
staticResources.js.push({
loadTime: "afterDOMReady",
contentType: "inline",
script: `
const socket = new WebSocket('${wsUrl}')
// reload(true) ensures resources like images and scripts are fetched again in firefox
socket.addEventListener('message', () => document.location.reload(true))
`,
})
}
return staticResources
}

View File

@ -0,0 +1,52 @@
import rehypeCitation from "rehype-citation"
import { PluggableList } from "unified"
import { visit } from "unist-util-visit"
import { QuartzTransformerPlugin } from "../types"
export interface Options {
bibliographyFile: string
suppressBibliography: boolean
linkCitations: boolean
csl: string
}
const defaultOptions: Options = {
bibliographyFile: "./bibliography.bib",
suppressBibliography: false,
linkCitations: false,
csl: "apa",
}
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "Citations",
htmlPlugins() {
const plugins: PluggableList = []
// Add rehype-citation to the list of plugins
plugins.push([
rehypeCitation,
{
bibliography: opts.bibliographyFile,
suppressBibliography: opts.suppressBibliography,
linkCitations: opts.linkCitations,
},
])
// Transform the HTML of the citattions; add data-no-popover property to the citation links
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
plugins.push(() => {
return (tree, _file) => {
visit(tree, "element", (node, index, parent) => {
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
node.properties["data-no-popover"] = true
}
})
}
})
return plugins
},
}
}

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,46 @@ 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
let currentDescriptionLength = 0
if (sentences[0] !== undefined && sentences[0].length >= len) {
const firstSentence = sentences[0].split(" ")
while (currentDescriptionLength < len) {
const sentence = firstSentence[sentenceIdx]
if (!sentence) break
finalDesc.push(sentence)
currentDescriptionLength += sentence.length
sentenceIdx++
}
finalDesc.push("...")
} else {
while (currentDescriptionLength < len) {
const sentence = sentences[sentenceIdx]
if (!sentence) break
const currentSentence = sentence.endsWith(".") ? sentence : sentence + "."
finalDesc.push(currentSentence)
currentDescriptionLength += currentSentence.length
sentenceIdx++
}
}
file.data.description = finalDesc.join(" ")
file.data.text = text
}
},

View File

@ -90,6 +90,7 @@ declare module "vfile" {
description: string
publish: boolean
draft: boolean
lang: string
enableToc: string
cssclasses: string[]
}>

View File

@ -1,5 +1,6 @@
export { FrontMatter } from "./frontmatter"
export { GitHubFlavoredMarkdown } from "./gfm"
export { Citations } from "./citations"
export { CreatedModifiedDate } from "./lastmod"
export { Latex } from "./latex"
export { Description } from "./description"

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
@ -100,15 +99,27 @@ export const externalLinkRegex = /^https?:\/\//i
export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g")
// !? -> optional embedding
// \[\[ -> open brace
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
// (\|[^\[\]\#]+)? -> | then one or more non-special characters (alias)
// !? -> optional embedding
// \[\[ -> open brace
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
// (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias)
export const wikilinkRegex = new RegExp(
/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\#]+)?\]\]/,
/!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/,
"g",
)
// ^\|([^\n])+\|\n(\|) -> matches the header row
// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator
// (\|([^\n])+\|\n)+ -> matches the body rows
export const tableRegex = new RegExp(
/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/,
"gm",
)
// matches any wikilink, only used for escaping wikilinks inside tables
export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/, "g")
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
const commentRegex = new RegExp(/%%[\s\S]*?%%/, "g")
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
@ -124,6 +135,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+))?)?$/,
@ -169,6 +181,21 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
src = src.toString()
}
// replace all wikilinks inside a table first
src = src.replace(tableRegex, (value) => {
// escape all aliases and headers in wikilinks inside a table
return value.replace(tableWikilinkRegex, (value, ...capture) => {
const [raw]: (string | undefined)[] = capture
let escaped = raw ?? ""
escaped = escaped.replace("#", "\\#")
// escape pipe characters if they are not already escaped
escaped = escaped.replace(/((^|[^\\])(\\\\)*)\|/g, "$1\\|")
return escaped
})
})
// replace all other wikilinks
src = src.replace(wikilinkRegex, (value, ...capture) => {
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
@ -189,9 +216,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
return src
},
markdownPlugins(ctx) {
markdownPlugins(_ctx) {
const plugins: PluggableList = []
const cfg = ctx.cfg.configuration
// regex replacements
plugins.push(() => {
@ -328,7 +354,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
children: [
{
type: "text",
value: `#${tag}`,
value: tag,
},
],
}
@ -528,12 +554,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 +601,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 +611,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 {
@ -508,3 +513,8 @@ ol.overflow {
padding-left: 1rem;
}
}
.katex-display {
overflow-x: auto;
overflow-y: hidden;
}

View File

@ -168,6 +168,9 @@ export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug
export function splitAnchor(link: string): [string, string] {
let [fp, anchor] = link.split("#", 2)
if (fp.endsWith(".pdf")) {
return [fp, anchor === undefined ? "" : `#${anchor}`]
}
anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
return [fp, anchor]
}

View File

@ -22,6 +22,7 @@ export interface Theme {
}
cdnCaching: boolean
colors: Colors
fontOrigin: "googleFonts" | "local"
}
export type ThemeKey = keyof Colors