This commit is contained in:
semanticdata 2023-11-13 13:24:12 -06:00
commit fb44358fe3
14 changed files with 130 additions and 60 deletions

View File

@ -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
}) })
``` ```

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -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)
} }

View File

@ -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>

View File

@ -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)

View File

@ -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 {

View File

@ -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: `

View File

@ -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("")

View File

@ -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 {

View File

@ -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
View 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)
}
}