mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 23:15:46 -05:00
Merge branch 'v4' of https://github.com/jackyzha0/quartz
This commit is contained in:
commit
fb44358fe3
@ -16,10 +16,10 @@ For example, here's what the default configuration looks like:
|
|||||||
|
|
||||||
```typescript title="quartz.layout.ts"
|
```typescript title="quartz.layout.ts"
|
||||||
Component.Breadcrumbs({
|
Component.Breadcrumbs({
|
||||||
spacerSymbol: ">", // symbol between crumbs
|
spacerSymbol: "❯", // symbol between crumbs
|
||||||
rootName: "Home", // name of first/root element
|
rootName: "Home", // name of first/root element
|
||||||
resolveFrontmatterTitle: false, // wether to resolve folder names through frontmatter titles (more computationally expensive)
|
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||||
hideOnRoot: true, // wether to hide breadcrumbs on root `index.md` page
|
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
|
|||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be nested?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can.
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
> > > [!example] You can even use multiple layers of nesting.
|
> > > [!example] You can even use multiple layers of nesting.
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +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.
|
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.
|
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.
|
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||||
|
|||||||
@ -41,6 +41,11 @@ export const SyncArgv = {
|
|||||||
default: true,
|
default: true,
|
||||||
describe: "create a git commit for your unsaved changes",
|
describe: "create a git commit for your unsaved changes",
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
string: true,
|
||||||
|
alias: ["m"],
|
||||||
|
describe: "option to override the default Quartz commit message",
|
||||||
|
},
|
||||||
push: {
|
push: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: true,
|
default: true,
|
||||||
|
|||||||
@ -483,8 +483,9 @@ export async function handleSync(argv) {
|
|||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
})
|
})
|
||||||
|
const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
|
||||||
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
||||||
spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
|
spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
|
||||||
|
|
||||||
if (contentStat.isSymbolicLink()) {
|
if (contentStat.isSymbolicLink()) {
|
||||||
// put symlink back
|
// put symlink back
|
||||||
|
|||||||
@ -28,9 +28,9 @@ interface BreadcrumbOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: BreadcrumbOptions = {
|
const defaultOptions: BreadcrumbOptions = {
|
||||||
spacerSymbol: ">",
|
spacerSymbol: "❯",
|
||||||
rootName: "Home",
|
rootName: "Home",
|
||||||
resolveFrontmatterTitle: false,
|
resolveFrontmatterTitle: true,
|
||||||
hideOnRoot: true,
|
hideOnRoot: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,25 +41,13 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a folderName (e.g. "features"), search for the corresponding `index.md` file
|
|
||||||
function findCurrentFile(allFiles: QuartzPluginData[], folderName: string) {
|
|
||||||
return allFiles.find((file) => {
|
|
||||||
if (file.slug?.endsWith("index")) {
|
|
||||||
const folderParts = file.filePath?.split("/")
|
|
||||||
if (folderParts) {
|
|
||||||
const name = folderParts[folderParts?.length - 2]
|
|
||||||
if (name === folderName) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||||
// Merge options with defaults
|
// Merge options with defaults
|
||||||
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
|
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
|
// computed index of folder name to its associated file data
|
||||||
|
let folderIndex: Map<string, QuartzPluginData> | undefined
|
||||||
|
|
||||||
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
// Hide crumbs on root if enabled
|
// Hide crumbs on root if enabled
|
||||||
if (options.hideOnRoot && fileData.slug === "index") {
|
if (options.hideOnRoot && fileData.slug === "index") {
|
||||||
@ -70,28 +58,39 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
||||||
const crumbs: CrumbData[] = [firstEntry]
|
const crumbs: CrumbData[] = [firstEntry]
|
||||||
|
|
||||||
|
if (!folderIndex && options.resolveFrontmatterTitle) {
|
||||||
|
folderIndex = new Map()
|
||||||
|
// construct the index for the first time
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (file.slug?.endsWith("index")) {
|
||||||
|
const folderParts = file.filePath?.split("/")
|
||||||
|
if (folderParts) {
|
||||||
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
|
folderIndex.set(folderName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Split slug into hierarchy/parts
|
// Split slug into hierarchy/parts
|
||||||
const slugParts = fileData.slug?.split("/")
|
const slugParts = fileData.slug?.split("/")
|
||||||
if (slugParts) {
|
if (slugParts) {
|
||||||
// full path until current part
|
// full path until current part
|
||||||
let currentPath = ""
|
let currentPath = ""
|
||||||
for (let i = 0; i < slugParts.length - 1; i++) {
|
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||||
let currentTitle = slugParts[i]
|
let curPathSegment = slugParts[i]
|
||||||
|
|
||||||
// TODO: performance optimizations/memoizing
|
|
||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
if (options?.resolveFrontmatterTitle) {
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
// try to find file for current path
|
if (currentFile) {
|
||||||
const currentFile = findCurrentFile(allFiles, currentTitle)
|
curPathSegment = currentFile.frontmatter!.title
|
||||||
if (currentFile) {
|
|
||||||
currentTitle = currentFile.frontmatter!.title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
currentPath += slugParts[i] + "/"
|
currentPath += slugParts[i] + "/"
|
||||||
|
|
||||||
// Format and add current crumb
|
// Format and add current crumb
|
||||||
const crumb = formatCrumb(currentTitle, fileData.slug!, currentPath as SimpleSlug)
|
const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
|
||||||
crumbs.push(crumb)
|
crumbs.push(crumb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>
|
<h2>
|
||||||
<a class="internal tag-link" href={`./${tag}`}>
|
<a class="internal tag-link" href={`../tags/${tag}`}>
|
||||||
#{tag}
|
#{tag}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -28,8 +28,11 @@ async function mouseEnterHandler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasAlreadyBeenFetched = () =>
|
||||||
|
[...link.children].some((child) => child.classList.contains("popover"))
|
||||||
|
|
||||||
// dont refetch if there's already a popover
|
// dont refetch if there's already a popover
|
||||||
if ([...link.children].some((child) => child.classList.contains("popover"))) {
|
if (hasAlreadyBeenFetched()) {
|
||||||
return setPosition(link.lastChild as HTMLElement)
|
return setPosition(link.lastChild as HTMLElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +52,11 @@ async function mouseEnterHandler(
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// bailout if another popover exists
|
||||||
|
if (hasAlreadyBeenFetched()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
normalizeRelativeURLs(html, targetUrl)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import micromorph from "micromorph"
|
import micromorph from "micromorph"
|
||||||
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
|
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
|
||||||
|
import { normalizeRelativeURLs } from "./popover.inline"
|
||||||
|
|
||||||
// adapted from `micromorph`
|
// adapted from `micromorph`
|
||||||
// https://github.com/natemoo-re/micromorph
|
// https://github.com/natemoo-re/micromorph
|
||||||
@ -18,6 +19,12 @@ const isLocalUrl = (href: string) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSamePage = (url: URL): boolean => {
|
||||||
|
const sameOrigin = url.origin === window.location.origin
|
||||||
|
const samePath = url.pathname === window.location.pathname
|
||||||
|
return sameOrigin && samePath
|
||||||
|
}
|
||||||
|
|
||||||
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
||||||
if (!isElement(target)) return
|
if (!isElement(target)) return
|
||||||
if (target.attributes.getNamedItem("target")?.value === "_blank") return
|
if (target.attributes.getNamedItem("target")?.value === "_blank") return
|
||||||
@ -46,6 +53,8 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
let title = html.querySelector("title")?.textContent
|
let title = html.querySelector("title")?.textContent
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = title
|
document.title = title
|
||||||
@ -93,8 +102,16 @@ function createRouter() {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("click", async (event) => {
|
window.addEventListener("click", async (event) => {
|
||||||
const { url } = getOpts(event) ?? {}
|
const { url } = getOpts(event) ?? {}
|
||||||
|
// dont hijack behaviour, just let browser act normally
|
||||||
if (!url || event.ctrlKey || event.metaKey) return
|
if (!url || event.ctrlKey || event.metaKey) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (isSamePage(url) && url.hash) {
|
||||||
|
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||||
|
el?.scrollIntoView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigate(url, false)
|
navigate(url, false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -140,6 +157,7 @@ if (!customElements.get("route-announcer")) {
|
|||||||
style:
|
style:
|
||||||
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define(
|
customElements.define(
|
||||||
"route-announcer",
|
"route-announcer",
|
||||||
class RouteAnnouncer extends HTMLElement {
|
class RouteAnnouncer extends HTMLElement {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
@ -25,7 +25,12 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
slugs.push(permalink as FullSlug)
|
slugs.push(permalink as FullSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slug of slugs) {
|
for (let slug of slugs) {
|
||||||
|
// fix any slugs that have trailing slash
|
||||||
|
if (slug.endsWith("/")) {
|
||||||
|
slug = joinSegments(slug, "index") as FullSlug
|
||||||
|
}
|
||||||
|
|
||||||
const redirUrl = resolveRelative(slug, file.data.slug!)
|
const redirUrl = resolveRelative(slug, file.data.slug!)
|
||||||
const fp = await emit({
|
const fp = await emit({
|
||||||
content: `
|
content: `
|
||||||
|
|||||||
@ -59,6 +59,17 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
</item>`
|
</item>`
|
||||||
|
|
||||||
const items = Array.from(idx)
|
const items = Array.from(idx)
|
||||||
|
.sort(([_, f1], [__, f2]) => {
|
||||||
|
if (f1.date && f2.date) {
|
||||||
|
return f2.date.getTime() - f1.date.getTime()
|
||||||
|
} else if (f1.date && !f2.date) {
|
||||||
|
return -1
|
||||||
|
} else if (!f1.date && f2.date) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return f1.title.localeCompare(f2.title)
|
||||||
|
})
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
||||||
.slice(0, limit ?? idx.size)
|
.slice(0, limit ?? idx.size)
|
||||||
.join("")
|
.join("")
|
||||||
|
|||||||
@ -65,7 +65,7 @@ a {
|
|||||||
color: var(--tertiary) !important;
|
color: var(--tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.internal {
|
&.internal:not(:has(> img)) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
@ -392,23 +392,33 @@ p {
|
|||||||
line-height: 1.6rem;
|
line-height: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.table-container {
|
||||||
margin: 1rem;
|
overflow-x: auto;
|
||||||
padding: 1.5rem;
|
|
||||||
border-collapse: collapse;
|
& > table {
|
||||||
& > * {
|
margin: 1rem;
|
||||||
line-height: 2rem;
|
padding: 1.5rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
min-width: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.4rem 1rem;
|
padding: 0.4rem 0.7rem;
|
||||||
border-bottom: 2px solid var(--gray);
|
border-bottom: 2px solid var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.2rem 1rem;
|
padding: 0.2rem 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import { Node, Root } from "hast"
|
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
|
||||||
import { trace } from "./trace"
|
|
||||||
import { type FilePath } from "./path"
|
|
||||||
|
|
||||||
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
|
||||||
try {
|
|
||||||
// @ts-ignore (preact makes it angry)
|
|
||||||
return toJsxRuntime(tree as Root, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
|
||||||
} catch (e) {
|
|
||||||
trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
quartz/util/jsx.tsx
Normal file
28
quartz/util/jsx.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
import { Node, Root } from "hast"
|
||||||
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
|
import { trace } from "./trace"
|
||||||
|
import { type FilePath } from "./path"
|
||||||
|
|
||||||
|
const customComponents: Components = {
|
||||||
|
table: (props) => (
|
||||||
|
<div class="table-container">
|
||||||
|
<table {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
||||||
|
try {
|
||||||
|
return toJsxRuntime(tree as Root, {
|
||||||
|
Fragment,
|
||||||
|
jsx: jsx as Jsx,
|
||||||
|
jsxs: jsxs as Jsx,
|
||||||
|
elementAttributeNameCase: "html",
|
||||||
|
components: customComponents,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user