Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66dd6bcf2d | ||
|
|
bb3877998a | ||
|
|
9450278680 |
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something about Quartz isn't working the way you expect
|
||||
title: ""
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
@@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -24,10 +24,9 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or improvement for Quartz
|
||||
title: ""
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
53
.github/workflows/ci.yaml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v4
|
||||
push:
|
||||
branches:
|
||||
- v4
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
if: ${{ github.repository == 'jackyzha0/quartz' }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Check types and style
|
||||
run: npm run check
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Ensure Quartz builds, check bundle info
|
||||
run: npx quartz build --bundleInfo
|
||||
|
||||
- name: Create release tag
|
||||
uses: Klemensas/action-autotag@stable
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
tag_prefix: "v"
|
||||
39
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- hugo
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||
|
||||
- name: Build Link Index
|
||||
uses: jackyzha0/hugo-obsidian@v2.20
|
||||
with:
|
||||
index: true
|
||||
input: content
|
||||
output: assets/indices
|
||||
root: .
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: '0.96.0'
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo --minify
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./public
|
||||
publish_branch: master # deploying branch
|
||||
cname: three.quartz.jzhao.xyz
|
||||
42
.github/workflows/docker-publish.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Create and publish a Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['hugo']
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
if: github.repository == 'jackyzha0/quartz'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
15
.gitignore
vendored
@@ -1,11 +1,8 @@
|
||||
.DS_Store
|
||||
.gitignore
|
||||
node_modules
|
||||
public
|
||||
prof
|
||||
tsconfig.tsbuildinfo
|
||||
.obsidian
|
||||
.quartz-cache
|
||||
private/
|
||||
.replit
|
||||
replit.nix
|
||||
resources
|
||||
.idea
|
||||
content/.obsidian
|
||||
assets/indices/linkIndex.json
|
||||
assets/indices/contentIndex.json
|
||||
linkmap
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
public
|
||||
node_modules
|
||||
.quartz-cache
|
||||
@@ -20,28 +20,28 @@ If you see someone who is making an extra effort to ensure our community is welc
|
||||
|
||||
The following behaviors are expected and requested of all community members:
|
||||
|
||||
- Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
||||
- Exercise consideration and respect in your speech and actions.
|
||||
- Attempt collaboration before conflict.
|
||||
- Refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||
- 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.
|
||||
- Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
|
||||
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
||||
* Exercise consideration and respect in your speech and actions.
|
||||
* Attempt collaboration before conflict.
|
||||
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||
* 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.
|
||||
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
|
||||
|
||||
## 4. Unacceptable Behavior
|
||||
|
||||
The following behaviors are considered harassment and are unacceptable within our community:
|
||||
|
||||
- Violence, threats of violence or violent language directed against another person.
|
||||
- Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
|
||||
- Posting or displaying sexually explicit or violent material.
|
||||
- Posting or threatening to post other people's personally identifying information ("doxing").
|
||||
- Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
|
||||
- Inappropriate photography or recording.
|
||||
- Inappropriate physical contact. You should have someone's consent before touching them.
|
||||
- Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
|
||||
- Deliberate intimidation, stalking or following (online or in person).
|
||||
- Advocating for, or encouraging, any of the above behavior.
|
||||
- Sustained disruption of community events, including talks and presentations.
|
||||
* Violence, threats of violence or violent language directed against another person.
|
||||
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
|
||||
* Posting or displaying sexually explicit or violent material.
|
||||
* Posting or threatening to post other people's personally identifying information ("doxing").
|
||||
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
|
||||
* Inappropriate photography or recording.
|
||||
* Inappropriate physical contact. You should have someone's consent before touching them.
|
||||
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
|
||||
* Deliberate intimidation, stalking or following (online or in person).
|
||||
* Advocating for, or encouraging, any of the above behavior.
|
||||
* Sustained disruption of community events, including talks and presentations.
|
||||
|
||||
## 5. Weapons Policy
|
||||
|
||||
@@ -59,11 +59,14 @@ If a community member engages in unacceptable behavior, the community organizers
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
## 8. Addressing Grievances
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
## 9. Scope
|
||||
|
||||
@@ -77,7 +80,7 @@ j.zhao2k19@gmail.com
|
||||
|
||||
## 11. License and attribution
|
||||
|
||||
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/).
|
||||
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/).
|
||||
|
||||
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).
|
||||
|
||||
|
||||
20
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM node:20-slim as builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY package.json .
|
||||
COPY package-lock.json* .
|
||||
RUN npm ci
|
||||
FROM alpine:3.16
|
||||
|
||||
FROM node:20-slim
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=builder /usr/src/app/ /usr/src/app/
|
||||
COPY . .
|
||||
CMD ["npx", "quartz", "build", "--serve"]
|
||||
RUN apk add --no-cache go hugo git make perl
|
||||
RUN git config --global --add safe.directory '/quartz'
|
||||
RUN go install github.com/jackyzha0/hugo-obsidian@latest
|
||||
ENV PATH="/root/go/bin:$PATH"
|
||||
RUN git clone https://github.com/jackyzha0/quartz.git /quartz
|
||||
|
||||
WORKDIR /quartz
|
||||
|
||||
CMD ["make", "serve"]
|
||||
|
||||
24
Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
.DEFAULT_GOAL := serve
|
||||
|
||||
help: ## Show all Makefile targets
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
update: ## Update Quartz to the latest version on Github
|
||||
go install github.com/jackyzha0/hugo-obsidian@latest
|
||||
@git remote show upstream || (echo "remote 'upstream' not present, setting 'upstream'" && git remote add upstream https://github.com/jackyzha0/quartz.git)
|
||||
git fetch upstream
|
||||
git log --oneline --decorate --graph ..upstream/hugo
|
||||
git checkout -p upstream/hugo -- layouts .github Makefile assets/js assets/styles/base.scss assets/styles/darkmode.scss config.toml data
|
||||
|
||||
update-force: ## Forcefully pull all changes and don't ask to patch
|
||||
go install github.com/jackyzha0/hugo-obsidian@latest
|
||||
@git remote show upstream || (echo "remote 'upstream' not present, setting 'upstream'" && git remote add upstream https://github.com/jackyzha0/quartz.git)
|
||||
git fetch upstream
|
||||
git checkout upstream/hugo -- layouts .github Makefile assets/js assets/styles/base.scss assets/styles/darkmode.scss config.toml data
|
||||
|
||||
serve: ## Serve Quartz locally
|
||||
hugo-obsidian -input=content -output=assets/indices -index -root=.
|
||||
hugo server --enableGitInfo --minify --bind=$(or $(HUGO_BIND),0.0.0.0) --baseURL=$(or $(HUGO_BASEURL),http://localhost) --port=$(or $(HUGO_PORT),1313) --appendPort=$(or $(HUGO_APPENDPORT),true) --liveReloadPort=$(or $(HUGO_LIVERELOADPORT),-1)
|
||||
|
||||
docker: ## Serve locally using Docker
|
||||
docker run -it --volume=$(shell pwd):/quartz -p 1313:1313 ghcr.io/jackyzha0/quartz:hugo
|
||||
31
README.md
@@ -1,20 +1,23 @@
|
||||
# Quartz v4
|
||||
# Quartz 3
|
||||
|
||||
> [!warning]
|
||||
> Quartz v3 is now deprecated and will no longer be maintained.
|
||||
> See how to migrate to [Quartz 4](https://quartz.jzhao.xyz)
|
||||
|
||||
Host your second brain and [digital garden](https://jzhao.xyz/posts/networked-thought) for free. Quartz features
|
||||
|
||||
1. Extremely fast natural-language search
|
||||
2. Customizable and hackable design based on Hugo
|
||||
3. Automatically generated backlinks, link previews, and local graph
|
||||
4. Built-in CJK + Latex Support and Admonition-style callouts
|
||||
5. Support for both Markdown Links and Wikilinks
|
||||
|
||||
Check out some of the [amazing gardens that community members](https://quartz.jzhao.xyz/notes/showcase/) have published with Quartz!
|
||||
|
||||
> “[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
|
||||
|
||||
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
||||
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
||||
🔗 Get Started: https://three.quartz.jzhao.xyz/
|
||||
|
||||
**If you are looking for Quartz v3, you can find it on the [`hugo` branch](https://github.com/jackyzha0/quartz/tree/hugo).**
|
||||
|
||||
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||
*Quartz Example Screenshot*
|
||||
|
||||
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
||||
|
||||
## Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/sponsors/jackyzha0">
|
||||
<img src="https://cdn.jsdelivr.net/gh/jackyzha0/jackyzha0/sponsorkit/sponsors.svg" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
0
assets/indices/.gitkeep
Normal file
6
assets/js/callouts.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const addCollapsibleCallouts = () => {
|
||||
const collapsibleCallouts = document.querySelectorAll("blockquote.callout-collapsible");
|
||||
collapsibleCallouts.forEach(el => el.addEventListener('click', event => {
|
||||
event.currentTarget.classList.toggle("callout-collapsed");
|
||||
}));
|
||||
}
|
||||
45
assets/js/clipboard.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const svgCopy =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>';
|
||||
const svgCheck =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>';
|
||||
|
||||
|
||||
const addCopyButtons = () => {
|
||||
let els = document.getElementsByClassName("highlight");
|
||||
// for each highlight
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
try {
|
||||
if (els[i].getElementsByClassName("clipboard-button").length) continue;
|
||||
|
||||
// find pre > code inside els[i]
|
||||
let codeBlocks = els[i].getElementsByTagName("code");
|
||||
|
||||
// line numbers are inside first code block
|
||||
let lastCodeBlock = codeBlocks[codeBlocks.length - 1];
|
||||
const button = document.createElement("button");
|
||||
button.className = "clipboard-button";
|
||||
button.type = "button";
|
||||
button.innerHTML = svgCopy;
|
||||
button.ariaLabel = "opy the shown code";
|
||||
// remove every second newline from lastCodeBlock.innerText
|
||||
button.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(lastCodeBlock.innerText.replace(/\n\n/g, "\n")).then(
|
||||
() => {
|
||||
button.blur();
|
||||
button.innerHTML = svgCheck;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = svgCopy
|
||||
button.style.borderColor = ""
|
||||
}, 2000);
|
||||
},
|
||||
(error) => (button.innerHTML = "Error")
|
||||
);
|
||||
});
|
||||
// find chroma inside els[i]
|
||||
let chroma = els[i].getElementsByClassName("chroma")[0];
|
||||
els[i].insertBefore(button, chroma);
|
||||
} catch(error) {
|
||||
console.debug(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
assets/js/code-title.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
function addTitleToCodeBlocks() {
|
||||
const els = document.getElementsByClassName("highlight");
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
try {
|
||||
if (els[i].title.length) {
|
||||
let div = document.createElement("div");
|
||||
if (els[i].getElementsByClassName("code-title").length) continue;
|
||||
div.textContent = els[i].title;
|
||||
div.classList.add("code-title")
|
||||
els[i].insertBefore(div, els[i].firstChild);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
assets/js/darkmode.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
||||
const currentTheme = localStorage.getItem('theme') ?? userPref
|
||||
const syntaxTheme = document.querySelector("#theme-link");
|
||||
|
||||
|
||||
{{ $darkSyntax := resources.Get "styles/_dark_syntax.scss" | resources.ToCSS (dict "outputStyle" "compressed") | resources.Fingerprint "md5" | resources.Minify }}
|
||||
{{ $lightSyntax := resources.Get "styles/_light_syntax.scss" | resources.ToCSS (dict "outputStyle" "compressed") | resources.Fingerprint "md5" | resources.Minify }}
|
||||
|
||||
if (currentTheme) {
|
||||
document.documentElement.setAttribute('saved-theme', currentTheme);
|
||||
syntaxTheme.href = currentTheme === 'dark' ? '{{ $darkSyntax.Permalink }}' : '{{ $lightSyntax.Permalink }}';
|
||||
}
|
||||
|
||||
const switchTheme = (e) => {
|
||||
if (e.target.checked) {
|
||||
document.documentElement.setAttribute('saved-theme', 'dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
syntaxTheme.href = '{{ $darkSyntax.Permalink }}';
|
||||
}
|
||||
else {
|
||||
document.documentElement.setAttribute('saved-theme', 'light')
|
||||
localStorage.setItem('theme', 'light')
|
||||
syntaxTheme.href = '{{ $lightSyntax.Permalink }}';
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Darkmode toggle
|
||||
const toggleSwitch = document.querySelector('#darkmode-toggle')
|
||||
|
||||
// listen for toggle
|
||||
toggleSwitch.addEventListener('change', switchTheme, false)
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
toggleSwitch.checked = true
|
||||
}
|
||||
})
|
||||
61
assets/js/full-text-search.js
Normal file
@@ -0,0 +1,61 @@
|
||||
; (async function() {
|
||||
const encoder = (str) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
const contentIndex = new FlexSearch.Document({
|
||||
cache: true,
|
||||
charset: "latin:extra",
|
||||
optimize: true,
|
||||
index: [
|
||||
{
|
||||
field: "content",
|
||||
tokenize: "reverse",
|
||||
encode: encoder,
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
tokenize: "forward",
|
||||
encode: encoder,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const { content } = await fetchData
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
contentIndex.add({
|
||||
id: key,
|
||||
title: value.title,
|
||||
content: removeMarkdown(value.content),
|
||||
})
|
||||
}
|
||||
|
||||
const formatForDisplay = (id) => ({
|
||||
id,
|
||||
url: id,
|
||||
title: content[id].title,
|
||||
content: content[id].content,
|
||||
})
|
||||
|
||||
registerHandlers((e) => {
|
||||
const term = e.target.value
|
||||
const searchResults = contentIndex.search(term, [
|
||||
{
|
||||
field: "content",
|
||||
limit: 10,
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
limit: 5,
|
||||
},
|
||||
])
|
||||
const getByField = (field) => {
|
||||
const results = searchResults.filter((x) => x.field === field)
|
||||
if (results.length === 0) {
|
||||
return []
|
||||
} else {
|
||||
return [...results[0].result]
|
||||
}
|
||||
}
|
||||
const allIds = new Set([...getByField("title"), ...getByField("content")])
|
||||
const finalResults = [...allIds].map(formatForDisplay)
|
||||
displayResults(term, finalResults, true)
|
||||
})
|
||||
})()
|
||||
279
assets/js/graph.js
Normal file
@@ -0,0 +1,279 @@
|
||||
async function drawGraph(baseUrl, isHome, pathColors, graphConfig) {
|
||||
|
||||
let {
|
||||
depth,
|
||||
enableDrag,
|
||||
enableLegend,
|
||||
enableZoom,
|
||||
opacityScale,
|
||||
scale,
|
||||
repelForce,
|
||||
fontSize } = graphConfig;
|
||||
|
||||
const container = document.getElementById("graph-container")
|
||||
const { index, links, content } = await fetchData
|
||||
|
||||
// Use .pathname to remove hashes / searchParams / text fragments
|
||||
const cleanUrl = window.location.origin + window.location.pathname
|
||||
|
||||
const curPage = cleanUrl.replace(/\/$/g, "").replace(baseUrl, "")
|
||||
|
||||
const parseIdsFromLinks = (links) => [
|
||||
...new Set(links.flatMap((link) => [link.source, link.target])),
|
||||
]
|
||||
|
||||
// Links is mutated by d3. We want to use links later on, so we make a copy and pass that one to d3
|
||||
// Note: shallow cloning does not work because it copies over references from the original array
|
||||
const copyLinks = JSON.parse(JSON.stringify(links))
|
||||
|
||||
const neighbours = new Set()
|
||||
const wl = [curPage || "/", "__SENTINEL"]
|
||||
if (depth >= 0) {
|
||||
while (depth >= 0 && wl.length > 0) {
|
||||
// compute neighbours
|
||||
const cur = wl.shift()
|
||||
if (cur === "__SENTINEL") {
|
||||
depth--
|
||||
wl.push("__SENTINEL")
|
||||
} else {
|
||||
neighbours.add(cur)
|
||||
const outgoing = index.links[cur] || []
|
||||
const incoming = index.backlinks[cur] || []
|
||||
wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parseIdsFromLinks(copyLinks).forEach((id) => neighbours.add(id))
|
||||
}
|
||||
|
||||
const data = {
|
||||
nodes: [...neighbours].map((id) => ({ id })),
|
||||
links: copyLinks.filter((l) => neighbours.has(l.source) && neighbours.has(l.target)),
|
||||
}
|
||||
|
||||
const color = (d) => {
|
||||
if (d.id === curPage || (d.id === "/" && curPage === "")) {
|
||||
return "var(--g-node-active)"
|
||||
}
|
||||
|
||||
for (const pathColor of pathColors) {
|
||||
const path = Object.keys(pathColor)[0]
|
||||
const colour = pathColor[path]
|
||||
if (d.id.startsWith(path)) {
|
||||
return colour
|
||||
}
|
||||
}
|
||||
|
||||
return "var(--g-node)"
|
||||
}
|
||||
|
||||
const drag = (simulation) => {
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(1).restart()
|
||||
d.fx = d.x
|
||||
d.fy = d.y
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x
|
||||
d.fy = event.y
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0)
|
||||
d.fx = null
|
||||
d.fy = null
|
||||
}
|
||||
|
||||
const noop = () => { }
|
||||
return d3
|
||||
.drag()
|
||||
.on("start", enableDrag ? dragstarted : noop)
|
||||
.on("drag", enableDrag ? dragged : noop)
|
||||
.on("end", enableDrag ? dragended : noop)
|
||||
}
|
||||
|
||||
const height = Math.max(container.offsetHeight, isHome ? 500 : 250)
|
||||
const width = container.offsetWidth
|
||||
|
||||
const simulation = d3
|
||||
.forceSimulation(data.nodes)
|
||||
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
||||
.force(
|
||||
"link",
|
||||
d3
|
||||
.forceLink(data.links)
|
||||
.id((d) => d.id)
|
||||
.distance(40),
|
||||
)
|
||||
.force("center", d3.forceCenter())
|
||||
|
||||
const svg = d3
|
||||
.select("#graph-container")
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
||||
|
||||
if (enableLegend) {
|
||||
const legend = [{ Current: "var(--g-node-active)" }, { Note: "var(--g-node)" }, ...pathColors]
|
||||
legend.forEach((legendEntry, i) => {
|
||||
const key = Object.keys(legendEntry)[0]
|
||||
const colour = legendEntry[key]
|
||||
svg
|
||||
.append("circle")
|
||||
.attr("cx", -width / 2 + 20)
|
||||
.attr("cy", height / 2 - 30 * (i + 1))
|
||||
.attr("r", 6)
|
||||
.style("fill", colour)
|
||||
svg
|
||||
.append("text")
|
||||
.attr("x", -width / 2 + 40)
|
||||
.attr("y", height / 2 - 30 * (i + 1))
|
||||
.text(key)
|
||||
.style("font-size", "15px")
|
||||
.attr("alignment-baseline", "middle")
|
||||
})
|
||||
}
|
||||
|
||||
// draw links between nodes
|
||||
const link = svg
|
||||
.append("g")
|
||||
.selectAll("line")
|
||||
.data(data.links)
|
||||
.join("line")
|
||||
.attr("class", "link")
|
||||
.attr("stroke", "var(--g-link)")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("data-source", (d) => d.source.id)
|
||||
.attr("data-target", (d) => d.target.id)
|
||||
|
||||
// svg groups
|
||||
const graphNode = svg.append("g").selectAll("g").data(data.nodes).enter().append("g")
|
||||
|
||||
// calculate radius
|
||||
const nodeRadius = (d) => {
|
||||
const numOut = index.links[d.id]?.length || 0
|
||||
const numIn = index.backlinks[d.id]?.length || 0
|
||||
return 2 + Math.sqrt(numOut + numIn)
|
||||
}
|
||||
|
||||
// draw individual nodes
|
||||
const node = graphNode
|
||||
.append("circle")
|
||||
.attr("class", "node")
|
||||
.attr("id", (d) => d.id)
|
||||
.attr("r", nodeRadius)
|
||||
.attr("fill", color)
|
||||
.style("cursor", "pointer")
|
||||
.on("click", (_, d) => {
|
||||
// SPA navigation
|
||||
const targ = `${baseUrl}${decodeURI(d.id).replace(/\s+/g, "-")}/`
|
||||
window.Million.navigate(new URL(targ), ".singlePage")
|
||||
plausible("Link Click", {
|
||||
props: {
|
||||
href: targ,
|
||||
broken: false,
|
||||
internal: true,
|
||||
graph: true,
|
||||
}
|
||||
})
|
||||
})
|
||||
.on("mouseover", function(_, d) {
|
||||
d3.selectAll(".node").transition().duration(100).attr("fill", "var(--g-node-inactive)")
|
||||
|
||||
const neighbours = parseIdsFromLinks([
|
||||
...(index.links[d.id] || []),
|
||||
...(index.backlinks[d.id] || []),
|
||||
])
|
||||
const neighbourNodes = d3.selectAll(".node").filter((d) => neighbours.includes(d.id))
|
||||
const currentId = d.id
|
||||
window.Million.prefetch(new URL(`${baseUrl}${decodeURI(d.id).replace(/\s+/g, "-")}/`))
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
.filter((d) => d.source.id === currentId || d.target.id === currentId)
|
||||
|
||||
// highlight neighbour nodes
|
||||
neighbourNodes.transition().duration(200).attr("fill", color)
|
||||
|
||||
// highlight links
|
||||
linkNodes.transition().duration(200).attr("stroke", "var(--g-link-active)")
|
||||
|
||||
const bigFont = fontSize * 1.5
|
||||
|
||||
// show text for self
|
||||
d3.select(this.parentNode)
|
||||
.raise()
|
||||
.select("text")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('opacityOld', d3.select(this.parentNode).select('text').style("opacity"))
|
||||
.style('opacity', 1)
|
||||
.style('font-size', bigFont + 'em')
|
||||
.attr('dy', d => nodeRadius(d) + 20 + 'px') // radius is in px
|
||||
})
|
||||
.on("mouseleave", function(_, d) {
|
||||
d3.selectAll(".node").transition().duration(200).attr("fill", color)
|
||||
|
||||
const currentId = d.id
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
.filter((d) => d.source.id === currentId || d.target.id === currentId)
|
||||
|
||||
linkNodes.transition().duration(200).attr("stroke", "var(--g-link)")
|
||||
|
||||
d3.select(this.parentNode)
|
||||
.select("text")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', d3.select(this.parentNode).select('text').attr("opacityOld"))
|
||||
.style('font-size', fontSize + 'em')
|
||||
.attr('dy', d => nodeRadius(d) + 8 + 'px') // radius is in px
|
||||
})
|
||||
.call(drag(simulation))
|
||||
|
||||
// draw labels
|
||||
const labels = graphNode
|
||||
.append("text")
|
||||
.attr("dx", 0)
|
||||
.attr("dy", (d) => nodeRadius(d) + 8 + "px")
|
||||
.attr("text-anchor", "middle")
|
||||
.text((d) => content[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "))
|
||||
.style('opacity', (opacityScale - 1) / 3.75)
|
||||
.style("pointer-events", "none")
|
||||
.style('font-size', fontSize + 'em')
|
||||
.raise()
|
||||
.call(drag(simulation))
|
||||
|
||||
// set panning
|
||||
|
||||
if (enableZoom) {
|
||||
svg.call(
|
||||
d3
|
||||
.zoom()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[width, height],
|
||||
])
|
||||
.scaleExtent([0.25, 4])
|
||||
.on("zoom", ({ transform }) => {
|
||||
link.attr("transform", transform)
|
||||
node.attr("transform", transform)
|
||||
const scale = transform.k * opacityScale;
|
||||
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
||||
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// progress the simulation
|
||||
simulation.on("tick", () => {
|
||||
link
|
||||
.attr("x1", (d) => d.source.x)
|
||||
.attr("y1", (d) => d.source.y)
|
||||
.attr("x2", (d) => d.target.x)
|
||||
.attr("y2", (d) => d.target.y)
|
||||
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y)
|
||||
labels.attr("x", (d) => d.x).attr("y", (d) => d.y)
|
||||
})
|
||||
}
|
||||
79
assets/js/popover.js
Normal file
@@ -0,0 +1,79 @@
|
||||
function htmlToElement(html) {
|
||||
const template = document.createElement("template")
|
||||
html = html.trim()
|
||||
template.innerHTML = html
|
||||
return template.content.firstChild
|
||||
}
|
||||
|
||||
function initPopover(baseURL, useContextualBacklinks) {
|
||||
const basePath = baseURL.replace(window.location.origin, "")
|
||||
fetchData.then(({ content }) => {
|
||||
const links = [...document.getElementsByClassName("internal-link")]
|
||||
links
|
||||
.filter(li => li.dataset.src || (li.dataset.idx && useContextualBacklinks))
|
||||
.forEach(li => {
|
||||
let el
|
||||
if (li.dataset.ctx) {
|
||||
const linkDest = content[li.dataset.src]
|
||||
const popoverElement = `<div class="popover">
|
||||
<h3>${linkDest.title}</h3>
|
||||
<p>${highlight(removeMarkdown(linkDest.content), li.dataset.ctx)}...</p>
|
||||
<p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
|
||||
</div>`
|
||||
el = htmlToElement(popoverElement)
|
||||
} else {
|
||||
const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")]
|
||||
if (linkDest) {
|
||||
let splitLink = li.href.split("#")
|
||||
let cleanedContent = removeMarkdown(linkDest.content)
|
||||
if (splitLink.length > 1) {
|
||||
let headingName = decodeURIComponent(splitLink[1]).replace(/\-/g, " ")
|
||||
let headingIndex = cleanedContent.toLowerCase().indexOf("<b>" + headingName + "</b>")
|
||||
cleanedContent = cleanedContent.substring(headingIndex, cleanedContent.length)
|
||||
}
|
||||
const popoverElement = `<div class="popover">
|
||||
<h3>${linkDest.title}</h3>
|
||||
<p>${cleanedContent.split(" ", 20).join(" ")}...</p>
|
||||
<p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
|
||||
</div>`
|
||||
el = htmlToElement(popoverElement)
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
li.appendChild(el)
|
||||
if (LATEX_ENABLED) {
|
||||
renderMathInElement(el, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$', right: '$', display: false },
|
||||
],
|
||||
throwOnError: false
|
||||
})
|
||||
}
|
||||
|
||||
li.addEventListener("mouseover", () => {
|
||||
// fix tooltip positioning
|
||||
window.FloatingUIDOM.computePosition(li, el, {
|
||||
middleware: [window.FloatingUIDOM.offset(10), window.FloatingUIDOM.inline(), window.FloatingUIDOM.shift()],
|
||||
}).then(({ x, y }) => {
|
||||
Object.assign(el.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
})
|
||||
})
|
||||
|
||||
el.classList.add("visible")
|
||||
plausible("Popover Hover", {
|
||||
props: {
|
||||
href: li.dataset.src
|
||||
}
|
||||
})
|
||||
})
|
||||
li.addEventListener("mouseout", () => {
|
||||
el.classList.remove("visible")
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
25
assets/js/router.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
apply,
|
||||
navigate,
|
||||
prefetch,
|
||||
router,
|
||||
} from "https://unpkg.com/million@1.11.5/dist/router.mjs"
|
||||
|
||||
export const attachSPARouting = (init, rerender) => {
|
||||
// Attach SPA functions to the global Million namespace
|
||||
window.Million = {
|
||||
apply,
|
||||
navigate,
|
||||
prefetch,
|
||||
router,
|
||||
}
|
||||
|
||||
const render = () => requestAnimationFrame(rerender)
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
apply((doc) => init(doc))
|
||||
init()
|
||||
router(".singlePage")
|
||||
render()
|
||||
})
|
||||
window.addEventListener("million:navigate", render)
|
||||
}
|
||||
54
assets/js/semantic-search.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Note: Currently, we use the REST API for Operand because of some unpkg/webpack issues.
|
||||
// In the future, we'd like to use the SDK (https://github.com/operandinc/typescript-sdk).
|
||||
// If someone knows how to do this w/o breaking the Operand typescript-sdk for npm users,
|
||||
// please let Morgan (@morgallant) and/or (@_jzhao) know! <3
|
||||
|
||||
const apiKey = "{{$.Site.Data.config.search.operandApiKey}}"
|
||||
const indexId = "{{$.Site.Data.config.search.operandIndexId}}"
|
||||
|
||||
function parseSearchResults(searchResults) {
|
||||
return searchResults.matches.map((m) => ({
|
||||
content: m.content,
|
||||
title: searchResults.objects[m.objectId].properties.properties._title.text,
|
||||
url: searchResults.objects[m.objectId].properties.properties._url.text,
|
||||
}))
|
||||
}
|
||||
|
||||
async function searchContents(query) {
|
||||
const result = await fetch("https://api.operand.ai/operand.v1.ObjectService/SearchWithin", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${apiKey}`,
|
||||
"Operand-Index-ID": `${indexId}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
limit: 10,
|
||||
}),
|
||||
})
|
||||
if (result.ok) {
|
||||
return parseSearchResults(await result.json())
|
||||
} else {
|
||||
console.error(result)
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(func, timeout = 200) {
|
||||
let timer
|
||||
return (...args) => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args)
|
||||
}, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
registerHandlers(
|
||||
debounce((e) => {
|
||||
let term = e.target.value
|
||||
if (term !== "") {
|
||||
searchContents(term).then((results) => displayResults(term, results))
|
||||
}
|
||||
}),
|
||||
)
|
||||
226
assets/js/util.js
Normal file
@@ -0,0 +1,226 @@
|
||||
// code from https://github.com/danestves/markdown-to-text
|
||||
const removeMarkdown = (
|
||||
markdown,
|
||||
options = {
|
||||
listUnicodeChar: false,
|
||||
stripListLeaders: true,
|
||||
gfm: true,
|
||||
useImgAltText: false,
|
||||
preserveLinks: false,
|
||||
},
|
||||
) => {
|
||||
let output = markdown || ""
|
||||
output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, "")
|
||||
|
||||
try {
|
||||
if (options.stripListLeaders) {
|
||||
if (options.listUnicodeChar)
|
||||
output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, options.listUnicodeChar + " $1")
|
||||
else output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$1")
|
||||
}
|
||||
if (options.gfm) {
|
||||
output = output
|
||||
.replace(/\n={2,}/g, "\n")
|
||||
.replace(/~{3}.*\n/g, "")
|
||||
.replace(/~~/g, "")
|
||||
.replace(/`{3}.*\n/g, "")
|
||||
}
|
||||
if (options.preserveLinks) {
|
||||
output = output.replace(/\[(.*?)\][\[\(](.*?)[\]\)]/g, "$1 ($2)")
|
||||
}
|
||||
output = output
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/^[=\-]{2,}\s*$/g, "")
|
||||
.replace(/\[\^.+?\](\: .*?$)?/g, "")
|
||||
.replace(/(#{1,6})\s+(.+)\1?/g, "<b>$2</b>")
|
||||
.replace(/\s{0,2}\[.*?\]: .*?$/g, "")
|
||||
.replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "")
|
||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "<a>$1</a>")
|
||||
.replace(/!?\[\[\S[^\[\]\|]*(?:\|([^\[\]]*))?\S\]\]/g, "<a>$1</a>")
|
||||
.replace(/^\s{0,3}>\s?/g, "")
|
||||
.replace(/(^|\n)\s{0,3}>\s?/g, "\n\n")
|
||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, "")
|
||||
.replace(/([\*_]{1,3})(\S.*?\S?)\1/g, "$2")
|
||||
.replace(/([\*_]{1,3})(\S.*?\S?)\1/g, "$2")
|
||||
.replace(/(`{3,})(.*?)\1/gm, "$2")
|
||||
.replace(/`(.+?)`/g, "$1")
|
||||
.replace(/\n{2,}/g, "\n\n")
|
||||
.replace(/\[![a-zA-Z]+\][-\+]? /g, "")
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return markdown
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const highlight = (content, term) => {
|
||||
const highlightWindow = 20
|
||||
// try to find direct match first
|
||||
const directMatchIdx = content.indexOf(term)
|
||||
if (directMatchIdx !== -1) {
|
||||
const h = highlightWindow
|
||||
const before = content.substring(0, directMatchIdx).split(" ").slice(-h)
|
||||
const after = content
|
||||
.substring(directMatchIdx + term.length, content.length - 2)
|
||||
.split(" ")
|
||||
.slice(0, h)
|
||||
return (
|
||||
(before.length === h ? `...${before.join(" ")}` : before.join(" ")) +
|
||||
`<span class="search-highlight">${term}</span>` +
|
||||
after.join(" ")
|
||||
)
|
||||
}
|
||||
|
||||
const tokenizedTerm = term.split(/\s+/).filter((t) => t !== "")
|
||||
const splitText = content.split(/\s+/).filter((t) => t !== "")
|
||||
const includesCheck = (token) =>
|
||||
tokenizedTerm.some((term) => token.toLowerCase().startsWith(term.toLowerCase()))
|
||||
|
||||
const occurrencesIndices = splitText.map(includesCheck)
|
||||
|
||||
// calculate best index
|
||||
let bestSum = 0
|
||||
let bestIndex = 0
|
||||
for (let i = 0; i < Math.max(occurrencesIndices.length - highlightWindow, 0); i++) {
|
||||
const window = occurrencesIndices.slice(i, i + highlightWindow)
|
||||
const windowSum = window.reduce((total, cur) => total + cur, 0)
|
||||
if (windowSum >= bestSum) {
|
||||
bestSum = windowSum
|
||||
bestIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
const startIndex = Math.max(bestIndex - highlightWindow, 0)
|
||||
const endIndex = Math.min(startIndex + 2 * highlightWindow, splitText.length)
|
||||
const mappedText = splitText
|
||||
.slice(startIndex, endIndex)
|
||||
.map((token) => {
|
||||
if (includesCheck(token)) {
|
||||
return `<span class="search-highlight">${token}</span>`
|
||||
}
|
||||
return token
|
||||
})
|
||||
.join(" ")
|
||||
.replaceAll('</span> <span class="search-highlight">', " ")
|
||||
return `${startIndex === 0 ? "" : "..."}${mappedText}${endIndex === splitText.length ? "" : "..."
|
||||
}`
|
||||
}
|
||||
|
||||
// Common utilities for search
|
||||
const resultToHTML = ({ url, title, content }) => {
|
||||
return `<button class="result-card" id="${url}">
|
||||
<h3>${title}</h3>
|
||||
<p>${content}</p>
|
||||
</button>`
|
||||
}
|
||||
|
||||
const redir = (id, term) => {
|
||||
const shouldTrim = PRODUCTION && SEARCH_ENABLED
|
||||
const baseURLPrefix = shouldTrim ? "" : BASE_URL.replace(/\/$/g, "")
|
||||
const urlString = `${baseURLPrefix}${id}#:~:text=${encodeURIComponent(term)}`
|
||||
window.Million.navigate(
|
||||
new URL(urlString),
|
||||
".singlePage",
|
||||
)
|
||||
closeSearch()
|
||||
plausible("Search", {
|
||||
props: {
|
||||
term
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
const source = document.getElementById("search-bar")
|
||||
const results = document.getElementById("results-container")
|
||||
const searchContainer = document.getElementById("search-container")
|
||||
if (searchContainer.style.display === "none" || searchContainer.style.display === "") {
|
||||
source.value = ""
|
||||
results.innerHTML = ""
|
||||
searchContainer.style.display = "block"
|
||||
source.focus()
|
||||
} else {
|
||||
searchContainer.style.display = "none"
|
||||
}
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
const searchContainer = document.getElementById("search-container")
|
||||
searchContainer.style.display = "none"
|
||||
}
|
||||
|
||||
const registerHandlers = (onInputFn) => {
|
||||
const source = document.getElementById("search-bar")
|
||||
const searchContainer = document.getElementById("search-container")
|
||||
let term
|
||||
source.addEventListener("keyup", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const anchor = document.getElementsByClassName("result-card")[0]
|
||||
redir(anchor.id, term)
|
||||
}
|
||||
})
|
||||
source.addEventListener("input", onInputFn)
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "k" && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault()
|
||||
openSearch()
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeSearch()
|
||||
}
|
||||
})
|
||||
|
||||
const searchButton = document.getElementById("search-icon")
|
||||
searchButton.addEventListener("click", (_) => {
|
||||
openSearch()
|
||||
})
|
||||
searchButton.addEventListener("keydown", (_) => {
|
||||
openSearch()
|
||||
})
|
||||
searchContainer.addEventListener("click", (_) => {
|
||||
closeSearch()
|
||||
})
|
||||
document.getElementById("search-space").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation()
|
||||
})
|
||||
}
|
||||
|
||||
const displayResults = (term, finalResults, extractHighlight = false) => {
|
||||
const results = document.getElementById("results-container")
|
||||
if (finalResults.length === 0) {
|
||||
results.innerHTML = `<button class="result-card">
|
||||
<h3>No results.</h3>
|
||||
<p>Try another search term?</p>
|
||||
</button>`
|
||||
} else {
|
||||
results.innerHTML = finalResults
|
||||
.map((result) => {
|
||||
if (extractHighlight) {
|
||||
return resultToHTML({
|
||||
url: result.url,
|
||||
title: highlight(result.title, term),
|
||||
content: highlight(removeMarkdown(result.content), term)
|
||||
})
|
||||
} else {
|
||||
return resultToHTML(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
.join("\n")
|
||||
if (LATEX_ENABLED) {
|
||||
renderMathInElement(results, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$', right: '$', display: false },
|
||||
],
|
||||
throwOnError: false
|
||||
})
|
||||
}
|
||||
|
||||
const anchors = [...document.getElementsByClassName("result-card")]
|
||||
anchors.forEach((anchor) => {
|
||||
anchor.onclick = () => redir(anchor.id, term)
|
||||
})
|
||||
}
|
||||
}
|
||||
174
assets/styles/_callouts.scss
Normal file
@@ -0,0 +1,174 @@
|
||||
:root {
|
||||
--callout-summary: #00b0ff;
|
||||
--callout-summary-accent: #7fd7ff;
|
||||
--callout-bug: #f50057;
|
||||
--callout-bug-accent: #ff7aa9;
|
||||
--callout-danger: #ff1744;
|
||||
--callout-danger-accent: #ff8aa1;
|
||||
--callout-example: #7c4dff;
|
||||
--callout-example-accent: #bda5ff;
|
||||
--callout-fail: #ff5252;
|
||||
--callout-fail-accent: #ffa8a8;
|
||||
--callout-info: #00b8d4;
|
||||
--callout-info-accent: #69ebff;
|
||||
--callout-note: #448aff;
|
||||
--callout-note-accent: #a1c4ff;
|
||||
--callout-question: #64dd17;
|
||||
--callout-question-accent: #b0f286;
|
||||
--callout-quote: #9e9e9e;
|
||||
--callout-quote-accent: #cecece;
|
||||
--callout-done: #00c853;
|
||||
--callout-done-accent: #63ffa4;
|
||||
--callout-important: #00bfa5;
|
||||
--callout-important-accent: #5fffe9;
|
||||
--callout-warning: #ff9100;
|
||||
--callout-warning-accent: #ffc87f;
|
||||
}
|
||||
|
||||
[saved-theme=dark] {
|
||||
--callout-summary: #00b0ff !important;
|
||||
--callout-summary-accent: #00587f !important;
|
||||
--callout-bug: #f50057 !important;
|
||||
--callout-bug-accent: #7a002b !important;
|
||||
--callout-danger: #ff1744 !important;
|
||||
--callout-danger-accent: #8b001a !important;
|
||||
--callout-example: #7c4dff !important;
|
||||
--callout-example-accent: #2b00a6 !important;
|
||||
--callout-fail: #ff5252 !important;
|
||||
--callout-fail-accent: #a80000 !important;
|
||||
--callout-info: #00b8d4 !important;
|
||||
--callout-info-accent: #005c6a !important;
|
||||
--callout-note: #448aff !important;
|
||||
--callout-note-accent: #003ca1 !important;
|
||||
--callout-question: #64dd17 !important;
|
||||
--callout-question-accent: #006429 !important;
|
||||
--callout-quote: #9e9e9e !important;
|
||||
--callout-quote-accent: #4f4f4f !important;
|
||||
--callout-done: #00c853 !important;
|
||||
--callout-done-accent: #006429 !important;
|
||||
--callout-important: #00bfa5 !important;
|
||||
--callout-important-accent: #005f52 !important;
|
||||
--callout-warning: #ff9100 !important;
|
||||
--callout-warning-accent: #7f4800 !important;
|
||||
}
|
||||
|
||||
blockquote.callout-collapsible {
|
||||
cursor: pointer;
|
||||
|
||||
&.callout-collapsible::after {
|
||||
content: '-';
|
||||
right: 6px;
|
||||
font-weight: bolder;
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote.callout-collapsed {
|
||||
& > p { border-bottom-right-radius: 5px !important; }
|
||||
padding-bottom: 0 !important;
|
||||
&::after {
|
||||
content: '+' !important;
|
||||
}
|
||||
& > *:not(:first-child) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote[class*="-callout"] {
|
||||
margin-right: 0;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
padding-left: 0 !important;
|
||||
padding-bottom: 0.25em;
|
||||
color: var(--dark);
|
||||
background-color: var(--lightgray);
|
||||
border-left: 6px solid var(--primary) !important;
|
||||
& > p {
|
||||
border-top-right-radius: 5px;
|
||||
padding: 0.5em 1em;
|
||||
margin: 0;
|
||||
color: var(--gray);
|
||||
&:first-child {
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
padding: 0.4em 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockquote[class*="-callout"] > p:first-child::after, blockquote.callout-collapsible::after {
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
margin: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
blockquote[class*="-callout"] > p:first-child {
|
||||
font-weight: bold;
|
||||
padding: 0.4em 35px;
|
||||
|
||||
&::after {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote[class*="-callout"] > p:empty {
|
||||
padding: 1.2em 35px;
|
||||
}
|
||||
|
||||
$summary: summary, abstract, tldr;
|
||||
$bug: bug;
|
||||
$danger: danger, error;
|
||||
$example: example;
|
||||
$fail: fail, failure, missing;
|
||||
$info: info, todo;
|
||||
$note: note;
|
||||
$question: question, help, faq;
|
||||
$quote: quote, cite;
|
||||
$done: done, success, check;
|
||||
$important: important, tip, hint;
|
||||
$warning: warning, caution, attention;
|
||||
$types: $summary, $bug, $danger, $example, $fail, $info, $note, $question, $quote, $done, $important, $warning;
|
||||
$svgs: ();
|
||||
$svgs: map-merge($svgs, ($summary: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='book' class='svg-inline--callout-fa fa-book fa-w-14' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill='currentColor' d='M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($bug: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='bug' class='svg-inline--callout-fa fa-bug fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M511.988 288.9c-.478 17.43-15.217 31.1-32.653 31.1H424v16c0 21.864-4.882 42.584-13.6 61.145l60.228 60.228c12.496 12.497 12.496 32.758 0 45.255-12.498 12.497-32.759 12.496-45.256 0l-54.736-54.736C345.886 467.965 314.351 480 280 480V236c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v244c-34.351 0-65.886-12.035-90.636-32.108l-54.736 54.736c-12.498 12.497-32.759 12.496-45.256 0-12.496-12.497-12.496-32.758 0-45.255l60.228-60.228C92.882 378.584 88 357.864 88 336v-16H32.666C15.23 320 .491 306.33.013 288.9-.484 270.816 14.028 256 32 256h56v-58.745l-46.628-46.628c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0L141.255 160h229.489l54.627-54.627c12.498-12.497 32.758-12.497 45.256 0 12.496 12.497 12.496 32.758 0 45.255L424 197.255V256h56c17.972 0 32.484 14.816 31.988 32.9zM257 0c-61.856 0-112 50.144-112 112h224C369 50.144 318.856 0 257 0z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($danger: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='bolt' class='svg-inline--callout-fa fa-bolt fa-w-10' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='currentColor' d='M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($example: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='list-ol' class='svg-inline--callout-fa fa-list-ol fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($fail: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='times-circle' class='svg-inline--callout-fa fa-times-circle fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($info: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='info-circle' class='svg-inline--callout-fa fa-info-circle fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($note: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='pencil-alt' class='svg-inline--callout-fa fa-pencil-alt fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($question: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='question-circle' class='svg-inline--callout-fa fa-question-circle fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($quote: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='quote-right' class='svg-inline--callout-fa fa-quote-right fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M464 32H336c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48zm-288 0H48C21.5 32 0 53.5 0 80v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($done: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='check-circle' class='svg-inline--callout-fa fa-check-circle fa-w-16' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='currentColor' d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($important: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='fire' class='svg-inline--callout-fa fa-fire fa-w-12' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='currentColor' d='M216 23.86c0-23.8-30.65-32.77-44.15-13.04C48 191.85 224 200 224 288c0 35.63-29.11 64.46-64.85 63.99-35.17-.45-63.15-29.77-63.15-64.94v-85.51c0-21.7-26.47-32.23-41.43-16.5C27.8 213.16 0 261.33 0 320c0 105.87 86.13 192 192 192s192-86.13 192-192c0-170.29-168-193-168-296.14z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
$svgs: map-merge($svgs, ($warning: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' data-icon='exclamation-triangle' class='svg-inline--callout-fa fa-exclamation-triangle fa-w-18' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath fill='currentColor' d='M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z'%3E%3C/path%3E%3C/svg%3E")));
|
||||
|
||||
@function getstr($l) {
|
||||
$v: nth($l, 1);
|
||||
@return $v;
|
||||
}
|
||||
|
||||
@each $type in $types {
|
||||
@each $s in $type {
|
||||
blockquote.#{$s}-callout {
|
||||
border-left: 6px solid var(--callout-#{getstr($type)}) !important;
|
||||
& > p:first-child {
|
||||
background-color: var(--callout-#{getstr($type)}-accent) !important;
|
||||
&::after {
|
||||
content: '';
|
||||
-webkit-mask: map-get($svgs, $type);
|
||||
mask: map-get($svgs, $type);
|
||||
background-color: var(--callout-#{getstr($type)}) !important;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
assets/styles/_dark_syntax.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
/* Background */ .bg { color: #f8f8f2; background-color: #282a36; }
|
||||
/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #282a36; }
|
||||
/* Other */ .chroma .x { }
|
||||
/* Error */ .chroma .err { }
|
||||
/* CodeLine */ .chroma .cl { }
|
||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||
/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
|
||||
/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* Line */ .chroma .line { display: flex; }
|
||||
/* Keyword */ .chroma .k { color: #ff79c6 }
|
||||
/* KeywordConstant */ .chroma .kc { color: #ff79c6 }
|
||||
/* KeywordDeclaration */ .chroma .kd { color: #8be9fd; font-style: italic }
|
||||
/* KeywordNamespace */ .chroma .kn { color: #ff79c6 }
|
||||
/* KeywordPseudo */ .chroma .kp { color: #ff79c6 }
|
||||
/* KeywordReserved */ .chroma .kr { color: #ff79c6 }
|
||||
/* KeywordType */ .chroma .kt { color: #8be9fd }
|
||||
/* Name */ .chroma .n { }
|
||||
/* NameAttribute */ .chroma .na { color: #50fa7b }
|
||||
/* NameBuiltin */ .chroma .nb { color: #8be9fd; font-style: italic }
|
||||
/* NameBuiltinPseudo */ .chroma .bp { }
|
||||
/* NameClass */ .chroma .nc { color: #50fa7b }
|
||||
/* NameConstant */ .chroma .no { }
|
||||
/* NameDecorator */ .chroma .nd { }
|
||||
/* NameEntity */ .chroma .ni { }
|
||||
/* NameException */ .chroma .ne { }
|
||||
/* NameFunction */ .chroma .nf { color: #50fa7b }
|
||||
/* NameFunctionMagic */ .chroma .fm { }
|
||||
/* NameLabel */ .chroma .nl { color: #8be9fd; font-style: italic }
|
||||
/* NameNamespace */ .chroma .nn { }
|
||||
/* NameOther */ .chroma .nx { }
|
||||
/* NameProperty */ .chroma .py { }
|
||||
/* NameTag */ .chroma .nt { color: #ff79c6 }
|
||||
/* NameVariable */ .chroma .nv { color: #8be9fd; font-style: italic }
|
||||
/* NameVariableClass */ .chroma .vc { color: #8be9fd; font-style: italic }
|
||||
/* NameVariableGlobal */ .chroma .vg { color: #8be9fd; font-style: italic }
|
||||
/* NameVariableInstance */ .chroma .vi { color: #8be9fd; font-style: italic }
|
||||
/* NameVariableMagic */ .chroma .vm { }
|
||||
/* Literal */ .chroma .l { }
|
||||
/* LiteralDate */ .chroma .ld { }
|
||||
/* LiteralString */ .chroma .s { color: #f1fa8c }
|
||||
/* LiteralStringAffix */ .chroma .sa { color: #f1fa8c }
|
||||
/* LiteralStringBacktick */ .chroma .sb { color: #f1fa8c }
|
||||
/* LiteralStringChar */ .chroma .sc { color: #f1fa8c }
|
||||
/* LiteralStringDelimiter */ .chroma .dl { color: #f1fa8c }
|
||||
/* LiteralStringDoc */ .chroma .sd { color: #f1fa8c }
|
||||
/* LiteralStringDouble */ .chroma .s2 { color: #f1fa8c }
|
||||
/* LiteralStringEscape */ .chroma .se { color: #f1fa8c }
|
||||
/* LiteralStringHeredoc */ .chroma .sh { color: #f1fa8c }
|
||||
/* LiteralStringInterpol */ .chroma .si { color: #f1fa8c }
|
||||
/* LiteralStringOther */ .chroma .sx { color: #f1fa8c }
|
||||
/* LiteralStringRegex */ .chroma .sr { color: #f1fa8c }
|
||||
/* LiteralStringSingle */ .chroma .s1 { color: #f1fa8c }
|
||||
/* LiteralStringSymbol */ .chroma .ss { color: #f1fa8c }
|
||||
/* LiteralNumber */ .chroma .m { color: #bd93f9 }
|
||||
/* LiteralNumberBin */ .chroma .mb { color: #bd93f9 }
|
||||
/* LiteralNumberFloat */ .chroma .mf { color: #bd93f9 }
|
||||
/* LiteralNumberHex */ .chroma .mh { color: #bd93f9 }
|
||||
/* LiteralNumberInteger */ .chroma .mi { color: #bd93f9 }
|
||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #bd93f9 }
|
||||
/* LiteralNumberOct */ .chroma .mo { color: #bd93f9 }
|
||||
/* Operator */ .chroma .o { color: #ff79c6 }
|
||||
/* OperatorWord */ .chroma .ow { color: #ff79c6 }
|
||||
/* Punctuation */ .chroma .p { }
|
||||
/* Comment */ .chroma .c { color: #6272a4 }
|
||||
/* CommentHashbang */ .chroma .ch { color: #6272a4 }
|
||||
/* CommentMultiline */ .chroma .cm { color: #6272a4 }
|
||||
/* CommentSingle */ .chroma .c1 { color: #6272a4 }
|
||||
/* CommentSpecial */ .chroma .cs { color: #6272a4 }
|
||||
/* CommentPreproc */ .chroma .cp { color: #ff79c6 }
|
||||
/* CommentPreprocFile */ .chroma .cpf { color: #ff79c6 }
|
||||
/* Generic */ .chroma .g { }
|
||||
/* GenericDeleted */ .chroma .gd { color: #ff5555 }
|
||||
/* GenericEmph */ .chroma .ge { text-decoration: underline }
|
||||
/* GenericError */ .chroma .gr { }
|
||||
/* GenericHeading */ .chroma .gh { font-weight: bold }
|
||||
/* GenericInserted */ .chroma .gi { color: #50fa7b; font-weight: bold }
|
||||
/* GenericOutput */ .chroma .go { color: #44475a }
|
||||
/* GenericPrompt */ .chroma .gp { }
|
||||
/* GenericStrong */ .chroma .gs { }
|
||||
/* GenericSubheading */ .chroma .gu { font-weight: bold }
|
||||
/* GenericTraceback */ .chroma .gt { }
|
||||
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
|
||||
/* TextWhitespace */ .chroma .w { }
|
||||
85
assets/styles/_light_syntax.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
/* Background */ .bg { color: #272822; background-color: #fafafa; }
|
||||
/* PreWrapper */ .chroma { color: #272822; background-color: #fafafa; }
|
||||
/* Other */ .chroma .x { }
|
||||
/* Error */ .chroma .err { }
|
||||
/* CodeLine */ .chroma .cl { }
|
||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||
/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
|
||||
/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* Line */ .chroma .line { display: flex; }
|
||||
/* Keyword */ .chroma .k { color: #00a8c8 }
|
||||
/* KeywordConstant */ .chroma .kc { color: #00a8c8 }
|
||||
/* KeywordDeclaration */ .chroma .kd { color: #00a8c8 }
|
||||
/* KeywordNamespace */ .chroma .kn { color: #f92672 }
|
||||
/* KeywordPseudo */ .chroma .kp { color: #00a8c8 }
|
||||
/* KeywordReserved */ .chroma .kr { color: #00a8c8 }
|
||||
/* KeywordType */ .chroma .kt { color: #00a8c8 }
|
||||
/* Name */ .chroma .n { color: #111111 }
|
||||
/* NameAttribute */ .chroma .na { color: #75af00 }
|
||||
/* NameBuiltin */ .chroma .nb { color: #111111 }
|
||||
/* NameBuiltinPseudo */ .chroma .bp { color: #111111 }
|
||||
/* NameClass */ .chroma .nc { color: #75af00 }
|
||||
/* NameConstant */ .chroma .no { color: #00a8c8 }
|
||||
/* NameDecorator */ .chroma .nd { color: #75af00 }
|
||||
/* NameEntity */ .chroma .ni { color: #111111 }
|
||||
/* NameException */ .chroma .ne { color: #75af00 }
|
||||
/* NameFunction */ .chroma .nf { color: #75af00 }
|
||||
/* NameFunctionMagic */ .chroma .fm { color: #111111 }
|
||||
/* NameLabel */ .chroma .nl { color: #111111 }
|
||||
/* NameNamespace */ .chroma .nn { color: #111111 }
|
||||
/* NameOther */ .chroma .nx { color: #75af00 }
|
||||
/* NameProperty */ .chroma .py { color: #111111 }
|
||||
/* NameTag */ .chroma .nt { color: #f92672 }
|
||||
/* NameVariable */ .chroma .nv { color: #111111 }
|
||||
/* NameVariableClass */ .chroma .vc { color: #111111 }
|
||||
/* NameVariableGlobal */ .chroma .vg { color: #111111 }
|
||||
/* NameVariableInstance */ .chroma .vi { color: #111111 }
|
||||
/* NameVariableMagic */ .chroma .vm { color: #111111 }
|
||||
/* Literal */ .chroma .l { color: #ae81ff }
|
||||
/* LiteralDate */ .chroma .ld { color: #d88200 }
|
||||
/* LiteralString */ .chroma .s { color: #d88200 }
|
||||
/* LiteralStringAffix */ .chroma .sa { color: #d88200 }
|
||||
/* LiteralStringBacktick */ .chroma .sb { color: #d88200 }
|
||||
/* LiteralStringChar */ .chroma .sc { color: #d88200 }
|
||||
/* LiteralStringDelimiter */ .chroma .dl { color: #d88200 }
|
||||
/* LiteralStringDoc */ .chroma .sd { color: #d88200 }
|
||||
/* LiteralStringDouble */ .chroma .s2 { color: #d88200 }
|
||||
/* LiteralStringEscape */ .chroma .se { color: #8045ff }
|
||||
/* LiteralStringHeredoc */ .chroma .sh { color: #d88200 }
|
||||
/* LiteralStringInterpol */ .chroma .si { color: #d88200 }
|
||||
/* LiteralStringOther */ .chroma .sx { color: #d88200 }
|
||||
/* LiteralStringRegex */ .chroma .sr { color: #d88200 }
|
||||
/* LiteralStringSingle */ .chroma .s1 { color: #d88200 }
|
||||
/* LiteralStringSymbol */ .chroma .ss { color: #d88200 }
|
||||
/* LiteralNumber */ .chroma .m { color: #ae81ff }
|
||||
/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
|
||||
/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
|
||||
/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
|
||||
/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
|
||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
|
||||
/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
|
||||
/* Operator */ .chroma .o { color: #f92672 }
|
||||
/* OperatorWord */ .chroma .ow { color: #f92672 }
|
||||
/* Punctuation */ .chroma .p { color: #111111 }
|
||||
/* Comment */ .chroma .c { color: #75715e }
|
||||
/* CommentHashbang */ .chroma .ch { color: #75715e }
|
||||
/* CommentMultiline */ .chroma .cm { color: #75715e }
|
||||
/* CommentSingle */ .chroma .c1 { color: #75715e }
|
||||
/* CommentSpecial */ .chroma .cs { color: #75715e }
|
||||
/* CommentPreproc */ .chroma .cp { color: #75715e }
|
||||
/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
|
||||
/* Generic */ .chroma .g { }
|
||||
/* GenericDeleted */ .chroma .gd { }
|
||||
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||
/* GenericError */ .chroma .gr { }
|
||||
/* GenericHeading */ .chroma .gh { }
|
||||
/* GenericInserted */ .chroma .gi { }
|
||||
/* GenericOutput */ .chroma .go { }
|
||||
/* GenericPrompt */ .chroma .gp { }
|
||||
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||
/* GenericSubheading */ .chroma .gu { }
|
||||
/* GenericTraceback */ .chroma .gt { }
|
||||
/* GenericUnderline */ .chroma .gl { }
|
||||
/* TextWhitespace */ .chroma .w { }
|
||||
625
assets/styles/base.scss
Normal file
@@ -0,0 +1,625 @@
|
||||
// Replace this with your own font imports!
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Inter:wght@400;600;700&family=Source+Sans+Pro:wght@400;600&display=swap');
|
||||
:root {
|
||||
--font-body: "Source Sans Pro", sans-serif;
|
||||
--font-header: "Inter", sans-serif;
|
||||
--font-mono: "Fira Code", monospace;
|
||||
}
|
||||
|
||||
// typography
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
&:lang(ar) {
|
||||
& p, & h1, & h2, & h3, article, header {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
& footer > p {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.singlePage {
|
||||
padding: 4em 30vw;
|
||||
margin: 0 auto;
|
||||
max-width: 1000px;
|
||||
@media all and (max-width: 1200px) {
|
||||
padding: 25px 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, thead {
|
||||
font-family: var(--font-header);
|
||||
color: var(--dark);
|
||||
font-weight: revert;
|
||||
margin: 2rem 0 0;
|
||||
padding: 2rem auto 1rem;
|
||||
|
||||
&:hover > .hanchor {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.hanchor {
|
||||
font-family: var(--font-header);
|
||||
opacity: 0.8;
|
||||
transition: color 0.3s ease;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
p, ul, text, a, tr, td, li, ol, ul {
|
||||
font-family: var(--font-body);
|
||||
color: var(--gray);
|
||||
fill: var(--gray);
|
||||
font-weight: revert;
|
||||
margin: revert;
|
||||
padding: revert;
|
||||
}
|
||||
|
||||
tbody, li, p {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.mainTOC {
|
||||
border-radius: 5px;
|
||||
padding: 0.75em 0;
|
||||
|
||||
& details {
|
||||
& summary {
|
||||
cursor: zoom-in;
|
||||
font-family: var(--font-header);
|
||||
color: var(--dark);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&[open] summary {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#TableOfContents > ol {
|
||||
counter-reset: section;
|
||||
margin-left: 0;
|
||||
padding-left: 1.5em;
|
||||
& > li {
|
||||
counter-increment: section;
|
||||
& > ol {
|
||||
counter-reset: subsection;
|
||||
& > li {
|
||||
counter-increment: subsection;
|
||||
&::marker {
|
||||
content: counter(section) "." counter(subsection) " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > li::marker {
|
||||
content: counter(section) " ";
|
||||
}
|
||||
|
||||
& > li::marker, & > li > ol > li::marker {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid var(--outlinegray);
|
||||
width: 100%;
|
||||
padding: 1.5em;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0.2em 1em;
|
||||
border: 1px solid var(--outlinegray);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
border-radius: 3px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
p > img + em {
|
||||
display: block;
|
||||
transform: translateY(-1em);
|
||||
}
|
||||
|
||||
sup {
|
||||
line-height: 0
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
border-left: 3px solid var(--secondary);
|
||||
padding-left: 1em;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.footnotes p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
margin-top: 2em;
|
||||
gap: 1.5em;
|
||||
justify-content: center;
|
||||
|
||||
.disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
& > li {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
& a {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
& a[href$="#"], &.active a {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
& > h1 {
|
||||
margin-top: 2em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
& > .meta {
|
||||
margin: 0 0 1em 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
& a {
|
||||
font-weight: 600;
|
||||
|
||||
&.internal-link {
|
||||
text-decoration: none;
|
||||
background-color: transparentize(#8f9fa9, 0.85);
|
||||
padding: 0 0.1em;
|
||||
margin: auto -0.1em;
|
||||
border-radius: 3px;
|
||||
|
||||
&.broken {
|
||||
opacity: 0.5;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
& .meta {
|
||||
margin: 1.5em 0;
|
||||
& > h1 {
|
||||
margin: 0;
|
||||
}
|
||||
& > p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
margin: 0.4em 0.2em;
|
||||
}
|
||||
|
||||
& > li > a {
|
||||
border-radius: 8px;
|
||||
border: var(--outlinegray) 1px solid;
|
||||
padding: 0.2em 0.5em;
|
||||
&::before {
|
||||
content: "#";
|
||||
margin-right: 0.3em;
|
||||
color: var(--outlinegray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backlinks a {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
sup > a {
|
||||
text-decoration: none;
|
||||
padding: 0 0.1em 0 0.2em;
|
||||
}
|
||||
|
||||
#page-title {
|
||||
margin: 0;
|
||||
& > a {
|
||||
font-family: var(--font-header);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--secondary);
|
||||
&:hover {
|
||||
color: var(--tertiary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: var(--font-mono);
|
||||
padding: 0.75em;
|
||||
border-radius: 3px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
padding: 0.15em 0.3em;
|
||||
border-radius: 5px;
|
||||
background: var(--lightgray);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {opacity:0;}
|
||||
100% {opacity:1;}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
text-align: center;
|
||||
& ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
margin: 2em auto;
|
||||
height: 1px;
|
||||
border: none;
|
||||
background-color: var(--outlinegray);
|
||||
}
|
||||
|
||||
.page-end {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2em;
|
||||
|
||||
@media all and (max-width: 780px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
& > .backlinks-container {
|
||||
& > ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
& > li {
|
||||
margin: 0.5em 0;
|
||||
padding: 0.25em 1em;
|
||||
border: var(--outlinegray) 1px solid;
|
||||
border-radius: 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& #graph-container {
|
||||
border: var(--outlinegray) 1px solid;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
min-height: 250px;
|
||||
margin: 0.5em 0;
|
||||
|
||||
& > svg {
|
||||
margin-bottom: -5px;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-top: 30vh;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 1em 0 2em;
|
||||
|
||||
& > h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
& > nav {
|
||||
@media all and (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#search-icon {
|
||||
background-color: var(--lightgray);
|
||||
border-radius: 4px;
|
||||
height: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
& > p {
|
||||
display: inline;
|
||||
padding: 0 1.5em 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
& svg {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
min-width: 18px;
|
||||
margin: 0 0.5em;
|
||||
|
||||
&:hover .search-path {
|
||||
stroke: var(--tertiary);
|
||||
}
|
||||
|
||||
.search-path {
|
||||
stroke: var(--gray);
|
||||
stroke-width: 2px;
|
||||
transition: stroke 0.5s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#search-container {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
display: none;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
|
||||
& > div {
|
||||
width: 50%;
|
||||
margin-top: 15vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media all and (max-width: 1200px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
background: var(--light);
|
||||
box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16);
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
& > input {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em 1em;
|
||||
font-family: var(--font-body);
|
||||
color: var(--dark);
|
||||
font-size: 1.1em;
|
||||
border: 1px solid var(--outlinegray);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
& .result-card {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: 1px solid var(--outlinegray);
|
||||
border-bottom: none;
|
||||
width: 100%;
|
||||
|
||||
// normalize button props
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
background: var(--light);
|
||||
outline: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: rgba(180, 180, 180, 0.15);
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom: 1px solid var(--outlinegray);
|
||||
}
|
||||
|
||||
& > h3, & > p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-highlight {
|
||||
background-color: #afbfc966;
|
||||
padding: 0.05em 0.2em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.section-ul {
|
||||
list-style: none;
|
||||
margin-top: 2em;
|
||||
padding-left: 0;
|
||||
|
||||
}
|
||||
|
||||
.section-li {
|
||||
margin-bottom: 1em;
|
||||
|
||||
& > .section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
& .tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 > a {
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
padding-right: 1em;
|
||||
flex-basis: 6em;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 {
|
||||
opacity: 1;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& .meta {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropin {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
1% {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
display: none;
|
||||
background-color: var(--light);
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
border: 1px solid var(--outlinegray);
|
||||
border-radius: 5px;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
user-select: none;
|
||||
overflow-wrap: anywhere;
|
||||
box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25);
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
display: inline-block;
|
||||
animation: dropin 0.2s ease;
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
& .meta {
|
||||
margin-top: 0.25rem;
|
||||
opacity: 0.5;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
& > p, & > a {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
#contact_buttons ul {
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
li a {
|
||||
padding: 0 1em;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: var(--highlighted);
|
||||
color: var(--gray);
|
||||
}
|
||||
47
assets/styles/clipboard.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.clipboard-button {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
float: right;
|
||||
right: 0;
|
||||
padding: 0.69em;
|
||||
margin: 0.5em;
|
||||
color: var(--outlinegray);
|
||||
border-color: var(--dark);
|
||||
background-color: var(--lightgray);
|
||||
filter: contrast(1.1);
|
||||
border: 2px solid;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8em;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: 0.12s;
|
||||
|
||||
& > svg {
|
||||
fill: var(--light);
|
||||
filter: contrast(0.3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-color: var(--primary);
|
||||
|
||||
& > svg {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
position: relative;
|
||||
|
||||
&:hover > .clipboard-button {
|
||||
opacity: 1;
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
assets/styles/code-title.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.code-title {
|
||||
color: var(--primary) ;
|
||||
font-family: var(--font-mono);
|
||||
width: max-content;
|
||||
overflow-x: auto;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
padding: 0.5em 0.6em 0.6em; // + 1.2 em
|
||||
max-width: calc(100% - 1.2em); // (-1.2 em) fits article width exactly
|
||||
margin-bottom: -0.2em;
|
||||
z-index: -1;
|
||||
border-top-left-radius: 0.3em;
|
||||
border-top-right-radius: 0.3em;
|
||||
font-size: 0.9em;
|
||||
background-color: var(--lightgray);
|
||||
filter: hue-rotate(-30deg) contrast(1.0) opacity(0.8);
|
||||
}
|
||||
31
assets/styles/custom.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
// Add your own CSS here!
|
||||
|
||||
:root {
|
||||
--light: #faf8f8;
|
||||
--dark: #141021;
|
||||
--secondary: #284b63;
|
||||
--tertiary: #84a59d;
|
||||
--visited: #afbfc9;
|
||||
--primary: #f28482;
|
||||
--gray: #4e4e4e;
|
||||
--lightgray: #f0f0f0;
|
||||
--outlinegray: #dadada;
|
||||
--million-progress-bar-color: var(--secondary);
|
||||
--highlighted: #f5dfaf88;
|
||||
}
|
||||
|
||||
[saved-theme="dark"] {
|
||||
--light: #1e1e21 !important;
|
||||
--dark: #fbfffe !important;
|
||||
--secondary: #6b879a !important;
|
||||
--visited: #4a575e !important;
|
||||
--tertiary: #84a59d !important;
|
||||
--primary: #f58382 !important;
|
||||
--gray: #d4d4d4 !important;
|
||||
--lightgray: #292633 !important;
|
||||
--outlinegray: #343434 !important;
|
||||
--highlighted: #574010;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
.darkmode {
|
||||
float: right;
|
||||
padding: 1em;
|
||||
min-width: 30px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0 10px;
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
& > .toggle {
|
||||
display: none;
|
||||
@@ -10,26 +14,18 @@
|
||||
}
|
||||
|
||||
& svg {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: calc(50% - 10px);
|
||||
fill: var(--darkgray);
|
||||
margin: 0 7px;
|
||||
fill: var(--gray);
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
:root[saved-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[saved-theme="light"] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root[saved-theme="dark"] .toggle ~ label {
|
||||
.toggle:checked ~ label {
|
||||
& > #dayIcon {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -38,11 +34,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
:root .toggle ~ label {
|
||||
.toggle:not(:checked) ~ label {
|
||||
& > #dayIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
& > #nightIcon {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
assets/styles/syntax.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
// Overrides
|
||||
/* Background */
|
||||
.chroma {
|
||||
overflow: hidden !important;
|
||||
background-color: var(--lightgray) !important;
|
||||
}
|
||||
|
||||
/* LineTable */
|
||||
.chroma .lntable {
|
||||
width: auto !important;
|
||||
overflow: auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* LineHighlight */
|
||||
.chroma .hl {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* LineNumbersTable */
|
||||
.chroma .lnt {
|
||||
margin-right: 0.0em !important;
|
||||
padding: 0 0.0em 0 0.0em !important;
|
||||
}
|
||||
|
||||
/* LineNumbers */
|
||||
.chroma .ln {
|
||||
margin-right: 0.0em !important;
|
||||
padding: 0 0.0em 0 0.0em !important;
|
||||
}
|
||||
|
||||
/* GenericDeleted */
|
||||
.chroma .gd {
|
||||
color: #8b080b !important;
|
||||
}
|
||||
|
||||
/* GenericInserted */
|
||||
.chroma .gi {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.lntd:first-of-type > .chroma {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.chroma code {
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: 0.85em !important;
|
||||
line-height: 2em !important;
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.chroma {
|
||||
border-radius: 3px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
pre.chroma {
|
||||
-moz-tab-size:4;-o-tab-size:4;tab-size:4;
|
||||
}
|
||||
|
||||
.katex {
|
||||
font-size: 1.1em !important;
|
||||
}
|
||||
33
config.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
baseURL = "https://three.quartz.jzhao.xyz/"
|
||||
languageCode = "en-us"
|
||||
relativeURLs = false
|
||||
disablePathToLower = true
|
||||
ignoreFiles = [
|
||||
"/content/templates/*",
|
||||
"/content/private/*",
|
||||
]
|
||||
summaryLength = 20
|
||||
paginate = 10
|
||||
enableGitInfo = true
|
||||
|
||||
[markup]
|
||||
[markup.tableOfContents]
|
||||
endLevel = 3
|
||||
ordered = true
|
||||
startLevel = 2
|
||||
[markup.highlight]
|
||||
noClasses = false
|
||||
anchorLineNos = false
|
||||
codeFences = true
|
||||
guessSyntax = true
|
||||
hl_Lines = ""
|
||||
lineAnchors = ""
|
||||
lineNoStart = 1
|
||||
lineNos = true
|
||||
lineNumbersInTable = true
|
||||
style = "dracula"
|
||||
[frontmatter]
|
||||
lastmod = ["lastmod", ":git", "date", "publishDate"]
|
||||
publishDate = ["publishDate", "date"]
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
||||
27
content/_index.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: 🪴 Quartz 3.3
|
||||
enableToc: false
|
||||
---
|
||||
|
||||
Host your second brain and [digital garden](https://jzhao.xyz/posts/networked-thought) for free. Quartz features
|
||||
|
||||
1. Extremely fast natural-language [[notes/search]]
|
||||
2. Customizable and hackable design based on [Hugo](https://gohugo.io/)
|
||||
3. Automatically generated backlinks, link previews, and local graph
|
||||
4. Built-in [[notes/CJK + Latex Support (测试) | CJK + Latex Support]] and [[notes/callouts | Admonition-style callouts]]
|
||||
5. Support for both Markdown Links and Wikilinks
|
||||
|
||||
Check out some of the [amazing gardens that community members](notes/showcase.md) have published with Quartz or read about [why I made Quartz](notes/philosophy.md) to begin with.
|
||||
|
||||
## Get Started
|
||||
> 📚 Step 1: [Setup your own digital garden using Quartz](notes/setup.md)
|
||||
|
||||
Returning user? Figure out how to [[notes/updating|update]] your existing Quartz garden.
|
||||
|
||||
If you prefer browsing the contents of this site through a list instead of a graph, you see a list of all [setup-related notes](/tags/setup).
|
||||
|
||||
### Troubleshooting
|
||||
- 🚧 [Troubleshooting and FAQ](notes/troubleshooting.md)
|
||||
- 🐛 [Submit an Issue](https://github.com/jackyzha0/quartz/issues)
|
||||
- 👀 [Discord Community](https://discord.gg/cRFFHYye7t)
|
||||
|
||||
40
content/notes/CJK + Latex Support (测试).md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "CJK + Latex Support (测试)"
|
||||
---
|
||||
|
||||
## Chinese, Japanese, Korean Support
|
||||
几乎在我们意识到之前,我们已经离开了地面。
|
||||
|
||||
우리가 그것을 알기도 전에 우리는 땅을 떠났습니다.
|
||||
|
||||
私たちがそれを知るほぼ前に、私たちは地面を離れていました。
|
||||
|
||||
## Latex
|
||||
|
||||
Block math works with two dollar signs `$$...$$`
|
||||
|
||||
$$f(x) = \int_{-\infty}^\infty
|
||||
f\hat(\xi),e^{2 \pi i \xi x}
|
||||
\,d\xi$$
|
||||
|
||||
Inline math also works with single dollar signs `$...$`. For example, Euler's identity but inline: $e^{i\pi} = -1$
|
||||
|
||||
Aligned equations work quite well:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
a &= b + c \\ &= e + f \\
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
And matrices
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 & 3 \\
|
||||
a & b & c
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
## RTL
|
||||
More information on configuring RTL languages like Arabic in the [config](notes/config.md) page.
|
||||
63
content/notes/callouts.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Callouts"
|
||||
---
|
||||
|
||||
## Callout support
|
||||
|
||||
Quartz supports the same Admonition-callout syntax as Obsidian.
|
||||
|
||||
This includes
|
||||
- 12 Distinct callout types (each with several aliases)
|
||||
- Collapsable callouts
|
||||
|
||||
See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts).
|
||||
|
||||
## Showcase
|
||||
|
||||
> [!EXAMPLE] Examples
|
||||
>
|
||||
> Aliases: example
|
||||
|
||||
> [!note] Notes
|
||||
>
|
||||
> Aliases: note
|
||||
|
||||
> [!abstract] Summaries
|
||||
>
|
||||
> Aliases: abstract, summary, tldr
|
||||
|
||||
> [!info] Info
|
||||
>
|
||||
> Aliases: info, todo
|
||||
|
||||
> [!tip] Hint
|
||||
>
|
||||
> Aliases: tip, hint, important
|
||||
|
||||
> [!success] Success
|
||||
>
|
||||
> Aliases: success, check, done
|
||||
|
||||
> [!question] Question
|
||||
>
|
||||
> Aliases: question, help, faq
|
||||
|
||||
> [!warning] Warning
|
||||
>
|
||||
> Aliases: warning, caution, attention
|
||||
|
||||
> [!failure] Failure
|
||||
>
|
||||
> Aliases: failure, fail, missing
|
||||
|
||||
> [!danger] Error
|
||||
>
|
||||
> Aliases: danger, error
|
||||
|
||||
> [!bug] Bug
|
||||
>
|
||||
> Aliases: bug
|
||||
|
||||
> [!quote] Quote
|
||||
>
|
||||
> Aliases: quote, cite
|
||||
228
content/notes/config.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
tags:
|
||||
- setup
|
||||
weight: 0
|
||||
---
|
||||
|
||||
## Configuration
|
||||
Quartz is designed to be extremely configurable. You can find the bulk of the configuration scattered throughout the repository depending on how in-depth you'd like to get.
|
||||
|
||||
The majority of configuration can be found under `data/config.yaml`. An annotated example configuration is shown below.
|
||||
|
||||
```yaml {title="data/config.yaml"}
|
||||
# The name to display in the footer
|
||||
name: Jacky Zhao
|
||||
|
||||
# whether to globally show the table of contents on each page
|
||||
# this can be turned off on a per-page basis by adding this to the
|
||||
# front-matter of that note
|
||||
enableToc: true
|
||||
|
||||
# whether to by-default open or close the table of contents on each page
|
||||
openToc: false
|
||||
|
||||
# whether to display on-hover link preview cards
|
||||
enableLinkPreview: true
|
||||
|
||||
# whether to render titles for code blocks
|
||||
enableCodeBlockTitle: true
|
||||
|
||||
# whether to render copy buttons for code blocks
|
||||
enableCodeBlockCopy: true
|
||||
|
||||
# whether to render callouts
|
||||
enableCallouts: true
|
||||
|
||||
# whether to try to process Latex
|
||||
enableLatex: true
|
||||
|
||||
# whether to enable single-page-app style rendering
|
||||
# this prevents flashes of unstyled content and improves
|
||||
# smoothness of Quartz. More info in issue #109 on GitHub
|
||||
enableSPA: true
|
||||
|
||||
# whether to render a footer
|
||||
enableFooter: true
|
||||
|
||||
# whether backlinks of pages should show the context in which
|
||||
# they were mentioned
|
||||
enableContextualBacklinks: true
|
||||
|
||||
# whether to show a section of recent notes on the home page
|
||||
enableRecentNotes: false
|
||||
|
||||
# whether to display an 'edit' button next to the last edited field
|
||||
# that links to github
|
||||
enableGitHubEdit: true
|
||||
GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content
|
||||
|
||||
# whether to render mermaid diagrams
|
||||
enableMermaid: true
|
||||
|
||||
# whether to use Operand to power semantic search
|
||||
# IMPORTANT: replace this API key with your own if you plan on using
|
||||
# Operand search!
|
||||
search:
|
||||
enableSemanticSearch: false
|
||||
operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
|
||||
operandIndexId: "REPLACE-WITH-YOUR-OPERAND-INDEX-ID"
|
||||
|
||||
# page description used for SEO
|
||||
description:
|
||||
Host your second brain and digital garden for free. Quartz features extremely fast full-text search,
|
||||
Wikilink support, backlinks, local graph, tags, and link previews.
|
||||
|
||||
# title of the home page (also for SEO)
|
||||
page_title:
|
||||
"🪴 Quartz 3.3"
|
||||
|
||||
# links to show in the footer
|
||||
links:
|
||||
- link_name: Twitter
|
||||
link: https://twitter.com/_jzhao
|
||||
- link_name: Github
|
||||
link: https://github.com/jackyzha0
|
||||
```
|
||||
|
||||
### Code Block Titles
|
||||
To add code block titles with Quartz:
|
||||
|
||||
1. Ensure that code block titles are enabled in Quartz's configuration:
|
||||
|
||||
```yaml {title="data/config.yaml", linenos=false}
|
||||
enableCodeBlockTitle: true
|
||||
```
|
||||
|
||||
2. Add the `title` attribute to the desired [code block
|
||||
fence](https://gohugo.io/content-management/syntax-highlighting/#highlighting-in-code-fences):
|
||||
|
||||
```markdown {linenos=false}
|
||||
```yaml {title="data/config.yaml"}
|
||||
enableCodeBlockTitle: true # example from step 1
|
||||
```
|
||||
```
|
||||
|
||||
**Note** that if `{title=<my-title>}` is included, and code block titles are not
|
||||
enabled, no errors will occur, and the title attribute will be ignored.
|
||||
|
||||
### HTML Favicons
|
||||
If you would like to customize the favicons of your Quartz-based website, you
|
||||
can add them to the `data/config.yaml` file. The **default** without any set
|
||||
`favicon` key is:
|
||||
|
||||
```html {title="layouts/partials/head.html", linenostart=15}
|
||||
<link rel="shortcut icon" href="icon.png" type="image/png">
|
||||
```
|
||||
|
||||
The default can be overridden by defining a value to the `favicon` key in your
|
||||
`data/config.yaml` file. For example, here is a `List[Dictionary]` example format, which is
|
||||
equivalent to the default:
|
||||
|
||||
```yaml {title="data/config.yaml", linenos=false}
|
||||
favicon:
|
||||
- { rel: "shortcut icon", href: "icon.png", type: "image/png" }
|
||||
# - { ... } # Repeat for each additional favicon you want to add
|
||||
```
|
||||
|
||||
In this format, the keys are identical to their HTML representations.
|
||||
|
||||
If you plan to add multiple favicons generated by a website (see list below), it
|
||||
may be easier to define it as HTML. Here is an example which appends the
|
||||
**Apple touch icon** to Quartz's default favicon:
|
||||
|
||||
```yaml {title="data/config.yaml", linenos=false}
|
||||
favicon: |
|
||||
<link rel="shortcut icon" href="icon.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
```
|
||||
|
||||
This second favicon will now be used as a web page icon when someone adds your
|
||||
webpage to the home screen of their Apple device. If you are interested in more
|
||||
information about the current and past standards of favicons, you can read
|
||||
[this article](https://www.emergeinteractive.com/insights/detail/the-essentials-of-favicons/).
|
||||
|
||||
**Note** that all generated favicon paths, defined by the `href`
|
||||
attribute, are relative to the `static/` directory.
|
||||
|
||||
### Graph View
|
||||
To customize the Interactive Graph view, you can poke around `data/graphConfig.yaml`.
|
||||
|
||||
```yaml {title="data/graphConfig.yaml"}
|
||||
# if true, a Global Graph will be shown on home page with full width, no backlink.
|
||||
# A different set of Local Graphs will be shown on sub pages.
|
||||
# if false, Local Graph will be default on every page as usual
|
||||
enableGlobalGraph: false
|
||||
|
||||
### Local Graph ###
|
||||
localGraph:
|
||||
# whether automatically generate a legend
|
||||
enableLegend: false
|
||||
|
||||
# whether to allow dragging nodes in the graph
|
||||
enableDrag: true
|
||||
|
||||
# whether to allow zooming and panning the graph
|
||||
enableZoom: true
|
||||
|
||||
# how many neighbours of the current node to show (-1 is all nodes)
|
||||
depth: 1
|
||||
|
||||
# initial zoom factor of the graph
|
||||
scale: 1.2
|
||||
|
||||
# how strongly nodes should repel each other
|
||||
repelForce: 2
|
||||
|
||||
# how strongly should nodes be attracted to the center of gravity
|
||||
centerForce: 1
|
||||
|
||||
# what the default link length should be
|
||||
linkDistance: 1
|
||||
|
||||
# how big the node labels should be
|
||||
fontSize: 0.6
|
||||
|
||||
# scale at which to start fading the labes on nodes
|
||||
opacityScale: 3
|
||||
|
||||
### Global Graph ###
|
||||
globalGraph:
|
||||
# same settings as above
|
||||
|
||||
### For all graphs ###
|
||||
# colour specific nodes path off of their path
|
||||
paths:
|
||||
- /moc: "#4388cc"
|
||||
```
|
||||
|
||||
|
||||
## Styling
|
||||
Want to go even more in-depth? You can add custom CSS styling and change existing colours through editing `assets/styles/custom.scss`. If you'd like to target specific parts of the site, you can add ids and classes to the HTML partials in `/layouts/partials`.
|
||||
|
||||
### Partials
|
||||
Partials are what dictate what gets rendered to the page. Want to change how pages are styled and structured? You can edit the appropriate layout in `/layouts`.
|
||||
|
||||
For example, the structure of the home page can be edited through `/layouts/index.html`. To customize the footer, you can edit `/layouts/partials/footer.html`
|
||||
|
||||
More info about partials on [Hugo's website.](https://gohugo.io/templates/partials/)
|
||||
|
||||
Still having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
|
||||
|
||||
## Language Support
|
||||
[CJK + Latex Support (测试)](notes/CJK%20+%20Latex%20Support%20(测试).md) comes out of the box with Quartz.
|
||||
|
||||
Want to support languages that read from right-to-left (like Arabic)? Hugo (and by proxy, Quartz) supports this natively.
|
||||
|
||||
Follow the steps [Hugo provides here](https://gohugo.io/content-management/multilingual/#configure-languages) and modify your `config.toml`
|
||||
|
||||
For example:
|
||||
|
||||
```toml
|
||||
defaultContentLanguage = 'ar'
|
||||
[languages]
|
||||
[languages.ar]
|
||||
languagedirection = 'rtl'
|
||||
title = 'مدونتي'
|
||||
weight = 1
|
||||
```
|
||||
17
content/notes/custom Domain.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "Custom Domain"
|
||||
---
|
||||
|
||||
### Registrar
|
||||
This step is only applicable if you are using a **custom domain**! If you are using a `<YOUR-USERNAME>.github.io` domain, you can skip this step.
|
||||
|
||||
For this last bit to take effect, you also need to create a CNAME record with the DNS provider you register your domain with (i.e. NameCheap, Google Domains).
|
||||
|
||||
GitHub has some [documentation on this](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site), but the tldr; is to
|
||||
|
||||
1. Go to your forked repository (`github.com/<YOUR-GITHUB-USERNAME>/quartz`) settings page and go to the Pages tab. Under "Custom domain", type your custom domain, then click **Save**.
|
||||
2. Go to your DNS Provider and create a CNAME record that points from your domain to `<YOUR-GITHUB-USERNAME.github.io.` (yes, with the trailing period).
|
||||
|
||||
*Example Configuration for Quartz*
|
||||
3. Wait 30 minutes to an hour for the network changes to kick in.
|
||||
4. Done!
|
||||
57
content/notes/docker.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Hosting with Docker"
|
||||
tags:
|
||||
- setup
|
||||
---
|
||||
|
||||
If you want to host Quartz on a machine without using a webpage hosting service, it may be easier to [install Docker Compose](https://docs.docker.com/compose/install/) and follow the instructions below than to [install Quartz's dependencies manually](notes/preview%20changes.md).
|
||||
## Hosting Quartz Locally
|
||||
You can serve Quartz locally at `http://localhost:1313` with the following script, replacing `/path/to/quartz` with the
|
||||
actual path to your Quartz folder.
|
||||
|
||||
docker-compose.yml
|
||||
```
|
||||
services:
|
||||
quartz-hugo:
|
||||
image: ghcr.io/jackyzha0/quartz:hugo
|
||||
container_name: quartz-hugo
|
||||
volumes:
|
||||
- /path/to/quartz:/quartz
|
||||
ports:
|
||||
- 1313:1313
|
||||
|
||||
# optional
|
||||
environment:
|
||||
- HUGO_BIND=0.0.0.0
|
||||
- HUGO_BASEURL=http://localhost
|
||||
- HUGO_PORT=1313
|
||||
- HUGO_APPENDPORT=true
|
||||
- HUGO_LIVERELOADPORT=-1
|
||||
```
|
||||
|
||||
Then run with: `docker-compose up -d` in the same directory as your `docker-compose.yml` file.
|
||||
|
||||
While the container is running, you can update the `quartz` fork with: `docker exec -it quartz-hugo make update`.
|
||||
|
||||
## Exposing Your Container to the Internet
|
||||
|
||||
### To Your Public IP Address with Port Forwarding (insecure)
|
||||
|
||||
Assuming you are already familiar with [port forwarding](https://en.wikipedia.org/wiki/Port_forwarding) and [setting it up with your router model](https://portforward.com):
|
||||
|
||||
1. You should set the environment variable `HUGO_BASEURL=http://your-public-ip` and then start your container.
|
||||
2. Set up port forwarding on your router from port `p` to `your-local-ip:1313`.
|
||||
3. You should now be able to access Quartz from outside your local network at `http://your-public-ip:p`.
|
||||
|
||||
However, your HTTP connection will be unencrypted and **this method is not secure**.
|
||||
|
||||
### To a Domain using Cloudflare Proxy
|
||||
|
||||
1. Port forward 443 (HTTPS) from your machine.
|
||||
2. Buy a custom domain (say, `your-domain.com`) from [Cloudflare](https://www.cloudflare.com/products/registrar/). Point a DNS A record from `your-domain.com` to your public IP address and enable the proxy.
|
||||
3. Set the environment variables `HUGO_BASEURL=https://your-domain.com`, `HUGO_PORT=443`, and `HUGO_APPENDPORT=false`. Change `1313:1313` to `443:443` for the `ports` in `docker-compose.yml`.
|
||||
4. Spin up your Quartz container and enjoy it at `https://your-domain.com`!
|
||||
|
||||
### To a Domain using a Reverse Proxy
|
||||
|
||||
If you want to serve more than just Quartz to the internet on this machine (or don't want to use the Cloudflare registrar and proxy), you should follow the steps in the section above (as appropriate) and also set up a reverse proxy, like [Traefik](https://doc.traefik.io/traefik). Be sure to configure your TLS certificates too!
|
||||
52
content/notes/editing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Editing Content in Quartz"
|
||||
tags:
|
||||
- setup
|
||||
weight: -4
|
||||
---
|
||||
|
||||
## Editing
|
||||
Quartz runs on top of [Hugo](https://gohugo.io/) so all notes are written in [Markdown](https://www.markdownguide.org/getting-started/).
|
||||
|
||||
### Folder Structure
|
||||
Here's a rough overview of what's what.
|
||||
|
||||
**All content in your garden can found in the `/content` folder.** To make edits, you can open any of the files and make changes directly and save it. You can organize content into any folder you'd like.
|
||||
|
||||
**To edit the main home page, open `/content/_index.md`.**
|
||||
|
||||
### Front Matter
|
||||
Hugo is picky when it comes to metadata for files. Make sure that your title is double-quoted and that you have a title defined at the top of your file like so, otherwise the generated page will not have a title!
|
||||
|
||||
You can also add tags here as well.
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Example Title"
|
||||
tags:
|
||||
- example-tag
|
||||
---
|
||||
|
||||
Rest of your content here...
|
||||
```
|
||||
|
||||
### Obsidian
|
||||
I recommend using [Obsidian](http://obsidian.md/) as a way to edit and grow your digital garden. It comes with a really nice editor and graphical interface to preview all of your local files.
|
||||
|
||||
This step is **highly recommended**.
|
||||
|
||||
> 🔗 Step 3: [How to setup your Obsidian Vault to work with Quartz](notes/obsidian.md)
|
||||
|
||||
## Previewing Changes
|
||||
This step is purely optional and mostly for those who want to see the published version of their digital garden locally before opening it up to the internet. This is *highly recommended* but not required.
|
||||
|
||||
> 👀 Step 4: [Preview Quartz Changes](notes/preview%20changes.md)
|
||||
|
||||
For those who like to live life more on the edge, viewing the garden through Obsidian gets you pretty close to the real thing.
|
||||
|
||||
## Publishing Changes
|
||||
Now that you know the basics of managing your digital garden using Quartz, you can publish it to the internet!
|
||||
|
||||
> 🌍 Step 5: [Hosting Quartz online!](notes/hosting.md)
|
||||
|
||||
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
|
||||
99
content/notes/hosting.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: "Deploying Quartz to the Web"
|
||||
tags:
|
||||
- setup
|
||||
weight: -1
|
||||
aliases:
|
||||
- hosting
|
||||
---
|
||||
|
||||
## Hosting on GitHub Pages
|
||||
Quartz is designed to be effortless to deploy. If you forked and cloned Quartz directly from the repository, everything should already be good to go! Follow the steps below.
|
||||
|
||||
### Enable GitHub Actions Permissions
|
||||
By default, GitHub disables workflows from modifying your files (for good reason!). However, Quartz needs this to write the actual site files back to GitHub.
|
||||
|
||||
Head to `Settings > Action > General > Workflow Permissions` and choose `Read and Write Permissions`
|
||||
|
||||
![[notes/images/github-actions.png]]
|
||||
*Enable GitHub Actions*
|
||||
|
||||
### Enable GitHub Pages
|
||||
|
||||
Head to the 'Settings' tab of your forked repository and go to the 'Pages' tab.
|
||||
|
||||
1. (IMPORTANT) Set the source to deploy from `master` (and not `hugo`) using `/ (root)`
|
||||
2. Set a custom domain here if you have one!
|
||||
|
||||
*Enable GitHub Pages*
|
||||
|
||||
### Pushing Changes
|
||||
To see your changes on the internet, we need to push it them to GitHub. Quartz is a `git` repository so updating it is the same workflow as you would follow as if it were just a regular software project.
|
||||
|
||||
```shell
|
||||
# Navigate to Quartz folder
|
||||
cd <path-to-quartz>
|
||||
|
||||
# Commit all changes
|
||||
git add .
|
||||
git commit -m "message describing changes"
|
||||
|
||||
# Push to GitHub to update site
|
||||
git push origin hugo
|
||||
```
|
||||
|
||||
Note: we specifically push to the `hugo` branch here. Our GitHub action automatically runs everytime a push to is detected to that branch and then updates the `master` branch for redeployment.
|
||||
|
||||
### Setting up the Site
|
||||
Now let's get this site up and running. Never hosted a site before? No problem. Have a fancy custom domain you already own or want to subdomain your Quartz? That's easy too.
|
||||
|
||||
Here, we take advantage of GitHub's free page hosting to deploy our site. Change `baseURL` in `/config.toml`.
|
||||
|
||||
Make sure that your `baseURL` has a trailing `/`!
|
||||
|
||||
[Reference `config.toml` here](https://github.com/jackyzha0/quartz/blob/hugo/config.toml)
|
||||
|
||||
```toml
|
||||
baseURL = "https://<YOUR-DOMAIN>/"
|
||||
```
|
||||
|
||||
If you are using this under a subdomain (e.g. `<YOUR-GITHUB-USERNAME>.github.io/quartz`), include the trailing `/`. **You need to do this especially if you are using GitHub!**
|
||||
|
||||
```toml
|
||||
baseURL = "https://<YOUR-GITHUB-USERNAME>.github.io/quartz/"
|
||||
```
|
||||
|
||||
Change `cname` in `/.github/workflows/deploy.yaml`. Again, if you don't have a custom domain to use, you can use `<YOUR-USERNAME>.github.io`.
|
||||
|
||||
Please note that the `cname` field should *not* have any path `e.g. end with /quartz` or have a trailing `/`.
|
||||
|
||||
[Reference `deploy.yaml` here](https://github.com/jackyzha0/quartz/blob/hugo/.github/workflows/deploy.yaml)
|
||||
|
||||
```yaml {title=".github/workflows/deploy.yaml"}
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }} # this can stay as is, GitHub fills this in for us!
|
||||
publish_dir: ./public
|
||||
publish_branch: master
|
||||
cname: <YOUR-DOMAIN>
|
||||
```
|
||||
|
||||
Have a custom domain? [Learn how to set it up with Quartz ](notes/custom%20Domain.md).
|
||||
|
||||
### Ignoring Files
|
||||
Only want to publish a subset of all of your notes? Don't worry, Quartz makes this a simple two-step process.
|
||||
|
||||
❌ [Excluding pages from being published](notes/ignore%20notes.md)
|
||||
|
||||
## Docker Support
|
||||
If you don't want to use a hosting service, you can host using [Docker](notes/docker.md) instead!
|
||||
I would *not use this method* unless you know what you are doing.
|
||||
|
||||
---
|
||||
|
||||
Now that your Quartz is live, let's figure out how to make Quartz really *yours*!
|
||||
|
||||
> Step 6: 🎨 [Customizing Quartz](notes/config.md)
|
||||
|
||||
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
|
||||
31
content/notes/ignore notes.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: "Ignoring Notes"
|
||||
---
|
||||
|
||||
### Quartz Ignore
|
||||
Edit `ignoreFiles` in `config.toml` to include paths you'd like to exclude from being rendered.
|
||||
|
||||
```toml
|
||||
...
|
||||
ignoreFiles = [
|
||||
"/content/templates/*",
|
||||
"/content/private/*",
|
||||
"<your path here>"
|
||||
]
|
||||
```
|
||||
|
||||
`ignoreFiles` supports the use of Regular Expressions (RegEx) so you can ignore patterns as well (e.g. ignoring all `.png`s by doing `\\.png$`).
|
||||
To ignore a specific file, you can also add the tag `draft: true` to the frontmatter of a note.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Some Private Note
|
||||
draft: true
|
||||
---
|
||||
...
|
||||
```
|
||||
|
||||
More details in [Hugo's documentation](https://gohugo.io/getting-started/configuration/#ignore-content-and-data-files-when-rendering).
|
||||
|
||||
### Global Ignore
|
||||
However, just adding to the `ignoreFiles` will only prevent the page from being access through Quartz. If you want to prevent the file from being pushed to GitHub (for example if you have a public repository), you need to also add the path to the `.gitignore` file at the root of the repository.
|
||||
BIN
content/notes/images/fork.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
content/notes/images/github-actions.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
content/notes/images/github-pages.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
content/notes/images/google-domains.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
content/notes/images/obsidian-settings.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
37
content/notes/obsidian.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Obsidian Vault Integration"
|
||||
tags:
|
||||
- setup
|
||||
weight: -3
|
||||
---
|
||||
|
||||
## Setup
|
||||
Obsidian is the preferred way to use Quartz. You can either create a new Obsidian Vault or link one that your already have.
|
||||
|
||||
### New Vault
|
||||
If you don't have an existing Vault, [download Obsidian](https://obsidian.md/) and create a new Vault in the `/content` folder that you created and cloned during the [setup](notes/setup.md) step.
|
||||
|
||||
### Linking an existing Vault
|
||||
The easiest way to use an existing Vault is to copy all of your files (directory and hierarchies intact) into the `/content` folder.
|
||||
|
||||
## Settings
|
||||
Great, now that you have your Obsidian linked to your Quartz, let's fix some settings so that they play well.
|
||||
|
||||
Open Settings > Files & Links and look for these two items:
|
||||
|
||||
1. Set the **New link format** to **Absolute Path in vault**. If you have a completely flat vault (no folders), this step isn't necessary.
|
||||
2. Turn **on** the **Automatically update internal links** setting.
|
||||
|
||||
|
||||
![[notes/images/obsidian-settings.png]]*Obsidian Settings*
|
||||
|
||||
## Templates
|
||||
Inserting front matter everytime you want to create a new Note gets annoying really quickly. Luckily, Obsidian supports templates which makes inserting new content really easily.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> **If you decide to overwrite the `/content` folder completely, don't remove the `/content/templates` folder!**
|
||||
|
||||
Head over to Options > Core Plugins and enable the Templates plugin. Then go to Options > Hotkeys and set a hotkey for 'Insert Template' (I recommend `[cmd]+T`). That way, when you create a new note, you can just press the hotkey for a new template and be ready to go!
|
||||
|
||||
> 👀 Step 4: [Preview Quartz Changes](notes/preview%20changes.md)
|
||||
17
content/notes/philosophy.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "Quartz Philosophy"
|
||||
---
|
||||
|
||||
> “[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
|
||||
|
||||
## Why Quartz?
|
||||
Hosting a public digital garden isn't easy. There are an overwhelming number of tutorials, resources, and guides for tools like [Notion](https://www.notion.so/), [Roam](https://roamresearch.com/), and [Obsidian](https://obsidian.md/), yet none of them have super easy to use *free* tools to publish that garden to the world.
|
||||
|
||||
I've personally found that
|
||||
1. It's nice to access notes from anywhere
|
||||
2. Having a public digital garden invites open conversations
|
||||
3. It makes keeping personal notes and knowledge *playful and fun*
|
||||
|
||||
I was really inspired by [Bianca](https://garden.bianca.digital/) and [Joel](https://joelhooks.com/digital-garden)'s digital gardens and wanted to try making my own.
|
||||
|
||||
**The goal of Quartz is to make hosting your own public digital garden free and simple.** You don't even need your own website. Quartz does all of that for you and gives your own little corner of the internet.
|
||||
41
content/notes/preview changes.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "Preview Changes"
|
||||
tags:
|
||||
- setup
|
||||
weight: -2
|
||||
---
|
||||
|
||||
If you'd like to preview what your Quartz site looks like before deploying it to the internet, the following
|
||||
instructions guide you through installing the proper dependencies to run it locally.
|
||||
|
||||
|
||||
## Install `hugo-obsidian`
|
||||
This step will generate the list of backlinks for Hugo to parse. Ensure you have [Go](https://golang.org/doc/install) (>= 1.16) installed.
|
||||
|
||||
```bash
|
||||
# Install and link `hugo-obsidian` locally
|
||||
go install github.com/jackyzha0/hugo-obsidian@latest
|
||||
```
|
||||
|
||||
If you are running into an error saying that `command not found: hugo-obsidian`, make sure you set your `GOPATH` correctly (see [[notes/troubleshooting#`command not found: hugo-obsidian`|the troubleshooting page]])! This will allow your terminal to correctly recognize hugo-obsidian as an executable.
|
||||
|
||||
## Installing Hugo
|
||||
Hugo is the static site generator that powers Quartz. [Install Hugo with "extended" Sass/SCSS version](https://gohugo.io/getting-started/installing/) first. Then,
|
||||
|
||||
```bash
|
||||
# Navigate to your local Quartz folder
|
||||
cd <location-of-your-local-quartz>
|
||||
|
||||
# Start local server
|
||||
make serve
|
||||
|
||||
# View your site in a browser at http://localhost:1313/
|
||||
```
|
||||
|
||||
> [!INFO] Docker Support
|
||||
>
|
||||
> If you have the Docker CLI installed already, you can avoid installing `hugo-obsidian` and `hugo`. Instead, open your terminal, navigate to your folder with Quartz and run `make docker`
|
||||
|
||||
Afterwards, start the Hugo server as shown above and your local backlinks and interactive graph should be populated! Now, let's get it hosted online.
|
||||
|
||||
> 🌍 Step 5: [Hosting Quartz online!](notes/hosting.md)
|
||||
37
content/notes/search.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Search"
|
||||
---
|
||||
|
||||
Quartz supports two modes of searching through content.
|
||||
|
||||
## Full-text
|
||||
Full-text search is the default in Quartz. It produces results that *exactly* match the search query. This is easier to setup but usually produces lower quality matches.
|
||||
|
||||
```yaml {title="data/config.yaml"}
|
||||
# the default option
|
||||
enableSemanticSearch: false
|
||||
```
|
||||
|
||||
## Natural Language
|
||||
Natural language search is powered by [Operand](https://beta.operand.ai/). It understands language like a person does and finds results that best match user intent. In this sense, it is closer to how Google Search works.
|
||||
|
||||
Natural language search tends to produce higher quality results than full-text search.
|
||||
|
||||
Here's how to set it up.
|
||||
|
||||
1. Login or Register for a new Operand account. Click the verification link sent to your email, and you'll be redirected to the dashboard. (Note) You do not need to enter a credit card to create an account, or get started with the Operand API. The first $10 of usage each month is free. To learn more, see pricing. If you go over your free quota, we'll (politely) reach out and ask you to configure billing.
|
||||
2. Create your first index. On the dashboard, under "Indexes", enter the name and description of your index, and click "Create Index". Note down the ID of the index (obtained by clicking on the index name in the list of indexes), as you'll need it in the next step. IDs are unique to each index, and look something like `uqv1duxxbdxu`.
|
||||
3. Click into the index you've created. Under "Index Something", select "SITEMAP" from the dropdown and click "Add Source".
|
||||
4. For the "Sitemap.xml URL", put your deployed site's base URL followed by `sitemap.xml`. For example, for `quartz.jzhao.xyz`, put `https://quartz.jzhao.xyz/sitemap.xml`. Leave the URL Regex empty.
|
||||
5. Get your API key. On the dashboard, under "API Keys", you can manage your API keys. If you don't already have an API key, click "Create API Key". You'll need this for the next step.
|
||||
6. Open `data/config.yaml`. Set `enableSemanticSearch` to `true`, `operandApiKey` to your copied key, and `operandIndexId` to the ID of the index we created from earlier..
|
||||
|
||||
```yaml {title="data/config.yaml"}
|
||||
# the default option
|
||||
search:
|
||||
enableSemanticSearch: true
|
||||
operandApiKey: "jp9k5hudse2a828z98kxd6z3payi8u90rnjf"
|
||||
operandIndexId: "s0kf3bd6tldw"
|
||||
```
|
||||
7. Push your changes to the site and wait for it to deploy.
|
||||
8. Check the Operand dashboard and wait for your site to index. Enjoy natural language search powered by Operand!
|
||||
36
content/notes/setup.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "Setup"
|
||||
tags:
|
||||
- setup
|
||||
weight: -5
|
||||
---
|
||||
|
||||
## Making your own Quartz
|
||||
Setting up Quartz requires a basic understanding of `git`. If you are unfamiliar, [this resource](https://resources.nwplus.io/2-beginner/how-to-git-github.html) is a great place to start!
|
||||
|
||||
### Forking
|
||||
> A fork is a copy of a repository. Forking a repository allows you to freely experiment with changes without affecting the original project.
|
||||
|
||||
Navigate to the GitHub repository for the Quartz project:
|
||||
|
||||
📁 [Quartz Repository](https://github.com/jackyzha0/quartz)
|
||||
|
||||
Then, Fork the repository into your own GitHub account. **Make sure that when you fork, you _uncheck_ the 'Copy the `hugo` branch only' option**.
|
||||
|
||||
If you don't have an account, you can make on for free [here](https://github.com/join). More details about forking a repo can be found on [GitHub's documentation](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
|
||||
|
||||
![[notes/images/fork.png]]
|
||||
|
||||
### Cloning
|
||||
After you've made a fork of the repository, you need to download the files locally onto your machine. Ensure you have `git`, then type the following command in your terminal replacing `YOUR-USERNAME` with your GitHub username.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/YOUR-USERNAME/quartz
|
||||
```
|
||||
|
||||
## Editing
|
||||
Great! Now you have everything you need to start editing and growing your digital garden. If you're ready to start writing content already, check out the recommended flow for editing notes in Quartz.
|
||||
|
||||
> ✏️ Step 2: [Editing Notes in Quartz](notes/editing.md)
|
||||
|
||||
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
|
||||
@@ -1,24 +1,23 @@
|
||||
---
|
||||
title: "Quartz Showcase"
|
||||
title: "Showcase"
|
||||
---
|
||||
|
||||
Want to see what Quartz can do? Here are some cool community gardens:
|
||||
Want to see what Quartz can do? Here are some cool community gardens :)
|
||||
|
||||
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
|
||||
- [Jacky Zhao's Garden](https://jzhao.xyz/)
|
||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
||||
- [AWAGMI Intern Notes](https://notes.awagmi.xyz/)
|
||||
- [Shihyu's PKM](https://shihyuho.github.io/pkm/)
|
||||
- [SlRvb's Site](https://slrvb.github.io/Site/)
|
||||
- [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/)
|
||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||
- [Siyang's Courtyard](https://siyangsun.github.io/courtyard/)
|
||||
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
||||
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
||||
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
||||
- [oldwinterの数字花园](https://garden.oldwinter.top/)
|
||||
- [SethMB Work](https://sethmb.xyz/)
|
||||
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
||||
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
||||
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
||||
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
||||
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
||||
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||
|
||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/hugo/content/notes/showcase.md)!
|
||||
86
content/notes/troubleshooting.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: "Troubleshooting and FAQ"
|
||||
---
|
||||
|
||||
Still having trouble? Here are a list of common questions and problems people encounter when installing Quartz.
|
||||
|
||||
While you're here, join our [Discord](https://discord.gg/cRFFHYye7t) :)
|
||||
|
||||
### Does Quartz have Latex support?
|
||||
Yes! See [CJK + Latex Support (测试)](notes/CJK%20+%20Latex%20Support%20(测试).md) for a brief demo.
|
||||
|
||||
### Can I use \<Obsidian Plugin\> in Quartz?
|
||||
Unless it produces direct Markdown output in the file, no. There currently is no way to bundle plugin code with Quartz.
|
||||
|
||||
The easiest way would be to add your own HTML partial that supports the functionality you are looking for.
|
||||
|
||||
### My GitHub pages is just showing the README and not Quartz
|
||||
Make sure you set the source to deploy from `master` (and not `hugo`) using `/ (root)`! See more in the [hosting](/notes/hosting) guide
|
||||
|
||||
### Some of my pages have 'January 1, 0001' as the last modified date
|
||||
This is a problem caused by `git` treating files as case-insensitive by default and some of your posts probably have capitalized file names. You can turn this off in your Quartz by running this command.
|
||||
|
||||
```shell
|
||||
# in the root of your Quartz (same folder as config.toml)
|
||||
git config core.ignorecase true
|
||||
|
||||
# or globally (not recommended)
|
||||
git config --global core.ignorecase true
|
||||
```
|
||||
|
||||
### Can I publish only a subset of my pages?
|
||||
Yes! Quartz makes selective publishing really easy. Heres a guide on [excluding pages from being published](notes/ignore%20notes.md).
|
||||
|
||||
### Can I host this myself and not on GitHub Pages?
|
||||
Yes! All built files can be found under `/public` in the `master` branch. More details under [hosting](notes/hosting.md).
|
||||
|
||||
### `command not found: hugo-obsidian`
|
||||
Make sure you set your `GOPATH` correctly! This will allow your terminal to correctly recognize `hugo-obsidian` as an executable.
|
||||
|
||||
```shell
|
||||
# Add the following 2 lines to your ~/.bash_profile (~/.zshrc if you are on Mac)
|
||||
export GOPATH=/Users/$USER/go
|
||||
export PATH=$GOPATH/bin:$PATH
|
||||
|
||||
# In your current terminal, to reload the session
|
||||
source ~/.bash_profile # again, (~/.zshrc if you are on Mac)
|
||||
```
|
||||
|
||||
### How come my notes aren't being rendered?
|
||||
You probably forgot to include front matter in your Markdown files. You can either setup [Obsidian](notes/obsidian.md) to do this for you or you need to manually define it. More details in [the 'how to edit' guide](notes/editing.md).
|
||||
|
||||
### My custom domain isn't working!
|
||||
Walk through the steps in [the hosting guide](notes/hosting.md) again. Make sure you wait 30 min to 1 hour for changes to take effect.
|
||||
|
||||
### How do I setup analytics?
|
||||
Quartz by default uses [Plausible](https://plausible.io/) for analytics.
|
||||
|
||||
If you would prefer to use Google Analytics, you can follow this [guide in the Hugo documentation](https://gohugo.io/templates/internal/#google-analytics).
|
||||
|
||||
Alternatively, you can also import your Google Analytics data into Plausible by [following this guide](https://plausible.io/docs/google-analytics-import).
|
||||
|
||||
|
||||
### How do I change the content on the home page?
|
||||
To edit the main home page, open `/content/_index.md`.
|
||||
|
||||
### How do I change the colours?
|
||||
You can change the theme by editing `assets/custom.scss`. More details on customization and themeing can be found in the [customization guide](notes/config.md).
|
||||
|
||||
### How do I add images?
|
||||
You can put images anywhere in the `/content` folder.
|
||||
|
||||
```markdown
|
||||
Example image (source is in content/notes/images/example.png)
|
||||

|
||||
```
|
||||
|
||||
### My Interactive Graph and Backlinks aren't up to date
|
||||
By default, the `linkIndex.json` (which Quartz needs to generate the Interactive Graph and Backlinks) are not regenerated locally. To set that up, see the guide on [local editing](notes/editing.md)
|
||||
|
||||
### Can I use React/Vue/some other framework?
|
||||
Not out of the box. You could probably make it work by editing `/layouts/_default/single.html` but that's not what Quartz is designed to work with. 99% of things you are trying to do with those frameworks you can accomplish perfectly fine using just vanilla HTML/CSS/JS.
|
||||
|
||||
## Still Stuck?
|
||||
Quartz isn't perfect! If you're still having troubles, file an issue in the GitHub repo with as much information as you can reasonably provide. Alternatively, you can message me on [Twitter](https://twitter.com/_jzhao) and I'll try to get back to you as soon as I can.
|
||||
|
||||
🐛 [Submit an Issue](https://github.com/jackyzha0/quartz/issues)
|
||||
34
content/notes/updating.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Updating"
|
||||
aliases:
|
||||
- update
|
||||
---
|
||||
|
||||
Haven't updated Quartz in a while and want all the cool new optimizations? On Unix/Mac systems you can run the following command for a one-line update! This command will show you a log summary of all commits since you last updated, press `q` to acknowledge this. Then, it will show you each change in turn and press `y` to accept the patch or `n` to reject it. Usually you should press `y` for most of these unless it conflicts with existing changes you've made!
|
||||
|
||||
```shell
|
||||
make update
|
||||
```
|
||||
|
||||
Or, if you don't want the interactive parts and just want to force update your local garden (this assumed that you are okay with some of your personalizations been overriden!)
|
||||
|
||||
```shell
|
||||
make update-force
|
||||
```
|
||||
|
||||
Or, manually checkout the changes yourself.
|
||||
|
||||
> [!warning] Warning!
|
||||
>
|
||||
> If you customized the files in `data/`, or anything inside `layouts/`, your customization may be overwritten!
|
||||
> Make sure you have a copy of these changes if you don't want to lose them.
|
||||
|
||||
|
||||
```shell
|
||||
# add Quartz as a remote host
|
||||
git remote add upstream git@github.com:jackyzha0/quartz.git
|
||||
|
||||
# index and fetch changes
|
||||
git fetch upstream
|
||||
git checkout -p upstream/hugo -- layouts .github Makefile assets/js assets/styles/base.scss assets/styles/darkmode.scss config.toml data
|
||||
```
|
||||
5
content/private/private-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Private Stuff"
|
||||
---
|
||||
|
||||
This page doesn't get published!
|
||||
3
content/templates/post.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "{{title}}"
|
||||
---
|
||||
29
data/config.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Jacky Zhao
|
||||
enableToc: true
|
||||
openToc: false
|
||||
enableLinkPreview: true
|
||||
enableLatex: true
|
||||
enableCodeBlockTitle: true
|
||||
enableCodeBlockCopy: true
|
||||
enableCallouts: true
|
||||
enableSPA: true
|
||||
enableFooter: true
|
||||
enableContextualBacklinks: true
|
||||
enableRecentNotes: false
|
||||
enableGitHubEdit: true
|
||||
enableMermaid: true
|
||||
GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content
|
||||
search:
|
||||
enableSemanticSearch: false
|
||||
operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
|
||||
operandIndexId: "REPLACE-WITH-YOUR-OPERAND-INDEX-ID"
|
||||
description:
|
||||
Host your second brain and digital garden for free. Quartz features extremely fast full-text search,
|
||||
Wikilink support, backlinks, local graph, tags, and link previews.
|
||||
page_title:
|
||||
"🪴 Quartz 3.3"
|
||||
links:
|
||||
- link_name: Twitter
|
||||
link: https://twitter.com/_jzhao
|
||||
- link_name: GitHub
|
||||
link: https://github.com/jackyzha0
|
||||
37
data/graphConfig.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# if true, a Global Graph will be shown on home page with full width, no backlink.
|
||||
# A different set of Local Graphs will be shown on sub pages.
|
||||
# if false, Local Graph will be default on every page as usual
|
||||
enableGlobalGraph: false
|
||||
|
||||
### Local Graph ###
|
||||
|
||||
localGraph:
|
||||
enableLegend: false
|
||||
enableDrag: true
|
||||
enableZoom: true
|
||||
depth: 1 # set to -1 to show full graph
|
||||
scale: 1.2
|
||||
repelForce: 2
|
||||
centerForce: 1
|
||||
linkDistance: 1
|
||||
fontSize: 0.6
|
||||
opacityScale: 3
|
||||
|
||||
### Global Graph ###
|
||||
|
||||
globalGraph:
|
||||
enableLegend: false
|
||||
enableDrag: true
|
||||
enableZoom: true
|
||||
depth: -1 # set to -1 to show full graph
|
||||
scale: 1.4
|
||||
repelForce: 1
|
||||
centerForce: 1
|
||||
linkDistance: 1
|
||||
fontSize: 0.5
|
||||
opacityScale: 3
|
||||
|
||||
### For all graphs ###
|
||||
|
||||
paths:
|
||||
- /moc: "#4388cc"
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
Quartz is a static site generator. How does it work?
|
||||
|
||||
This question is best answered by tracing what happens when a user (you!) runs `npx quartz build` in the command line:
|
||||
|
||||
## On the server
|
||||
|
||||
1. After running `npx quartz build`, npm will look at `package.json` to find the `bin` entry for `quartz` which points at `./quartz/bootstrap-cli.mjs`.
|
||||
2. This file has a [shebang](<https://en.wikipedia.org/wiki/Shebang_(Unix)>) line at the top which tells npm to execute it using Node.
|
||||
3. `bootstrap-cli.mjs` is responsible for a few things:
|
||||
1. Parsing the command-line arguments using [yargs](http://yargs.js.org/).
|
||||
2. Transpiling and bundling the rest of Quartz (which is in Typescript) to regular JavaScript using [esbuild](https://esbuild.github.io/). The `esbuild` configuration here is slightly special as it also handles `.scss` file imports using [esbuild-sass-plugin v2](https://www.npmjs.com/package/esbuild-sass-plugin). Additionally, we bundle 'inline' client-side scripts (any `.inline.ts` file) that components declare using a custom `esbuild` plugin that runs another instance of `esbuild` which bundles for the browser instead of `node`. Modules of both types are imported as plain text.
|
||||
3. Running the local preview server if `--serve` is set. This starts two servers:
|
||||
1. A WebSocket server on port 3001 to handle hot-reload signals. This tracks all inbound connections and sends a 'rebuild' message a server-side change is detected (either content or configuration).
|
||||
2. An HTTP file-server on a user defined port (normally 8080) to serve the actual website files.
|
||||
4. If the `--serve` flag is set, it also starts a file watcher to detect source-code changes (e.g. anything that is `.ts`, `.tsx`, `.scss`, or packager files). On a change, we rebuild the module (step 2 above) using esbuild's [rebuild API](https://esbuild.github.io/api/#rebuild) which drastically reduces the build times.
|
||||
5. After transpiling the main Quartz build module (`quartz/build.ts`), we write it to a cache file `.quartz-cache/transpiled-build.mjs` and then dynamically import this using `await import(cacheFile)`. However, we need to be pretty smart about how to bust Node's [import cache](https://github.com/nodejs/modules/issues/307) so we add a random query string to fake Node into thinking it's a new module. This does, however, cause memory leaks so we just hope that the user doesn't hot-reload their configuration too many times in a single session :)) (it leaks about ~350kB memory on each reload). After importing the module, we then invoke it, passing in the command line arguments we parsed earlier along with a callback function to signal the client to refresh.
|
||||
4. In `build.ts`, we start by installing source map support manually to account for the query string cache busting hack we introduced earlier. Then, we start processing content:
|
||||
1. Clean the output directory.
|
||||
2. Recursively glob all files in the `content` folder, respecting the `.gitignore`.
|
||||
3. Parse the Markdown files.
|
||||
1. Quartz detects the number of threads available and chooses to spawn worker threads if there are >128 pieces of content to parse (rough heuristic). If it needs to spawn workers, it will invoke esbuild again to transpile the worker script `quartz/worker.ts`. Then, a work-stealing [workerpool](https://www.npmjs.com/package/workerpool) is then created and batches of 128 files are assigned to workers.
|
||||
2. Each worker (or just the main thread if there is no concurrency) creates a [unified](https://github.com/unifiedjs/unified) parser based off of the plugins defined in the [[configuration]].
|
||||
3. Parsing has three steps:
|
||||
1. Read the file into a [vfile](https://github.com/vfile/vfile).
|
||||
2. Applied plugin-defined text transformations over the content.
|
||||
3. Slugify the file path and store it in the data for the file. See the page on [[paths]] for more details about how path logic works in Quartz (spoiler: its complicated).
|
||||
4. Markdown parsing using [remark-parse](https://www.npmjs.com/package/remark-parse) (text to [mdast](https://github.com/syntax-tree/mdast)).
|
||||
5. Apply plugin-defined Markdown-to-Markdown transformations.
|
||||
6. Convert Markdown into HTML using [remark-rehype](https://github.com/remarkjs/remark-rehype) ([mdast](https://github.com/syntax-tree/mdast) to [hast](https://github.com/syntax-tree/hast)).
|
||||
7. Apply plugin-defined HTML-to-HTML transformations.
|
||||
4. Filter out unwanted content using plugins.
|
||||
5. Emit files using plugins.
|
||||
1. Gather all the static resources (e.g. external CSS, JS modules, etc.) each emitter plugin declares.
|
||||
2. Emitters that emit HTML files do a bit of extra work here as they need to transform the [hast](https://github.com/syntax-tree/hast) produced in the parse step to JSX. This is done using [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) with the [Preact](https://preactjs.com/) runtime. Finally, the JSX is rendered to HTML using [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) which statically renders the JSX to HTML (i.e. doesn't care about `useState`, `useEffect`, or any other React/Preact interactive bits). Here, we also do a bunch of fun stuff like assemble the page [[layout]] from `quartz.layout.ts`, assemble all the inline scripts that actually get shipped to the client, and all the transpiled styles. The bulk of this logic can be found in `quartz/components/renderPage.tsx`. Other fun things of note:
|
||||
1. CSS is minified and transformed using [Lightning CSS](https://github.com/parcel-bundler/lightningcss) to add vendor prefixes and do syntax lowering.
|
||||
2. Scripts are split into `beforeDOMLoaded` and `afterDOMLoaded` and are inserted in the `<head>` and `<body>` respectively.
|
||||
3. Finally, each emitter plugin is responsible for emitting and writing it's own emitted files to disk.
|
||||
6. If the `--serve` flag was detected, we also set up another file watcher to detect content changes (only `.md` files). We keep a content map that tracks the parsed AST and plugin data for each slug and update this on file changes. Newly added or modified paths are rebuilt and added to the content map. Then, all the filters and emitters are run over the resulting content map. This file watcher is debounced with a threshold of 250ms. On success, we send a client refresh signal using the passed in callback function.
|
||||
|
||||
## On the client
|
||||
|
||||
1. The browser opens a Quartz page and loads the HTML. The `<head>` also links to page styles (emitted to `public/index.css`) and page-critical JS (emitted to `public/prescript.js`)
|
||||
2. Then, once the body is loaded, the browser loads the non-critical JS (emitted to `public/postscript.js`)
|
||||
3. Once the page is done loading, the page will then dispatch a custom synthetic browser event `"nav"`. This is used so client-side scripts declared by components can 'setup' anything that requires access to the page DOM.
|
||||
1. If the [[SPA Routing|enableSPA option]] is enabled in the [[configuration]], this `"nav"` event is also fired on any client-navigation to allow for components to unregister and reregister any event handlers and state.
|
||||
2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts.
|
||||
|
||||
The architecture and design of the plugin system was intentionally left pretty vague here as this is described in much more depth in the guide on [[making plugins|making your own plugin]].
|
||||
@@ -1,233 +0,0 @@
|
||||
---
|
||||
title: Creating your own Quartz components
|
||||
---
|
||||
|
||||
> [!warning]
|
||||
> This guide assumes you have experience writing JavaScript and are familiar with TypeScript.
|
||||
|
||||
Normally on the web, we write layout code using HTML which looks something like the following:
|
||||
|
||||
```html
|
||||
<article>
|
||||
<h1>An article header</h1>
|
||||
<p>Some content</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
This piece of HTML represents an article with a leading header that says "An article header" and a paragraph that contains the text "Some content". This is combined with CSS to style the page and JavaScript to add interactivity.
|
||||
|
||||
However, HTML doesn't let you create reusable templates. If you wanted to create a new page, you would need to copy and paste the above snippet and edit the header and content yourself. This isn't great if we have a lot of content on our site that shares a lot of similar layout. The smart people who created React also had similar complaints and invented the concept of Components -- JavaScript functions that return JSX -- to solve the code duplication problem.
|
||||
|
||||
In effect, components allow you to write a JavaScript function that takes some data and produces HTML as an output. **While Quartz doesn't use React, it uses the same component concept to allow you to easily express layout templates in your Quartz site.**
|
||||
|
||||
## An Example Component
|
||||
|
||||
### Constructor
|
||||
|
||||
Component files are written in `.tsx` files that live in the `quartz/components` folder. These are re-exported in `quartz/components/index.ts` so you can use them in layouts and other components more easily.
|
||||
|
||||
Each component file should have a default export that satisfies the `QuartzComponentConstructor` function signature. It's a function that takes in a single optional parameter `opts` and returns a Quartz Component. The type of the parameters `opts` is defined by the interface `Options` which you as the component creator also decide.
|
||||
|
||||
In your component, you can use the values from the configuration option to change the rendering behaviour inside of your component. For example, the component in the code snippet below will not render if the `favouriteNumber` option is below 0.
|
||||
|
||||
```tsx {11-17}
|
||||
interface Options {
|
||||
favouriteNumber: number
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
favouriteNumber: 42,
|
||||
}
|
||||
|
||||
export default ((userOpts?: Options) => {
|
||||
const opts = { ...userOpts, ...defaultOpts }
|
||||
function YourComponent(props: QuartzComponentProps) {
|
||||
if (opts.favouriteNumber < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <p>My favourite number is {opts.favouriteNumber}</p>
|
||||
}
|
||||
|
||||
return YourComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
The Quartz component itself (lines 11-17 highlighted above) looks like a React component. It takes in properties (sometimes called [props](https://react.dev/learn/passing-props-to-a-component)) and returns JSX.
|
||||
|
||||
All Quartz components accept the same set of props:
|
||||
|
||||
```tsx title="quartz/components/types.ts"
|
||||
// simplified for sake of demonstration
|
||||
export type QuartzComponentProps = {
|
||||
fileData: QuartzPluginData
|
||||
cfg: GlobalConfiguration
|
||||
tree: Node<QuartzPluginData>
|
||||
allFiles: QuartzPluginData[]
|
||||
displayClass?: "mobile-only" | "desktop-only"
|
||||
}
|
||||
```
|
||||
|
||||
- `fileData`: Any metadata [[making plugins|plugins]] may have added to the current page.
|
||||
- `fileData.slug`: slug of the current page.
|
||||
- `fileData.frontmatter`: any frontmatter parsed.
|
||||
- `cfg`: The `configuration` field in `quartz.config.ts`.
|
||||
- `tree`: the resulting [HTML AST](https://github.com/syntax-tree/hast) after processing and transforming the file. This is useful if you'd like to render the content using [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) (you can find an example of this in `quartz/components/pages/Content.tsx`).
|
||||
- `allFiles`: Metadata for all files that have been parsed. Useful for doing page listings or figuring out the overall site structure.
|
||||
- `displayClass`: a utility class that indicates a preference from the user about how to render it in a mobile or desktop setting. Helpful if you want to conditionally hide a component on mobile or desktop.
|
||||
|
||||
### Styling
|
||||
|
||||
Quartz components can also define a `.css` property on the actual function component which will get picked up by Quartz. This is expected to be a CSS string which can either be inlined or imported from a `.scss` file.
|
||||
|
||||
Note that inlined styles **must** be plain vanilla CSS:
|
||||
|
||||
```tsx {6-10} title="quartz/components/YourComponent.tsx"
|
||||
export default (() => {
|
||||
function YourComponent() {
|
||||
return <p class="red-text">Example Component</p>
|
||||
}
|
||||
|
||||
YourComponent.css = `
|
||||
p.red-text {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
return YourComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
Imported styles, however, can be from SCSS files:
|
||||
|
||||
```tsx {1-2,9} title="quartz/components/YourComponent.tsx"
|
||||
// assuming your stylesheet is in quartz/components/styles/YourComponent.scss
|
||||
import styles from "./styles/YourComponent.scss"
|
||||
|
||||
export default (() => {
|
||||
function YourComponent() {
|
||||
return <p>Example Component</p>
|
||||
}
|
||||
|
||||
YourComponent.css = styles
|
||||
return YourComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> Quartz does not use CSS modules so any styles you declare here apply _globally_. If you only want it to apply to your component, make sure you use specific class names and selectors.
|
||||
|
||||
### Scripts and Interactivity
|
||||
|
||||
What about interactivity? Suppose you want to add an-click handler for example. Like the `.css` property on the component, you can also declare `.beforeDOMLoaded` and `.afterDOMLoaded` properties that are strings that contain the script.
|
||||
|
||||
```tsx title="quartz/components/YourComponent.tsx"
|
||||
export default (() => {
|
||||
function YourComponent() {
|
||||
return <button id="btn">Click me</button>
|
||||
}
|
||||
|
||||
YourComponent.beforeDOM = `
|
||||
console.log("hello from before the page loads!")
|
||||
`
|
||||
|
||||
YourComponent.afterDOM = `
|
||||
document.getElementById('btn').onclick = () => {
|
||||
alert('button clicked!')
|
||||
}
|
||||
`
|
||||
return YourComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
> [!hint]
|
||||
> For those coming from React, Quartz components are different from React components in that it only uses JSX for templating and layout. Hooks like `useEffect`, `useState`, etc. are not rendered and other properties that accept functions like `onClick` handlers will not work. Instead, do it using a regular JS script that modifies the DOM element directly.
|
||||
|
||||
As the names suggest, the `.beforeDOMLoaded` scripts are executed _before_ the page is done loading so it doesn't have access to any elements on the page. This is mostly used to prefetch any critical data.
|
||||
|
||||
The `.afterDOMLoaded` script executes once the page has been completely loaded. This is a good place to setup anything that should last for the duration of a site visit (e.g. getting something saved from local storage).
|
||||
|
||||
If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you can listen for the `"nav"` event that gets fired whenever a page loads (which may happen on navigation if [[SPA Routing]] is enabled).
|
||||
|
||||
```ts
|
||||
document.addEventListener("nav", () => {
|
||||
// do page specific logic here
|
||||
// e.g. attach event listeners
|
||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||
toggleSwitch.removeEventListener("change", switchTheme)
|
||||
toggleSwitch.addEventListener("change", switchTheme)
|
||||
})
|
||||
```
|
||||
|
||||
It is best practice to also unmount any existing event handlers to prevent memory leaks.
|
||||
|
||||
#### Importing Code
|
||||
|
||||
Of course, it isn't always practical (nor desired!) to write your code as a string literal in the component.
|
||||
|
||||
Quartz supports importing component code through `.inline.ts` files.
|
||||
|
||||
```tsx title="quartz/components/YourComponent.tsx"
|
||||
// @ts-ignore: typescript doesn't know about our inline bundling system
|
||||
// so we need to silence the error
|
||||
import script from "./scripts/graph.inline"
|
||||
|
||||
export default (() => {
|
||||
function YourComponent() {
|
||||
return <button id="btn">Click me</button>
|
||||
}
|
||||
|
||||
YourComponent.afterDOM = script
|
||||
return YourComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
```ts title="quartz/components/scripts/graph.inline.ts"
|
||||
// any imports here are bundled for the browser
|
||||
import * as d3 from "d3"
|
||||
|
||||
document.getElementById("btn").onclick = () => {
|
||||
alert("button clicked!")
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, like what is shown in the example above, you can import packages in `.inline.ts` files. This will be bundled by Quartz and included in the actual script.
|
||||
|
||||
### Using a Component
|
||||
|
||||
After creating your custom component, re-export it in `quartz/components/index.ts`:
|
||||
|
||||
```ts title="quartz/components/index.ts" {4,10}
|
||||
import ArticleTitle from "./ArticleTitle"
|
||||
import Content from "./pages/Content"
|
||||
import Darkmode from "./Darkmode"
|
||||
import YourComponent from "./YourComponent"
|
||||
|
||||
export { ArticleTitle, Content, Darkmode, YourComponent }
|
||||
```
|
||||
|
||||
Then, you can use it like any other component in `quartz.layout.ts` via `Component.YourComponent()`. See the [[configuration#Layout|layout]] section for more details.
|
||||
|
||||
As Quartz components are just functions that return React components, you can compositionally use them in other Quartz components.
|
||||
|
||||
```tsx title="quartz/components/AnotherComponent.tsx"
|
||||
import YourComponent from "./YourComponent"
|
||||
|
||||
export default (() => {
|
||||
function AnotherComponent(props: QuartzComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<p>It's nested!</p>
|
||||
<YourComponent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return AnotherComponent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
> [!hint]
|
||||
> Look in `quartz/components` for more examples of components in Quartz as reference for your own components!
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: "Advanced"
|
||||
---
|
||||
@@ -1,302 +0,0 @@
|
||||
---
|
||||
title: Making your own plugins
|
||||
---
|
||||
|
||||
> [!warning]
|
||||
> This part of the documentation will assume you have working knowledge in TypeScript and will include code snippets that describe the interface of what Quartz plugins should look like.
|
||||
|
||||
Quartz's plugins are a series of transformations over content. This is illustrated in the diagram of the processing pipeline below:
|
||||
|
||||
![[quartz transform pipeline.png]]
|
||||
|
||||
All plugins are defined as a function that takes in a single parameter for options `type OptionType = object | undefined` and return an object that corresponds to the type of plugin it is.
|
||||
|
||||
```ts
|
||||
type OptionType = object | undefined
|
||||
type QuartzPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzPluginInstance
|
||||
type QuartzPluginInstance =
|
||||
| QuartzTransformerPluginInstance
|
||||
| QuartzFilterPluginInstance
|
||||
| QuartzEmitterPluginInstance
|
||||
```
|
||||
|
||||
The following sections will go into detail for what methods can be implemented for each plugin type. Before we do that, let's clarify a few more ambiguous types:
|
||||
|
||||
- `BuildCtx` is defined in `quartz/ctx.ts`. It consists of
|
||||
- `argv`: The command line arguments passed to the Quartz [[build]] command
|
||||
- `cfg`: The full Quartz [[configuration]]
|
||||
- `allSlugs`: a list of all the valid content slugs (see [[paths]] for more information on what a `ServerSlug` is)
|
||||
- `StaticResources` is defined in `quartz/resources.tsx`. It consists of
|
||||
- `css`: a list of URLs for stylesheets that should be loaded
|
||||
- `js`: a list of scripts that should be loaded. A script is described with the `JSResource` type which is also defined in `quartz/resources.tsx`. It allows you to define a load time (either before or after the DOM has been loaded), whether it should be a module, and either the source URL or the inline content of the script.
|
||||
|
||||
## Transformers
|
||||
|
||||
Transformers **map** over content, taking a Markdown file and outputting modified content or adding metadata to the file itself.
|
||||
|
||||
```ts
|
||||
export type QuartzTransformerPluginInstance = {
|
||||
name: string
|
||||
textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
|
||||
markdownPlugins?: (ctx: BuildCtx) => PluggableList
|
||||
htmlPlugins?: (ctx: BuildCtx) => PluggableList
|
||||
externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
|
||||
}
|
||||
```
|
||||
|
||||
All transformer plugins must define at least a `name` field to register the plugin and a few optional functions that allow you to hook into various parts of transforming a single Markdown file.
|
||||
|
||||
- `textTransform` performs a text-to-text transformation _before_ a file is parsed into the [Markdown AST](https://github.com/syntax-tree/mdast).
|
||||
- `markdownPlugins` defines a list of [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md). `remark` is a tool that transforms Markdown to Markdown in a structured way.
|
||||
- `htmlPlugins` defines a list of [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md). Similar to how `remark` works, `rehype` is a tool that transforms HTML to HTML in a structured way.
|
||||
- `externalResources` defines any external resources the plugin may need to load on the client-side for it to work properly.
|
||||
|
||||
Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library).
|
||||
|
||||
A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[Latex]] plugin:
|
||||
|
||||
```ts title="quartz/plugins/transformers/latex.ts"
|
||||
import remarkMath from "remark-math"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import rehypeMathjax from "rehype-mathjax/svg.js"
|
||||
import { QuartzTransformerPlugin } from "../types"
|
||||
|
||||
interface Options {
|
||||
renderEngine: "katex" | "mathjax"
|
||||
}
|
||||
|
||||
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
||||
const engine = opts?.renderEngine ?? "katex"
|
||||
return {
|
||||
name: "Latex",
|
||||
markdownPlugins() {
|
||||
return [remarkMath]
|
||||
},
|
||||
htmlPlugins() {
|
||||
if (engine === "katex") {
|
||||
// if you need to pass options into a plugin, you
|
||||
// can use a tuple of [plugin, options]
|
||||
return [[rehypeKatex, { output: "html" }]]
|
||||
} else {
|
||||
return [rehypeMathjax]
|
||||
}
|
||||
},
|
||||
externalResources() {
|
||||
if (engine === "katex") {
|
||||
return {
|
||||
css: ["https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"],
|
||||
js: [
|
||||
{
|
||||
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
|
||||
loadTime: "afterDOMReady",
|
||||
contentType: "external",
|
||||
},
|
||||
],
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Another common thing that transformer plugins will do is parse a file and add extra data for that file:
|
||||
|
||||
```ts
|
||||
export const AddWordCount: QuartzTransformerPlugin = () => {
|
||||
return {
|
||||
name: "AddWordCount",
|
||||
markdownPlugins() {
|
||||
return [
|
||||
() => {
|
||||
return (tree, file) => {
|
||||
// tree is an `mdast` root element
|
||||
// file is a `vfile`
|
||||
const text = file.value
|
||||
const words = text.split(" ").length
|
||||
file.data.wordcount = words
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// tell typescript about our custom data fields we are adding
|
||||
// other plugins will then also be aware of this data field
|
||||
declare module "vfile" {
|
||||
interface DataMap {
|
||||
wordcount: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, you can also perform transformations over Markdown or HTML ASTs using the `visit` function from the `unist-util-visit` package or the `findAndReplace` function from the `mdast-util-find-and-replace` package.
|
||||
|
||||
```ts
|
||||
export const TextTransforms: QuartzTransformerPlugin = () => {
|
||||
return {
|
||||
name: "TextTransforms",
|
||||
markdownPlugins() {
|
||||
return [() => {
|
||||
return (tree, file) => {
|
||||
// replace _text_ with the italics version
|
||||
findAndReplace(tree, /_(.+)_/, (_value: string, ...capture: string[]) => {
|
||||
// inner is the text inside of the () of the regex
|
||||
const [inner] = capture
|
||||
// return an mdast node
|
||||
// https://github.com/syntax-tree/mdast
|
||||
return {
|
||||
type: "emphasis",
|
||||
children: [{ type: 'text', value: inner }]
|
||||
}
|
||||
})
|
||||
|
||||
// remove all links (replace with just the link content)
|
||||
// match by 'type' field on an mdast node
|
||||
// https://github.com/syntax-tree/mdast#link in this example
|
||||
visit(tree, "link", (link: Link) => {
|
||||
return {
|
||||
type: "paragraph"
|
||||
children: [{ type: 'text', value: link.title }]
|
||||
}
|
||||
})
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All transformer plugins can be found under `quartz/plugins/transformers`. If you decide to write your own transformer plugin, don't forget to re-export it under `quartz/plugins/transformers/index.ts`
|
||||
|
||||
A parting word: transformer plugins are quite complex so don't worry if you don't get them right away. Take a look at the built in transformers and see how they operate over content to get a better sense for how to accomplish what you are trying to do.
|
||||
|
||||
## Filters
|
||||
|
||||
Filters **filter** content, taking the output of all the transformers and determining what files to actually keep and what to discard.
|
||||
|
||||
```ts
|
||||
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
|
||||
opts?: Options,
|
||||
) => QuartzFilterPluginInstance
|
||||
|
||||
export type QuartzFilterPluginInstance = {
|
||||
name: string
|
||||
shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
|
||||
}
|
||||
```
|
||||
|
||||
A filter plugin must define a `name` field and a `shouldPublish` function that takes in a piece of content that has been processed by all the transformers and returns a `true` or `false` depending on whether it should be passed to the emitter plugins or not.
|
||||
|
||||
For example, here is the built-in plugin for removing drafts:
|
||||
|
||||
```ts title="quartz/plugins/filters/draft.ts"
|
||||
import { QuartzFilterPlugin } from "../types"
|
||||
|
||||
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
|
||||
name: "RemoveDrafts",
|
||||
shouldPublish(_ctx, [_tree, vfile]) {
|
||||
// uses frontmatter parsed from transformers
|
||||
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
||||
return !draftFlag
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Emitters
|
||||
|
||||
Emitters **reduce** over content, taking in a list of all the transformed and filtered content and creating output files.
|
||||
|
||||
```ts
|
||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
||||
opts?: Options,
|
||||
) => QuartzEmitterPluginInstance
|
||||
|
||||
export type QuartzEmitterPluginInstance = {
|
||||
name: string
|
||||
emit(
|
||||
ctx: BuildCtx,
|
||||
content: ProcessedContent[],
|
||||
resources: StaticResources,
|
||||
emitCallback: EmitCallback,
|
||||
): Promise<FilePath[]>
|
||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||
}
|
||||
```
|
||||
|
||||
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
||||
|
||||
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
|
||||
|
||||
```ts
|
||||
export type EmitCallback = (data: {
|
||||
// the name of the file to emit (not including the file extension)
|
||||
slug: ServerSlug
|
||||
// the file extension
|
||||
ext: `.${string}` | ""
|
||||
// the file content to add
|
||||
content: string
|
||||
}) => Promise<FilePath>
|
||||
```
|
||||
|
||||
This is a thin wrapper around writing to the appropriate output folder and ensuring that intermediate directories exist. If you choose to use the native Node `fs` APIs, ensure you emit to the `argv.output` folder as well.
|
||||
|
||||
If you are creating an emitter plugin that needs to render components, there are three more things to be aware of:
|
||||
|
||||
- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information.
|
||||
- You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML.
|
||||
- If you need to render an HTML AST to JSX, you can use the `htmlToJsx` function from `quartz/util/jsx.ts`. An example of this can be found in `quartz/components/pages/Content.tsx`.
|
||||
|
||||
For example, the following is a simplified version of the content page plugin that renders every single page.
|
||||
|
||||
```tsx title="quartz/plugins/emitters/contentPage.tsx"
|
||||
export const ContentPage: QuartzEmitterPlugin = () => {
|
||||
// construct the layout
|
||||
const layout: FullPageLayout = {
|
||||
...sharedPageComponents,
|
||||
...defaultContentPageLayout,
|
||||
pageBody: Content(),
|
||||
}
|
||||
const { head, header, beforeBody, pageBody, left, right, footer } = layout
|
||||
return {
|
||||
name: "ContentPage",
|
||||
getQuartzComponents() {
|
||||
return [head, ...header, ...beforeBody, pageBody, ...left, ...right, footer]
|
||||
},
|
||||
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
||||
const cfg = ctx.cfg.configuration
|
||||
const fps: FilePath[] = []
|
||||
const allFiles = content.map((c) => c[1].data)
|
||||
for (const [tree, file] of content) {
|
||||
const slug = canonicalizeServer(file.data.slug!)
|
||||
const externalResources = pageResources(slug, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
fileData: file.data,
|
||||
externalResources,
|
||||
cfg,
|
||||
children: [],
|
||||
tree,
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(slug, componentData, opts, externalResources)
|
||||
const fp = await emit({
|
||||
content,
|
||||
slug: file.data.slug!,
|
||||
ext: ".html",
|
||||
})
|
||||
|
||||
fps.push(fp)
|
||||
}
|
||||
return fps
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that it takes in a `FullPageLayout` as the options. It's made by combining a `SharedLayout` and a `PageLayout` both of which are provided through the `quartz.layout.ts` file.
|
||||
|
||||
> [!hint]
|
||||
> Look in `quartz/plugins` for more examples of plugins in Quartz as reference for your own plugins!
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
title: Paths in Quartz
|
||||
---
|
||||
|
||||
Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places.
|
||||
|
||||
A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path.
|
||||
|
||||
It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it.
|
||||
|
||||
Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.org/play#example/nominal-typing).
|
||||
|
||||
```typescript
|
||||
// instead of
|
||||
type FullSlug = string
|
||||
|
||||
// we do
|
||||
type FullSlug = string & { __brand: "full" }
|
||||
|
||||
// that way, the following will fail typechecking
|
||||
const slug: FullSlug = "some random string"
|
||||
```
|
||||
|
||||
While this prevents most typing mistakes _within_ our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from _accidentally_ mistaking a string for a client slug when we forcibly cast it.
|
||||
|
||||
Thus, we still need to be careful when casting from a string to one of these nominal types in the 'entrypoints', illustrated with hexagon shapes in the diagram below.
|
||||
|
||||
The following diagram draws the relationships between all the path sources, nominal path types, and what functions in `quartz/path.ts` convert between them.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Browser{{Browser}} --> Window{{Body}} & LinkElement{{Link Element}}
|
||||
Window --"getFullSlug()"--> FullSlug[Full Slug]
|
||||
LinkElement --".href"--> Relative[Relative URL]
|
||||
FullSlug --"simplifySlug()" --> SimpleSlug[Simple Slug]
|
||||
SimpleSlug --"pathToRoot()"--> Relative
|
||||
SimpleSlug --"resolveRelative()" --> Relative
|
||||
MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links]
|
||||
Links --"transformLink()"--> Relative
|
||||
FilePath --"slugifyFilePath()"--> FullSlug[Full Slug]
|
||||
style FullSlug stroke-width:4px
|
||||
```
|
||||
|
||||
Here are the main types of slugs with a rough description of each type of path:
|
||||
|
||||
- `FilePath`: a real file path to a file on disk. Cannot be relative and must have a file extension.
|
||||
- `FullSlug`: cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug.
|
||||
- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path.
|
||||
- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash.
|
||||
|
||||
To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/path.test.ts`.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: Authoring Content
|
||||
---
|
||||
|
||||
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initailized. Any Markdown in this folder will get processed by Quartz.
|
||||
|
||||
It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments.
|
||||
|
||||
Got everything setup? Let's [[build]] and preview your Quartz locally!
|
||||
|
||||
## Syntax
|
||||
|
||||
As Quartz uses Markdown files as the main way of writing content, it fully supports Markdown syntax. By default, Quartz also ships with a few syntax extensions like [Github Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) (footnotes, strikethrough, tables, tasklists) and [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown) ([[callouts]], [[wikilinks]]).
|
||||
|
||||
Additionally, Quartz also allows you to specify additional metadata in your notes called **frontmatter**.
|
||||
|
||||
```md title="content/note.md"
|
||||
---
|
||||
title: Example Title
|
||||
draft: false
|
||||
tags:
|
||||
- example-tag
|
||||
---
|
||||
|
||||
The rest of your content lives here. You can use **Markdown** here :)
|
||||
```
|
||||
|
||||
Some common frontmatter fields that are natively supported by Quartz:
|
||||
|
||||
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
||||
- `aliases`: Other names for this note. This is a list of strings.
|
||||
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
||||
- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format.
|
||||
|
||||
## Syncing your Content
|
||||
|
||||
When your Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`.
|
||||
|
||||
> [!hint] Flags and options
|
||||
> For full help options, you can run `npx quartz sync --help`.
|
||||
>
|
||||
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
||||
>
|
||||
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
||||
> - `-v` or `--verbose`: print out extra logging information
|
||||
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
||||
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
||||
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: "Building your Quartz"
|
||||
---
|
||||
|
||||
Once you've [[index#🪴 Get Started|initialized]] Quartz, let's see what it looks like locally:
|
||||
|
||||
```bash
|
||||
npx quartz build --serve
|
||||
```
|
||||
|
||||
This will start a local web server to run your Quartz on your computer. Open a web browser and visit `http://localhost:8080/` to view it.
|
||||
|
||||
> [!hint] Flags and options
|
||||
> For full help options, you can run `npx quartz build --help`.
|
||||
>
|
||||
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
||||
>
|
||||
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
||||
> - `-v` or `--verbose`: print out extra logging information
|
||||
> - `-o` or `--output`: the output folder. This is normally just `public`
|
||||
> - `--serve`: run a local hot-reloading server to preview your Quartz
|
||||
> - `--port`: what port to run the local preview server on
|
||||
> - `--concurrency`: how many threads to use to parse notes
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
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` or changing [[layout|the layout]] in `quartz.layout.ts`.
|
||||
|
||||
> [!tip]
|
||||
> If you edit Quartz configuration 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, helping you avoid configuration mistakes!
|
||||
|
||||
The configuration of Quartz can be broken down into two main parts:
|
||||
|
||||
```ts title="quartz.config.ts"
|
||||
const config: QuartzConfig = {
|
||||
configuration: { ... },
|
||||
plugins: { ... },
|
||||
}
|
||||
```
|
||||
|
||||
## General Configuration
|
||||
|
||||
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:
|
||||
|
||||
- `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site.
|
||||
- `enableSPA`: whether to enable [[SPA Routing]] on your site.
|
||||
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
||||
- `analytics`: what to use for analytics on your site. Values can be
|
||||
- `null`: don't use analytics;
|
||||
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
|
||||
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
|
||||
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
||||
- 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.
|
||||
- `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. See [[private pages]] for more details.
|
||||
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
|
||||
- `theme`: configure how the site looks.
|
||||
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
|
||||
- `header`: Font to use for headers
|
||||
- `code`: Font for inline and block quotes.
|
||||
- `body`: Font for everything
|
||||
- `colors`: controls the theming of the site.
|
||||
- `light`: page background
|
||||
- `lightgray`: borders
|
||||
- `gray`: graph links, heavier borders
|
||||
- `darkgray`: body text
|
||||
- `dark`: header text and icons
|
||||
- `secondary`: link colour, current [[graph view|graph]] node
|
||||
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
||||
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
||||
|
||||
## Plugins
|
||||
|
||||
You can think of Quartz plugins as a series of transformations over content.
|
||||
|
||||
![[quartz transform pipeline.png]]
|
||||
|
||||
```ts
|
||||
plugins: {
|
||||
transformers: [...],
|
||||
filters: [...],
|
||||
emitters: [...],
|
||||
}
|
||||
```
|
||||
|
||||
- [[making plugins#Transformers|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description)
|
||||
- [[making plugins#Filters|Filters]] **filter** content (e.g. filtering out drafts)
|
||||
- [[making plugins#Emitters|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag)
|
||||
|
||||
By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz.
|
||||
|
||||
> [!note]
|
||||
> 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.
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
transformers: [
|
||||
Plugin.FrontMatter(), // uses default options
|
||||
Plugin.Latex({ renderEngine: "katex" }), // specify some options
|
||||
]
|
||||
```
|
||||
|
||||
If you'd like to make your own plugins, read the guide on [[making plugins]] for more information.
|
||||
@@ -1,7 +0,0 @@
|
||||
Quartz comes shipped with a Docker image that will allow you to preview your Quartz locally without installing Node.
|
||||
|
||||
You can run the below one-liner to run Quartz in Docker.
|
||||
|
||||
```sh
|
||||
docker run --rm -itp 8080:8080 $(docker build -q .)
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Block Math
|
||||
|
||||
Block math can be rendered by delimiting math expression with `$$`.
|
||||
|
||||
```
|
||||
$$
|
||||
f(x) = \int_{-\infty}^\infty
|
||||
f\hat(\xi),e^{2 \pi i \xi x}
|
||||
\,d\xi
|
||||
$$
|
||||
```
|
||||
|
||||
$$
|
||||
f(x) = \int_{-\infty}^\infty
|
||||
f\hat(\xi),e^{2 \pi i \xi x}
|
||||
\,d\xi
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
a &= b + c \\ &= e + f \\
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 2 & 3 \\
|
||||
a & b & c
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
### Inline Math
|
||||
|
||||
Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$
|
||||
|
||||
### Escaping symbols
|
||||
|
||||
There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex.
|
||||
|
||||
To get around this, you can escape the dollar sign by doing `\$` instead.
|
||||
|
||||
For example:
|
||||
|
||||
- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2
|
||||
- Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2
|
||||
|
||||
## MathJax
|
||||
|
||||
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' })`
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`.
|
||||
- Plugin: `quartz/plugins/transformers/latex.ts`
|
||||
@@ -1,28 +0,0 @@
|
||||
Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
||||
|
||||
By default, Quartz will render Mermaid diagrams to match the site theme.
|
||||
|
||||
> [!warning]
|
||||
> 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()`.
|
||||
|
||||
## Syntax
|
||||
|
||||
To add a Mermaid diagram, create a mermaid code block.
|
||||
|
||||
````
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!
|
||||
```
|
||||
````
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian.
|
||||
|
||||
By default, Quartz ships with `Plugin.ObsidianFlavoredMarkdown` which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]].
|
||||
|
||||
It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the `Plugin.FrontMatter` transformer plugin.
|
||||
|
||||
Finally, Quartz also provides `Plugin.CrawlLinks` which allows you to customize Quartz's link resolution behaviour to match Obsidian.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Frontmatter parsing:
|
||||
- Disabling: remove all instances of `Plugin.FrontMatter()` from `quartz.config.ts`.
|
||||
- Customize default values for frontmatter: edit `quartz/plugins/transformers/frontmatter.ts`
|
||||
- Obsidian Flavored Markdown:
|
||||
- Disabling: remove all instances of `Plugin.ObsidianFlavoredMarkdown()` from `quartz.config.ts`
|
||||
- Customizing features: `Plugin.ObsidianFlavoredMarkdown` has several other options to toggle on and off:
|
||||
- `comments`: whether to enable `%%` style Obsidian comments. Defaults to `true`
|
||||
- `highlight`: whether to enable `==` style highlights. Defaults to `true`
|
||||
- `wikilinks`: whether to enable turning [[wikilinks]] into regular links. Defaults to `true`
|
||||
- `callouts`: whether to enable [[callouts]]. Defaults to `true`
|
||||
- `mermaid`: whether to enable [[Mermaid diagrams]]. Defaults to `true`
|
||||
- `parseTags`: whether to try and parse tags in the content body. Defaults to `true`
|
||||
- `enableInHtmlEmbed`: whether to try and parse Obsidian flavoured markdown in raw HTML. Defaults to `false`
|
||||
- Link resolution behaviour:
|
||||
- Disabling: remove all instances of `Plugin.CrawlLinks()` from `quartz.config.ts`
|
||||
- Changing link resolution preference: set `markdownLinkResolution` to one of `absolute`, `relative` or `shortest`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown.
|
||||
|
||||
Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown.
|
||||
|
||||
```typescript title="quartz.config.ts"
|
||||
plugins: {
|
||||
transformers: [
|
||||
Plugin.FrontMatter({ delims: "+++", language: "toml" }), // if toml frontmatter
|
||||
// ...
|
||||
Plugin.OxHugoFlavouredMarkdown(),
|
||||
Plugin.GitHubFlavoredMarkdown(),
|
||||
// ...
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Quartz by default doesn't understand `org-roam` files as they aren't Markdown. You're responsible for using an external tool like `ox-hugo` to export the `org-roam` files as Markdown content to Quartz and managing the static assets so that they're available in the final output.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Link resolution
|
||||
- `wikilinks`: Whether to replace `{{ relref }}` with Quartz [[wikilinks]]
|
||||
- `removePredefinedAnchor`: Whether to remove [pre-defined anchor set by ox-hugo](https://ox-hugo.scripter.co/doc/anchors/).
|
||||
- Image handling
|
||||
- `replaceFigureWithMdImg`: Whether to replace `<figure/>` with `![]()`
|
||||
- Formatting
|
||||
- `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`)
|
||||
- `replaceOrgLatex`: Whether to replace org-mode formatting for latex fragments with what `Plugin.Latex` supports.
|
||||
|
||||
> [!warning]
|
||||
>
|
||||
> While you can use `Plugin.OxHugoFlavoredMarkdown` and `Plugin.ObsidianFlavoredMarkdown` together, it's not recommended because it might mutate the file in unexpected ways. Use with caution.
|
||||
@@ -1,7 +0,0 @@
|
||||
Quartz creates an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`.
|
||||
- Change number of entries: set the `rssLimit` field of `Plugin.ContentIndex` to be the desired value. It defaults to latest 10 items.
|
||||
- Use rich HTML output in RSS: set `rssFullHtml` field of `Plugin.ContentIndex` to be `true`.
|
||||
@@ -1,7 +0,0 @@
|
||||
Single-page-app style rendering. This prevents flashes of unstyled content and improves the smoothness of Quartz.
|
||||
|
||||
Under the hood, this is done by hijacking page navigations and instead fetching the HTML via a `GET` request and then diffing and selectively replacing parts of the page using [micromorph](https://github.com/natemoo-re/micromorph). This allows us to change the content of the page without fully refreshing the page, reducing the amount of content that the browser needs to load.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Disable SPA Routing: set the `enableSPA` field of the [[configuration]] in `quartz.config.ts` to be `false`.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
title: Backlinks
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`.
|
||||
- Component: `quartz/components/Backlinks.tsx`
|
||||
- Style: `quartz/components/styles/backlinks.scss`
|
||||
- Script: `quartz/components/scripts/search.inline.ts`
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: "Breadcrumbs"
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders.
|
||||
|
||||
By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!).
|
||||
|
||||
## Customization
|
||||
|
||||
Most configuration can be done by passing in options to `Component.Breadcrumbs()`.
|
||||
|
||||
For example, here's what the default configuration looks like:
|
||||
|
||||
```typescript title="quartz.layout.ts"
|
||||
Component.Breadcrumbs({
|
||||
spacerSymbol: "❯", // symbol between crumbs
|
||||
rootName: "Home", // name of first/root element
|
||||
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||
})
|
||||
```
|
||||
|
||||
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
|
||||
|
||||
You can also adjust where the breadcrumbs will be displayed by adjusting the [[layout]] (moving `Component.Breadcrumbs()` up or down)
|
||||
|
||||
Want to customize it even more?
|
||||
|
||||
- Removing breadcrumbs: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`.
|
||||
- Component: `quartz/components/Breadcrumbs.tsx`
|
||||
- Style: `quartz/components/styles/breadcrumbs.scss`
|
||||
- Script: inline at `quartz/components/Breadcrumbs.tsx`
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
title: Callouts
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
Quartz supports the same Admonition-callout syntax as Obsidian.
|
||||
|
||||
This includes
|
||||
|
||||
- 12 Distinct callout types (each with several aliases)
|
||||
- Collapsable callouts
|
||||
|
||||
```
|
||||
> [!info] Title
|
||||
> This is a callout!
|
||||
```
|
||||
|
||||
See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts).
|
||||
|
||||
> [!warning]
|
||||
> 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()`.
|
||||
|
||||
## Customization
|
||||
|
||||
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
|
||||
- Editing icons: `quartz/plugins/transformers/ofm.ts`
|
||||
|
||||
## Showcase
|
||||
|
||||
> [!info]
|
||||
> Default title
|
||||
|
||||
> [!question]+ Can callouts be nested?
|
||||
>
|
||||
> > [!todo]- Yes!, they can. And collapsed!
|
||||
> >
|
||||
> > > [!example] You can even use multiple layers of nesting.
|
||||
|
||||
> [!EXAMPLE] Examples
|
||||
>
|
||||
> Aliases: example
|
||||
|
||||
> [!note] Notes
|
||||
>
|
||||
> Aliases: note
|
||||
|
||||
> [!abstract] Summaries
|
||||
>
|
||||
> Aliases: abstract, summary, tldr
|
||||
|
||||
> [!info] Info
|
||||
>
|
||||
> Aliases: info, todo
|
||||
|
||||
> [!tip] Hint
|
||||
>
|
||||
> Aliases: tip, hint, important
|
||||
|
||||
> [!success] Success
|
||||
>
|
||||
> Aliases: success, check, done
|
||||
|
||||
> [!question] Question
|
||||
>
|
||||
> Aliases: question, help, faq
|
||||
|
||||
> [!warning] Warning
|
||||
>
|
||||
> Aliases: warning, caution, attention
|
||||
|
||||
> [!failure] Failure
|
||||
>
|
||||
> Aliases: failure, fail, missing
|
||||
|
||||
> [!danger] Error
|
||||
>
|
||||
> Aliases: danger, error
|
||||
|
||||
> [!bug] Bug
|
||||
>
|
||||
> Aliases: bug
|
||||
|
||||
> [!quote] Quote
|
||||
>
|
||||
> Aliases: quote, cite
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
title: "Darkmode"
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Quartz supports darkmode out of the box that respects the user's theme preference. Any future manual toggles of the darkmode switch will be saved in the browser's local storage so it can be persisted across future page loads.
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing darkmode: delete all usages of `Component.Darkmode()` from `quartz.layout.ts`.
|
||||
- Component: `quartz/components/Darkmode.tsx`
|
||||
- Style: `quartz/components/styles/darkmode.scss`
|
||||
- Script: `quartz/components/scripts/darkmode.inline.ts`
|
||||
@@ -1,245 +0,0 @@
|
||||
---
|
||||
title: "Explorer"
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and is highly customizable.
|
||||
|
||||
By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]].
|
||||
|
||||
Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead.
|
||||
|
||||
> [!info]
|
||||
> The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages.
|
||||
>
|
||||
> To clear/delete the explorer state from local storage, delete the `fileTree` entry (guide on how to delete a key from local storage in chromium based browsers can be found [here](https://docs.devolutions.net/kb/general-knowledge-base/clear-browser-local-storage/clear-chrome-local-storage/)). You can disable this by passing `useSavedState: false` as an argument.
|
||||
|
||||
## Customization
|
||||
|
||||
Most configuration can be done by passing in options to `Component.Explorer()`.
|
||||
|
||||
For example, here's what the default configuration looks like:
|
||||
|
||||
```typescript title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
title: "Explorer", // title of the explorer component
|
||||
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
||||
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
||||
useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer
|
||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||
sortFn: (a, b) => {
|
||||
... // default implementation shown later
|
||||
},
|
||||
filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder
|
||||
mapFn: undefined,
|
||||
// what order to apply functions in
|
||||
order: ["filter", "map", "sort"],
|
||||
})
|
||||
```
|
||||
|
||||
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
|
||||
|
||||
Want to customize it even more?
|
||||
|
||||
- Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts`
|
||||
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
|
||||
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
|
||||
- Component:
|
||||
- Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx`
|
||||
- Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx`
|
||||
- Style: `quartz/components/styles/explorer.scss`
|
||||
- Script: `quartz/components/scripts/explorer.inline.ts`
|
||||
|
||||
## Advanced customization
|
||||
|
||||
This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function.
|
||||
All functions you can pass work with the `FileNode` class, which has the following properties:
|
||||
|
||||
```ts title="quartz/components/ExplorerNode.tsx" {2-5}
|
||||
export class FileNode {
|
||||
children: FileNode[] // children of current node
|
||||
name: string // last part of slug
|
||||
displayName: string // what actually should be displayed in the explorer
|
||||
file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail
|
||||
depth: number // depth of current node
|
||||
|
||||
... // rest of implementation
|
||||
}
|
||||
```
|
||||
|
||||
Every function you can pass is optional. By default, only a `sort` function will be used:
|
||||
|
||||
```ts title="Default sort function"
|
||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||
Component.Explorer({
|
||||
sortFn: (a, b) => {
|
||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
|
||||
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
|
||||
return a.displayName.localeCompare(b.displayName, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
})
|
||||
}
|
||||
if (a.file && !b.file) {
|
||||
return 1
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one.
|
||||
|
||||
For more information on how to use `sort`, `filter` and `map`, you can check [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) and [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||
|
||||
Type definitions look like this:
|
||||
|
||||
```ts
|
||||
sortFn: (a: FileNode, b: FileNode) => number
|
||||
filterFn: (node: FileNode) => boolean
|
||||
mapFn: (node: FileNode) => void
|
||||
```
|
||||
|
||||
> [!tip]
|
||||
> You can check if a `FileNode` is a folder or a file like this:
|
||||
>
|
||||
> ```ts
|
||||
> if (node.file) {
|
||||
> // node is a file
|
||||
> } else {
|
||||
> // node is a folder
|
||||
> }
|
||||
> ```
|
||||
|
||||
## Basic examples
|
||||
|
||||
These examples show the basic usage of `sort`, `map` and `filter`.
|
||||
|
||||
### Use `sort` to put files first
|
||||
|
||||
Using this example, the explorer will alphabetically sort everything, but put all **files** above all **folders**.
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
sortFn: (a, b) => {
|
||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||
return a.displayName.localeCompare(b.displayName)
|
||||
}
|
||||
if (a.file && !b.file) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Change display names (`map`)
|
||||
|
||||
Using this example, the display names of all `FileNodes` (folders + files) will be converted to full upper case.
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
mapFn: (node) => {
|
||||
node.displayName = node.displayName.toUpperCase()
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Remove list of elements (`filter`)
|
||||
|
||||
Using this example, you can remove elements from your explorer by providing an array of folders/files using the `omit` set.
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
filterFn: (node) => {
|
||||
// set containing names of everything you want to filter out
|
||||
const omit = new Set(["authoring content", "tags", "hosting"])
|
||||
return !omit.has(node.name.toLowerCase())
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove.
|
||||
|
||||
### Show every element in explorer
|
||||
|
||||
To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`.
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
filterFn: undefined, // apply no filter function, every file and folder will visible
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced examples
|
||||
|
||||
### Add emoji prefix
|
||||
|
||||
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
mapFn: (node) => {
|
||||
// dont change name of root node
|
||||
if (node.depth > 0) {
|
||||
// set emoji for file/folder
|
||||
if (node.file) {
|
||||
node.displayName = "📄 " + node.displayName
|
||||
} else {
|
||||
node.displayName = "📁 " + node.displayName
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Putting it all together
|
||||
|
||||
In this example, we're going to customize the explorer by using functions from examples above to [[#Add emoji prefix | add emoji prefixes]], [[#remove-list-of-elements-filter| filter out some folders]] and [[#use-sort-to-put-files-first | sort with files above folders]].
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
filterFn: sampleFilterFn,
|
||||
mapFn: sampleMapFn,
|
||||
sortFn: sampleSortFn,
|
||||
order: ["filter", "sort", "map"],
|
||||
})
|
||||
```
|
||||
|
||||
Notice how we customized the `order` array here. This is done because the default order applies the `sort` function last. While this normally works well, it would cause unintended behavior here, since we changed the first characters of all display names. In our example, `sort` would be applied based off the emoji prefix instead of the first _real_ character.
|
||||
|
||||
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
||||
|
||||
> [!tip]
|
||||
> When writing more complicated functions, the `layout` file can start to look very cramped.
|
||||
> You can fix this by defining your functions in another file.
|
||||
>
|
||||
> ```ts title="functions.ts"
|
||||
> import { Options } from "./quartz/components/ExplorerNode"
|
||||
> export const mapFn: Options["mapFn"] = (node) => {
|
||||
> // implement your function here
|
||||
> }
|
||||
> export const filterFn: Options["filterFn"] = (node) => {
|
||||
> // implement your function here
|
||||
> }
|
||||
> export const sortFn: Options["sortFn"] = (a, b) => {
|
||||
> // implement your function here
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> You can then import them like this:
|
||||
>
|
||||
> ```ts title="quartz.layout.ts"
|
||||
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
||||
> Component.Explorer({
|
||||
> mapFn: mapFn,
|
||||
> filterFn: filterFn,
|
||||
> sortFn: sortFn,
|
||||
> })
|
||||
> ```
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: Folder and Tag Listings
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
Quartz creates listing pages for any folders and tags you have.
|
||||
|
||||
## Folder Listings
|
||||
|
||||
Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep.
|
||||
|
||||
Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then, Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`.
|
||||
|
||||
By default, Quartz will title the page `Folder: <name of folder>` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the description of the folder.
|
||||
|
||||
For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it.
|
||||
|
||||
## Tag Listings
|
||||
|
||||
Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag.
|
||||
|
||||
Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each layer of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz.
|
||||
|
||||
Like folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description.
|
||||
|
||||
## Customization
|
||||
|
||||
The layout for both the folder and content pages can be customized. By default, they use the `defaultListPageLayout` in `quartz.layouts.ts`. If you'd like to make more involved changes to the layout and don't mind editing some [[creating components|Quartz components]], you can take a look at `quartz/components/pages/FolderContent.tsx` and `quartz/components/pages/TagContent.tsx` respectively.
|
||||
|
||||
- Removing folder listings: remove `Plugin.FolderPage()` from `emitters` in `quartz.config.ts`
|
||||
- Removing tag listings: remove `Plugin.TagPage()` from `emitters` in `quartz.config.ts`
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: Full-text Search
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
It can be opened by either clicking on the search bar or pressing `⌘`/`ctrl` + `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.
|
||||
|
||||
To search content by tags, you can either press `⌘`/`ctrl` + `shift` + `K` or start your query with `#` (e.g. `#components`).
|
||||
|
||||
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). You are also able to navigate search results using `ArrowUp` and `ArrowDown`.
|
||||
|
||||
> [!info]
|
||||
> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
||||
|
||||
### Indexing Behaviour
|
||||
|
||||
By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed.
|
||||
|
||||
It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title, content and tags, weighing title matches above content matches.
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing search: delete all usages of `Component.Search()` from `quartz.layout.ts`.
|
||||
- Component: `quartz/components/Search.tsx`
|
||||
- Style: `quartz/components/styles/search.scss`
|
||||
- Script: `quartz/components/scripts/search.inline.ts`
|
||||
- You can edit `contextWindowWords`, `numSearchResults` or `numTagResults` to suit your needs
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: "Graph View"
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Quartz features a graph-view that can show both a local graph view and a global graph view.
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
By default, the node radius is proportional to the total number of incoming and outgoing internal links from that file.
|
||||
|
||||
Additionally, similar to how browsers highlight visited links a different colour, the graph view will also show nodes that you have visited in a different colour.
|
||||
|
||||
> [!info]
|
||||
> Graph View requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
||||
|
||||
## Customization
|
||||
|
||||
Most configuration can be done by passing in options to `Component.Graph()`.
|
||||
|
||||
For example, here's what the default configuration looks like:
|
||||
|
||||
```typescript title="quartz.layout.ts"
|
||||
Component.Graph({
|
||||
localGraph: {
|
||||
drag: true, // whether to allow panning the view around
|
||||
zoom: true, // whether to allow zooming in and out
|
||||
depth: 1, // how many hops of notes to display
|
||||
scale: 1.1, // default view scale
|
||||
repelForce: 0.5, // how much nodes should repel each other
|
||||
centerForce: 0.3, // how much force to use when trying to center the nodes
|
||||
linkDistance: 30, // how long should the links be by default?
|
||||
fontSize: 0.6, // what size should the node labels be?
|
||||
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
zoom: true,
|
||||
depth: -1,
|
||||
scale: 0.9,
|
||||
repelForce: 0.5,
|
||||
centerForce: 0.3,
|
||||
linkDistance: 30,
|
||||
fontSize: 0.6,
|
||||
opacityScale: 1,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
|
||||
|
||||
Want to customize it even more?
|
||||
|
||||
- Removing graph view: delete all usages of `Component.Graph()` from `quartz.layout.ts`.
|
||||
- Component: `quartz/components/Graph.tsx`
|
||||
- Style: `quartz/components/styles/graph.scss`
|
||||
- Script: `quartz/components/scripts/graph.inline.ts`
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Feature List
|
||||
---
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
title: Popover Previews
|
||||
---
|
||||
|
||||
Like Wikipedia, when you hover over a link in Quartz, there is a popup of a page preview that you can scroll to see the entire content. Links to headers will also scroll the popup to show that specific header in view.
|
||||
|
||||
By default, Quartz only fetches previews for pages inside your vault due to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). It does this by selecting all HTML elements with the `popover-hint` class. For most pages, this includes the page title, page metadata like words and time to read, tags, and the actual page content.
|
||||
|
||||
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`.
|
||||
- Style: `quartz/components/styles/popover.scss`
|
||||
- Script: `quartz/components/scripts/popover.inline.ts`
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: Private Pages
|
||||
tags:
|
||||
- plugin/filter
|
||||
---
|
||||
|
||||
There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction:
|
||||
|
||||
## Filter Plugins
|
||||
|
||||
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `draft: true` in the frontmatter.
|
||||
|
||||
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter.
|
||||
|
||||
> [!warning]
|
||||
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.
|
||||
>
|
||||
> `"!(PublicMedia)**/!(*.md)", "!(*.md)"`
|
||||
|
||||
## `ignorePatterns`
|
||||
|
||||
This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [fast-glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here.
|
||||
|
||||
> [!note]
|
||||
> Bash's glob syntax is slightly different from fast-glob's and using bash's syntax may lead to unexpected results.
|
||||
|
||||
Common examples include:
|
||||
|
||||
- `some/folder`: exclude the entire of `some/folder`
|
||||
- `*.md`: exclude all files with a `.md` extension
|
||||
- `!*.md` exclude all files that _don't_ have a `.md` extension
|
||||
- `**/private`: exclude any files or folders named `private` at any level of nesting
|
||||
|
||||
> [!warning]
|
||||
> Marking something as private via either a plugin or through the `ignorePatterns` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: Recent Notes
|
||||
tags: component
|
||||
---
|
||||
|
||||
Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`.
|
||||
|
||||
## Customization
|
||||
|
||||
- Changing the title from "Recent notes": pass in an additional parameter to `Component.RecentNotes({ title: "Recent writing" })`
|
||||
- Changing the number of recent notes: pass in an additional parameter to `Component.RecentNotes({ limit: 5 })`
|
||||
- Show a 'see more' link: pass in an additional parameter to `Component.RecentNotes({ linkToMore: "tags/components" })`. This field should be a full slug to a page that exists.
|
||||
- Customize filtering: pass in an additional parameter to `Component.RecentNotes({ filter: someFilterFunction })`. The filter function should be a function that has the signature `(f: QuartzPluginData) => boolean`.
|
||||
- Customize sorting: pass in an additional parameter to `Component.RecentNotes({ sort: someSortFunction })`. By default, Quartz will sort by date and then tie break lexographically. The sort function should be a function that has the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. See `byDateAndAlphabetical` in `quartz/components/PageList.tsx` for an example.
|
||||
- Component: `quartz/components/RecentNotes.tsx`
|
||||
- Style: `quartz/components/styles/recentNotes.scss`
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
title: Syntax Highlighting
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting.
|
||||
|
||||
And, unlike some client-side highlighters, it has a full TextMate parser grammar instead of using Regexes, allowing for highly accurate code highlighting.
|
||||
|
||||
In short, it generates HTML that looks exactly like your code in an editor like VS Code. Under the hood, it's powered by [Rehype Pretty Code](https://rehype-pretty-code.netlify.app/) which uses [Shiki](https://github.com/shikijs/shiki).
|
||||
|
||||
> [!warning]
|
||||
> Syntax highlighting does have an impact on build speed if you have a lot of code snippets in your notes.
|
||||
|
||||
## Formatting
|
||||
|
||||
Text inside `backticks` on a line will be formatted like code.
|
||||
|
||||
````
|
||||
```ts
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
return cleanPath + anchor
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
```ts
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
return cleanPath + anchor
|
||||
}
|
||||
```
|
||||
|
||||
### Titles
|
||||
|
||||
Add a file title to your code block, with text inside double quotes (`""`):
|
||||
|
||||
````
|
||||
```js title="..."
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
```ts title="quartz/path.ts"
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
return cleanPath + anchor
|
||||
}
|
||||
```
|
||||
|
||||
### Line highlighting
|
||||
|
||||
Place a numeric range inside `{}`.
|
||||
|
||||
````
|
||||
```js {1-3,4}
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
```ts {2-3,6}
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
return cleanPath + anchor
|
||||
}
|
||||
```
|
||||
|
||||
### Word highlighting
|
||||
|
||||
A series of characters, like a literal regex.
|
||||
|
||||
````
|
||||
```js /useState/
|
||||
const [age, setAge] = useState(50);
|
||||
const [name, setName] = useState('Taylor');
|
||||
```
|
||||
````
|
||||
|
||||
```js /useState/
|
||||
const [age, setAge] = useState(50)
|
||||
const [name, setName] = useState("Taylor")
|
||||
```
|
||||
|
||||
### Line numbers
|
||||
|
||||
Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`:
|
||||
|
||||
````
|
||||
```js showLineNumbers{number}
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
```ts showLineNumbers{20}
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
return cleanPath + anchor
|
||||
}
|
||||
```
|
||||
|
||||
### Escaping code blocks
|
||||
|
||||
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.
|
||||
|
||||
`````
|
||||
````
|
||||
```js /useState/
|
||||
const [age, setAge] = useState(50);
|
||||
const [name, setName] = useState('Taylor');
|
||||
```
|
||||
````
|
||||
`````
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing syntax highlighting: delete all usages of `Plugin.SyntaxHighlighting()` from `quartz.config.ts`.
|
||||
- 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.
|
||||
- Plugin: `quartz/plugins/transformers/syntax.ts`
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: "Table of Contents"
|
||||
tags:
|
||||
- component
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
||||
|
||||
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
||||
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
|
||||
|
||||
> [!info]
|
||||
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||
|
||||
## Customization
|
||||
|
||||
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
||||
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
||||
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
||||
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
|
||||
- Component: `quartz/components/TableOfContents.tsx`
|
||||
- Style:
|
||||
- Modern (default): `quartz/components/styles/toc.scss`
|
||||
- Legacy Quartz 3 style: `quartz/components/styles/legacyToc.scss`
|
||||
- Script: `quartz/components/scripts/toc.inline.ts`
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
## high priority backlog
|
||||
|
||||
- static dead link detection
|
||||
- block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
|
||||
- note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files
|
||||
- docker support
|
||||
|
||||
## misc backlog
|
||||
|
||||
- breadcrumbs component
|
||||
- cursor chat extension
|
||||
- https://giscus.app/ extension
|
||||
- sidenotes? https://github.com/capnfabs/paperesque
|
||||
- direct match in search using double quotes
|
||||
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
||||
- audio/video embed styling
|
||||
- Canvas
|
||||
- parse all images in page: use this for page lists if applicable?
|
||||
- CV mode? with print stylesheet
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: Wikilinks
|
||||
---
|
||||
|
||||
Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time.
|
||||
|
||||
Quartz supports Wikilinks by default and these links are resolved by Quartz using `Plugin.CrawlLinks`. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax.
|
||||
|
||||
This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
||||
|
||||
## Syntax
|
||||
|
||||
- `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file`
|
||||
- `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override`
|
||||
- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md`
|
||||
- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md`
|
||||
221
docs/hosting.md
@@ -1,221 +0,0 @@
|
||||
---
|
||||
title: Hosting
|
||||
---
|
||||
|
||||
Quartz effectively turns your Markdown files and other resources into a bundle of HTML, JS, and CSS files (a website!).
|
||||
|
||||
However, if you'd like to publish your site to the world, you need a way to host it online. This guide will detail how to deploy with either GitHub Pages or Cloudflare pages but any service that allows you to deploy static HTML should work as well (e.g. Netlify, Replit, etc.)
|
||||
|
||||
> [!hint]
|
||||
> Some Quartz features (like [[RSS Feed]] and sitemap generation) require `baseUrl` to be configured properly in your [[configuration]] to work properly. Make sure you set this before deploying!
|
||||
|
||||
## Cloudflare Pages
|
||||
|
||||
1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select your account.
|
||||
2. In Account Home, select **Workers & Pages** > **Create application** > **Pages** > **Connect to Git**.
|
||||
3. Select the new GitHub repository that you created and, in the **Set up builds and deployments** section, provide the following information:
|
||||
|
||||
| Configuration option | Value |
|
||||
| ---------------------- | ------------------ |
|
||||
| Production branch | `v4` |
|
||||
| Framework preset | `None` |
|
||||
| Build command | `npx quartz build` |
|
||||
| Build output directory | `public` |
|
||||
|
||||
Press "Save and deploy" and Cloudflare should have a deployed version of your site in about a minute. Then, every time you sync your Quartz changes to GitHub, your site should be updated.
|
||||
|
||||
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
|
||||
|
||||
## GitHub Pages
|
||||
|
||||
Like Quartz 3, you can deploy the site generated by Quartz 4 via GitHub Pages.
|
||||
|
||||
> [!warning]
|
||||
> Quartz generates files in the format of `file.html` instead of `file/index.html` which means the trailing slashes for _non-folder paths_ are dropped. As GitHub pages does not do this redirect, this may cause existing links to your site that use trailing slashes to break. If not breaking existing links is important to you, consider using [[#Cloudflare Pages]].
|
||||
|
||||
In your local Quartz, create a new file `quartz/.github/workflows/deploy.yml`.
|
||||
|
||||
```yaml title="quartz/.github/workflows/deploy.yml"
|
||||
name: Deploy Quartz site to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v4
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git info
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.14
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Quartz
|
||||
run: npx quartz build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
1. Head to "Settings" tab of your forked repository and in the sidebar, click "Pages". Under "Source", select "GitHub Actions".
|
||||
2. Commit these changes by doing `npx quartz sync`. This should deploy your site to `<github-username>.github.io/<repository-name>`.
|
||||
|
||||
> [!hint]
|
||||
> If you get an error about not being allowed to deploy to `github-pages` due to environment protection rules, make sure you remove any existing GitHub pages environments.
|
||||
>
|
||||
> You can do this by going to your Settings page on your GitHub fork and going to the Environments tab and pressing the trash icon. The GitHub action will recreate the environment for you correctly the next time you sync your Quartz.
|
||||
|
||||
### Custom Domain
|
||||
|
||||
Here's how to add a custom domain to your GitHub pages deployment.
|
||||
|
||||
1. Head to the "Settings" tab of your forked repository.
|
||||
2. In the "Code and automation" section of the sidebar, click "Pages".
|
||||
3. Under "Custom Domain", type your custom domain and click "Save".
|
||||
4. This next step depends on whether you are using an apex domain (`example.com`) or a subdomain (`subdomain.example.com`).
|
||||
- If you are using an apex domain, navigate to your DNS provider and create an `A` record that points your apex domain to GitHub's name servers which have the following IP addresses:
|
||||
- `185.199.108.153`
|
||||
- `185.199.109.153`
|
||||
- `185.199.110.153`
|
||||
- `185.199.111.153`
|
||||
- If you are using a subdomain, navigate to your DNS provider and create a `CNAME` record that points your subdomain to the default domain for your site. For example, if you want to use the subdomain `quartz.example.com` for your user site, create a `CNAME` record that points `quartz.example.com` to `<github-username>.github.io`.
|
||||
|
||||
![[dns records.png]]_The above shows a screenshot of Google Domains configured for both `jzhao.xyz` (an apex domain) and `quartz.jzhao.xyz` (a subdomain)._
|
||||
|
||||
See the [GitHub documentation](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-a-subdomain) for more detail about how to setup your own custom domain with GitHub Pages.
|
||||
|
||||
> [!question] Why aren't my changes showing up?
|
||||
> There could be many different reasons why your changes aren't showing up but the most likely reason is that you forgot to push your changes to GitHub.
|
||||
>
|
||||
> Make sure you save your changes to Git and sync it to GitHub by doing `npx quartz sync`. This will also make sure to pull any updates you may have made from other devices so you have them locally.
|
||||
|
||||
## Vercel
|
||||
|
||||
### Fix URLs
|
||||
|
||||
Before deploying to Vercel, a `vercel.json` file is required at the root of the project directory. It needs to contain the following configuration so that URLs don't require the `.html` extension:
|
||||
|
||||
```json title="vercel.json"
|
||||
{
|
||||
"cleanUrls": true
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
1. Log in to the [Vercel Dashboard](https://vercel.com/dashboard) and click "Add New..." > Project
|
||||
2. Import the Git repository containing your Quartz project.
|
||||
3. Give the project a name (lowercase characters and hyphens only)
|
||||
4. Check that these configuration options are set:
|
||||
|
||||
| Configuration option | Value |
|
||||
| ----------------------------------------- | ------------------ |
|
||||
| Framework Preset | `Other` |
|
||||
| Root Directory | `./` |
|
||||
| Build and Output Settings > Build Command | `npx quartz build` |
|
||||
|
||||
5. Press Deploy. Once it's live, you'll have 2 `*.vercel.app` URLs to view the page.
|
||||
|
||||
### Custom Domain
|
||||
|
||||
> [!note]
|
||||
> If there is something already hosted on the domain, these steps will not work without replacing the previous content. As a workaround, you could use Next.js rewrites or use the next section to create a subdomain.
|
||||
|
||||
1. Update the `baseUrl` in `quartz.config.js` if necessary.
|
||||
2. Go to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel.
|
||||
3. Connect the domain to Vercel
|
||||
4. Press "Add" to connect a custom domain to Vercel.
|
||||
5. Select your Quartz repository and press Continue.
|
||||
6. Enter the domain you want to connect it to.
|
||||
7. Follow the instructions to update your DNS records until you see "Valid Configuration"
|
||||
|
||||
### Use a Subdomain
|
||||
|
||||
Using `docs.example.com` is an example of a subdomain. They're a simple way of connecting multiple deployments to one domain.
|
||||
|
||||
1. Update the `baseUrl` in `quartz.config.js` if necessary.
|
||||
2. Ensure your domain has been added to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel.
|
||||
3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project.
|
||||
4. Go to the Settings tab and then click Domains in the sidebar
|
||||
5. Enter your subdomain into the field and press Add
|
||||
|
||||
## GitLab Pages
|
||||
|
||||
You can configure GitLab CI to build and deploy a Quartz 4 project.
|
||||
|
||||
In your local Quartz, create a new file `.gitlab-ci.yaml`.
|
||||
|
||||
```yaml title=".gitlab-ci.yaml"
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
NODE_VERSION: "18.14"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||
before_script:
|
||||
- apt-get update -q && apt-get install -y nodejs npm
|
||||
- npm install -g n
|
||||
- n $NODE_VERSION
|
||||
- hash -r
|
||||
- npm ci
|
||||
script:
|
||||
- npx prettier --write .
|
||||
- npm run check
|
||||
- npx quartz build
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
cache:
|
||||
paths:
|
||||
- ~/.npm/
|
||||
key: "${CI_COMMIT_REF_SLUG}-node-${CI_COMMIT_REF_NAME}"
|
||||
tags:
|
||||
- docker
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||
script:
|
||||
- echo "Deploying to GitLab Pages..."
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
```
|
||||
|
||||
When `.gitlab-ci.yaml` is commited, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy` -> `Pages` in the sidebar.
|
||||
|
||||
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
|
||||
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 72 KiB |
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Welcome to Quartz 4
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## 🪴 Get Started
|
||||
|
||||
Quartz requires **at least [Node](https://nodejs.org/) v18.14** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
|
||||
|
||||
Then, in your terminal of choice, enter the following commands line by line:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/jackyzha0/quartz.git
|
||||
cd quartz
|
||||
npm i
|
||||
npx quartz create
|
||||
```
|
||||
|
||||
This will guide you through initializing your Quartz with content. Once you've done so, see how to:
|
||||
|
||||
1. [[authoring content|Author content]] in Quartz
|
||||
2. [[configuration|Configure]] Quartz's behaviour
|
||||
3. Change Quartz's [[layout]]
|
||||
4. [[build|Build and preview]] Quartz
|
||||
5. [[hosting|Host]] Quartz online
|
||||
|
||||
> [!info]
|
||||
> Coming from Quartz 3? See the [[migrating from Quartz 3|migration guide]] for the differences between Quartz 3 and Quartz 4 and how to migrate.
|
||||
|
||||
## 🔧 Features
|
||||
|
||||
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
|
||||
- Hot-reload for both configuration and content
|
||||
- Simple JSX layouts and [[creating components|page components]]
|
||||
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
||||
- Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]]
|
||||
|
||||
For a comprehensive list of features, visit the [features page](/features). You can read more about the _why_ behind these features on the [[philosophy]] page and a technical overview on the [[architecture]] page.
|
||||
|
||||
### 🚧 Troubleshooting + Updating
|
||||
|
||||
Having trouble with Quartz? Try searching for your issue using the search feature. If you haven't already, [[upgrading|upgrade]] to the newest version of Quartz to see if this fixes your issue.
|
||||
|
||||
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).
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
title: Layout
|
||||
---
|
||||
|
||||
Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To enable easy customization, these emitters allow you to fully rearrange the layout of the page. The default page layouts can be found in `quartz.layout.ts`.
|
||||
|
||||
Each page is composed of multiple different sections which contain `QuartzComponents`. The following code snippet lists all of the valid sections that you can add components to:
|
||||
|
||||
```typescript title="quartz/cfg.ts"
|
||||
export interface FullPageLayout {
|
||||
head: QuartzComponent // single component
|
||||
header: QuartzComponent[] // laid out horizontally
|
||||
beforeBody: QuartzComponent[] // laid out vertically
|
||||
pageBody: QuartzComponent // single component
|
||||
left: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
||||
right: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
||||
footer: QuartzComponent // single component
|
||||
}
|
||||
```
|
||||
|
||||
These correspond to following parts of the page:
|
||||
|
||||
![[quartz layout.png|800]]
|
||||
|
||||
> [!note]
|
||||
> There are two additional layout fields that are _not_ shown in the above diagram.
|
||||
>
|
||||
> 1. `head` is a single component that renders the `<head>` [tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) in the HTML. This doesn't appear visually on the page and is only is responsible for metadata about the document like the tab title, scripts, and styles.
|
||||
> 2. `header` is a set of components that are laid out horizontally and appears _before_ the `beforeBody` section. This enables you to replicate the old Quartz 3 header bar where the title, search bar, and dark mode toggle. By default, Quartz 4 doesn't place any components in the `header`.
|
||||
|
||||
Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components.
|
||||
|
||||
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
|
||||
|
||||
### Style
|
||||
|
||||
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.
|
||||
|
||||
You can see the base style sheet in `quartz/styles/base.scss` and write your own in `quartz/styles/custom.scss`.
|
||||
|
||||
> [!note]
|
||||
> Some components may provide their own styling as well! For example, `quartz/components/Darkmode.tsx` imports styles from `quartz/components/styles/darkmode.scss`. If you'd like to customize styling for a specific component, double check the component definition to see how its styles are defined.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
title: "Migrating from Quartz 3"
|
||||
---
|
||||
|
||||
As you already have Quartz locally, you don't need to fork or clone it again. Simply just checkout the alpha branch, install the dependencies, and import your old vault.
|
||||
|
||||
```bash
|
||||
git fetch
|
||||
git checkout v4
|
||||
git pull upstream v4
|
||||
npm i
|
||||
npx quartz create
|
||||
```
|
||||
|
||||
If you get an error like `fatal: 'upstream' does not appear to be a git repository`, make sure you add `upstream` as a remote origin:
|
||||
|
||||
```shell
|
||||
git remote add upstream https://github.com/jackyzha0/quartz.git
|
||||
```
|
||||
|
||||
When running `npx quartz create`, you will be prompted as to how to initialize your content folder. Here, you can choose to import or link your previous content folder and Quartz should work just as you expect it to.
|
||||
|
||||
> [!note]
|
||||
> If the existing content folder you'd like to use is at the _same_ path on a different branch, clone the repo again somewhere at a _different_ path in order to use it.
|
||||
|
||||
## Key changes
|
||||
|
||||
1. **Removing Hugo and `hugo-obsidian`**: Hugo worked well for earlier versions of Quartz but it also made it hard for people outside of the Golang and Hugo communities to fully understand what Quartz was doing under the hood and be able to properly customize it to their needs. Quartz 4 now uses a Node-based static-site generation process which should lead to a much more helpful error messages and an overall smoother user experience.
|
||||
2. **Full-hot reload**: The many rough edges of how `hugo-obsidian` integrated with Hugo meant that watch mode didn't re-trigger `hugo-obsidian` to update the content index. This lead to a lot of weird cases where the watch mode output wasn't accurate. Quartz 4 now uses a cohesive parse, filter, and emit pipeline which gets run on every change so hot-reloads are always accurate.
|
||||
3. **Replacing Go template syntax with JSX**: Quartz 3 used [Go templates](https://pkg.go.dev/text/template) to create layouts for pages. However, the syntax isn't great for doing any sort of complex rendering (like [text processing](https://github.com/jackyzha0/quartz/blob/hugo/layouts/partials/textprocessing.html)) and it got very difficult to make any meaningful layout changes to Quartz 3. Quartz 4 uses an extension of JavaScript syntax called JSX which allows you to write layout code that looks like HTML in JavaScript which is significantly easier to understand and maintain.
|
||||
4. **A new extensible [[configuration]] and [[configuration#Plugins|plugin]] system**: Quartz 3 was hard to configure without technical knowledge of how Hugo's partials worked. Extensions were even hard to make. Quartz 4's configuration and plugin system is designed to be extended by users while making updating to new versions of Quartz easy.
|
||||
|
||||
## Things to update
|
||||
|
||||
- You will need to update your deploy scripts. See the [[hosting]] guide for more details.
|
||||
- Ensure that your default branch on GitHub is updated from `hugo` to `v4`.
|
||||
- [[folder and tag listings|Folder and tag listings]] have also changed.
|
||||
- Folder descriptions should go under `content/<folder-name>/index.md` where `<folder-name>` is the name of the folder.
|
||||
- Tag descriptions should go under `content/tags/<tag-name>.md` where `<tag-name>` is the name of the tag.
|
||||
- Some HTML layout may not be the same between Quartz 3 and Quartz 4. If you depended on a particular HTML hierarchy or class names, you may need to update your custom CSS to reflect these changes.
|
||||
- If you customized the layout of Quartz 3, you may need to translate these changes from Go templates back to JSX as Quartz 4 no longer uses Hugo. For components, check out the guide on [[creating components]] for more details on this.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: Philosophy of Quartz
|
||||
---
|
||||
|
||||
## A garden should be a true hypertext
|
||||
|
||||
> 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.
|
||||
>
|
||||
> _(The Garden and the Stream)_
|
||||
|
||||
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.
|
||||
|
||||
My goal with a digital garden is not purely as an organizing system and information store (though it works nicely for that). I want my digital garden to be a playground for new ways ideas can connect together. As a result, existing formal organizing systems like Zettelkasten or the hierarchical folder structures of Notion don’t work well for me. There is way too much upfront friction that by the time I’ve thought about how to organize my thought into folders categories, I’ve lost it.
|
||||
|
||||
Quartz embraces the inherent rhizomatic and web-like nature of our thinking and tries to encourage note-taking in a similar form.
|
||||
|
||||
---
|
||||
|
||||
## A garden should be shared
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
> “[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
|
||||
|
||||
**The goal of Quartz is to make sharing your digital garden free and simple.** At its core, Quartz is designed to be easy to use enough for non-technical people to get going but also powerful enough that senior developers can tweak it to work how they'd like it to work.
|
||||