mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-28 07:14:05 -06:00
Merge branch 'v4' of github-bfahrenfort:jackyzha0/quartz into v4
This commit is contained in:
commit
e61f62d555
3
docs/advanced/index.md
Normal file
3
docs/advanced/index.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "Advanced"
|
||||
---
|
||||
@ -8,6 +8,8 @@ Quartz features an explorer that allows you to navigate all files and folders on
|
||||
|
||||
By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]].
|
||||
|
||||
Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead.
|
||||
|
||||
> [!info]
|
||||
> The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages.
|
||||
>
|
||||
@ -29,7 +31,7 @@ Component.Explorer({
|
||||
sortFn: (a, b) => {
|
||||
... // default implementation shown later
|
||||
},
|
||||
filterFn: undefined,
|
||||
filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder
|
||||
mapFn: undefined,
|
||||
// what order to apply functions in
|
||||
order: ["filter", "map", "sort"],
|
||||
@ -57,7 +59,8 @@ All functions you can pass work with the `FileNode` class, which has the followi
|
||||
```ts title="quartz/components/ExplorerNode.tsx" {2-5}
|
||||
export class FileNode {
|
||||
children: FileNode[] // children of current node
|
||||
name: string // name of node (only useful for folders)
|
||||
name: string // last part of slug
|
||||
displayName: string // what actually should be displayed in the explorer
|
||||
file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail
|
||||
depth: number // depth of current node
|
||||
|
||||
@ -72,7 +75,7 @@ Every function you can pass is optional. By default, only a `sort` function will
|
||||
Component.Explorer({
|
||||
sortFn: (a, b) => {
|
||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
return a.displayName.localeCompare(b.displayName)
|
||||
}
|
||||
if (a.file && !b.file) {
|
||||
return 1
|
||||
@ -120,7 +123,7 @@ Using this example, the explorer will alphabetically sort everything, but put al
|
||||
Component.Explorer({
|
||||
sortFn: (a, b) => {
|
||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
return a.displayName.localeCompare(b.displayName)
|
||||
}
|
||||
if (a.file && !b.file) {
|
||||
return -1
|
||||
@ -138,7 +141,7 @@ Using this example, the display names of all `FileNodes` (folders + files) will
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
mapFn: (node) => {
|
||||
node.name = node.name.toUpperCase()
|
||||
node.displayName = node.displayName.toUpperCase()
|
||||
},
|
||||
})
|
||||
```
|
||||
@ -152,13 +155,23 @@ Component.Explorer({
|
||||
filterFn: (node) => {
|
||||
// set containing names of everything you want to filter out
|
||||
const omit = new Set(["authoring content", "tags", "hosting"])
|
||||
return omit.has(node.name.toLowerCase())
|
||||
return !omit.has(node.name.toLowerCase())
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove.
|
||||
|
||||
### Show every element in explorer
|
||||
|
||||
To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`.
|
||||
|
||||
```ts title="quartz.layout.ts"
|
||||
Component.Explorer({
|
||||
filterFn: undefined, // apply no filter function, every file and folder will visible
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced examples
|
||||
|
||||
### Add emoji prefix
|
||||
@ -172,9 +185,9 @@ Component.Explorer({
|
||||
if (node.depth > 0) {
|
||||
// set emoji for file/folder
|
||||
if (node.file) {
|
||||
node.name = "📄 " + node.name
|
||||
node.displayName = "📄 " + node.displayName
|
||||
} else {
|
||||
node.name = "📁 " + node.name
|
||||
node.displayName = "📁 " + node.displayName
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@ tags:
|
||||
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
||||
|
||||
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
||||
You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page.
|
||||
|
||||
> [!info]
|
||||
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||
|
||||
@ -18,4 +18,4 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
||||
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
||||
|
||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/content/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/v4/docs/showcase.md)!
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "@jackyzha0/quartz",
|
||||
"description": "🌱 publish your digital garden and notes as a website",
|
||||
"private": true,
|
||||
"version": "4.0.11",
|
||||
"version": "4.1.0",
|
||||
"type": "module",
|
||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@ -4,17 +4,18 @@ import explorerStyle from "./styles/explorer.scss"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/explorer.inline"
|
||||
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
|
||||
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
||||
const defaultOptions = (): Options => ({
|
||||
const defaultOptions = {
|
||||
title: "Explorer",
|
||||
folderClickBehavior: "collapse",
|
||||
folderDefaultState: "collapsed",
|
||||
useSavedState: true,
|
||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||
sortFn: (a, b) => {
|
||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
return a.displayName.localeCompare(b.displayName)
|
||||
}
|
||||
if (a.file && !b.file) {
|
||||
return 1
|
||||
@ -22,52 +23,63 @@ const defaultOptions = (): Options => ({
|
||||
return -1
|
||||
}
|
||||
},
|
||||
filterFn: (node) => node.name !== "tags",
|
||||
order: ["filter", "map", "sort"],
|
||||
})
|
||||
} satisfies Options
|
||||
|
||||
export default ((userOpts?: Partial<Options>) => {
|
||||
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||
// Parse config
|
||||
const opts: Options = { ...defaultOptions(), ...userOpts }
|
||||
// Parse config
|
||||
const opts: Options = { ...defaultOptions, ...userOpts }
|
||||
|
||||
// Construct tree from allFiles
|
||||
const fileTree = new FileNode("")
|
||||
allFiles.forEach((file) => fileTree.add(file, 1))
|
||||
// memoized
|
||||
let fileTree: FileNode
|
||||
let jsonTree: string
|
||||
|
||||
/**
|
||||
* Keys of this object must match corresponding function name of `FileNode`,
|
||||
* while values must be the argument that will be passed to the function.
|
||||
*
|
||||
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
||||
*/
|
||||
const functions = {
|
||||
map: opts.mapFn,
|
||||
sort: opts.sortFn,
|
||||
filter: opts.filterFn,
|
||||
}
|
||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||
if (!fileTree) {
|
||||
// Construct tree from allFiles
|
||||
fileTree = new FileNode("")
|
||||
allFiles.forEach((file) => fileTree.add(file, 1))
|
||||
|
||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||
if (opts.order) {
|
||||
// Order is important, use loop with index instead of order.map()
|
||||
for (let i = 0; i < opts.order.length; i++) {
|
||||
const functionName = opts.order[i]
|
||||
if (functions[functionName]) {
|
||||
// for every entry in order, call matching function in FileNode and pass matching argument
|
||||
// e.g. i = 0; functionName = "filter"
|
||||
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
||||
/**
|
||||
* Keys of this object must match corresponding function name of `FileNode`,
|
||||
* while values must be the argument that will be passed to the function.
|
||||
*
|
||||
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
||||
*/
|
||||
const functions = {
|
||||
map: opts.mapFn,
|
||||
sort: opts.sortFn,
|
||||
filter: opts.filterFn,
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
||||
fileTree[functionName].call(fileTree, functions[functionName])
|
||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||
if (opts.order) {
|
||||
// Order is important, use loop with index instead of order.map()
|
||||
for (let i = 0; i < opts.order.length; i++) {
|
||||
const functionName = opts.order[i]
|
||||
if (functions[functionName]) {
|
||||
// for every entry in order, call matching function in FileNode and pass matching argument
|
||||
// e.g. i = 0; functionName = "filter"
|
||||
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
||||
|
||||
// @ts-ignore
|
||||
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
||||
fileTree[functionName].call(fileTree, functions[functionName])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all folders of tree. Initialize with collapsed state
|
||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||
|
||||
// Stringify to pass json tree as data attribute ([data-tree])
|
||||
jsonTree = JSON.stringify(folders)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all folders of tree. Initialize with collapsed state
|
||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||
|
||||
// Stringify to pass json tree as data attribute ([data-tree])
|
||||
const jsonTree = JSON.stringify(folders)
|
||||
|
||||
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||
constructFileTree(allFiles)
|
||||
return (
|
||||
<div class={`explorer ${displayClass}`}>
|
||||
<button
|
||||
@ -78,7 +90,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
>
|
||||
<h3>{opts.title}</h3>
|
||||
<h1>{opts.title}</h1>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
@ -97,7 +109,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
<div id="explorer-content">
|
||||
<ul class="overflow" id="explorer-ul">
|
||||
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
|
||||
<div id="explorer-end" />
|
||||
<li id="explorer-end" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -29,19 +29,28 @@ export type FolderState = {
|
||||
export class FileNode {
|
||||
children: FileNode[]
|
||||
name: string
|
||||
displayName: string
|
||||
file: QuartzPluginData | null
|
||||
depth: number
|
||||
|
||||
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
||||
this.children = []
|
||||
this.name = name
|
||||
this.displayName = name
|
||||
this.file = file ? structuredClone(file) : null
|
||||
this.depth = depth ?? 0
|
||||
}
|
||||
|
||||
private insert(file: DataWrapper) {
|
||||
if (file.path.length === 1) {
|
||||
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
||||
if (file.path[0] !== "index.md") {
|
||||
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
||||
} else {
|
||||
const title = file.file.frontmatter?.title
|
||||
if (title && title !== "index" && file.path[0] === "index.md") {
|
||||
this.displayName = title
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const next = file.path[0]
|
||||
file.path = file.path.splice(1)
|
||||
@ -145,12 +154,12 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<li>
|
||||
{node.file ? (
|
||||
// Single file node
|
||||
<li key={node.file.slug}>
|
||||
<a href={resolveRelative(fileData.slug!, node.file.slug!)} data-for={node.file.slug}>
|
||||
{node.name}
|
||||
{node.displayName}
|
||||
</a>
|
||||
</li>
|
||||
) : (
|
||||
@ -174,17 +183,17 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||
<li key={node.name} data-folderpath={folderPath}>
|
||||
<div key={node.name} data-folderpath={folderPath}>
|
||||
{folderBehavior === "link" ? (
|
||||
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
||||
{node.name}
|
||||
{node.displayName}
|
||||
</a>
|
||||
) : (
|
||||
<button class="folder-button">
|
||||
<h3 class="folder-title">{node.name}</h3>
|
||||
<p class="folder-title">{node.displayName}</p>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Recursively render children of folder */}
|
||||
@ -210,6 +219,6 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,4 +20,13 @@ document.addEventListener("nav", () => {
|
||||
if (currentTheme === "dark") {
|
||||
toggleSwitch.checked = true
|
||||
}
|
||||
|
||||
// Listen for changes in prefers-color-scheme
|
||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||
const newTheme = e.matches ? "dark" : "light"
|
||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||
localStorage.setItem("theme", newTheme)
|
||||
toggleSwitch.checked = e.matches
|
||||
})
|
||||
})
|
||||
|
||||
@ -113,9 +113,11 @@ function setupExplorer() {
|
||||
) as HTMLElement
|
||||
|
||||
// Get corresponding content <ul> tag and set state
|
||||
const folderUL = folderLi.parentElement?.nextElementSibling
|
||||
if (folderUL) {
|
||||
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
||||
if (folderLi) {
|
||||
const folderUL = folderLi.parentElement?.nextElementSibling
|
||||
if (folderUL) {
|
||||
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
button#explorer {
|
||||
all: unset;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
@ -8,7 +9,7 @@ button#explorer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& h3 {
|
||||
& h1 {
|
||||
font-size: 1rem;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
@ -58,7 +59,7 @@ button#explorer {
|
||||
max-height 0.35s ease,
|
||||
transform 0.35s ease,
|
||||
opacity 0.2s ease;
|
||||
& div > li > a {
|
||||
& li > a {
|
||||
color: var(--dark);
|
||||
opacity: 0.75;
|
||||
pointer-events: all;
|
||||
@ -80,19 +81,20 @@ svg {
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
& li > a {
|
||||
// other selector is more specific, needs important
|
||||
color: var(--secondary) !important;
|
||||
opacity: 1 !important;
|
||||
font-size: 1.05rem !important;
|
||||
& div > a {
|
||||
color: var(--secondary);
|
||||
font-family: var(--headerFont);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& li > a:hover {
|
||||
// other selector is more specific, needs important
|
||||
color: var(--tertiary) !important;
|
||||
& div > a:hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
& li > button {
|
||||
& div > button {
|
||||
color: var(--dark);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
@ -102,15 +104,15 @@ svg {
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: var(--headerFont);
|
||||
|
||||
& h3 {
|
||||
& p {
|
||||
font-size: 0.95rem;
|
||||
display: inline-block;
|
||||
color: var(--secondary);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.5rem;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@ -138,5 +140,7 @@ div:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||
|
||||
#explorer-end {
|
||||
// needs height so IntersectionObserver gets triggered
|
||||
height: 1px;
|
||||
height: 4px;
|
||||
// remove default margin from li
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -400,6 +400,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
return (tree: Root, file) => {
|
||||
const base = pathToRoot(file.data.slug!)
|
||||
findAndReplace(tree, tagRegex, (_value: string, tag: string) => {
|
||||
// Check if the tag only includes numbers
|
||||
if (/^\d+$/.test(tag)) {
|
||||
return false
|
||||
}
|
||||
tag = slugTag(tag)
|
||||
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
||||
file.data.frontmatter.tags.push(tag)
|
||||
|
||||
@ -446,7 +446,7 @@ video {
|
||||
|
||||
ul.overflow,
|
||||
ol.overflow {
|
||||
max-height: 300;
|
||||
max-height: 400;
|
||||
overflow-y: auto;
|
||||
|
||||
// clearfix
|
||||
|
||||
Loading…
Reference in New Issue
Block a user