diff --git a/content/Atomic Notes.md b/content/Atomic Notes.md index f45a19be9..a5b1176df 100644 --- a/content/Atomic Notes.md +++ b/content/Atomic Notes.md @@ -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 diff --git a/content/Aurora Borealis Sighting.md b/content/Aurora Borealis Sighting.md index d7017e7e4..094cb3c5d 100644 --- a/content/Aurora Borealis Sighting.md +++ b/content/Aurora Borealis Sighting.md @@ -12,18 +12,18 @@ 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 | -| Farmington | 24 miles | 35 minutes | 3 | -| Jordan | 22 miles | 25 minutes | 4 | -| Norwood | 35 miles | 40 minutes | 5 | -| Gaylord | 55 miles | 60 minutes | 6 | -| Rapidan | 75 miles | 75 minutes | 7 | -| Chengwatana | 95 miles | 90 minutes | 8 | -| Sacred Heart | 105 miles | 120 minutes | 9 | +| Location | Distance | Travel Time | Rank\* | +| :----------- | :-------: | :---------: | :----: | +| Minneapolis | 16 miles | 25 minutes | 0 | +| Bloomington | 2 miles | 5 minutes | 1 | +| Chaska | 15 miles | 20 minutes | 2 | +| Farmington | 24 miles | 35 minutes | 3 | +| Jordan | 22 miles | 25 minutes | 4 | +| Norwood | 35 miles | 40 minutes | 5 | +| Gaylord | 55 miles | 60 minutes | 6 | +| Rapidan | 75 miles | 75 minutes | 7 | +| Chengwatana | 95 miles | 90 minutes | 8 | +| Sacred Heart | 105 miles | 120 minutes | 9 | **\*** Higher Rank is considered better. diff --git a/content/Books.md b/content/Books.md index 2741063bc..736d1f673 100644 --- a/content/Books.md +++ b/content/Books.md @@ -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]() 2. [Love, Stargirl](https://en.wikipedia.org/wiki/Love,_Stargirl) ## Robert Pinsky diff --git a/content/CSS.md b/content/CSS.md index 1c556aa17..1a770ebd6 100644 --- a/content/CSS.md +++ b/content/CSS.md @@ -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; + } } ``` diff --git a/content/Chuckwalla.md b/content/Chuckwalla.md index 8e69bfeae..22a695c72 100644 --- a/content/Chuckwalla.md +++ b/content/Chuckwalla.md @@ -16,7 +16,7 @@ Chuckwallas have a robust build with wide bodies and flattened midsections. Thei mindmap root((Iguanidae)) Iguana - Sauromalus + Sauromalus Ctenosaura Conolophus Dipsosaurus diff --git a/content/Digital Garden.md b/content/Digital Garden.md index 55549b4d0..ce73597ec 100644 --- a/content/Digital Garden.md +++ b/content/Digital Garden.md @@ -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). diff --git a/content/Emmet Cheat Sheet.md b/content/Emmet Cheat Sheet.md index 8b1c7700f..9e3d5ca81 100644 --- a/content/Emmet Cheat Sheet.md +++ b/content/Emmet Cheat Sheet.md @@ -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: @@ -28,9 +28,9 @@ div>ul>li ```html
- +
``` @@ -76,7 +76,7 @@ div+div>p>span+em^^^bq ```html
-

+

``` @@ -89,11 +89,11 @@ ul>li*5 ```html ``` @@ -105,15 +105,15 @@ div>(header>ul>li*2>a)+footer>p ```html
-
-
    -
  • -
  • -
-
-
-

-
+
+
    +
  • +
  • +
+
+
+

+
``` @@ -123,17 +123,17 @@ div>(header>ul>li*2>a)+footer>p ```html
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
``` @@ -167,11 +167,11 @@ ul>li.item$*5 ```html ``` @@ -181,11 +181,11 @@ ul>li.item$$$*5 ```html ``` @@ -223,11 +223,11 @@ ul>li.item$@-3*5 ```html ``` @@ -270,6 +270,6 @@ p{Click }+a{here}+{ to continue} ``` ```html -

Click

+

Click

here to continue ``` diff --git a/content/Espanso Cheat Sheet.md b/content/Espanso Cheat Sheet.md index 4b06b1309..c62196423 100644 --- a/content/Espanso Cheat Sheet.md +++ b/content/Espanso Cheat Sheet.md @@ -73,11 +73,11 @@ Visit the [Documentation](https://espanso.org/docs/get-started/). ### Run CLI Command ```yaml - - name: getip - type: shell - params: - cmd: "curl ifconfig.me" - shell: cmd +- name: getip + type: shell + params: + cmd: "curl ifconfig.me" + shell: cmd ``` ## Global Variables diff --git a/content/Guitar.md b/content/Guitar.md index 647151cc4..b2e2ca86d 100644 --- a/content/Guitar.md +++ b/content/Guitar.md @@ -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/) diff --git a/content/Markdown.md b/content/Markdown.md index b68e2f534..625905bc8 100644 --- a/content/Markdown.md +++ b/content/Markdown.md @@ -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 diff --git a/content/NeoVim.md b/content/NeoVim.md index 84f776805..7624fec1a 100644 --- a/content/NeoVim.md +++ b/content/NeoVim.md @@ -20,62 +20,62 @@ NeoVim is a fork of Vim focused on extensibility and usability. This is my short ## Keybindings -| Key Combination | Command | -| --- | --- | -| `` | `` | -| **Unsorted** | -| `h` | `^` | -| `l` | `g_` | -| `a` | `:keepjumps normal! ggVG` | -| `gy` | `"+y` | -| `gp` | `"+p` | -| `x` | `"_x` | -| `e` | `NvimTreeToggle` | -| **Commands** | -| `w` | `write` | -| `bq` | `bdelete` | -| `bl` | `buffer #` | -| `` | `Lexplore` | -| `` | `` | -| **Telescope** | -| `` | `Telescope buffers` | -| `?` | `Telescope oldfiles` | -| `ff` | `Telescope find_files` | -| `fg` | `Telescope live_grep` | -| `fd` | `Telescope diagnostics` | -| `fs` | `Telescope current_buffer_fuzzy_find` | -| **Telescope (builtin)** | -| `ff` | `builtin.find_files, {}` | -| `fg` | `builtin.live_grep, {}` | -| `fb` | `builtin.buffers, {}` | -| `fh` | `builtin.help_tags, {}` | -| **Normal Mode** | -| `` | `:q!` | -| `` | `:bd` | -| **Moving Vertically** | -| `` | `zz` | -| `` | `zz` | -| `n` | `nzzzv` | -| `N` | `Nzzzv` | -| **Tab Navigation** | -| `` | `gT` | -| `` | `gt` | -| `` | `:tabnew` | +| Key Combination | Command | +| -------------------------- | ---------------------------------------------- | +| `` | `` | +| **Unsorted** | +| `h` | `^` | +| `l` | `g_` | +| `a` | `:keepjumps normal! ggVG` | +| `gy` | `"+y` | +| `gp` | `"+p` | +| `x` | `"_x` | +| `e` | `NvimTreeToggle` | +| **Commands** | +| `w` | `write` | +| `bq` | `bdelete` | +| `bl` | `buffer #` | +| `` | `Lexplore` | +| `` | `` | +| **Telescope** | +| `` | `Telescope buffers` | +| `?` | `Telescope oldfiles` | +| `ff` | `Telescope find_files` | +| `fg` | `Telescope live_grep` | +| `fd` | `Telescope diagnostics` | +| `fs` | `Telescope current_buffer_fuzzy_find` | +| **Telescope (builtin)** | +| `ff` | `builtin.find_files, {}` | +| `fg` | `builtin.live_grep, {}` | +| `fb` | `builtin.buffers, {}` | +| `fh` | `builtin.help_tags, {}` | +| **Normal Mode** | +| `` | `:q!` | +| `` | `:bd` | +| **Moving Vertically** | +| `` | `zz` | +| `` | `zz` | +| `n` | `nzzzv` | +| `N` | `Nzzzv` | +| **Tab Navigation** | +| `` | `gT` | +| `` | `gt` | +| `` | `:tabnew` | | **Pane/Window Navigation** | -| `` | `h` | -| `` | `j` | -| `` | `k` | -| `` | `l` | -| `` | `h` | -| `` | `j` | -| `` | `k` | -| `` | `l` | -| **Terminal** | -| `` | `:sp term://pwshi` | -| `tv` | `:lcd %:p:h:vsp term://pwshi` | -| `th` | `:lcd %:p:h:sp term://pwshi` | -| `` | `` | -| `:q!` | `:q!` | +| `` | `h` | +| `` | `j` | +| `` | `k` | +| `` | `l` | +| `` | `h` | +| `` | `j` | +| `` | `k` | +| `` | `l` | +| **Terminal** | +| `` | `:sp term://pwshi` | +| `tv` | `:lcd %:p:h:vsp term://pwshi` | +| `th` | `:lcd %:p:h:sp term://pwshi` | +| `` | `` | +| `:q!` | `:q!` | ## Plugins diff --git a/content/SSD NVMe Comparison.md b/content/SSD NVMe Comparison.md index 4bb1caca8..2d958838f 100644 --- a/content/SSD NVMe Comparison.md +++ b/content/SSD NVMe Comparison.md @@ -43,8 +43,8 @@ Let's break it down: ### Storage - 1 point per GB - - Less accurate the bigger the SSD - - 3 TB and higher drives scale exp/log instead of linearly. + - Less accurate the bigger the SSD + - 3 TB and higher drives scale exp/log instead of linearly. ### Price @@ -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 | @@ -98,38 +98,40 @@ 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 | -| 980 1 TB $50 V | 0.050 | 5.00 | 1000 | 1 | 1005 | -| 980P 2 TB $120 MLC | 0.060 | 4.00 | 2000 | 1.25 | 2505 | -| 980P 1 TB $70 V | 0.070 | 3.00 | 1000 | 1 | 1003 | -| 990P 1 TB $80 MLC | 0.080 | 2.00 | 1000 | 1.25 | 1253 | -| Crucial 1 TB $40 3D | 0.040 | 6.00 | 1000 | 0.5 | 503 | -| Inland 500 GB $23 QLC | 0.046 | 5.40 | 500 | 0.75 | 379 | -| Inland 1 TB $40 QLC | 0.040 | 6.00 | 1000 | 0.75 | 755 | -| Inland 2 TB $70 QLC | 0.035 | 6.50 | 2000 | 0.75 | 1505 | -| 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 | +| 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 | +| 980 1 TB $50 V | 0.050 | 5.00 | 1000 | 1 | 1005 | +| 980P 2 TB $120 MLC | 0.060 | 4.00 | 2000 | 1.25 | 2505 | +| 980P 1 TB $70 V | 0.070 | 3.00 | 1000 | 1 | 1003 | +| 990P 1 TB $80 MLC | 0.080 | 2.00 | 1000 | 1.25 | 1253 | +| Crucial 1 TB $40 3D | 0.040 | 6.00 | 1000 | 0.5 | 503 | +| Inland 500 GB $23 QLC | 0.046 | 5.40 | 500 | 0.75 | 379 | +| Inland 1 TB $40 QLC | 0.040 | 6.00 | 1000 | 0.75 | 755 | +| Inland 2 TB $70 QLC | 0.035 | 6.50 | 2000 | 0.75 | 1505 | +| 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 | +| 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 | +| Platinum 2TB $80 TLC | 0.040 | 6 | 2000 | 1 | 2006 | | Platinum 1TB $43 TLC | 0.043 | 5.7 | 1000 | 1 | 1006 | -| Professional 256GB $20 3D | 0.078 | 2.2 | 256 | 0.5 | 129 | +| Professional 256GB $20 3D | 0.078 | 2.2 | 256 | 0.5 | 129 | | Professional 125GB $15 3D | 0.120 | 0 | 125 | 0.5 | 63 | -| 870 EVO 1TB $50 MLC | 0.050 | 5 | 1000 | 1.25 | 1256 | +| 870 EVO 1TB $50 MLC | 0.050 | 5 | 1000 | 1.25 | 1256 | | 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 | +| 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 diff --git a/content/Sans-serif.md b/content/Sans-serif.md index 40b64d195..8395c8ad1 100644 --- a/content/Sans-serif.md +++ b/content/Sans-serif.md @@ -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; ``` diff --git a/content/Serif.md b/content/Serif.md index 4545c14ee..bfbfee046 100644 --- a/content/Serif.md +++ b/content/Serif.md @@ -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; ``` diff --git a/content/Words.md b/content/Words.md index 91a85d0f6..23382496f 100644 --- a/content/Words.md +++ b/content/Words.md @@ -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) diff --git a/content/index.md b/content/index.md index ead084ddc..3a87a2596 100644 --- a/content/index.md +++ b/content/index.md @@ -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 @@ -14,11 +14,11 @@ You will find within a wide range of topics, expanding and exploring ideas acros Like me, all notes contained within should be considered work-in-progress. Expect changes at all content levels. That said, I try not to let perfectionism get in the way. That means what you read here is not authoritative or complete, and is not representative of my best work. Please keep that in mind as you navigate around the garden. I'm glad you are here. Enjoy your visit! - Want to learn more _about me_? - Check out the [[./About|About]] page. + Check out the [[./About|About]] page. - Want to read more of my material? - Visit my [Blog](https://miguelpimentel.do/). + Visit my [Blog](https://miguelpimentel.do/). - Want to learn more _about the site_? - Review the site's [[./Meta|Meta]] page. + Review the site's [[./Meta|Meta]] page. I leave you with some fun quotes. Feel free to stop by the [[./Quotes|Quotes]]. diff --git a/content/markdown-showcase.md b/content/markdown-showcase.md index ad5bd722a..baf64c3d8 100644 --- a/content/markdown-showcase.md +++ b/content/markdown-showcase.md @@ -55,9 +55,10 @@ 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. -> +> > Here's some example code: > `Markdown.generate();` @@ -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 - 1. First Sub-item - 2. Second Sub-item +- Item + 1. First Sub-item + 2. Second Sub-item + 1. Numbered Item 2. Another one - 1. Sub-item - * Unordered again + 1. Sub-item + - Unordered again ## Code @@ -102,25 +104,25 @@ Let us use some `inline code` and check out how it `looks`. Here's some `more`. ```html -
- Example -
- +
+ Example +
+ ``` ```css .niceClass { - color: blue; - background-color: #fff; + color: blue; + background-color: #fff; } ``` ```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,36 +130,36 @@ 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    | Markdown    | In    | Table | -| ---------- | --------- | ----------------- | ---------- | -| _italics_ | **bold** | ~~strikethrough~~    | `code` | +| 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. @@ -204,49 +207,49 @@ Most salamanders are nocturnal, and hunt for insects, worms, and ot ## Callouts > [!EXAMPLE] Examples -> +> > Aliases: example > [!note] Notes -> +> > Aliases: note > [!abstract] Summaries -> +> > Aliases: abstract, summary, tldr > [!info] Info -> +> > Aliases: info, todo > [!tip] Hint -> +> > Aliases: tip, hint, important > [!success] Success -> +> > Aliases: success, check, done > [!question] Question -> +> > Aliases: question, help, faq > [!warning] Warning -> +> > Aliases: warning, caution, attention > [!failure] Failure -> +> > Aliases: failure, fail, missing > [!danger] Error -> +> > Aliases: danger, error > [!bug] Bug -> +> > Aliases: bug > [!quote] Quote -> +> > Aliases: quote, cite diff --git a/package.json b/package.json index 47af05109..8832e55e2 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97ae3b3e7..cde6d9d2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/quartz.layout.ts b/quartz.layout.ts index b5b1340b7..7eb387ebb 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -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()), ], diff --git a/quartz/build.ts b/quartz/build.ts index 1f90301e9..ed166bb6c 100644 --- a/quartz/build.ts +++ b/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 | null> type BuildData = { ctx: BuildCtx @@ -29,8 +33,11 @@ type BuildData = { toRebuild: Set toRemove: Set 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 | 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() 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() + + 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 ) { diff --git a/quartz/cli/args.js b/quartz/cli/args.js index 7ed5b078e..123d0ac55 100644 --- a/quartz/cli/args.js +++ b/quartz/cli/args.js @@ -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: "", diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts new file mode 100644 index 000000000..43eb4024f --- /dev/null +++ b/quartz/depgraph.test.ts @@ -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() + 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() + 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() + 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() + 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() + 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() + graph.addEdge("A.md", "B.md") + + // Add a new file C.md that transcludes B.md + // B.md -> C.md + const other = new DepGraph() + 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) + }) + }) +}) diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts new file mode 100644 index 000000000..1efad0770 --- /dev/null +++ b/quartz/depgraph.ts @@ -0,0 +1,187 @@ +export default class DepGraph { + // node: incoming and outgoing edges + _graph = new Map; outgoing: Set }>() + + 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, 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 { + let stack: T[] = [node] + let visited = new Set() + let leafNodes = new Set() + + // 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 { + const leafNodes = this.getLeafNodes(node) + let visited = new Set() + let upstreamNodes = new Set() + + // 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 + } +} diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts index 7fd978e6d..a422acb79 100644 --- a/quartz/i18n/index.ts +++ b/quartz/i18n/index.ts @@ -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 diff --git a/quartz/i18n/locales/ar-SA.ts b/quartz/i18n/locales/ar-SA.ts new file mode 100644 index 000000000..d8efeffef --- /dev/null +++ b/quartz/i18n/locales/ar-SA.ts @@ -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 diff --git a/quartz/i18n/locales/de-DE.ts b/quartz/i18n/locales/de-DE.ts index c6531bef4..f2125bf60 100644 --- a/quartz/i18n/locales/de-DE.ts +++ b/quartz/i18n/locales/de-DE.ts @@ -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", diff --git a/quartz/i18n/locales/definition.ts b/quartz/i18n/locales/definition.ts index de607eb56..baf2a5875 100644 --- a/quartz/i18n/locales/definition.ts +++ b/quartz/i18n/locales/definition.ts @@ -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 diff --git a/quartz/i18n/locales/en-US.ts b/quartz/i18n/locales/en-US.ts index 6ba6d0e5d..3ba28a0d5 100644 --- a/quartz/i18n/locales/en-US.ts +++ b/quartz/i18n/locales/en-US.ts @@ -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", diff --git a/quartz/i18n/locales/es-ES.ts b/quartz/i18n/locales/es-ES.ts index 4d395429d..92a74a03c 100644 --- a/quartz/i18n/locales/es-ES.ts +++ b/quartz/i18n/locales/es-ES.ts @@ -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", diff --git a/quartz/i18n/locales/fr-FR.ts b/quartz/i18n/locales/fr-FR.ts index b30ff049f..fee1ad921 100644 --- a/quartz/i18n/locales/fr-FR.ts +++ b/quartz/i18n/locales/fr-FR.ts @@ -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é", diff --git a/quartz/i18n/locales/ja-JP.ts b/quartz/i18n/locales/ja-JP.ts index a886d84f8..f684c36b3 100644 --- a/quartz/i18n/locales/ja-JP.ts +++ b/quartz/i18n/locales/ja-JP.ts @@ -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: "バックリンクはありません", diff --git a/quartz/i18n/locales/nl-NL.ts b/quartz/i18n/locales/nl-NL.ts index 5426a1e1f..b40362355 100644 --- a/quartz/i18n/locales/nl-NL.ts +++ b/quartz/i18n/locales/nl-NL.ts @@ -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", diff --git a/quartz/i18n/locales/ro-RO.ts b/quartz/i18n/locales/ro-RO.ts index 8fce4a90d..39f1344be 100644 --- a/quartz/i18n/locales/ro-RO.ts +++ b/quartz/i18n/locales/ro-RO.ts @@ -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", diff --git a/quartz/i18n/locales/uk-UA.ts b/quartz/i18n/locales/uk-UA.ts index bf664b6f8..d60b1f8cf 100644 --- a/quartz/i18n/locales/uk-UA.ts +++ b/quartz/i18n/locales/uk-UA.ts @@ -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: "Зворотних посилань не знайдено", diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx index 079adbcad..f9d7a8620 100644 --- a/quartz/plugins/emitters/404.tsx +++ b/quartz/plugins/emitters/404.tsx @@ -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() + }, async emit(ctx, _content, resources): Promise { const cfg = ctx.cfg.configuration const slug = "404" as FullSlug diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index 963c84001..377a7fe10 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -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() + }, async emit(ctx, content, _resources): Promise { const { argv } = ctx const fps: FilePath[] = [] diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts index cc97b2e3e..379cd5b2d 100644 --- a/quartz/plugins/emitters/assets.ts +++ b/quartz/plugins/emitters/assets.ts @@ -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() + + 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 { // glob all non MD/MDX/HTML files in content folder and copy it over const assetsPath = argv.output diff --git a/quartz/plugins/emitters/cname.ts b/quartz/plugins/emitters/cname.ts index 3e17fea2e..cbed2a8b4 100644 --- a/quartz/plugins/emitters/cname.ts +++ b/quartz/plugins/emitters/cname.ts @@ -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() + }, async emit({ argv, cfg }, _content, _resources): Promise { if (!cfg.configuration.baseUrl) { console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration")) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 4033bdf7b..290cb6f2a 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -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[] @@ -149,9 +150,10 @@ function addGlobalPageResources( loadTime: "afterDOMReady", contentType: "inline", script: ` - const socket = new WebSocket('${wsUrl}') - socket.addEventListener('message', () => document.location.reload()) - `, + const socket = new WebSocket('${wsUrl}') + // reload(true) ensures resources like images and scripts are fetched again in firefox + socket.addEventListener('message', () => document.location.reload(true)) + `, }) } } @@ -171,6 +173,24 @@ export const ComponentResources: QuartzEmitterPlugin = (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() + + 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 { const promises: Promise[] = [] const cfg = ctx.cfg.configuration @@ -201,7 +221,10 @@ export const ComponentResources: QuartzEmitterPlugin = (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 = (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), }), diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 1c86b71b8..c0fef86d2 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -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 export type ContentDetails = { @@ -92,6 +93,26 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { opts = { ...defaultOptions, ...opts } return { name: "ContentIndex", + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + 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[] = [] diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index b11890b32..e531b36e8 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -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> = (userOpts) => { const opts: FullPageLayout = { @@ -27,6 +28,18 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp getQuartzComponents() { return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] }, + async getDependencyGraph(ctx, content, _resources) { + // TODO handle transclusions + const graph = new DepGraph() + + 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 { const cfg = ctx.cfg.configuration const fps: FilePath[] = [] @@ -60,7 +73,7 @@ export const ContentPage: QuartzEmitterPlugin> = (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.`, diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index 35c360aab..7a62cda56 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -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> = (userOpts) => { const opts: FullPageLayout = { @@ -37,6 +38,13 @@ export const FolderPage: QuartzEmitterPlugin> = (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() + }, async emit(ctx, content, resources): Promise { const fps: FilePath[] = [] const allFiles = content.map((c) => c[1].data) diff --git a/quartz/plugins/emitters/static.ts b/quartz/plugins/emitters/static.ts index 9f93d9b0a..c52c62879 100644 --- a/quartz/plugins/emitters/static.ts +++ b/quartz/plugins/emitters/static.ts @@ -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() + + 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 { const staticPath = joinSegments(QUARTZ, "static") const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 2411c6837..332c758d5 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -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> = (userOpts) => { const opts: FullPageLayout = { @@ -34,6 +35,10 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) getQuartzComponents() { return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] }, + async getDependencyGraph(ctx, _content, _resources) { + // TODO implement + return new DepGraph() + }, async emit(ctx, content, resources): Promise { const fps: FilePath[] = [] const allFiles = content.map((c) => c[1].data) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index f8a28c486..89b297b2e 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -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 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 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 // 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, }, diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts index a361bb9fd..a23f5d6f4 100644 --- a/quartz/plugins/types.ts +++ b/quartz/plugins/types.ts @@ -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 getQuartzComponents(ctx: BuildCtx): QuartzComponent[] + getDependencyGraph?( + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + ): Promise> } diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index 13e0bf864..e05611418 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -6,6 +6,7 @@ export interface Argv { verbose: boolean output: string serve: boolean + fastRebuild: boolean port: number wsPort: number remoteDevHost?: string