mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 22:44:14 +02:00
Compare commits
4 Commits
95d57ad29e
...
1fedbfe4df
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1fedbfe4df | ||
![]() |
91189dfd2f | ||
![]() |
fbc45548f7 | ||
![]() |
2e299c67cc |
40
ambient.d.ts
vendored
Normal file
40
ambient.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
declare module "*.scss" {
|
||||
const content: string
|
||||
export = content
|
||||
}
|
||||
|
||||
declare module "$config" {
|
||||
import { QuartzConfig } from "./quartz"
|
||||
|
||||
const config: QuartzConfig
|
||||
export = config
|
||||
}
|
||||
|
||||
declare module "$layout" {
|
||||
import { SharedLayout, PageLayout } from "./quartz/cfg"
|
||||
|
||||
export const sharedPageComponents: SharedLayout
|
||||
export const defaultContentPageLayout: PageLayout
|
||||
export const defaultListPageLayout: PageLayout
|
||||
}
|
||||
|
||||
declare module "$styles" {
|
||||
const content: string
|
||||
export = content
|
||||
}
|
||||
|
||||
declare module "quartz" {
|
||||
// without this the export below does nothing for some reason
|
||||
// sometimes TS is funn
|
||||
import("./quartz")
|
||||
|
||||
export * from "./quartz"
|
||||
}
|
||||
|
||||
// dom custom event
|
||||
interface CustomEventMap {
|
||||
nav: CustomEvent<{ url: FullSlug }>
|
||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
||||
}
|
||||
|
||||
declare const fetchData: Promise<ContentIndex>
|
@ -17,7 +17,7 @@ This question is best answered by tracing what happens when a user (you!) runs `
|
||||
1. A WebSocket server on port 3001 to handle hot-reload signals. This tracks all inbound connections and sends a 'rebuild' message a server-side change is detected (either content or configuration).
|
||||
2. An HTTP file-server on a user defined port (normally 8080) to serve the actual website files.
|
||||
4. If the `--serve` flag is set, it also starts a file watcher to detect source-code changes (e.g. anything that is `.ts`, `.tsx`, `.scss`, or packager files). On a change, we rebuild the module (step 2 above) using esbuild's [rebuild API](https://esbuild.github.io/api/#rebuild) which drastically reduces the build times.
|
||||
5. After transpiling the main Quartz build module (`quartz/build.ts`), we write it to a cache file `.quartz-cache/transpiled-build.mjs` and then dynamically import this using `await import(cacheFile)`. However, we need to be pretty smart about how to bust Node's [import cache](https://github.com/nodejs/modules/issues/307) so we add a random query string to fake Node into thinking it's a new module. This does, however, cause memory leaks so we just hope that the user doesn't hot-reload their configuration too many times in a single session :)) (it leaks about ~350kB memory on each reload). After importing the module, we then invoke it, passing in the command line arguments we parsed earlier along with a callback function to signal the client to refresh.
|
||||
5. After transpiling the main Quartz build module (`quartz/build.ts`), we write it to a cache file `node_modules/.cache/quartz/transpiled-build.mjs` and then dynamically import this using `await import(cacheFile)`. However, we need to be pretty smart about how to bust Node's [import cache](https://github.com/nodejs/modules/issues/307) so we add a random query string to fake Node into thinking it's a new module. This does, however, cause memory leaks so we just hope that the user doesn't hot-reload their configuration too many times in a single session :)) (it leaks about ~350kB memory on each reload). After importing the module, we then invoke it, passing in the command line arguments we parsed earlier along with a callback function to signal the client to refresh.
|
||||
4. In `build.ts`, we start by installing source map support manually to account for the query string cache busting hack we introduced earlier. Then, we start processing content:
|
||||
1. Clean the output directory.
|
||||
2. Recursively glob all files in the `content` folder, respecting the `.gitignore`.
|
||||
|
@ -36,6 +36,7 @@ Component.Graph({
|
||||
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||
removeTags: [], // what tags to remove from the graph
|
||||
showTags: true, // whether to show tags in the graph
|
||||
enableRadial: false, // whether to constrain the graph, similar to Obsidian
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
@ -49,6 +50,7 @@ Component.Graph({
|
||||
opacityScale: 1,
|
||||
removeTags: [], // what tags to remove from the graph
|
||||
showTags: true, // whether to show tags in the graph
|
||||
enableRadial: true, // whether to constrain the graph, similar to Obsidian
|
||||
},
|
||||
})
|
||||
```
|
||||
|
12
index.d.ts
vendored
12
index.d.ts
vendored
@ -1,12 +0,0 @@
|
||||
declare module "*.scss" {
|
||||
const content: string
|
||||
export = content
|
||||
}
|
||||
|
||||
// dom custom event
|
||||
interface CustomEventMap {
|
||||
nav: CustomEvent<{ url: FullSlug }>
|
||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
||||
}
|
||||
|
||||
declare const fetchData: Promise<ContentIndex>
|
@ -34,6 +34,7 @@
|
||||
"bin": {
|
||||
"quartz": "./quartz/bootstrap-cli.mjs"
|
||||
},
|
||||
"types": "./ambient.d.ts",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.9.1",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { QuartzConfig } from "./quartz/cfg"
|
||||
import * as Plugin from "./quartz/plugins"
|
||||
import { QuartzConfig, Plugin } from "quartz"
|
||||
|
||||
/**
|
||||
* Quartz 4.0 Configuration
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { PageLayout, SharedLayout } from "./quartz/cfg"
|
||||
import * as Component from "./quartz/components"
|
||||
import { Component, PageLayout, SharedLayout } from "quartz"
|
||||
|
||||
// components shared across all pages
|
||||
export const sharedPageComponents: SharedLayout = {
|
||||
@ -27,7 +26,7 @@ export const defaultContentPageLayout: PageLayout = {
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
Component.Explorer(),
|
||||
],
|
||||
right: [
|
||||
Component.Graph(),
|
||||
@ -44,7 +43,7 @@ export const defaultListPageLayout: PageLayout = {
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Search(),
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
Component.Explorer(),
|
||||
],
|
||||
right: [],
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import workerpool from "workerpool"
|
||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
||||
const cacheFile = process.argv[2]
|
||||
const { parseMarkdown, processHtml } = await import(cacheFile)
|
||||
workerpool.worker({
|
||||
parseMarkdown,
|
||||
|
@ -8,7 +8,7 @@ import chalk from "chalk"
|
||||
import { parseMarkdown } from "./processors/parse"
|
||||
import { filterContent } from "./processors/filter"
|
||||
import { emitContent } from "./processors/emit"
|
||||
import cfg from "../quartz.config"
|
||||
import cfg from "$config"
|
||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path"
|
||||
import chokidar from "chokidar"
|
||||
import { ProcessedContent } from "./plugins/vfile"
|
||||
@ -42,12 +42,13 @@ function newBuildId() {
|
||||
return Math.random().toString(36).substring(2, 8)
|
||||
}
|
||||
|
||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
async function buildQuartz(quartzRoot: string, argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
const ctx: BuildCtx = {
|
||||
buildId: newBuildId(),
|
||||
argv,
|
||||
cfg,
|
||||
allSlugs: [],
|
||||
quartzRoot,
|
||||
}
|
||||
|
||||
const perf = new PerfTimer()
|
||||
@ -413,9 +414,9 @@ async function rebuildFromEntrypoint(
|
||||
release()
|
||||
}
|
||||
|
||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||
export default async (quartzRoot: string, argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||
try {
|
||||
return await buildQuartz(argv, mut, clientRefresh)
|
||||
return await buildQuartz(quartzRoot, argv, mut, clientRefresh)
|
||||
} catch (err) {
|
||||
trace("\nExiting Quartz due to a fatal error", err as Error)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { readFileSync } from "fs"
|
||||
import { accessSync, readFileSync } from "fs"
|
||||
|
||||
/**
|
||||
* All constants relating to helpers or handlers
|
||||
@ -7,9 +7,26 @@ import { readFileSync } from "fs"
|
||||
export const ORIGIN_NAME = "origin"
|
||||
export const UPSTREAM_NAME = "upstream"
|
||||
export const QUARTZ_SOURCE_BRANCH = "v4"
|
||||
|
||||
export const cwd = process.cwd()
|
||||
export const cacheDir = path.join(cwd, ".quartz-cache")
|
||||
export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs"
|
||||
export const fp = "./quartz/build.ts"
|
||||
export const { version } = JSON.parse(readFileSync("./package.json").toString())
|
||||
|
||||
function selectCacheDir() {
|
||||
try {
|
||||
const node_modules = path.join(cwd, "node_modules")
|
||||
accessSync(node_modules) // check if node_modules exists
|
||||
return path.join(node_modules, ".cache", "quartz")
|
||||
} catch {
|
||||
// standalone quartz bin?
|
||||
return path.join(cwd, ".quartz-cache")
|
||||
}
|
||||
}
|
||||
|
||||
export const cacheDir = selectCacheDir()
|
||||
export const cacheFile = path.join(cacheDir, "transpiled-build.mjs")
|
||||
export const contentCacheFolder = path.join(cacheDir, "content-cache")
|
||||
|
||||
export const quartzRoot = path.resolve(import.meta.dirname, "..")
|
||||
export const fp = path.join(quartzRoot, "build.ts")
|
||||
export const { version } = JSON.parse(
|
||||
readFileSync(path.resolve(quartzRoot, "..", "package.json")).toString(),
|
||||
)
|
||||
|
@ -31,7 +31,9 @@ import {
|
||||
fp,
|
||||
cacheFile,
|
||||
cwd,
|
||||
quartzRoot,
|
||||
} from "./constants.js"
|
||||
import { pathToFileURL } from "url"
|
||||
|
||||
/**
|
||||
* Handles `npx quartz create`
|
||||
@ -232,6 +234,12 @@ export async function handleBuild(argv) {
|
||||
metafile: true,
|
||||
sourcemap: true,
|
||||
sourcesContent: false,
|
||||
alias: {
|
||||
$config: path.join(cwd, "quartz.config.ts"),
|
||||
$layout: path.join(cwd, "quartz.layout.ts"),
|
||||
$styles: path.join(cwd, "styles.scss"),
|
||||
quartz: path.resolve(quartzRoot, ".."),
|
||||
},
|
||||
plugins: [
|
||||
sassPlugin({
|
||||
type: "css-text",
|
||||
@ -303,8 +311,9 @@ export async function handleBuild(argv) {
|
||||
release()
|
||||
|
||||
if (argv.bundleInfo) {
|
||||
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
|
||||
const meta = result.metafile.outputs[outputFileName]
|
||||
// metafile.outputs always uses /
|
||||
const output = path.relative(cwd, cacheFile).replaceAll("\\", "/")
|
||||
const meta = result.metafile.outputs[output]
|
||||
console.log(
|
||||
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
|
||||
meta.bytes,
|
||||
@ -313,12 +322,14 @@ export async function handleBuild(argv) {
|
||||
console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
|
||||
}
|
||||
|
||||
// absolute path on windows has to be a file:// url
|
||||
const url = pathToFileURL(cacheFile)
|
||||
// bypass module cache
|
||||
// https://github.com/nodejs/modules/issues/307
|
||||
const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`)
|
||||
// ^ this import is relative, so base "cacheFile" path can't be used
|
||||
url.searchParams.set("update", randomUUID())
|
||||
const { default: buildQuartz } = await import(url)
|
||||
|
||||
cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
|
||||
cleanupBuild = await buildQuartz(quartzRoot, argv, buildMutex, clientRefresh)
|
||||
clientRefresh()
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import explorerStyle from "./styles/explorer.scss"
|
||||
import style from "./styles/explorer.scss"
|
||||
|
||||
// @ts-ignore
|
||||
import script from "./scripts/explorer.inline"
|
||||
@ -83,18 +83,46 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
lastBuildId = ctx.buildId
|
||||
constructFileTree(allFiles)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={classNames(displayClass, "explorer")}>
|
||||
<button
|
||||
type="button"
|
||||
id="explorer"
|
||||
id="mobile-explorer"
|
||||
class="collapsed hide-until-loaded"
|
||||
data-behavior={opts.folderClickBehavior}
|
||||
data-collapsed={opts.folderDefaultState}
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
data-mobile={true}
|
||||
aria-controls="explorer-content"
|
||||
aria-expanded={opts.folderDefaultState === "open"}
|
||||
aria-expanded={false}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-menu"
|
||||
>
|
||||
<line x1="4" x2="20" y1="12" y2="12" />
|
||||
<line x1="4" x2="20" y1="6" y2="6" />
|
||||
<line x1="4" x2="20" y1="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="desktop-explorer"
|
||||
class="title-button"
|
||||
data-behavior={opts.folderClickBehavior}
|
||||
data-collapsed={opts.folderDefaultState}
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
data-mobile={false}
|
||||
aria-controls="explorer-content"
|
||||
aria-expanded={true}
|
||||
>
|
||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||
<svg
|
||||
@ -122,7 +150,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
)
|
||||
}
|
||||
|
||||
Explorer.css = explorerStyle
|
||||
Explorer.css = style
|
||||
Explorer.afterDOMLoaded = script
|
||||
return Explorer
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
@ -18,6 +18,7 @@ export interface D3Config {
|
||||
removeTags: string[]
|
||||
showTags: boolean
|
||||
focusOnHover?: boolean
|
||||
enableRadial?: boolean
|
||||
}
|
||||
|
||||
interface GraphOptions {
|
||||
@ -39,6 +40,7 @@ const defaultOptions: GraphOptions = {
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: false,
|
||||
enableRadial: false,
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
@ -53,10 +55,11 @@ const defaultOptions: GraphOptions = {
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: true,
|
||||
enableRadial: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default ((opts?: GraphOptions) => {
|
||||
export default ((opts?: Partial<GraphOptions>) => {
|
||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { FolderState } from "../ExplorerNode"
|
||||
|
||||
// Current state of folders
|
||||
type MaybeHTMLElement = HTMLElement | undefined
|
||||
let currentExplorerState: FolderState[]
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||
const explorerUl = document.getElementById("explorer-ul")
|
||||
@ -16,23 +18,43 @@ const observer = new IntersectionObserver((entries) => {
|
||||
})
|
||||
|
||||
function toggleExplorer(this: HTMLElement) {
|
||||
// Toggle collapsed state of entire explorer
|
||||
this.classList.toggle("collapsed")
|
||||
|
||||
// Toggle collapsed aria state of entire explorer
|
||||
this.setAttribute(
|
||||
"aria-expanded",
|
||||
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||
)
|
||||
const content = this.nextElementSibling as MaybeHTMLElement
|
||||
if (!content) return
|
||||
|
||||
const content = (
|
||||
this.nextElementSibling?.nextElementSibling
|
||||
? this.nextElementSibling.nextElementSibling
|
||||
: this.nextElementSibling
|
||||
) as MaybeHTMLElement
|
||||
if (!content) return
|
||||
content.classList.toggle("collapsed")
|
||||
content.classList.toggle("explorer-viewmode")
|
||||
|
||||
// Prevent scroll under
|
||||
if (document.querySelector("#mobile-explorer")) {
|
||||
// Disable scrolling on the page when the explorer is opened on mobile
|
||||
const bodySelector = document.querySelector("#quartz-body")
|
||||
if (bodySelector) bodySelector.classList.toggle("lock-scroll")
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFolder(evt: MouseEvent) {
|
||||
evt.stopPropagation()
|
||||
|
||||
// Element that was clicked
|
||||
const target = evt.target as MaybeHTMLElement
|
||||
if (!target) return
|
||||
|
||||
// Check if target was svg icon or button
|
||||
const isSvg = target.nodeName === "svg"
|
||||
|
||||
// corresponding <ul> element relative to clicked button/folder
|
||||
const childFolderContainer = (
|
||||
isSvg
|
||||
? target.parentElement?.nextSibling
|
||||
@ -42,10 +64,14 @@ function toggleFolder(evt: MouseEvent) {
|
||||
isSvg ? target.nextElementSibling : target.parentElement
|
||||
) as MaybeHTMLElement
|
||||
if (!(childFolderContainer && currentFolderParent)) return
|
||||
|
||||
// <li> element of folder (stores folder-path dataset)
|
||||
childFolderContainer.classList.toggle("open")
|
||||
|
||||
// Collapse folder container
|
||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||
setFolderState(childFolderContainer, !isCollapsed)
|
||||
|
||||
// Save folder state to localStorage
|
||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
||||
@ -53,20 +79,34 @@ function toggleFolder(evt: MouseEvent) {
|
||||
}
|
||||
|
||||
function setupExplorer() {
|
||||
const explorer = document.getElementById("explorer")
|
||||
if (!explorer) return
|
||||
// Set click handler for collapsing entire explorer
|
||||
const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
|
||||
|
||||
if (explorer.dataset.behavior === "collapse") {
|
||||
for (const explorer of allExplorers) {
|
||||
// Get folder state from local storage
|
||||
const storageTree = localStorage.getItem("fileTree")
|
||||
|
||||
// Convert to bool
|
||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||
|
||||
if (explorer) {
|
||||
// Get config
|
||||
const collapseBehavior = explorer.dataset.behavior
|
||||
|
||||
// Add click handlers for all folders (click handler on folder "label")
|
||||
if (collapseBehavior === "collapse") {
|
||||
for (const item of document.getElementsByClassName(
|
||||
"folder-button",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
item.addEventListener("click", toggleFolder)
|
||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||
}
|
||||
}
|
||||
|
||||
explorer.addEventListener("click", toggleExplorer)
|
||||
// Add click handler to main explorer
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
explorer.addEventListener("click", toggleExplorer)
|
||||
}
|
||||
|
||||
// Set up click handlers for each folder (click handler on folder "icon")
|
||||
for (const item of document.getElementsByClassName(
|
||||
@ -77,8 +117,6 @@ function setupExplorer() {
|
||||
}
|
||||
|
||||
// Get folder state from local storage
|
||||
const storageTree = localStorage.getItem("fileTree")
|
||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||
const oldExplorerState: FolderState[] =
|
||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
||||
@ -86,24 +124,61 @@ function setupExplorer() {
|
||||
? JSON.parse(explorer.dataset.tree)
|
||||
: []
|
||||
currentExplorerState = []
|
||||
|
||||
for (const { path, collapsed } of newExplorerState) {
|
||||
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
||||
currentExplorerState.push({
|
||||
path,
|
||||
collapsed: oldIndex.get(path) ?? collapsed,
|
||||
})
|
||||
}
|
||||
|
||||
currentExplorerState.map((folderState) => {
|
||||
const folderLi = document.querySelector(
|
||||
`[data-folderpath='${folderState.path}']`,
|
||||
`[data-folderpath='${folderState.path.replace("'", "-")}']`,
|
||||
) as MaybeHTMLElement
|
||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
||||
if (folderUl) {
|
||||
setFolderState(folderUl, folderState.collapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExplorerFolders() {
|
||||
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
|
||||
/\/index$/g,
|
||||
"",
|
||||
)
|
||||
const allFolders = document.querySelectorAll(".folder-outer")
|
||||
|
||||
allFolders.forEach((element) => {
|
||||
const folderUl = Array.from(element.children).find((child) =>
|
||||
child.matches("ul[data-folderul]"),
|
||||
)
|
||||
if (folderUl) {
|
||||
if (currentFile.includes(folderUl.getAttribute("data-folderul") ?? "")) {
|
||||
if (!element.classList.contains("open")) {
|
||||
element.classList.add("open")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener("resize", setupExplorer)
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const explorer = document.querySelector("#mobile-explorer")
|
||||
if (explorer) {
|
||||
explorer.classList.add("collapsed")
|
||||
const content = explorer.nextElementSibling?.nextElementSibling as HTMLElement
|
||||
if (content) {
|
||||
content.classList.add("collapsed")
|
||||
content.classList.toggle("explorer-viewmode")
|
||||
}
|
||||
}
|
||||
setupExplorer()
|
||||
|
||||
observer.disconnect()
|
||||
|
||||
// select pseudo element at end of list
|
||||
@ -111,6 +186,12 @@ document.addEventListener("nav", () => {
|
||||
if (lastItem) {
|
||||
observer.observe(lastItem)
|
||||
}
|
||||
|
||||
// Hide explorer on mobile until it is requested
|
||||
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
|
||||
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
|
||||
|
||||
toggleExplorerFolders()
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
forceCenter,
|
||||
forceLink,
|
||||
forceCollide,
|
||||
forceRadial,
|
||||
zoomIdentity,
|
||||
select,
|
||||
drag,
|
||||
@ -87,6 +88,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
removeTags,
|
||||
showTags,
|
||||
focusOnHover,
|
||||
enableRadial,
|
||||
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||
|
||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||
@ -161,15 +163,20 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
})),
|
||||
}
|
||||
|
||||
const width = graph.offsetWidth
|
||||
const height = Math.max(graph.offsetHeight, 250)
|
||||
|
||||
// we virtualize the simulation and use pixi to actually render it
|
||||
// Calculate the radius of the container circle
|
||||
const radius = Math.min(width, height) / 2 - 40 // 40px padding
|
||||
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||
.force("center", forceCenter().strength(centerForce))
|
||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||
|
||||
const width = graph.offsetWidth
|
||||
const height = Math.max(graph.offsetHeight, 250)
|
||||
if (enableRadial)
|
||||
simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
|
||||
|
||||
// precompute style prop strings as pixi doesn't support css variables
|
||||
const cssVars = [
|
||||
|
@ -1,14 +1,70 @@
|
||||
@use "../../styles/variables.scss" as *;
|
||||
|
||||
@media all and ($mobile) {
|
||||
.page > #quartz-body {
|
||||
// Shift page position when toggling Explorer on mobile.
|
||||
& > :not(.sidebar.left:has(.explorer)) {
|
||||
transform: translateX(0);
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
|
||||
transform: translateX(100dvw);
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
// Sticky top bar (stays in place when scrolling down on mobile).
|
||||
.sidebar.left:has(.explorer) {
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
// Hide Explorer on mobile until done loading.
|
||||
// Prevents ugly animation on page load.
|
||||
.hide-until-loaded ~ #explorer-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explorer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
|
||||
@media all and ($mobile) {
|
||||
order: -1;
|
||||
height: initial;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
button#mobile-explorer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button#desktop-explorer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media all and ($mobile) {
|
||||
button#mobile-explorer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
button#desktop-explorer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.desktop-only {
|
||||
@media all and not ($mobile) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/*&:after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
@ -23,7 +79,8 @@
|
||||
}*/
|
||||
}
|
||||
|
||||
button#explorer {
|
||||
button#mobile-explorer,
|
||||
button#desktop-explorer {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
@ -68,19 +125,19 @@ button#explorer {
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 0px;
|
||||
transition:
|
||||
max-height 0.35s ease,
|
||||
visibility 0s linear 0.35s;
|
||||
margin-top: 0.5rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 100%;
|
||||
transition:
|
||||
max-height 0.35s ease,
|
||||
visibility 0s linear 0s;
|
||||
margin-top: 0.5rem;
|
||||
visibility: visible;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 0;
|
||||
transition:
|
||||
max-height 0.35s ease,
|
||||
visibility 0s linear 0.35s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
& ul {
|
||||
@ -91,12 +148,14 @@ button#explorer {
|
||||
max-height 0.35s ease,
|
||||
transform 0.35s ease,
|
||||
opacity 0.2s ease;
|
||||
|
||||
& li > a {
|
||||
color: var(--dark);
|
||||
opacity: 0.75;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
> #explorer-ul {
|
||||
max-height: none;
|
||||
}
|
||||
@ -179,3 +238,80 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||
// remove default margin from li
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.explorer {
|
||||
@media all and ($mobile) {
|
||||
#explorer-content {
|
||||
box-sizing: border-box;
|
||||
overscroll-behavior: none;
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: var(--light);
|
||||
max-width: 100dvw;
|
||||
left: -100dvw;
|
||||
width: 100%;
|
||||
transition: transform 300ms ease-in-out;
|
||||
overflow: hidden;
|
||||
padding: $topSpacing 2rem 2rem;
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
margin-top: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&:not(.collapsed) {
|
||||
transform: translateX(100dvw);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
ul.overflow {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
transform: translateX(0);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
#mobile-explorer {
|
||||
margin: 5px;
|
||||
z-index: 101;
|
||||
|
||||
&:not(.collapsed) .lucide-menu {
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.lucide-menu {
|
||||
stroke: var(--darkgray);
|
||||
transition: transform 200ms ease;
|
||||
|
||||
&:hover {
|
||||
stroke: var(--dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
4
quartz/index.ts
Normal file
4
quartz/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * as Component from "./components"
|
||||
export * as Plugin from "./plugins"
|
||||
export * from "./i18n"
|
||||
export * from "./cfg"
|
@ -4,7 +4,7 @@ import BodyConstructor from "../../components/Body"
|
||||
import { pageResources, renderPage } from "../../components/renderPage"
|
||||
import { FullPageLayout } from "../../cfg"
|
||||
import { FilePath, FullSlug } from "../../util/path"
|
||||
import { sharedPageComponents } from "../../../quartz.layout"
|
||||
import { sharedPageComponents } from "$layout"
|
||||
import { NotFound } from "../../components"
|
||||
import { defaultProcessedContent } from "../vfile"
|
||||
import { write } from "./helpers"
|
||||
|
@ -14,6 +14,7 @@ import { Features, transform } from "lightningcss"
|
||||
import { transform as transpile } from "esbuild"
|
||||
import { write } from "./helpers"
|
||||
import DepGraph from "../../depgraph"
|
||||
import path from "path"
|
||||
|
||||
type ComponentResources = {
|
||||
css: string[]
|
||||
@ -183,8 +184,13 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
|
||||
getQuartzComponents() {
|
||||
return []
|
||||
},
|
||||
async getDependencyGraph(_ctx, _content, _resources) {
|
||||
return new DepGraph<FilePath>()
|
||||
async getDependencyGraph(ctx, _content, _resources) {
|
||||
const graph = new DepGraph<FilePath>()
|
||||
graph.addEdge(
|
||||
path.join(ctx.argv.output, "index.css") as FilePath,
|
||||
path.join(process.cwd(), "styles.scss") as FilePath,
|
||||
)
|
||||
return graph
|
||||
},
|
||||
async emit(ctx, _content, _resources): Promise<FilePath[]> {
|
||||
const promises: Promise<FilePath>[] = []
|
||||
@ -245,6 +251,7 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
|
||||
googleFontsStyleSheet,
|
||||
...componentResources.css,
|
||||
styles,
|
||||
await import("$styles").then((s) => s.default ?? s).catch(() => ""),
|
||||
)
|
||||
const [prescript, postscript] = await Promise.all([
|
||||
joinScripts(componentResources.beforeDOMLoaded),
|
||||
|
@ -10,7 +10,7 @@ import { pageResources, renderPage } from "../../components/renderPage"
|
||||
import { FullPageLayout } from "../../cfg"
|
||||
import { Argv } from "../../util/ctx"
|
||||
import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path"
|
||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import { defaultContentPageLayout, sharedPageComponents } from "$layout"
|
||||
import { Content } from "../../components"
|
||||
import chalk from "chalk"
|
||||
import { write } from "./helpers"
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
pathToRoot,
|
||||
simplifySlug,
|
||||
} from "../../util/path"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "$layout"
|
||||
import { FolderContent } from "../../components"
|
||||
import { write } from "./helpers"
|
||||
import { i18n } from "../../i18n"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FilePath, QUARTZ, joinSegments } from "../../util/path"
|
||||
import { FilePath, joinSegments } from "../../util/path"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
import fs from "fs"
|
||||
import { glob } from "../../util/glob"
|
||||
@ -9,10 +9,10 @@ export const Static: QuartzEmitterPlugin = () => ({
|
||||
getQuartzComponents() {
|
||||
return []
|
||||
},
|
||||
async getDependencyGraph({ argv, cfg }, _content, _resources) {
|
||||
async getDependencyGraph({ argv, cfg, quartzRoot }, _content, _resources) {
|
||||
const graph = new DepGraph<FilePath>()
|
||||
|
||||
const staticPath = joinSegments(QUARTZ, "static")
|
||||
const staticPath = joinSegments(quartzRoot, "static")
|
||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||
for (const fp of fps) {
|
||||
graph.addEdge(
|
||||
@ -23,8 +23,8 @@ export const Static: QuartzEmitterPlugin = () => ({
|
||||
|
||||
return graph
|
||||
},
|
||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
||||
const staticPath = joinSegments(QUARTZ, "static")
|
||||
async emit({ argv, cfg, quartzRoot }, _content, _resources): Promise<FilePath[]> {
|
||||
const staticPath = joinSegments(quartzRoot, "static")
|
||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
|
||||
recursive: true,
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
joinSegments,
|
||||
pathToRoot,
|
||||
} from "../../util/path"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import { defaultListPageLayout, sharedPageComponents } from "$layout"
|
||||
import { TagContent } from "../../components"
|
||||
import { write } from "./helpers"
|
||||
import { i18n } from "../../i18n"
|
||||
|
@ -7,7 +7,7 @@ import { Root as HTMLRoot } from "hast"
|
||||
import { MarkdownContent, ProcessedContent } from "../plugins/vfile"
|
||||
import { PerfTimer } from "../util/perf"
|
||||
import { read } from "to-vfile"
|
||||
import { FilePath, FullSlug, QUARTZ, slugifyFilePath } from "../util/path"
|
||||
import { FilePath, FullSlug, slugifyFilePath } from "../util/path"
|
||||
import path from "path"
|
||||
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
||||
import { QuartzLogger } from "../util/log"
|
||||
@ -49,20 +49,28 @@ function* chunks<T>(arr: T[], n: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function transpileWorkerScript() {
|
||||
// transpile worker script
|
||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
||||
const fp = "./quartz/worker.ts"
|
||||
return esbuild.build({
|
||||
async function transpileWorkerScript(ctx: BuildCtx): Promise<string> {
|
||||
// import.meta.dirname is the cache folder, because we're in transpiled-build.mjs atm technically
|
||||
const cacheFile = path.join(import.meta.dirname, "transpiled-worker.mjs")
|
||||
const fp = path.join(ctx.quartzRoot, "worker.ts")
|
||||
await esbuild.build({
|
||||
entryPoints: [fp],
|
||||
outfile: path.join(QUARTZ, cacheFile),
|
||||
outfile: cacheFile,
|
||||
bundle: true,
|
||||
keepNames: true,
|
||||
minifyWhitespace: true,
|
||||
minifySyntax: true,
|
||||
platform: "node",
|
||||
format: "esm",
|
||||
packages: "external",
|
||||
sourcemap: true,
|
||||
sourcesContent: false,
|
||||
alias: {
|
||||
$config: path.join(process.cwd(), "quartz.config.ts"),
|
||||
$layout: path.join(process.cwd(), "quartz.layout.ts"),
|
||||
$styles: path.join(process.cwd(), "styles.scss"),
|
||||
quartz: path.resolve(ctx.quartzRoot, ".."),
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "css-and-scripts-as-text",
|
||||
@ -79,6 +87,7 @@ async function transpileWorkerScript() {
|
||||
},
|
||||
],
|
||||
})
|
||||
return cacheFile
|
||||
}
|
||||
|
||||
export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
||||
@ -164,11 +173,12 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
await transpileWorkerScript()
|
||||
const pool = workerpool.pool("./quartz/bootstrap-worker.mjs", {
|
||||
const transpiledWorker = await transpileWorkerScript(ctx)
|
||||
const pool = workerpool.pool(path.join(ctx.quartzRoot, "bootstrap-worker.mjs"), {
|
||||
minWorkers: "max",
|
||||
maxWorkers: concurrency,
|
||||
workerType: "thread",
|
||||
workerThreadOpts: { argv: [transpiledWorker] },
|
||||
})
|
||||
const errorHandler = (err: any) => {
|
||||
console.error(`${err}`.replace(/^error:\s*/i, ""))
|
||||
@ -177,7 +187,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
|
||||
const mdPromises: WorkerPromise<[MarkdownContent[], FullSlug[]]>[] = []
|
||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||
mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, argv, chunk]))
|
||||
mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, ctx.quartzRoot, argv, chunk]))
|
||||
}
|
||||
const mdResults: [MarkdownContent[], FullSlug[]][] =
|
||||
await WorkerPromise.all(mdPromises).catch(errorHandler)
|
||||
@ -187,7 +197,9 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
||||
ctx.allSlugs.push(...extraSlugs)
|
||||
}
|
||||
for (const [mdChunk, _] of mdResults) {
|
||||
childPromises.push(pool.exec("processHtml", [ctx.buildId, argv, mdChunk, ctx.allSlugs]))
|
||||
childPromises.push(
|
||||
pool.exec("processHtml", [ctx.buildId, ctx.quartzRoot, argv, mdChunk, ctx.allSlugs]),
|
||||
)
|
||||
}
|
||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch(errorHandler)
|
||||
|
||||
|
@ -18,4 +18,5 @@ export interface BuildCtx {
|
||||
argv: Argv
|
||||
cfg: QuartzConfig
|
||||
allSlugs: FullSlug[]
|
||||
quartzRoot: string
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ export const clone = rfdc()
|
||||
|
||||
// this file must be isomorphic so it can't use node libs (e.g. path)
|
||||
|
||||
export const QUARTZ = "quartz"
|
||||
|
||||
/// Utility type to simulate nominal types in TypeScript
|
||||
type SlugLike<T> = string & { __brand: T }
|
||||
|
||||
|
@ -6,7 +6,7 @@ export const options: sourceMapSupport.Options = {
|
||||
// source map hack to get around query param
|
||||
// import cache busting
|
||||
retrieveSourceMap(source) {
|
||||
if (source.includes(".quartz-cache")) {
|
||||
if (source.includes("?update")) {
|
||||
let realSource = fileURLToPath(source.split("?", 2)[0] + ".map")
|
||||
return {
|
||||
map: fs.readFileSync(realSource, "utf8"),
|
||||
|
@ -1,6 +1,5 @@
|
||||
import sourceMapSupport from "source-map-support"
|
||||
sourceMapSupport.install(options)
|
||||
import cfg from "../quartz.config"
|
||||
import { Argv, BuildCtx } from "./util/ctx"
|
||||
import { FilePath, FullSlug } from "./util/path"
|
||||
import {
|
||||
@ -12,9 +11,12 @@ import {
|
||||
import { options } from "./util/sourcemap"
|
||||
import { MarkdownContent, ProcessedContent } from "./plugins/vfile"
|
||||
|
||||
import cfg from "$config"
|
||||
|
||||
// only called from worker thread
|
||||
export async function parseMarkdown(
|
||||
buildId: string,
|
||||
quartzRoot: string,
|
||||
argv: Argv,
|
||||
fps: FilePath[],
|
||||
): Promise<[MarkdownContent[], FullSlug[]]> {
|
||||
@ -27,6 +29,7 @@ export async function parseMarkdown(
|
||||
cfg,
|
||||
argv,
|
||||
allSlugs,
|
||||
quartzRoot,
|
||||
}
|
||||
return [await createFileParser(ctx, fps)(createMdProcessor(ctx)), allSlugs]
|
||||
}
|
||||
@ -34,6 +37,7 @@ export async function parseMarkdown(
|
||||
// only called from worker thread
|
||||
export function processHtml(
|
||||
buildId: string,
|
||||
quartzRoot: string,
|
||||
argv: Argv,
|
||||
mds: MarkdownContent[],
|
||||
allSlugs: FullSlug[],
|
||||
@ -43,6 +47,7 @@ export function processHtml(
|
||||
cfg,
|
||||
argv,
|
||||
allSlugs,
|
||||
quartzRoot,
|
||||
}
|
||||
return createMarkdownParser(ctx, mds)(createHtmlProcessor(ctx))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user