The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

run prettier

+1805 -1400
+7 -6
.github/ISSUE_TEMPLATE/bug_report.md
··· 1 1 --- 2 2 name: Bug report 3 3 about: Something about Quartz isn't working the way you expect 4 - title: '' 4 + title: "" 5 5 labels: bug 6 - assignees: '' 7 - 6 + assignees: "" 8 7 --- 9 8 10 9 **Describe the bug** ··· 12 11 13 12 **To Reproduce** 14 13 Steps to reproduce the behavior: 14 + 15 15 1. Go to '...' 16 16 2. Click on '....' 17 17 3. Scroll down to '....' ··· 24 24 If applicable, add screenshots to help explain your problem. 25 25 26 26 **Desktop (please complete the following information):** 27 - - Device: [e.g. iPhone6] 28 - - OS: [e.g. iOS] 29 - - Browser [e.g. chrome, safari] 27 + 28 + - Device: [e.g. iPhone6] 29 + - OS: [e.g. iOS] 30 + - Browser [e.g. chrome, safari] 30 31 31 32 **Additional context** 32 33 Add any other context about the problem here.
+2 -3
.github/ISSUE_TEMPLATE/feature_request.md
··· 1 1 --- 2 2 name: Feature request 3 3 about: Suggest an idea or improvement for Quartz 4 - title: '' 4 + title: "" 5 5 labels: enhancement 6 - assignees: '' 7 - 6 + assignees: "" 8 7 --- 9 8 10 9 **Is your feature request related to a problem? Please describe.**
+2 -2
.github/workflows/ci.yaml
··· 1 - name: Build and Test 1 + name: Build and Test 2 2 3 3 on: 4 4 push: ··· 34 34 run: npm test 35 35 36 36 - name: Ensure Quartz builds 37 - run: npx quartz build 37 + run: npx quartz build
+19 -22
CODE_OF_CONDUCT.md
··· 20 20 21 21 The following behaviors are expected and requested of all community members: 22 22 23 - * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 - * Exercise consideration and respect in your speech and actions. 25 - * Attempt collaboration before conflict. 26 - * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 - * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 - * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 23 + - Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 + - Exercise consideration and respect in your speech and actions. 25 + - Attempt collaboration before conflict. 26 + - Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 + - Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 + - Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 29 30 30 ## 4. Unacceptable Behavior 31 31 32 32 The following behaviors are considered harassment and are unacceptable within our community: 33 33 34 - * Violence, threats of violence or violent language directed against another person. 35 - * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 - * Posting or displaying sexually explicit or violent material. 37 - * Posting or threatening to post other people's personally identifying information ("doxing"). 38 - * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 - * Inappropriate photography or recording. 40 - * Inappropriate physical contact. You should have someone's consent before touching them. 41 - * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 - * Deliberate intimidation, stalking or following (online or in person). 43 - * Advocating for, or encouraging, any of the above behavior. 44 - * Sustained disruption of community events, including talks and presentations. 34 + - Violence, threats of violence or violent language directed against another person. 35 + - Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 + - Posting or displaying sexually explicit or violent material. 37 + - Posting or threatening to post other people's personally identifying information ("doxing"). 38 + - Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 + - Inappropriate photography or recording. 40 + - Inappropriate physical contact. You should have someone's consent before touching them. 41 + - Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 + - Deliberate intimidation, stalking or following (online or in person). 43 + - Advocating for, or encouraging, any of the above behavior. 44 + - Sustained disruption of community events, including talks and presentations. 45 45 46 46 ## 5. Weapons Policy 47 47 ··· 58 58 ## 7. Reporting Guidelines 59 59 60 60 If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. j.zhao2k19@gmail.com. 61 - 62 - 63 61 64 62 Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 63 66 64 ## 8. Addressing Grievances 67 65 68 - If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 69 - 66 + If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 70 67 71 68 ## 9. Scope 72 69 ··· 80 77 81 78 ## 11. License and attribution 82 79 83 - The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 80 + The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 84 81 85 82 Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 86 83
+1 -1
content/advanced/creating components.md
··· 2 2 title: Creating your own Quartz components 3 3 --- 4 4 5 - See the [component listing](/tags/component) for a full-list of the Quartz built-in components. 5 + See the [component listing](/tags/component) for a full-list of the Quartz built-in components.
+2 -1
content/advanced/making plugins.md
··· 5 5 This part of the documentation will assume you have some basic coding knowledge and will include code snippets that describe the interface of what Quartz plugins should look like. 6 6 7 7 ## Transformers 8 + 8 9 ```ts 9 10 export type QuartzTransformerPluginInstance = { 10 11 name: string ··· 17 18 18 19 ## Filters 19 20 20 - ## Emitters 21 + ## Emitters
+27 -24
content/configuration.md
··· 2 2 title: Configuration 3 3 --- 4 4 5 - Quartz is meant to be extremely configurable, even if you don't know any coding. Most of the configuration you should need can be done by just editing `quartz.config.ts`. 5 + Quartz is meant to be extremely configurable, even if you don't know any coding. Most of the configuration you should need can be done by just editing `quartz.config.ts`. 6 6 7 7 If you edit this file using a text-editor that has TypeScript language support like VSCode, it will warn you when you you've made an error in your configuration. 8 8 ··· 16 16 ``` 17 17 18 18 ## General Configuration 19 + 19 20 This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: 20 21 21 22 - `pageTitle`: used as an anchor to return to the home page. This is also used when generating the [[RSS Feed]] for your site. 22 23 - `enableSPA`: whether to enable [[SPA Routing]] on your site. 23 24 - `enablePopovers`: whether to enable [[popover previews]] on your site. 24 - - `analytics`: what to use for analytics on your site. Values can be 25 - - `null`: don't use analytics; 26 - - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or 27 - - `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics 28 - - `caononicalUrl`: sometimes called `baseURL` in other site generators. This is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `https://quartz.jzhao.xyz/` for this site). Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter *where* you end up actually deploying it. 29 - - `ignorePatterns`: a list of [glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. 25 + - `analytics`: what to use for analytics on your site. Values can be 26 + - `null`: don't use analytics; 27 + - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or 28 + - `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics 29 + - `caononicalUrl`: sometimes called `baseURL` in other site generators. This is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `https://quartz.jzhao.xyz/` for this site). Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it. 30 + - `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. 30 31 - `theme`: configure how the site looks. 31 - - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. 32 - - `header`: Font to use for headers 33 - - `code`: Font for inline and block quotes. 34 - - `body`: Font for everything 35 - - `colors`: controls the theming of the site. 36 - - `light`: page background 37 - - `lightgray`: borders 38 - - `gray`: graph links, heavier borders 39 - - `darkgray`: body text 40 - - `dark`: header text and icons 41 - - `secondary`: link colour, current [[graph view|graph]] node 42 - - `tertiary`: hover states and visited [[graph view|graph]] nodes 43 - - `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]] 32 + - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. 33 + - `header`: Font to use for headers 34 + - `code`: Font for inline and block quotes. 35 + - `body`: Font for everything 36 + - `colors`: controls the theming of the site. 37 + - `light`: page background 38 + - `lightgray`: borders 39 + - `gray`: graph links, heavier borders 40 + - `darkgray`: body text 41 + - `dark`: header text and icons 42 + - `secondary`: link colour, current [[graph view|graph]] node 43 + - `tertiary`: hover states and visited [[graph view|graph]] nodes 44 + - `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]] 44 45 45 46 ## Plugins 47 + 46 48 You can think of Quartz plugins as a series of transformations over content. 47 49 48 50 ![[quartz-transform-pipeline.png]] ··· 62 64 By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz. 63 65 64 66 > [!note] 65 - > Each node is modified by every transformer *in order*. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins. 67 + > Each node is modified by every transformer _in order_. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins. 66 68 67 69 Additionally, plugins may also have their own configuration settings that you can pass in. For example, the [[Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax. 68 70 69 71 ```ts 70 72 transformers: [ 71 - Plugin.FrontMatter(), // uses default options 72 - Plugin.Latex({ renderEngine: 'katex' }) // specify some options 73 + Plugin.FrontMatter(), // uses default options 74 + Plugin.Latex({ renderEngine: "katex" }), // specify some options 73 75 ] 74 76 ``` 75 77 76 78 ### Layout 77 - Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To make sure that 79 + 80 + Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To make sure that 78 81 79 82 ### Components
+8 -2
content/features/Latex.md
··· 3 3 ## Formatting 4 4 5 5 ### Block Math 6 + 6 7 Block math can be rendered by delimiting math expression with `$$`. 7 8 8 9 ``` ··· 20 21 $$ 21 22 22 23 ### Inline Math 24 + 23 25 Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$ 24 26 25 27 ### Escaping symbols 26 - There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex. 28 + 29 + There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex. 27 30 28 31 To get around this, you can escape the dollar sign by doing `\$` instead. 29 32 30 33 For example: 34 + 31 35 - Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2 32 36 - Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2 33 37 34 38 ## MathJax 39 + 35 40 In `quartz.config.ts`, you can configure Quartz to use [MathJax SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html) by replacing `Plugin.Latex({ renderEngine: 'katex' })` with `Plugin.Latex({ renderEngine: 'mathjax' })` 36 41 37 42 ## Customization 43 + 38 44 - Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`. 39 - - Plugin: `quartz/plugins/transformers/latex.ts` 45 + - Plugin: `quartz/plugins/transformers/latex.ts`
+1 -1
content/features/Mermaid diagrams.md
··· 1 1 > [!warning] 2 - > Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is *after* `Plugin.SyntaxHighlighting()`. 2 + > Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`. 3 3 4 4 ```mermaid 5 5 sequenceDiagram
+1 -1
content/features/SPA Routing.md
··· 1 - Single-page-app style rendering. This prevents flashes of unstyled content and improves smoothness of Quartz 1 + Single-page-app style rendering. This prevents flashes of unstyled content and improves smoothness of Quartz
+3 -2
content/features/backlinks.md
··· 1 1 --- 2 2 title: Backlinks 3 3 tags: 4 - - component 4 + - component 5 5 --- 6 6 7 7 A backlink for a note is a link from another note to that note. Links in the backlink pane also feature rich [[popover previews]] if you have that feature enabled. 8 8 9 9 ## Customization 10 + 10 11 - Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.config.ts`. 11 12 - Component: `quartz/components/Backlinks.tsx` 12 13 - Style: `quartz/components/styles/backlinks.scss` 13 - - Script: `quartz/components/scripts/search.inline.ts` 14 + - Script: `quartz/components/scripts/search.inline.ts`
+11 -10
content/features/callouts.md
··· 3 3 --- 4 4 5 5 > [!warning] 6 - > Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is *after* `Plugin.SyntaxHighlighting()`. 7 - 6 + > Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`. 8 7 9 8 > [!info] 10 9 > Default title 11 10 12 11 > [!question]+ Can callouts be nested? 12 + > 13 13 > > [!todo]- Yes!, they can. 14 - > > > [!example] You can even use multiple layers of nesting. 14 + > > 15 + > > > [!example] You can even use multiple layers of nesting. 15 16 16 17 > [!EXAMPLE] Examples 17 18 > ··· 21 22 > 22 23 > Aliases: note 23 24 24 - > [!abstract] Summaries 25 + > [!abstract] Summaries 25 26 > 26 27 > Aliases: abstract, summary, tldr 27 28 28 - > [!info] Info 29 + > [!info] Info 29 30 > 30 31 > Aliases: info, todo 31 32 32 - > [!tip] Hint 33 + > [!tip] Hint 33 34 > 34 35 > Aliases: tip, hint, important 35 36 36 - > [!success] Success 37 + > [!success] Success 37 38 > 38 39 > Aliases: success, check, done 39 40 40 - > [!question] Question 41 + > [!question] Question 41 42 > 42 43 > Aliases: question, help, faq 43 44 44 - > [!warning] Warning 45 + > [!warning] Warning 45 46 > 46 47 > Aliases: warning, caution, attention 47 48 48 - > [!failure] Failure 49 + > [!failure] Failure 49 50 > 50 51 > Aliases: failure, fail, missing 51 52
+6 -4
content/features/full-text search.md
··· 1 1 --- 2 2 title: Full-text Search 3 - tags: 4 - - component 3 + tags: 4 + - component 5 5 --- 6 6 7 7 Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words. 8 8 9 - It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. 9 + It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. 10 10 11 11 This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). 12 12 ··· 14 14 > Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. 15 15 16 16 ### Indexing Behaviour 17 + 17 18 By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed. 18 19 19 20 It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title and content, weighing title matches above content matches. 20 21 21 22 ## Customization 23 + 22 24 - Removing search: delete all usages of `Component.Search()` from `quartz.config.ts`. 23 25 - Component: `quartz/components/Search.tsx` 24 26 - Style: `quartz/components/styles/search.scss` 25 27 - Script: `quartz/components/scripts/search.inline.ts` 26 - - You can edit `contextWindowWords` or `numSearchResults` to suit your needs 28 + - You can edit `contextWindowWords` or `numSearchResults` to suit your needs
+12 -11
content/features/graph view.md
··· 1 1 --- 2 2 title: "Graph View" 3 3 tags: 4 - - component 4 + - component 5 5 --- 6 6 7 - Quartz features a graph-view that can show both a local graph view and a global graph view. 7 + Quartz features a graph-view that can show both a local graph view and a global graph view. 8 8 9 - - The local graph view shows files that either link to the current file or are linked from the current file. In other words, it shows all notes that are *at most* one hop away. 10 - - The global graph view can be toggled by clicking the graph icon on the top-right of the local graph view. It shows *all* the notes in your graph and how they connect to each other. 9 + - The local graph view shows files that either link to the current file or are linked from the current file. In other words, it shows all notes that are _at most_ one hop away. 10 + - The global graph view can be toggled by clicking the graph icon on the top-right of the local graph view. It shows _all_ the notes in your graph and how they connect to each other. 11 11 12 12 By default, the node radius is proportional to the total number of incoming and outgoing internal links from that file. 13 13 ··· 17 17 > Graph View requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. 18 18 19 19 ## Customization 20 + 20 21 Most configuration can be done by passing in options to `Component.Graph()`. 21 22 22 23 For example, here's what the default configuration looks like: ··· 26 27 localGraph: { 27 28 drag: true, // whether to allow panning the view around 28 29 zoom: true, // whether to allow zooming in and out 29 - depth: 1, // how many hops of notes to display 30 + depth: 1, // how many hops of notes to display 30 31 scale: 1.1, // default view scale 31 - repelForce: 0.5, // how much nodes should repel each other 32 + repelForce: 0.5, // how much nodes should repel each other 32 33 centerForce: 0.3, // how much force to use when trying to center the nodes 33 34 linkDistance: 30, // how long should the links be by default? 34 - fontSize: 0.6, // what size should the node labels be? 35 - opacityScale: 1 // how quickly do we fade out the labels when zooming out? 35 + fontSize: 0.6, // what size should the node labels be? 36 + opacityScale: 1, // how quickly do we fade out the labels when zooming out? 36 37 }, 37 38 globalGraph: { 38 39 drag: true, ··· 43 44 centerForce: 0.3, 44 45 linkDistance: 30, 45 46 fontSize: 0.6, 46 - opacityScale: 1 47 - } 47 + opacityScale: 1, 48 + }, 48 49 }) 49 50 ``` 50 51 ··· 55 56 - Removing graph view: delete all usages of `Component.Graph()` from `quartz.config.ts`. 56 57 - Component: `quartz/components/Graph.tsx` 57 58 - Style: `quartz/components/styles/graph.scss` 58 - - Script: `quartz/components/scripts/graph.inline.ts` 59 + - Script: `quartz/components/scripts/graph.inline.ts`
+1 -1
content/features/index.md
··· 1 1 --- 2 2 title: Feature List 3 - --- 3 + ---
+2 -1
content/features/popover previews.md
··· 9 9 When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover. 10 10 11 11 ## Configuration 12 + 12 13 - Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`. 13 14 - Style: `quartz/components/styles/popover.scss` 14 - - Script: `quartz/components/scripts/popover.inline.ts` 15 + - Script: `quartz/components/scripts/popover.inline.ts`
+13 -6
content/features/syntax highlighting.md
··· 12 12 > Syntax highlighting does have an impact on build speed if you have a lot of code snippets in your notes. 13 13 14 14 ## Formatting 15 + 15 16 Text inside `backticks` on a line will be formatted like code. 16 17 17 18 ```` ··· 37 38 ``` 38 39 39 40 ### Titles 41 + 40 42 Add a file title to your code block, with text inside double quotes (`""`): 41 43 42 44 ```` 43 45 ```js title="..." 44 - 46 + 45 47 ``` 46 48 ```` 47 49 ··· 56 58 ``` 57 59 58 60 ### Line highlighting 61 + 59 62 Place a numeric range inside `{}`. 60 63 61 64 ```` 62 65 ```js {1-3,4} 63 - 66 + 64 67 ``` 65 68 ```` 66 69 ··· 75 78 ``` 76 79 77 80 ### Word highlighting 81 + 78 82 A series of characters, like a literal regex. 79 83 80 84 ```` ··· 85 89 ```` 86 90 87 91 ```js /useState/ 88 - const [age, setAge] = useState(50); 89 - const [name, setName] = useState('Taylor'); 92 + const [age, setAge] = useState(50) 93 + const [name, setName] = useState("Taylor") 90 94 ``` 91 95 92 96 ### Line numbers 97 + 93 98 Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`: 94 99 95 100 ```` 96 101 ```js showLineNumbers{number} 97 - 102 + 98 103 ``` 99 104 ```` 100 105 ··· 109 114 ``` 110 115 111 116 ### Escaping code blocks 117 + 112 118 You can format a codeblock inside of a codeblock by wrapping it with another level of backtick fences that has one more backtick than the previous fence. 113 119 114 120 ````` ··· 121 127 ````` 122 128 123 129 ## Customization 130 + 124 131 - Removing syntax highlighting: delete all usages of `Plugin.SyntaxHighlighting()` from `quartz.config.ts`. 125 132 - Style: By default, Quartz uses derivatives of the GitHub light and dark themes. You can customize the colours in the `quartz/styles/syntax.scss` file. 126 - - Plugin: `quartz/plugins/transformers/syntax.ts` 133 + - Plugin: `quartz/plugins/transformers/syntax.ts`
+2 -2
content/features/table of contents.md
··· 1 1 --- 2 2 title: "Table of Contents" 3 3 tags: 4 - - component 5 - --- 4 + - component 5 + ---
+5 -5
content/features/upcoming features.md
··· 12 12 - custom md blocks (e.g. for poetry) 13 13 - sidenotes? [https://github.com/capnfabs/paperesque](https://github.com/capnfabs/paperesque) 14 14 - watch mode 15 - - watch for markdown changes and quartz config changes 16 - - markdown changes only involve processing that single markdown file (at least for parsing) and then rerunning the filter and emitters 17 - - config changes rebuild the whole thing 15 + - watch for markdown changes and quartz config changes 16 + - markdown changes only involve processing that single markdown file (at least for parsing) and then rerunning the filter and emitters 17 + - config changes rebuild the whole thing 18 18 - direct match in search using double quotes 19 19 - attachments path 20 20 - [https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI](https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI) ··· 26 26 - audio/video embed styling 27 27 - Canvas 28 28 - mermaid styling: [https://mermaid.js.org/config/theming.html#theme-variables-reference-table](https://mermaid.js.org/config/theming.html#theme-variables-reference-table) 29 - - [https://github.com/jackyzha0/quartz/issues/331](https://github.com/jackyzha0/quartz/issues/331) 29 + - [https://github.com/jackyzha0/quartz/issues/331](https://github.com/jackyzha0/quartz/issues/331) 30 30 - block links: [https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note](https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note) 31 31 - note/header/block transcludes: [https://help.obsidian.md/Linking+notes+and+files/Embedding+files](https://help.obsidian.md/Linking+notes+and+files/Embedding+files) 32 32 - parse all images in page: use this for page lists if applicable? 33 - - CV mode? with print stylesheet 33 + - CV mode? with print stylesheet
+3 -1
content/index.md
··· 5 5 Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, wikis, and [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. 6 6 7 7 ## 🪴 Get Started 8 + 8 9 Quartz requires **at least [Node](https://nodejs.org/) v16** to function correctly. In your terminal of choice, enter the following commands line by line: 9 10 10 11 ```shell ··· 26 27 - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes 27 28 - Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]] 28 29 29 - For a comprehensive list of features, visit the [features page](/features). You can read more the *why* behind these features on the [[philosophy]] page. 30 + For a comprehensive list of features, visit the [features page](/features). You can read more the _why_ behind these features on the [[philosophy]] page. 30 31 31 32 ### 🚧 Troubleshooting 33 + 32 34 Having trouble with Quartz? Try searching for your issue using the search feature. If you're still having trouble, feel free to [submit an issue](https://github.com/jackyzha0/quartz/issues) if you feel you found a bug or ask for help in our [Discord Community](https://discord.gg/cRFFHYye7t).
+2 -2
content/philosophy.md
··· 5 5 ## A garden should be a true hypertext 6 6 7 7 > The garden is the web as topology. Every walk through the garden creates new paths, new meanings, and when we add things to the garden we add them in a way that allows many future, unpredicted relationships. 8 - > 8 + > 9 9 > (The Garden and the Stream) 10 10 11 11 The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes? The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking. ··· 20 20 21 21 The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea. 22 22 23 - Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing. 23 + Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing. 24 24 25 25 > “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming 26 26
+1 -1
content/tags/component.md
··· 2 2 title: Components 3 3 --- 4 4 5 - Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information. 5 + Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information.
+5 -3
globals.d.ts
··· 1 1 export declare global { 2 2 interface Document { 3 - addEventListener<K extends keyof CustomEventMap>(type: K, 4 - listener: (this: Document, ev: CustomEventMap[K]) => void): void; 5 - dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void; 3 + addEventListener<K extends keyof CustomEventMap>( 4 + type: K, 5 + listener: (this: Document, ev: CustomEventMap[K]) => void, 6 + ): void 7 + dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void 6 8 } 7 9 interface Window { 8 10 spaNavigate(url: URL, isBack: boolean = false)
+2 -2
index.d.ts
··· 1 - declare module '*.scss' { 1 + declare module "*.scss" { 2 2 const content: string 3 3 export = content 4 4 } 5 5 6 6 // dom custom event 7 7 interface CustomEventMap { 8 - "nav": CustomEvent<{ url: CanonicalSlug }>; 8 + nav: CustomEvent<{ url: CanonicalSlug }> 9 9 } 10 10 11 11 declare const fetchData: Promise<ContentIndex>
+32 -43
quartz.config.ts
··· 7 7 enableSPA: true, 8 8 enablePopovers: true, 9 9 analytics: { 10 - provider: 'plausible', 10 + provider: "plausible", 11 11 }, 12 12 baseUrl: "quartz.jzhao.xyz", 13 13 ignorePatterns: ["private", "templates"], ··· 19 19 }, 20 20 colors: { 21 21 lightMode: { 22 - light: '#faf8f8', 23 - lightgray: '#e5e5e5', 24 - gray: '#b8b8b8', 25 - darkgray: '#4e4e4e', 26 - dark: '#2b2b2b', 27 - secondary: '#284b63', 28 - tertiary: '#84a59d', 29 - highlight: 'rgba(143, 159, 169, 0.15)', 22 + light: "#faf8f8", 23 + lightgray: "#e5e5e5", 24 + gray: "#b8b8b8", 25 + darkgray: "#4e4e4e", 26 + dark: "#2b2b2b", 27 + secondary: "#284b63", 28 + tertiary: "#84a59d", 29 + highlight: "rgba(143, 159, 169, 0.15)", 30 30 }, 31 31 darkMode: { 32 - light: '#161618', 33 - lightgray: '#393639', 34 - gray: '#646464', 35 - darkgray: '#d4d4d4', 36 - dark: '#ebebec', 37 - secondary: '#7b97aa', 38 - tertiary: '#84a59d', 39 - highlight: 'rgba(143, 159, 169, 0.15)', 32 + light: "#161618", 33 + lightgray: "#393639", 34 + gray: "#646464", 35 + darkgray: "#d4d4d4", 36 + dark: "#ebebec", 37 + secondary: "#7b97aa", 38 + tertiary: "#84a59d", 39 + highlight: "rgba(143, 159, 169, 0.15)", 40 40 }, 41 - } 42 - } 41 + }, 42 + }, 43 43 } 44 44 45 45 const sharedPageComponents = { ··· 47 47 header: [], 48 48 footer: Component.Footer({ 49 49 links: { 50 - "GitHub": "https://github.com/jackyzha0/quartz", 51 - "Discord Community": "https://discord.gg/cRFFHYye7t" 52 - } 53 - }) 50 + GitHub: "https://github.com/jackyzha0/quartz", 51 + "Discord Community": "https://discord.gg/cRFFHYye7t", 52 + }, 53 + }), 54 54 } 55 55 56 56 const contentPageLayout: PageLayout = { 57 - beforeBody: [ 58 - Component.ArticleTitle(), 59 - Component.ReadingTime(), 60 - Component.TagList(), 61 - ], 57 + beforeBody: [Component.ArticleTitle(), Component.ReadingTime(), Component.TagList()], 62 58 left: [ 63 59 Component.PageTitle(), 64 60 Component.MobileOnly(Component.Spacer()), ··· 66 62 Component.Darkmode(), 67 63 Component.DesktopOnly(Component.TableOfContents()), 68 64 ], 69 - right: [ 70 - Component.Graph(), 71 - Component.Backlinks(), 72 - ], 65 + right: [Component.Graph(), Component.Backlinks()], 73 66 } 74 67 75 68 const listPageLayout: PageLayout = { 76 - beforeBody: [ 77 - Component.ArticleTitle() 78 - ], 69 + beforeBody: [Component.ArticleTitle()], 79 70 left: [ 80 71 Component.PageTitle(), 81 72 Component.MobileOnly(Component.Spacer()), 82 73 Component.Search(), 83 - Component.Darkmode() 74 + Component.Darkmode(), 84 75 ], 85 76 right: [], 86 77 } ··· 92 83 Plugin.FrontMatter(), 93 84 Plugin.TableOfContents(), 94 85 Plugin.CreatedModifiedDate({ 95 - priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower 86 + priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower 96 87 }), 97 88 Plugin.SyntaxHighlighting(), 98 89 Plugin.ObsidianFlavoredMarkdown(), 99 90 Plugin.GitHubFlavoredMarkdown(), 100 - Plugin.CrawlLinks({ markdownLinkResolution: 'shortest' }), 101 - Plugin.Latex({ renderEngine: 'katex' }), 91 + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), 92 + Plugin.Latex({ renderEngine: "katex" }), 102 93 Plugin.Description(), 103 94 ], 104 - filters: [ 105 - Plugin.RemoveDrafts(), 106 - ], 95 + filters: [Plugin.RemoveDrafts()], 107 96 emitters: [ 108 97 Plugin.AliasRedirects(), 109 98 Plugin.ContentPage({ ··· 125 114 enableSiteMap: true, 126 115 enableRSS: true, 127 116 }), 128 - ] 117 + ], 129 118 }, 130 119 } 131 120
+200 -149
quartz/bootstrap-cli.mjs
··· 1 1 #!/usr/bin/env node 2 - import { promises, readFileSync } from 'fs' 3 - import yargs from 'yargs' 4 - import path from 'path' 5 - import { hideBin } from 'yargs/helpers' 6 - import esbuild from 'esbuild' 7 - import chalk from 'chalk' 8 - import { sassPlugin } from 'esbuild-sass-plugin' 9 - import fs from 'fs' 10 - import { intro, isCancel, outro, select, text } from '@clack/prompts' 11 - import { rimraf } from 'rimraf' 12 - import prettyBytes from 'pretty-bytes' 13 - import { spawnSync } from 'child_process' 2 + import { promises, readFileSync } from "fs" 3 + import yargs from "yargs" 4 + import path from "path" 5 + import { hideBin } from "yargs/helpers" 6 + import esbuild from "esbuild" 7 + import chalk from "chalk" 8 + import { sassPlugin } from "esbuild-sass-plugin" 9 + import fs from "fs" 10 + import { intro, isCancel, outro, select, text } from "@clack/prompts" 11 + import { rimraf } from "rimraf" 12 + import prettyBytes from "pretty-bytes" 13 + import { spawnSync } from "child_process" 14 14 15 - const UPSTREAM_NAME = 'upstream' 16 - const QUARTZ_SOURCE_BRANCH = 'v4-alpha' 15 + const UPSTREAM_NAME = "upstream" 16 + const QUARTZ_SOURCE_BRANCH = "v4-alpha" 17 17 const cwd = process.cwd() 18 18 const cacheDir = path.join(cwd, ".quartz-cache") 19 19 const cacheFile = "./.quartz-cache/transpiled-build.mjs" ··· 24 24 const CommonArgv = { 25 25 directory: { 26 26 string: true, 27 - alias: ['d'], 28 - default: 'content', 29 - describe: 'directory to look for content files' 27 + alias: ["d"], 28 + default: "content", 29 + describe: "directory to look for content files", 30 30 }, 31 31 verbose: { 32 32 boolean: true, 33 - alias: ['v'], 33 + alias: ["v"], 34 34 default: false, 35 - describe: 'print out extra logging information' 36 - } 35 + describe: "print out extra logging information", 36 + }, 37 37 } 38 38 39 39 const SyncArgv = { ··· 41 41 commit: { 42 42 boolean: true, 43 43 default: true, 44 - describe: 'create a git commit for your unsaved changes' 44 + describe: "create a git commit for your unsaved changes", 45 45 }, 46 46 push: { 47 47 boolean: true, 48 48 default: true, 49 - describe: 'push updates to your Quartz fork' 49 + describe: "push updates to your Quartz fork", 50 50 }, 51 51 force: { 52 52 boolean: true, 53 - alias: ['f'], 53 + alias: ["f"], 54 54 default: true, 55 - describe: 'whether to apply the --force flag to git commands' 55 + describe: "whether to apply the --force flag to git commands", 56 56 }, 57 57 pull: { 58 58 boolean: true, 59 59 default: true, 60 - describe: 'pull updates from your Quartz fork' 61 - } 60 + describe: "pull updates from your Quartz fork", 61 + }, 62 62 } 63 63 64 64 const BuildArgv = { 65 65 ...CommonArgv, 66 66 output: { 67 67 string: true, 68 - alias: ['o'], 69 - default: 'public', 70 - describe: 'output folder for files' 68 + alias: ["o"], 69 + default: "public", 70 + describe: "output folder for files", 71 71 }, 72 72 serve: { 73 73 boolean: true, 74 74 default: false, 75 - describe: 'run a local server to live-preview your Quartz' 75 + describe: "run a local server to live-preview your Quartz", 76 76 }, 77 77 port: { 78 78 number: true, 79 79 default: 8080, 80 - describe: 'port to serve Quartz on' 80 + describe: "port to serve Quartz on", 81 81 }, 82 82 } 83 - 84 83 85 84 function escapePath(fp) { 86 85 return fp ··· 91 90 } 92 91 93 92 function exitIfCancel(val) { 94 - 95 93 if (isCancel(val)) { 96 94 outro(chalk.red("Exiting")) 97 95 process.exit(0) ··· 101 99 } 102 100 103 101 async function stashContentFolder(contentFolder) { 104 - await fs.promises.cp(contentFolder, contentCacheFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true }) 102 + await fs.promises.cp(contentFolder, contentCacheFolder, { 103 + force: true, 104 + recursive: true, 105 + verbatimSymlinks: true, 106 + preserveTimestamps: true, 107 + }) 105 108 await fs.promises.rm(contentFolder, { force: true, recursive: true }) 106 109 } 107 110 108 111 async function popContentFolder(contentFolder) { 109 - await fs.promises.cp(contentCacheFolder, contentFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true }) 112 + await fs.promises.cp(contentCacheFolder, contentFolder, { 113 + force: true, 114 + recursive: true, 115 + verbatimSymlinks: true, 116 + preserveTimestamps: true, 117 + }) 110 118 await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) 111 119 } 112 120 113 121 yargs(hideBin(process.argv)) 114 122 .scriptName("quartz") 115 123 .version(version) 116 - .usage('$0 <cmd> [args]') 117 - .command('create', 'Initialize Quartz', CommonArgv, async argv => { 124 + .usage("$0 <cmd> [args]") 125 + .command("create", "Initialize Quartz", CommonArgv, async (argv) => { 118 126 console.log() 119 127 intro(chalk.bgGreen.black(` Quartz v${version} `)) 120 128 const contentFolder = path.join(cwd, argv.directory) 121 - const setupStrategy = exitIfCancel(await select({ 122 - message: `Choose how to initialize the content in \`${contentFolder}\``, 123 - options: [ 124 - { value: 'new', label: "Empty Quartz" }, 125 - { value: 'copy', label: "Replace with an existing folder", hint: "overwrites `content`" }, 126 - { value: 'symlink', label: "Symlink an existing folder", hint: "don't select this unless you know what you are doing!" }, 127 - { value: 'keep', label: "Keep the existing files" }, 128 - ] 129 - })) 129 + const setupStrategy = exitIfCancel( 130 + await select({ 131 + message: `Choose how to initialize the content in \`${contentFolder}\``, 132 + options: [ 133 + { value: "new", label: "Empty Quartz" }, 134 + { value: "copy", label: "Replace with an existing folder", hint: "overwrites `content`" }, 135 + { 136 + value: "symlink", 137 + label: "Symlink an existing folder", 138 + hint: "don't select this unless you know what you are doing!", 139 + }, 140 + { value: "keep", label: "Keep the existing files" }, 141 + ], 142 + }), 143 + ) 130 144 131 145 async function rmContentFolder() { 132 146 const contentStat = await fs.promises.lstat(contentFolder) ··· 139 153 } 140 154 } 141 155 142 - if (setupStrategy === 'copy' || setupStrategy === 'symlink') { 143 - const originalFolder = escapePath(exitIfCancel(await text({ 144 - message: "Enter the full path to existing content folder", 145 - placeholder: 'On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path', 146 - validate(fp) { 147 - const fullPath = escapePath(fp) 148 - if (!fs.existsSync(fullPath)) { 149 - return "The given path doesn't exist" 150 - } else if (!fs.lstatSync(fullPath).isDirectory()) { 151 - return "The given path is not a folder" 152 - } 153 - } 154 - }))) 156 + if (setupStrategy === "copy" || setupStrategy === "symlink") { 157 + const originalFolder = escapePath( 158 + exitIfCancel( 159 + await text({ 160 + message: "Enter the full path to existing content folder", 161 + placeholder: 162 + "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", 163 + validate(fp) { 164 + const fullPath = escapePath(fp) 165 + if (!fs.existsSync(fullPath)) { 166 + return "The given path doesn't exist" 167 + } else if (!fs.lstatSync(fullPath).isDirectory()) { 168 + return "The given path is not a folder" 169 + } 170 + }, 171 + }), 172 + ), 173 + ) 155 174 156 175 await rmContentFolder() 157 - if (setupStrategy === 'copy') { 176 + if (setupStrategy === "copy") { 158 177 await fs.promises.cp(originalFolder, contentFolder, { recursive: true }) 159 - } else if (setupStrategy === 'symlink') { 160 - await fs.promises.symlink(originalFolder, contentFolder, 'dir') 178 + } else if (setupStrategy === "symlink") { 179 + await fs.promises.symlink(originalFolder, contentFolder, "dir") 161 180 } 162 - } else if (setupStrategy === 'new') { 181 + } else if (setupStrategy === "new") { 163 182 await rmContentFolder() 164 183 await fs.promises.mkdir(contentFolder) 165 - await fs.promises.writeFile(path.join(contentFolder, "index.md"), 184 + await fs.promises.writeFile( 185 + path.join(contentFolder, "index.md"), 166 186 `--- 167 187 title: Welcome to Quartz 168 188 --- 169 189 170 190 This is a blank Quartz installation. 171 191 See the [documentation](https://quartz.jzhao.xyz) for how to get started. 172 - ` 192 + `, 173 193 ) 174 194 } 175 - 195 + 176 196 // get a prefered link resolution strategy 177 - const linkResolutionStrategy = exitIfCancel(await select({ 178 - message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`, 179 - options: [ 180 - { value: 'absolute', label: "Treat links as absolute path", hint: "for content made for Quartz 3 and Hugo" }, 181 - { value: 'shortest', label: "Treat links as shortest path", hint: "for most Obsidian vaults" }, 182 - { value: 'relative', label: "Treat links as relative paths", hint: "for just normal Markdown files" }, 183 - ] 184 - })) 197 + const linkResolutionStrategy = exitIfCancel( 198 + await select({ 199 + message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`, 200 + options: [ 201 + { 202 + value: "absolute", 203 + label: "Treat links as absolute path", 204 + hint: "for content made for Quartz 3 and Hugo", 205 + }, 206 + { 207 + value: "shortest", 208 + label: "Treat links as shortest path", 209 + hint: "for most Obsidian vaults", 210 + }, 211 + { 212 + value: "relative", 213 + label: "Treat links as relative paths", 214 + hint: "for just normal Markdown files", 215 + }, 216 + ], 217 + }), 218 + ) 185 219 186 220 // now, do config changes 187 221 const configFilePath = path.join(cwd, "quartz.config.ts") 188 - let configContent = await fs.promises.readFile(configFilePath, { encoding: 'utf-8' }) 189 - configContent = configContent.replace(/markdownLinkResolution: '(.+)'/, `markdownLinkResolution: '${linkResolutionStrategy}'`) 222 + let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" }) 223 + configContent = configContent.replace( 224 + /markdownLinkResolution: '(.+)'/, 225 + `markdownLinkResolution: '${linkResolutionStrategy}'`, 226 + ) 190 227 await fs.promises.writeFile(configFilePath, configContent) 191 228 192 229 outro(`You're all set! Not sure what to do next? Try: ··· 195 232 • Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting) 196 233 `) 197 234 }) 198 - .command('update', 'Get the latest Quartz updates', CommonArgv, async argv => { 235 + .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => { 199 236 const contentFolder = path.join(cwd, argv.directory) 200 237 console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) 201 - console.log('Backing up your content') 238 + console.log("Backing up your content") 202 239 await stashContentFolder(contentFolder) 203 - console.log("Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.") 204 - spawnSync('git', ['pull', UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' }) 240 + console.log( 241 + "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.", 242 + ) 243 + spawnSync("git", ["pull", UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" }) 205 244 await popContentFolder(contentFolder) 206 - console.log(chalk.green('Done!')) 245 + console.log(chalk.green("Done!")) 207 246 }) 208 - .command('sync', 'Sync your Quartz to and from GitHub.', SyncArgv, async argv => { 247 + .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => { 209 248 const contentFolder = path.join(cwd, argv.directory) 210 249 console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) 211 - console.log('Backing up your content') 250 + console.log("Backing up your content") 212 251 213 252 if (argv.commit) { 214 - const currentTimestamp = new Date().toLocaleString('en-US', { dateStyle: "medium", timeStyle: "short" }) 215 - spawnSync('git', ['commit', '-am', `Quartz sync: ${currentTimestamp}`], { stdio: 'inherit' }) 253 + const currentTimestamp = new Date().toLocaleString("en-US", { 254 + dateStyle: "medium", 255 + timeStyle: "short", 256 + }) 257 + spawnSync("git", ["commit", "-am", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" }) 216 258 } 217 259 218 260 await stashContentFolder(contentFolder) 219 261 220 262 if (argv.pull) { 221 - console.log("Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.") 222 - spawnSync('git', ['pull', 'origin', QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' }) 263 + console.log( 264 + "Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.", 265 + ) 266 + spawnSync("git", ["pull", "origin", QUARTZ_SOURCE_BRANCH], { stdio: "inherit" }) 223 267 } 224 268 225 269 await popContentFolder(contentFolder) 226 270 if (argv.push) { 227 271 console.log("Pushing your changes") 228 - const args = argv.force ? 229 - ['push', '-f', 'origin', QUARTZ_SOURCE_BRANCH] : 230 - ['push', 'origin', QUARTZ_SOURCE_BRANCH] 231 - spawnSync('git', args, { stdio: 'inherit' }) 272 + const args = argv.force 273 + ? ["push", "-f", "origin", QUARTZ_SOURCE_BRANCH] 274 + : ["push", "origin", QUARTZ_SOURCE_BRANCH] 275 + spawnSync("git", args, { stdio: "inherit" }) 232 276 } 233 277 234 - console.log(chalk.green('Done!')) 278 + console.log(chalk.green("Done!")) 235 279 }) 236 - .command('build', 'Build Quartz into a bundle of static HTML files', BuildArgv, async argv => { 237 - const result = await esbuild.build({ 238 - entryPoints: [fp], 239 - outfile: path.join("quartz", cacheFile), 240 - bundle: true, 241 - keepNames: true, 242 - platform: "node", 243 - format: "esm", 244 - jsx: "automatic", 245 - jsxImportSource: "preact", 246 - packages: "external", 247 - metafile: true, 248 - sourcemap: true, 249 - plugins: [ 250 - sassPlugin({ 251 - type: 'css-text', 252 - }), 253 - { 254 - name: 'inline-script-loader', 255 - setup(build) { 256 - build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => { 257 - let text = await promises.readFile(args.path, 'utf8') 280 + .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => { 281 + const result = await esbuild 282 + .build({ 283 + entryPoints: [fp], 284 + outfile: path.join("quartz", cacheFile), 285 + bundle: true, 286 + keepNames: true, 287 + platform: "node", 288 + format: "esm", 289 + jsx: "automatic", 290 + jsxImportSource: "preact", 291 + packages: "external", 292 + metafile: true, 293 + sourcemap: true, 294 + plugins: [ 295 + sassPlugin({ 296 + type: "css-text", 297 + }), 298 + { 299 + name: "inline-script-loader", 300 + setup(build) { 301 + build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => { 302 + let text = await promises.readFile(args.path, "utf8") 258 303 259 - // remove default exports that we manually inserted 260 - text = text.replace('export default', '') 261 - text = text.replace('export', '') 304 + // remove default exports that we manually inserted 305 + text = text.replace("export default", "") 306 + text = text.replace("export", "") 262 307 263 - const sourcefile = path.relative(path.resolve('.'), args.path) 264 - const resolveDir = path.dirname(sourcefile) 265 - const transpiled = await esbuild.build({ 266 - stdin: { 267 - contents: text, 268 - loader: 'ts', 269 - resolveDir, 270 - sourcefile, 271 - }, 272 - write: false, 273 - bundle: true, 274 - platform: "browser", 275 - format: "esm", 308 + const sourcefile = path.relative(path.resolve("."), args.path) 309 + const resolveDir = path.dirname(sourcefile) 310 + const transpiled = await esbuild.build({ 311 + stdin: { 312 + contents: text, 313 + loader: "ts", 314 + resolveDir, 315 + sourcefile, 316 + }, 317 + write: false, 318 + bundle: true, 319 + platform: "browser", 320 + format: "esm", 321 + }) 322 + const rawMod = transpiled.outputFiles[0].text 323 + return { 324 + contents: rawMod, 325 + loader: "text", 326 + } 276 327 }) 277 - const rawMod = transpiled.outputFiles[0].text 278 - return { 279 - contents: rawMod, 280 - loader: 'text', 281 - } 282 - }) 283 - } 284 - } 285 - ] 286 - }).catch(err => { 287 - console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) 288 - console.log(`Reason: ${chalk.grey(err)}`) 289 - console.log("hint: make sure all the required dependencies are installed (run `npm install`)") 290 - process.exit(1) 291 - }) 328 + }, 329 + }, 330 + ], 331 + }) 332 + .catch((err) => { 333 + console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) 334 + console.log(`Reason: ${chalk.grey(err)}`) 335 + console.log( 336 + "hint: make sure all the required dependencies are installed (run `npm install`)", 337 + ) 338 + process.exit(1) 339 + }) 292 340 293 341 if (argv.verbose) { 294 - const outputFileName = 'quartz/.quartz-cache/transpiled-build.mjs' 342 + const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs" 295 343 const meta = result.metafile.outputs[outputFileName] 296 - console.log(`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(meta.bytes)})`) 344 + console.log( 345 + `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes( 346 + meta.bytes, 347 + )})`, 348 + ) 297 349 } 298 350 299 351 const { default: buildQuartz } = await import(cacheFile) ··· 302 354 .showHelpOnFail(false) 303 355 .help() 304 356 .strict() 305 - .demandCommand() 306 - .argv 357 + .demandCommand().argv
+2 -2
quartz/bootstrap-worker.mjs
··· 1 1 #!/usr/bin/env node 2 - import workerpool from 'workerpool' 2 + import workerpool from "workerpool" 3 3 const cacheFile = "./.quartz-cache/transpiled-worker.mjs" 4 4 const { parseFiles } = await import(cacheFile) 5 5 workerpool.worker({ 6 - parseFiles 6 + parseFiles, 7 7 })
+46 -32
quartz/build.ts
··· 1 - import 'source-map-support/register.js' 1 + import "source-map-support/register.js" 2 2 import path from "path" 3 3 import { PerfTimer } from "./perf" 4 4 import { rimraf } from "rimraf" ··· 12 12 import cfg from "../quartz.config" 13 13 import { FilePath } from "./path" 14 14 import chokidar from "chokidar" 15 - import { ProcessedContent } from './plugins/vfile' 16 - import WebSocket, { WebSocketServer } from 'ws' 15 + import { ProcessedContent } from "./plugins/vfile" 16 + import WebSocket, { WebSocketServer } from "ws" 17 17 18 18 interface Argv { 19 19 directory: string ··· 29 29 const output = argv.output 30 30 31 31 const pluginCount = Object.values(cfg.plugins).flat().length 32 - const pluginNames = (key: 'transformers' | 'filters' | 'emitters') => cfg.plugins[key].map(plugin => plugin.name) 32 + const pluginNames = (key: "transformers" | "filters" | "emitters") => 33 + cfg.plugins[key].map((plugin) => plugin.name) 33 34 if (argv.verbose) { 34 35 console.log(`Loaded ${pluginCount} plugins`) 35 - console.log(` Transformers: ${pluginNames('transformers').join(", ")}`) 36 - console.log(` Filters: ${pluginNames('filters').join(", ")}`) 37 - console.log(` Emitters: ${pluginNames('emitters').join(", ")}`) 36 + console.log(` Transformers: ${pluginNames("transformers").join(", ")}`) 37 + console.log(` Filters: ${pluginNames("filters").join(", ")}`) 38 + console.log(` Emitters: ${pluginNames("emitters").join(", ")}`) 38 39 } 39 40 40 41 // clean 41 - perf.addEvent('clean') 42 + perf.addEvent("clean") 42 43 await rimraf(output) 43 - console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince('clean')}`) 44 + console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) 44 45 45 46 // glob 46 - perf.addEvent('glob') 47 - const fps = await globby('**/*.md', { 47 + perf.addEvent("glob") 48 + const fps = await globby("**/*.md", { 48 49 cwd: argv.directory, 49 50 ignore: cfg.configuration.ignorePatterns, 50 51 gitignore: true, 51 52 }) 52 - console.log(`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince('glob')}`) 53 + console.log( 54 + `Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, 55 + ) 53 56 54 - const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}` as FilePath) 55 - const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose) 57 + const filePaths = fps.map((fp) => `${argv.directory}${path.sep}${fp}` as FilePath) 58 + const parsedFiles = await parseMarkdown( 59 + cfg.plugins.transformers, 60 + argv.directory, 61 + filePaths, 62 + argv.verbose, 63 + ) 56 64 const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose) 57 65 await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose) 58 66 console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) ··· 60 68 if (argv.serve) { 61 69 const wss = new WebSocketServer({ port: 3001 }) 62 70 const connections: WebSocket[] = [] 63 - wss.on('connection', ws => connections.push(ws)) 71 + wss.on("connection", (ws) => connections.push(ws)) 64 72 65 73 const ignored = await isGitIgnored() 66 74 const contentMap = new Map<FilePath, ProcessedContent>() ··· 69 77 contentMap.set(vfile.data.filePath!, content) 70 78 } 71 79 72 - async function rebuild(fp: string, action: 'add' | 'change' | 'unlink') { 73 - perf.addEvent('rebuild') 80 + async function rebuild(fp: string, action: "add" | "change" | "unlink") { 81 + perf.addEvent("rebuild") 74 82 if (!ignored(fp)) { 75 83 console.log(chalk.yellow(`Detected change in ${fp}, rebuilding...`)) 76 84 const fullPath = `${argv.directory}${path.sep}${fp}` as FilePath 77 - if (action === 'add' || action === 'change') { 78 - const [parsedContent] = await parseMarkdown(cfg.plugins.transformers, argv.directory, [fullPath], argv.verbose) 85 + if (action === "add" || action === "change") { 86 + const [parsedContent] = await parseMarkdown( 87 + cfg.plugins.transformers, 88 + argv.directory, 89 + [fullPath], 90 + argv.verbose, 91 + ) 79 92 contentMap.set(fullPath, parsedContent) 80 - } else if (action === 'unlink') { 93 + } else if (action === "unlink") { 81 94 contentMap.delete(fullPath) 82 95 } 83 96 ··· 85 98 const parsedFiles = [...contentMap.values()] 86 99 const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose) 87 100 await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose) 88 - console.log(chalk.green(`Done rebuilding in ${perf.timeSince('rebuild')}`)) 89 - connections.forEach(conn => conn.send('rebuild')) 101 + console.log(chalk.green(`Done rebuilding in ${perf.timeSince("rebuild")}`)) 102 + connections.forEach((conn) => conn.send("rebuild")) 90 103 } 91 104 } 92 105 93 - const watcher = chokidar.watch('.', { 106 + const watcher = chokidar.watch(".", { 94 107 persistent: true, 95 108 cwd: argv.directory, 96 109 ignoreInitial: true, 97 110 }) 98 111 99 112 watcher 100 - .on('add', fp => rebuild(fp, 'add')) 101 - .on('change', fp => rebuild(fp, 'change')) 102 - .on('unlink', fp => rebuild(fp, 'unlink')) 113 + .on("add", (fp) => rebuild(fp, "add")) 114 + .on("change", (fp) => rebuild(fp, "change")) 115 + .on("unlink", (fp) => rebuild(fp, "unlink")) 103 116 104 117 const server = http.createServer(async (req, res) => { 105 118 await serveHandler(req, res, { ··· 107 120 directoryListing: false, 108 121 }) 109 122 const status = res.statusCode 110 - const statusString = (status >= 200 && status < 300) ? 111 - chalk.green(`[${status}]`) : 112 - (status >= 300 && status < 400) ? 113 - chalk.yellow(`[${status}]`) : 114 - chalk.red(`[${status}]`) 123 + const statusString = 124 + status >= 200 && status < 300 125 + ? chalk.green(`[${status}]`) 126 + : status >= 300 && status < 400 127 + ? chalk.yellow(`[${status}]`) 128 + : chalk.red(`[${status}]`) 115 129 console.log(statusString + chalk.grey(` ${req.url}`)) 116 130 }) 117 131 server.listen(argv.port) 118 132 console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`)) 119 - console.log('hint: exit with ctrl+c') 133 + console.log("hint: exit with ctrl+c") 120 134 } 121 135 }
+20 -20
quartz/cfg.ts
··· 5 5 export type Analytics = 6 6 | null 7 7 | { 8 - provider: 'plausible' 9 - } 8 + provider: "plausible" 9 + } 10 10 | { 11 - provider: 'google', 12 - tagId: string 13 - } 11 + provider: "google" 12 + tagId: string 13 + } 14 14 15 15 export interface GlobalConfiguration { 16 - pageTitle: string, 16 + pageTitle: string 17 17 /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ 18 - enableSPA: boolean, 18 + enableSPA: boolean 19 19 /** Whether to display Wikipedia-style popovers when hovering over links */ 20 - enablePopovers: boolean, 20 + enablePopovers: boolean 21 21 /** Analytics mode */ 22 22 analytics: Analytics 23 23 /** Glob patterns to not search */ 24 - ignorePatterns: string[], 24 + ignorePatterns: string[] 25 25 /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. 26 - * Quartz will avoid using this as much as possible and use relative URLs most of the time 27 - */ 28 - baseUrl?: string, 26 + * Quartz will avoid using this as much as possible and use relative URLs most of the time 27 + */ 28 + baseUrl?: string 29 29 theme: Theme 30 30 } 31 31 32 32 export interface QuartzConfig { 33 - configuration: GlobalConfiguration, 34 - plugins: PluginTypes, 33 + configuration: GlobalConfiguration 34 + plugins: PluginTypes 35 35 } 36 36 37 37 export interface FullPageLayout { 38 38 head: QuartzComponent 39 - header: QuartzComponent[], 40 - beforeBody: QuartzComponent[], 41 - pageBody: QuartzComponent, 42 - left: QuartzComponent[], 43 - right: QuartzComponent[], 44 - footer: QuartzComponent, 39 + header: QuartzComponent[] 40 + beforeBody: QuartzComponent[] 41 + pageBody: QuartzComponent 42 + left: QuartzComponent[] 43 + right: QuartzComponent[] 44 + footer: QuartzComponent 45 45 } 46 46 47 47 export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
+19 -9
quartz/components/Backlinks.tsx
··· 4 4 5 5 function Backlinks({ fileData, allFiles }: QuartzComponentProps) { 6 6 const slug = canonicalizeServer(fileData.slug!) 7 - const backlinkFiles = allFiles.filter(file => file.links?.includes(slug)) 8 - return <div class="backlinks"> 9 - <h3>Backlinks</h3> 10 - <ul class="overflow"> 11 - {backlinkFiles.length > 0 ? 12 - backlinkFiles.map(f => <li><a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">{f.frontmatter?.title}</a></li>) 13 - : <li>No backlinks found</li>} 14 - </ul> 15 - </div> 7 + const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) 8 + return ( 9 + <div class="backlinks"> 10 + <h3>Backlinks</h3> 11 + <ul class="overflow"> 12 + {backlinkFiles.length > 0 ? ( 13 + backlinkFiles.map((f) => ( 14 + <li> 15 + <a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal"> 16 + {f.frontmatter?.title} 17 + </a> 18 + </li> 19 + )) 20 + ) : ( 21 + <li>No backlinks found</li> 22 + )} 23 + </ul> 24 + </div> 25 + ) 16 26 } 17 27 18 28 Backlinks.css = style
+3 -6
quartz/components/Body.tsx
··· 1 1 // @ts-ignore 2 - import clipboardScript from './scripts/clipboard.inline' 3 - import clipboardStyle from './styles/clipboard.scss' 2 + import clipboardScript from "./scripts/clipboard.inline" 3 + import clipboardStyle from "./styles/clipboard.scss" 4 4 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 5 5 6 6 function Body({ children }: QuartzComponentProps) { 7 - return <div id="quartz-body"> 8 - {children} 9 - </div> 7 + return <div id="quartz-body">{children}</div> 10 8 } 11 9 12 10 Body.afterDOMLoaded = clipboardScript 13 11 Body.css = clipboardStyle 14 12 15 13 export default (() => Body) satisfies QuartzComponentConstructor 16 -
+39 -41
quartz/components/Darkmode.tsx
··· 1 - // @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as 1 + // @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as 2 2 // modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads 3 3 // see: https://v8.dev/features/modules#defer 4 4 import darkmodeScript from "./scripts/darkmode.inline" 5 - import styles from './styles/darkmode.scss' 5 + import styles from "./styles/darkmode.scss" 6 6 import { QuartzComponentConstructor } from "./types" 7 7 8 8 function Darkmode() { 9 - return <div class="darkmode"> 10 - <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> 11 - <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}> 12 - <svg 13 - xmlns="http://www.w3.org/2000/svg" 14 - xmlnsXlink="http://www.w3.org/1999/xlink" 15 - version="1.1" 16 - id="dayIcon" 17 - x="0px" 18 - y="0px" 19 - viewBox="0 0 35 35" 20 - style="enable-background:new 0 0 35 35;" 21 - xmlSpace="preserve" 22 - > 23 - <title>Light mode</title> 24 - <path 25 - d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z" 26 - ></path> 27 - </svg> 28 - </label> 29 - <label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}> 30 - <svg 31 - xmlns="http://www.w3.org/2000/svg" 32 - xmlnsXlink="http://www.w3.org/1999/xlink" 33 - version="1.1" 34 - id="nightIcon" 35 - x="0px" 36 - y="0px" 37 - viewBox="0 0 100 100" 38 - style="enable-background='new 0 0 100 100'" 39 - xmlSpace="preserve" 40 - > 41 - <title>Dark mode</title> 42 - <path 43 - d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z" 44 - ></path> 45 - </svg> 46 - </label> 47 - </div> 9 + return ( 10 + <div class="darkmode"> 11 + <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> 12 + <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}> 13 + <svg 14 + xmlns="http://www.w3.org/2000/svg" 15 + xmlnsXlink="http://www.w3.org/1999/xlink" 16 + version="1.1" 17 + id="dayIcon" 18 + x="0px" 19 + y="0px" 20 + viewBox="0 0 35 35" 21 + style="enable-background:new 0 0 35 35;" 22 + xmlSpace="preserve" 23 + > 24 + <title>Light mode</title> 25 + <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path> 26 + </svg> 27 + </label> 28 + <label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}> 29 + <svg 30 + xmlns="http://www.w3.org/2000/svg" 31 + xmlnsXlink="http://www.w3.org/1999/xlink" 32 + version="1.1" 33 + id="nightIcon" 34 + x="0px" 35 + y="0px" 36 + viewBox="0 0 100 100" 37 + style="enable-background='new 0 0 100 100'" 38 + xmlSpace="preserve" 39 + > 40 + <title>Dark mode</title> 41 + <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path> 42 + </svg> 43 + </label> 44 + </div> 45 + ) 48 46 } 49 47 50 48 Darkmode.beforeDOMLoaded = darkmodeScript
+2 -2
quartz/components/Date.tsx
··· 3 3 } 4 4 5 5 export function Date({ date }: Props) { 6 - const formattedDate = date.toLocaleDateString('en-US', { 6 + const formattedDate = date.toLocaleDateString("en-US", { 7 7 year: "numeric", 8 8 month: "short", 9 - day: '2-digit' 9 + day: "2-digit", 10 10 }) 11 11 return <>{formattedDate}</> 12 12 }
+16 -8
quartz/components/Footer.tsx
··· 1 1 import { QuartzComponentConstructor } from "./types" 2 2 import style from "./styles/footer.scss" 3 - import {version} from "../../package.json" 3 + import { version } from "../../package.json" 4 4 5 5 interface Options { 6 6 links: Record<string, string> ··· 10 10 function Footer() { 11 11 const year = new Date().getFullYear() 12 12 const links = opts?.links ?? [] 13 - return <footer> 14 - <hr /> 15 - <p>Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}</p> 16 - <ul>{Object.entries(links).map(([text, link]) => <li> 17 - <a href={link}>{text}</a> 18 - </li>)}</ul> 19 - </footer> 13 + return ( 14 + <footer> 15 + <hr /> 16 + <p> 17 + Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year} 18 + </p> 19 + <ul> 20 + {Object.entries(links).map(([text, link]) => ( 21 + <li> 22 + <a href={link}>{text}</a> 23 + </li> 24 + ))} 25 + </ul> 26 + </footer> 27 + ) 20 28 } 21 29 22 30 Footer.css = style
+38 -25
quartz/components/Graph.tsx
··· 4 4 import style from "./styles/graph.scss" 5 5 6 6 export interface D3Config { 7 - drag: boolean, 8 - zoom: boolean, 9 - depth: number, 10 - scale: number, 11 - repelForce: number, 12 - centerForce: number, 13 - linkDistance: number, 14 - fontSize: number, 7 + drag: boolean 8 + zoom: boolean 9 + depth: number 10 + scale: number 11 + repelForce: number 12 + centerForce: number 13 + linkDistance: number 14 + fontSize: number 15 15 opacityScale: number 16 16 } 17 17 18 18 interface GraphOptions { 19 - localGraph: Partial<D3Config> | undefined, 19 + localGraph: Partial<D3Config> | undefined 20 20 globalGraph: Partial<D3Config> | undefined 21 21 } 22 22 ··· 30 30 centerForce: 0.3, 31 31 linkDistance: 30, 32 32 fontSize: 0.6, 33 - opacityScale: 1 33 + opacityScale: 1, 34 34 }, 35 35 globalGraph: { 36 36 drag: true, ··· 41 41 centerForce: 0.3, 42 42 linkDistance: 30, 43 43 fontSize: 0.6, 44 - opacityScale: 1 45 - } 44 + opacityScale: 1, 45 + }, 46 46 } 47 47 48 48 export default ((opts?: GraphOptions) => { 49 49 function Graph() { 50 50 const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph } 51 51 const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph } 52 - return <div class="graph"> 53 - <h3>Graph View</h3> 54 - <div class="graph-outer"> 55 - <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> 56 - <svg version="1.1" id="global-graph-icon" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 57 - viewBox="0 0 55 55" fill="currentColor" xmlSpace="preserve"> 58 - <path d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17 52 + return ( 53 + <div class="graph"> 54 + <h3>Graph View</h3> 55 + <div class="graph-outer"> 56 + <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> 57 + <svg 58 + version="1.1" 59 + id="global-graph-icon" 60 + xmlns="http://www.w3.org/2000/svg" 61 + xmlnsXlink="http://www.w3.org/1999/xlink" 62 + x="0px" 63 + y="0px" 64 + viewBox="0 0 55 55" 65 + fill="currentColor" 66 + xmlSpace="preserve" 67 + > 68 + <path 69 + d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17 59 70 s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4 60 71 c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562 61 72 C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829 ··· 65 76 C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2 66 77 S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4 67 78 s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2 68 - s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"/> 69 - </svg> 70 - </div> 71 - <div id="global-graph-outer"> 72 - <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> 79 + s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z" 80 + /> 81 + </svg> 82 + </div> 83 + <div id="global-graph-outer"> 84 + <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> 85 + </div> 73 86 </div> 74 - </div> 87 + ) 75 88 } 76 89 77 90 Graph.css = style
+23 -17
quartz/components/Head.tsx
··· 12 12 const iconPath = baseDir + "/static/icon.png" 13 13 const ogImagePath = baseDir + "/static/og-image.png" 14 14 15 - return <head> 16 - <title>{title}</title> 17 - <meta charSet="utf-8" /> 18 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 19 - <meta property="og:title" content={title} /> 20 - <meta property="og:description" content={title} /> 21 - <meta property="og:image" content={ogImagePath} /> 22 - <meta property="og:width" content="1200" /> 23 - <meta property="og:height" content="675" /> 24 - <link rel="icon" href={iconPath} /> 25 - <meta name="description" content={description} /> 26 - <meta name="generator" content="Quartz" /> 27 - <link rel="preconnect" href="https://fonts.googleapis.com"/> 28 - <link rel="preconnect" href="https://fonts.gstatic.com"/> 29 - {css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)} 30 - {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))} 31 - </head> 15 + return ( 16 + <head> 17 + <title>{title}</title> 18 + <meta charSet="utf-8" /> 19 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 20 + <meta property="og:title" content={title} /> 21 + <meta property="og:description" content={title} /> 22 + <meta property="og:image" content={ogImagePath} /> 23 + <meta property="og:width" content="1200" /> 24 + <meta property="og:height" content="675" /> 25 + <link rel="icon" href={iconPath} /> 26 + <meta name="description" content={description} /> 27 + <meta name="generator" content="Quartz" /> 28 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 29 + <link rel="preconnect" href="https://fonts.gstatic.com" /> 30 + {css.map((href) => ( 31 + <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve /> 32 + ))} 33 + {js 34 + .filter((resource) => resource.loadTime === "beforeDOMReady") 35 + .map((res) => JSResourceToScriptElement(res, true))} 36 + </head> 37 + ) 32 38 } 33 39 34 40 return Head
+1 -3
quartz/components/Header.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 2 3 3 function Header({ children }: QuartzComponentProps) { 4 - return (children.length > 0) ? <header> 5 - {children} 6 - </header> : null 4 + return children.length > 0 ? <header>{children}</header> : null 7 5 } 8 6 9 7 Header.css = `
+40 -21
quartz/components/PageList.tsx
··· 17 17 // otherwise, sort lexographically by title 18 18 const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" 19 19 const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" 20 - return f1Title.localeCompare(f2Title) 20 + return f1Title.localeCompare(f2Title) 21 21 } 22 22 23 23 export function PageList({ fileData, allFiles }: QuartzComponentProps) { 24 24 const slug = canonicalizeServer(fileData.slug!) 25 - return <ul class="section-ul"> 26 - {allFiles.sort(byDateAndAlphabetical).map(page => { 27 - const title = page.frontmatter?.title 28 - const pageSlug = canonicalizeServer(page.slug!) 29 - const tags = page.frontmatter?.tags ?? [] 25 + return ( 26 + <ul class="section-ul"> 27 + {allFiles.sort(byDateAndAlphabetical).map((page) => { 28 + const title = page.frontmatter?.title 29 + const pageSlug = canonicalizeServer(page.slug!) 30 + const tags = page.frontmatter?.tags ?? [] 30 31 31 - return <li class="section-li"> 32 - <div class="section"> 33 - {page.dates && <p class="meta"> 34 - <Date date={page.dates.modified} /> 35 - </p>} 36 - <div class="desc"> 37 - <h3><a href={resolveRelative(slug, pageSlug)} class="internal">{title}</a></h3> 38 - </div> 39 - <ul class="tags"> 40 - {tags.map(tag => <li><a class="internal" href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}>#{tag}</a></li>)} 41 - </ul> 42 - </div> 43 - </li> 44 - })} 45 - </ul> 32 + return ( 33 + <li class="section-li"> 34 + <div class="section"> 35 + {page.dates && ( 36 + <p class="meta"> 37 + <Date date={page.dates.modified} /> 38 + </p> 39 + )} 40 + <div class="desc"> 41 + <h3> 42 + <a href={resolveRelative(slug, pageSlug)} class="internal"> 43 + {title} 44 + </a> 45 + </h3> 46 + </div> 47 + <ul class="tags"> 48 + {tags.map((tag) => ( 49 + <li> 50 + <a 51 + class="internal" 52 + href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)} 53 + > 54 + #{tag} 55 + </a> 56 + </li> 57 + ))} 58 + </ul> 59 + </div> 60 + </li> 61 + ) 62 + })} 63 + </ul> 64 + ) 46 65 } 47 66 48 67 PageList.css = `
+5 -1
quartz/components/PageTitle.tsx
··· 5 5 const title = cfg?.pageTitle ?? "Untitled Quartz" 6 6 const slug = canonicalizeServer(fileData.slug!) 7 7 const baseDir = pathToRoot(slug) 8 - return <h1 class="page-title"><a href={baseDir}>{title}</a></h1> 8 + return ( 9 + <h1 class="page-title"> 10 + <a href={baseDir}>{title}</a> 11 + </h1> 12 + ) 9 13 } 10 14 11 15 PageTitle.css = `
+5 -1
quartz/components/ReadingTime.tsx
··· 5 5 const text = fileData.text 6 6 if (text) { 7 7 const { text: timeTaken, words } = readingTime(text) 8 - return <p class="reading-time">{words} words, {timeTaken}</p> 8 + return ( 9 + <p class="reading-time"> 10 + {words} words, {timeTaken} 11 + </p> 12 + ) 9 13 } else { 10 14 return null 11 15 }
+32 -18
quartz/components/Search.tsx
··· 5 5 6 6 export default (() => { 7 7 function Search() { 8 - return <div class="search"> 9 - <div id="search-icon"> 10 - <p>Search</p> 11 - <div></div> 12 - <svg tabIndex={0} aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"> 13 - <title id="title">Search</title> 14 - <desc id="desc">Search</desc> 15 - <g class="search-path" fill="none"> 16 - <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> 17 - <circle cx="8" cy="8" r="7" /> 18 - </g> 19 - </svg> 20 - </div> 21 - <div id="search-container"> 22 - <div id="search-space"> 23 - <input autocomplete="off" id="search-bar" name="search" type="text" aria-label="Search for something" placeholder="Search for something" /> 24 - <div id="results-container"> 8 + return ( 9 + <div class="search"> 10 + <div id="search-icon"> 11 + <p>Search</p> 12 + <div></div> 13 + <svg 14 + tabIndex={0} 15 + aria-labelledby="title desc" 16 + role="img" 17 + xmlns="http://www.w3.org/2000/svg" 18 + viewBox="0 0 19.9 19.7" 19 + > 20 + <title id="title">Search</title> 21 + <desc id="desc">Search</desc> 22 + <g class="search-path" fill="none"> 23 + <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> 24 + <circle cx="8" cy="8" r="7" /> 25 + </g> 26 + </svg> 27 + </div> 28 + <div id="search-container"> 29 + <div id="search-space"> 30 + <input 31 + autocomplete="off" 32 + id="search-bar" 33 + name="search" 34 + type="text" 35 + aria-label="Search for something" 36 + placeholder="Search for something" 37 + /> 38 + <div id="results-container"></div> 25 39 </div> 26 40 </div> 27 41 </div> 28 - </div> 42 + ) 29 43 } 30 44 31 45 Search.afterDOMLoaded = script
+49 -26
quartz/components/TableOfContents.tsx
··· 6 6 import script from "./scripts/toc.inline" 7 7 8 8 interface Options { 9 - layout: 'modern' | 'legacy' 9 + layout: "modern" | "legacy" 10 10 } 11 11 12 12 const defaultOptions: Options = { 13 - layout: 'modern' 13 + layout: "modern", 14 14 } 15 15 16 16 function TableOfContents({ fileData }: QuartzComponentProps) { ··· 18 18 return null 19 19 } 20 20 21 - return <div class="desktop-only"> 22 - <button type="button" id="toc"> 23 - <h3>Table of Contents</h3> 24 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold"> 25 - <polyline points="6 9 12 15 18 9"></polyline> 26 - </svg> 27 - </button> 28 - <div id="toc-content"> 29 - <ul class="overflow"> 30 - {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 31 - <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a> 32 - </li>)} 33 - </ul> 21 + return ( 22 + <div class="desktop-only"> 23 + <button type="button" id="toc"> 24 + <h3>Table of Contents</h3> 25 + <svg 26 + xmlns="http://www.w3.org/2000/svg" 27 + width="24" 28 + height="24" 29 + viewBox="0 0 24 24" 30 + fill="none" 31 + stroke="currentColor" 32 + stroke-width="2" 33 + stroke-linecap="round" 34 + stroke-linejoin="round" 35 + class="fold" 36 + > 37 + <polyline points="6 9 12 15 18 9"></polyline> 38 + </svg> 39 + </button> 40 + <div id="toc-content"> 41 + <ul class="overflow"> 42 + {fileData.toc.map((tocEntry) => ( 43 + <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 44 + <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> 45 + {tocEntry.text} 46 + </a> 47 + </li> 48 + ))} 49 + </ul> 50 + </div> 34 51 </div> 35 - </div> 52 + ) 36 53 } 37 54 TableOfContents.css = modernStyle 38 55 TableOfContents.afterDOMLoaded = script ··· 42 59 return null 43 60 } 44 61 45 - return <details id="toc" open> 46 - <summary> 47 - <h3>Table of Contents</h3> 48 - </summary> 49 - <ul> 50 - {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 51 - <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a> 52 - </li>)} 53 - </ul> 54 - </details> 62 + return ( 63 + <details id="toc" open> 64 + <summary> 65 + <h3>Table of Contents</h3> 66 + </summary> 67 + <ul> 68 + {fileData.toc.map((tocEntry) => ( 69 + <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 70 + <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> 71 + {tocEntry.text} 72 + </a> 73 + </li> 74 + ))} 75 + </ul> 76 + </details> 77 + ) 55 78 } 56 79 LegacyTableOfContents.css = legacyStyle 57 80
+16 -8
quartz/components/TagList.tsx
··· 1 1 import { canonicalizeServer, pathToRoot } from "../path" 2 2 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 - import { slug as slugAnchor } from 'github-slugger' 3 + import { slug as slugAnchor } from "github-slugger" 4 4 5 5 function TagList({ fileData }: QuartzComponentProps) { 6 6 const tags = fileData.frontmatter?.tags 7 7 const slug = canonicalizeServer(fileData.slug!) 8 8 const baseDir = pathToRoot(slug) 9 9 if (tags && tags.length > 0) { 10 - return <ul class="tags">{tags.map(tag => { 11 - const display = `#${tag}` 12 - const linkDest = baseDir + `/tags/${slugAnchor(tag)}` 13 - return <li> 14 - <a href={linkDest} class="internal">{display}</a> 15 - </li> 16 - })}</ul> 10 + return ( 11 + <ul class="tags"> 12 + {tags.map((tag) => { 13 + const display = `#${tag}` 14 + const linkDest = baseDir + `/tags/${slugAnchor(tag)}` 15 + return ( 16 + <li> 17 + <a href={linkDest} class="internal"> 18 + {display} 19 + </a> 20 + </li> 21 + ) 22 + })} 23 + </ul> 24 + ) 17 25 } else { 18 26 return null 19 27 }
+3 -3
quartz/components/index.ts
··· 9 9 import Spacer from "./Spacer" 10 10 import TableOfContents from "./TableOfContents" 11 11 import TagList from "./TagList" 12 - import Graph from "./Graph" 12 + import Graph from "./Graph" 13 13 import Backlinks from "./Backlinks" 14 14 import Search from "./Search" 15 15 import Footer from "./Footer" ··· 33 33 Search, 34 34 Footer, 35 35 DesktopOnly, 36 - MobileOnly 37 - } 36 + MobileOnly, 37 + }
+2 -2
quartz/components/pages/Content.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "../types" 2 - import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' 2 + import { Fragment, jsx, jsxs } from "preact/jsx-runtime" 3 3 import { toJsxRuntime } from "hast-util-to-jsx-runtime" 4 4 5 5 function Content({ tree }: QuartzComponentProps) { 6 6 // @ts-ignore (preact makes it angry) 7 - const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) 7 + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" }) 8 8 return <article class="popover-hint">{content}</article> 9 9 } 10 10
+14 -12
quartz/components/pages/FolderContent.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "../types" 2 - import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' 2 + import { Fragment, jsx, jsxs } from "preact/jsx-runtime" 3 3 import { toJsxRuntime } from "hast-util-to-jsx-runtime" 4 4 import path from "path" 5 5 6 - import style from '../styles/listPage.scss' 6 + import style from "../styles/listPage.scss" 7 7 import { PageList } from "../PageList" 8 8 import { canonicalizeServer } from "../../path" 9 9 10 10 function FolderContent(props: QuartzComponentProps) { 11 11 const { tree, fileData, allFiles } = props 12 12 const folderSlug = canonicalizeServer(fileData.slug!) 13 - const allPagesInFolder = allFiles.filter(file => { 13 + const allPagesInFolder = allFiles.filter((file) => { 14 14 const fileSlug = file.slug ?? "" 15 15 const prefixed = fileSlug.startsWith(folderSlug) 16 16 const folderParts = folderSlug.split(path.posix.sep) ··· 21 21 22 22 const listProps = { 23 23 ...props, 24 - allFiles: allPagesInFolder 24 + allFiles: allPagesInFolder, 25 25 } 26 - 26 + 27 27 // @ts-ignore 28 - const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) 29 - return <div class="popover-hint"> 30 - <article>{content}</article> 31 - <p>{allPagesInFolder.length} items under this folder.</p> 32 - <div> 33 - <PageList {...listProps} /> 28 + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" }) 29 + return ( 30 + <div class="popover-hint"> 31 + <article>{content}</article> 32 + <p>{allPagesInFolder.length} items under this folder.</p> 33 + <div> 34 + <PageList {...listProps} /> 35 + </div> 34 36 </div> 35 - </div> 37 + ) 36 38 } 37 39 38 40 FolderContent.css = style + PageList.css
+13 -11
quartz/components/pages/TagContent.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "../types" 2 - import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' 2 + import { Fragment, jsx, jsxs } from "preact/jsx-runtime" 3 3 import { toJsxRuntime } from "hast-util-to-jsx-runtime" 4 - import style from '../styles/listPage.scss' 4 + import style from "../styles/listPage.scss" 5 5 import { PageList } from "../PageList" 6 6 import { ServerSlug, canonicalizeServer } from "../../path" 7 7 ··· 11 11 12 12 if (slug?.startsWith("tags/")) { 13 13 const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug) 14 - const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag)) 14 + const allPagesWithTag = allFiles.filter((file) => (file.frontmatter?.tags ?? []).includes(tag)) 15 15 const listProps = { 16 16 ...props, 17 - allFiles: allPagesWithTag 17 + allFiles: allPagesWithTag, 18 18 } 19 19 20 20 // @ts-ignore 21 - const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) 22 - return <div class="popover-hint"> 23 - <article>{content}</article> 24 - <p>{allPagesWithTag.length} items with this tag.</p> 25 - <div> 26 - <PageList {...listProps} /> 21 + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" }) 22 + return ( 23 + <div class="popover-hint"> 24 + <article>{content}</article> 25 + <p>{allPagesWithTag.length} items with this tag.</p> 26 + <div> 27 + <PageList {...listProps} /> 28 + </div> 27 29 </div> 28 - </div> 30 + ) 29 31 } else { 30 32 throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`) 31 33 }
+82 -42
quartz/components/renderPage.tsx
··· 1 - import { render } from "preact-render-to-string"; 2 - import { QuartzComponent, QuartzComponentProps } from "./types"; 1 + import { render } from "preact-render-to-string" 2 + import { QuartzComponent, QuartzComponentProps } from "./types" 3 3 import HeaderConstructor from "./Header" 4 4 import BodyConstructor from "./Body" 5 - import { JSResourceToScriptElement, StaticResources } from "../resources"; 6 - import { CanonicalSlug, pathToRoot } from "../path"; 5 + import { JSResourceToScriptElement, StaticResources } from "../resources" 6 + import { CanonicalSlug, pathToRoot } from "../path" 7 7 8 8 interface RenderComponents { 9 9 head: QuartzComponent 10 - header: QuartzComponent[], 11 - beforeBody: QuartzComponent[], 12 - pageBody: QuartzComponent, 13 - left: QuartzComponent[], 14 - right: QuartzComponent[], 15 - footer: QuartzComponent, 10 + header: QuartzComponent[] 11 + beforeBody: QuartzComponent[] 12 + pageBody: QuartzComponent 13 + left: QuartzComponent[] 14 + right: QuartzComponent[] 15 + footer: QuartzComponent 16 16 } 17 17 18 - export function pageResources(slug: CanonicalSlug, staticResources: StaticResources): StaticResources { 18 + export function pageResources( 19 + slug: CanonicalSlug, 20 + staticResources: StaticResources, 21 + ): StaticResources { 19 22 const baseDir = pathToRoot(slug) 20 23 21 24 const contentIndexPath = baseDir + "/static/contentIndex.json" ··· 25 28 css: [baseDir + "/index.css", ...staticResources.css], 26 29 js: [ 27 30 { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, 28 - { loadTime: "beforeDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, 31 + { 32 + loadTime: "beforeDOMReady", 33 + contentType: "inline", 34 + spaPreserve: true, 35 + script: contentIndexScript, 36 + }, 29 37 ...staticResources.js, 30 - { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } 31 - ] 38 + { 39 + src: baseDir + "/postscript.js", 40 + loadTime: "afterDOMReady", 41 + moduleType: "module", 42 + contentType: "external", 43 + }, 44 + ], 32 45 } 33 46 } 34 47 35 - export function renderPage(slug: CanonicalSlug, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string { 36 - const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components 48 + export function renderPage( 49 + slug: CanonicalSlug, 50 + componentData: QuartzComponentProps, 51 + components: RenderComponents, 52 + pageResources: StaticResources, 53 + ): string { 54 + const { 55 + head: Head, 56 + header, 57 + beforeBody, 58 + pageBody: Content, 59 + left, 60 + right, 61 + footer: Footer, 62 + } = components 37 63 const Header = HeaderConstructor() 38 64 const Body = BodyConstructor() 39 65 40 - const LeftComponent = 66 + const LeftComponent = ( 41 67 <div class="left sidebar"> 42 - {left.map(BodyComponent => <BodyComponent {...componentData} />)} 68 + {left.map((BodyComponent) => ( 69 + <BodyComponent {...componentData} /> 70 + ))} 43 71 </div> 72 + ) 44 73 45 - const RightComponent = 74 + const RightComponent = ( 46 75 <div class="right sidebar"> 47 - {right.map(BodyComponent => <BodyComponent {...componentData} />)} 76 + {right.map((BodyComponent) => ( 77 + <BodyComponent {...componentData} /> 78 + ))} 48 79 </div> 80 + ) 49 81 50 - const doc = <html> 51 - <Head {...componentData} /> 52 - <body data-slug={slug}> 53 - <div id="quartz-root" class="page"> 54 - <Body {...componentData}> 55 - {LeftComponent} 56 - <div class="center"> 57 - <div class="page-header"> 58 - <Header {...componentData} > 59 - {header.map(HeaderComponent => <HeaderComponent {...componentData} />)} 60 - </Header> 61 - <div class="popover-hint"> 62 - {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} 82 + const doc = ( 83 + <html> 84 + <Head {...componentData} /> 85 + <body data-slug={slug}> 86 + <div id="quartz-root" class="page"> 87 + <Body {...componentData}> 88 + {LeftComponent} 89 + <div class="center"> 90 + <div class="page-header"> 91 + <Header {...componentData}> 92 + {header.map((HeaderComponent) => ( 93 + <HeaderComponent {...componentData} /> 94 + ))} 95 + </Header> 96 + <div class="popover-hint"> 97 + {beforeBody.map((BodyComponent) => ( 98 + <BodyComponent {...componentData} /> 99 + ))} 100 + </div> 63 101 </div> 102 + <Content {...componentData} /> 64 103 </div> 65 - <Content {...componentData} /> 66 - </div> 67 - {RightComponent} 68 - </Body> 69 - <Footer {...componentData} /> 70 - </div> 71 - </body> 72 - {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} 73 - </html> 104 + {RightComponent} 105 + </Body> 106 + <Footer {...componentData} /> 107 + </div> 108 + </body> 109 + {pageResources.js 110 + .filter((resource) => resource.loadTime === "afterDOMReady") 111 + .map((res) => JSResourceToScriptElement(res))} 112 + </html> 113 + ) 74 114 75 115 return "<!DOCTYPE html>\n" + render(doc) 76 116 }
+3 -1
quartz/components/scripts/callout.inline.ts
··· 7 7 } 8 8 9 9 function setupCallout() { 10 - const collapsible = document.getElementsByClassName(`callout is-collapsible`) as HTMLCollectionOf<HTMLElement> 10 + const collapsible = document.getElementsByClassName( 11 + `callout is-collapsible`, 12 + ) as HTMLCollectionOf<HTMLElement> 11 13 for (const div of collapsible) { 12 14 const title = div.firstElementChild 13 15
+12 -13
quartz/components/scripts/darkmode.inline.ts
··· 1 - const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark' 2 - const currentTheme = localStorage.getItem('theme') ?? userPref 3 - document.documentElement.setAttribute('saved-theme', currentTheme) 1 + const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark" 2 + const currentTheme = localStorage.getItem("theme") ?? userPref 3 + document.documentElement.setAttribute("saved-theme", currentTheme) 4 4 5 5 document.addEventListener("nav", () => { 6 6 const switchTheme = (e: any) => { 7 7 if (e.target.checked) { 8 - document.documentElement.setAttribute('saved-theme', 'dark') 9 - localStorage.setItem('theme', 'dark') 10 - } 11 - else { 12 - document.documentElement.setAttribute('saved-theme', 'light') 13 - localStorage.setItem('theme', 'light') 8 + document.documentElement.setAttribute("saved-theme", "dark") 9 + localStorage.setItem("theme", "dark") 10 + } else { 11 + document.documentElement.setAttribute("saved-theme", "light") 12 + localStorage.setItem("theme", "light") 14 13 } 15 14 } 16 15 17 16 // Darkmode toggle 18 - const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement 19 - toggleSwitch.removeEventListener('change', switchTheme) 20 - toggleSwitch.addEventListener('change', switchTheme) 21 - if (currentTheme === 'dark') { 17 + const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement 18 + toggleSwitch.removeEventListener("change", switchTheme) 19 + toggleSwitch.addEventListener("change", switchTheme) 20 + if (currentTheme === "dark") { 22 21 toggleSwitch.checked = true 23 22 } 24 23 })
+38 -40
quartz/components/scripts/graph.inline.ts
··· 1 1 import { ContentDetails } from "../../plugins/emitters/contentIndex" 2 - import * as d3 from 'd3' 2 + import * as d3 from "d3" 3 3 import { registerEscapeHandler, removeAllChildren } from "./util" 4 4 import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path" 5 5 6 6 type NodeData = { 7 - id: CanonicalSlug, 8 - text: string, 7 + id: CanonicalSlug 8 + text: string 9 9 tags: string[] 10 10 } & d3.SimulationNodeDatum 11 11 12 12 type LinkData = { 13 - source: CanonicalSlug, 13 + source: CanonicalSlug 14 14 target: CanonicalSlug 15 15 } 16 16 ··· 40 40 centerForce, 41 41 linkDistance, 42 42 fontSize, 43 - opacityScale 43 + opacityScale, 44 44 } = JSON.parse(graph.dataset["cfg"]!) 45 45 46 46 const data = await fetchData ··· 66 66 wl.push("__SENTINEL") 67 67 } else { 68 68 neighbourhood.add(cur) 69 - const outgoing = links.filter(l => l.source === cur) 70 - const incoming = links.filter(l => l.target === cur) 69 + const outgoing = links.filter((l) => l.source === cur) 70 + const incoming = links.filter((l) => l.target === cur) 71 71 wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source)) 72 72 } 73 73 } 74 74 } else { 75 - Object.keys(data).forEach(id => neighbourhood.add(id as CanonicalSlug)) 75 + Object.keys(data).forEach((id) => neighbourhood.add(id as CanonicalSlug)) 76 76 } 77 77 78 - const graphData: { nodes: NodeData[], links: LinkData[] } = { 79 - nodes: [...neighbourhood].map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })), 80 - links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) 78 + const graphData: { nodes: NodeData[]; links: LinkData[] } = { 79 + nodes: [...neighbourhood].map((url) => ({ 80 + id: url, 81 + text: data[url]?.title ?? url, 82 + tags: data[url]?.tags ?? [], 83 + })), 84 + links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), 81 85 } 82 86 83 87 const simulation: d3.Simulation<NodeData, LinkData> = d3 ··· 96 100 const width = graph.offsetWidth 97 101 98 102 const svg = d3 99 - .select<HTMLElement, NodeData>('#' + container) 103 + .select<HTMLElement, NodeData>("#" + container) 100 104 .append("svg") 101 105 .attr("width", width) 102 106 .attr("height", height) 103 - .attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) 107 + .attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) 104 108 105 109 // draw links between nodes 106 110 const link = svg ··· 145 149 d.fy = null 146 150 } 147 151 148 - const noop = () => { } 152 + const noop = () => {} 149 153 return d3 150 154 .drag<Element, NodeData>() 151 155 .on("start", enableDrag ? dragstarted : noop) ··· 170 174 const targ = resolveRelative(slug, d.id) 171 175 window.spaNavigate(new URL(targ, getClientSlug(window))) 172 176 }) 173 - .on("mouseover", function(_, d) { 177 + .on("mouseover", function (_, d) { 174 178 const neighbours: CanonicalSlug[] = data[slug].links ?? [] 175 - const neighbourNodes = d3.selectAll<HTMLElement, NodeData>(".node").filter((d) => neighbours.includes(d.id)) 179 + const neighbourNodes = d3 180 + .selectAll<HTMLElement, NodeData>(".node") 181 + .filter((d) => neighbours.includes(d.id)) 176 182 console.log(neighbourNodes) 177 183 const currentId = d.id 178 184 const linkNodes = d3 ··· 183 189 neighbourNodes.transition().duration(200).attr("fill", color) 184 190 185 191 // highlight links 186 - linkNodes 187 - .transition() 188 - .duration(200) 189 - .attr("stroke", "var(--gray)") 190 - .attr("stroke-width", 1) 191 - 192 + linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) 192 193 193 194 const bigFont = fontSize * 1.5 194 195 ··· 199 200 .select("text") 200 201 .transition() 201 202 .duration(200) 202 - .attr('opacityOld', d3.select(parent).select('text').style("opacity")) 203 - .style('opacity', 1) 204 - .style('font-size', bigFont + 'em') 203 + .attr("opacityOld", d3.select(parent).select("text").style("opacity")) 204 + .style("opacity", 1) 205 + .style("font-size", bigFont + "em") 205 206 }) 206 - .on("mouseleave", function(_, d) { 207 + .on("mouseleave", function (_, d) { 207 208 const currentId = d.id 208 209 const linkNodes = d3 209 210 .selectAll(".link") ··· 216 217 .select("text") 217 218 .transition() 218 219 .duration(200) 219 - .style('opacity', d3.select(parent).select('text').attr("opacityOld")) 220 - .style('font-size', fontSize + 'em') 220 + .style("opacity", d3.select(parent).select("text").attr("opacityOld")) 221 + .style("font-size", fontSize + "em") 221 222 }) 222 223 // @ts-ignore 223 224 .call(drag(simulation)) ··· 228 229 .attr("dx", 0) 229 230 .attr("dy", (d) => -nodeRadius(d) + "px") 230 231 .attr("text-anchor", "middle") 231 - .text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " ")) 232 - .style('opacity', (opacityScale - 1) / 3.75) 232 + .text( 233 + (d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "), 234 + ) 235 + .style("opacity", (opacityScale - 1) / 3.75) 233 236 .style("pointer-events", "none") 234 - .style('font-size', fontSize + 'em') 237 + .style("font-size", fontSize + "em") 235 238 .raise() 236 239 // @ts-ignore 237 240 .call(drag(simulation)) ··· 249 252 .on("zoom", ({ transform }) => { 250 253 link.attr("transform", transform) 251 254 node.attr("transform", transform) 252 - const scale = transform.k * opacityScale; 255 + const scale = transform.k * opacityScale 253 256 const scaledOpacity = Math.max((scale - 1) / 3.75, 0) 254 257 labels.attr("transform", transform).style("opacity", scaledOpacity) 255 258 }), ··· 263 266 .attr("y1", (d: any) => d.source.y) 264 267 .attr("x2", (d: any) => d.target.x) 265 268 .attr("y2", (d: any) => d.target.y) 266 - node 267 - .attr("cx", (d: any) => d.x) 268 - .attr("cy", (d: any) => d.y) 269 - labels 270 - .attr("x", (d: any) => d.x) 271 - .attr("y", (d: any) => d.y) 269 + node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y) 270 + labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y) 272 271 }) 273 272 } 274 273 275 274 function renderGlobalGraph() { 276 - const slug = getCanonicalSlug(window) 275 + const slug = getCanonicalSlug(window) 277 276 const container = document.getElementById("global-graph-outer") 278 277 const sidebar = container?.closest(".sidebar") as HTMLElement 279 278 container?.classList.add("active") ··· 305 304 containerIcon?.removeEventListener("click", renderGlobalGraph) 306 305 containerIcon?.addEventListener("click", renderGlobalGraph) 307 306 }) 308 -
+1 -1
quartz/components/scripts/plausible.inline.ts
··· 1 - import Plausible from 'plausible-tracker' 1 + import Plausible from "plausible-tracker" 2 2 const { trackPageview } = Plausible() 3 3 document.addEventListener("nav", () => trackPageview())
+11 -19
quartz/components/scripts/popover.inline.ts
··· 2 2 3 3 // from micromorph/src/utils.ts 4 4 // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 5 - export function normalizeRelativeURLs( 6 - el: Element | Document, 7 - base: string | URL 8 - ) { 5 + export function normalizeRelativeURLs(el: Element | Document, base: string | URL) { 9 6 const update = (el: Element, attr: string, base: string | URL) => { 10 7 el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname) 11 8 } 12 9 13 - el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => 14 - update(item, 'href', base) 15 - ) 10 + el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base)) 16 11 17 - el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => 18 - update(item, 'src', base) 19 - ) 12 + el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base)) 20 13 } 21 14 22 15 const p = new DOMParser() 23 - async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: { clientX: number, clientY: number }) { 16 + async function mouseEnterHandler( 17 + this: HTMLLinkElement, 18 + { clientX, clientY }: { clientX: number; clientY: number }, 19 + ) { 24 20 const link = this 25 21 async function setPosition(popoverElement: HTMLElement) { 26 22 const { x, y } = await computePosition(link, popoverElement, { 27 - middleware: [ 28 - inline({ x: clientX, y: clientY }), 29 - shift(), 30 - flip() 31 - ] 23 + middleware: [inline({ x: clientX, y: clientY }), shift(), flip()], 32 24 }) 33 25 Object.assign(popoverElement.style, { 34 26 left: `${x}px`, ··· 37 29 } 38 30 39 31 // dont refetch if there's already a popover 40 - if ([...link.children].some(child => child.classList.contains("popover"))) { 32 + if ([...link.children].some((child) => child.classList.contains("popover"))) { 41 33 return setPosition(link.lastChild as HTMLElement) 42 34 } 43 35 ··· 68 60 const popoverInner = document.createElement("div") 69 61 popoverInner.classList.add("popover-inner") 70 62 popoverElement.appendChild(popoverInner) 71 - elts.forEach(elt => popoverInner.appendChild(elt)) 63 + elts.forEach((elt) => popoverInner.appendChild(elt)) 72 64 73 65 setPosition(popoverElement) 74 66 link.appendChild(popoverElement) ··· 77 69 const heading = popoverInner.querySelector(hash) as HTMLElement | null 78 70 if (heading) { 79 71 // leave ~12px of buffer when scrolling to a heading 80 - popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' }) 72 + popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" }) 81 73 } 82 74 } 83 75 }
+28 -24
quartz/components/scripts/search.inline.ts
··· 4 4 import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path" 5 5 6 6 interface Item { 7 - slug: CanonicalSlug, 8 - title: string, 9 - content: string, 7 + slug: CanonicalSlug 8 + title: string 9 + content: string 10 10 } 11 11 12 12 let index: Document<Item> | undefined = undefined ··· 15 15 const numSearchResults = 5 16 16 function highlight(searchTerm: string, text: string, trim?: boolean) { 17 17 // try to highlight longest tokens first 18 - const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "").sort((a, b) => b.length - a.length) 19 - let tokenizedText = text 18 + const tokenizedTerms = searchTerm 20 19 .split(/\s+/) 21 - .filter(t => t !== "") 20 + .filter((t) => t !== "") 21 + .sort((a, b) => b.length - a.length) 22 + let tokenizedText = text.split(/\s+/).filter((t) => t !== "") 22 23 23 24 let startIndex = 0 24 25 let endIndex = tokenizedText.length - 1 25 26 if (trim) { 26 - const includesCheck = (tok: string) => tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase())) 27 + const includesCheck = (tok: string) => 28 + tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase())) 27 29 const occurencesIndices = tokenizedText.map(includesCheck) 28 30 29 31 let bestSum = 0 ··· 42 44 tokenizedText = tokenizedText.slice(startIndex, endIndex) 43 45 } 44 46 45 - const slice = tokenizedText.map(tok => { 46 - // see if this tok is prefixed by any search terms 47 - for (const searchTok of tokenizedTerms) { 48 - if (tok.toLowerCase().includes(searchTok.toLowerCase())) { 49 - const regex = new RegExp(searchTok.toLowerCase(), "gi") 50 - return tok.replace(regex, `<span class="highlight">$&</span>`) 47 + const slice = tokenizedText 48 + .map((tok) => { 49 + // see if this tok is prefixed by any search terms 50 + for (const searchTok of tokenizedTerms) { 51 + if (tok.toLowerCase().includes(searchTok.toLowerCase())) { 52 + const regex = new RegExp(searchTok.toLowerCase(), "gi") 53 + return tok.replace(regex, `<span class="highlight">$&</span>`) 54 + } 51 55 } 52 - } 53 - return tok 54 - }) 56 + return tok 57 + }) 55 58 .join(" ") 56 59 57 - return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..."}` 60 + return `${startIndex === 0 ? "" : "..."}${slice}${ 61 + endIndex === tokenizedText.length - 1 ? "" : "..." 62 + }` 58 63 } 59 64 60 65 const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) ··· 113 118 button.classList.add("result-card") 114 119 button.id = slug 115 120 button.innerHTML = `<h3>${title}</h3><p>${content}</p>` 116 - button.addEventListener('click', () => { 121 + button.addEventListener("click", () => { 117 122 const targ = resolveRelative(currentSlug, slug) 118 123 window.spaNavigate(new URL(targ, getClientSlug(window))) 119 124 }) ··· 132 137 } else { 133 138 results.append(...finalResults.map(resultToHTML)) 134 139 } 135 - 136 140 } 137 141 138 142 function onType(e: HTMLElementEventMap["input"]) { ··· 140 144 const searchResults = index?.search(term, numSearchResults) ?? [] 141 145 const getByField = (field: string): CanonicalSlug[] => { 142 146 const results = searchResults.filter((x) => x.field === field) 143 - return results.length === 0 ? [] : [...results[0].result] as CanonicalSlug[] 147 + return results.length === 0 ? [] : ([...results[0].result] as CanonicalSlug[]) 144 148 } 145 149 146 150 // order titles ahead of content 147 151 const allIds: Set<CanonicalSlug> = new Set([...getByField("title"), ...getByField("content")]) 148 - const finalResults = [...allIds].map(id => formatForDisplay(term, id)) 152 + const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) 149 153 displayResults(finalResults) 150 154 } 151 155 ··· 160 164 if (!index) { 161 165 index = new Document({ 162 166 cache: true, 163 - charset: 'latin:extra', 167 + charset: "latin:extra", 164 168 optimize: true, 165 169 encode: encoder, 166 170 document: { ··· 174 178 field: "content", 175 179 tokenize: "reverse", 176 180 }, 177 - ] 181 + ], 178 182 }, 179 183 }) 180 184 ··· 182 186 await index.addAsync(slug, { 183 187 slug: slug as CanonicalSlug, 184 188 title: fileData.title, 185 - content: fileData.content 189 + content: fileData.content, 186 190 }) 187 191 } 188 192 }
+34 -29
quartz/components/scripts/spa.inline.ts
··· 5 5 // https://github.com/natemoo-re/micromorph 6 6 7 7 const NODE_TYPE_ELEMENT = 1 8 - let announcer = document.createElement('route-announcer') 9 - const isElement = (target: EventTarget | null): target is Element => (target as Node)?.nodeType === NODE_TYPE_ELEMENT 8 + let announcer = document.createElement("route-announcer") 9 + const isElement = (target: EventTarget | null): target is Element => 10 + (target as Node)?.nodeType === NODE_TYPE_ELEMENT 10 11 const isLocalUrl = (href: string) => { 11 12 try { 12 13 const url = new URL(href) ··· 16 17 } 17 18 return true 18 19 } 19 - } catch (e) { } 20 + } catch (e) {} 20 21 return false 21 22 } 22 23 23 - const getOpts = ({ target }: Event): { url: URL, scroll?: boolean } | undefined => { 24 + const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => { 24 25 if (!isElement(target)) return 25 26 const a = target.closest("a") 26 27 if (!a) return 27 - if ('routerIgnore' in a.dataset) return 28 + if ("routerIgnore" in a.dataset) return 28 29 const { href } = a 29 30 if (!isLocalUrl(href)) return 30 - return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined } 31 + return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined } 31 32 } 32 33 33 34 function notifyNav(url: CanonicalSlug) { ··· 44 45 window.location.assign(url) 45 46 }) 46 47 47 - if (!contents) return; 48 + if (!contents) return 48 49 if (!isBack) { 49 50 history.pushState({}, "", url) 50 51 window.scrollTo({ top: 0 }) ··· 54 55 if (title) { 55 56 document.title = title 56 57 } else { 57 - const h1 = document.querySelector('h1') 58 + const h1 = document.querySelector("h1") 58 59 title = h1?.innerText ?? h1?.textContent ?? url.pathname 59 60 } 60 61 if (announcer.textContent !== title) { 61 62 announcer.textContent = title 62 63 } 63 - announcer.dataset.persist = '' 64 + announcer.dataset.persist = "" 64 65 html.body.appendChild(announcer) 65 66 66 67 micromorph(document.body, html.body) 67 68 68 - // now, patch head 69 - const elementsToRemove = document.head.querySelectorAll(':not([spa-preserve])') 70 - elementsToRemove.forEach(el => el.remove()) 71 - const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])') 72 - elementsToAdd.forEach(el => document.head.appendChild(el)) 69 + // now, patch head 70 + const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") 71 + elementsToRemove.forEach((el) => el.remove()) 72 + const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") 73 + elementsToAdd.forEach((el) => document.head.appendChild(el)) 73 74 74 75 notifyNav(getCanonicalSlug(window)) 75 76 delete announcer.dataset.persist ··· 101 102 }) 102 103 } 103 104 104 - return new class Router { 105 + return new (class Router { 105 106 go(pathname: RelativeURL) { 106 107 const url = new URL(pathname, window.location.toString()) 107 108 return navigate(url, false) ··· 114 115 forward() { 115 116 return window.history.forward() 116 117 } 117 - } 118 + })() 118 119 } 119 120 120 121 createRouter() 121 122 notifyNav(getCanonicalSlug(window)) 122 123 123 - if (!customElements.get('route-announcer')) { 124 + if (!customElements.get("route-announcer")) { 124 125 const attrs = { 125 - 'aria-live': 'assertive', 126 - 'aria-atomic': 'true', 127 - 'style': 'position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px' 126 + "aria-live": "assertive", 127 + "aria-atomic": "true", 128 + style: 129 + "position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px", 128 130 } 129 - customElements.define('route-announcer', class RouteAnnouncer extends HTMLElement { 130 - constructor() { 131 - super() 132 - } 133 - connectedCallback() { 134 - for (const [key, value] of Object.entries(attrs)) { 135 - this.setAttribute(key, value) 131 + customElements.define( 132 + "route-announcer", 133 + class RouteAnnouncer extends HTMLElement { 134 + constructor() { 135 + super() 136 + } 137 + connectedCallback() { 138 + for (const [key, value] of Object.entries(attrs)) { 139 + this.setAttribute(key, value) 140 + } 136 141 } 137 - } 138 - }) 142 + }, 143 + ) 139 144 }
+2 -2
quartz/components/scripts/toc.inline.ts
··· 1 1 const bufferPx = 150 2 - const observer = new IntersectionObserver(entries => { 2 + const observer = new IntersectionObserver((entries) => { 3 3 for (const entry of entries) { 4 4 const slug = entry.target.id 5 5 const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`) ··· 38 38 // update toc entry highlighting 39 39 observer.disconnect() 40 40 const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]") 41 - headers.forEach(header => observer.observe(header)) 41 + headers.forEach((header) => observer.observe(header)) 42 42 })
+1 -1
quartz/components/scripts/util.ts
··· 15 15 outsideContainer?.removeEventListener("click", click) 16 16 outsideContainer?.addEventListener("click", click) 17 17 document.removeEventListener("keydown", esc) 18 - document.addEventListener('keydown', esc) 18 + document.addEventListener("keydown", esc) 19 19 } 20 20 21 21 export function removeAllChildren(node: HTMLElement) {
+3 -3
quartz/components/styles/graph.scss
··· 3 3 .graph { 4 4 & > h3 { 5 5 font-size: 1rem; 6 - margin: 0 6 + margin: 0; 7 7 } 8 8 9 9 & > .graph-outer { ··· 26 26 top: 0; 27 27 right: 0; 28 28 border-radius: 4px; 29 - background-color: transparent; 29 + background-color: transparent; 30 30 transition: background-color 0.5s ease; 31 31 cursor: pointer; 32 32 &:hover { ··· 52 52 53 53 & > #global-graph-container { 54 54 border: 1px solid var(--lightgray); 55 - background-color: var(--light); 55 + background-color: var(--light); 56 56 border-radius: 5px; 57 57 box-sizing: border-box; 58 58 position: fixed;
+1 -1
quartz/components/styles/legacyToc.scss
··· 12 12 margin: 0; 13 13 } 14 14 } 15 - 15 + 16 16 & ul { 17 17 list-style: none; 18 18 margin: 0.5rem 1.25rem;
+1 -1
quartz/components/styles/listPage.scss
··· 25 25 } 26 26 27 27 & > .desc > h3 > a { 28 - background-color: transparent; 28 + background-color: transparent; 29 29 } 30 30 31 31 & > .meta {
+6 -3
quartz/components/styles/popover.scss
··· 32 32 border: 1px solid var(--lightgray); 33 33 background-color: var(--light); 34 34 border-radius: 5px; 35 - box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25); 35 + box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25); 36 36 overflow: auto; 37 37 } 38 38 ··· 42 42 43 43 visibility: hidden; 44 44 opacity: 0; 45 - transition: opacity 0.3s ease, visibility 0.3s ease; 45 + transition: 46 + opacity 0.3s ease, 47 + visibility 0.3s ease; 46 48 47 49 @media all and (max-width: $mobileBreakpoint) { 48 50 display: none !important; 49 51 } 50 52 } 51 53 52 - a:hover .popover, .popover:hover { 54 + a:hover .popover, 55 + .popover:hover { 53 56 animation: dropin 0.3s ease; 54 57 animation-fill-mode: forwards; 55 58 animation-delay: 0.2s;
+6 -4
quartz/components/styles/search.scss
··· 67 67 width: 100%; 68 68 border-radius: 5px; 69 69 background: var(--light); 70 - box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16); 70 + box-shadow: 71 + 0 14px 50px rgba(27, 33, 48, 0.12), 72 + 0 10px 30px rgba(27, 33, 48, 0.16); 71 73 margin-bottom: 2em; 72 74 } 73 75 ··· 108 110 font-weight: 700; 109 111 } 110 112 111 - &:hover, &:focus { 113 + &:hover, 114 + &:focus { 112 115 background: var(--lightgray); 113 116 } 114 117 ··· 127 130 margin: 0; 128 131 } 129 132 130 - & > p { 133 + & > p { 131 134 margin-bottom: 0; 132 135 } 133 136 } 134 137 } 135 - 136 138 } 137 139 } 138 140 }
+6 -5
quartz/components/styles/toc.scss
··· 15 15 } 16 16 17 17 & .fold { 18 - margin-left: 0.5rem; 18 + margin-left: 0.5rem; 19 19 transition: transform 0.3s ease; 20 20 opacity: 0.8; 21 21 } 22 22 23 23 &.collapsed .fold { 24 - transform: rotateZ(-90deg) 24 + transform: rotateZ(-90deg); 25 25 } 26 26 } 27 - 27 + 28 28 #toc-content { 29 29 list-style: none; 30 30 overflow: hidden; ··· 42 42 & > li > a { 43 43 color: var(--dark); 44 44 opacity: 0.35; 45 - transition: 0.5s ease opacity, 0.3s ease color; 45 + transition: 46 + 0.5s ease opacity, 47 + 0.3s ease color; 46 48 &.in-view { 47 49 opacity: 0.75; 48 50 } ··· 55 57 } 56 58 } 57 59 } 58 -
+9 -7
quartz/components/types.ts
··· 11 11 children: (QuartzComponent | JSX.Element)[] 12 12 tree: Node<QuartzPluginData> 13 13 allFiles: QuartzPluginData[] 14 - displayClass?: 'mobile-only' | 'desktop-only' 14 + displayClass?: "mobile-only" | "desktop-only" 15 15 } & JSX.IntrinsicAttributes & { 16 - [key: string]: any 17 - } 16 + [key: string]: any 17 + } 18 18 19 19 export type QuartzComponent = ComponentType<QuartzComponentProps> & { 20 - css?: string, 21 - beforeDOMLoaded?: string, 22 - afterDOMLoaded?: string, 20 + css?: string 21 + beforeDOMLoaded?: string 22 + afterDOMLoaded?: string 23 23 } 24 24 25 - export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (opts: Options) => QuartzComponent 25 + export type QuartzComponentConstructor<Options extends object | undefined = undefined> = ( 26 + opts: Options, 27 + ) => QuartzComponent
+1 -1
quartz/log.ts
··· 1 - import { Spinner } from 'cli-spinner' 1 + import { Spinner } from "cli-spinner" 2 2 3 3 export class QuartzLogger { 4 4 verbose: boolean
+101 -69
quartz/path.test.ts
··· 1 - import test, { describe } from 'node:test' 2 - import * as path from './path' 3 - import assert from 'node:assert' 1 + import test, { describe } from "node:test" 2 + import * as path from "./path" 3 + import assert from "node:assert" 4 4 5 - describe('typeguards', () => { 6 - test('isClientSlug', () => { 5 + describe("typeguards", () => { 6 + test("isClientSlug", () => { 7 7 assert(path.isClientSlug("http://example.com")) 8 8 assert(path.isClientSlug("http://example.com/index")) 9 9 assert(path.isClientSlug("http://example.com/index.html")) ··· 23 23 assert(!path.isClientSlug("https")) 24 24 }) 25 25 26 - test('isCanonicalSlug', () => { 26 + test("isCanonicalSlug", () => { 27 27 assert(path.isCanonicalSlug("")) 28 28 assert(path.isCanonicalSlug("abc")) 29 29 assert(path.isCanonicalSlug("notindex")) ··· 41 41 assert(!path.isCanonicalSlug("index.html")) 42 42 }) 43 43 44 - test('isRelativeURL', () => { 44 + test("isRelativeURL", () => { 45 45 assert(path.isRelativeURL(".")) 46 46 assert(path.isRelativeURL("..")) 47 47 assert(path.isRelativeURL("./abc/def")) ··· 58 58 assert(!path.isRelativeURL("./abc/def.md")) 59 59 }) 60 60 61 - test('isServerSlug', () => { 61 + test("isServerSlug", () => { 62 62 assert(path.isServerSlug("index")) 63 63 assert(path.isServerSlug("abc/def")) 64 64 ··· 72 72 assert(!path.isServerSlug("note with spaces")) 73 73 }) 74 74 75 - test('isFilePath', () => { 75 + test("isFilePath", () => { 76 76 assert(path.isFilePath("content/index.md")) 77 77 assert(path.isFilePath("content/test.png")) 78 78 assert(!path.isFilePath("../test.pdf")) ··· 81 81 }) 82 82 }) 83 83 84 - 85 - describe('transforms', () => { 86 - function asserts<Inp, Out>(pairs: [string, string][], transform: (inp: Inp) => Out, checkPre: (x: any) => x is Inp, checkPost: (x: any) => x is Out) { 84 + describe("transforms", () => { 85 + function asserts<Inp, Out>( 86 + pairs: [string, string][], 87 + transform: (inp: Inp) => Out, 88 + checkPre: (x: any) => x is Inp, 89 + checkPost: (x: any) => x is Out, 90 + ) { 87 91 for (const [inp, expected] of pairs) { 88 92 assert(checkPre(inp), `${inp} wasn't the expected input type`) 89 93 const actual = transform(inp) 90 - assert.strictEqual(actual, expected, `after transforming ${inp}, '${actual}' was not '${expected}'`) 94 + assert.strictEqual( 95 + actual, 96 + expected, 97 + `after transforming ${inp}, '${actual}' was not '${expected}'`, 98 + ) 91 99 assert(checkPost(actual), `${actual} wasn't the expected output type`) 92 100 } 93 101 } 94 102 95 - test('canonicalizeServer', () => { 96 - asserts([ 97 - ["index", ""], 98 - ["abc/index", "abc"], 99 - ["abc/def", "abc/def"], 100 - ], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug) 103 + test("canonicalizeServer", () => { 104 + asserts( 105 + [ 106 + ["index", ""], 107 + ["abc/index", "abc"], 108 + ["abc/def", "abc/def"], 109 + ], 110 + path.canonicalizeServer, 111 + path.isServerSlug, 112 + path.isCanonicalSlug, 113 + ) 101 114 }) 102 115 103 - test('canonicalizeClient', () => { 104 - asserts([ 105 - ["http://localhost:3000", ""], 106 - ["http://localhost:3000/index", ""], 107 - ["http://localhost:3000/test", "test"], 108 - ["http://example.com", ""], 109 - ["http://example.com/index", ""], 110 - ["http://example.com/index.html", ""], 111 - ["http://example.com/", ""], 112 - ["https://example.com", ""], 113 - ["https://example.com/abc/def", "abc/def"], 114 - ["https://example.com/abc/def/", "abc/def"], 115 - ["https://example.com/abc/def#cool", "abc/def"], 116 - ["https://example.com/abc/def?field=1&another=2", "abc/def"], 117 - ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"], 118 - ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"], 119 - ], path.canonicalizeClient, path.isClientSlug, path.isCanonicalSlug) 116 + test("canonicalizeClient", () => { 117 + asserts( 118 + [ 119 + ["http://localhost:3000", ""], 120 + ["http://localhost:3000/index", ""], 121 + ["http://localhost:3000/test", "test"], 122 + ["http://example.com", ""], 123 + ["http://example.com/index", ""], 124 + ["http://example.com/index.html", ""], 125 + ["http://example.com/", ""], 126 + ["https://example.com", ""], 127 + ["https://example.com/abc/def", "abc/def"], 128 + ["https://example.com/abc/def/", "abc/def"], 129 + ["https://example.com/abc/def#cool", "abc/def"], 130 + ["https://example.com/abc/def?field=1&another=2", "abc/def"], 131 + ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"], 132 + ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"], 133 + ], 134 + path.canonicalizeClient, 135 + path.isClientSlug, 136 + path.isCanonicalSlug, 137 + ) 120 138 }) 121 139 122 - describe('slugifyFilePath', () => { 123 - asserts([ 124 - ["content/index.md", "content/index"], 125 - ["content/_index.md", "content/index"], 126 - ["/content/index.md", "content/index"], 127 - ["content/cool.png", "content/cool"], 128 - ["index.md", "index"], 129 - ["note with spaces.md", "note-with-spaces"], 130 - ], path.slugifyFilePath, path.isFilePath, path.isServerSlug) 140 + describe("slugifyFilePath", () => { 141 + asserts( 142 + [ 143 + ["content/index.md", "content/index"], 144 + ["content/_index.md", "content/index"], 145 + ["/content/index.md", "content/index"], 146 + ["content/cool.png", "content/cool"], 147 + ["index.md", "index"], 148 + ["note with spaces.md", "note-with-spaces"], 149 + ], 150 + path.slugifyFilePath, 151 + path.isFilePath, 152 + path.isServerSlug, 153 + ) 131 154 }) 132 155 133 - describe('transformInternalLink', () => { 134 - asserts([ 135 - ["", "."], 136 - [".", "."], 137 - ["./", "."], 138 - ["./index", "."], 139 - ["./index.html", "."], 140 - ["./index.md", "."], 141 - ["content", "./content"], 142 - ["content/test.md", "./content/test"], 143 - ["./content/test.md", "./content/test"], 144 - ["../content/test.md", "../content/test"], 145 - ["tags/", "./tags"], 146 - ["/tags/", "./tags"], 147 - ["content/with spaces", "./content/with-spaces"], 148 - ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"], 149 - ], path.transformInternalLink, (_x: string): _x is string => true, path.isRelativeURL) 156 + describe("transformInternalLink", () => { 157 + asserts( 158 + [ 159 + ["", "."], 160 + [".", "."], 161 + ["./", "."], 162 + ["./index", "."], 163 + ["./index.html", "."], 164 + ["./index.md", "."], 165 + ["content", "./content"], 166 + ["content/test.md", "./content/test"], 167 + ["./content/test.md", "./content/test"], 168 + ["../content/test.md", "../content/test"], 169 + ["tags/", "./tags"], 170 + ["/tags/", "./tags"], 171 + ["content/with spaces", "./content/with-spaces"], 172 + ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"], 173 + ], 174 + path.transformInternalLink, 175 + (_x: string): _x is string => true, 176 + path.isRelativeURL, 177 + ) 150 178 }) 151 179 152 - describe('pathToRoot', () => { 153 - asserts([ 154 - ["", "."], 155 - ["abc", ".."], 156 - ["abc/def", "../.."], 157 - ], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL) 180 + describe("pathToRoot", () => { 181 + asserts( 182 + [ 183 + ["", "."], 184 + ["abc", ".."], 185 + ["abc/def", "../.."], 186 + ], 187 + path.pathToRoot, 188 + path.isCanonicalSlug, 189 + path.isRelativeURL, 190 + ) 158 191 }) 159 192 }) 160 -
+39 -39
quartz/path.ts
··· 1 - import { slug as slugAnchor } from 'github-slugger' 2 - import { trace } from './trace' 1 + import { slug as slugAnchor } from "github-slugger" 2 + import { trace } from "./trace" 3 3 4 4 // Quartz Paths 5 5 // Things in boxes are not actual types but rather sources which these types can be acquired from ··· 46 46 const STRICT_TYPE_CHECKS = false 47 47 const HARD_EXIT_ON_FAIL = false 48 48 49 - function conditionCheck<T>(name: string, label: 'pre' | 'post', s: T, chk: (x: any) => x is T) { 49 + function conditionCheck<T>(name: string, label: "pre" | "post", s: T, chk: (x: any) => x is T) { 50 50 if (STRICT_TYPE_CHECKS && !chk(s)) { 51 51 trace(`${name} failed ${label}-condition check: ${s} does not pass ${chk.name}`, new Error()) 52 52 if (HARD_EXIT_ON_FAIL) { ··· 66 66 } 67 67 68 68 /** Canonical slug, should be used whenever you need to refer to the location of a file/note. 69 - * On the client, this is normally stored in `document.body.dataset.slug` 70 - */ 69 + * On the client, this is normally stored in `document.body.dataset.slug` 70 + */ 71 71 export type CanonicalSlug = SlugLike<"canonical"> 72 72 export function isCanonicalSlug(s: string): s is CanonicalSlug { 73 73 const validStart = !(s.startsWith(".") || s.startsWith("/")) ··· 76 76 } 77 77 78 78 /** A relative link, can be found on `href`s but can also be constructed for 79 - * client-side navigation (e.g. search and graph) 80 - */ 79 + * client-side navigation (e.g. search and graph) 80 + */ 81 81 export type RelativeURL = SlugLike<"relative"> 82 82 export function isRelativeURL(s: string): s is RelativeURL { 83 83 const validStart = /^\.{1,2}/.test(s) ··· 102 102 103 103 export function getClientSlug(window: Window): ClientSlug { 104 104 const res = window.location.href as ClientSlug 105 - conditionCheck(getClientSlug.name, 'post', res, isClientSlug) 105 + conditionCheck(getClientSlug.name, "post", res, isClientSlug) 106 106 return res 107 107 } 108 108 109 109 export function getCanonicalSlug(window: Window): CanonicalSlug { 110 110 const res = window.document.body.dataset.slug! as CanonicalSlug 111 - conditionCheck(getCanonicalSlug.name, 'post', res, isCanonicalSlug) 111 + conditionCheck(getCanonicalSlug.name, "post", res, isCanonicalSlug) 112 112 return res 113 113 } 114 114 115 115 export function canonicalizeClient(slug: ClientSlug): CanonicalSlug { 116 - conditionCheck(canonicalizeClient.name, 'pre', slug, isClientSlug) 116 + conditionCheck(canonicalizeClient.name, "pre", slug, isClientSlug) 117 117 const { pathname } = new URL(slug) 118 118 let fp = pathname.slice(1) 119 - fp = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '') 119 + fp = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "") 120 120 const res = _canonicalize(fp) as CanonicalSlug 121 - conditionCheck(canonicalizeClient.name, 'post', res, isCanonicalSlug) 121 + conditionCheck(canonicalizeClient.name, "post", res, isCanonicalSlug) 122 122 return res 123 123 } 124 124 125 125 export function canonicalizeServer(slug: ServerSlug): CanonicalSlug { 126 - conditionCheck(canonicalizeServer.name, 'pre', slug, isServerSlug) 126 + conditionCheck(canonicalizeServer.name, "pre", slug, isServerSlug) 127 127 let fp = slug as string 128 128 const res = _canonicalize(fp) as CanonicalSlug 129 - conditionCheck(canonicalizeServer.name, 'post', res, isCanonicalSlug) 129 + conditionCheck(canonicalizeServer.name, "post", res, isCanonicalSlug) 130 130 return res 131 131 } 132 132 133 133 export function slugifyFilePath(fp: FilePath): ServerSlug { 134 - conditionCheck(slugifyFilePath.name, 'pre', fp, isFilePath) 134 + conditionCheck(slugifyFilePath.name, "pre", fp, isFilePath) 135 135 fp = _stripSlashes(fp) as FilePath 136 - const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '') 136 + const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "") 137 137 let slug = withoutFileExt 138 - .split('/') 139 - .map((segment) => segment.replace(/\s/g, '-')) // slugify all segments 140 - .join('/') // always use / as sep 141 - .replace(/\/$/, '') // remove trailing slash 138 + .split("/") 139 + .map((segment) => segment.replace(/\s/g, "-")) // slugify all segments 140 + .join("/") // always use / as sep 141 + .replace(/\/$/, "") // remove trailing slash 142 142 143 143 // treat _index as index 144 144 if (_endsWith(slug, "_index")) { 145 145 slug = slug.replace(/_index$/, "index") 146 146 } 147 147 148 - conditionCheck(slugifyFilePath.name, 'post', slug, isServerSlug) 148 + conditionCheck(slugifyFilePath.name, "post", slug, isServerSlug) 149 149 return slug as ServerSlug 150 150 } 151 151 152 152 export function transformInternalLink(link: string): RelativeURL { 153 153 let [fplike, anchor] = splitAnchor(decodeURI(link)) 154 - let segments = fplike.split("/").filter(x => x.length > 0) 154 + let segments = fplike.split("/").filter((x) => x.length > 0) 155 155 let prefix = segments.filter(_isRelativeSegment).join("/") 156 - let fp = segments.filter(seg => !_isRelativeSegment(seg)).join("/") 156 + let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/") 157 157 158 158 // implicit markdown 159 159 if (!_hasFileExtension(fp)) { ··· 164 164 fp = _trimSuffix(fp, "index") 165 165 166 166 let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp)) 167 - const res = _addRelativeToStart(joined) + anchor as RelativeURL 168 - conditionCheck(transformInternalLink.name, 'post', res, isRelativeURL) 167 + const res = (_addRelativeToStart(joined) + anchor) as RelativeURL 168 + conditionCheck(transformInternalLink.name, "post", res, isRelativeURL) 169 169 return res 170 170 } 171 171 172 172 // resolve /a/b/c to ../../ 173 173 export function pathToRoot(slug: CanonicalSlug): RelativeURL { 174 - conditionCheck(pathToRoot.name, 'pre', slug, isCanonicalSlug) 174 + conditionCheck(pathToRoot.name, "pre", slug, isCanonicalSlug) 175 175 let rootPath = slug 176 - .split('/') 177 - .filter(x => x !== '') 178 - .map(_ => '..') 179 - .join('/') 176 + .split("/") 177 + .filter((x) => x !== "") 178 + .map((_) => "..") 179 + .join("/") 180 180 181 181 const res = _addRelativeToStart(rootPath) as RelativeURL 182 - conditionCheck(pathToRoot.name, 'post', res, isRelativeURL) 182 + conditionCheck(pathToRoot.name, "post", res, isRelativeURL) 183 183 return res 184 184 } 185 185 186 186 export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL { 187 - conditionCheck(resolveRelative.name, 'pre', current, isCanonicalSlug) 188 - conditionCheck(resolveRelative.name, 'pre', target, isCanonicalSlug) 187 + conditionCheck(resolveRelative.name, "pre", current, isCanonicalSlug) 188 + conditionCheck(resolveRelative.name, "pre", target, isCanonicalSlug) 189 189 const res = joinSegments(pathToRoot(current), target) as RelativeURL 190 - conditionCheck(resolveRelative.name, 'post', res, isRelativeURL) 190 + conditionCheck(resolveRelative.name, "post", res, isRelativeURL) 191 191 return res 192 192 } 193 193 194 194 export function splitAnchor(link: string): [string, string] { 195 195 let [fp, anchor] = link.split("#", 2) 196 - anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor) 196 + anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor) 197 197 return [fp, anchor] 198 198 } 199 199 200 200 export function joinSegments(...args: string[]): string { 201 - return args.filter(segment => segment !== "").join('/') 201 + return args.filter((segment) => segment !== "").join("/") 202 202 } 203 203 204 204 export const QUARTZ = "quartz" 205 205 206 206 function _canonicalize(fp: string): string { 207 207 fp = _trimSuffix(fp, "index") 208 - return _stripSlashes(fp) 208 + return _stripSlashes(fp) 209 209 } 210 210 211 211 function _endsWith(s: string, suffix: string): boolean { 212 - return s === suffix || s.endsWith("/" + suffix) 212 + return s === suffix || s.endsWith("/" + suffix) 213 213 } 214 214 215 215 function _trimSuffix(s: string, suffix: string): string { 216 216 if (_endsWith(s, suffix)) { 217 - s = s.slice(0, -(suffix.length)) 217 + s = s.slice(0, -suffix.length) 218 218 } 219 219 return s 220 220 }
+4 -4
quartz/perf.ts
··· 1 - import chalk from 'chalk' 2 - import pretty from 'pretty-time' 1 + import chalk from "chalk" 2 + import pretty from "pretty-time" 3 3 4 4 export class PerfTimer { 5 5 evts: { [key: string]: [number, number] } 6 6 7 7 constructor() { 8 8 this.evts = {} 9 - this.addEvent('start') 9 + this.addEvent("start") 10 10 } 11 11 12 12 addEvent(evtName: string) { ··· 14 14 } 15 15 16 16 timeSince(evtName?: string): string { 17 - return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? 'start']))) 17 + return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"]))) 18 18 } 19 19 }
+10 -4
quartz/plugins/emitters/aliases.ts
··· 1 - import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, resolveRelative } from "../../path" 1 + import { 2 + CanonicalSlug, 3 + FilePath, 4 + ServerSlug, 5 + canonicalizeServer, 6 + resolveRelative, 7 + } from "../../path" 2 8 import { QuartzEmitterPlugin } from "../types" 3 - import path from 'path' 9 + import path from "path" 4 10 5 11 export const AliasRedirects: QuartzEmitterPlugin = () => ({ 6 12 name: "AliasRedirects", ··· 24 30 for (const alias of aliases) { 25 31 const slug = path.posix.join(dir, alias) as ServerSlug 26 32 27 - const fp = slug + ".html" as FilePath 33 + const fp = (slug + ".html") as FilePath 28 34 const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug) 29 35 await emit({ 30 36 content: ` ··· 47 53 } 48 54 } 49 55 return fps 50 - } 56 + }, 51 57 })
+23 -19
quartz/plugins/emitters/contentIndex.ts
··· 5 5 6 6 export type ContentIndex = Map<CanonicalSlug, ContentDetails> 7 7 export type ContentDetails = { 8 - title: string, 9 - links: CanonicalSlug[], 10 - tags: string[], 11 - content: string, 12 - date?: Date, 13 - description?: string, 8 + title: string 9 + links: CanonicalSlug[] 10 + tags: string[] 11 + content: string 12 + date?: Date 13 + description?: string 14 14 } 15 15 16 16 interface Options { ··· 31 31 <loc>https://${base}/${slug}</loc> 32 32 <lastmod>${content.date?.toISOString()}</lastmod> 33 33 </url>` 34 - const urls = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("") 34 + const urls = Array.from(idx) 35 + .map(([slug, content]) => createURLEntry(slug, content)) 36 + .join("") 35 37 return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>` 36 38 } 37 39 ··· 47 49 <pubDate>${content.date?.toUTCString()}</pubDate> 48 50 </items>` 49 51 50 - const items = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("") 52 + const items = Array.from(idx) 53 + .map(([slug, content]) => createURLEntry(slug, content)) 54 + .join("") 51 55 return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0"> 52 56 <channel> 53 57 <title>${cfg.pageTitle}</title> ··· 71 75 const slug = canonicalizeServer(file.data.slug!) 72 76 const date = file.data.dates?.modified ?? new Date() 73 77 if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { 74 - linkIndex.set(slug, { 75 - title: file.data.frontmatter?.title!, 76 - links: file.data.links ?? [], 77 - tags: file.data.frontmatter?.tags ?? [], 78 - content: file.data.text ?? "", 79 - date: date, 80 - description: file.data.description ?? "" 81 - }) 78 + linkIndex.set(slug, { 79 + title: file.data.frontmatter?.title!, 80 + links: file.data.links ?? [], 81 + tags: file.data.frontmatter?.tags ?? [], 82 + content: file.data.text ?? "", 83 + date: date, 84 + description: file.data.description ?? "", 85 + }) 82 86 } 83 87 } 84 88 ··· 86 90 await emit({ 87 91 content: generateSiteMap(cfg, linkIndex), 88 92 slug: "sitemap" as ServerSlug, 89 - ext: ".xml" 93 + ext: ".xml", 90 94 }) 91 95 emitted.push("sitemap.xml" as FilePath) 92 96 } ··· 95 99 await emit({ 96 100 content: generateRSSFeed(cfg, linkIndex), 97 101 slug: "index" as ServerSlug, 98 - ext: ".xml" 102 + ext: ".xml", 99 103 }) 100 104 emitted.push("index.xml" as FilePath) 101 105 } ··· 109 113 delete content.description 110 114 delete content.date 111 115 return [slug, content] 112 - }) 116 + }), 113 117 ) 114 118 115 119 await emit({
+8 -11
quartz/plugins/emitters/contentPage.tsx
··· 8 8 9 9 export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { 10 10 if (!opts) { 11 - throw new Error("ContentPage must be initialized with options specifiying the components to use") 11 + throw new Error( 12 + "ContentPage must be initialized with options specifiying the components to use", 13 + ) 12 14 } 13 15 14 16 const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts ··· 22 24 }, 23 25 async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> { 24 26 const fps: FilePath[] = [] 25 - const allFiles = content.map(c => c[1].data) 27 + const allFiles = content.map((c) => c[1].data) 26 28 for (const [tree, file] of content) { 27 29 const slug = canonicalizeServer(file.data.slug!) 28 30 const externalResources = pageResources(slug, resources) ··· 32 34 cfg, 33 35 children: [], 34 36 tree, 35 - allFiles 37 + allFiles, 36 38 } 37 39 38 - const content = renderPage( 39 - slug, 40 - componentData, 41 - opts, 42 - externalResources 43 - ) 40 + const content = renderPage(slug, componentData, opts, externalResources) 44 41 45 - const fp = file.data.slug + ".html" as FilePath 42 + const fp = (file.data.slug + ".html") as FilePath 46 43 await emit({ 47 44 content, 48 45 slug: file.data.slug!, ··· 52 49 fps.push(fp) 53 50 } 54 51 return fps 55 - } 52 + }, 56 53 } 57 54 }
+24 -21
quartz/plugins/emitters/folderPage.tsx
··· 24 24 }, 25 25 async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> { 26 26 const fps: FilePath[] = [] 27 - const allFiles = content.map(c => c[1].data) 27 + const allFiles = content.map((c) => c[1].data) 28 28 29 - const folders: Set<CanonicalSlug> = new Set(allFiles.flatMap(data => { 30 - const slug = data.slug 31 - const folderName = path.dirname(slug ?? "") as CanonicalSlug 32 - if (slug && folderName !== "." && folderName !== "tags") { 33 - return [folderName] 34 - } 35 - return [] 36 - })) 29 + const folders: Set<CanonicalSlug> = new Set( 30 + allFiles.flatMap((data) => { 31 + const slug = data.slug 32 + const folderName = path.dirname(slug ?? "") as CanonicalSlug 33 + if (slug && folderName !== "." && folderName !== "tags") { 34 + return [folderName] 35 + } 36 + return [] 37 + }), 38 + ) 37 39 38 - const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([ 39 - folder, defaultProcessedContent({ slug: joinSegments(folder, "index") as ServerSlug, frontmatter: { title: `Folder: ${folder}`, tags: [] } }) 40 - ]))) 40 + const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries( 41 + [...folders].map((folder) => [ 42 + folder, 43 + defaultProcessedContent({ 44 + slug: joinSegments(folder, "index") as ServerSlug, 45 + frontmatter: { title: `Folder: ${folder}`, tags: [] }, 46 + }), 47 + ]), 48 + ) 41 49 42 50 for (const [tree, file] of content) { 43 51 const slug = canonicalizeServer(file.data.slug!) ··· 56 64 cfg, 57 65 children: [], 58 66 tree, 59 - allFiles 67 + allFiles, 60 68 } 61 69 62 - const content = renderPage( 63 - slug, 64 - componentData, 65 - opts, 66 - externalResources 67 - ) 70 + const content = renderPage(slug, componentData, opts, externalResources) 68 71 69 - const fp = file.data.slug! + ".html" as FilePath 72 + const fp = (file.data.slug! + ".html") as FilePath 70 73 await emit({ 71 74 content, 72 75 slug: file.data.slug!, ··· 76 79 fps.push(fp) 77 80 } 78 81 return fps 79 - } 82 + }, 80 83 } 81 84 }
+5 -5
quartz/plugins/emitters/index.ts
··· 1 - export { ContentPage } from './contentPage' 2 - export { TagPage } from './tagPage' 3 - export { FolderPage } from './folderPage' 4 - export { ContentIndex } from './contentIndex' 5 - export { AliasRedirects } from './aliases' 1 + export { ContentPage } from "./contentPage" 2 + export { TagPage } from "./tagPage" 3 + export { FolderPage } from "./folderPage" 4 + export { ContentIndex } from "./contentIndex" 5 + export { AliasRedirects } from "./aliases"
+15 -14
quartz/plugins/emitters/tagPage.tsx
··· 23 23 }, 24 24 async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> { 25 25 const fps: FilePath[] = [] 26 - const allFiles = content.map(c => c[1].data) 26 + const allFiles = content.map((c) => c[1].data) 27 27 28 - const tags: Set<string> = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? [])) 29 - const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...tags].map(tag => ([ 30 - tag, defaultProcessedContent({ slug: `tags/${tag}/index` as ServerSlug, frontmatter: { title: `Tag: ${tag}`, tags: [] } }) 31 - ]))) 28 + const tags: Set<string> = new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? [])) 29 + const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries( 30 + [...tags].map((tag) => [ 31 + tag, 32 + defaultProcessedContent({ 33 + slug: `tags/${tag}/index` as ServerSlug, 34 + frontmatter: { title: `Tag: ${tag}`, tags: [] }, 35 + }), 36 + ]), 37 + ) 32 38 33 39 for (const [tree, file] of content) { 34 40 const slug = file.data.slug! ··· 50 56 cfg, 51 57 children: [], 52 58 tree, 53 - allFiles 59 + allFiles, 54 60 } 55 61 56 - const content = renderPage( 57 - slug, 58 - componentData, 59 - opts, 60 - externalResources 61 - ) 62 + const content = renderPage(slug, componentData, opts, externalResources) 62 63 63 - const fp = file.data.slug + ".html" as FilePath 64 + const fp = (file.data.slug + ".html") as FilePath 64 65 await emit({ 65 66 content, 66 67 slug: file.data.slug!, ··· 70 71 fps.push(fp) 71 72 } 72 73 return fps 73 - } 74 + }, 74 75 } 75 76 }
+1 -1
quartz/plugins/filters/draft.ts
··· 5 5 shouldPublish([_tree, vfile]) { 6 6 const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false 7 7 return !draftFlag 8 - } 8 + }, 9 9 })
+1 -1
quartz/plugins/filters/explicit.ts
··· 5 5 shouldPublish([_tree, vfile]) { 6 6 const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false 7 7 return publishFlag 8 - } 8 + }, 9 9 })
+2 -2
quartz/plugins/filters/index.ts
··· 1 - export { RemoveDrafts } from './draft' 2 - export { ExplicitPublish } from './explicit' 1 + export { RemoveDrafts } from "./draft" 2 + export { ExplicitPublish } from "./explicit"
+26 -23
quartz/plugins/index.ts
··· 1 - import { GlobalConfiguration } from '../cfg' 2 - import { QuartzComponent } from '../components/types' 3 - import { StaticResources } from '../resources' 4 - import { joinStyles } from '../theme' 5 - import { EmitCallback, PluginTypes } from './types' 6 - import styles from '../styles/base.scss' 7 - import { FilePath, ServerSlug } from '../path' 1 + import { GlobalConfiguration } from "../cfg" 2 + import { QuartzComponent } from "../components/types" 3 + import { StaticResources } from "../resources" 4 + import { joinStyles } from "../theme" 5 + import { EmitCallback, PluginTypes } from "./types" 6 + import styles from "../styles/base.scss" 7 + import { FilePath, ServerSlug } from "../path" 8 8 9 9 export type ComponentResources = { 10 - css: string[], 11 - beforeDOMLoaded: string[], 10 + css: string[] 11 + beforeDOMLoaded: string[] 12 12 afterDOMLoaded: string[] 13 13 } 14 14 ··· 24 24 const componentResources = { 25 25 css: new Set<string>(), 26 26 beforeDOMLoaded: new Set<string>(), 27 - afterDOMLoaded: new Set<string>() 27 + afterDOMLoaded: new Set<string>(), 28 28 } 29 29 30 30 for (const component of allComponents) { ··· 39 39 componentResources.afterDOMLoaded.add(afterDOMLoaded) 40 40 } 41 41 } 42 - 42 + 43 43 return { 44 44 css: [...componentResources.css], 45 45 beforeDOMLoaded: [...componentResources.beforeDOMLoaded], 46 - afterDOMLoaded: [...componentResources.afterDOMLoaded] 46 + afterDOMLoaded: [...componentResources.afterDOMLoaded], 47 47 } 48 48 } 49 49 50 50 function joinScripts(scripts: string[]): string { 51 51 // wrap with iife to prevent scope collision 52 - return scripts.map(script => `(function () {${script}})();`).join("\n") 52 + return scripts.map((script) => `(function () {${script}})();`).join("\n") 53 53 } 54 54 55 - export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> { 55 + export async function emitComponentResources( 56 + cfg: GlobalConfiguration, 57 + res: ComponentResources, 58 + emit: EmitCallback, 59 + ): Promise<FilePath[]> { 56 60 const fps = await Promise.all([ 57 61 emit({ 58 62 slug: "index" as ServerSlug, 59 63 ext: ".css", 60 - content: joinStyles(cfg.theme, styles, ...res.css) 64 + content: joinStyles(cfg.theme, styles, ...res.css), 61 65 }), 62 66 emit({ 63 67 slug: "prescript" as ServerSlug, 64 68 ext: ".js", 65 - content: joinScripts(res.beforeDOMLoaded) 69 + content: joinScripts(res.beforeDOMLoaded), 66 70 }), 67 71 emit({ 68 72 slug: "postscript" as ServerSlug, 69 73 ext: ".js", 70 - content: joinScripts(res.afterDOMLoaded) 71 - }) 74 + content: joinScripts(res.afterDOMLoaded), 75 + }), 72 76 ]) 73 77 return fps 74 - 75 78 } 76 79 77 80 export function getStaticResourcesFromPlugins(plugins: PluginTypes) { ··· 93 96 return staticResources 94 97 } 95 98 96 - export * from './transformers' 97 - export * from './filters' 98 - export * from './emitters' 99 + export * from "./transformers" 100 + export * from "./filters" 101 + export * from "./emitters" 99 102 100 - declare module 'vfile' { 103 + declare module "vfile" { 101 104 // inserted in processors.ts 102 105 interface DataMap { 103 106 slug: ServerSlug
+13 -9
quartz/plugins/transformers/description.ts
··· 1 - import { Root as HTMLRoot } from 'hast' 1 + import { Root as HTMLRoot } from "hast" 2 2 import { toString } from "hast-util-to-string" 3 3 import { QuartzTransformerPlugin } from "../types" 4 4 ··· 7 7 } 8 8 9 9 const defaultOptions: Options = { 10 - descriptionLength: 150 10 + descriptionLength: 150, 11 11 } 12 12 13 13 const escapeHTML = (unsafe: string) => { 14 - return unsafe.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;'); 14 + return unsafe 15 + .replaceAll("&", "&amp;") 16 + .replaceAll("<", "&lt;") 17 + .replaceAll(">", "&gt;") 18 + .replaceAll('"', "&quot;") 19 + .replaceAll("'", "&#039;") 15 20 } 16 21 17 22 export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { ··· 26 31 const text = escapeHTML(toString(tree)) 27 32 28 33 const desc = frontMatterDescription ?? text 29 - const sentences = desc.replace(/\s+/g, ' ').split('.') 34 + const sentences = desc.replace(/\s+/g, " ").split(".") 30 35 let finalDesc = "" 31 36 let sentenceIdx = 0 32 37 const len = opts.descriptionLength 33 38 while (finalDesc.length < len) { 34 39 const sentence = sentences[sentenceIdx] 35 40 if (!sentence) break 36 - finalDesc += sentence + '.' 41 + finalDesc += sentence + "." 37 42 sentenceIdx++ 38 43 } 39 44 40 45 file.data.description = finalDesc 41 46 file.data.text = text 42 47 } 43 - } 48 + }, 44 49 ] 45 - } 50 + }, 46 51 } 47 52 } 48 53 49 - declare module 'vfile' { 54 + declare module "vfile" { 50 55 interface DataMap { 51 56 description: string 52 57 text: string 53 58 } 54 59 } 55 -
+15 -12
quartz/plugins/transformers/frontmatter.ts
··· 1 1 import matter from "gray-matter" 2 - import remarkFrontmatter from 'remark-frontmatter' 2 + import remarkFrontmatter from "remark-frontmatter" 3 3 import { QuartzTransformerPlugin } from "../types" 4 - import yaml from 'js-yaml' 5 - import { slug as slugAnchor } from 'github-slugger' 4 + import yaml from "js-yaml" 5 + import { slug as slugAnchor } from "github-slugger" 6 6 7 7 export interface Options { 8 - language: 'yaml' | 'toml', 8 + language: "yaml" | "toml" 9 9 delims: string | string[] 10 10 } 11 11 12 12 const defaultOptions: Options = { 13 - language: 'yaml', 14 - delims: '---' 13 + language: "yaml", 14 + delims: "---", 15 15 } 16 16 17 17 export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { ··· 26 26 const { data } = matter(file.value, { 27 27 ...opts, 28 28 engines: { 29 - yaml: s => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object 30 - } 29 + yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, 30 + }, 31 31 }) 32 32 33 33 // tag is an alias for tags ··· 36 36 } 37 37 38 38 if (data.tags && !Array.isArray(data.tags)) { 39 - data.tags = data.tags.toString().split(",").map((tag: string) => tag.trim()) 39 + data.tags = data.tags 40 + .toString() 41 + .split(",") 42 + .map((tag: string) => tag.trim()) 40 43 } 41 44 42 45 // slug them all!! ··· 46 49 file.data.frontmatter = { 47 50 title: file.stem ?? "Untitled", 48 51 tags: [], 49 - ...data 52 + ...data, 50 53 } 51 54 } 52 - } 55 + }, 53 56 ] 54 57 }, 55 58 } 56 59 } 57 60 58 - declare module 'vfile' { 61 + declare module "vfile" { 59 62 interface DataMap { 60 63 frontmatter: { [key: string]: any } & { 61 64 title: string
+19 -10
quartz/plugins/transformers/gfm.ts
··· 1 1 import remarkGfm from "remark-gfm" 2 - import smartypants from 'remark-smartypants' 2 + import smartypants from "remark-smartypants" 3 3 import { QuartzTransformerPlugin } from "../types" 4 4 import rehypeSlug from "rehype-slug" 5 5 import rehypeAutolinkHeadings from "rehype-autolink-headings" ··· 11 11 12 12 const defaultOptions: Options = { 13 13 enableSmartyPants: true, 14 - linkHeadings: true 14 + linkHeadings: true, 15 15 } 16 16 17 - export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { 17 + export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( 18 + userOpts, 19 + ) => { 18 20 const opts = { ...defaultOptions, ...userOpts } 19 21 return { 20 22 name: "GitHubFlavoredMarkdown", ··· 23 25 }, 24 26 htmlPlugins() { 25 27 if (opts.linkHeadings) { 26 - return [rehypeSlug, [rehypeAutolinkHeadings, { 27 - behavior: 'append', content: { 28 - type: 'text', 29 - value: ' §', 30 - } 31 - }]] 28 + return [ 29 + rehypeSlug, 30 + [ 31 + rehypeAutolinkHeadings, 32 + { 33 + behavior: "append", 34 + content: { 35 + type: "text", 36 + value: " §", 37 + }, 38 + }, 39 + ], 40 + ] 32 41 } else { 33 42 return [] 34 43 } 35 - } 44 + }, 36 45 } 37 46 }
+9 -9
quartz/plugins/transformers/index.ts
··· 1 - export { FrontMatter } from './frontmatter' 2 - export { GitHubFlavoredMarkdown } from './gfm' 3 - export { CreatedModifiedDate } from './lastmod' 4 - export { Latex } from './latex' 5 - export { Description } from './description' 6 - export { CrawlLinks } from './links' 7 - export { ObsidianFlavoredMarkdown } from './ofm' 8 - export { SyntaxHighlighting } from './syntax' 9 - export { TableOfContents } from './toc' 1 + export { FrontMatter } from "./frontmatter" 2 + export { GitHubFlavoredMarkdown } from "./gfm" 3 + export { CreatedModifiedDate } from "./lastmod" 4 + export { Latex } from "./latex" 5 + export { Description } from "./description" 6 + export { CrawlLinks } from "./links" 7 + export { ObsidianFlavoredMarkdown } from "./ofm" 8 + export { SyntaxHighlighting } from "./syntax" 9 + export { TableOfContents } from "./toc"
+8 -6
quartz/plugins/transformers/lastmod.ts
··· 1 1 import fs from "fs" 2 - import path from 'path' 2 + import path from "path" 3 3 import { Repository } from "@napi-rs/simple-git" 4 4 import { QuartzTransformerPlugin } from "../types" 5 5 6 6 export interface Options { 7 - priority: ('frontmatter' | 'git' | 'filesystem')[], 7 + priority: ("frontmatter" | "git" | "filesystem")[] 8 8 } 9 9 10 10 const defaultOptions: Options = { 11 - priority: ['frontmatter', 'git', 'filesystem'] 11 + priority: ["frontmatter", "git", "filesystem"], 12 12 } 13 13 14 14 type MaybeDate = undefined | string | number 15 - export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { 15 + export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = ( 16 + userOpts, 17 + ) => { 16 18 const opts = { ...defaultOptions, ...userOpts } 17 19 return { 18 20 name: "CreatedModifiedDate", ··· 51 53 published: published ? new Date(published) : new Date(), 52 54 } 53 55 } 54 - } 56 + }, 55 57 ] 56 58 }, 57 59 } 58 60 } 59 61 60 - declare module 'vfile' { 62 + declare module "vfile" { 61 63 interface DataMap { 62 64 dates: { 63 65 created: Date
+20 -24
quartz/plugins/transformers/latex.ts
··· 1 1 import remarkMath from "remark-math" 2 - import rehypeKatex from 'rehype-katex' 3 - import rehypeMathjax from 'rehype-mathjax/svg.js' 2 + import rehypeKatex from "rehype-katex" 3 + import rehypeMathjax from "rehype-mathjax/svg.js" 4 4 import { QuartzTransformerPlugin } from "../types" 5 5 6 6 interface Options { 7 - renderEngine: 'katex' | 'mathjax' 7 + renderEngine: "katex" | "mathjax" 8 8 } 9 9 10 10 export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => { 11 - const engine = opts?.renderEngine ?? 'katex' 11 + const engine = opts?.renderEngine ?? "katex" 12 12 return { 13 13 name: "Latex", 14 14 markdownPlugins() { 15 15 return [remarkMath] 16 16 }, 17 17 htmlPlugins() { 18 - return [ 19 - engine === 'katex' 20 - ? [rehypeKatex, { output: 'html' }] 21 - : [rehypeMathjax] 22 - ] 18 + return [engine === "katex" ? [rehypeKatex, { output: "html" }] : [rehypeMathjax]] 23 19 }, 24 20 externalResources() { 25 - return engine === 'katex' 21 + return engine === "katex" 26 22 ? { 27 - css: [ 28 - // base css 29 - "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css", 30 - ], 31 - js: [ 32 - { 33 - // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md 34 - src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", 35 - loadTime: "afterDOMReady", 36 - contentType: 'external' 37 - } 38 - ] 39 - } 23 + css: [ 24 + // base css 25 + "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css", 26 + ], 27 + js: [ 28 + { 29 + // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md 30 + src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", 31 + loadTime: "afterDOMReady", 32 + contentType: "external", 33 + }, 34 + ], 35 + } 40 36 : {} 41 - } 37 + }, 42 38 } 43 39 }
+83 -67
quartz/plugins/transformers/links.ts
··· 1 1 import { QuartzTransformerPlugin } from "../types" 2 - import { CanonicalSlug, RelativeURL, canonicalizeServer, joinSegments, pathToRoot, resolveRelative, splitAnchor, transformInternalLink } from "../../path" 2 + import { 3 + CanonicalSlug, 4 + RelativeURL, 5 + canonicalizeServer, 6 + joinSegments, 7 + pathToRoot, 8 + resolveRelative, 9 + splitAnchor, 10 + transformInternalLink, 11 + } from "../../path" 3 12 import path from "path" 4 - import { visit } from 'unist-util-visit' 13 + import { visit } from "unist-util-visit" 5 14 import isAbsoluteUrl from "is-absolute-url" 6 15 7 16 interface Options { 8 17 /** How to resolve Markdown paths */ 9 - markdownLinkResolution: 'absolute' | 'relative' | 'shortest' 18 + markdownLinkResolution: "absolute" | "relative" | "shortest" 10 19 /** Strips folders from a link so that it looks nice */ 11 20 prettyLinks: boolean 12 21 } 13 22 14 23 const defaultOptions: Options = { 15 - markdownLinkResolution: 'absolute', 24 + markdownLinkResolution: "absolute", 16 25 prettyLinks: true, 17 26 } 18 27 ··· 21 30 return { 22 31 name: "LinkProcessing", 23 32 htmlPlugins() { 24 - return [() => { 25 - return (tree, file) => { 26 - const curSlug = canonicalizeServer(file.data.slug!) 27 - const transformLink = (target: string): RelativeURL => { 28 - const targetSlug = transformInternalLink(target).slice("./".length) 29 - let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) 30 - if (opts.markdownLinkResolution === 'relative') { 31 - return targetSlug as RelativeURL 32 - } else if (opts.markdownLinkResolution === 'shortest') { 33 - // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5 34 - const allSlugs = file.data.allSlugs! 33 + return [ 34 + () => { 35 + return (tree, file) => { 36 + const curSlug = canonicalizeServer(file.data.slug!) 37 + const transformLink = (target: string): RelativeURL => { 38 + const targetSlug = transformInternalLink(target).slice("./".length) 39 + let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) 40 + if (opts.markdownLinkResolution === "relative") { 41 + return targetSlug as RelativeURL 42 + } else if (opts.markdownLinkResolution === "shortest") { 43 + // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5 44 + const allSlugs = file.data.allSlugs! 45 + 46 + // if the file name is unique, then it's just the filename 47 + const matchingFileNames = allSlugs.filter((slug) => { 48 + const parts = slug.split(path.posix.sep) 49 + const fileName = parts.at(-1) 50 + return targetCanonical === fileName 51 + }) 35 52 36 - // if the file name is unique, then it's just the filename 37 - const matchingFileNames = allSlugs.filter(slug => { 38 - const parts = slug.split(path.posix.sep) 39 - const fileName = parts.at(-1) 40 - return targetCanonical === fileName 41 - }) 53 + if (matchingFileNames.length === 1) { 54 + const targetSlug = canonicalizeServer(matchingFileNames[0]) 55 + return (resolveRelative(curSlug, targetSlug) + targetAnchor) as RelativeURL 56 + } 42 57 43 - if (matchingFileNames.length === 1) { 44 - const targetSlug = canonicalizeServer(matchingFileNames[0]) 45 - return resolveRelative(curSlug, targetSlug) + targetAnchor as RelativeURL 58 + // if it's not unique, then it's the absolute path from the vault root 59 + // (fall-through case) 46 60 } 47 61 48 - // if it's not unique, then it's the absolute path from the vault root 49 - // (fall-through case) 62 + // treat as absolute 63 + return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL 50 64 } 51 65 52 - // treat as absolute 53 - return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL 54 - } 55 - 56 - const outgoing: Set<CanonicalSlug> = new Set() 57 - visit(tree, 'element', (node, _index, _parent) => { 58 - // rewrite all links 59 - if ( 60 - node.tagName === 'a' && 61 - node.properties && 62 - typeof node.properties.href === 'string' 63 - ) { 64 - let dest = node.properties.href as RelativeURL 65 - node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" 66 + const outgoing: Set<CanonicalSlug> = new Set() 67 + visit(tree, "element", (node, _index, _parent) => { 68 + // rewrite all links 69 + if ( 70 + node.tagName === "a" && 71 + node.properties && 72 + typeof node.properties.href === "string" 73 + ) { 74 + let dest = node.properties.href as RelativeURL 75 + node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" 66 76 67 - // don't process external links or intra-document anchors 68 - if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { 69 - dest = node.properties.href = transformLink(dest) 70 - const canonicalDest = path.normalize(joinSegments(curSlug, dest)) 71 - const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) 72 - outgoing.add(destCanonical as CanonicalSlug) 73 - } 77 + // don't process external links or intra-document anchors 78 + if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { 79 + dest = node.properties.href = transformLink(dest) 80 + const canonicalDest = path.normalize(joinSegments(curSlug, dest)) 81 + const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) 82 + outgoing.add(destCanonical as CanonicalSlug) 83 + } 74 84 75 - // rewrite link internals if prettylinks is on 76 - if (opts.prettyLinks && node.children.length === 1 && node.children[0].type === 'text') { 77 - node.children[0].value = path.basename(node.children[0].value) 85 + // rewrite link internals if prettylinks is on 86 + if ( 87 + opts.prettyLinks && 88 + node.children.length === 1 && 89 + node.children[0].type === "text" 90 + ) { 91 + node.children[0].value = path.basename(node.children[0].value) 92 + } 78 93 } 79 - } 80 94 81 - // transform all other resources that may use links 82 - if ( 83 - ["img", "video", "audio", "iframe"].includes(node.tagName) && 84 - node.properties && 85 - typeof node.properties.src === 'string' 86 - ) { 87 - if (!isAbsoluteUrl(node.properties.src)) { 88 - const ext = path.extname(node.properties.src) 89 - node.properties.src = transformLink(path.join("assets", node.properties.src)) + ext 95 + // transform all other resources that may use links 96 + if ( 97 + ["img", "video", "audio", "iframe"].includes(node.tagName) && 98 + node.properties && 99 + typeof node.properties.src === "string" 100 + ) { 101 + if (!isAbsoluteUrl(node.properties.src)) { 102 + const ext = path.extname(node.properties.src) 103 + node.properties.src = 104 + transformLink(path.join("assets", node.properties.src)) + ext 105 + } 90 106 } 91 - } 92 - }) 107 + }) 93 108 94 - file.data.links = [...outgoing] 95 - } 96 - }] 97 - } 109 + file.data.links = [...outgoing] 110 + } 111 + }, 112 + ] 113 + }, 98 114 } 99 115 } 100 116 101 - declare module 'vfile' { 117 + declare module "vfile" { 102 118 interface DataMap { 103 119 links: CanonicalSlug[] 104 120 }
+62 -50
quartz/plugins/transformers/ofm.ts
··· 1 1 import { PluggableList } from "unified" 2 2 import { QuartzTransformerPlugin } from "../types" 3 - import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast' 3 + import { Root, HTML, BlockContent, DefinitionContent, Code } from "mdast" 4 4 import { findAndReplace } from "mdast-util-find-and-replace" 5 - import { slug as slugAnchor } from 'github-slugger' 5 + import { slug as slugAnchor } from "github-slugger" 6 6 import rehypeRaw from "rehype-raw" 7 7 import { visit } from "unist-util-visit" 8 8 import path from "path" ··· 71 71 bug: "bug", 72 72 example: "example", 73 73 quote: "quote", 74 - cite: "quote" 74 + cite: "quote", 75 75 } 76 76 77 77 return calloutMapping[callout] ··· 94 94 } 95 95 96 96 const capitalize = (s: string): string => { 97 - return s.substring(0, 1).toUpperCase() + s.substring(1); 97 + return s.substring(0, 1).toUpperCase() + s.substring(1) 98 98 } 99 99 100 - // Match wikilinks 100 + // Match wikilinks 101 101 // !? -> optional embedding 102 102 // \[\[ -> open brace 103 103 // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) ··· 105 105 // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias) 106 106 const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g") 107 107 108 - // Match highlights 108 + // Match highlights 109 109 const highlightRegex = new RegExp(/==(.+)==/, "g") 110 110 111 - // Match comments 111 + // Match comments 112 112 const commentRegex = new RegExp(/%%(.+)%%/, "g") 113 113 114 114 // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts 115 115 const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) 116 116 117 - export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { 117 + export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( 118 + userOpts, 119 + ) => { 118 120 const opts = { ...defaultOptions, ...userOpts } 119 121 return { 120 122 name: "ObsidianFlavoredMarkdown", ··· 154 156 width ||= "auto" 155 157 height ||= "auto" 156 158 return { 157 - type: 'image', 159 + type: "image", 158 160 url, 159 161 data: { 160 162 hProperties: { 161 - width, height 162 - } 163 - } 163 + width, 164 + height, 165 + }, 166 + }, 164 167 } 165 168 } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) { 166 169 return { 167 - type: 'html', 168 - value: `<video src="${url}" controls></video>` 170 + type: "html", 171 + value: `<video src="${url}" controls></video>`, 169 172 } 170 - } else if ([".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)) { 173 + } else if ( 174 + [".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext) 175 + ) { 171 176 return { 172 - type: 'html', 173 - value: `<audio src="${url}" controls></audio>` 177 + type: "html", 178 + value: `<audio src="${url}" controls></audio>`, 174 179 } 175 180 } else if ([".pdf"].includes(ext)) { 176 181 return { 177 - type: 'html', 178 - value: `<iframe src="${url}"></iframe>` 182 + type: "html", 183 + value: `<iframe src="${url}"></iframe>`, 179 184 } 180 185 } else { 181 186 // TODO: this is the node embed case ··· 187 192 // const url = transformInternalLink(fp + anchor) 188 193 const url = fp + anchor 189 194 return { 190 - type: 'link', 195 + type: "link", 191 196 url, 192 - children: [{ 193 - type: 'text', 194 - value: alias ?? fp 195 - }] 197 + children: [ 198 + { 199 + type: "text", 200 + value: alias ?? fp, 201 + }, 202 + ], 196 203 } 197 204 }) 198 205 } 199 - } 200 - ) 206 + }) 201 207 } 202 208 203 209 if (opts.highlight) { ··· 206 212 findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => { 207 213 const [inner] = capture 208 214 return { 209 - type: 'html', 210 - value: `<span class="text-highlight">${inner}</span>` 215 + type: "html", 216 + value: `<span class="text-highlight">${inner}</span>`, 211 217 } 212 218 }) 213 219 } 214 220 }) 215 221 } 216 - 222 + 217 223 if (opts.comments) { 218 224 plugins.push(() => { 219 225 return (tree: Root, _file) => { 220 226 findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => { 221 227 return { 222 - type: 'text', 223 - value: '' 228 + type: "text", 229 + value: "", 224 230 } 225 231 }) 226 232 } ··· 252 258 const calloutType = typeString.toLowerCase() as keyof typeof callouts 253 259 const collapse = collapseChar === "+" || collapseChar === "-" 254 260 const defaultState = collapseChar === "-" ? "collapsed" : "expanded" 255 - const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType) 261 + const title = 262 + match.input.slice(calloutDirective.length).trim() || capitalize(calloutType) 256 263 257 264 const toggleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold"> 258 265 <polyline points="6 9 12 15 18 9"></polyline> ··· 266 273 <div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div> 267 274 <div class="callout-title-inner">${title}</div> 268 275 ${collapse ? toggleIcon : ""} 269 - </div>` 276 + </div>`, 270 277 } 271 278 272 279 const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode] 273 280 if (remainingText.length > 0) { 274 281 blockquoteContent.push({ 275 - type: 'paragraph', 276 - children: [{ 277 - type: 'text', 278 - value: remainingText, 279 - }, ...restChildren] 282 + type: "paragraph", 283 + children: [ 284 + { 285 + type: "text", 286 + value: remainingText, 287 + }, 288 + ...restChildren, 289 + ], 280 290 }) 281 291 } 282 292 ··· 287 297 node.data = { 288 298 hProperties: { 289 299 ...(node.data?.hProperties ?? {}), 290 - className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`, 300 + className: `callout ${collapse ? "is-collapsible" : ""} ${ 301 + defaultState === "collapsed" ? "is-collapsed" : "" 302 + }`, 291 303 "data-callout": calloutType, 292 304 "data-callout-fold": collapse, 293 - } 305 + }, 294 306 } 295 307 } 296 308 }) ··· 301 313 if (opts.mermaid) { 302 314 plugins.push(() => { 303 315 return (tree: Root, _file) => { 304 - visit(tree, 'code', (node: Code) => { 305 - if (node.lang === 'mermaid') { 316 + visit(tree, "code", (node: Code) => { 317 + if (node.lang === "mermaid") { 306 318 node.data = { 307 319 hProperties: { 308 - className: 'mermaid' 309 - } 320 + className: "mermaid", 321 + }, 310 322 } 311 323 } 312 324 }) ··· 325 337 if (opts.callouts) { 326 338 js.push({ 327 339 script: calloutScript, 328 - loadTime: 'afterDOMReady', 329 - contentType: 'inline' 340 + loadTime: "afterDOMReady", 341 + contentType: "inline", 330 342 }) 331 343 } 332 344 ··· 336 348 import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; 337 349 mermaid.initialize({ startOnLoad: true }); 338 350 `, 339 - loadTime: 'afterDOMReady', 340 - moduleType: 'module', 341 - contentType: 'inline' 351 + loadTime: "afterDOMReady", 352 + moduleType: "module", 353 + contentType: "inline", 342 354 }) 343 355 } 344 356 345 357 return { js } 346 - } 358 + }, 347 359 } 348 360 }
+9 -4
quartz/plugins/transformers/syntax.ts
··· 4 4 export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({ 5 5 name: "SyntaxHighlighting", 6 6 htmlPlugins() { 7 - return [[rehypePrettyCode, { 8 - theme: 'css-variables', 9 - } satisfies Partial<CodeOptions>]] 10 - } 7 + return [ 8 + [ 9 + rehypePrettyCode, 10 + { 11 + theme: "css-variables", 12 + } satisfies Partial<CodeOptions>, 13 + ], 14 + ] 15 + }, 11 16 })
+35 -29
quartz/plugins/transformers/toc.ts
··· 2 2 import { Root } from "mdast" 3 3 import { visit } from "unist-util-visit" 4 4 import { toString } from "mdast-util-to-string" 5 - import { slug as slugAnchor } from 'github-slugger' 5 + import { slug as slugAnchor } from "github-slugger" 6 6 7 7 export interface Options { 8 - maxDepth: 1 | 2 | 3 | 4 | 5 | 6, 9 - minEntries: 1, 8 + maxDepth: 1 | 2 | 3 | 4 | 5 | 6 9 + minEntries: 1 10 10 showByDefault: boolean 11 11 } 12 12 ··· 17 17 } 18 18 19 19 interface TocEntry { 20 - depth: number, 21 - text: string, 20 + depth: number 21 + text: string 22 22 slug: string // this is just the anchor (#some-slug), not the canonical slug 23 23 } 24 24 25 - export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { 25 + export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = ( 26 + userOpts, 27 + ) => { 26 28 const opts = { ...defaultOptions, ...userOpts } 27 29 return { 28 30 name: "TableOfContents", 29 31 markdownPlugins() { 30 - return [() => { 31 - return async (tree: Root, file) => { 32 - const display = file.data.frontmatter?.enableToc ?? opts.showByDefault 33 - if (display) { 34 - const toc: TocEntry[] = [] 35 - let highestDepth: number = opts.maxDepth 36 - visit(tree, 'heading', (node) => { 37 - if (node.depth <= opts.maxDepth) { 38 - const text = toString(node) 39 - highestDepth = Math.min(highestDepth, node.depth) 40 - toc.push({ 41 - depth: node.depth, 42 - text, 43 - slug: slugAnchor(text) 44 - }) 45 - } 46 - }) 32 + return [ 33 + () => { 34 + return async (tree: Root, file) => { 35 + const display = file.data.frontmatter?.enableToc ?? opts.showByDefault 36 + if (display) { 37 + const toc: TocEntry[] = [] 38 + let highestDepth: number = opts.maxDepth 39 + visit(tree, "heading", (node) => { 40 + if (node.depth <= opts.maxDepth) { 41 + const text = toString(node) 42 + highestDepth = Math.min(highestDepth, node.depth) 43 + toc.push({ 44 + depth: node.depth, 45 + text, 46 + slug: slugAnchor(text), 47 + }) 48 + } 49 + }) 47 50 48 - if (toc.length > opts.minEntries) { 49 - file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth })) 51 + if (toc.length > opts.minEntries) { 52 + file.data.toc = toc.map((entry) => ({ 53 + ...entry, 54 + depth: entry.depth - highestDepth, 55 + })) 56 + } 50 57 } 51 58 } 52 - } 53 - }] 59 + }, 60 + ] 54 61 }, 55 62 } 56 63 } 57 64 58 - declare module 'vfile' { 65 + declare module "vfile" { 59 66 interface DataMap { 60 67 toc: TocEntry[] 61 68 } 62 69 } 63 -
+19 -7
quartz/plugins/types.ts
··· 6 6 import { FilePath, ServerSlug } from "../path" 7 7 8 8 export interface PluginTypes { 9 - transformers: QuartzTransformerPluginInstance[], 10 - filters: QuartzFilterPluginInstance[], 11 - emitters: QuartzEmitterPluginInstance[], 9 + transformers: QuartzTransformerPluginInstance[] 10 + filters: QuartzFilterPluginInstance[] 11 + emitters: QuartzEmitterPluginInstance[] 12 12 } 13 13 14 14 type OptionType = object | undefined 15 - export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzTransformerPluginInstance 15 + export type QuartzTransformerPlugin<Options extends OptionType = undefined> = ( 16 + opts?: Options, 17 + ) => QuartzTransformerPluginInstance 16 18 export type QuartzTransformerPluginInstance = { 17 19 name: string 18 20 textTransform?: (src: string | Buffer) => string | Buffer ··· 21 23 externalResources?: () => Partial<StaticResources> 22 24 } 23 25 24 - export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance 26 + export type QuartzFilterPlugin<Options extends OptionType = undefined> = ( 27 + opts?: Options, 28 + ) => QuartzFilterPluginInstance 25 29 export type QuartzFilterPluginInstance = { 26 30 name: string 27 31 shouldPublish(content: ProcessedContent): boolean 28 32 } 29 33 30 - export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance 34 + export type QuartzEmitterPlugin<Options extends OptionType = undefined> = ( 35 + opts?: Options, 36 + ) => QuartzEmitterPluginInstance 31 37 export type QuartzEmitterPluginInstance = { 32 38 name: string 33 - emit(contentDir: string, cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<FilePath[]> 39 + emit( 40 + contentDir: string, 41 + cfg: GlobalConfiguration, 42 + content: ProcessedContent[], 43 + resources: StaticResources, 44 + emitCallback: EmitCallback, 45 + ): Promise<FilePath[]> 34 46 getQuartzComponents(): QuartzComponent[] 35 47 } 36 48
+3 -3
quartz/plugins/vfile.ts
··· 1 - import { Node, Parent } from 'hast' 2 - import { Data, VFile } from 'vfile' 1 + import { Node, Parent } from "hast" 2 + import { Data, VFile } from "vfile" 3 3 4 4 export type QuartzPluginData = Data 5 5 export type ProcessedContent = [Node<QuartzPluginData>, VFile] 6 6 7 7 export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent { 8 - const root: Parent = { type: 'root', children: [] } 8 + const root: Parent = { type: "root", children: [] } 9 9 const vfile = new VFile("") 10 10 vfile.data = vfileData 11 11 return [root, vfile]
+39 -18
quartz/processors/emit.ts
··· 2 2 import fs from "fs" 3 3 import { GlobalConfiguration, QuartzConfig } from "../cfg" 4 4 import { PerfTimer } from "../perf" 5 - import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins" 5 + import { 6 + ComponentResources, 7 + emitComponentResources, 8 + getComponentResources, 9 + getStaticResourcesFromPlugins, 10 + } from "../plugins" 6 11 import { EmitCallback } from "../plugins/types" 7 12 import { ProcessedContent } from "../plugins/vfile" 8 13 import { FilePath, QUARTZ, slugifyFilePath } from "../path" 9 14 import { globbyStream } from "globby" 10 15 11 16 // @ts-ignore 12 - import spaRouterScript from '../components/scripts/spa.inline' 17 + import spaRouterScript from "../components/scripts/spa.inline" 13 18 // @ts-ignore 14 - import plausibleScript from '../components/scripts/plausible.inline' 19 + import plausibleScript from "../components/scripts/plausible.inline" 15 20 // @ts-ignore 16 - import popoverScript from '../components/scripts/popover.inline' 17 - import popoverStyle from '../components/styles/popover.scss' 21 + import popoverScript from "../components/scripts/popover.inline" 22 + import popoverStyle from "../components/styles/popover.scss" 18 23 import { StaticResources } from "../resources" 19 24 import { QuartzLogger } from "../log" 20 25 import { googleFontHref } from "../theme" 21 26 import { trace } from "../trace" 22 27 23 - function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean, staticResources: StaticResources, componentResources: ComponentResources) { 28 + function addGlobalPageResources( 29 + cfg: GlobalConfiguration, 30 + reloadScript: boolean, 31 + staticResources: StaticResources, 32 + componentResources: ComponentResources, 33 + ) { 24 34 staticResources.css.push(googleFontHref(cfg.theme)) 25 35 26 36 // popovers ··· 33 43 const tagId = cfg.analytics.tagId 34 44 staticResources.js.push({ 35 45 src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`, 36 - contentType: 'external', 37 - loadTime: 'afterDOMReady', 46 + contentType: "external", 47 + loadTime: "afterDOMReady", 38 48 }) 39 49 componentResources.afterDOMLoaded.push(` 40 50 window.dataLayer = window.dataLayer || []; ··· 47 57 page_title: document.title, 48 58 page_location: location.href, 49 59 }); 50 - });` 51 - ) 60 + });`) 52 61 } else if (cfg.analytics?.provider === "plausible") { 53 62 componentResources.afterDOMLoaded.push(plausibleScript) 54 63 } ··· 60 69 componentResources.afterDOMLoaded.push(` 61 70 window.spaNavigate = (url, _) => window.location.assign(url) 62 71 const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } }) 63 - document.dispatchEvent(event)` 64 - ) 72 + document.dispatchEvent(event)`) 65 73 } 66 74 67 75 if (reloadScript) { ··· 71 79 script: ` 72 80 const socket = new WebSocket('ws://localhost:3001') 73 81 socket.addEventListener('message', () => document.location.reload()) 74 - ` 82 + `, 75 83 }) 76 84 } 77 85 } 78 86 79 - export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], reloadScript: boolean, verbose: boolean) { 87 + export async function emitContent( 88 + contentFolder: string, 89 + output: string, 90 + cfg: QuartzConfig, 91 + content: ProcessedContent[], 92 + reloadScript: boolean, 93 + verbose: boolean, 94 + ) { 80 95 const perf = new PerfTimer() 81 96 const log = new QuartzLogger(verbose) 82 97 ··· 95 110 // component specific scripts and styles 96 111 const componentResources = getComponentResources(cfg.plugins) 97 112 98 - // important that this goes *after* component scripts 99 - // as the "nav" event gets triggered here and we should make sure 113 + // important that this goes *after* component scripts 114 + // as the "nav" event gets triggered here and we should make sure 100 115 // that everyone else had the chance to register a listener for it 101 116 addGlobalPageResources(cfg.configuration, reloadScript, staticResources, componentResources) 102 117 ··· 112 127 // emitter plugins 113 128 for (const emitter of cfg.plugins.emitters) { 114 129 try { 115 - const emitted = await emitter.emit(contentFolder, cfg.configuration, content, staticResources, emit) 130 + const emitted = await emitter.emit( 131 + contentFolder, 132 + cfg.configuration, 133 + content, 134 + staticResources, 135 + emit, 136 + ) 116 137 emittedFiles += emitted.length 117 138 118 139 if (verbose) { ··· 141 162 const fp = rawFp as FilePath 142 163 const ext = path.extname(fp) 143 164 const src = path.join(contentFolder, fp) as FilePath 144 - const name = slugifyFilePath(fp as FilePath) + ext as FilePath 165 + const name = (slugifyFilePath(fp as FilePath) + ext) as FilePath 145 166 const dest = path.join(assetsPath, name) as FilePath 146 167 const dir = path.dirname(dest) as FilePath 147 168 await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
+6 -2
quartz/processors/filter.ts
··· 2 2 import { QuartzFilterPluginInstance } from "../plugins/types" 3 3 import { ProcessedContent } from "../plugins/vfile" 4 4 5 - export function filterContent(plugins: QuartzFilterPluginInstance[], content: ProcessedContent[], verbose: boolean): ProcessedContent[] { 5 + export function filterContent( 6 + plugins: QuartzFilterPluginInstance[], 7 + content: ProcessedContent[], 8 + verbose: boolean, 9 + ): ProcessedContent[] { 6 10 const perf = new PerfTimer() 7 11 const initialLength = content.length 8 12 for (const plugin of plugins) { 9 13 const updatedContent = content.filter(plugin.shouldPublish) 10 14 11 15 if (verbose) { 12 - const diff = content.filter(x => !updatedContent.includes(x)) 16 + const diff = content.filter((x) => !updatedContent.includes(x)) 13 17 for (const file of diff) { 14 18 console.log(`[filter:${plugin.name}] ${file[1].data.slug}`) 15 19 }
+48 -39
quartz/processors/parse.ts
··· 1 - import esbuild from 'esbuild' 2 - import remarkParse from 'remark-parse' 3 - import remarkRehype from 'remark-rehype' 1 + import esbuild from "esbuild" 2 + import remarkParse from "remark-parse" 3 + import remarkRehype from "remark-rehype" 4 4 import { Processor, unified } from "unified" 5 - import { Root as MDRoot } from 'remark-parse/lib' 6 - import { Root as HTMLRoot } from 'hast' 7 - import { ProcessedContent } from '../plugins/vfile' 8 - import { PerfTimer } from '../perf' 9 - import { read } from 'to-vfile' 10 - import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from '../path' 11 - import path from 'path' 12 - import os from 'os' 13 - import workerpool, { Promise as WorkerPromise } from 'workerpool' 14 - import { QuartzTransformerPluginInstance } from '../plugins/types' 15 - import { QuartzLogger } from '../log' 16 - import { trace } from '../trace' 5 + import { Root as MDRoot } from "remark-parse/lib" 6 + import { Root as HTMLRoot } from "hast" 7 + import { ProcessedContent } from "../plugins/vfile" 8 + import { PerfTimer } from "../perf" 9 + import { read } from "to-vfile" 10 + import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from "../path" 11 + import path from "path" 12 + import os from "os" 13 + import workerpool, { Promise as WorkerPromise } from "workerpool" 14 + import { QuartzTransformerPluginInstance } from "../plugins/types" 15 + import { QuartzLogger } from "../log" 16 + import { trace } from "../trace" 17 17 18 18 export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void> 19 19 export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor { ··· 21 21 let processor = unified().use(remarkParse) 22 22 23 23 // MD AST -> MD AST transforms 24 - for (const plugin of transformers.filter(p => p.markdownPlugins)) { 24 + for (const plugin of transformers.filter((p) => p.markdownPlugins)) { 25 25 processor = processor.use(plugin.markdownPlugins!()) 26 26 } 27 27 28 28 // MD AST -> HTML AST 29 29 processor = processor.use(remarkRehype, { allowDangerousHtml: true }) 30 - 31 30 32 31 // HTML AST -> HTML AST transforms 33 - for (const plugin of transformers.filter(p => p.htmlPlugins)) { 32 + for (const plugin of transformers.filter((p) => p.htmlPlugins)) { 34 33 processor = processor.use(plugin.htmlPlugins!()) 35 34 } 36 35 ··· 57 56 packages: "external", 58 57 plugins: [ 59 58 { 60 - name: 'css-and-scripts-as-text', 59 + name: "css-and-scripts-as-text", 61 60 setup(build) { 62 61 build.onLoad({ filter: /\.scss$/ }, (_) => ({ 63 - contents: '', 64 - loader: 'text' 62 + contents: "", 63 + loader: "text", 65 64 })) 66 65 build.onLoad({ filter: /\.inline\.(ts|js)$/ }, (_) => ({ 67 - contents: '', 68 - loader: 'text' 66 + contents: "", 67 + loader: "text", 69 68 })) 70 - } 71 - } 72 - ] 69 + }, 70 + }, 71 + ], 73 72 }) 74 73 } 75 74 76 - export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) { 75 + export function createFileParser( 76 + transformers: QuartzTransformerPluginInstance[], 77 + baseDir: string, 78 + fps: FilePath[], 79 + allSlugs: ServerSlug[], 80 + verbose: boolean, 81 + ) { 77 82 return async (processor: QuartzProcessor) => { 78 83 const res: ProcessedContent[] = [] 79 84 for (const fp of fps) { ··· 84 89 file.value = file.value.toString().trim() 85 90 86 91 // Text -> Text transforms 87 - for (const plugin of transformers.filter(p => p.textTransform)) { 92 + for (const plugin of transformers.filter((p) => p.textTransform)) { 88 93 file.value = plugin.textTransform!(file.value) 89 94 } 90 95 ··· 110 115 } 111 116 } 112 117 113 - export async function parseMarkdown(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], verbose: boolean): Promise<ProcessedContent[]> { 118 + export async function parseMarkdown( 119 + transformers: QuartzTransformerPluginInstance[], 120 + baseDir: string, 121 + fps: FilePath[], 122 + verbose: boolean, 123 + ): Promise<ProcessedContent[]> { 114 124 const perf = new PerfTimer() 115 125 const log = new QuartzLogger(verbose) 116 126 ··· 118 128 let concurrency = fps.length < CHUNK_SIZE ? 1 : os.availableParallelism() 119 129 120 130 // get all slugs ahead of time as each thread needs a copy 121 - const allSlugs = fps.map(fp => slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath)) 131 + const allSlugs = fps.map((fp) => 132 + slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath), 133 + ) 122 134 123 135 let res: ProcessedContent[] = [] 124 136 log.start(`Parsing input files using ${concurrency} threads`) ··· 128 140 res = await parse(processor) 129 141 } else { 130 142 await transpileWorkerScript() 131 - const pool = workerpool.pool( 132 - './quartz/bootstrap-worker.mjs', 133 - { 134 - minWorkers: 'max', 135 - maxWorkers: concurrency, 136 - workerType: 'thread' 137 - } 138 - ) 143 + const pool = workerpool.pool("./quartz/bootstrap-worker.mjs", { 144 + minWorkers: "max", 145 + maxWorkers: concurrency, 146 + workerType: "thread", 147 + }) 139 148 140 149 const childPromises: WorkerPromise<ProcessedContent[]>[] = [] 141 150 for (const chunk of chunks(fps, CHUNK_SIZE)) { 142 - childPromises.push(pool.exec('parseFiles', [baseDir, chunk, allSlugs, verbose])) 151 + childPromises.push(pool.exec("parseFiles", [baseDir, chunk, allSlugs, verbose])) 143 152 } 144 153 145 154 const results: ProcessedContent[][] = await WorkerPromise.all(childPromises)
+23 -14
quartz/resources.tsx
··· 2 2 import { JSX } from "preact/jsx-runtime" 3 3 4 4 export type JSResource = { 5 - loadTime: 'beforeDOMReady' | 'afterDOMReady' 6 - moduleType?: 'module', 5 + loadTime: "beforeDOMReady" | "afterDOMReady" 6 + moduleType?: "module" 7 7 spaPreserve?: boolean 8 - } & ({ 9 - src: string 10 - contentType: 'external' 11 - } | { 12 - script: string 13 - contentType: 'inline' 14 - }) 8 + } & ( 9 + | { 10 + src: string 11 + contentType: "external" 12 + } 13 + | { 14 + script: string 15 + contentType: "inline" 16 + } 17 + ) 15 18 16 19 export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element { 17 - const scriptType = resource.moduleType ?? 'application/javascript' 20 + const scriptType = resource.moduleType ?? "application/javascript" 18 21 const spaPreserve = preserve ?? resource.spaPreserve 19 - if (resource.contentType === 'external') { 20 - return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve}/> 22 + if (resource.contentType === "external") { 23 + return ( 24 + <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} /> 25 + ) 21 26 } else { 22 27 const content = resource.script 23 - return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script> 28 + return ( 29 + <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}> 30 + {content} 31 + </script> 32 + ) 24 33 } 25 34 } 26 35 27 36 export interface StaticResources { 28 - css: string[], 37 + css: string[] 29 38 js: JSResource[] 30 39 }
+34 -15
quartz/styles/base.scss
··· 21 21 border-radius: 5px; 22 22 } 23 23 24 - p, ul, text, a, tr, td, li, ol, ul, .katex, .math { 24 + p, 25 + ul, 26 + text, 27 + a, 28 + tr, 29 + td, 30 + li, 31 + ol, 32 + ul, 33 + .katex, 34 + .math { 25 35 color: var(--darkgray); 26 36 fill: var(--darkgray); 27 37 } ··· 79 89 font-size: 2rem; 80 90 } 81 91 82 - & li:has(> input[type='checkbox']) { 92 + & li:has(> input[type="checkbox"]) { 83 93 list-style-type: none; 84 94 padding-left: 0; 85 95 margin-left: -1.4rem; ··· 144 154 } 145 155 } 146 156 147 - & .center, & footer { 157 + & .center, 158 + & footer { 148 159 width: $pageWidth; 149 160 margin-left: auto; 150 161 margin-right: auto; ··· 195 206 } 196 207 } 197 208 198 - 199 - 200 - h1, h2, h3, h4, h5, h6 { 209 + h1, 210 + h2, 211 + h3, 212 + h4, 213 + h5, 214 + h6 { 201 215 &[id] > a[href^="#"] { 202 216 margin: 0 0.5rem; 203 217 opacity: 0; ··· 277 291 } 278 292 } 279 293 280 - &[data-line-numbers-max-digits='2'] > [data-line]::before { 294 + &[data-line-numbers-max-digits="2"] > [data-line]::before { 281 295 width: 2rem; 282 296 } 283 - 284 - &[data-line-numbers-max-digits='3'] > [data-line]::before { 297 + 298 + &[data-line-numbers-max-digits="3"] > [data-line]::before { 285 299 width: 3rem; 286 300 } 287 301 } ··· 296 310 background: var(--lightgray); 297 311 } 298 312 299 - tbody, li, p { 313 + tbody, 314 + li, 315 + p { 300 316 line-height: 1.5rem; 301 317 } 302 318 ··· 307 323 border-collapse: collapse; 308 324 } 309 325 310 - td, th { 326 + td, 327 + th { 311 328 padding: 0.2rem 1rem; 312 329 border: 1px solid var(--gray); 313 330 } ··· 331 348 background-color: var(--lightgray); 332 349 } 333 350 334 - audio, video { 351 + audio, 352 + video { 335 353 width: 100%; 336 354 border-radius: 5px; 337 355 } ··· 340 358 flex: 1 1 auto; 341 359 } 342 360 343 - ul.overflow, ol.overflow { 361 + ul.overflow, 362 + ol.overflow { 344 363 height: 400px; 345 364 overflow-y: scroll; 346 365 ··· 354 373 355 374 &:after { 356 375 pointer-events: none; 357 - content: ''; 376 + content: ""; 358 377 width: 100%; 359 - height: 50px; 378 + height: 50px; 360 379 position: absolute; 361 380 left: 0; 362 381 bottom: 0;
+69 -69
quartz/styles/callouts.scss
··· 1 1 @use "sass:color"; 2 2 3 3 .callout { 4 - border: 1px solid var(--border); 5 - background-color: var(--bg); 6 - border-radius: 5px; 7 - padding: 0 1rem; 8 - overflow-y: hidden; 4 + border: 1px solid var(--border); 5 + background-color: var(--bg); 6 + border-radius: 5px; 7 + padding: 0 1rem; 8 + overflow-y: hidden; 9 9 transition: max-height 0.3s ease; 10 10 11 11 & > *:nth-child(2) { 12 12 margin-top: 0; 13 13 } 14 14 15 - &[data-callout="note"] { 16 - --color: #448aff; 17 - --border: #448aff22; 18 - --bg: #448aff09; 19 - } 15 + &[data-callout="note"] { 16 + --color: #448aff; 17 + --border: #448aff22; 18 + --bg: #448aff09; 19 + } 20 20 21 - &[data-callout="abstract"] { 22 - --color: #00b0ff; 23 - --border: #00b0ff22; 24 - --bg: #00b0ff09; 25 - } 21 + &[data-callout="abstract"] { 22 + --color: #00b0ff; 23 + --border: #00b0ff22; 24 + --bg: #00b0ff09; 25 + } 26 26 27 - &[data-callout="info"], &[data-callout="todo"] { 28 - --color: #00b8d4; 29 - --border: #00b8d422; 30 - --bg: #00b8d409; 31 - } 27 + &[data-callout="info"], 28 + &[data-callout="todo"] { 29 + --color: #00b8d4; 30 + --border: #00b8d422; 31 + --bg: #00b8d409; 32 + } 32 33 33 - &[data-callout="tip"] { 34 - --color: #00bfa5; 35 - --border: #00bfa522; 36 - --bg: #00bfa509; 37 - } 34 + &[data-callout="tip"] { 35 + --color: #00bfa5; 36 + --border: #00bfa522; 37 + --bg: #00bfa509; 38 + } 38 39 39 - &[data-callout="success"] { 40 - --color: #09ad7a; 41 - --border: #09ad7122; 42 - --bg: #09ad7109; 43 - } 40 + &[data-callout="success"] { 41 + --color: #09ad7a; 42 + --border: #09ad7122; 43 + --bg: #09ad7109; 44 + } 44 45 45 - &[data-callout="question"] { 46 - --color: #dba642; 47 - --border: #dba64222; 48 - --bg: #dba64209; 49 - } 46 + &[data-callout="question"] { 47 + --color: #dba642; 48 + --border: #dba64222; 49 + --bg: #dba64209; 50 + } 50 51 51 - &[data-callout="warning"] { 52 - --color: #db8942; 53 - --border: #db894222; 54 - --bg: #db894209; 55 - } 52 + &[data-callout="warning"] { 53 + --color: #db8942; 54 + --border: #db894222; 55 + --bg: #db894209; 56 + } 57 + 58 + &[data-callout="failure"], 59 + &[data-callout="danger"], 60 + &[data-callout="bug"] { 61 + --color: #db4242; 62 + --border: #db424222; 63 + --bg: #db424209; 64 + } 56 65 57 - &[data-callout="failure"], &[data-callout="danger"], &[data-callout="bug"] { 58 - --color: #db4242; 59 - --border: #db424222; 60 - --bg: #db424209; 61 - } 66 + &[data-callout="example"] { 67 + --color: #7a43b5; 68 + --border: #7a43b522; 69 + --bg: #7a43b509; 70 + } 62 71 63 - &[data-callout="example"] { 64 - --color: #7a43b5; 65 - --border: #7a43b522; 66 - --bg: #7a43b509; 67 - } 72 + &[data-callout="quote"] { 73 + --color: var(--secondary); 74 + --border: var(--lightgray); 75 + } 68 76 69 - &[data-callout="quote"] { 70 - --color: var(--secondary); 71 - --border: var(--lightgray); 72 - } 73 - 74 77 &.is-collapsed > .callout-title > .fold { 75 - transform: rotateZ(-90deg) 78 + transform: rotateZ(-90deg); 76 79 } 77 80 } 78 81 79 - 80 82 .callout-title { 81 - display: flex; 82 - align-items: center; 83 - gap: 5px; 84 - padding: 1rem 0; 85 - color: var(--color); 83 + display: flex; 84 + align-items: center; 85 + gap: 5px; 86 + padding: 1rem 0; 87 + color: var(--color); 86 88 87 - & .fold { 88 - margin-left: 0.5rem; 89 + & .fold { 90 + margin-left: 0.5rem; 89 91 transition: transform 0.3s ease; 90 92 opacity: 0.8; 91 93 cursor: pointer; 92 94 } 93 - 94 95 } 95 96 96 97 .callout-icon { 97 - width: 18px; 98 - height: 18px; 98 + width: 18px; 99 + height: 18px; 99 100 } 100 101 101 102 .callout-title-inner { 102 - font-weight: 700; 103 + font-weight: 700; 103 104 } 104 -
+1 -1
quartz/styles/variables.scss
··· 3 3 $tabletBreakpoint: 1200px; 4 4 $sidePanelWidth: 400px; 5 5 $topSpacing: 6rem; 6 - $fullPageWidth: $pageWidth + 2 * $sidePanelWidth 6 + $fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
+13 -12
quartz/theme.ts
··· 1 1 export interface ColorScheme { 2 - light: string, 3 - lightgray: string, 4 - gray: string, 5 - darkgray: string, 6 - dark: string, 7 - secondary: string, 8 - tertiary: string, 2 + light: string 3 + lightgray: string 4 + gray: string 5 + darkgray: string 6 + dark: string 7 + secondary: string 8 + tertiary: string 9 9 highlight: string 10 10 } 11 11 12 12 export interface Theme { 13 13 typography: { 14 - header: string, 15 - body: string, 14 + header: string 15 + body: string 16 16 code: string 17 - }, 17 + } 18 18 colors: { 19 - lightMode: ColorScheme, 19 + lightMode: ColorScheme 20 20 darkMode: ColorScheme 21 21 } 22 22 } 23 23 24 - const DEFAULT_SANS_SERIF = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif" 24 + const DEFAULT_SANS_SERIF = 25 + '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' 25 26 const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" 26 27 export function googleFontHref(theme: Theme) { 27 28 const { code, header, body } = theme.typography
+6 -2
quartz/trace.ts
··· 4 4 export function trace(msg: string, err: Error) { 5 5 const stack = err.stack 6 6 console.log() 7 - console.log(chalk.bgRed.white.bold(" ERROR ") + chalk.red(` ${msg}`) + (err.message.length > 0 ? `: ${err.message}` : "")) 7 + console.log( 8 + chalk.bgRed.white.bold(" ERROR ") + 9 + chalk.red(` ${msg}`) + 10 + (err.message.length > 0 ? `: ${err.message}` : ""), 11 + ) 8 12 if (!stack) { 9 13 return 10 14 } 11 15 12 16 let reachedEndOfLegibleTrace = false 13 - for (const line of stack.split('\n').slice(1)) { 17 + for (const line of stack.split("\n").slice(1)) { 14 18 if (reachedEndOfLegibleTrace) { 15 19 break 16 20 }
+6 -1
quartz/worker.ts
··· 6 6 const processor = createProcessor(transformers) 7 7 8 8 // only called from worker thread 9 - export async function parseFiles(baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) { 9 + export async function parseFiles( 10 + baseDir: string, 11 + fps: FilePath[], 12 + allSlugs: ServerSlug[], 13 + verbose: boolean, 14 + ) { 10 15 const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose) 11 16 return parse(processor) 12 17 }
+3 -13
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 - "lib": [ 4 - "esnext", 5 - "DOM", 6 - "DOM.Iterable" 7 - ], 3 + "lib": ["esnext", "DOM", "DOM.Iterable"], 8 4 "experimentalDecorators": true, 9 5 "module": "esnext", 10 6 "target": "esnext", ··· 19 15 "jsx": "react-jsx", 20 16 "jsxImportSource": "preact" 21 17 }, 22 - "include": [ 23 - "**/*.ts", 24 - "**/*.tsx", 25 - "./package.json" 26 - ], 27 - "exclude": [ 28 - "build/**/*.d.ts" 29 - ] 18 + "include": ["**/*.ts", "**/*.tsx", "./package.json"], 19 + "exclude": ["build/**/*.d.ts"] 30 20 }