mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-01-13 23:28:40 +01:00
Compare commits
9 Commits
translatio
...
2acdec323f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2acdec323f | ||
|
|
9e58857746 | ||
|
|
4bd714b7be | ||
|
|
78e13bcb40 | ||
|
|
7d49dff074 | ||
|
|
cdebd05dc9 | ||
|
|
2a9290b3df | ||
|
|
771c05ff18 | ||
|
|
6dd772bf00 |
46
.github/workflows/preview.yaml
vendored
Normal file
46
.github/workflows/preview.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build Preview Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-preview:
|
||||
if: ${{ github.repository == 'jackyzha0/quartz' }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Preview
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v4
|
||||
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: Build Quartz
|
||||
run: npx quartz build -d docs -v
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: AdrianGonz97/refined-cf-pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
projectName: quartz
|
||||
directory: public
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||
"@napi-rs/simple-git": "0.1.19",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"@webgpu/types": "^0.1.60",
|
||||
"ansi-truncate": "^1.2.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.4.1",
|
||||
@@ -2001,9 +2002,9 @@
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.44",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz",
|
||||
"integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==",
|
||||
"version": "0.1.60",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
|
||||
"integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@myriaddreamin/rehype-typst": "^0.5.4",
|
||||
"@napi-rs/simple-git": "0.1.19",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"@webgpu/types": "^0.1.60",
|
||||
"ansi-truncate": "^1.2.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.4.1",
|
||||
|
||||
@@ -86,6 +86,7 @@ const config: QuartzConfig = {
|
||||
}),
|
||||
Plugin.Assets(),
|
||||
Plugin.Static(),
|
||||
Plugin.Favicon(),
|
||||
Plugin.NotFoundPage(),
|
||||
// Comment out CustomOgImages to speed up build time
|
||||
Plugin.CustomOgImages(),
|
||||
|
||||
@@ -589,7 +589,8 @@ export async function handleSync(argv) {
|
||||
await popContentFolder(contentFolder)
|
||||
if (argv.push) {
|
||||
console.log("Pushing your changes")
|
||||
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], {
|
||||
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim()
|
||||
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
if (res.status !== 0) {
|
||||
|
||||
@@ -75,7 +75,7 @@ function renderTranscludes(
|
||||
const classNames = (node.properties?.className ?? []) as string[]
|
||||
if (classNames.includes("transclude")) {
|
||||
const inner = node.children[0] as Element
|
||||
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||
const transcludeTarget = (inner.properties["data-slug"] ?? slug) as FullSlug
|
||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
||||
if (!page) {
|
||||
return
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
function toggleCallout(this: HTMLElement) {
|
||||
const outerBlock = this.parentElement!
|
||||
outerBlock.classList.toggle("is-collapsed")
|
||||
const content = outerBlock.getElementsByClassName("callout-content")[0] as HTMLElement
|
||||
if (!content) return
|
||||
const collapsed = outerBlock.classList.contains("is-collapsed")
|
||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||
outerBlock.style.maxHeight = height + "px"
|
||||
|
||||
// walk and adjust height of all parents
|
||||
let current = outerBlock
|
||||
let parent = outerBlock.parentElement
|
||||
while (parent) {
|
||||
if (!parent.classList.contains("callout")) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapsed = parent.classList.contains("is-collapsed")
|
||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||
parent.style.maxHeight = height + "px"
|
||||
|
||||
current = parent
|
||||
parent = parent.parentElement
|
||||
}
|
||||
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
|
||||
}
|
||||
|
||||
function setupCallout() {
|
||||
@@ -27,15 +12,15 @@ function setupCallout() {
|
||||
`callout is-collapsible`,
|
||||
) as HTMLCollectionOf<HTMLElement>
|
||||
for (const div of collapsible) {
|
||||
const title = div.firstElementChild
|
||||
if (!title) continue
|
||||
const title = div.getElementsByClassName("callout-title")[0] as HTMLElement
|
||||
const content = div.getElementsByClassName("callout-content")[0] as HTMLElement
|
||||
if (!title || !content) continue
|
||||
|
||||
title.addEventListener("click", toggleCallout)
|
||||
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
|
||||
|
||||
const collapsed = div.classList.contains("is-collapsed")
|
||||
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||
div.style.maxHeight = height + "px"
|
||||
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,18 @@ let currentExplorerState: Array<FolderState>
|
||||
function toggleExplorer(this: HTMLElement) {
|
||||
const nearestExplorer = this.closest(".explorer") as HTMLElement
|
||||
if (!nearestExplorer) return
|
||||
nearestExplorer.classList.toggle("collapsed")
|
||||
const explorerCollapsed = nearestExplorer.classList.toggle("collapsed")
|
||||
nearestExplorer.setAttribute(
|
||||
"aria-expanded",
|
||||
nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||
)
|
||||
|
||||
if (!explorerCollapsed) {
|
||||
// Stop <html> from being scrollable when mobile explorer is open
|
||||
document.documentElement.classList.add("mobile-no-scroll")
|
||||
} else {
|
||||
document.documentElement.classList.remove("mobile-no-scroll")
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFolder(evt: MouseEvent) {
|
||||
@@ -270,12 +277,25 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
if (mobileExplorer.checkVisibility()) {
|
||||
explorer.classList.add("collapsed")
|
||||
explorer.setAttribute("aria-expanded", "false")
|
||||
|
||||
// Allow <html> to be scrollable when mobile explorer is collapsed
|
||||
document.documentElement.classList.remove("mobile-no-scroll")
|
||||
}
|
||||
|
||||
mobileExplorer.classList.remove("hide-until-loaded")
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
// Desktop explorer opens by default, and it stays open when the window is resized
|
||||
// to mobile screen size. Applies `no-scroll` to <html> in this edge case.
|
||||
const explorer = document.querySelector(".explorer")
|
||||
if (explorer && !explorer.classList.contains("collapsed")) {
|
||||
document.documentElement.classList.add("mobile-no-scroll")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
||||
}
|
||||
|
||||
@@ -68,6 +68,15 @@ type TweenNode = {
|
||||
stop: () => void
|
||||
}
|
||||
|
||||
async function determineGraphicsAPI(): Promise<"webgpu" | "webgl"> {
|
||||
const adapter = await navigator.gpu?.requestAdapter().catch(() => null)
|
||||
if (!adapter) {
|
||||
return "webgl"
|
||||
}
|
||||
// Devices with WebGPU but no float32-blendable feature fail to render the graph
|
||||
return adapter.features.has("float32-blendable") ? "webgpu" : "webgl"
|
||||
}
|
||||
|
||||
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
|
||||
const slug = simplifySlug(fullSlug)
|
||||
const visited = getVisited()
|
||||
@@ -349,6 +358,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
|
||||
tweens.forEach((tween) => tween.stop())
|
||||
tweens.clear()
|
||||
|
||||
const pixiPreference = await determineGraphicsAPI()
|
||||
const app = new Application()
|
||||
await app.init({
|
||||
width,
|
||||
@@ -357,7 +367,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
|
||||
autoStart: false,
|
||||
autoDensity: true,
|
||||
backgroundAlpha: 0,
|
||||
preference: "webgpu",
|
||||
preference: pixiPreference,
|
||||
resolution: window.devicePixelRatio,
|
||||
eventMode: "static",
|
||||
})
|
||||
|
||||
@@ -3,12 +3,13 @@ import { normalizeRelativeURLs } from "../../util/path"
|
||||
import { fetchCanonical } from "./util"
|
||||
|
||||
const p = new DOMParser()
|
||||
let activeAnchor: HTMLAnchorElement | null = null
|
||||
|
||||
async function mouseEnterHandler(
|
||||
this: HTMLAnchorElement,
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
) {
|
||||
const link = this
|
||||
const link = (activeAnchor = this)
|
||||
if (link.dataset.noPopover === "true") {
|
||||
return
|
||||
}
|
||||
@@ -44,10 +45,9 @@ async function mouseEnterHandler(
|
||||
targetUrl.search = ""
|
||||
const popoverId = `popover-${link.pathname}`
|
||||
const prevPopoverElement = document.getElementById(popoverId)
|
||||
const hasAlreadyBeenFetched = () => !!document.getElementById(popoverId)
|
||||
|
||||
// dont refetch if there's already a popover
|
||||
if (hasAlreadyBeenFetched()) {
|
||||
if (!!document.getElementById(popoverId)) {
|
||||
showPopover(prevPopoverElement as HTMLElement)
|
||||
return
|
||||
}
|
||||
@@ -56,11 +56,6 @@ async function mouseEnterHandler(
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
// bailout if another popover exists
|
||||
if (hasAlreadyBeenFetched()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!response) return
|
||||
const [contentType] = response.headers.get("Content-Type")!.split(";")
|
||||
const [contentTypeCategory, typeInfo] = contentType.split("/")
|
||||
@@ -107,11 +102,20 @@ async function mouseEnterHandler(
|
||||
elts.forEach((elt) => popoverInner.appendChild(elt))
|
||||
}
|
||||
|
||||
if (!!document.getElementById(popoverId)) {
|
||||
return
|
||||
}
|
||||
|
||||
document.body.appendChild(popoverElement)
|
||||
if (activeAnchor !== this) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopover(popoverElement)
|
||||
}
|
||||
|
||||
function clearActivePopover() {
|
||||
activeAnchor = null
|
||||
const allPopoverElements = document.querySelectorAll(".popover")
|
||||
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
|
||||
}
|
||||
|
||||
@@ -263,22 +263,8 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||
}
|
||||
}
|
||||
|
||||
.no-scroll {
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html:has(.no-scroll) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media all and not ($mobile) {
|
||||
.no-scroll {
|
||||
opacity: 1 !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
html:has(.no-scroll) {
|
||||
overflow: auto !important;
|
||||
.mobile-no-scroll {
|
||||
@media all and ($mobile) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
16
quartz/plugins/emitters/favicon.ts
Normal file
16
quartz/plugins/emitters/favicon.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import sharp from "sharp"
|
||||
import { joinSegments, QUARTZ, FilePath } from "../../util/path"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
|
||||
export const Favicon: QuartzEmitterPlugin = () => ({
|
||||
name: "Favicon",
|
||||
async *emit({ argv }) {
|
||||
const iconPath = joinSegments(QUARTZ, "static", "icon.png")
|
||||
const dest = joinSegments(argv.output, "favicon.ico") as FilePath
|
||||
|
||||
await sharp(iconPath).resize(48, 48).toFormat("png").toFile(dest)
|
||||
|
||||
yield dest
|
||||
},
|
||||
async *partialEmit() {},
|
||||
})
|
||||
@@ -5,6 +5,7 @@ export { ContentIndex as ContentIndex } from "./contentIndex"
|
||||
export { AliasRedirects } from "./aliases"
|
||||
export { Assets } from "./assets"
|
||||
export { Static } from "./static"
|
||||
export { Favicon } from "./favicon"
|
||||
export { ComponentResources } from "./componentResources"
|
||||
export { NotFoundPage } from "./404"
|
||||
export { CNAME } from "./cname"
|
||||
|
||||
@@ -191,7 +191,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
|
||||
|
||||
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
|
||||
const displayAnchor = anchor ? `#${anchor.trim().replace(/^#+/, "")}` : ""
|
||||
const blockRef = Boolean(rawHeader?.startsWith("#^")) ? "^" : ""
|
||||
const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : ""
|
||||
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
||||
const embedDisplay = value.startsWith("!") ? "!" : ""
|
||||
|
||||
@@ -221,7 +222,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
let [rawFp, rawHeader, rawAlias] = capture
|
||||
const fp = rawFp?.trim() ?? ""
|
||||
const anchor = rawHeader?.trim() ?? ""
|
||||
const alias = rawAlias?.slice(1).trim()
|
||||
const alias: string | undefined = rawAlias?.slice(1).trim()
|
||||
|
||||
// embed cases
|
||||
if (value.startsWith("!")) {
|
||||
@@ -463,6 +464,30 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
})
|
||||
}
|
||||
|
||||
// For the rest of the MD callout elements other than the title, wrap them with
|
||||
// two nested HTML <div>s (use some hacked mdhast component to achieve this) of
|
||||
// class `callout-content` and `callout-content-inner` respectively for
|
||||
// grid-based collapsible animation.
|
||||
if (calloutContent.length > 0) {
|
||||
node.children = [
|
||||
node.children[0],
|
||||
{
|
||||
data: { hProperties: { className: ["callout-content"] }, hName: "div" },
|
||||
type: "blockquote",
|
||||
children: [
|
||||
{
|
||||
data: {
|
||||
hProperties: { className: ["callout-content-inner"] },
|
||||
hName: "div",
|
||||
},
|
||||
type: "blockquote",
|
||||
children: [...calloutContent],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// replace first line of blockquote with title and rest of the paragraph text
|
||||
node.children.splice(0, 1, ...blockquoteContent)
|
||||
|
||||
@@ -484,21 +509,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
"data-callout-metadata": calloutMetaData,
|
||||
},
|
||||
}
|
||||
|
||||
// Add callout-content class to callout body if it has one.
|
||||
if (calloutContent.length > 0) {
|
||||
const contentData: BlockContent | DefinitionContent = {
|
||||
data: {
|
||||
hProperties: {
|
||||
className: "callout-content",
|
||||
},
|
||||
hName: "div",
|
||||
},
|
||||
type: "blockquote",
|
||||
children: [...calloutContent],
|
||||
}
|
||||
node.children = [node.children[0], contentData]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
border-radius: 5px;
|
||||
padding: 0 1rem;
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > .callout-content > :first-child {
|
||||
margin-top: 0;
|
||||
& > .callout-content {
|
||||
display: grid;
|
||||
transition: grid-template-rows 0.3s ease;
|
||||
|
||||
& > .callout-content-inner {
|
||||
overflow: hidden;
|
||||
|
||||
& > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');
|
||||
|
||||
Reference in New Issue
Block a user