Merge branch 'hugo' into cli

This commit is contained in:
Liam Arbuckle 2022-03-22 16:04:32 +08:00 committed by GitHub
commit 976c228f6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1255 additions and 613 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [jackyzha0]

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea or improvement for Quartz
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -12,11 +12,12 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build Link Index - name: Build Link Index
uses: jackyzha0/hugo-obsidian@v2.5 uses: jackyzha0/hugo-obsidian@v2.10
with: with:
index: true index: true
input: content input: content
output: data output: assets/indices
root: .
- name: Setup Hugo - name: Setup Hugo
uses: peaceiris/actions-hugo@v2 uses: peaceiris/actions-hugo@v2
@ -33,4 +34,4 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public publish_dir: ./public
publish_branch: master # deploying branch publish_branch: master # deploying branch
cname: quartz.jzhao.xyz cname: quartz.jzhao.xyz

4
.gitignore vendored
View File

@ -3,5 +3,5 @@ public
resources resources
.idea .idea
content/.obsidian content/.obsidian
data/linkIndex.yaml assets/indices/linkIndex.json
data/contentIndex.yaml assets/indices/contentIndex.json

0
.hugo_build.lock Normal file
View File

93
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,93 @@
# Citizen Code of Conduct
## 1. Purpose
A primary goal of the Quartz community is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
We invite all those who participate in the Quartz community to help us create safe and positive experiences for everyone.
## 2. Open [Source/Culture/Tech] Citizenship
A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
## 3. Expected Behavior
The following behaviors are expected and requested of all community members:
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
* Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
## 4. Unacceptable Behavior
The following behaviors are considered harassment and are unacceptable within our community:
* Violence, threats of violence or violent language directed against another person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
* Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other people's personally identifying information ("doxing").
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
* Inappropriate photography or recording.
* Inappropriate physical contact. You should have someone's consent before touching them.
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
* Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations.
## 5. Weapons Policy
No weapons will be allowed at Quartz community events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter.
## 6. Consequences of Unacceptable Behavior
Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
Anyone asked to stop unacceptable behavior is expected to comply immediately.
If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
## 7. Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. j.zhao2k19@gmail.com.
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
## 8. Addressing Grievances
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
## 9. Scope
We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business.
This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
## 10. Contact info
j.zhao2k19@gmail.com
## 11. License and attribution
The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
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).
_Revision 2.3. Posted 6 March 2017._
_Revision 2.2. Posted 4 February 2016._
_Revision 2.1. Posted 23 June 2014._
_Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
.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}'
serve: ## serve
hugo-obsidian -input=content -output=assets/indices -index -root=. && hugo server

0
assets/indices/.gitkeep Normal file
View File

220
assets/js/graph.js Normal file
View File

@ -0,0 +1,220 @@
async function drawGraph(url, baseUrl, pathColors, depth, enableDrag, enableLegend, enableZoom) {
const { index, links, content } = await fetchData
const curPage = url.replace(baseUrl, "")
const parseIdsFromLinks = (links) => [...(new Set(links.flatMap(link => ([link.source, link.target]))))]
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(links).forEach(id => neighbours.add(id))
}
const data = {
nodes: [...neighbours].map(id => ({ id })),
links: links.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 = 250
const width = document.getElementById("graph-container").offsetWidth
const simulation = d3.forceSimulation(data.nodes)
.force("charge", d3.forceManyBody().strength(-30))
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("center", d3.forceCenter());
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr("viewBox", [-width / 2, -height / 2, width, height]);
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")
// draw individual nodes
const node = graphNode.append("circle")
.attr("class", "node")
.attr("id", (d) => d.id)
.attr("r", (d) => {
const numOut = index.links[d.id]?.length || 0
const numIn = index.backlinks[d.id]?.length || 0
return 3 + (numOut + numIn) / 4
})
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
window.location.href = baseUrl + '/' + decodeURI(d.id).replace(/\s+/g, '-')
})
.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
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)")
// show text for self
d3.select(this.parentNode)
.select("text")
.raise()
.transition()
.duration(200)
.style("opacity", 1)
}).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", 0)
})
.call(drag(simulation));
// draw labels
const labels = graphNode.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text((d) => content[decodeURI(d.id).replace(/\s+/g, '-')]?.title || d.id.replace("-", " "))
.style("opacity", 0)
.style("pointer-events", "none")
.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);
labels.attr("transform", transform);
}));
}
// 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)
});
}

34
assets/js/popover.js Normal file
View File

@ -0,0 +1,34 @@
function htmlToElement(html) {
const template = document.createElement('template')
html = html.trim()
template.innerHTML = html
return template.content.firstChild
}
function initPopover(baseURL) {
const basePath = baseURL.replace(window.location.origin, "")
document.addEventListener("DOMContentLoaded", () => {
fetchData.then(({ content }) => {
const links = [...document.getElementsByClassName("internal-link")]
links.forEach(li => {
const linkDest = content[li.dataset.src.replace(basePath, "")]
// const linkDest = content[li.dataset.src]
if (linkDest) {
const popoverElement = `<div class="popover">
<h3>${linkDest.title}</h3>
<p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p>
<p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
</div>`
const el = htmlToElement(popoverElement)
li.appendChild(el)
li.addEventListener("mouseover", () => {
el.classList.add("visible")
})
li.addEventListener("mouseout", () => {
el.classList.remove("visible")
})
}
})
})
})
}

247
assets/js/search.js Normal file
View File

@ -0,0 +1,247 @@
// 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(/\s{0,2}\[.*?\]: .*?$/g, "")
.replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "")
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "$1")
.replace(/^\s{0,3}>\s?/g, "")
.replace(/(^|\n)\s{0,3}>\s?/g, "\n\n")
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, "")
.replace(
/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm,
"$1$2$3"
)
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
.replace(/(`{3,})(.*?)\1/gm, "$2")
.replace(/`(.+?)`/g, "$1")
.replace(/\n{2,}/g, "\n\n");
} catch (e) {
console.error(e);
return markdown;
}
return output;
};
// -----
(async function () {
const contentIndex = new FlexSearch.Document({
cache: true,
charset: "latin:extra",
optimize: true,
worker: true,
document: {
index: [{
field: "content",
tokenize: "strict",
context: {
resolution: 5,
depth: 3,
bidirectional: true
},
suggest: true,
}, {
field: "title",
tokenize: "forward",
}]
}
})
const { content } = await fetchData
for (const [key, value] of Object.entries(content)) {
contentIndex.add({
id: key,
title: value.title,
content: removeMarkdown(value.content),
})
}
const highlight = (content, term) => {
const highlightWindow = 20
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 ? "" : "..."}`
}
const resultToHTML = ({ url, title, content, term }) => {
const text = removeMarkdown(content)
const resultTitle = highlight(title, term)
const resultText = highlight(text, term)
return `<button class="result-card" id="${url}">
<h3>${resultTitle}</h3>
<p>${resultText}</p>
</button>`
}
const redir = (id, term) => {
window.location.href = BASE_URL + `${id}#:~:text=${encodeURIComponent(term)}`
}
const formatForDisplay = id => ({
id,
url: id,
title: content[id].title,
content: content[id].content
})
const source = document.getElementById('search-bar')
const results = document.getElementById("results-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', (e) => {
term = e.target.value
contentIndex.search(term, [
{
field: "content",
limit: 10,
suggest: true,
},
{
field: "title",
limit: 5,
}
]).then(searchResults => {
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)
// display
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 => resultToHTML({
...result,
term,
}))
.join("\n")
const anchors = document.getElementsByClassName("result-card");
[...anchors].forEach(anchor => {
anchor.onclick = () => redir(anchor.id, term)
})
}
})
})
const searchContainer = document.getElementById("search-container")
function openSearch() {
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() {
searchContainer.style.display = "none"
}
document.addEventListener('keydown', (event) => {
if (event.key === "/") {
event.preventDefault()
openSearch()
}
if (event.key === "Escape") {
event.preventDefault()
closeSearch()
}
})
const searchButton = document.getElementById("search-icon")
searchButton.addEventListener('click', (evt) => {
openSearch()
})
searchButton.addEventListener('keydown', (evt) => {
openSearch()
})
searchContainer.addEventListener('click', (evt) => {
closeSearch()
})
document.getElementById("search-space").addEventListener('click', (evt) => {
evt.stopPropagation()
})
})()

View File

@ -6,7 +6,7 @@
--lt-colours-gray: var(--outlinegray) !important; --lt-colours-gray: var(--outlinegray) !important;
} }
h1, h2, h3, h4, ol, ul, thead { h1, h2, h3, h4, h5, h6, ol, ul, thead {
font-family: Inter; font-family: Inter;
color: var(--dark); color: var(--dark);
font-weight: revert; font-weight: revert;
@ -23,19 +23,6 @@ p, ul, text {
padding: revert; padding: revert;
} }
a {
font-family: Inter;
font-weight: 700;
font-size: 1em;
text-decoration: none;
transition: all 0.2s ease;
color: var(--secondary);
&:hover {
color: var(--tertiary) !important;
}
}
#TableOfContents > ol { #TableOfContents > ol {
counter-reset: section; counter-reset: section;
margin-left: 0em; margin-left: 0em;
@ -94,14 +81,6 @@ p, tbody, li {
line-height: 1.5em; line-height: 1.5em;
} }
h2 {
opacity: 0.85;
}
h3 {
opacity: 0.75;
}
blockquote { blockquote {
margin-left: 0em; margin-left: 0em;
border-left: 3px solid var(--secondary); border-left: 3px solid var(--secondary);
@ -125,12 +104,94 @@ td, th {
margin: 0.5em 0; margin: 0.5em 0;
} }
article a { .pagination {
font-family: Source Sans Pro; list-style: none;
padding-left: 0;
display: flex;
margin-top: 2em;
gap: 1.5em;
justify-content: center;
& > li {
text-align: center;
display: inline-block;
& a {
background-color: transparent !important;
}
& a[href$="#"] {
opacity: 0.2;
}
}
}
.section {
& h3 > a {
font-weight: 700;
font-family: Inter;
margin: 0;
}
& p {
margin-top: 0;
}
}
article {
& > .meta {
margin: -1.5em 0 1em 0;
opacity: 0.7;
}
& > .tags {
list-style: none;
padding-left: 0;
& .meta {
& > h1 {
margin: 0;
}
& > p {
margin: 0;
}
}
& > li {
display: inline-block;
}
& > li > a {
border-radius: 8px;
border: var(--outlinegray) 1px solid;
padding: 0.2em 0.5em;
&::before {
content: "#";
margin-right: 0.3em;
color: var(--outlinegray);
}
}
}
& a {
font-family: Source Sans Pro;
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;
}
}
& p {
overflow-wrap: anywhere;
}
}
.backlinks a {
font-weight: 600; font-weight: 600;
text-decoration: underline; font-size: 0.9rem;
text-decoration-color: var(--tertiary);
text-decoration-thickness: .15em;
} }
sup > a { sup > a {
@ -138,6 +199,19 @@ sup > a {
padding: 0 0.1em 0 0.2em; padding: 0 0.1em 0 0.2em;
} }
a {
font-family: Inter, sans-serif;
font-size: 1em;
font-weight: 700;
text-decoration: none;
transition: all 0.2s ease;
color: var(--secondary);
&:hover {
color: var(--tertiary) !important;
}
}
pre { pre {
font-family: 'Fira Code'; font-family: 'Fira Code';
padding: 0.75em; padding: 0.75em;
@ -155,13 +229,22 @@ code {
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
&:lang(ar) {
& p, & h1, & h2, & h3, article {
direction: rtl;
text-align: right;
}
}
} }
body { body {
margin: 0; margin: 0;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
overflow-x: hidden; //overflow-x: hidden;
max-width: 100%;
box-sizing: border-box;
background-color: var(--light); background-color: var(--light);
} }
@ -190,25 +273,18 @@ hr {
background-color: var(--dark); background-color: var(--dark);
} }
// internal link
a[href^="/"] {
text-decoration: none;
background-color: #afbfc922;
padding: 0 0.2em;
border-radius: 3px;
}
.singlePage { .singlePage {
margin: 4em 30vw; padding: 4em 30vw;
@media all and (max-width: 1200px) { @media all and (max-width: 1200px) {
margin: 25px 5vw; padding: 25px 5vw;
} }
} }
.page-end { .page-end {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 2em;
@media all and (max-width: 780px) { @media all and (max-width: 780px) {
flex-direction: column; flex-direction: column;
@ -222,7 +298,6 @@ a[href^="/"] {
& > ul { & > ul {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
margin-right: 2em;
& > li { & > li {
margin: 0.5em 0; margin: 0.5em 0;
@ -235,7 +310,7 @@ a[href^="/"] {
& #graph-container { & #graph-container {
border: var(--outlinegray) 1px solid; border: var(--outlinegray) 1px solid;
border-radius: 5px border-radius: 5px;
} }
} }
@ -243,6 +318,10 @@ a[href^="/"] {
margin-top: 30vh; margin-top: 30vh;
} }
article > h1 {
font-size: 2em;
}
header { header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -370,4 +449,70 @@ header {
} }
} }
} }
} }
.section-ul {
list-style: none;
padding-left: 0;
& > li {
border: 1px solid var(--outlinegray);
border-radius: 5px;
padding: 0 1em;
margin-bottom: 1em;
& h3 {
opacity: 1;
font-weight: 700;
margin-bottom: 0em;
}
& .meta {
opacity: 0.6;
}
}
}
.popover {
z-index: 999;
position: absolute;
width: 20em;
display: inline-block;
visibility: hidden;
background-color: var(--light);
padding: 1em;
border: 1px solid var(--outlinegray);
border-radius: 5px;
transform: translate(-50%, 40%);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
transition-delay: 0.3s;
user-select: none;
@media all and (max-width: 600px) {
display: none;
}
&.visible {
opacity: 1;
visibility: visible;
transform: translate(-50%, 20%);
}
& > h3 {
font-size: 1rem;
margin: 0.25em 0;
}
& > .meta {
margin-top: 0.25em;
opacity: 0.5;
}
& > p {
margin: 0;
font-weight: 400;
user-select: none;
}
}

View File

@ -14,7 +14,7 @@
[saved-theme="dark"] { [saved-theme="dark"] {
--light: #1e1e21 !important; --light: #1e1e21 !important;
--dark: #fbfffe !important; --dark: #fbfffe !important;
--secondary: #5b778a !important; --secondary: #6b879a !important;
--visited: #4a575e !important; --visited: #4a575e !important;
--tertiary: #84a59d !important; --tertiary: #84a59d !important;
--primary: #f58382 !important; --primary: #f58382 !important;

View File

@ -2,12 +2,15 @@ baseURL = "https://quartz.jzhao.xyz/"
languageCode = "en-us" languageCode = "en-us"
googleAnalytics = "G-XYFD95KB4J" googleAnalytics = "G-XYFD95KB4J"
pygmentsUseClasses = true pygmentsUseClasses = true
relativeURLs = true relativeURLs = false
disablePathToLower = true disablePathToLower = true
ignoreFiles = [ ignoreFiles = [
"/content/templates/*", "/content/templates/*",
"/content/private/*", "/content/private/*",
] ]
summaryLength = 20
paginate = 10
enableGitInfo = true
[markup] [markup]
[markup.tableOfContents] [markup.tableOfContents]
@ -24,4 +27,4 @@ ignoreFiles = [
lineNos = true lineNos = true
lineNumbersInTable = true lineNumbersInTable = true
style = "dracula" style = "dracula"
tabWidth = 4 tabWidth = 4

View File

@ -1,18 +1,27 @@
--- ---
title: 🪴 Quartz v2.1 title: 🪴 Quartz 3.1
--- ---
Host your second brain and [digital garden](https://jzhao.xyz/posts/digital-gardening) for free. Quartz features Host your second brain and [digital garden](https://jzhao.xyz/posts/digital-gardening) for free. Quartz features
1. Extremely fast full-text search by pressing `/` 1. Extremely fast full-text search by pressing `/`
2. Beautiful, out-of-the-box website creation and deployment 2. Display for backlinks of each note
3. Display for backlinks of each note 3. Fully customizable local graph view
4. A customizable graph view 4. Endlessly powerful page and theme customization using CSS
5. Endlessly powerful page and theme customization 5. Automatically generated tag and section lists of content
6. Beautiful link previews
## Get Started ## Get Started
> 📚 [Setup your own digital garden using Quartz](notes/setup.md) > 📚 [Setup your own digital garden using Quartz](notes/setup.md)
Not convinced yet? Look at some [community digital gardens](moc/showcase) built with Quartz, or read about [why I made Quartz](notes/philosophy.md) to begin with! Not convinced yet? Look at some [community digital gardens](notes/showcase.md) built with Quartz, or read about [why I made Quartz](notes/philosophy.md) to begin with!
## Content Lists
If you prefer browsing the contents of this site through a list instead of a graph, you can find content lists here too:
- [All Notes](/notes)
- [Setup-related Notes](/tags/setup)
## Troubleshooting ## Troubleshooting
- 🚧 [Troubleshooting and FAQ](notes/troubleshooting.md) - 🚧 [Troubleshooting and FAQ](notes/troubleshooting.md)
- 🐛 [Submit an Issue](https://github.com/jackyzha0/quartz/issues) - 🐛 [Submit an Issue](https://github.com/jackyzha0/quartz/issues)
- 👀 [Discord Community](https://discord.gg/cRFFHYye7t)

30
content/notes/Obsidian.md Normal file
View File

@ -0,0 +1,30 @@
---
title: "Obsidian Vault Integration"
tags:
- setup
---
## Setup
Obsidian is the preferred way to use Quartz. You can either create a new Obsidian Vault or link one that your already have.
### New Vault
If you don't have an existing Vault, [download Obsidian](https://obsidian.md/) and create a new Vault in the `/content` folder that you created and cloned during the [setup](notes/setup.md) step.
### Linking an existing Vault
The easiest way to use an existing Vault is to copy all of your files (directory and hierarchies intact) into the `/content` folder.
## Settings
Great, now that you have your Obsidian linked to your Quartz, let's fix some settings so that they play well.
1. Under Options > Files and Links, set the New link format to always be Absolute Path in Vault and disabled WikiLinks so Obsidian generates regular Markdown links.
2. Go to Settings > Files & Links > Turn "on" automatically update internal links.
3. Use [[Wikilinks]] should be off (Wikilinks are not officially supported yet)
![Obsidian Settings](/notes/images/obsidian-settings.png)*Obsidian Settings*
## Templates
Inserting front matter everytime you want to create a new Note gets annoying really quickly. Luckily, Obsidian supports templates which makes inserting new content really easily.
**If you decide to overwrite the `/content` folder completely, don't remove the `/content/templates` folder!**
Head over to Options > Core Plugins and enable the Templates plugin. Then go to Options > Hotkeys and set a hotkey for 'Insert Template' (I recomment `[cmd]+T`). That way, when you create a new note, you can just press the hotkey for a new template and be ready to go!

View File

@ -1,5 +1,7 @@
--- ---
title: "Configuration" title: "Configuration"
tags:
- setup
--- ---
<!--Add search engine to Quartz like Notion>Super --> <!--Add search engine to Quartz like Notion>Super -->
@ -11,6 +13,7 @@ title: "Configuration"
```yaml ```yaml
name: Your name here! # Shows in the footer name: Your name here! # Shows in the footer
enableToc: true # Whether to show a Table of Contents enableToc: true # Whether to show a Table of Contents
enableLinkPreview: true # whether to render card previews for links
description: Page description to show to search engines description: Page description to show to search engines
page_title: Quartz Example Page # Default Page Title page_title: Quartz Example Page # Default Page Title
@ -28,6 +31,7 @@ To customize the Interactive Graph view, you can poke around `data/graphConfig.y
enableLegend: false # automatically generate a legend enableLegend: false # automatically generate a legend
enableDrag: true # allow dragging nodes in the graph enableDrag: true # allow dragging nodes in the graph
enableZoom: true # allow zooming and panning the graph enableZoom: true # allow zooming and panning the graph
depth: -1 # how many neighbours of the current node to show (-1 is all nodes)
paths: # colour specific nodes path off of their path paths: # colour specific nodes path off of their path
- /moc: "#4388cc" - /moc: "#4388cc"
``` ```
@ -43,4 +47,20 @@ For example, the structure of the home page can be edited through `/layouts/inde
More info about partials on [Hugo's website.](https://gohugo.io/templates/partials/) More info about partials on [Hugo's website.](https://gohugo.io/templates/partials/)
Still having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md). Still having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
## Multilingual
Want to support languages that read from right-to-left (like Arabic)? Hugo (and by proxy, Quartz) supports this natively.
Follow the steps [Hugo provides here](https://gohugo.io/content-management/multilingual/#configure-languages) and modify your `config.toml`
For example:
```toml
defaultContentLanguage = 'ar'
[languages]
[languages.ar]
languagedirection = 'rtl'
title = 'مدونتي'
weight = 1
```

View File

@ -0,0 +1,17 @@
---
title: "Custom Domain"
---
### Registrar
This step is only applicable if you are using a **custom domain**! If you are using a `<YOUR-USERNAME>.github.io` domain, you can skip this step.
For this last bit to take effect, you also need to create a CNAME record with the DNS provider you register your domain with (i.e. NameCheap, Google Domains).
GitHub has some [documentation on this](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site), but the tldr; is to
1. Go to your forked repository (`github.com/<YOUR-GITHUB-USERNAME>/quartz`) settings page and go to the Pages tab. Under "Custom domain", type your custom domain, then click **Save**.
2. Go to your DNS Provider and create a CNAME record that points from your domain to `<YOUR-GITHUB-USERNAME.github.io.` (yes, with the trailing period).
![Example Configuration for Quartz](/notes/images/google-domains.png)*Example Configuration for Quartz*
3. Wait 30 minutes to an hour for the network changes to kick in.
4. Done!

View File

@ -1,16 +1,18 @@
--- ---
title: "Editing Content in Quartz" title: "Editing Content in Quartz"
tags:
- setup
--- ---
## Editing ## Editing
Quartz runs on top of [Hugo](https://gohugo.io/) so all notes are written in [Markdown](https://www.markdownguide.org/getting-started/). Quartz runs on top of [Hugo](https://gohugo.io/) so all notes are written in [Markdown](https://www.markdownguide.org/getting-started/).
### Obsidian ### Obsidian
I *strongly* recommend using [Obsidian](http://obsidian.md/) as a way to edit and grow your digital garden. It comes with a really nice editor and graphical interface to preview all of my local files. I recommend using [Obsidian](http://obsidian.md/) as a way to edit and grow your digital garden. It comes with a really nice editor and graphical interface to preview all of your local files.
**🔗 [How to link your Obsidian Vault](notes/obsidian.md)** This step is **highly recommended**.
Of course, all the files are in Markdown so you could just use your favourite text editor of choice. 🔗 [How to setup your Obsidian Vault to work with Quartz](notes/obsidian.md)
### Ignoring Files ### Ignoring Files
Only want to publish a subset of all of your notes? Don't worry, Quartz makes this a simple two-step process. Only want to publish a subset of all of your notes? Don't worry, Quartz makes this a simple two-step process.
@ -22,7 +24,7 @@ Here's a rough overview of what's what.
**All content in your garden can found in the `/content` folder.** To make edits, you can open any of the files and make changes directly and save it. You can organize content into any folder you'd like. **All content in your garden can found in the `/content` folder.** To make edits, you can open any of the files and make changes directly and save it. You can organize content into any folder you'd like.
**To edit the main home page, open `/content/_index.md`.** This is the home page which is slightly special. You don't need front matter here! **To edit the main home page, open `/content/_index.md`.*
To create a link between notes in your garden, just create a normal link using Markdown pointing to the document in question. Please note that **all links should be relative to the root `/content` path**. To create a link between notes in your garden, just create a normal link using Markdown pointing to the document in question. Please note that **all links should be relative to the root `/content` path**.
@ -31,19 +33,29 @@ For example, I want to link this current document to `notes/config.md`.
[A link to the config page](notes/config.md) [A link to the config page](notes/config.md)
``` ```
Similarly, you can put local images anywhere in the `/content` folder.
```markdown
Example image (source is in content/notes/images/example.png)
![Example Image](/content/notes/images/example.png)
```
### Front Matter ### Front Matter
Hugo is picky when it comes to metadata for files. Make sure that your title is double-quoted and that you have a title defined at the top of your file like so: Hugo is picky when it comes to metadata for files. Make sure that your title is double-quoted and that you have a title defined at the top of your file like so. You can also add tags here as well.
```markdown ```markdown
--- ---
title: "Example Title" title: "Example Title"
tags:
- example-tag
disableToc: true # do not show a table of contents on this page if enabled
--- ---
Rest of your content here... Rest of your content here...
``` ```
## Previewing Changes ## Previewing Changes
This step is purely optional and mostly for those who want to see the published version of their digital garden locally before opening it up to the internet. This is *highly recommended*. This step is purely optional and mostly for those who want to see the published version of their digital garden locally before opening it up to the internet. This is *highly recommended* but not required.
👀 [Preview Quartz Changes](notes/preview%20changes.md) 👀 [Preview Quartz Changes](notes/preview%20changes.md)
@ -54,4 +66,4 @@ Now that you know the basics of managing your digital garden using Quartz, you c
🌍 [Hosting Quartz online!](notes/hosting.md) 🌍 [Hosting Quartz online!](notes/hosting.md)
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md). Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).

View File

@ -1,26 +1,28 @@
--- ---
title: "Deploying Quartz to the Web" title: "Deploying Quartz to the Web"
tags:
- setup
--- ---
## GitHub Pages ## GitHub Pages
Quartz is designed to be effortless to deploy. If you forked and cloned Quartz directly from the repository, everything should already be good to go! You can head to `<YOUR-GITHUB-USERNAME.github.io/quartz` to see it live. Quartz is designed to be effortless to deploy. If you forked and cloned Quartz directly from the repository, everything should already be good to go! Follow the steps below.
### Enable GitHub Actions ### Enable GitHub Actions
By default, GitHub disables workflows from running automatically on Forked Repostories. Head to the 'Actions' tab of your forked repository and Enable Workflows to setup deploying your Quartz site! By default, GitHub disables workflows from running automatically on Forked Repostories. Head to the 'Actions' tab of your forked repository and Enable Workflows to setup deploying your Quartz site!
![Enable GitHub Actions](/notes/images/github-actions.png)*Enable GitHub Actions* ![Enable GitHub Actions](notes/images/github-actions.png)*Enable GitHub Actions*
### Enable GitHub Pages ### Enable GitHub Pages
Head to the 'Settings' tab of your forked repository and go to the 'Pages' tab. Head to the 'Settings' tab of your forked repository and go to the 'Pages' tab.
1. Set the source to deploy from `master` using `/ (root)` 1. (IMPORTANT) Set the source to deploy from `master` (and not `hugo`) using `/ (root)`
2. Set a custom domain here if you have one! 2. Set a custom domain here if you have one!
![Enable GitHub Pages](/notes/images/github-pages.png)*Enable GitHub Pages* ![Enable GitHub Pages](/notes/images/github-pages.png)*Enable GitHub Pages*
### Pushing Changes ### Pushing Changes
To see your changes on the internet, we need to push it them to GitHub. Quartz is essentially a `git` repository so updating it is the same workflow as you would follow as normal. To see your changes on the internet, we need to push it them to GitHub. Quartz is a `git` repository so updating it is the same workflow as you would follow as if it were just a regular software project.
```shell ```shell
# Navigate to Quartz folder # Navigate to Quartz folder
@ -34,20 +36,32 @@ git commit -m "message describing changes"
git push origin hugo git push origin hugo
``` ```
Note: we specifically push to the `hugo` branch here. Our GitHub action automatically runs everytime a push to is detected to that branch and then updates the `master` branch for redeployment.
### Setting up the Site ### Setting up the Site
Now let's get this site up and running. Never hosted a site before? No problem. Have a fancy custom domain you already own or want to subdomain your Quartz? That's easy too. Now let's get this site up and running. Never hosted a site before? No problem. Have a fancy custom domain you already own or want to subdomain your Quartz? That's easy too.
Here, we take advantage of GitHub's free page hosting to deploy our site. Change `baseURL` in `/config.toml`. If you don't have a custom domain to use, you can use `<YOUR-USERNAME>.github.io` (which GitHub gives to you for free!) as your domain. Here, we take advantage of GitHub's free page hosting to deploy our site. Change `baseURL` in `/config.toml`.
[Reference.](https://github.com/jackyzha0/quartz/blob/hugo/config.toml) Make sure that your `baseURL` has a trailing `/`!
[Reference `config.toml` here](https://github.com/jackyzha0/quartz/blob/hugo/config.toml)
```toml ```toml
baseURL = "https://<YOUR-DOMAIN>/" baseURL = "https://<YOUR-DOMAIN>/"
``` ```
If you are using this under a subdomain (e.g. `<YOUR-GITHUB-USERNAME>.github.io/quartz`), include the trailing `/`.
```toml
baseURL = "https://<YOUR-GITHUB-USERNAME>.github.io/quartz/"
```
Change `cname` in `/.github/workflows/deploy.yaml`. Again, if you don't have a custom domain to use, you can use `<YOUR-USERNAME>.github.io`. Change `cname` in `/.github/workflows/deploy.yaml`. Again, if you don't have a custom domain to use, you can use `<YOUR-USERNAME>.github.io`.
[Reference.](https://github.com/jackyzha0/quartz/blob/hugo/.github/workflows/deploy.yaml) Please note that the `cname` field should *not* have any path `e.g. end with /quartz` or have a trailing `/`.
[Reference `deploy.yaml` here](https://github.com/jackyzha0/quartz/blob/hugo/.github/workflows/deploy.yaml)
```yaml ```yaml
- name: Deploy - name: Deploy
@ -59,22 +73,7 @@ Change `cname` in `/.github/workflows/deploy.yaml`. Again, if you don't have a c
cname: <YOUR-DOMAIN> cname: <YOUR-DOMAIN>
``` ```
### Registrar Have a custom domain? [Learn how to set it up with Quartz ](notes/custom%20Domain.md).
This step is only applicable if you are using a **custom domain**! If you are using `<YOUR-USERNAME>.github.io`, you can skip this step.
For this last bit to take effect, you also need to create a CNAME record with the DNS provider you register your domain with (i.e. NameCheap, Google Domains).
GitHub has some [documentation on this](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site), but the tldr; is to
1. Go to your forked repository (`github.com/<YOUR-GITHUB-USERNAME>/quartz`) settings page and go to the Pages tab. Under "Custom domain", type your custom domain, then click **Save**.
2. Go to your DNS Provider and create a CNAME record that points from your domain to `<YOUR-GITHUB-USERNAME.github.io.` (yes, with the trailing period).
![Example Configuration for Quartz](/notes/images/google-domains.png)*Example Configuration for Quartz*
3. Wait 30 minutes to an hour for the network changes to kick in.
4. Done!
## External Hosting
Don't want to use GitHub Pages? Hugo builds everything for you! Everything is a packaged set of static files ready to deploy in `/public`. You can then upload this folder to a cloud provider to deploy. Alternatively, most providers also give users the option to link a GitHub repository and a folder to deploy. When doing this, ensure you select `/public` folder under the `master` branch.
--- ---
@ -82,4 +81,4 @@ Now that your Quartz is live, let's figure out how to make Quartz really *yours*
🎨 [Customizing Quartz](notes/config.md) 🎨 [Customizing Quartz](notes/config.md)
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md). Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).

View File

@ -1,26 +1,30 @@
--- ---
title: "Obsidian Vault Integration" title: "Obsidian Vault Integration"
tags:
- setup
--- ---
## Setup ## Setup
Obsidian is the preferred way to use Quartz. You can either create a new Obsidian Vault or link one that your already have. Obsidian is the preferred way to use Quartz. You can either create a new Obsidian Vault or link one that your already have.
### New Vault ### New Vault
If you don't have an existing Vault, [download Obsidian](https://obsidian.md/) and create a new Vault in the `/content` folder that you created and cloned during the [setup](notes/setup.md). If you don't have an existing Vault, [download Obsidian](https://obsidian.md/) and create a new Vault in the `/content` folder that you created and cloned during the [setup](notes/setup.md) step.
### Linking an existing Vault ### Linking an existing Vault
The easiest way to use an existing Vault is to copy all of our files (directory and hierarchies intact) into the `/content` folder. The easiest way to use an existing Vault is to copy all of your files (directory and hierarchies intact) into the `/content` folder.
## Settings ## Settings
Great, now that you have your Obsidian linked to your Quartz, let's fix some settings so that they play well. Great, now that you have your Obsidian linked to your Quartz, let's fix some settings so that they play well.
Under Options > Files and Links, set the New link format to always be Absolute Path in Vault and disabled WikiLinks so Obsidian generates regular Markdown links. 1. Under Options > Files and Links, set the New link format to always be Absolute Path in Vault and disabled WikiLinks so Obsidian generates regular Markdown links.
2. Go to Settings > Files & Links > Turn "on" automatically update internal links.
3. Use [[Wikilinks]] should be off (Wikilinks are not officially supported yet)
![Obsidian Settings](/notes/images/obsidian-settings.png)*Obsidian Settings* ![Obsidian Settings](/notes/images/obsidian-settings.png)*Obsidian Settings*
## Templates ## Templates
Inserting front matter everytime you want to create a new Note gets really annoying really quickly. Luckily, Obsidian supports templates which makes inserting new content really easily. Inserting front matter everytime you want to create a new Note gets annoying really quickly. Luckily, Obsidian supports templates which makes inserting new content really easily.
**If you decide to overwrite the `/content` folder completely, don't remove the `/content/templates` folder!** **If you decide to overwrite the `/content` folder completely, don't remove the `/content/templates` folder!**
Head over to Options > Core Plugins and enable the Templates plugin. Then go to Options > Hotkeys and set a hotkey for 'Insert Template'. That way, when you create a new note, you can just press the hotkey for a new template and be ready to go! Head over to Options > Core Plugins and enable the Templates plugin. Then go to Options > Hotkeys and set a hotkey for 'Insert Template' (I recomment `[cmd]+T`). That way, when you create a new note, you can just press the hotkey for a new template and be ready to go!

View File

@ -4,24 +4,28 @@ title: "Preview Changes"
If you'd like to preview what your Quartz site looks like before deploying it to the internet, here's exactly how to do that! If you'd like to preview what your Quartz site looks like before deploying it to the internet, here's exactly how to do that!
Note that both of these steps need to be completed.
## Install `hugo-obsidian` ## Install `hugo-obsidian`
This step will generate the list of backlinks for Hugo to parse. Ensure you have [Go](https://golang.org/doc/install) (>= 1.16) installed. This step will generate the list of backlinks for Hugo to parse. Ensure you have [Go](https://golang.org/doc/install) (>= 1.16) installed.
```shell ```shell
# Install and link `hugo-obsidian` locally # Install and link `hugo-obsidian` locally
$ go install github.com/jackyzha0/hugo-obsidian $ go install github.com/jackyzha0/hugo-obsidian@latest
# Navigate to your local Quartz folder # Navigate to your local Quartz folder
$ cd <location-of-your-local-quartz> $ cd <location-of-your-local-quartz>
# Scrape all links in your Quartz folder and generate info for Quartz # Scrape all links in your Quartz folder and generate info for Quartz
$ hugo-obsidian -input=content -output=data -index=true $ hugo-obsidian -input=content -output=assets/indices -index -root=.
``` ```
If you are running into an error saying that `command not found: hugo-obsidian`, make sure you set your `GOPATH` correctly! This will allow your terminal to correctly recognize hugo-obsidian as an executable.
Afterwards, start the Hugo server as shown above and your local backlinks and interactive graph should be populated! Afterwards, start the Hugo server as shown above and your local backlinks and interactive graph should be populated!
## Installing Hugo ## Installing Hugo
Hugo is the static site generator that powers Quartz. If you'd like to preview your site locally, [install Hugo](https://gohugo.io/getting-started/installing/). Hugo is the static site generator that powers Quartz. [Install Hugo with "extended" Sass/SCSS version](https://gohugo.io/getting-started/installing/) first. Then,
``` ```
# Navigate to your local Quartz folder # Navigate to your local Quartz folder
@ -31,4 +35,4 @@ $ cd <location-of-your-local-quartz>
$ hugo server $ hugo server
# View your site in a browser at http://localhost:1313/ # View your site in a browser at http://localhost:1313/
``` ```

View File

@ -1,5 +1,7 @@
--- ---
title: "Setup" title: "Setup"
tags:
- setup
--- ---
## Making your own Quartz ## Making your own Quartz
@ -26,4 +28,16 @@ Great! Now you have everything you need to start editing and growing your digita
✏️ [Editing Notes in Quartz](notes/editing.md) ✏️ [Editing Notes in Quartz](notes/editing.md)
Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md). Having problems? Checkout our [FAQ and Troubleshooting guide](notes/troubleshooting.md).
## Updating
Haven't updated Quartz in a while and want all the cool new optimizations?
```shell
# add Quartz as a remote host
git remote add upstream git@github.com:jackyzha0/quartz.git
# index and fetch changes
git fetch upstream
git checkout upstream/hugo -- layouts .github Makefile assets config.toml data layouts static
```

View File

@ -12,5 +12,7 @@ Want to see what Quartz can do? Here are some cool community gardens :)
- [Chloe's Garden](https://garden.chloeabrasada.online/) - [Chloe's Garden](https://garden.chloeabrasada.online/)
- [SlRvb's Site](https://slrvb.github.io/Site/) - [SlRvb's Site](https://slrvb.github.io/Site/)
- [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/) - [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/)
- [Brandon Boswell's Garden](https://brandonkboswell.com)
- [Siyang's Courtyard](https://siyangsun.github.io/courtyard/)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/hugo/content/moc/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/hugo/content/notes/showcase.md)!

View File

@ -2,7 +2,24 @@
title: "Troubleshooting and FAQ" title: "Troubleshooting and FAQ"
--- ---
## Common Pitfalls Still having trouble? Here are a list of common questions and problems people encounter when installing Quartz.
While you're here, join our [Discord](https://discord.gg/cRFFHYye7t) :)
### My GitHub pages is just showing the README and not Quartz
Make sure you set the source to deploy from `master` (and not `hugo`) using `/ (root)`! See more in the [hosting](/notes/hosting) guide
### Some of my pages have 'January 1, 0001' as the last modified date
This is a problem caused by `git` treating files as case-insensitive by default and some of your posts probably have capitalized file names. You can turn this off in your Quartz by running this command.
```shell
# in the root of your Quartz (same folder as config.toml)
git config core.ignorecase true
# or globally (not recommended)
git config --global core.ignorecase true
```
### Can I publish only a subset of my pages? ### Can I publish only a subset of my pages?
Yes! Quartz makes selective publishing really easy. Heres a guide on [excluding pages from being published](notes/ignore%20notes.md). Yes! Quartz makes selective publishing really easy. Heres a guide on [excluding pages from being published](notes/ignore%20notes.md).
@ -25,7 +42,7 @@ source ~/.bash_profile
``` ```
### How come my notes aren't being rendered? ### How come my notes aren't being rendered?
You probably forgot to include front matter in your Markdown files. You can either setup [Obsidian](notes/obsidian) to do this for you or you need to manually define it. More details in [the 'how to edit' guide](notes/editing.md). You probably forgot to include front matter in your Markdown files. You can either setup [obsidian](notes/obsidian.md) to do this for you or you need to manually define it. More details in [the 'how to edit' guide](notes/editing.md).
### My custom domain isn't working! ### My custom domain isn't working!
Walk through the steps in [the hosting guide](notes/hosting.md) again. Make sure you wait 30 min to 1 hour for changes to take effect. Walk through the steps in [the hosting guide](notes/hosting.md) again. Make sure you wait 30 min to 1 hour for changes to take effect.

View File

@ -1,10 +1,11 @@
name: Jacky Zhao name: Jacky Zhao
enableToc: true enableToc: true
enableLinkPreview: true
description: description:
Here is the page description. This is an example Quartz site that details installation, Here is the page description. This is an example Quartz site that details installation,
setup, customization, and troubleshooting for Quartz itself. setup, customization, and troubleshooting for Quartz itself.
page_title: page_title:
Quartz Example Page "🪴 Quartz 3.1"
links: links:
- link_name: Twitter - link_name: Twitter
link: https://twitter.com/_jzhao link: https://twitter.com/_jzhao

View File

@ -1,5 +1,6 @@
enableLegend: false enableLegend: false
enableDrag: true enableDrag: true
enableZoom: true enableZoom: true
depth: -1 # set to -1 to show full graph
paths: paths:
- /moc: "#4388cc" - /moc: "#4388cc"

View File

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

View File

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

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
<header>
<h1 id="page-title"><a href="{{ .Site.BaseURL }}">{{ .Site.Data.config.page_title }}</a></h1>
<svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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 class="spacer"></div>
{{partial "darkmode.html" .}}
</header>
<article>
<h1>All {{.Title}}</h1>
{{partial "page-list.html" .Paginator.Pages.ByLastmod.Reverse }}
{{ template "_internal/pagination.html" .}}
</article>
{{partial "contact.html" .}}
</div>
{{partial "popover.html" .}}
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{ .Lang }}">
{{ partial "head.html" . }} {{ partial "head.html" . }}
<body> <body>
@ -7,21 +7,32 @@
<div class="singlePage"> <div class="singlePage">
<!-- Begin actual content --> <!-- Begin actual content -->
<header> <header>
{{if .Title}}<h1>{{ .Title }}</h1>{{end}} <h1 id="page-title"><a href="{{ .Site.BaseURL }}">{{ .Site.Data.config.page_title }}</a></h1>
<svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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> <svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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 class="spacer"></div> <div class="spacer"></div>
{{partial "darkmode.html" .}} {{partial "darkmode.html" .}}
</header> </header>
<article> <article>
{{if $.Site.Data.config.enableToc}} {{if .Title}}<h1>{{ .Title }}</h1>{{end}}
<aside class="mainTOC"> <p class="meta">
<h3>Table of Contents</h3> Last updated {{if ne .Date .Lastmod}}{{ .Lastmod.Format "January 2, 2006" }}{{else}}Unknown{{end}}
{{ .TableOfContents }} </p>
</aside> <ul class="tags">
{{end}} {{ range (.GetTerms "tags") }}
{{.Content}} <li><a href="{{ .Permalink }}">{{ .LinkTitle | humanize }}</a></li>
{{ end }}
</ul>
{{ if (and $.Site.Data.config.enableToc (not (.Params.disableToc))) }}
<aside class="mainTOC">
<h3>Table of Contents</h3>
{{ .TableOfContents }}
</aside>
{{end}}
{{.Content}}
</article> </article>
{{partial "footer.html" .}} {{partial "footer.html" .}}
{{partial "popover.html" .}}
</div> </div>
</body> </body>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
<header>
<h1 id="page-title"><a href="{{ .Site.BaseURL }}">{{ .Site.Data.config.page_title }}</a></h1>
<svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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 class="spacer"></div>
{{partial "darkmode.html" .}}
</header>
<article>
<h1>All {{.Title}}</h1>
<div class="tags">
{{ range .Site.Taxonomies.tags.ByCount }}
<div class="meta">
<h1><a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a></h1>
<p><b>{{ .Count }}</b> notes with this tag {{if gt .Count 2}}(showing first 2 results){{end}}</p>
</div>
{{ with ($.Site.GetPage (printf "/tags/%s" .Page.Title)) }}
{{partial "page-list.html" (first 2 .Pages.ByLastmod.Reverse)}}
{{ end }}
{{ end }}
</div>
</article>
{{partial "contact.html" .}}
</div>
{{partial "popover.html" .}}
</body>
</html>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
{{ partial "head.html" . }}
<body>
{{partial "search.html" .}}
<div class="singlePage">
<!-- Begin actual content -->
<header>
<h1 id="page-title"><a href="{{ .Site.BaseURL }}">{{ .Site.Data.config.page_title }}</a></h1>
<svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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 class="spacer"></div>
{{partial "darkmode.html" .}}
</header>
<article>
<h1>Tag: {{.Title | humanize}}</h1>
{{partial "page-list.html" .Paginator.Pages}}
{{ template "_internal/pagination.html" . }}
</article>
{{partial "contact.html" .}}
</div>
{{partial "popover.html" .}}
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{ .Lang }}">
{{ partial "head.html" . }} {{ partial "head.html" . }}
<body> <body>
@ -7,13 +7,13 @@
<div class="singlePage"> <div class="singlePage">
<!-- Begin actual content --> <!-- Begin actual content -->
<header> <header>
{{if .Title}}<h1>{{ .Title }}</h1>{{end}} <h1>{{if .Title}}{{ .Title }}{{else}}Untitled{{end}}</h1>
<svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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> <svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open 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 class="spacer"></div> <div class="spacer"></div>
{{partial "darkmode.html" .}} {{partial "darkmode.html" .}}
</header> </header>
<article> <article>
{{if $.Site.Data.config.enableToc}} {{ if (and $.Site.Data.config.enableToc (not (.Params.disableToc))) }}
<aside class="mainTOC"> <aside class="mainTOC">
<h3>Table of Contents</h3> <h3>Table of Contents</h3>
{{ .TableOfContents }} {{ .TableOfContents }}
@ -22,6 +22,7 @@
{{- .Content -}} {{- .Content -}}
</article> </article>
{{partial "footer.html" .}} {{partial "footer.html" .}}
{{partial "popover.html" .}}
</div> </div>
</body> </body>
</html> </html>

View File

@ -3,14 +3,14 @@
{{$url := urls.Parse .Site.BaseURL }} {{$url := urls.Parse .Site.BaseURL }}
{{$host := strings.TrimRight "/" $url.Path }} {{$host := strings.TrimRight "/" $url.Path }}
{{$curPage := strings.TrimPrefix $host (strings.TrimRight "/" .Page.RelPermalink) }} {{$curPage := strings.TrimPrefix $host (strings.TrimRight "/" .Page.RelPermalink) }}
{{$inbound := index $.Site.Data.linkIndex.index.backlinks $curPage}} {{$linkIndex := getJSON "/assets/indices/linkIndex.json"}}
{{$inbound := index $linkIndex.index.backlinks $curPage}}
{{$contentTable := getJSON "/assets/indices/contentIndex.json"}}
{{if $inbound}} {{if $inbound}}
{{- range $inbound -}} {{$cleanedInbound := apply (apply $inbound "index" "." "source") "replace" "." " " "-"}}
{{$src := index . "source"}} {{- range $cleanedInbound | uniq -}}
{{$src = replace $src " " "-"}}
<li> <li>
<a href="{{$src}}">{{index . "source"}}</a> <a href="{{$url}}{{.}}">{{index (index $contentTable .) "title"}}</a>
</li> </li>
{{- end -}} {{- end -}}
{{else}} {{else}}
@ -18,4 +18,4 @@
No backlinks found No backlinks found
</li> </li>
{{end}} {{end}}
</ul> </ul>

View File

@ -0,0 +1,12 @@
<!-- Contact Info -->
<div id="contact_buttons">
<footer>
<p>Made by {{ $.Site.Data.config.name }} using <a href="https://github.com/jackyzha0/quartz">Quartz</a>, © {{ dateFormat "2006" now }}</p>
{{ if not .IsHome }}
<a href="/">Home</a>
{{end}}
{{- range $.Site.Data.config.links -}}
<a href="{{.link}}">{{.link_name}}</a>
{{- end -}}
</footer>
</div>

View File

@ -8,15 +8,4 @@
</div> </div>
</div> </div>
<!-- Contact Info --> {{partial "contact.html" .}}
<div id="contact_buttons">
<footer>
<p>Made by {{ $.Site.Data.config.name }} using <a href="https://github.com/jackyzha0/quartz">Quartz</a>, © {{ dateFormat "2006" now }}</p>
{{ if not .IsHome }}
<a href="/">Home</a>
{{end}}
{{- range $.Site.Data.config.links -}}
<a href="{{.link}}">{{.link_name}}</a>
{{- end -}}
</footer>
</div>

View File

@ -1,4 +1,4 @@
<script src="https://cdn.jsdelivr.net/npm/d3@6"></script> <script src="https://cdn.jsdelivr.net/npm/d3@6.7.0/dist/d3.min.js" integrity="sha256-+7jaYCp29O1JusNWHaYtgUn6EhuP0VaFuswhNV06MyI=" crossorigin="anonymous"></script>
<h3>Interactive Graph</h3> <h3>Interactive Graph</h3>
<div id="graph-container"></div> <div id="graph-container"></div>
<style> <style>
@ -10,210 +10,16 @@
--g-link-active: #5a7282; --g-link-active: #5a7282;
} }
</style> </style>
{{ $js := resources.Get "js/graph.js" | resources.Fingerprint "md5" }}
<script src="{{ $js.Permalink }}"></script>
<script> <script>
const index = {{$.Site.Data.linkIndex.index}} drawGraph(
const links = {{$.Site.Data.linkIndex.links}} {{strings.TrimRight "/" .Page.Permalink}},
const curPage = {{ strings.TrimRight "/" .Page.RelPermalink }} {{strings.TrimRight "/" .Site.BaseURL}},
const pathColors = {{$.Site.Data.graphConfig.paths}} {{$.Site.Data.graphConfig.paths}},
{{$.Site.Data.graphConfig.depth}},
const parseIdsFromLinks = (links) => [...(new Set(links.flatMap(link => ([link.source, link.target]))))] {{$.Site.Data.graphConfig.enableDrag}},
{{$.Site.Data.graphConfig.enableLegend}},
const data = { {{$.Site.Data.graphConfig.enableZoom}}
nodes: parseIdsFromLinks(links).map(id => ({id})), )
links, </script>
}
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 enableDrag = {{$.Site.Data.graphConfig.enableDrag}}
const noop = () => {}
return d3.drag()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop);
}
const height = 250
const width = document.getElementById("graph-container").offsetWidth
const simulation = d3.forceSimulation(data.nodes)
.force("charge", d3.forceManyBody().strength(-20))
.force("link", d3.forceLink(data.links)
.id(d => d.id)
)
.force("center", d3.forceCenter());
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr("viewBox", [-width / 2, -height / 2, width, height]);
// legend
const enableLegend = {{$.Site.Data.graphConfig.enableLegend}}
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")
// draw individual nodes
const node = graphNode.append("circle")
.attr("class", "node")
.attr("id", (d) => d.id)
.attr("r", (d) => {
const numOut = index.links[d.id]?.length || 0
const numIn = index.backlinks[d.id]?.length || 0
return 3 + (numOut + numIn) / 4
})
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
window.location.href = {{.Site.BaseURL}} + d.id.replace(" ", "-").replace("%20", "-");
})
.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
const links = 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
links
.transition()
.duration(200)
.attr("stroke", "var(--g-link-active)")
// show text for self
d3.select(this.parentNode)
.select("text")
.raise()
.transition()
.duration(200)
.style("opacity", 1)
}).on("mouseleave", function (_,d) {
d3.selectAll(".node")
.transition()
.duration(200)
.attr("fill", color)
const currentId = d.id
const links = d3.selectAll(".link").filter(d => d.source.id === currentId || d.target.id === currentId)
links
.transition()
.duration(200)
.attr("stroke", "var(--g-link)")
d3.select(this.parentNode)
.select("text")
.transition()
.duration(200)
.style("opacity", 0)
})
.call(drag(simulation));
// draw labels
const labels = graphNode.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text((d) => d.id.replace("%20", "-"))
.style("opacity", 0)
.style("pointer-events", "none")
.call(drag(simulation));
// set panning
const enableZoom = {{$.Site.Data.graphConfig.enableZoom}}
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);
labels.attr("transform", transform);
}));
}
// 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);
});
</script>

View File

@ -1,27 +1,45 @@
<head> <head>
<!-- Meta tags --> <!-- Meta tags -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="description" content="{{$.Site.Data.config.description}}"> <meta name="description" content="{{if .IsHome}}{{$.Site.Data.config.description}}{{else}}{{.Summary}}{{end}}">
<title>{{ if .Title }}{{ .Title }}{{ else }}{{ $.Site.Data.config.page_title }}{{ end }}</title> <title>{{ if .Title }}{{ .Title }}{{ else }}{{ $.Site.Data.config.page_title }}{{ end }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="/icon.png" /> <link rel="shortcut icon" type="image/png" href="/icon.png" />
<!-- CSS Stylesheets and Fonts --> <!-- CSS Stylesheets and Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Source+Sans+Pro:wght@400;700&family=Fira+Code:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Source+Sans+Pro:wght@400;600;700&family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
{{ $css := slice "custom.scss" "base.scss" "darkmode.scss" "syntax.scss"}} {{$sass := resources.Match "styles/[!_]*.scss" }}
{{range $css}} {{$css := slice }}
{{$sass := resources.Get . | resources.ToCSS }} {{range $sass}}
{{with $sass | minify}} {{$scss := . | resources.ToCSS (dict "outputStyle" "compressed") }}
<style> {{$css = $css | append $scss}}
{{.Content | safeCSS}}
</style>
{{end}}
{{end}} {{end}}
{{$finalCss := $css | resources.Concat "styles.css" | resources.Fingerprint "md5" | resources.Minify }}
<link href="{{$finalCss.Permalink}}" rel="stylesheet">
{{- with resources.Get "darkmode.js" | minify -}} {{ $darkMode := resources.Get "js/darkmode.js" | resources.Fingerprint "md5" | resources.Minify }}
<script src="{{$darkMode.Permalink}}"></script>
<!-- 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> <script>
{{.Content | safeJS }} 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,
}))
</script> </script>
{{- end -}}
</head> </head>
{{ template "_internal/google_analytics.html" . }} {{ template "_internal/google_analytics.html" . }}

View File

@ -0,0 +1,15 @@
<ul class="section-ul">
{{- range . -}}
<li class="section-li">
<div class="section">
<div class="desc">
<h3><a href="{{ .Permalink }}">{{- .Title -}}</a></h3>
<p>{{- .Summary -}}{{if .Truncated}}...{{end}}</p>
</div>
<p class="meta">
{{ .ReadingTime }} minute read. Last updated {{if ne .Date .Lastmod}}{{ .Lastmod.Format "January 2, 2006" }}{{else}}Unknown{{end}}
</p>
</div>
</li>
{{- end -}}
</ul>

View File

@ -0,0 +1,7 @@
{{if $.Site.Data.config.enableLinkPreview}}
{{ $js := resources.Get "js/popover.js" | resources.Fingerprint "md5" | resources.Minify }}
<script src="{{ $js.Permalink }}"></script>
<script>
initPopover({{strings.TrimRight "/" .Site.BaseURL }})
</script>
{{end}}

View File

@ -5,251 +5,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/gh/nextapps-de/flexsearch@0.7.2/dist/flexsearch.bundle.js"></script> <script src="https://cdn.jsdelivr.net/npm/flexsearch@0.7.21/dist/flexsearch.bundle.js" integrity="sha256-i3A0NZGkhsKjVMzFxv3ksk0DZh3aXqu0l49Bbh0MdjE=" crossorigin="anonymous" defer></script>
<script> {{ $js := resources.Get "js/search.js" | resources.Fingerprint "md5" | resources.Minify }}
// code from https://github.com/danestves/markdown-to-text <script defer src="{{ $js.Permalink }}"></script>
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(/\s{0,2}\[.*?\]: .*?$/g, "")
.replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "")
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "$1")
.replace(/^\s{0,3}>\s?/g, "")
.replace(/(^|\n)\s{0,3}>\s?/g, "\n\n")
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, "")
.replace(
/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm,
"$1$2$3"
)
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
.replace(/(`{3,})(.*?)\1/gm, "$2")
.replace(/`(.+?)`/g, "$1")
.replace(/\n{2,}/g, "\n\n");
} catch (e) {
console.error(e);
return markdown;
}
return output;
};
</script>
<script>
const contentIndex = new FlexSearch.Document({
cache: true,
charset: "latin:extra",
optimize: true,
worker: true,
document: {
index: [{
field: "content",
tokenize: "strict",
context: {
resolution: 5,
depth: 3,
bidirectional: true
},
suggest: true,
}, {
field: "title",
tokenize: "forward",
}]
}
})
const scrapedContent = {{$.Site.Data.contentIndex}}
for (const [key, value] of Object.entries(scrapedContent)) {
contentIndex.add({
id: key,
title: value.title,
content: removeMarkdown(value.content),
})
}
const highlight = (content, term) => {
const highlightWindow = 20
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 ? "" : "..."}`
}
const resultToHTML = ({url, title, content, term}) => {
const text = removeMarkdown(content)
const resultTitle = highlight(title, term)
const resultText = highlight(text, term)
return `<button class="result-card" id="${url}">
<h3>${resultTitle}</h3>
<p>${resultText}</p>
</button>`
}
const redir = (id, term) => {
window.location.href = "{{.Site.BaseURL}}" + `${id}#:~:text=${encodeURIComponent(term)}`
}
const fetch = id => ({
id,
url: id,
title: scrapedContent[id].title,
content: scrapedContent[id].content
})
const source = document.getElementById('search-bar')
const results = document.getElementById("results-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', (e) => {
term = e.target.value
contentIndex.search(term, [
{
field: "content",
limit: 10,
suggest: true,
},
{
field: "title",
limit: 5,
}
]).then(searchResults => {
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(fetch)
// display
if (finalResults.length === 0) {
results.innerHTML = `<div class="result-card">
<p>No results.</p>
</div>`
} else {
results.innerHTML = finalResults
.map(result => resultToHTML({
...result,
term,
}))
.join("\n")
const anchors = document.getElementsByClassName("result-card");
[...anchors].forEach(anchor => {
anchor.onclick = () => redir(anchor.id, term)
})
}
})
})
const searchContainer = document.getElementById("search-container")
function openSearch() {
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() {
searchContainer.style.display = "none"
}
document.addEventListener('keydown', (event) => {
if (event.key === "/") {
event.preventDefault()
openSearch()
}
if (event.key === "Escape") {
event.preventDefault()
closeSearch()
}
})
window.addEventListener('DOMContentLoaded', () => {
const searchButton = document.getElementById("search-icon")
searchButton.addEventListener('click', (evt) => {
openSearch()
})
searchButton.addEventListener('keydown', (evt) => {
openSearch()
})
searchContainer.addEventListener('click', (evt) => {
closeSearch()
})
document.getElementById("search-space").addEventListener('click', (evt) => {
evt.stopPropagation()
})
})
</script>

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB