feat(experimental): partial rebuilds (jackyzha0#716) (#55)

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
Co-authored-by: kabirgh <15871468+kabirgh@users.noreply.github.com>
Co-authored-by: Alq <ahmed.elq53@gmail.com>
Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
This commit is contained in:
Miguel Pimentel 2024-02-10 18:55:47 -08:00 committed by GitHub
parent 49fe192361
commit 7450bd7a81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1146 additions and 279 deletions

View File

@ -17,6 +17,7 @@ Title: Atomic Note: Importance of Exercise
Tags: #exercise #health #wellness
Regular exercise confers numerous health benefits, including:
- Improved cardiovascular health
- Increased strength and flexibility
- Weight management

View File

@ -13,7 +13,7 @@ Unfortunately, this forecast did not come to pass. The solar activity remained w
## Light Pollution Table
| Location | Distance | Travel Time | Rank\* |
| :--- | :---: | :---: | :---: |
| :----------- | :-------: | :---------: | :----: |
| Minneapolis | 16 miles | 25 minutes | 0 |
| Bloomington | 2 miles | 5 minutes | 1 |
| Chaska | 15 miles | 20 minutes | 2 |

View File

@ -21,18 +21,18 @@ Non-inclusive, non-comprehensive list of books I've read.
### Stephanie Plum Series
1. _[One for the Money](https://en.wikipedia.org/wiki/One_for_the_Money_(novel) "One for the Money (novel)")_ (1994)
1. _[One for the Money](https://en.wikipedia.org/wiki/One_for_the_Money_(novel) "One for the Money (novel)")\_ (1994)
2. _[Two for the Dough](https://en.wikipedia.org/wiki/Two_for_the_Dough "Two for the Dough")_ (1996)
3. _[Three to Get Deadly](https://en.wikipedia.org/wiki/Three_to_Get_Deadly "Three to Get Deadly")_ (1997)
4. _[Four to Score](https://en.wikipedia.org/wiki/Four_to_Score_(novel) "Four to Score (novel)")_ (1998)
5. _[High Five](https://en.wikipedia.org/wiki/High_Five_(novel))_ (1999)
4. _[Four to Score](https://en.wikipedia.org/wiki/Four_to_Score_(novel) "Four to Score (novel)")\_ (1998)
5. _[High Five](https://en.wikipedia.org/wiki/High_Five_(novel))\_ (1999)
6. _[Hot Six](https://en.wikipedia.org/wiki/Hot_Six "Hot Six")_ (2000)
7. _[Seven Up](https://en.wikipedia.org/wiki/Seven_Up_(novel) "Seven Up (novel)")_ (2001)
8. _[Hard Eight](https://en.wikipedia.org/wiki/Hard_Eight_(novel) "Hard Eight (novel)")_ (2002)
9. _[To the Nines](https://en.wikipedia.org/wiki/To_the_Nines_(novel) "To the Nines (novel)")_ (2003)
10. _[Ten Big Ones](https://en.wikipedia.org/wiki/Ten_Big_Ones_(novel) "Ten Big Ones (novel)")_ (2004)
11. _[Eleven on Top](https://en.wikipedia.org/wiki/Eleven_on_Top_(novel) "Eleven on Top (novel)")_ (2005)
12. _[Twelve Sharp](https://en.wikipedia.org/wiki/Twelve_Sharp_(novel) "Twelve Sharp (novel)")_ (2006)
7. _[Seven Up](https://en.wikipedia.org/wiki/Seven_Up_(novel) "Seven Up (novel)")\_ (2001)
8. _[Hard Eight](https://en.wikipedia.org/wiki/Hard_Eight_(novel) "Hard Eight (novel)")\_ (2002)
9. _[To the Nines](https://en.wikipedia.org/wiki/To_the_Nines_(novel) "To the Nines (novel)")\_ (2003)
10. _[Ten Big Ones](https://en.wikipedia.org/wiki/Ten_Big_Ones_(novel) "Ten Big Ones (novel)")\_ (2004)
11. _[Eleven on Top](https://en.wikipedia.org/wiki/Eleven_on_Top_(novel) "Eleven on Top (novel)")\_ (2005)
12. _[Twelve Sharp](https://en.wikipedia.org/wiki/Twelve_Sharp_(novel) "Twelve Sharp (novel)")\_ (2006)
13. _[Lean Mean Thirteen](https://en.wikipedia.org/wiki/Lean_Mean_Thirteen "Lean Mean Thirteen")_ (2007)
14. _[Fearless Fourteen](https://en.wikipedia.org/wiki/Fearless_Fourteen "Fearless Fourteen")_ (2008)
15. _[Finger Lickin' Fifteen](https://en.wikipedia.org/wiki/Finger_Lickin%27_Fifteen "Finger Lickin' Fifteen")_ (2009)
@ -54,7 +54,7 @@ Non-inclusive, non-comprehensive list of books I've read.
## Jerry Spinelli
1. [Stargirl](https://en.wikipedia.org/wiki/Stargirl_(novel))
1. [Stargirl](<https://en.wikipedia.org/wiki/Stargirl_(novel)>)
2. [Love, Stargirl](https://en.wikipedia.org/wiki/Love,_Stargirl)
## Robert Pinsky

View File

@ -32,9 +32,9 @@ All _mobile-first-designs_ media queries and 1 _desktop-first-design_ media quer
```scss
@mixin screen-min($min) {
@media (min-width: $min) {
@content
@content;
}
};
}
```
### Anything Below a Certain Screen Width (_desktop-first-design_)
@ -42,19 +42,19 @@ All _mobile-first-designs_ media queries and 1 _desktop-first-design_ media quer
```scss
@mixin screen-max($max) {
@media (max-width: $max - 1) {
@content
@content;
}
};
}
```
### Anything In-between Two Values (_hybrid_)
```scss
@mixin screen-minmax($min, $max){
@media (min-width: $min) and (max-width: $max - 1){
@content
@mixin screen-minmax($min, $max) {
@media (min-width: $min) and (max-width: $max - 1) {
@content;
}
};
}
```
```scss
@ -66,10 +66,20 @@ All _mobile-first-designs_ media queries and 1 _desktop-first-design_ media quer
.container {
margin: 0 auto;
width: 100%;
@include screen-min(768px){max-width: 750px;}
@include screen-min(992px){max-width: 970px;}
@include screen-min(1200px){max-width: 1170px;}
@include screen-min(1400px){max-width: 1370px;}
@include screen-min(1600px){max-width: 1570px;}
@include screen-min(768px) {
max-width: 750px;
}
@include screen-min(992px) {
max-width: 970px;
}
@include screen-min(1200px) {
max-width: 1170px;
}
@include screen-min(1400px) {
max-width: 1370px;
}
@include screen-min(1600px) {
max-width: 1570px;
}
}
```

View File

@ -17,6 +17,6 @@ Digital Gardens are explorable rather than structured as a strictly linear strea
## Kinds of Notes
- 🌱 _Seedlings_ for very rough and early ideas.
- 🌿 _Budding_ for work I've cleaned up and clarified.
- 🌳 _Evergreen_ for work that is reasonably complete (though I still tend these over time).
- 🌱 *Seedlings* for very rough and early ideas.
- 🌿 *Budding* for work I've cleaned up and clarified.
- 🌳 *Evergreen* for work that is reasonably complete (though I still tend these over time).

View File

@ -9,7 +9,7 @@ compartir: true
## Notes on Abbreviation Formatting
When you get familiar with Emmet's abbreviations syntax, you may want to use some formatting to make your abbreviations more readable. But it won't work, because space is a _stop symbol,_ where Emmet stops abbreviation parsing. Many users mistakenly think that each abbreviation should be written in a new line, but they are wrong: you can type and expand the abbreviation anywhere in the text.
When you get familiar with Emmet's abbreviations syntax, you may want to use some formatting to make your abbreviations more readable. But it won't work, because space is a *stop symbol,* where Emmet stops abbreviation parsing. Many users mistakenly think that each abbreviation should be written in a new line, but they are wrong: you can type and expand the abbreviation anywhere in the text.
This is why Emmet needs some indicators (like spaces) where it should stop parsing to not expand anything that you don't need. If you're still thinking that such formatting is required for complex abbreviations to make them more readable:
@ -270,6 +270,6 @@ p{Click }+a{here}+{ to continue}
```
```html
<p>Click </p>
<p>Click</p>
<a href="">here</a> to continue
```

View File

@ -73,7 +73,7 @@ Visit the [Documentation](https://espanso.org/docs/get-started/).
### Run CLI Command
```yaml
- name: getip
- name: getip
type: shell
params:
cmd: "curl ifconfig.me"

View File

@ -5,6 +5,7 @@ compartir: true
updated: 2023-12-04
enableToc: true
---
## Tabs for the Guitar
When you are looking at a tab, you will see six horizontal lines. These lines represent the strings of the guitar. The bottom line is the 6th string (the thickest string on your guitar, low e) and the top line is the thinnest string (the first string, high e).
@ -41,5 +42,5 @@ You can then finish out the riff by grabbing the 6th fret on the 6th string with
## Sources
- [ultimate-guitar.com](https://tabs.ultimate-guitar.com/tab/metallica/enter-sandman-tabs-8595 )
- [ultimate-guitar.com](https://tabs.ultimate-guitar.com/tab/metallica/enter-sandman-tabs-8595)
- [guitarlessons.org](https://www.guitarlessons.org/lessons/read-guitar-tabs/)

View File

@ -14,7 +14,7 @@ Markdown is a lightweight [[./Markup Language|Markup Language]] that you can use
## Markdown Flavors
 There are several popular flavors of Markdown that add extra features and extensions to the original syntax. Two of the most popular ones include:
There are several popular flavors of Markdown that add extra features and extensions to the original syntax. Two of the most popular ones include:
### CommonMark

View File

@ -21,7 +21,7 @@ NeoVim is a fork of Vim focused on extensibility and usability. This is my short
## Keybindings
| Key Combination | Command |
| --- | --- |
| -------------------------- | ---------------------------------------------- |
| `<leader>` | `<space>` |
| **Unsorted** |
| `<leader>h` | `^` |

View File

@ -63,7 +63,7 @@ Selected $0.10 as the baseline after averaging some calculations.
## NVMe M.2 2280 M Key
| Brand | Storage | Price | Notes |
| ------------ |:-------:|:-----:| ---------- |
| ------------ | :-----: | :---: | ---------- |
| 970 EVO Plus | 500 GB | $35 | MLC V-NAND |
| 970 EVO Plus | 2 TB | $100 | MLC V-NAND |
| 970 EVO Plus | 1 TB | $50 | V-NAND |
@ -82,7 +82,7 @@ Selected $0.10 as the baseline after averaging some calculations.
## SSD
| Brand | Storage | Price | Notes |
| ------------ |:-------:|:-----:| ---------- |
| ------------ | :-----: | :---: | ---------- |
| Inland | 1 TB | $50 | TLC |
| Inland | 512 GB | $25 | TLC |
| Platinum | 2 TB | $80 | TLC |
@ -99,7 +99,7 @@ Selected $0.10 as the baseline after averaging some calculations.
### NVMe
| Description | $ / GB | per $0.01 | per GB | Coef | Score |
| ------------------------ |:------:|:-------------:|:--------:|:-----------:|:-----:|
| ------------------------ | :----: | :-------: | :----: | :--: | :---: |
| 970 500 GB $35 MLC | 0.070 | 3.00 | 500 | 1.25 | 629 |
| 970 2 TB $100 MLC | 0.050 | 5.00 | 2000 | 1.25 | 2506 |
| 970 1 TB $100 MLC | 0.103 | 0.00 | 1000 | 1.25 | 1250 |
@ -114,12 +114,13 @@ Selected $0.10 as the baseline after averaging some calculations.
| Performance 1 TB $55 TLC | 0.055 | 4.50 | 1000 | 1 | 1005 |
| Prime 500 GB $30 TLC | 0.060 | 4.00 | 500 | 1 | 504 |
| Prime 1 TB $50 TLC | 0.050 | 5.00 | 1000 | 1 | 1005 |
\*_Higher is better._
### SSD
| Description | $ / GB | 1 per cent | 1 per GB | Coefficient | Score |
| ------------------------- |:------:|:----------:|:--------:|:-----------:|:-----:|
| ------------------------- | :----: | :--------: | :------: | :---------: | :---: |
| Inland 1TB $50 TLC | 0.050 | 5 | 1000 | 1 | 1005 |
| Inland 512GB $25 TLC | 0.049 | 5.1 | 512 | 1 | 517 |
| Platinum 2TB $80 TLC | 0.040 | 6 | 2000 | 1 | 2006 |
@ -130,6 +131,7 @@ Selected $0.10 as the baseline after averaging some calculations.
| 870 EVO 4TB $220 MLC | 0.055 | 4.5 | 4000 | 1.25 | 5006 |
| 870 EVO 500GB $40 MLC | 0.020 | 8 | 500 | 1.25 | 635 |
| 870 QVO 1TB $70 QLC | 0.070 | 3 | 1000 | 0.75 | 753 |
\*_Higher is better._
## Conclusions

View File

@ -12,5 +12,6 @@ In typography and lettering, a "sans-serif", "sans serif", "gothic", or simply "
## Font Family in CSS
```css
font-family: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, "Nimbus Sans L", Roboto, Noto, "Segoe UI", Arial, Helvetica, "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, "Nimbus Sans L", Roboto,
Noto, "Segoe UI", Arial, Helvetica, "Helvetica Neue", sans-serif;
```

View File

@ -12,5 +12,6 @@ In typography, a serif (/ˈsɛrɪf/) is a small line or stroke regularly attache
## Font Family in CSS
```css
font-family: Constantia, "Lucida Bright", Lucidabright, "Lucida Serif", Lucida, "DejaVu Serif", "Bitstream Vera Serif", "Liberation Serif", Georgia, serif;
font-family: Constantia, "Lucida Bright", Lucidabright, "Lucida Serif", Lucida, "DejaVu Serif",
"Bitstream Vera Serif", "Liberation Serif", Georgia, serif;
```

View File

@ -15,7 +15,7 @@ compartir: true
1. Capable of soothing or eliminating pain.
2. Not likely to offend or arouse tensions.
Also used as a _noun_ to describe something that soothes, calms, or comforts.
Also used as a _noun_ to describe something that soothes, calms, or comforts.
### Arete (noun)
@ -110,7 +110,7 @@ Also used as a _noun_ to describe something that soothes, calms, or comforts.
1. To understand intuitively or by empathy, to establish rapport with.
2. To empathize or communicate sympathetically (with); also, to experience enjoyment.
3. [Neologism](https://en.wikipedia.org/wiki/Neologism "Neologism") coined by American writer [Robert A. Heinlein](https://en.wikipedia.org/wiki/Robert_A._Heinlein "Robert A. Heinlein") for his 1961 [science fiction](https://en.wikipedia.org/wiki/Science_fiction "Science fiction") novel _[Stranger in a Strange Land](https://en.wikipedia.org/wiki/Stranger_in_a_Strange_Land "Stranger in a Strange Land")_.
3. [Neologism](https://en.wikipedia.org/wiki/Neologism "Neologism") coined by American writer [Robert A. Heinlein](https://en.wikipedia.org/wiki/Robert_A._Heinlein "Robert A. Heinlein") for his 1961 [science fiction](https://en.wikipedia.org/wiki/Science_fiction "Science fiction") novel *[Stranger in a Strange Land](https://en.wikipedia.org/wiki/Stranger_in_a_Strange_Land "Stranger in a Strange Land")*.
### Halcyon (noun)

View File

@ -1,5 +1,5 @@
---
title: Forgetful Notes
title: Welcome!
description: Forgetful Notes—A digital garden of knowledge. A platform for my learning and creative endeavours. A space for thinking through, building upon, and coming back to.
updated: 2024-02-08
compartir: true

View File

@ -55,6 +55,7 @@ You can [link](https://example.dom/) to external pages. and other internal [[./M
### Formatted Example
> **Blockquote Embedded List**
>
> 1. This is the first list item.
> 2. This is the second list item.
>
@ -75,21 +76,22 @@ In arcu magna, aliquet vel pretium et, molestie et arcu. Mauris lobortis nulla e
In arcu magna, aliquet vel pretium et, molestie et arcu. Mauris lobortis nulla et felis ullamcorper bibendum. Phasellus et hendrerit mauris.
* List item
* Another item
* And another item
- List item
- Another item
- And another item
### Nested List
In arcu magna, aliquet vel pretium et, molestie et arcu. Mauris lobortis nulla et felis ullamcorper bibendum. Phasellus et hendrerit mauris.
* Item
- Item
1. First Sub-item
2. Second Sub-item
1. Numbered Item
2. Another one
1. Sub-item
* Unordered again
- Unordered again
## Code
@ -119,8 +121,8 @@ Let us use some `inline code` and check out how it `looks`. Here's some `more`.
```js
// Javascript code with syntax highlighting.
var fun = function lang(l) {
dateformat.i18n = require('./lang/' + l)
return true;
dateformat.i18n = require("./lang/" + l)
return true
}
```
@ -128,35 +130,35 @@ var fun = function lang(l) {
In arcu magna, aliquet vel pretium et, molestie et arcu. Mauris lobortis nulla et felis ullamcorper bibendum. Phasellus et hendrerit mauris.
|head one|head two|head three|
|---|:---:|---:|
|ok|good swedish fish|nice|
|out of stock|good and plenty|nice|
|ok|good `oreos`|hmm|
|ok|good `zoute` drop|yumm|
| head one | head two | head three |
| ------------ | :---------------: | ---------: |
| ok | good swedish fish | nice |
| out of stock | good and plenty | nice |
| ok | good `oreos` | hmm |
| ok | good `zoute` drop | yumm |
### Simple Example
Title 1 | Title 2 | Title 3 | Title 4
--------------------- | --------------------- | --------------------- | ---------------------
lorem | lorem ipsum | lorem ipsum dolor | lorem ipsum dolor sit
lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit
lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit
lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit
| Title 1 | Title 2 | Title 3 | Title 4 |
| --------------------- | --------------------- | --------------------- | --------------------- |
| lorem | lorem ipsum | lorem ipsum dolor | lorem ipsum dolor sit |
| lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit |
| lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit |
| lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit | lorem ipsum dolor sit |
### Longer Example
Title 1 | Title 2 | Title 3 | Title 4
--- | --- | --- | ---
lorem | lorem ipsum | lorem ipsum dolor | lorem ipsum dolor sit
lorem ipsum dolor sit amet | lorem ipsum dolor sit amet consectetur | lorem ipsum dolor sit amet | lorem ipsum dolor sit
lorem ipsum dolor | lorem ipsum | lorem | lorem ipsum
lorem ipsum dolor | lorem ipsum dolor sit | lorem ipsum dolor sit amet | lorem ipsum dolor sit amet consectetur
| Title 1 | Title 2 | Title 3 | Title 4 |
| -------------------------- | -------------------------------------- | -------------------------- | -------------------------------------- |
| lorem | lorem ipsum | lorem ipsum dolor | lorem ipsum dolor sit |
| lorem ipsum dolor sit amet | lorem ipsum dolor sit amet consectetur | lorem ipsum dolor sit amet | lorem ipsum dolor sit |
| lorem ipsum dolor | lorem ipsum | lorem | lorem ipsum |
| lorem ipsum dolor | lorem ipsum dolor sit | lorem ipsum dolor sit amet | lorem ipsum dolor sit amet consectetur |
### Inline Markdown Within Tables
| Inline&nbsp;&nbsp;&nbsp; | Markdown&nbsp;&nbsp;&nbsp; | In&nbsp;&nbsp;&nbsp; | Table |
| ---------- | --------- | ----------------- | ---------- |
| ------------------------ | -------------------------- | ----------------------------------- | ------ |
| _italics_ | **bold** | ~~strikethrough~~&nbsp;&nbsp;&nbsp; | `code` |
## Horizontal Rule
@ -167,6 +169,7 @@ lorem ipsum dolor | lorem ipsum dolor sit | lorem ipsum dolor sit amet | lorem i
- [ ] Pending Task
- [x] Completed Task
* [-] Won't Do Task
* [/] In Progress Task
* [*] You are a star.

View File

@ -62,7 +62,7 @@
"mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5",
"preact": "^10.19.3",
"preact": "^10.19.4",
"preact-render-to-string": "^6.3.1",
"pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0",
@ -100,14 +100,14 @@
"@types/d3": "^7.4.3",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.16",
"@types/node": "^20.11.17",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"esbuild": "^0.19.12",
"prettier": "^3.2.5",
"tsx": "^4.7.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
}
}

48
pnpm-lock.yaml generated
View File

@ -75,11 +75,11 @@ dependencies:
specifier: ^0.4.5
version: 0.4.5
preact:
specifier: ^10.19.3
version: 10.19.3
specifier: ^10.19.4
version: 10.19.4
preact-render-to-string:
specifier: ^6.3.1
version: 6.3.1(preact@10.19.3)
version: 6.3.1(preact@10.19.4)
pretty-bytes:
specifier: ^6.1.1
version: 6.1.1
@ -185,8 +185,8 @@ devDependencies:
specifier: ^4.0.9
version: 4.0.9
"@types/node":
specifier: ^20.11.16
version: 20.11.16
specifier: ^20.11.17
version: 20.11.17
"@types/pretty-time":
specifier: ^1.1.5
version: 1.1.5
@ -206,8 +206,8 @@ devDependencies:
specifier: ^3.2.5
version: 3.2.5
tsx:
specifier: ^4.7.0
version: 4.7.0
specifier: ^4.7.1
version: 4.7.1
typescript:
specifier: ^5.3.3
version: 5.3.3
@ -747,7 +747,7 @@ packages:
integrity: sha512-TMO6mWltW0lCu1de8DMRq9+59OP/tEjghS+rs8ZEQ2EgYP5yV3bGw0tS14TMyJGqFaoVChNvhkVzv9RC1UgX+w==,
}
dependencies:
"@types/node": 20.11.16
"@types/node": 20.11.17
dev: true
/@types/d3-array@3.2.1:
@ -884,10 +884,10 @@ packages:
"@types/d3-color": 3.1.3
dev: true
/@types/d3-path@3.0.2:
/@types/d3-path@3.1.0:
resolution:
{
integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==,
integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==,
}
dev: true
@ -941,7 +941,7 @@ packages:
integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==,
}
dependencies:
"@types/d3-path": 3.0.2
"@types/d3-path": 3.1.0
dev: true
/@types/d3-time-format@4.0.3:
@ -1007,7 +1007,7 @@ packages:
"@types/d3-geo": 3.1.0
"@types/d3-hierarchy": 3.1.6
"@types/d3-interpolate": 3.0.4
"@types/d3-path": 3.0.2
"@types/d3-path": 3.1.0
"@types/d3-polygon": 3.0.2
"@types/d3-quadtree": 3.0.6
"@types/d3-random": 3.0.3
@ -1108,10 +1108,10 @@ packages:
"@types/unist": 2.0.10
dev: false
/@types/node@20.11.16:
/@types/node@20.11.17:
resolution:
{
integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==,
integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==,
}
dependencies:
undici-types: 5.26.5
@ -1152,7 +1152,7 @@ packages:
integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==,
}
dependencies:
"@types/node": 20.11.16
"@types/node": 20.11.17
dev: true
/@types/yargs-parser@21.0.3:
@ -2422,7 +2422,7 @@ packages:
estree-util-is-identifier-name: 3.0.0
hast-util-whitespace: 3.0.0
mdast-util-mdx-expression: 2.0.0
mdast-util-mdx-jsx: 3.0.0
mdast-util-mdx-jsx: 3.1.0
mdast-util-mdxjs-esm: 2.0.1
property-information: 6.4.1
space-separated-tokens: 2.0.2
@ -3126,10 +3126,10 @@ packages:
- supports-color
dev: false
/mdast-util-mdx-jsx@3.0.0:
/mdast-util-mdx-jsx@3.1.0:
resolution:
{
integrity: sha512-XZuPPzQNBPAlaqsTTgRrcJnyFbSOBovSadFgbFu8SnuNgm+6Bdx1K+IWoitsmj6Lq6MNtI+ytOqwN70n//NaBA==,
integrity: sha512-A8AJHlR7/wPQ3+Jre1+1rq040fX9A4Q1jG8JxmSNp/PLPHg80A6475wxTp3KzHpApFH6yWxFotHrJQA3dXP6/w==,
}
dependencies:
"@types/estree-jsx": 1.0.4
@ -3825,7 +3825,7 @@ packages:
engines: { node: ">=8.6" }
dev: false
/preact-render-to-string@6.3.1(preact@10.19.3):
/preact-render-to-string@6.3.1(preact@10.19.4):
resolution:
{
integrity: sha512-NQ28WrjLtWY6lKDlTxnFpKHZdpjfF+oE6V4tZ0rTrunHrtZp6Dm0oFrcJalt/5PNeqJz4j1DuZDS0Y6rCBoqDA==,
@ -3833,14 +3833,14 @@ packages:
peerDependencies:
preact: ">=10"
dependencies:
preact: 10.19.3
preact: 10.19.4
pretty-format: 3.8.0
dev: false
/preact@10.19.3:
/preact@10.19.4:
resolution:
{
integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==,
integrity: sha512-dwaX5jAh0Ga8uENBX1hSOujmKWgx9RtL80KaKUFLc6jb4vCEAc3EeZ0rnQO/FO4VgjfPMfoLFWnNG8bHuZ9VLw==,
}
dev: false
@ -4632,10 +4632,10 @@ packages:
}
dev: false
/tsx@4.7.0:
/tsx@4.7.1:
resolution:
{
integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==,
integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==,
}
engines: { node: ">=18.0.0" }
hasBin: true

View File

@ -20,9 +20,9 @@ export const sharedPageComponents: SharedLayout = {
// components for pages that display a single page (e.g. a single note)
export const defaultContentPageLayout: PageLayout = {
beforeBody: [
Component.Breadcrumbs(),
// Component.Breadcrumbs(),
Component.ArticleTitle(),
Component.ContentMeta(),
// Component.ContentMeta(),
// Component.TagList(),
Component.MobileOnly(Component.Spacer()),
],

View File

@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob"
import { trace } from "./util/trace"
import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex"
import DepGraph from "./depgraph"
import { getStaticResourcesFromPlugins } from "./plugins"
type Dependencies = Record<string, DepGraph<FilePath> | null>
type BuildData = {
ctx: BuildCtx
@ -29,8 +33,11 @@ type BuildData = {
toRebuild: Set<FilePath>
toRemove: Set<FilePath>
lastBuildMs: number
dependencies: Dependencies
}
type FileEvent = "add" | "change" | "delete"
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = {
argv,
@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const parsedFiles = await parseMarkdown(ctx, filePaths)
const filteredContent = filterContent(ctx, parsedFiles)
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
// Only build dependency graphs if we're doing a fast rebuild
if (argv.fastRebuild) {
const staticResources = getStaticResourcesFromPlugins(ctx)
for (const emitter of cfg.plugins.emitters) {
dependencies[emitter.name] =
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
}
}
await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
release()
if (argv.serve) {
return startServing(ctx, mut, parsedFiles, clientRefresh)
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
}
}
@ -83,9 +102,11 @@ async function startServing(
mut: Mutex,
initialContent: ProcessedContent[],
clientRefresh: () => void,
dependencies: Dependencies, // emitter name: dep graph
) {
const { argv } = ctx
// cache file parse results
const contentMap = new Map<FilePath, ProcessedContent>()
for (const content of initialContent) {
const [_tree, vfile] = content
@ -95,6 +116,7 @@ async function startServing(
const buildData: BuildData = {
ctx,
mut,
dependencies,
contentMap,
ignored: await isGitIgnored(),
initialSlugs: ctx.allSlugs,
@ -110,19 +132,181 @@ async function startServing(
ignoreInitial: true,
})
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
watcher
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData))
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData))
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData))
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
return async () => {
await watcher.close()
}
}
async function partialRebuildFromEntrypoint(
filepath: string,
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
const { argv, cfg } = ctx
// don't do anything for gitignored files
if (ignored(filepath)) {
return
}
const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart
const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
release()
return
}
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
// UPDATE DEP GRAPH
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
const staticResources = getStaticResourcesFromPlugins(ctx)
let processedFiles: ProcessedContent[] = []
switch (action) {
case "add":
// add to cache when new file is added
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// update the dep graph by asking all emitters whether they depend on this file
for (const emitter of cfg.plugins.emitters) {
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)
}
}
break
case "change":
// invalidate cache when file is changed
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// only content files can have added/removed dependencies because of transclusions
if (path.extname(fp) === ".md") {
for (const emitter of cfg.plugins.emitters) {
// get new dependencies from all emitters for this file
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
// emmiter may not define a dependency graph. nothing to update if so
if (emitterGraph) {
// merge the new dependencies into the dep graph
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}
}
}
break
case "delete":
toRemove.add(fp)
break
}
if (argv.verbose) {
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
}
// EMIT
perf.addEvent("rebuild")
let emittedFiles = 0
const destinationsToDelete = new Set<FilePath>()
for (const emitter of cfg.plugins.emitters) {
const depGraph = dependencies[emitter.name]
// emitter hasn't defined a dependency graph. call it with all processed files
if (depGraph === null) {
if (argv.verbose) {
console.log(
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
)
}
const files = [...contentMap.values()].filter(
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
)
const emittedFps = await emitter.emit(ctx, files, staticResources)
if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
emittedFiles += emittedFps.length
continue
}
// only call the emitter if it uses this file
if (depGraph.hasNode(fp)) {
// re-emit using all files that are needed for the downstream of this file
// eg. for ContentIndex, the dep graph could be:
// a.md --> contentIndex.json
// b.md ------^
//
// if a.md changes, we need to re-emit contentIndex.json,
// 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))
// if file was deleted, don't give it to the emitter
.filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(file)!)
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
emittedFiles += emittedFps.length
}
}
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])
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))
}
toRemove.clear()
release()
clientRefresh()
}
async function rebuildFromEntrypoint(
fp: string,
action: "add" | "change" | "delete",
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {

View File

@ -71,6 +71,11 @@ export const BuildArgv = {
default: false,
describe: "run a local server to live-preview your Quartz",
},
fastRebuild: {
boolean: true,
default: false,
describe: "[experimental] rebuild only the changed files",
},
baseDir: {
string: true,
default: "",

96
quartz/depgraph.test.ts Normal file
View File

@ -0,0 +1,96 @@
import test, { describe } from "node:test"
import DepGraph from "./depgraph"
import assert from "node:assert"
describe("DepGraph", () => {
test("getLeafNodes", () => {
const graph = new DepGraph<string>()
graph.addEdge("A", "B")
graph.addEdge("B", "C")
graph.addEdge("D", "C")
assert.deepStrictEqual(graph.getLeafNodes("A"), new Set(["C"]))
assert.deepStrictEqual(graph.getLeafNodes("B"), new Set(["C"]))
assert.deepStrictEqual(graph.getLeafNodes("C"), new Set(["C"]))
assert.deepStrictEqual(graph.getLeafNodes("D"), new Set(["C"]))
})
describe("getLeafNodeAncestors", () => {
test("gets correct ancestors in a graph without cycles", () => {
const graph = new DepGraph<string>()
graph.addEdge("A", "B")
graph.addEdge("B", "C")
graph.addEdge("D", "B")
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "D"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "D"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "D"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "D"]))
})
test("gets correct ancestors in a graph with cycles", () => {
const graph = new DepGraph<string>()
graph.addEdge("A", "B")
graph.addEdge("B", "C")
graph.addEdge("C", "A")
graph.addEdge("C", "D")
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "C"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "C"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "C"]))
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "C"]))
})
})
describe("updateIncomingEdgesForNode", () => {
test("merges when node exists", () => {
// A.md -> B.md -> B.html
const graph = new DepGraph<string>()
graph.addEdge("A.md", "B.md")
graph.addEdge("B.md", "B.html")
// B.md is edited so it removes the A.md transclusion
// and adds C.md transclusion
// C.md -> B.md
const other = new DepGraph<string>()
other.addEdge("C.md", "B.md")
other.addEdge("B.md", "B.html")
// A.md -> B.md removed, C.md -> B.md added
// C.md -> B.md -> B.html
graph.updateIncomingEdgesForNode(other, "B.md")
const expected = {
nodes: ["A.md", "B.md", "B.html", "C.md"],
edges: [
["B.md", "B.html"],
["C.md", "B.md"],
],
}
assert.deepStrictEqual(graph.export(), expected)
})
test("adds node if it does not exist", () => {
// A.md -> B.md
const graph = new DepGraph<string>()
graph.addEdge("A.md", "B.md")
// Add a new file C.md that transcludes B.md
// B.md -> C.md
const other = new DepGraph<string>()
other.addEdge("B.md", "C.md")
// B.md -> C.md added
// A.md -> B.md -> C.md
graph.updateIncomingEdgesForNode(other, "C.md")
const expected = {
nodes: ["A.md", "B.md", "C.md"],
edges: [
["A.md", "B.md"],
["B.md", "C.md"],
],
}
assert.deepStrictEqual(graph.export(), expected)
})
})
})

187
quartz/depgraph.ts Normal file
View File

@ -0,0 +1,187 @@
export default class DepGraph<T> {
// node: incoming and outgoing edges
_graph = new Map<T, { incoming: Set<T>; outgoing: Set<T> }>()
constructor() {
this._graph = new Map()
}
export(): Object {
return {
nodes: this.nodes,
edges: this.edges,
}
}
toString(): string {
return JSON.stringify(this.export(), null, 2)
}
// BASIC GRAPH OPERATIONS
get nodes(): T[] {
return Array.from(this._graph.keys())
}
get edges(): [T, T][] {
let edges: [T, T][] = []
this.forEachEdge((edge) => edges.push(edge))
return edges
}
hasNode(node: T): boolean {
return this._graph.has(node)
}
addNode(node: T): void {
if (!this._graph.has(node)) {
this._graph.set(node, { incoming: new Set(), outgoing: new Set() })
}
}
removeNode(node: T): void {
if (this._graph.has(node)) {
this._graph.delete(node)
}
}
hasEdge(from: T, to: T): boolean {
return Boolean(this._graph.get(from)?.outgoing.has(to))
}
addEdge(from: T, to: T): void {
this.addNode(from)
this.addNode(to)
this._graph.get(from)!.outgoing.add(to)
this._graph.get(to)!.incoming.add(from)
}
removeEdge(from: T, to: T): void {
if (this._graph.has(from) && this._graph.has(to)) {
this._graph.get(from)!.outgoing.delete(to)
this._graph.get(to)!.incoming.delete(from)
}
}
// returns -1 if node does not exist
outDegree(node: T): number {
return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1
}
// returns -1 if node does not exist
inDegree(node: T): number {
return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1
}
forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void {
this._graph.get(node)?.outgoing.forEach(callback)
}
forEachInNeighbor(node: T, callback: (neighbor: T) => void): void {
this._graph.get(node)?.incoming.forEach(callback)
}
forEachEdge(callback: (edge: [T, T]) => void): void {
for (const [source, { outgoing }] of this._graph.entries()) {
for (const target of outgoing) {
callback([source, target])
}
}
}
// DEPENDENCY ALGORITHMS
// 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
// If an incoming edge was deleted in other, it is deleted in this graph
updateIncomingEdgesForNode(other: DepGraph<T>, node: T): void {
this.addNode(node)
// Add edge if it is present in other
other.forEachInNeighbor(node, (neighbor) => {
this.addEdge(neighbor, node)
})
// For node provided, remove incoming edge if it is absent in other
this.forEachEdge(([source, target]) => {
if (target === node && !other.hasEdge(source, target)) {
this.removeEdge(source, target)
}
})
}
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
// Eg. if the graph is A -> B -> C
// D ---^
// and the node is B, this function returns [C]
getLeafNodes(node: T): Set<T> {
let stack: T[] = [node]
let visited = new Set<T>()
let leafNodes = new Set<T>()
// DFS
while (stack.length > 0) {
let node = stack.pop()!
// If the node is already visited, skip it
if (visited.has(node)) {
continue
}
visited.add(node)
// Check if the node is a leaf node (i.e. destination path)
if (this.outDegree(node) === 0) {
leafNodes.add(node)
}
// Add all unvisited neighbors to the stack
this.forEachOutNeighbor(node, (neighbor) => {
if (!visited.has(neighbor)) {
stack.push(neighbor)
}
})
}
return leafNodes
}
// Get all ancestors of the leaf nodes reachable from the node provided
// Eg. if the graph is A -> B -> C
// D ---^
// and the node is B, this function returns [A, B, D]
getLeafNodeAncestors(node: T): Set<T> {
const leafNodes = this.getLeafNodes(node)
let visited = new Set<T>()
let upstreamNodes = new Set<T>()
// Backwards DFS for each leaf node
leafNodes.forEach((leafNode) => {
let stack: T[] = [leafNode]
while (stack.length > 0) {
let node = stack.pop()!
if (visited.has(node)) {
continue
}
visited.add(node)
// Add node if it's not a leaf node (i.e. destination path)
// Assumes destination file cannot depend on another destination file
if (this.outDegree(node) !== 0) {
upstreamNodes.add(node)
}
// Add all unvisited parents to the stack
this.forEachInNeighbor(node, (parentNode) => {
if (!visited.has(parentNode)) {
stack.push(parentNode)
}
})
}
})
return upstreamNodes
}
}

View File

@ -1,4 +1,4 @@
import { Translation } from "./locales/definition"
import { Translation, CalloutTranslation } from "./locales/definition"
import en from "./locales/en-US"
import fr from "./locales/fr-FR"
import ja from "./locales/ja-JP"
@ -6,6 +6,7 @@ import de from "./locales/de-DE"
import nl from "./locales/nl-NL"
import ro from "./locales/ro-RO"
import es from "./locales/es-ES"
import ar from "./locales/ar-SA"
import uk from "./locales/uk-UA"
export const TRANSLATIONS = {
@ -17,9 +18,30 @@ export const TRANSLATIONS = {
"ro-RO": ro,
"ro-MD": ro,
"es-ES": es,
"ar-SA": ar,
"ar-AE": ar,
"ar-QA": ar,
"ar-BH": ar,
"ar-KW": ar,
"ar-OM": ar,
"ar-YE": ar,
"ar-IR": ar,
"ar-SY": ar,
"ar-IQ": ar,
"ar-JO": ar,
"ar-PL": ar,
"ar-LB": ar,
"ar-EG": ar,
"ar-SD": ar,
"ar-LY": ar,
"ar-MA": ar,
"ar-TN": ar,
"ar-DZ": ar,
"ar-MR": ar,
"uk-UA": uk,
} as const
export const defaultTranslation = "en-US"
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation]
export type ValidLocale = keyof typeof TRANSLATIONS
export type ValidCallout = keyof CalloutTranslation

View File

@ -0,0 +1,80 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "غير معنون",
description: "لم يتم تقديم أي وصف",
},
components: {
callout: {
note: "ملاحظة",
abstract: "ملخص",
info: "معلومات",
todo: "للقيام",
tip: "نصيحة",
success: "نجاح",
question: "سؤال",
warning: "تحذير",
failure: "فشل",
danger: "خطر",
bug: "خلل",
example: "مثال",
quote: "اقتباس",
},
backlinks: {
title: "وصلات العودة",
noBacklinksFound: "لا يوجد وصلات عودة",
},
themeToggle: {
lightMode: "الوضع النهاري",
darkMode: "الوضع الليلي",
},
explorer: {
title: "المستعرض",
},
footer: {
createdWith: "أُنشئ باستخدام",
},
graph: {
title: "التمثيل التفاعلي",
},
recentNotes: {
title: "آخر الملاحظات",
seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`,
linkToOriginal: "وصلة للملاحظة الرئيسة",
},
search: {
title: "بحث",
searchBarPlaceholder: "ابحث عن شيء ما",
},
tableOfContents: {
title: "فهرس المحتويات",
},
},
pages: {
rss: {
recentNotes: "آخر الملاحظات",
lastFewNotes: ({ count }) => `آخر ${count} ملاحظة`,
},
error: {
title: "غير موجود",
notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.",
},
folderContent: {
folder: "مجلد",
itemsUnderFolder: ({ count }) =>
count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`,
},
tagContent: {
tag: "الوسم",
tagIndex: "مؤشر الوسم",
itemsUnderTag: ({ count }) =>
count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`,
showingFirst: ({ count }) => `إظهار أول ${count} أوسمة.`,
totalTags: ({ count }) => `يوجد ${count} أوسمة.`,
},
},
} as const satisfies Translation

View File

@ -6,6 +6,21 @@ export default {
description: "Keine Beschreibung angegeben",
},
components: {
callout: {
note: "Hinweis",
abstract: "Zusammenfassung",
info: "Info",
todo: "Zu erledigen",
tip: "Tipp",
success: "Erfolg",
question: "Frage",
warning: "Warnung",
failure: "Misserfolg",
danger: "Gefahr",
bug: "Fehler",
example: "Beispiel",
quote: "Zitat",
},
backlinks: {
title: "Backlinks",
noBacklinksFound: "Keine Backlinks gefunden",

View File

@ -1,11 +1,28 @@
import { FullSlug } from "../../util/path"
export interface CalloutTranslation {
note: string
abstract: string
info: string
todo: string
tip: string
success: string
question: string
warning: string
failure: string
danger: string
bug: string
example: string
quote: string
}
export interface Translation {
propertyDefaults: {
title: string
description: string
}
components: {
callout: CalloutTranslation
backlinks: {
title: string
noBacklinksFound: string

View File

@ -6,6 +6,21 @@ export default {
description: "No description provided",
},
components: {
callout: {
note: "Note",
abstract: "Abstract",
info: "Info",
todo: "Todo",
tip: "Tip",
success: "Success",
question: "Question",
warning: "Warning",
failure: "Failure",
danger: "Danger",
bug: "Bug",
example: "Example",
quote: "Quote",
},
backlinks: {
title: "Backlinks",
noBacklinksFound: "No backlinks found",

View File

@ -6,6 +6,21 @@ export default {
description: "Sin descripción",
},
components: {
callout: {
note: "Nota",
abstract: "Resumen",
info: "Información",
todo: "Por hacer",
tip: "Consejo",
success: "Éxito",
question: "Pregunta",
warning: "Advertencia",
failure: "Fallo",
danger: "Peligro",
bug: "Error",
example: "Ejemplo",
quote: "Cita",
},
backlinks: {
title: "Enlaces de Retroceso",
noBacklinksFound: "No se han encontrado enlaces traseros",

View File

@ -6,6 +6,21 @@ export default {
description: "Aucune description fournie",
},
components: {
callout: {
note: "Note",
abstract: "Résumé",
info: "Info",
todo: "À faire",
tip: "Conseil",
success: "Succès",
question: "Question",
warning: "Avertissement",
failure: "Échec",
danger: "Danger",
bug: "Bogue",
example: "Exemple",
quote: "Citation",
},
backlinks: {
title: "Liens retour",
noBacklinksFound: "Aucun lien retour trouvé",

View File

@ -6,6 +6,21 @@ export default {
description: "説明なし",
},
components: {
callout: {
note: "ノート",
abstract: "抄録",
info: "情報",
todo: "やるべきこと",
tip: "ヒント",
success: "成功",
question: "質問",
warning: "警告",
failure: "失敗",
danger: "危険",
bug: "バグ",
example: "例",
quote: "引用",
},
backlinks: {
title: "バックリンク",
noBacklinksFound: "バックリンクはありません",

View File

@ -6,6 +6,21 @@ export default {
description: "Geen beschrijving gegeven.",
},
components: {
callout: {
note: "Notitie",
abstract: "Samenvatting",
info: "Info",
todo: "Te doen",
tip: "Tip",
success: "Succes",
question: "Vraag",
warning: "Waarschuwing",
failure: "Mislukking",
danger: "Gevaar",
bug: "Bug",
example: "Voorbeeld",
quote: "Citaat",
},
backlinks: {
title: "Backlinks",
noBacklinksFound: "Geen backlinks gevonden",

View File

@ -6,6 +6,21 @@ export default {
description: "Nici o descriere furnizată",
},
components: {
callout: {
note: "Notă",
abstract: "Rezumat",
info: "Informație",
todo: "De făcut",
tip: "Sfat",
success: "Succes",
question: "Întrebare",
warning: "Avertisment",
failure: "Eșec",
danger: "Pericol",
bug: "Bug",
example: "Exemplu",
quote: "Citat",
},
backlinks: {
title: "Legături înapoi",
noBacklinksFound: "Nu s-au găsit legături înapoi",

View File

@ -6,6 +6,21 @@ export default {
description: "Опис не надано",
},
components: {
callout: {
note: "Примітка",
abstract: "Абстракт",
info: "Інформація",
todo: "Завдання",
tip: "Порада",
success: "Успіх",
question: "Питання",
warning: "Попередження",
failure: "Невдача",
danger: "Небезпека",
bug: "Баг",
example: "Приклад",
quote: "Цитата",
},
backlinks: {
title: "Зворотні посилання",
noBacklinksFound: "Зворотних посилань не знайдено",

View File

@ -9,6 +9,7 @@ import { NotFound } from "../../components"
import { defaultProcessedContent } from "../vfile"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import DepGraph from "../../depgraph"
export const NotFoundPage: QuartzEmitterPlugin = () => {
const opts: FullPageLayout = {
@ -27,6 +28,9 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
getQuartzComponents() {
return [Head, Body, pageBody, Footer]
},
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},
async emit(ctx, _content, resources): Promise<FilePath[]> {
const cfg = ctx.cfg.configuration
const slug = "404" as FullSlug

View File

@ -2,12 +2,17 @@ import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from
import { QuartzEmitterPlugin } from "../types"
import path from "path"
import { write } from "./helpers"
import DepGraph from "../../depgraph"
export const AliasRedirects: QuartzEmitterPlugin = () => ({
name: "AliasRedirects",
getQuartzComponents() {
return []
},
async getDependencyGraph(_ctx, _content, _resources) {
// TODO implement
return new DepGraph<FilePath>()
},
async emit(ctx, content, _resources): Promise<FilePath[]> {
const { argv } = ctx
const fps: FilePath[] = []

View File

@ -3,6 +3,7 @@ import { QuartzEmitterPlugin } from "../types"
import path from "path"
import fs from "fs"
import { glob } from "../../util/glob"
import DepGraph from "../../depgraph"
export const Assets: QuartzEmitterPlugin = () => {
return {
@ -10,6 +11,24 @@ export const Assets: QuartzEmitterPlugin = () => {
getQuartzComponents() {
return []
},
async getDependencyGraph(ctx, _content, _resources) {
const { argv, cfg } = ctx
const graph = new DepGraph<FilePath>()
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
for (const fp of fps) {
const ext = path.extname(fp)
const src = joinSegments(argv.directory, fp) as FilePath
const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath
const dest = joinSegments(argv.output, name) as FilePath
graph.addEdge(src, dest)
}
return graph
},
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
// glob all non MD/MDX/HTML files in content folder and copy it over
const assetsPath = argv.output

View File

@ -2,6 +2,7 @@ import { FilePath, joinSegments } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import fs from "fs"
import chalk from "chalk"
import DepGraph from "../../depgraph"
export function extractDomainFromBaseUrl(baseUrl: string) {
const url = new URL(`https://${baseUrl}`)
@ -13,6 +14,9 @@ export const CNAME: QuartzEmitterPlugin = () => ({
getQuartzComponents() {
return []
},
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
if (!cfg.configuration.baseUrl) {
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))

View File

@ -14,6 +14,7 @@ import { googleFontHref, joinStyles } from "../../util/theme"
import { Features, transform } from "lightningcss"
import { transform as transpile } from "esbuild"
import { write } from "./helpers"
import DepGraph from "../../depgraph"
type ComponentResources = {
css: string[]
@ -150,7 +151,8 @@ function addGlobalPageResources(
contentType: "inline",
script: `
const socket = new WebSocket('${wsUrl}')
socket.addEventListener('message', () => document.location.reload())
// reload(true) ensures resources like images and scripts are fetched again in firefox
socket.addEventListener('message', () => document.location.reload(true))
`,
})
}
@ -171,6 +173,24 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
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 emit(ctx, _content, resources): Promise<FilePath[]> {
const promises: Promise<FilePath>[] = []
const cfg = ctx.cfg.configuration
@ -201,7 +221,10 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
// the static name of this file.
const [filename, ext] = url.split("/").pop()!.split(".")
googleFontsStyleSheet = googleFontsStyleSheet.replace(url, `/fonts/${filename}.ttf`)
googleFontsStyleSheet = googleFontsStyleSheet.replace(
url,
`/static/fonts/${filename}.ttf`,
)
promises.push(
fetch(url)
@ -214,7 +237,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
.then((buf) =>
write({
ctx,
slug: joinSegments("fonts", filename) as FullSlug,
slug: joinSegments("static", "fonts", filename) as FullSlug,
ext: `.${ext}`,
content: Buffer.from(buf),
}),

View File

@ -7,6 +7,7 @@ import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import DepGraph from "../../depgraph"
export type ContentIndex = Map<FullSlug, ContentDetails>
export type ContentDetails = {
@ -92,6 +93,26 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
opts = { ...defaultOptions, ...opts }
return {
name: "ContentIndex",
async getDependencyGraph(ctx, content, _resources) {
const graph = new DepGraph<FilePath>()
for (const [_tree, file] of content) {
const sourcePath = file.data.filePath!
graph.addEdge(
sourcePath,
joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath,
)
if (opts?.enableSiteMap) {
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath)
}
if (opts?.enableRSS) {
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath)
}
}
return graph
},
async emit(ctx, content, _resources) {
const cfg = ctx.cfg.configuration
const emitted: FilePath[] = []

View File

@ -4,11 +4,12 @@ import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
import { FilePath, pathToRoot } from "../../util/path"
import { FilePath, joinSegments, pathToRoot } from "../../util/path"
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { Content } from "../../components"
import chalk from "chalk"
import { write } from "./helpers"
import DepGraph from "../../depgraph"
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
@ -27,6 +28,18 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
},
async getDependencyGraph(ctx, content, _resources) {
// TODO handle transclusions
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 emit(ctx, content, resources): Promise<FilePath[]> {
const cfg = ctx.cfg.configuration
const fps: FilePath[] = []
@ -60,7 +73,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
fps.push(fp)
}
if (!containsIndex) {
if (!containsIndex && !ctx.argv.fastRebuild) {
console.log(
chalk.yellow(
`\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`,

View File

@ -19,6 +19,7 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
import { FolderContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import DepGraph from "../../depgraph"
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
@ -37,6 +38,13 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
},
async getDependencyGraph(ctx, content, _resources) {
// Example graph:
// nested/file.md --> nested/file.html
// \-------> nested/index.html
// TODO implement
return new DepGraph<FilePath>()
},
async emit(ctx, content, resources): Promise<FilePath[]> {
const fps: FilePath[] = []
const allFiles = content.map((c) => c[1].data)

View File

@ -2,12 +2,27 @@ import { FilePath, QUARTZ, joinSegments } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import fs from "fs"
import { glob } from "../../util/glob"
import DepGraph from "../../depgraph"
export const Static: QuartzEmitterPlugin = () => ({
name: "Static",
getQuartzComponents() {
return []
},
async getDependencyGraph({ argv, cfg }, _content, _resources) {
const graph = new DepGraph<FilePath>()
const staticPath = joinSegments(QUARTZ, "static")
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
for (const fp of fps) {
graph.addEdge(
joinSegments("static", fp) as FilePath,
joinSegments(argv.output, "static", fp) as FilePath,
)
}
return graph
},
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
const staticPath = joinSegments(QUARTZ, "static")
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)

View File

@ -16,6 +16,7 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay
import { TagContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import DepGraph from "../../depgraph"
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
@ -34,6 +35,10 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
},
async getDependencyGraph(ctx, _content, _resources) {
// TODO implement
return new DepGraph<FilePath>()
},
async emit(ctx, content, resources): Promise<FilePath[]> {
const fps: FilePath[] = []
const allFiles = content.map((c) => c[1].data)

View File

@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types"
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
import { Blockquote, 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,6 +17,7 @@ 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
@ -185,8 +186,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
return src
},
markdownPlugins() {
markdownPlugins(ctx) {
const plugins: PluggableList = []
const cfg = ctx.cfg.configuration
// regex replacements
plugins.push(() => {
@ -407,7 +409,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
children: [
{
type: "text",
value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ",
value: useDefaultTitle
? capitalize(
i18n(cfg.locale).components.callout[calloutType as ValidCallout] ??
calloutType,
)
: titleContent + " ",
},
...restOfTitle,
],
@ -443,13 +450,19 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
// replace first line of blockquote with title and rest of the paragraph text
node.children.splice(0, 1, ...blockquoteContent)
const classNames = ["callout", calloutType]
if (collapse) {
classNames.push("is-collapsible")
}
if (defaultState === "collapsed") {
classNames.push("is-collapsed")
}
// add properties to base blockquote
node.data = {
hProperties: {
...(node.data?.hProperties ?? {}),
className: `callout ${calloutType} ${collapse ? "is-collapsible" : ""} ${
defaultState === "collapsed" ? "is-collapsed" : ""
}`,
className: classNames.join(" "),
"data-callout": calloutType,
"data-callout-fold": collapse,
},

View File

@ -4,6 +4,7 @@ import { ProcessedContent } from "./vfile"
import { QuartzComponent } from "../components/types"
import { FilePath } from "../util/path"
import { BuildCtx } from "../util/ctx"
import DepGraph from "../depgraph"
export interface PluginTypes {
transformers: QuartzTransformerPluginInstance[]
@ -38,4 +39,9 @@ export type QuartzEmitterPluginInstance = {
name: string
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
getDependencyGraph?(
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
): Promise<DepGraph<FilePath>>
}

View File

@ -6,6 +6,7 @@ export interface Argv {
verbose: boolean
output: string
serve: boolean
fastRebuild: boolean
port: number
wsPort: number
remoteDevHost?: string