mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 23:15:46 -05:00
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:
parent
49fe192361
commit
7450bd7a81
@ -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
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,9 +42,9 @@ 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_)
|
||||
@ -52,9 +52,9 @@ All _mobile-first-designs_ media queries and 1 _desktop-first-design_ media quer
|
||||
```scss
|
||||
@mixin screen-minmax($min, $max) {
|
||||
@media (min-width: $min) and (max-width: $max - 1) {
|
||||
@content
|
||||
@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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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` | `^` |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
```
|
||||
|
||||
@ -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;
|
||||
```
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
@ -129,7 +131,7 @@ 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 |
|
||||
@ -137,26 +139,26 @@ In arcu magna, aliquet vel pretium et, molestie et arcu. Mauris lobortis nulla e
|
||||
|
||||
### 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 | Markdown | In | Table |
|
||||
| ---------- | --------- | ----------------- | ---------- |
|
||||
| ------------------------ | -------------------------- | ----------------------------------- | ------ |
|
||||
| _italics_ | **bold** | ~~strikethrough~~ | `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.
|
||||
|
||||
@ -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
48
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
@ -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()),
|
||||
],
|
||||
|
||||
194
quartz/build.ts
194
quartz/build.ts
@ -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
|
||||
) {
|
||||
|
||||
@ -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
96
quartz/depgraph.test.ts
Normal 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
187
quartz/depgraph.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
80
quartz/i18n/locales/ar-SA.ts
Normal file
80
quartz/i18n/locales/ar-SA.ts
Normal 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
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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é",
|
||||
|
||||
@ -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: "バックリンクはありません",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: "Зворотних посилань не знайдено",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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[] = []
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
|
||||
@ -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[] = []
|
||||
|
||||
@ -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.`,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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>>
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ export interface Argv {
|
||||
verbose: boolean
|
||||
output: string
|
||||
serve: boolean
|
||||
fastRebuild: boolean
|
||||
port: number
|
||||
wsPort: number
|
||||
remoteDevHost?: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user