Merge remote-tracking branch 'upstream/v4' into v4

This commit is contained in:
Tomoya Matsuura(MacBookPro) 2023-08-21 12:27:12 +09:00
commit b06b792576
180 changed files with 13135 additions and 3809 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -1,10 +1,9 @@
--- ---
name: Bug report name: Bug report
about: Something about Quartz isn't working the way you expect about: Something about Quartz isn't working the way you expect
title: '' title: ""
labels: bug labels: bug
assignees: '' assignees: ""
--- ---
**Describe the bug** **Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS] - Device: [e.g. iPhone6]
- Browser [e.g. chrome, safari] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@ -1,10 +1,9 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea or improvement for Quartz about: Suggest an idea or improvement for Quartz
title: '' title: ""
labels: enhancement labels: enhancement
assignees: '' assignees: ""
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

44
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Build and Test
on:
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
run: npx quartz build

View File

@ -1,85 +0,0 @@
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hugo site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["hugo"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.117.0
steps:
- name: Make .GitInfo compatible with CJK characters
run: git config --global core.quotepath false
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
# - name: Install Dart Sass
# run: sudo snap install dart-sass
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Pages
id: pages
uses: actions/configure-pages@v3
# - name: Install Node.js dependencies
# run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build Link Index
uses: jackyzha0/hugo-obsidian@v2.20
with:
index: true
input: content
output: assets/indices
root: .
- name: Build with Hugo
env:
# For maximum backward compatibility with Hugo modules
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: |
hugo \
--minify \
--baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ./public
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View File

@ -1,42 +0,0 @@
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 }}

12
.gitignore vendored
View File

@ -1,7 +1,9 @@
.DS_Store .DS_Store
.gitignore
node_modules
public public
resources prof
.idea tsconfig.tsbuildinfo
assets/indices/linkIndex.json .obsidian
assets/indices/contentIndex.json .quartz-cache
linkmap private/

View File

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
public
node_modules
.quartz-cache

View File

@ -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: 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. - 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. - Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict. - Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech. - 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. - 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. - 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 ## 4. Unacceptable Behavior
The following behaviors are considered harassment and are unacceptable within our community: The following behaviors are considered harassment and are unacceptable within our community:
* Violence, threats of violence or violent language directed against another person. - Violence, threats of violence or violent language directed against another person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. - Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
* Posting or displaying sexually explicit or violent material. - Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other people's personally identifying information ("doxing"). - 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. - Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
* Inappropriate photography or recording. - Inappropriate photography or recording.
* Inappropriate physical contact. You should have someone's consent before touching them. - 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. - 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). - Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior. - Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations. - Sustained disruption of community events, including talks and presentations.
## 5. Weapons Policy ## 5. Weapons Policy
@ -59,14 +59,11 @@ 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. 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. 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 ## 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 ## 9. Scope
@ -80,7 +77,7 @@ j.zhao2k19@gmail.com
## 11. License and attribution ## 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). 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).

View File

@ -1,11 +0,0 @@
FROM alpine:3.16
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"]

View File

@ -1,24 +0,0 @@
.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

View File

@ -1,22 +1,21 @@
# https://garden.matsuuratomoya.com
# https://garden.matsuuratomoya.com w/ Quartz v4
---
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 > “[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
🔗 Get Started: https://quartz.jzhao.xyz/ 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.
![Quartz Example Screenshot](./screenshot.png)*Quartz Example Screenshot* **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://four.quartz.jzhao.xyz/
[Join the Discord Community](https://discord.gg/cRFFHYye7t) [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>

View File

@ -1,6 +0,0 @@
const addCollapsibleCallouts = () => {
const collapsibleCallouts = document.querySelectorAll("blockquote.callout-collapsible");
collapsibleCallouts.forEach(el => el.addEventListener('click', event => {
event.currentTarget.classList.toggle("callout-collapsed");
}));
}

View File

@ -1,45 +0,0 @@
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);
}
}
}

View File

@ -1,17 +0,0 @@
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);
}
}
}

View File

@ -1,37 +0,0 @@
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
}
})

View File

@ -1,61 +0,0 @@
; (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)
})
})()

View File

@ -1,284 +0,0 @@
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) => {
const str = content[d.id]?.title ||
decodeURI(d.id.charAt(1).toUpperCase() + d.id.slice(2))
.replace("-", " ");
return str.slice(0, 30) + (str.length > 30 ? "..." : "")
})
.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)
})
}

View File

@ -1,79 +0,0 @@
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")
})
}
})
})
}

View File

@ -1,25 +0,0 @@
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)
}

View File

@ -1,54 +0,0 @@
// 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))
}
}),
)

View File

@ -1,226 +0,0 @@
// 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)
})
}
}

View File

@ -1,174 +0,0 @@
: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;
}
}
}
}
}

View File

@ -1,85 +0,0 @@
/* 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 { }

View File

@ -1,85 +0,0 @@
/* 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 { }

View File

@ -1,625 +0,0 @@
// 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);
}

View File

@ -1,20 +0,0 @@
.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);
}

View File

@ -1,31 +0,0 @@
// Add your own CSS here!
:root {
--light: #d7eff7;
--dark: #284b63;
--secondary: #207e8f;
--tertiary: #84a59d;
--visited: #afbfc9;
--primary: #FA8F2D;
--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;
}

View File

@ -1,66 +0,0 @@
// 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;
}

View File

@ -1,31 +0,0 @@
title = "Matsuura Tomoya Research Note"
baseURL = "https://garden.matsuuratomoya.com"
languageCode = "ja"
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"
[markup.goldmark.renderer]
unsafe = true

View File

@ -1,5 +0,0 @@
---
title: "Private Stuff"
---
This page doesn't get published!

View File

@ -1,4 +0,0 @@
---
title: "{{title}}"
tags:
---

View File

@ -1,32 +0,0 @@
name: Tomoya Matsuura
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/tomoyanonymous/quartz-research-note/tree/hugo/content
search:
enableSemanticSearch: false
operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
operandIndexId: "REPLACE-WITH-YOUR-OPERAND-INDEX-ID"
description:
松浦知也の研究ノートです.
page_title:
"Matsuura Tomoya Research Note"
links:
- link_name: Top
link: https://matsuuratomoya.com
- link_name: Mastodon
link: https://social.matsuuratomoya.com/@tomoya
- link_name: Twitter
link: https://twitter.com/tomoya_nonymous
- link_name: GitHub
link: https://github.com/tomoyanonymous

View File

@ -1,37 +0,0 @@
# 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: true
### Local Graph ###
localGraph:
enableLegend: false
enableDrag: true
enableZoom: true
depth: 2 # 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"

12
globals.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
export declare global {
interface Document {
addEventListener<K extends keyof CustomEventMap>(
type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void,
): void
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void
}
interface Window {
spaNavigate(url: URL, isBack: boolean = false)
}
}

View File

@ -1,65 +0,0 @@
[404_message]
other = "يبدو أنك ضللت الطريق. هذه الصفحة غير موجودة (أو قد تكون خاصة)."
[404_back]
other = "↳ العودة للرئيسية."
[all_posts]
other = "كل منشورات {{.Title}}"
[last_updated]
other = "آخر تعديل"
[notes_count]
other = "ملاحظات بهذه التسمية"
[first_10]
other = "(تعرض أول 10 نتائج فقط)"
[tag]
other = "التسمية"
[backlinks]
other = "الروابط الخلفية"
[no_backlinks]
other = "لا توجد روابط خلفية"
[home]
other = "الرئيسية"
[light_mode]
other = "السمة الفاتحة"
[dark_mode]
other = "السمة الداكنة"
[edit_source]
other = "تعديل المصدر"
[interactive_graph]
other = "المخطط التفاعلي"
[search]
other = "البحث"
[search_icon]
other = "أيقونة البحث"
[icon_search]
other = "أيقونة فتح نافذة البحث"
[recent_notes]
other = "الملاحظات اﻷخيرة"
[first_3_notes]
other = "أول 3 {{ .notes }}"
[search_for_something]
other = "ابحث عن شيء ما..."
[toc]
other = "الفهرس"
[copyright]
other = "صُمم بواسطة {{ .name }} باستخدام <a href='https://github.com/jackyzha0/quartz'>كوارتز</a>، {{ .year }} ©"

View File

@ -1,65 +0,0 @@
[404_message]
other = "এই পাতাটি নেই (অথবা ব্যক্তিগত)"
[404_back]
other = "↳ হোম পেজে ফিরে যাই"
[all_posts]
other = "সকল {{.Title}}"
[last_updated]
other = "সর্বশেষ পরিবর্তিত"
[notes_count]
other = "সংখ্যক এই ট্যাগের"
[first_10]
other = "প্রথম ১০ টি ফলাফল"
[tag]
other = "ট্যাগ"
[backlinks]
other = "পিছন পাতা"
[no_backlinks]
other = "পিছনে কোন পাতা নেই"
[home]
other = "হোম"
[light_mode]
other = "আলোকিত"
[dark_mode]
other = "অন্ধকার"
[edit_source]
other = "সম্পাদন করুন"
[interactive_graph]
other = "জ্ঞানকোষের গ্রাফ"
[search]
other = "খুঁজুন"
[search_icon]
other = "খোঁজার আইকন"
[icon_search]
other = "জ্ঞানকোষ"
[recent_notes]
other = "সাম্প্রতিক"
[first_3_notes]
other = "প্রথম ৩ {{ .notes }}"
[search_for_something]
other = "খুঁজুন..."
[toc]
other = "সূচিপত্র"
[copyright]
other = "{{ .name }} কর্তৃক <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a> ব্যবহার করে তৈরিকৃত © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey! Hast du dich verirrt? Diese Seite existiert leider nicht (oder ist privat)."
[404_back]
other = "↳ Zurück zur Startseite."
[all_posts]
other = "Alle {{.Title}}"
[last_updated]
other = "Zuletzt aktualisiert"
[notes_count]
other = "Beiträge mit diesem Tag"
[first_10]
other = "Zeige die ersten 10 Ergebnisse"
[tag]
other = "Tag"
[backlinks]
other = "Backlinks"
[no_backlinks]
other = "Keine Backlinks gefunden"
[home]
other = "Home"
[light_mode]
other = "Light Mode"
[dark_mode]
other = "Dark Mode"
[edit_source]
other = "Quelldatei bearbeiten"
[interactive_graph]
other = "Interaktiver Graph"
[search]
other = "Suche"
[search_icon]
other = "Suchsymbol"
[icon_search]
other = "Symbol, um die Suche zu öffnen"
[recent_notes]
other = "Neuste Beiträge"
[first_3_notes]
other = "Die ersten 3 {{ .notes }}"
[search_for_something]
other = "Suche nach etwas ..."
[toc]
other = "Inhaltsverzeichnis"
[copyright]
other = "Made by {{ .name }} using <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey! You look a little lost. This page doesn't exist (or may be private)."
[404_back]
other = "↳ Let's get you home."
[all_posts]
other = "All {{.Title}}"
[last_updated]
other = "Last updated"
[notes_count]
other = "notes with this tag"
[first_10]
other = "showing first 10 results"
[tag]
other = "Tag"
[backlinks]
other = "Backlinks"
[no_backlinks]
other = "No backlinks found"
[home]
other = "Home"
[light_mode]
other = "Light Mode"
[dark_mode]
other = "Dark Mode"
[edit_source]
other = "Edit Source"
[interactive_graph]
other = "Interactive Graph"
[search]
other = "Search"
[search_icon]
other = "Search Icon"
[icon_search]
other = "Icon to open search"
[recent_notes]
other = "Recent Notes"
[first_3_notes]
other = "first 3 {{ .notes }}"
[search_for_something]
other = "Search for something..."
[toc]
other = "Table of Contents"
[copyright]
other = "Made by {{ .name }} using <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey! Te ves un poco perdido. Esta página no existe (o puede que sea privada)."
[404_back]
other = "↳ Vamos a llevarte de regreso a casa."
[all_posts]
other = "Todos {{.Title}}"
[last_updated]
other = "Actualizado por última vez"
[notes_count]
other = "notas con esta etiqueta"
[first_10]
other = "mostrando los primeros 10 resultados"
[tag]
other = "Etiqueta"
[backlinks]
other = "Backlinks"
[no_backlinks]
other = "No se encontraron backlinks"
[home]
other = "Casa"
[light_mode]
other = "Modo Claro"
[dark_mode]
other = "Modo Oscuro"
[edit_source]
other = "Editar Fuente"
[interactive_graph]
other = "Gráfico Interactivo"
[search]
other = "Búsqueda"
[search_icon]
other = "Ícono de Búsqueda"
[icon_search]
other = "Ícono para abrir la búsqueda"
[recent_notes]
other = "Notas Recientes"
[first_3_notes]
other = "primeras 3 {{ .notes }}"
[search_for_something]
other = "Buscar algo..."
[toc]
other = "Tabla de Contenido"
[copyright]
other = "Hecho por {{ .name }} usando <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey ! Vous semblez perdu‧e. Cette page n'existe pas (ou est privée)."
[404_back]
other = "↳ Clique ici pour retourner sur la page d'accueil"
[all_posts]
other = "Tout {{.Title}}"
[last_updated]
other = "Dernière modification"
[notes_count]
other = "notes avec ce tag"
[first_10]
other = "les 10 premiers résultats"
[tag]
other = "Tag"
[backlinks]
other = "Backlinks"
[no_backlinks]
other = "Pas de backlinks trouvés"
[home]
other = "Accueil"
[light_mode]
other = "Mode Clair"
[dark_mode]
other = "Mode Sombre"
[edit_source]
other = "Modifier la source"
[interactive_graph]
other = "Graphique interactif"
[search]
other = "Rechercher"
[search_icon]
other = "l'icône de recherche"
[icon_search]
other = "L'icône pour ouvrir la recherche"
[recent_notes]
other = "Notes récentes"
[first_3_notes]
other = "les 3 premières {{ .notes }}"
[search_for_something]
other = "Rechercher quelque-chose..."
[toc]
other = "Table des matières"
[copyright]
other = "Fait par {{ .name }} en utilisant <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey, ti sei perso? Questa pagina non esiste (o è privata)"
[404_back]
other = "↳ Torna alla home."
[all_posts]
other = "Tutti {{.Title}}"
[last_updated]
other = "Ultima modifica"
[notes_count]
other = "note con questo tag"
[first_10]
other = "mostrando i primi 10 risultati"
[tag]
other = "Tag"
[backlinks]
other = "Backlinks"
[no_backlinks]
other = "Nessun Backlink trovato"
[home]
other = "Home"
[light_mode]
other = "Modalità Chiara"
[dark_mode]
other = "Modalità Scura"
[edit_source]
other = "Modifica Sorgente"
[interactive_graph]
other = "Grafico Interattivo"
[search]
other = "Cerca"
[search_icon]
other = "Icona di ricerca"
[icon_search]
other = "Icona per aprire la ricerca"
[recent_notes]
other = "Note Recenti"
[first_3_notes]
other = "prime 3 {{ .notes }}"
[search_for_something]
other = "Cerca qualcosa..."
[toc]
other = "Indice"
[copyright]
other = "Realizzato da {{ .name }} con <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hei! Ser ut til at du har gått deg vill. Denne siden finnes ikke (eller den kan være privat)."
[404_back]
other = "↳ La oss få deg hjem."
[all_posts]
other = "Alle {{.Title}}"
[last_updated]
other = "Sist oppdatert"
[notes_count]
other = "notater med denne taggen"
[first_10]
other = "viser første 10 resultatene"
[tag]
other = "Tag"
[backlinks]
other = "Tilbakekoblinger"
[no_backlinks]
other = "Ingen tilbakekoblinger funnet"
[home]
other = "Hjem"
[light_mode]
other = "Lys Modus"
[dark_mode]
other = "Mørk Modus"
[edit_source]
other = "Rediger Kilde"
[interactive_graph]
other = "Interaktiv Graf"
[search]
other = "Søk"
[search_icon]
other = "Søkeikon"
[icon_search]
other = "Ikon for å åpne søk"
[recent_notes]
other = "Nylige notater"
[first_3_notes]
other = "første 3 {{ .notes }}"
[search_for_something]
other = "Søk etter noe..."
[toc]
other = "Innholdsfortegnelse"
[copyright]
other = "Opprettet av {{ .name }} ved hjelp av <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Hey! Biraz kaybolmuş görünüyorsun. Bu sayfa mevcut değil (veya özel olabilir)."
[404_back]
other = "↳ Seni eve götürelim."
[all_posts]
other = "Hepsi {{.Title}}"
[last_updated]
other = "Son güncelleme"
[notes_count]
other = "Bu etikete sahip notlar"
[first_10]
other = "İlk 10 sonuç gösteriliyor"
[tag]
other = "Etiket"
[backlinks]
other = "Geri bağlantılar"
[no_backlinks]
other = "Geri bağlantı bulunamadı"
[home]
other = "Ev"
[light_mode]
other = "Aydınlık Modu"
[dark_mode]
other = "Karanlık Modu"
[edit_source]
other = "Kaynağı Düzenle"
[interactive_graph]
other = "Etkileşimli Grafik"
[search]
other = "Ara"
[search_icon]
other = "Arama Simgesi"
[icon_search]
other = "Aramayı açmak için simge tıklayın"
[recent_notes]
other = "Son Notlar"
[first_3_notes]
other = "İlk Üç {{ .notes }}"
[search_for_something]
other = "Bir şey ara..."
[toc]
other = "İçindekiler"
[copyright]
other = "{{ .name }} tarafından <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a> kullanılarak yapılmıştır, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "Хей! Виглядаєте здивовано. Цієї сторінки не існує (або вона приватна)."
[404_back]
other = "↳ Повернемося додому."
[all_posts]
other = "Всі {{.Title}}"
[last_updated]
other = "Оновлено"
[notes_count]
other = "нонаток з цим тегом"
[first_10]
other = "показано 10 перших результатів"
[tag]
other = "Тег"
[backlinks]
other = "Зворотнє посилання"
[no_backlinks]
other = "Зворотних посилань не знайдено"
[home]
other = "Дім"
[light_mode]
other = "Світлий Режим"
[dark_mode]
other = "Темний Режим"
[edit_source]
other = "Редагувати Джерело"
[interactive_graph]
other = "Інтерактивний граф"
[search]
other = "Пошук"
[search_icon]
other = "Іконка Пошуку"
[icon_search]
other = "Іконка для відкриття пошуку"
[recent_notes]
other = "Нещодавні Нотатки"
[first_3_notes]
other = "перші 3 {{ .notes }}"
[search_for_something]
other = "Знайти щось..."
[toc]
other = "Зміст"
[copyright]
other = "Створено {{ .name }} з використанням <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a>, © {{ .year }}"

View File

@ -1,65 +0,0 @@
[404_message]
other = "喔哦...... 你是不是迷路了呀..... (⌯' '⌯ ) 这个页面并不存在(也许它还未被发布)。"
[404_back]
other = "↳ 回到主页"
[all_posts]
other = "所有 {{.Title}}"
[last_updated]
other = "最后更新于"
[notes_count]
other = "带有此标签的笔记"
[first_10]
other = "正在展示前10个结果"
[tag]
other = "标签"
[backlinks]
other = "反向链接"
[no_backlinks]
other = "没有找到反向链接"
[home]
other = "主页"
[light_mode]
other = "明亮模式"
[dark_mode]
other = "黑暗模式"
[edit_source]
other = "编辑源码"
[interactive_graph]
other = "互动图"
[search]
other = "搜索"
[search_icon]
other = "搜索图标"
[icon_search]
other = "打开搜索图标"
[recent_notes]
other = "近期笔记"
[first_3_notes]
other = "前3个 {{ .notes }}"
[search_for_something]
other = "进行搜索......"
[toc]
other = "目录"
[copyright]
other = "由 {{ .name }} 用 <a href=\"https://github.com/jackyzha0/quartz\">Quartz</a> 创造, © {{ .year }}"

11
index.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare module "*.scss" {
const content: string
export = content
}
// dom custom event
interface CustomEventMap {
nav: CustomEvent<{ url: FullSlug }>
}
declare const fetchData: Promise<ContentIndex>

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
<div class="singlePage">
{{partial "darkmode.html" .}}
<div class="centered">
<h1>404.</h1>
<h3>{{ i18n "404_message" }}</h3>
<a href="{{ .Site.BaseURL }}">{{ i18n "404_back" }}</a>
</div>
</div>
</body>
</html>

View File

@ -1,4 +0,0 @@
<div class="mermaid">
{{- .Inner | safeHTML }}
</div>
{{ .Page.Store.Set "hasMermaid" true }}

View File

@ -1,9 +0,0 @@
{{$src := .Destination | safeURL }}
{{$width := index (split .Text "|") 1 | default "auto" }}
{{$external := strings.HasPrefix $src "http" }}
{{- if $external -}}
<img src="{{ $src }}" width="{{ $width }}" alt="{{ .Text }}" {{ with .Title }} title="{{ . }}" {{ end }} />
{{- else -}}
{{$fixedUrl := (cond (hasPrefix $src "/") $src (print "/" $src)) | urlize}}
<img src="{{.Page.Site.BaseURL}}{{ $fixedUrl }}" width="{{ $width }}" alt="{{ .Text }}" {{ with .Title }} title="{{ . }}" {{ end }} />
{{- end -}}

View File

@ -1,16 +0,0 @@
{{$dashedurl := replace .Destination "%20" "-" }}
{{$external := strings.HasPrefix $dashedurl "http" }}
{{- if $external -}}
<a href="{{ $dashedurl }}" rel="noopener">{{ .Text | safeHTML }}</a>
{{- else -}}
{{$trimmed := strings.TrimSuffix ".md" (.Destination | safeURL)}}
{{$spacedurl := replace $trimmed "%20" " " }}
{{$fixedUrl := (cond (hasPrefix $spacedurl "/") $spacedurl (print "/" $spacedurl)) | urlize}}
{{$nonexistent := eq (.Page.GetPage $spacedurl).RelPermalink ""}}
{{$rooted := default $spacedurl ((.Page.GetPage $spacedurl).RelPermalink) }}
<a
{{if not $nonexistent}}href="{{$rooted}}"{{end}}
rel="noopener" class="internal-link{{if $nonexistent}} broken{{end}}"
data-src="{{$rooted}}">{{- .Text | safeHTML -}}
</a>
{{- end -}}

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ block "head" . }}
{{ end }}
<body>
{{ block "main" . }}
{{ end }}
</body>
</html>

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
{{partial "header.html" .}}
<article>
<h1>{{ i18n "all_posts" . }}</h1>
{{with .Params.description}}
<p>{{.}}</p>
{{end}}
{{partial "page-list.html" .Paginator.Pages.ByLastmod.Reverse }}
{{ template "_internal/pagination.html" .}}
</article>
{{partial "contact.html" .}}
</div>
</body>
</html>

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
{{partial "header.html" .}}
<article>
{{if .Title}}<h1>{{ .Title }}</h1>{{end}}
<p class="meta">
{{ i18n "last_updated" }} {{ partial "date-fmt.html" .}}
{{ partial "github.html" . }}
</p>
<ul class="tags">
{{ range (.GetTerms "tags") }}
<li><a href="{{ .Permalink }}">
{{if (eq $.Site.Language.Lang "en")}}
{{ .LinkTitle | humanize }}
{{else}}
{{ .LinkTitle }}
{{end}}
</a>
</li>
{{ end }}
</ul>
{{partial "toc.html" .}}
{{partial "textprocessing.html" . }}
</article>
{{partial "footer.html" .}}
</div>
</body>
</html>

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
{{partial "header.html" .}}
<article>
<h1>{{ i18n "all_posts" . }}</h1>
{{with .Params.description}}
<p>{{.}}</p>
{{end}}
<div class="tags">
{{ range .Site.Taxonomies.tags.ByCount }}
<div class="meta">
<h1><a href="{{ .Page.Permalink }}">{{ .Page.Title | humanize }}</a></h1>
<p><b>{{ .Count }}</b> {{ i18n "notes_count" }} {{if gt .Count 10}}({{ i18n "first_10"}}){{end}}</p>
</div>
{{ with ($.Site.GetPage (printf "/tags/%s" .Page.Title)) }}
{{partial "page-list.html" (first 10 .Pages.ByLastmod.Reverse)}}
{{ end }}
{{ end }}
</div>
</article>
{{partial "contact.html" .}}
</div>
</body>
</html>

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
{{partial "header.html" .}}
<article>
<h1>{{ i18n "tag" }}: {{ .Title }}</h1>
{{with .Params.description}}
<p>{{.}}</p>
{{end}}
{{partial "page-list.html" .Paginator.Pages}}
{{ template "_internal/pagination.html" . }}
</article>
{{partial "contact.html" .}}
</div>
</body>
</html>

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="{{ .Lang }}">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
{{partial "header.html" .}}
<article>
{{partial "toc.html" .}}
{{partial "textprocessing.html" . }}
{{if $.Site.Data.config.enableRecentNotes}}
{{partial "recent.html" . }}
{{end}}
</article>
{{partial "footerIndex.html" .}}
</div>
</body>
</html>

View File

@ -1,30 +0,0 @@
<h3>{{ i18n "backlinks" }}</h3>
<ul class="backlinks">
{{$url := urls.Parse .Site.BaseURL }}
{{$host := strings.TrimRight "/" $url.Path }}
{{$curPage := strings.TrimPrefix $host (strings.TrimRight "/" .Page.RelPermalink)}}
{{$linkIndex := getJSON "/assets/indices/linkIndex.json"}}
{{$inbound := index $linkIndex.index.backlinks $curPage}}
{{$contentTable := getJSON "/assets/indices/contentIndex.json"}}
{{if $inbound}}
{{$backlinks := dict "SENTINEL" "SENTINEL"}}
{{range $k, $v := $inbound}}
{{$cleanedInbound := replace $v.source " " "-"}}
{{$ctx := $v.text}}
{{$backlinks = merge $backlinks (dict $cleanedInbound $ctx)}}
{{end}}
{{- range $lnk, $ctx := $backlinks -}}
{{$l := printf "%s%s/" $host $lnk}}
{{$l = cond (eq $l "//") "/" $l}}
{{with (index $contentTable $lnk)}}
<li>
<a href="{{$l}}" data-ctx="{{$ctx}}" data-src="{{$lnk}}" class="internal-link">{{index (index . "title")}}</a>
</li>
{{end}}
{{- end -}}
{{else}}
<li>
{{ i18n "no_backlinks" }}
</li>
{{end}}
</ul>

View File

@ -1,19 +0,0 @@
<!-- set data/config yaml file based on website language -->
{{ $config := cond (eq $.Site.Language.Lang "en") "config" (printf "config.%s" $.Site.Language.Lang) }}
{{ $data := index $.Site.Data $config }}
<!-- Contact Info -->
<div id="contact_buttons">
<footer>
{{ $name := $data.name | default $.Site.Data.config.name }}
{{ $year := dateFormat "2006" now }}
<p>{{ i18n "copyright" (dict "name" $name "year" $year) | safeHTML}}</p>
<ul>
{{ if not .IsHome }}
<li><a href="{{ $.Site.BaseURL}}">{{ i18n "home" }}</a></li>
{{end}}
{{- range $data.links | default $.Site.Data.config.links -}}
<li><a href="{{.link}}">{{.link_name}}</a></li>
{{- end -}}
</ul>
</footer>
</div>

View File

@ -1,15 +0,0 @@
<div class='darkmode'>
<input class='toggle' id='darkmode-toggle' type='checkbox' tabindex="-1">
<label id="toggle-label-light" for='darkmode-toggle' tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="dayIcon" x="0px" y="0px" viewBox="0 0 35 35" style="enable-background:new 0 0 35 35;" xml:space="preserve">
<title>{{ i18n "light_mode" }}</title>
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z" />
</svg>
</label>
<label id="toggle-label-dark" for='darkmode-toggle' tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="nightIcon" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background='new 0 0 100 100'" xml:space="preserve">
<title>{{ i18n "dark_mode" }}</title>
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z" />
</svg>
</label>
</div>

View File

@ -1,7 +0,0 @@
{{if .Date}}
{{.Date.Format "Jan 2, 2006"}}
{{else if .Lastmod}}
{{.Lastmod.Format "Jan 2, 2006"}}
{{else}}
Unknown
{{end}}

View File

@ -1,16 +0,0 @@
<hr/>
{{if $.Site.Data.config.enableFooter}}
<div class="page-end" id="footer">
<div class="backlinks-container">
{{partial "backlinks.html" .}}
</div>
<div>
{{partial "graph.html" .}}
</div>
</div>
{{end}}
{{partial "contact.html" .}}

View File

@ -1,24 +0,0 @@
{{if $.Site.Data.config.enableFooter}}
{{if $.Site.Data.graphConfig.enableGlobalGraph}}
<div class="page-end" id="footer">
<div>
{{partial "graph.html" .}}
</div>
</div>
{{else}}
<hr/>
<div class="page-end" id="footer">
<div class="backlinks-container">
{{partial "backlinks.html" .}}
</div>
<div>
{{partial "graph.html" .}}
</div>
</div>
{{end}}
{{end}}
{{partial "contact.html" .}}

View File

@ -1,3 +0,0 @@
{{if $.Site.Data.config.enableGitHubEdit}}
<a href="{{$.Site.Data.config.GitHubLink}}/{{.File.Path}}" rel="noopener">{{ i18n "edit_source" }}</a>
{{end}}

View File

@ -1,18 +0,0 @@
<script
src="https://cdn.jsdelivr.net/npm/d3@6.7.0/dist/d3.min.js"
integrity="sha256-+7jaYCp29O1JusNWHaYtgUn6EhuP0VaFuswhNV06MyI="
crossorigin="anonymous"
></script>
<h3>{{ i18n "interactive_graph" }}</h3>
<div id="graph-container"></div>
<style>
:root {
--g-node: var(--secondary);
--g-node-active: var(--primary);
--g-node-inactive: var(--visited);
--g-link: var(--outlinegray);
--g-link-active: #5a7282;
}
</style>
{{ $js := resources.Get "js/graph.js" | resources.Fingerprint "md5" }}
<script src="{{ $js.Permalink }}"></script>

View File

@ -1,252 +0,0 @@
<!-- set data/config yaml file based on website language -->
{{ $config := cond (eq $.Site.Language.Lang "en") "config" (printf "config.%s" $.Site.Language.Lang) }}
{{ $data := index $.Site.Data $config }}
<head>
<!-- Meta tags -->
<meta charset="UTF-8" />
<meta
name="description"
content="{{if .IsHome}}{{$data.description | default $.Site.Data.config.description}}{{else}}{{.Summary}}{{end}}"
/>
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{if .IsHome}}{{$data.description | default $.Site.Data.config.description}}{{else}}{{.Summary}}{{end}}">
<meta property="og:type" content="website">
<meta property="og:image" content="{{.Site.BaseURL}}icon.png">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:width" content="200">
<meta property="og:height" content="200">
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ .Title }}" />
<meta name="twitter:description" content="{{if .IsHome}}{{$data.description | default $.Site.Data.config.description}}{{else}}{{.Summary}}{{end}}" />
<meta name="twitter:image" content="{{.Site.BaseURL}}icon.png">
{{ range $data.links }}
{{ if strings.Contains .link "twitter.com" }}
{{ $twitter_handle := index (split .link "/") (sub (len (split .link "/")) 1) }}
<meta name="twitter:site" content="{{ $twitter_handle }}" />
{{ end }}
{{ end }}
<title>
{{ if .Title }}{{ .Title }}{{ else }}{{ $data.page_title | default $.Site.Data.config.page_title }}{{
end }}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- HTML Favicon -->
{{ $favicon := $data.favicon | default $.Site.Data.config.favicon | default (slice (dict "rel" "shortcut icon" "type" "image/png" "href" "icon.png")) }}
{{ $type := (printf "%T" $favicon) }}
{{ if eq $type "string" }}
{{ $favicon | safeHTML }}
{{ else }}
{{ range $favicon }}
<link rel="{{.rel}}" {{if .type}}type="{{.type}}"{{end}} {{if .sizes}}sizes="{{.sizes}}"{{end}} href="{{$.Site.BaseURL}}/{{.href}}" />
{{- end }}
{{ end }}
<!-- CSS Stylesheets and Fonts -->
{{$sass := resources.Match "styles/[!_]*.scss" }}
{{$css := slice }}
{{range $sass}}
{{$scss := . | resources.ToCSS (dict "outputStyle" "compressed") }}
{{$css = $css | append $scss}}
{{end}}
{{if $data.enableCallouts | default $.Site.Data.config.enableCallouts}}
{{$scss := resources.Get "styles/_callouts.scss" | resources.ToCSS (dict "outputStyle" "compressed") }}
{{$css = $css | append $scss}}
{{end}}
{{$finalCss := $css | resources.Concat "styles.css" | resources.Fingerprint "md5" | resources.Minify }}
<link href="{{$finalCss.Permalink}}" rel="stylesheet" />
{{$lightSyntax := resources.Get "styles/_light_syntax.scss" | resources.ToCSS (dict "outputStyle" "compressed") | resources.Fingerprint "md5" | resources.Minify }}
<link href="{{$lightSyntax.Permalink}}" rel="stylesheet" id="theme-link">
<!-- Base scripts -->
{{$scripts := (slice "js/darkmode.js" "js/util.js")}}
{{range $scripts}}
{{$scriptname := .}}
{{ $s := resources.Get $scriptname | resources.ExecuteAsTemplate $scriptname . | resources.Fingerprint "md5" | resources.Minify }}
<script src="{{$s.Permalink}}"></script>
{{end}}
{{partial "katex.html" .}}
{{partial "mermaid.html" .}}
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/core@1.2.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1"></script>
{{ $popover := resources.Get "js/popover.js" | resources.Fingerprint "md5" |
resources.Minify }}
<script defer src="{{$popover.Permalink}}"></script>
<!-- Optional scripts -->
{{ if $data.enableCodeBlockTitle | default $.Site.Data.config.enableCallouts }}
{{ $codeTitle := resources.Get "js/code-title.js" | resources.Fingerprint "md5" | resources.Minify }}
<script defer src="{{$codeTitle.Permalink}}"></script>
{{end}}
{{ if $data.enableCodeBlockCopy | default $.Site.Data.config.enableCodeBlockCopy }}
{{ $clipboard := resources.Get "js/clipboard.js" | resources.Fingerprint "md5" | resources.Minify }}
<script defer src="{{$clipboard.Permalink}}"></script>
{{ end }}
{{ if $data.enableCallouts | default $.Site.Data.config.enableCallouts }}
{{ $callouts := resources.Get "js/callouts.js" | resources.Fingerprint "md5" | resources.Minify }}
<script defer src="{{$callouts.Permalink}}"></script>
{{ end }}
<!-- Preload page vars -->
{{$linkIndex := resources.Get "indices/linkIndex.json" | resources.Fingerprint
"md5" | resources.Minify | }} {{$contentIndex := resources.Get
"indices/contentIndex.json" | resources.Fingerprint "md5" | resources.Minify
}}
<script>
const SEARCH_ENABLED = {{.Site.Data.config.search.enableSemanticSearch}}
const LATEX_ENABLED = {{.Site.Data.config.enableLatex}}
const PRODUCTION = {{ hugo.IsProduction }}
const BASE_URL = {{.Site.BaseURL}}
const fetchData = Promise.all([
fetch("{{ $linkIndex.Permalink }}")
.then(data => data.json())
.then(data => ({
index: data.index,
links: data.links,
})),
fetch("{{ $contentIndex.Permalink }}")
.then(data => data.json()),
])
.then(([{index, links}, content]) => ({
index,
links,
content,
}))
const render = () => {
// NOTE: everything within this callback will be executed for every page navigation. This is a good place to put JavaScript that loads or modifies data on the page, adds event listeners, etc. If you are only dealing with basic DOM replacement, use the init function
const siteBaseURL = new URL(BASE_URL);
const pathBase = siteBaseURL.pathname;
const pathWindow = window.location.pathname;
const isHome = pathBase == pathWindow;
{{if $data.enableCodeBlockCopy | default $.Site.Data.config.enableCodeBlockCopy -}}
addCopyButtons();
{{ end }}
{{if $data.enableSPA | default $.Site.Data.config.enableSPA -}}
addTitleToCodeBlocks();
{{ end }}
{{if $data.enableCallouts | default $.Site.Data.config.enableCallouts -}}
addCollapsibleCallouts();
{{ end }}
{{if $data.enableLinkPreview | default $.Site.Data.config.enableLinkPreview}}
initPopover(
{{strings.TrimRight "/" .Site.BaseURL }},
{{$data.enableContextualBacklinks | default $.Site.Data.config.enableContextualBacklinks}}
)
{{end}}
{{if $data.enableFooter | default $.Site.Data.config.enableFooter}}
const footer = document.getElementById("footer")
if (footer) {
const container = document.getElementById("graph-container")
// retry if the graph is not ready
if (!container) return requestAnimationFrame(render)
// clear the graph in case there is anything within it
container.textContent = ""
const drawGlobal = isHome && {{$.Site.Data.graphConfig.enableGlobalGraph}};
drawGraph(
{{strings.TrimRight "/" .Site.BaseURL}},
drawGlobal,
{{$.Site.Data.graphConfig.paths}},
drawGlobal ? {{$.Site.Data.graphConfig.globalGraph}} : {{$.Site.Data.graphConfig.localGraph}}
);
}
{{end}}
{{if $data.enableMermaid | default $.Site.Data.config.enableMermaid}}
var els = document.getElementsByClassName("mermaid");
if (els.length > 0) {
import('https://unpkg.com/mermaid@9/dist/mermaid.esm.min.mjs').then(
(obj) => {
// init forces mermaid to render mermaid markdown without waiting
// for DOMContentLoaded event
obj.default.init();
}
)
}
{{end}}
// analytics
function clickHandler(evt) {
const target = evt.target
const classNames = target.className.split(" ")
const broken = classNames.includes("broken")
const internal = classNames.includes("internal-link")
plausible("Link Click", {
props: {
href: target.href,
broken,
internal,
graph: false,
}
})
}
const links = document.querySelectorAll("a")
for (link of links) {
if (link.className.includes("root-title")) {
link.addEventListener('click', clickHandler, {once: true})
}
}
}
const init = (doc = document) => {
// NOTE: everything within this callback will be executed for initial page navigation. This is a good place to put JavaScript that only replaces DOM nodes.
{{if $data.enableCodeBlockCopy | default $.Site.Data.config.enableCodeBlockCopy -}}
addCopyButtons();
{{ end }}
{{if $data.enableCodeBlockTitle | default $.Site.Data.config.enableCodeBlockTitle -}}
addTitleToCodeBlocks();
{{- end -}}
{{if $data.enableLatex | default $.Site.Data.config.enableLatex}}
renderMathInElement(doc.body, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
],
macros: {
'': "'"
},
throwOnError : false
});
{{end}}
};
</script>
{{if $data.enableSPA | default $.Site.Data.config.enableSPA}}
{{ $router := resources.Get "js/router.js" | resources.Fingerprint "md5" |
resources.Minify }}
<script type="module">
import { attachSPARouting } from "{{$router.Permalink}}"
attachSPARouting(init, render)
</script>
{{else}}
<script>
window.Million = {
navigate: (url) => (window.location.href = url),
prefetch: () => {},
}
window.addEventListener("DOMContentLoaded", () => {
init()
render()
})
</script>
{{end}}
{{ $trimmedURL := trim (index (split .Site.BaseURL "://") 1) "/" }}
<script defer data-domain="{{$trimmedURL}}" src="https://plausible.io/js/script.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
</head>

View File

@ -1,11 +0,0 @@
<header>
{{ $config := cond (eq $.Site.Language.Lang "en") "config" (printf "config.%s" $.Site.Language.Lang) }}
<h1 id="page-title"><a class="root-title" href="{{ "" | absLangURL }}">{{ ( index $.Site.Data $config ).page_title | default $.Site.Data.config.page_title }}</a></h1>
<div class="spacer"></div>
<div id="search-icon">
<p>{{ i18n "search" }}</p>
<svg tabindex="0" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">{{ i18n "search_icon" }}</title><desc id="desc">{{ i18n "icon_search" }}</desc><g class="search-path" fill="none"><path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4"/><circle cx="8" cy="8" r="7"/></g></svg>
</div>
{{partial "darkmode.html" .}}
</header>

View File

@ -1,14 +0,0 @@
{{if $.Site.Data.config.enableLatex}}
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.css" as="style"
onload="this.onload=null;this.rel='stylesheet'"
integrity="sha384-R4558gYOUz8mP9YWpZJjofhk+zx0AS11p36HnD2ZKj/6JR5z27gSSULCNHIRReVs" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.js"
integrity="sha384-z1fJDqw8ZApjGO3/unPWUPsIymfsJmyrDVWC8Tv/a1HeOtGmkwNd/7xUS0Xcnvsx"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/auto-render.min.js"
integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/contrib/copy-tex.min.js"
integrity="sha384-ww/583aHhxWkz5DEVn6OKtNiIaLi2iBRNZXfJRiY1Ai7tnJ9UXpEsyvOITVpTl4A"
crossorigin="anonymous"></script>
{{end}}

View File

@ -1,8 +0,0 @@
{{if $.Site.Data.config.enableMermaid}}
{{ if .Page.Store.Get "hasMermaid" }}
<script type="module">
import mermaid from 'https://unpkg.com/mermaid@9/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
{{ end }}
{{ end }}

View File

@ -1,20 +0,0 @@
<ul class="section-ul">
{{- range . -}}
<li class="section-li">
<div class="section">
<p class="meta">
{{partial "date-fmt.html" .}}
</p>
<div class="desc">
<h3><a href="{{ .Permalink }}" class="internal-link" data-src="{{ .RelPermalink }}">{{- .Title -}}</a></h3>
</div>
<div class="spacer"></div>
<ul class="tags">
{{ range (.GetTerms "tags") }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle | title}}</a></li>
{{ end }}
</ul>
</div>
</li>
{{- end -}}
</ul>

View File

@ -1,12 +0,0 @@
<div class="content-list">
<h2>{{ i18n "recent_notes" }}</h2>
<!--
You can also configure this to find related pages!
All you need to pass into the "page-list.html" partial
is a collection of pages.
https://gohugo.io/content-management/related/
-->
{{$notes := .Site.RegularPages.ByLastmod.Reverse}}
{{partial "page-list.html" (first 3 $notes)}}
</div>

View File

@ -1,18 +0,0 @@
<div id="search-container">
<div id="search-space">
<input autocomplete="off" id="search-bar" name="search" type="text" aria-label="{{ i18n "search" }}"
placeholder="{{ i18n "search_for_something" }}" dir="{{ $.Site.Language.LanguageDirection }}">
<div id="results-container">
</div>
</div>
</div>
{{if $.Site.Data.config.search.enableSemanticSearch}}
{{ $js := resources.Get "js/semantic-search.js" | resources.ExecuteAsTemplate "js/semantic-search.js" . | resources.Fingerprint "md5" | resources.Minify }}
<script defer type="module" src="{{ $js.Permalink }}"></script>
{{else}}
<script src="https://cdn.jsdelivr.net/npm/flexsearch@0.7.21/dist/flexsearch.bundle.js"
integrity="sha256-i3A0NZGkhsKjVMzFxv3ksk0DZh3aXqu0l49Bbh0MdjE=" crossorigin="anonymous" defer></script>
{{ $js := resources.Get "js/full-text-search.js" | resources.Fingerprint "md5" | resources.Minify }}
<script defer src="{{ $js.Permalink }}"></script>
{{end}}

View File

@ -1,162 +0,0 @@
{{ $content := .Content }}
{{ $raw := .RawContent }}
{{ $page := .Page }}
{{/* Escape slashes for Latex to fix line breaks */}}
{{$latex := findRE "(?:\\${2}([^\\$]+)\\${2})|(?:\\$([^\\$]*)\\$)" $content}}
{{range $latex}}
{{$fixed := replaceRE "\\\\(?: +|\\n)" "\\\\ " .}}
{{$content = replace $content . $fixed}}
{{end}}
{{/* Wikilinks */}}
{{$wikilinks := $content | findRE "!?\\[\\[\\S[^\\[\\]\\|]*(?:\\|[^\\[\\]]*)?\\S\\]\\]" }}
{{$codefences := $raw | findRE "\\x60[^\\x60\\n]+\\x60"}}
{{$codeblocks := $raw | findRE "\\x60{3}[^\\x60]+\\x60{3}"}}
{{$code := union $codefences $codeblocks}}
{{range $wikilinks}}
{{$cur := .}}
{{$incode := false}}
{{range $code}}
{{if (in . $cur)}}
{{$incode = true}}
{{end}}
{{end}}
{{if not $incode}}
<!-- remove link delimiters -->
{{$inner := . | strings.TrimPrefix "!" | strings.TrimPrefix "[[" | strings.TrimSuffix "]]" }}
<!-- split from alias -->
{{$split := split $inner "|"}}
<!-- separate link path -->
{{$path := index $split 0}}
{{$reference := split $path "#"}}
<!-- path with heading link removed -->
{{$title := index $reference 0}}
<!-- $display is hyperlink display text -->
<!-- use alias, else title -->
{{$display := default $title (index $split 1)}}
<!-- remove subfolder from title -->
{{$display := index (last 1 (split $display "/")) 0}}
<!-- attempt to get title -->
{{$searchtitle := $title }}
{{$curpage := $page.GetPage $searchtitle }}
<!-- attempt to search md file instead -->
{{ if (eq $curpage.String "nopPage") }}
{{$searchtitle = (add $title ".md") }}
{{$curpage = $page.GetPage $searchtitle }}
{{ end }}
<!-- attempt to reverse typographer behaviour -->
{{ if (eq $curpage.String "nopPage") }}
{{$searchtitle = (replace $searchtitle "&amp;" "&") }}
{{$searchtitle = (replace $searchtitle "&quot;" "\"") }}
{{$searchtitle = (replace $searchtitle "&rdquo;" "\"") }}
{{$searchtitle = (replace $searchtitle "&ldquo;" "\"") }}
{{$searchtitle = (replace $searchtitle "&rsquo;" "'") }}
{{$searchtitle = (replace $searchtitle "&lsquo;" "'") }}
{{$curpage = $page.GetPage $searchtitle }}
{{ end }}
{{$relpath := relURL $path}}
<!-- If path to Hugo page -->
{{if not (eq $curpage.String "nopPage") }}
{{$block := default "" (index $reference 1)}}
{{$block = strings.TrimRight "/" (cond (eq $block "") $block (printf "#%s" $block)) | urlize | lower}}
{{$href := strings.TrimRight "/" $curpage.RelPermalink}}
{{$link := printf "<a href=\"%s%s\" rel=\"noopener\" class=\"internal-link\" data-src=\"%s\">%s</a>" $href $block $href $display}}
{{$content = replace $content . $link}}
<!-- If path to existing file -->
{{else if fileExists $relpath}}
{{$splitpath := split $relpath "/"}}
{{$dirname := first (sub (len $splitpath) 1) $splitpath | path.Join | urlize}}
{{$basename := index (last 1 $splitpath) 0}}
{{$href := printf "/%s/%s" $dirname $basename}}
<!-- Embedded? -->
{{if (hasPrefix . "!")}}
{{ $embed_ext := lower (path.Ext $href) }}
<!-- Image -->
{{if in ".png .jpg .jpeg .gif .bmp .svg" $embed_ext }}
{{$width := default "auto" (index $split 1) }}
{{$link := printf "<img src=\"%s\" width=\"%s\" />" $href $width}}
{{$content = replace $content . $link}}
<!-- Video -->
{{else if in ".mp4 .webm .ogv .mov .mkv" $embed_ext}}
{{$link := printf "<video src=\"%s\" style=\"width: -webkit-fill-available;\" controls></video>" $href}}
{{$content = replace $content . $link}}
<!-- Audio -->
{{else if in ".mp3 .webm .wav .m4a .ogg .3gp .flac" $embed_ext}}
{{$link := printf "<audio src=\"%s\" controls></audio>" $href}}
{{$content = replace $content . $link}}
<!-- PDF -->
{{else if in ".pdf" $embed_ext }}
{{$src_link := printf "<a href=\"%s\" rel=\"noopener\" class=\"internal-link\">[source]</a>" $href}}
{{$iframe_link := printf "<iframe src=\"%s\" style=\"height: -webkit-fill-available; width: -webkit-fill-available;\"></iframe>" $href}}
{{$link := printf "%s<br>%s" $src_link $iframe_link}}
{{$content = replace $content . $link}}
<!-- other -->
{{else}}
{{$link := printf "<a href=\"%s\" rel=\"noopener\" class=\"internal-link\">%s</a>" $href $href}}
{{$content = replace $content . $link}}
{{end}}
{{else}}
{{$link := printf "<a href=\"%s\" rel=\"noopener\" class=\"internal-link\">%s</a>" $href $display}}
{{$content = replace $content . $link}}
{{end}}
<!-- Broken path -->
{{else}}
{{$link := printf "<a class=\"internal-link broken\">%s</a>" $display}}
{{$content = replace $content . $link}}
{{end}}
{{end}}
{{end}}
{{/* Add jumpable anchors */}}
{{ $content = $content | replaceRE "(<h[1-9] id=\"([^\"]+)\">)(.+)(</h[1-9]>)" `<a href="#${2}">${1}<span class="hanchor" ariaLabel="Anchor"># </span>${3}${4}</a>` }}
{{/* Callouts */}}
{{if $.Site.Data.config.enableCallouts}}
{{ $content = $content | replaceRE "<blockquote>" "<blockquote class=callout>" }}
{{ $blockquoteclasses := findRE `\[!.+\]` $content }}
{{ $blockquoteclasses1 := findRE "<blockquote.*?>(.|\n)*?</blockquote>" $content }}
{{ $blockquotetags := findRE `blockquote class=callout` $content }}
{{ $counter := 0 }}
{{ $counter1 := 0 }}
{{ $finder := index $blockquoteclasses1 $counter }}
{{range $blockquotetags}}
{{ $finder = index $blockquoteclasses1 $counter }}
{{ if (in $finder "[!") }}
{{ $inner := index $blockquoteclasses $counter1 }}
{{ if (in $finder "]-") }}
{{ $inner = $inner | replaceRE `\[!([a-zA-Z]+)\]` `callout-collapsible callout-collapsed ${1}`}}
{{ else if (in $finder "]+") }}
{{ $inner = $inner | replaceRE `\[!([a-zA-Z]+)\]` `callout-collapsible ${1}`}}
{{ else}}
{{ $inner = $inner | replaceRE `\[!([a-zA-Z]+)\]` `${1}` }}
{{ end }}
{{ $inner = printf "blockquote class=\"%s-callout\"" $inner | lower}}
{{ $content = replace $content . $inner 1}}
{{ $counter1 = add $counter1 1 }}
{{ else }}
{{ $inner := print "blockquote" }}
{{ $content = replace $content . $inner 1}}
{{ end }}
{{ $counter = add $counter 1 }}
{{end}}
{{ $content = $content | replaceRE `\[![a-zA-Z]+\][-\+]?` "" }}
{{ $content = $content | replaceRE "blockquote class=callout" "blockquote" }}
{{ $content = $content | replaceRE `(?s)(<blockquote class="\S+-callout">.*?)<br>(.*?<\/blockquote)` `${1}</p><p>${2}` }}
{{end}}
{{/* Make ==text== into <mark>text</mark> */}}
{{$mark := findRE "==([^=\n]+)==" $content}}
{{range $mark}}
{{$fixed := printf "<mark>%s</mark>" (replace . "==" "")}}
{{$content = replace $content . $fixed}}
{{end}}
{{ $content | safeHTML }}

View File

@ -1,9 +0,0 @@
{{ $hasHeaders := gt (len (findRE "<h\\d.*?>(.|\n)*?</h\\d>" .Content)) 0 }}
{{ if (or (and (not $.Site.Data.config.enableToc) .Params.enableToc) (and $.Site.Data.config.enableToc (ne .Params.enableToc false) $hasHeaders)) }}
<aside class="mainTOC">
<details {{ if $.Site.Data.config.openToc }}open {{ end }}>
<summary>{{ i18n "toc" }}</summary>
{{ .TableOfContents }}
</details>
</aside>
{{end}}

6162
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

105
package.json Normal file
View File

@ -0,0 +1,105 @@
{
"name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website",
"private": true,
"version": "4.0.8",
"type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT",
"homepage": "https://quartz.jzhao.xyz",
"repository": {
"type": "git",
"url": "https://github.com/jackyzha0/quartz.git"
},
"scripts": {
"check": "tsc --noEmit && npx prettier . --check",
"format": "npx prettier . --write",
"test": "tsx ./quartz/util/path.test.ts",
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
},
"engines": {
"node": ">=18.14"
},
"keywords": [
"site generator",
"ssg",
"digital-garden",
"markdown",
"blog",
"quartz"
],
"bin": {
"quartz": "./quartz/bootstrap-cli.mjs"
},
"dependencies": {
"@clack/prompts": "^0.6.3",
"@floating-ui/dom": "^1.4.0",
"@napi-rs/simple-git": "^0.1.8",
"async-mutex": "^0.4.0",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"cli-spinner": "^0.2.10",
"d3": "^7.8.5",
"esbuild-sass-plugin": "^2.9.0",
"flexsearch": "0.7.21",
"github-slugger": "^2.0.0",
"globby": "^13.1.4",
"gray-matter": "^4.0.3",
"hast-util-to-html": "^8.0.4",
"hast-util-to-jsx-runtime": "^1.2.0",
"hast-util-to-string": "^2.0.0",
"is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0",
"lightningcss": "^1.21.5",
"mdast-util-find-and-replace": "^2.2.2",
"mdast-util-to-hast": "^12.3.0",
"mdast-util-to-string": "^3.2.0",
"micromorph": "^0.4.5",
"plausible-tracker": "^0.3.8",
"preact": "^10.14.1",
"preact-render-to-string": "^6.0.3",
"pretty-bytes": "^6.1.0",
"pretty-time": "^1.1.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-katex": "^6.0.3",
"rehype-mathjax": "^4.0.3",
"rehype-pretty-code": "^0.10.0",
"rehype-raw": "^6.1.1",
"rehype-slug": "^5.1.0",
"remark": "^14.0.2",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-smartypants": "^2.0.0",
"rimraf": "^5.0.1",
"serve-handler": "^6.1.5",
"source-map-support": "^0.5.21",
"to-vfile": "^7.2.4",
"unified": "^10.1.2",
"unist-util-visit": "^4.1.2",
"vfile": "^5.3.7",
"workerpool": "^6.4.0",
"ws": "^8.13.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/cli-spinner": "^0.2.1",
"@types/d3": "^7.4.0",
"@types/flexsearch": "^0.7.3",
"@types/hast": "^2.3.4",
"@types/js-yaml": "^4.0.5",
"@types/node": "^20.1.2",
"@types/pretty-time": "^1.1.2",
"@types/source-map-support": "^0.5.6",
"@types/workerpool": "^6.4.0",
"@types/ws": "^8.5.5",
"@types/yargs": "^17.0.24",
"esbuild": "^0.18.11",
"prettier": "^3.0.0",
"tsx": "^3.12.7",
"typescript": "^5.0.4"
}
}

75
quartz.config.ts Normal file
View File

@ -0,0 +1,75 @@
import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins"
const config: QuartzConfig = {
configuration: {
pageTitle: "Matsuura Tomoya Research Note",
enableSPA: true,
enablePopovers: true,
analytics: {
provider: "plausible",
},
baseUrl: "garden.matsuuratomoya.com",
ignorePatterns: ["private", "templates"],
theme: {
typography: {
header: "Schibsted Grotesk",
body: "Source Sans Pro",
code: "IBM Plex Mono",
},
colors: {
lightMode: {
light: "#faf8f8",
lightgray: "#e5e5e5",
gray: "#b8b8b8",
darkgray: "#4e4e4e",
dark: "#2b2b2b",
secondary: "#284b63",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
},
darkMode: {
light: "#161618",
lightgray: "#393639",
gray: "#646464",
darkgray: "#d4d4d4",
dark: "#ebebec",
secondary: "#7b97aa",
tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)",
},
},
},
},
plugins: {
transformers: [
Plugin.FrontMatter(),
Plugin.TableOfContents(),
Plugin.CreatedModifiedDate({
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
}),
Plugin.SyntaxHighlighting(),
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
Plugin.GitHubFlavoredMarkdown(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Latex({ renderEngine: "katex" }),
Plugin.Description(),
],
filters: [Plugin.RemoveDrafts()],
emitters: [
Plugin.AliasRedirects(),
Plugin.ComponentResources({ fontOrigin: "googleFonts" }),
Plugin.ContentPage(),
Plugin.FolderPage(),
Plugin.TagPage(),
Plugin.ContentIndex({
enableSiteMap: true,
enableRSS: true,
}),
Plugin.Assets(),
Plugin.Static(),
],
},
}
export default config

41
quartz.layout.ts Normal file
View File

@ -0,0 +1,41 @@
import { PageLayout, SharedLayout } from "./quartz/cfg"
import * as Component from "./quartz/components"
// components shared across all pages
export const sharedPageComponents: SharedLayout = {
head: Component.Head(),
header: [],
footer: Component.Footer({
links: {
"Top": "https://matsuuratomoya.com",
"Mastodon": "https://social.matsuuratomoya.com/@tomoya",
"Twitter": "https://twitter.com/tomoya_nonymous",
"GitHub": "https://github.com/tomoyanonymous",
},
}),
}
// components for pages that display a single page (e.g. a single note)
export const defaultContentPageLayout: PageLayout = {
beforeBody: [Component.ArticleTitle(), Component.ContentMeta(), Component.TagList()],
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(Component.TableOfContents()),
],
right: [Component.Graph(), Component.Backlinks()],
}
// components for pages that display lists of pages (e.g. tags or folders)
export const defaultListPageLayout: PageLayout = {
beforeBody: [Component.ArticleTitle()],
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
],
right: [],
}

540
quartz/bootstrap-cli.mjs Executable file
View File

@ -0,0 +1,540 @@
#!/usr/bin/env node
import { promises, readFileSync } from "fs"
import yargs from "yargs"
import path from "path"
import { hideBin } from "yargs/helpers"
import esbuild from "esbuild"
import chalk from "chalk"
import { sassPlugin } from "esbuild-sass-plugin"
import fs from "fs"
import { intro, isCancel, outro, select, text } from "@clack/prompts"
import { rimraf } from "rimraf"
import chokidar from "chokidar"
import prettyBytes from "pretty-bytes"
import { execSync, spawnSync } from "child_process"
import http from "http"
import serveHandler from "serve-handler"
import { WebSocketServer } from "ws"
import { randomUUID } from "crypto"
import { Mutex } from "async-mutex"
const ORIGIN_NAME = "origin"
const UPSTREAM_NAME = "upstream"
const QUARTZ_SOURCE_BRANCH = "v4"
const cwd = process.cwd()
const cacheDir = path.join(cwd, ".quartz-cache")
const cacheFile = "./.quartz-cache/transpiled-build.mjs"
const fp = "./quartz/build.ts"
const { version } = JSON.parse(readFileSync("./package.json").toString())
const contentCacheFolder = path.join(cacheDir, "content-cache")
const CommonArgv = {
directory: {
string: true,
alias: ["d"],
default: "content",
describe: "directory to look for content files",
},
verbose: {
boolean: true,
alias: ["v"],
default: false,
describe: "print out extra logging information",
},
}
const SyncArgv = {
...CommonArgv,
commit: {
boolean: true,
default: true,
describe: "create a git commit for your unsaved changes",
},
push: {
boolean: true,
default: true,
describe: "push updates to your Quartz fork",
},
pull: {
boolean: true,
default: true,
describe: "pull updates from your Quartz fork",
},
}
const BuildArgv = {
...CommonArgv,
output: {
string: true,
alias: ["o"],
default: "public",
describe: "output folder for files",
},
serve: {
boolean: true,
default: false,
describe: "run a local server to live-preview your Quartz",
},
baseDir: {
string: true,
default: "",
describe: "base path to serve your local server on",
},
port: {
number: true,
default: 8080,
describe: "port to serve Quartz on",
},
bundleInfo: {
boolean: true,
default: false,
describe: "show detailed bundle information",
},
concurrency: {
number: true,
describe: "how many threads to use to parse notes",
},
}
function escapePath(fp) {
return fp
.replace(/\\ /g, " ") // unescape spaces
.replace(/^".*"$/, "$1")
.replace(/^'.*"$/, "$1")
.trim()
}
function exitIfCancel(val) {
if (isCancel(val)) {
outro(chalk.red("Exiting"))
process.exit(0)
} else {
return val
}
}
async function stashContentFolder(contentFolder) {
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
await fs.promises.cp(contentFolder, contentCacheFolder, {
force: true,
recursive: true,
verbatimSymlinks: true,
preserveTimestamps: true,
})
await fs.promises.rm(contentFolder, { force: true, recursive: true })
}
async function popContentFolder(contentFolder) {
await fs.promises.rm(contentFolder, { force: true, recursive: true })
await fs.promises.cp(contentCacheFolder, contentFolder, {
force: true,
recursive: true,
verbatimSymlinks: true,
preserveTimestamps: true,
})
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
}
function gitPull(origin, branch) {
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
if (out.stderr) {
throw new Error(`Error while pulling updates: ${out.stderr}`)
}
}
yargs(hideBin(process.argv))
.scriptName("quartz")
.version(version)
.usage("$0 <cmd> [args]")
.command("create", "Initialize Quartz", CommonArgv, async (argv) => {
console.log()
intro(chalk.bgGreen.black(` Quartz v${version} `))
const contentFolder = path.join(cwd, argv.directory)
const setupStrategy = exitIfCancel(
await select({
message: `Choose how to initialize the content in \`${contentFolder}\``,
options: [
{ value: "new", label: "Empty Quartz" },
{ value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
{
value: "symlink",
label: "Symlink an existing folder",
hint: "don't select this unless you know what you are doing!",
},
{ value: "keep", label: "Keep the existing files" },
],
}),
)
async function rmContentFolder() {
const contentStat = await fs.promises.lstat(contentFolder)
if (contentStat.isSymbolicLink()) {
await fs.promises.unlink(contentFolder)
} else {
await rimraf(contentFolder)
}
}
if (setupStrategy === "copy" || setupStrategy === "symlink") {
const originalFolder = escapePath(
exitIfCancel(
await text({
message: "Enter the full path to existing content folder",
placeholder:
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
validate(fp) {
const fullPath = escapePath(fp)
if (!fs.existsSync(fullPath)) {
return "The given path doesn't exist"
} else if (!fs.lstatSync(fullPath).isDirectory()) {
return "The given path is not a folder"
}
},
}),
),
)
await rmContentFolder()
if (setupStrategy === "copy") {
await fs.promises.cp(originalFolder, contentFolder, {
recursive: true,
preserveTimestamps: true,
})
} else if (setupStrategy === "symlink") {
await fs.promises.symlink(originalFolder, contentFolder, "dir")
}
} else if (setupStrategy === "new") {
await rmContentFolder()
await fs.promises.mkdir(contentFolder)
await fs.promises.writeFile(
path.join(contentFolder, "index.md"),
`---
title: Welcome to Quartz
---
This is a blank Quartz installation.
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
`,
)
}
// get a prefered link resolution strategy
const linkResolutionStrategy = exitIfCancel(
await select({
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
options: [
{
value: "absolute",
label: "Treat links as absolute path",
hint: "for content made for Quartz 3 and Hugo",
},
{
value: "shortest",
label: "Treat links as shortest path",
hint: "for most Obsidian vaults",
},
{
value: "relative",
label: "Treat links as relative paths",
hint: "for just normal Markdown files",
},
],
}),
)
// now, do config changes
const configFilePath = path.join(cwd, "quartz.config.ts")
let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
configContent = configContent.replace(
/markdownLinkResolution: '(.+)'/,
`markdownLinkResolution: '${linkResolutionStrategy}'`,
)
await fs.promises.writeFile(configFilePath, configContent)
outro(`You're all set! Not sure what to do next? Try:
Customizing Quartz a bit more by editing \`quartz.config.ts\`
Running \`npx quartz build --serve\` to preview your Quartz locally
Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
`)
})
.command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
const contentFolder = path.join(cwd, argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
console.log("Backing up your content")
execSync(
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
)
await stashContentFolder(contentFolder)
console.log(
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
)
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
await popContentFolder(contentFolder)
console.log("Ensuring dependencies are up to date")
spawnSync("npm", ["i"], { stdio: "inherit" })
console.log(chalk.green("Done!"))
})
.command(
"restore",
"Try to restore your content folder from the cache",
CommonArgv,
async (argv) => {
const contentFolder = path.join(cwd, argv.directory)
await popContentFolder(contentFolder)
},
)
.command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
const contentFolder = path.join(cwd, argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
console.log("Backing up your content")
if (argv.commit) {
const contentStat = await fs.promises.lstat(contentFolder)
if (contentStat.isSymbolicLink()) {
const linkTarg = await fs.promises.readlink(contentFolder)
console.log(chalk.yellow("Detected symlink, trying to dereference before committing"))
// stash symlink file
await stashContentFolder(contentFolder)
// follow symlink and copy content
await fs.promises.cp(linkTarg, contentFolder, {
recursive: true,
preserveTimestamps: true,
})
}
const currentTimestamp = new Date().toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
spawnSync("git", ["add", "."], { stdio: "inherit" })
spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
if (contentStat.isSymbolicLink()) {
// put symlink back
await popContentFolder(contentFolder)
}
}
await stashContentFolder(contentFolder)
if (argv.pull) {
console.log(
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
)
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
}
await popContentFolder(contentFolder)
if (argv.push) {
console.log("Pushing your changes")
spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
}
console.log(chalk.green("Done!"))
})
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
const ctx = await esbuild.context({
entryPoints: [fp],
outfile: path.join("quartz", cacheFile),
bundle: true,
keepNames: true,
minifyWhitespace: true,
minifySyntax: true,
platform: "node",
format: "esm",
jsx: "automatic",
jsxImportSource: "preact",
packages: "external",
metafile: true,
sourcemap: true,
sourcesContent: false,
plugins: [
sassPlugin({
type: "css-text",
cssImports: true,
}),
{
name: "inline-script-loader",
setup(build) {
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
let text = await promises.readFile(args.path, "utf8")
// remove default exports that we manually inserted
text = text.replace("export default", "")
text = text.replace("export", "")
const sourcefile = path.relative(path.resolve("."), args.path)
const resolveDir = path.dirname(sourcefile)
const transpiled = await esbuild.build({
stdin: {
contents: text,
loader: "ts",
resolveDir,
sourcefile,
},
write: false,
bundle: true,
platform: "browser",
format: "esm",
})
const rawMod = transpiled.outputFiles[0].text
return {
contents: rawMod,
loader: "text",
}
})
},
},
],
})
const buildMutex = new Mutex()
const timeoutIds = new Set()
const build = async (clientRefresh) => {
await buildMutex.acquire()
const result = await ctx.rebuild().catch((err) => {
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${chalk.grey(err)}`)
process.exit(1)
})
if (argv.bundleInfo) {
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
const meta = result.metafile.outputs[outputFileName]
console.log(
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
meta.bytes,
)})`,
)
console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
}
// bypass module cache
// https://github.com/nodejs/modules/issues/307
const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`)
await buildQuartz(argv, clientRefresh)
clientRefresh()
buildMutex.release()
}
const rebuild = (clientRefresh) => {
timeoutIds.forEach((id) => clearTimeout(id))
timeoutIds.clear()
timeoutIds.add(setTimeout(() => build(clientRefresh), 250))
}
if (argv.serve) {
const connections = []
const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
argv.baseDir = "/" + argv.baseDir
}
await build(clientRefresh)
const server = http.createServer(async (req, res) => {
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
console.log(
chalk.red(
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
),
)
res.writeHead(404)
res.end()
return
}
// strip baseDir prefix
req.url = req.url?.slice(argv.baseDir.length)
const serve = async () => {
await serveHandler(req, res, {
public: argv.output,
directoryListing: false,
})
const status = res.statusCode
const statusString =
status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`))
}
const redirect = (newFp) => {
newFp = argv.baseDir + newFp
res.writeHead(302, {
Location: newFp,
})
console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`))
res.end()
}
let fp = req.url?.split("?")[0] ?? "/"
// handle redirects
if (fp.endsWith("/")) {
// /trailing/
// does /trailing/index.html exist? if so, serve it
const indexFp = path.posix.join(fp, "index.html")
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
req.url = fp
return serve()
}
// does /trailing.html exist? if so, redirect to /trailing
let base = fp.slice(0, -1)
if (path.extname(base) === "") {
base += ".html"
}
if (fs.existsSync(path.posix.join(argv.output, base))) {
return redirect(fp.slice(0, -1))
}
} else {
// /regular
// does /regular.html exist? if so, serve it
let base = fp
if (path.extname(base) === "") {
base += ".html"
}
if (fs.existsSync(path.posix.join(argv.output, base))) {
req.url = fp
return serve()
}
// does /regular/index.html exist? if so, redirect to /regular/
let indexFp = path.posix.join(fp, "index.html")
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
return redirect(fp + "/")
}
}
return serve()
})
server.listen(argv.port)
const wss = new WebSocketServer({ port: 3001 })
wss.on("connection", (ws) => connections.push(ws))
console.log(
chalk.cyan(
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
),
)
console.log("hint: exit with ctrl+c")
chokidar
.watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], {
ignoreInitial: true,
})
.on("all", async () => {
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
rebuild(clientRefresh)
})
} else {
await build(() => {})
ctx.dispose()
}
})
.showHelpOnFail(false)
.help()
.strict()
.demandCommand().argv

View File

@ -0,0 +1,7 @@
#!/usr/bin/env node
import workerpool from "workerpool"
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
const { parseFiles } = await import(cacheFile)
workerpool.worker({
parseFiles,
})

172
quartz/build.ts Normal file
View File

@ -0,0 +1,172 @@
import sourceMapSupport from "source-map-support"
sourceMapSupport.install(options)
import path from "path"
import { PerfTimer } from "./util/perf"
import { rimraf } from "rimraf"
import { isGitIgnored } from "globby"
import chalk from "chalk"
import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit"
import cfg from "../quartz.config"
import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile"
import { Argv, BuildCtx } from "./util/ctx"
import { glob, toPosixPath } from "./util/glob"
import { trace } from "./util/trace"
import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex"
async function buildQuartz(argv: Argv, clientRefresh: () => void) {
const ctx: BuildCtx = {
argv,
cfg,
allSlugs: [],
}
const perf = new PerfTimer()
const output = argv.output
const pluginCount = Object.values(cfg.plugins).flat().length
const pluginNames = (key: "transformers" | "filters" | "emitters") =>
cfg.plugins[key].map((plugin) => plugin.name)
if (argv.verbose) {
console.log(`Loaded ${pluginCount} plugins`)
console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
console.log(` Filters: ${pluginNames("filters").join(", ")}`)
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
}
perf.addEvent("clean")
await rimraf(output)
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
perf.addEvent("glob")
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
const fps = allFiles.filter((fp) => fp.endsWith(".md"))
console.log(
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
)
const filePaths = fps.map((fp) => joinSegments(argv.directory, fp) as FilePath)
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
const parsedFiles = await parseMarkdown(ctx, filePaths)
const filteredContent = filterContent(ctx, parsedFiles)
await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
if (argv.serve) {
return startServing(ctx, parsedFiles, clientRefresh)
}
}
// setup watcher for rebuilds
async function startServing(
ctx: BuildCtx,
initialContent: ProcessedContent[],
clientRefresh: () => void,
) {
const { argv } = ctx
const ignored = await isGitIgnored()
const contentMap = new Map<FilePath, ProcessedContent>()
for (const content of initialContent) {
const [_tree, vfile] = content
contentMap.set(vfile.data.filePath!, content)
}
const initialSlugs = ctx.allSlugs
const buildMutex = new Mutex()
const timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()
const toRebuild: Set<FilePath> = new Set()
const toRemove: Set<FilePath> = new Set()
const trackedAssets: Set<FilePath> = new Set()
async function rebuild(fp: string, action: "add" | "change" | "delete") {
// don't do anything for gitignored files
if (ignored(fp)) {
return
}
// dont bother rebuilding for non-content files, just track and refresh
fp = toPosixPath(fp)
const filePath = joinSegments(argv.directory, fp) as FilePath
if (path.extname(fp) !== ".md") {
if (action === "add" || action === "change") {
trackedAssets.add(filePath)
} else if (action === "delete") {
trackedAssets.delete(filePath)
}
clientRefresh()
return
}
if (action === "add" || action === "change") {
toRebuild.add(filePath)
} else if (action === "delete") {
toRemove.add(filePath)
}
timeoutIds.forEach((id) => clearTimeout(id))
// debounce rebuilds every 250ms
timeoutIds.add(
setTimeout(async () => {
await buildMutex.acquire()
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) {
const [_tree, vfile] = content
contentMap.set(vfile.data.filePath!, content)
}
for (const fp of toRemove) {
contentMap.delete(fp)
}
await rimraf(argv.output)
const parsedFiles = [...contentMap.values()]
const filteredContent = filterContent(ctx, parsedFiles)
await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
} catch {
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
}
clientRefresh()
toRebuild.clear()
toRemove.clear()
buildMutex.release()
}, 250),
)
}
const watcher = chokidar.watch(".", {
persistent: true,
cwd: argv.directory,
ignoreInitial: true,
})
watcher
.on("add", (fp) => rebuild(fp, "add"))
.on("change", (fp) => rebuild(fp, "change"))
.on("unlink", (fp) => rebuild(fp, "delete"))
}
export default async (argv: Argv, clientRefresh: () => void) => {
try {
return await buildQuartz(argv, clientRefresh)
} catch (err) {
trace("\nExiting Quartz due to a fatal error", err as Error)
}
}

48
quartz/cfg.ts Normal file
View File

@ -0,0 +1,48 @@
import { QuartzComponent } from "./components/types"
import { PluginTypes } from "./plugins/types"
import { Theme } from "./util/theme"
export type Analytics =
| null
| {
provider: "plausible"
}
| {
provider: "google"
tagId: string
}
export interface GlobalConfiguration {
pageTitle: string
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean
/** Whether to display Wikipedia-style popovers when hovering over links */
enablePopovers: boolean
/** Analytics mode */
analytics: Analytics
/** Glob patterns to not search */
ignorePatterns: string[]
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
* Quartz will avoid using this as much as possible and use relative URLs most of the time
*/
baseUrl?: string
theme: Theme
}
export interface QuartzConfig {
configuration: GlobalConfiguration
plugins: PluginTypes
}
export interface FullPageLayout {
head: QuartzComponent
header: QuartzComponent[]
beforeBody: QuartzComponent[]
pageBody: QuartzComponent
left: QuartzComponent[]
right: QuartzComponent[]
footer: QuartzComponent
}
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer">

View File

@ -0,0 +1,17 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function ArticleTitle({ fileData }: QuartzComponentProps) {
const title = fileData.frontmatter?.title
if (title) {
return <h1 class="article-title">{title}</h1>
} else {
return null
}
}
ArticleTitle.css = `
.article-title {
margin: 2rem 0 0 0;
}
`
export default (() => ArticleTitle) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,29 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path"
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return (
<div class="backlinks">
<h3>Backlinks</h3>
<ul class="overflow">
{backlinkFiles.length > 0 ? (
backlinkFiles.map((f) => (
<li>
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
{f.frontmatter?.title}
</a>
</li>
))
) : (
<li>No backlinks found</li>
)}
</ul>
</div>
)
}
Backlinks.css = style
export default (() => Backlinks) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,13 @@
// @ts-ignore
import clipboardScript from "./scripts/clipboard.inline"
import clipboardStyle from "./styles/clipboard.scss"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function Body({ children }: QuartzComponentProps) {
return <div id="quartz-body">{children}</div>
}
Body.afterDOMLoaded = clipboardScript
Body.css = clipboardStyle
export default (() => Body) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,29 @@
import { formatDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time"
export default (() => {
function ContentMetadata({ fileData }: QuartzComponentProps) {
const text = fileData.text
if (text) {
const segments: string[] = []
const { text: timeTaken, words: _words } = readingTime(text)
if (fileData.dates?.modified) {
segments.push(formatDate(fileData.dates.modified))
}
segments.push(timeTaken)
return <p class="content-meta">{segments.join(", ")}</p>
} else {
return null
}
}
ContentMetadata.css = `
.content-meta {
margin-top: 0;
color: var(--gray);
}
`
return ContentMetadata
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,51 @@
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
// see: https://v8.dev/features/modules#defer
import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor } from "./types"
function Darkmode() {
return (
<div class="darkmode">
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
version="1.1"
id="dayIcon"
x="0px"
y="0px"
viewBox="0 0 35 35"
style="enable-background:new 0 0 35 35;"
xmlSpace="preserve"
>
<title>Light mode</title>
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
</svg>
</label>
<label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
version="1.1"
id="nightIcon"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background='new 0 0 100 100'"
xmlSpace="preserve"
>
<title>Dark mode</title>
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
</svg>
</label>
</div>
)
}
Darkmode.beforeDOMLoaded = darkmodeScript
Darkmode.css = styles
export default (() => Darkmode) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,15 @@
interface Props {
date: Date
}
export function formatDate(d: Date): string {
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "2-digit",
})
}
export function Date({ date }: Props) {
return <>{formatDate(date)}</>
}

View File

@ -0,0 +1,18 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => {
if (component) {
const Component = component
function DesktopOnly(props: QuartzComponentProps) {
return <Component displayClass="desktop-only" {...props} />
}
DesktopOnly.displayName = component.displayName
DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded
DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
DesktopOnly.css = component?.css
return DesktopOnly
} else {
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,32 @@
import { QuartzComponentConstructor } from "./types"
import style from "./styles/footer.scss"
import { version } from "../../package.json"
interface Options {
links: Record<string, string>
}
export default ((opts?: Options) => {
function Footer() {
const year = new Date().getFullYear()
const links = opts?.links ?? []
return (
<footer>
<hr />
<p>
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
</p>
<ul>
{Object.entries(links).map(([text, link]) => (
<li>
<a href={link}>{text}</a>
</li>
))}
</ul>
</footer>
)
}
Footer.css = style
return Footer
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,94 @@
import { QuartzComponentConstructor } from "./types"
// @ts-ignore
import script from "./scripts/graph.inline"
import style from "./styles/graph.scss"
export interface D3Config {
drag: boolean
zoom: boolean
depth: number
scale: number
repelForce: number
centerForce: number
linkDistance: number
fontSize: number
opacityScale: number
}
interface GraphOptions {
localGraph: Partial<D3Config> | undefined
globalGraph: Partial<D3Config> | undefined
}
const defaultOptions: GraphOptions = {
localGraph: {
drag: true,
zoom: true,
depth: 1,
scale: 1.1,
repelForce: 0.5,
centerForce: 0.3,
linkDistance: 30,
fontSize: 0.6,
opacityScale: 1,
},
globalGraph: {
drag: true,
zoom: true,
depth: -1,
scale: 0.9,
repelForce: 0.5,
centerForce: 0.3,
linkDistance: 30,
fontSize: 0.6,
opacityScale: 1,
},
}
export default ((opts?: GraphOptions) => {
function Graph() {
const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
return (
<div class="graph">
<h3>Graph View</h3>
<div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<svg
version="1.1"
id="global-graph-icon"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 55 55"
fill="currentColor"
xmlSpace="preserve"
>
<path
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/>
</svg>
</div>
<div id="global-graph-outer">
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
</div>
</div>
)
}
Graph.css = style
Graph.afterDOMLoaded = script
return Graph
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,40 @@
import { joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default (() => {
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
const title = fileData.frontmatter?.title ?? "Untitled"
const description = fileData.description?.trim() ?? "No description provided"
const { css, js } = externalResources
const baseDir = pathToRoot(fileData.slug!)
const iconPath = joinSegments(baseDir, "static/icon.png")
const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
return (
<head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{cfg.baseUrl && <meta property="og:image" content={ogImagePath} />}
<meta property="og:width" content="1200" />
<meta property="og:height" content="675" />
<link rel="icon" href={iconPath} />
<meta name="description" content={description} />
<meta name="generator" content="Quartz" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
{css.map((href) => (
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
))}
{js
.filter((resource) => resource.loadTime === "beforeDOMReady")
.map((res) => JSResourceToScriptElement(res, true))}
</head>
)
}
return Head
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,22 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function Header({ children }: QuartzComponentProps) {
return children.length > 0 ? <header>{children}</header> : null
}
Header.css = `
header {
display: flex;
flex-direction: row;
align-items: center;
margin: 2rem 0;
gap: 1.5rem;
}
header h1 {
margin: 0;
flex: auto;
}
`
export default (() => Header) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,18 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => {
if (component) {
const Component = component
function MobileOnly(props: QuartzComponentProps) {
return <Component displayClass="mobile-only" {...props} />
}
MobileOnly.displayName = component.displayName
MobileOnly.afterDOMLoaded = component?.afterDOMLoaded
MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
MobileOnly.css = component?.css
return MobileOnly
} else {
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,82 @@
import { FullSlug, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile"
import { Date } from "./Date"
import { QuartzComponentProps } from "./types"
export function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number {
if (f1.dates && f2.dates) {
// sort descending by last modified
return f2.dates.modified.getTime() - f1.dates.modified.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
type Props = {
limit?: number
} & QuartzComponentProps
export function PageList({ fileData, allFiles, limit }: Props) {
let list = allFiles.sort(byDateAndAlphabetical)
if (limit) {
list = list.slice(0, limit)
}
return (
<ul class="section-ul">
{list.map((page) => {
const title = page.frontmatter?.title
const tags = page.frontmatter?.tags ?? []
return (
<li class="section-li">
<div class="section">
{page.dates && (
<p class="meta">
<Date date={page.dates.modified} />
</p>
)}
<div class="desc">
<h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
{title}
</a>
</h3>
</div>
<ul class="tags">
{tags.map((tag) => (
<li>
<a
class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
>
#{tag}
</a>
</li>
))}
</ul>
</div>
</li>
)
})}
</ul>
)
}
PageList.css = `
.section h3 {
margin: 0;
}
.section > .tags {
margin: 0;
}
`

View File

@ -0,0 +1,20 @@
import { pathToRoot } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function PageTitle({ fileData, cfg }: QuartzComponentProps) {
const title = cfg?.pageTitle ?? "Untitled Quartz"
const baseDir = pathToRoot(fileData.slug!)
return (
<h1 class="page-title">
<a href={baseDir}>{title}</a>
</h1>
)
}
PageTitle.css = `
.page-title {
margin: 0;
}
`
export default (() => PageTitle) satisfies QuartzComponentConstructor

Some files were not shown because too many files have changed in this diff Show More