mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-01 02:07:55 +01:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8c025d6a | ||
|
|
54b4a5567c | ||
|
|
610b04406f | ||
|
|
82bd08d14a | ||
|
|
649090de1b | ||
|
|
b5fec6c87f | ||
|
|
0d314db1f8 | ||
|
|
660aae62e0 | ||
|
|
9a599aebea | ||
|
|
296c1cf83f | ||
|
|
516d9a27e7 | ||
|
|
6a05fa777c | ||
|
|
3f0be7fbe4 | ||
|
|
ea08c0511a | ||
|
|
727b9b5d72 | ||
|
|
50f0ba29a2 | ||
|
|
95b1141b9d | ||
|
|
a26eb59392 | ||
|
|
5befcf4780 | ||
|
|
f861a7c160 | ||
|
|
06426c8f7e | ||
|
|
8fc7b9f4c6 | ||
|
|
2de48b267a |
@@ -20,6 +20,7 @@ Component.Breadcrumbs({
|
||||
rootName: "Home", // name of first/root element
|
||||
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||
showCurrentPage: true, // wether to display the current page in the breadcrumbs
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -167,6 +167,17 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c
|
||||
4. Go to the Settings tab and then click Domains in the sidebar
|
||||
5. Enter your subdomain into the field and press Add
|
||||
|
||||
## Netlify
|
||||
|
||||
Like Vercel, you can also deploy the site generated by Quartz 4 via Netlify.
|
||||
|
||||
1. Log in to the [Netlify dashboard](https://app.netlify.com/) and click "Add new site".
|
||||
2. Select your Git provider and repository containing your Quartz project.
|
||||
3. Under "Build command", enter `npx quartz build`.
|
||||
4. Under "Publish directory", enter `public`.
|
||||
5. Press Deploy. Once it's live, you'll have a `*.netlify.app` URL to view the page.
|
||||
6. To add a custom domain, check "Domain management" in the left sidebar, just like with Vercel.
|
||||
|
||||
## GitLab Pages
|
||||
|
||||
You can configure GitLab CI to build and deploy a Quartz 4 project.
|
||||
|
||||
@@ -20,5 +20,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
||||
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
||||
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
||||
|
||||
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)!
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@jackyzha0/quartz",
|
||||
"version": "4.0.11",
|
||||
"version": "4.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@jackyzha0/quartz",
|
||||
"version": "4.0.11",
|
||||
"version": "4.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.6.3",
|
||||
@@ -85,7 +85,8 @@
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14"
|
||||
"node": ">=18.14",
|
||||
"npm": ">=9.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@jackyzha0/quartz",
|
||||
"description": "🌱 publish your digital garden and notes as a website",
|
||||
"private": true,
|
||||
"version": "4.1.1",
|
||||
"version": "4.1.2",
|
||||
"type": "module",
|
||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -25,6 +25,10 @@ interface BreadcrumbOptions {
|
||||
* Wether to display breadcrumbs on root `index.md`
|
||||
*/
|
||||
hideOnRoot: boolean
|
||||
/**
|
||||
* Wether to display the current page in the breadcrumbs.
|
||||
*/
|
||||
showCurrentPage: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: BreadcrumbOptions = {
|
||||
@@ -32,6 +36,7 @@ const defaultOptions: BreadcrumbOptions = {
|
||||
rootName: "Home",
|
||||
resolveFrontmatterTitle: true,
|
||||
hideOnRoot: true,
|
||||
showCurrentPage: true,
|
||||
}
|
||||
|
||||
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
||||
@@ -95,10 +100,12 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
}
|
||||
|
||||
// Add current file to crumb (can directly use frontmatter title)
|
||||
crumbs.push({
|
||||
displayName: fileData.frontmatter!.title,
|
||||
path: "",
|
||||
})
|
||||
if (options.showCurrentPage) {
|
||||
crumbs.push({
|
||||
displayName: fileData.frontmatter!.title,
|
||||
path: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
return (
|
||||
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
||||
|
||||
@@ -3,9 +3,10 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||
import HeaderConstructor from "./Header"
|
||||
import BodyConstructor from "./Body"
|
||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||
import { FullSlug, RelativeURL, joinSegments } from "../util/path"
|
||||
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { Root, Element, ElementContent } from "hast"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
|
||||
interface RenderComponents {
|
||||
head: QuartzComponent
|
||||
@@ -49,6 +50,18 @@ export function pageResources(
|
||||
}
|
||||
}
|
||||
|
||||
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
|
||||
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
|
||||
if (!pageIndex) {
|
||||
pageIndex = new Map()
|
||||
for (const file of allFiles) {
|
||||
pageIndex.set(file.slug!, file)
|
||||
}
|
||||
}
|
||||
|
||||
return pageIndex
|
||||
}
|
||||
|
||||
export function renderPage(
|
||||
slug: FullSlug,
|
||||
componentData: QuartzComponentProps,
|
||||
@@ -62,17 +75,15 @@ export function renderPage(
|
||||
if (classNames.includes("transclude")) {
|
||||
const inner = node.children[0] as Element
|
||||
const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
|
||||
|
||||
// TODO: avoid this expensive find operation and construct an index ahead of time
|
||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
||||
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
|
||||
let blockRef = node.properties?.dataBlock as string | undefined
|
||||
if (blockRef?.startsWith("^")) {
|
||||
if (blockRef?.startsWith("#^")) {
|
||||
// block transclude
|
||||
blockRef = blockRef.slice(1)
|
||||
blockRef = blockRef.slice("#^".length)
|
||||
let blockNode = page.blocks?.[blockRef]
|
||||
if (blockNode) {
|
||||
if (blockNode.tagName === "li") {
|
||||
@@ -84,7 +95,7 @@ export function renderPage(
|
||||
}
|
||||
|
||||
node.children = [
|
||||
blockNode,
|
||||
normalizeHastElement(blockNode, slug, transcludeTarget),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
@@ -104,7 +115,7 @@ export function renderPage(
|
||||
break
|
||||
}
|
||||
|
||||
if (startIdx) {
|
||||
if (startIdx !== undefined) {
|
||||
endIdx = i
|
||||
} else if (el.properties?.id === blockRef) {
|
||||
startIdx = i
|
||||
@@ -112,12 +123,14 @@ export function renderPage(
|
||||
}
|
||||
}
|
||||
|
||||
if (!startIdx) {
|
||||
if (startIdx === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
node.children = [
|
||||
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]),
|
||||
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
|
||||
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||
),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
@@ -135,7 +148,9 @@ export function renderPage(
|
||||
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||
],
|
||||
},
|
||||
...(page.htmlAst.children as ElementContent[]),
|
||||
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||
),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
|
||||
@@ -120,9 +120,9 @@ function setupExplorer() {
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
} else if (explorer?.dataset.tree) {
|
||||
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
||||
explorerState = JSON.parse(explorer?.dataset.tree as string)
|
||||
explorerState = JSON.parse(explorer.dataset.tree)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +130,13 @@ window.addEventListener("resize", setupExplorer)
|
||||
document.addEventListener("nav", () => {
|
||||
setupExplorer()
|
||||
|
||||
const explorerContent = document.getElementById("explorer-ul")
|
||||
observer.disconnect()
|
||||
|
||||
// select pseudo element at end of list
|
||||
const lastItem = document.getElementById("explorer-end")
|
||||
|
||||
observer.disconnect()
|
||||
observer.observe(lastItem as Element)
|
||||
if (lastItem) {
|
||||
observer.observe(lastItem)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex"
|
||||
import * as d3 from "d3"
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||
@@ -46,20 +46,22 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
showTags,
|
||||
} = JSON.parse(graph.dataset["cfg"]!)
|
||||
|
||||
const data = await fetchData
|
||||
|
||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
||||
simplifySlug(k as FullSlug),
|
||||
v,
|
||||
]),
|
||||
)
|
||||
const links: LinkData[] = []
|
||||
const tags: SimpleSlug[] = []
|
||||
|
||||
const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug)))
|
||||
|
||||
for (const [src, details] of Object.entries<ContentDetails>(data)) {
|
||||
const source = simplifySlug(src as FullSlug)
|
||||
const validLinks = new Set(data.keys())
|
||||
for (const [source, details] of data.entries()) {
|
||||
const outgoing = details.links ?? []
|
||||
|
||||
for (const dest of outgoing) {
|
||||
if (validLinks.has(dest)) {
|
||||
links.push({ source, target: dest })
|
||||
links.push({ source: source, target: dest })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
tags.push(...localTags.filter((tag) => !tags.includes(tag)))
|
||||
|
||||
for (const tag of localTags) {
|
||||
links.push({ source, target: tag })
|
||||
links.push({ source: source, target: tag })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,17 +95,17 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
|
||||
validLinks.forEach((id) => neighbourhood.add(id))
|
||||
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
||||
}
|
||||
|
||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||
nodes: [...neighbourhood].map((url) => {
|
||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url
|
||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
|
||||
return {
|
||||
id: url,
|
||||
text: text,
|
||||
tags: data[url]?.tags ?? [],
|
||||
tags: data.get(url)?.tags ?? [],
|
||||
}
|
||||
}),
|
||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
||||
@@ -200,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||
})
|
||||
.on("mouseover", function (_, d) {
|
||||
const neighbours: SimpleSlug[] = data[fullSlug].links ?? []
|
||||
const neighbours: SimpleSlug[] = data.get(slug)?.links ?? []
|
||||
const neighbourNodes = d3
|
||||
.selectAll<HTMLElement, NodeData>(".node")
|
||||
.filter((d) => neighbours.includes(d.id))
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||
|
||||
// from micromorph/src/utils.ts
|
||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||
export function normalizeRelativeURLs(el: Element | Document, base: string | URL) {
|
||||
const update = (el: Element, attr: string, base: string | URL) => {
|
||||
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
||||
}
|
||||
|
||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
|
||||
|
||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
|
||||
}
|
||||
import { normalizeRelativeURLs } from "../../util/path"
|
||||
|
||||
const p = new DOMParser()
|
||||
async function mouseEnterHandler(
|
||||
@@ -43,8 +32,6 @@ async function mouseEnterHandler(
|
||||
const hash = targetUrl.hash
|
||||
targetUrl.hash = ""
|
||||
targetUrl.search = ""
|
||||
// prevent hover of the same page
|
||||
if (thisUrl.toString() === targetUrl.toString()) return
|
||||
|
||||
const contents = await fetch(`${targetUrl}`)
|
||||
.then((res) => res.text())
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import micromorph from "micromorph"
|
||||
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
|
||||
import { normalizeRelativeURLs } from "./popover.inline"
|
||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||
|
||||
// adapted from `micromorph`
|
||||
// https://github.com/natemoo-re/micromorph
|
||||
|
||||
const NODE_TYPE_ELEMENT = 1
|
||||
let announcer = document.createElement("route-announcer")
|
||||
const isElement = (target: EventTarget | null): target is Element =>
|
||||
@@ -45,7 +43,14 @@ let p: DOMParser
|
||||
async function navigate(url: URL, isBack: boolean = false) {
|
||||
p = p || new DOMParser()
|
||||
const contents = await fetch(`${url}`)
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
const contentType = res.headers.get("content-type")
|
||||
if (contentType?.startsWith("text/html")) {
|
||||
return res.text()
|
||||
} else {
|
||||
window.location.assign(url)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.assign(url)
|
||||
})
|
||||
@@ -109,6 +114,7 @@ function createRouter() {
|
||||
if (isSamePage(url) && url.hash) {
|
||||
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||
el?.scrollIntoView()
|
||||
history.pushState({}, "", url)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ button#toc {
|
||||
overflow: hidden;
|
||||
max-height: none;
|
||||
transition: max-height 0.5s ease;
|
||||
position: relative;
|
||||
|
||||
&.collapsed > .overflow::after {
|
||||
opacity: 0;
|
||||
|
||||
29
quartz/plugins/emitters/cname.ts
Normal file
29
quartz/plugins/emitters/cname.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { FilePath, joinSegments } from "../../util/path"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
import fs from "fs"
|
||||
import chalk from "chalk"
|
||||
|
||||
export function extractDomainFromBaseUrl(baseUrl: string) {
|
||||
const url = new URL(`https://${baseUrl}`)
|
||||
return url.hostname
|
||||
}
|
||||
|
||||
export const CNAME: QuartzEmitterPlugin = () => ({
|
||||
name: "CNAME",
|
||||
getQuartzComponents() {
|
||||
return []
|
||||
},
|
||||
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||
if (!cfg.configuration.baseUrl) {
|
||||
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||
return []
|
||||
}
|
||||
const path = joinSegments(argv.output, "CNAME")
|
||||
const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl)
|
||||
if (!content) {
|
||||
return []
|
||||
}
|
||||
fs.writeFileSync(path, content)
|
||||
return [path] as FilePath[]
|
||||
},
|
||||
})
|
||||
@@ -7,3 +7,4 @@ export { Assets } from "./assets"
|
||||
export { Static } from "./static"
|
||||
export { ComponentResources } from "./componentResources"
|
||||
export { NotFoundPage } from "./404"
|
||||
export { CNAME } from "./cname"
|
||||
|
||||
@@ -11,7 +11,10 @@ export const Static: QuartzEmitterPlugin = () => ({
|
||||
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||
const staticPath = joinSegments(QUARTZ, "static")
|
||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { recursive: true })
|
||||
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
})
|
||||
return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -54,6 +54,16 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||
node.properties.className ??= []
|
||||
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
||||
|
||||
// Check if the link has alias text
|
||||
if (
|
||||
node.children.length === 1 &&
|
||||
node.children[0].type === "text" &&
|
||||
node.children[0].value !== dest
|
||||
) {
|
||||
// Add the 'alias' class if the text content is not the same as the href
|
||||
node.properties.className.push("alias")
|
||||
}
|
||||
|
||||
if (opts.openLinksInNewTab) {
|
||||
node.properties.target = "_blank"
|
||||
}
|
||||
@@ -71,14 +81,16 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
||||
const url = new URL(dest, `https://base.com/${curSlug}`)
|
||||
const canonicalDest = url.pathname
|
||||
const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
||||
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
||||
if (destCanonical.endsWith("/")) {
|
||||
destCanonical += "index"
|
||||
}
|
||||
|
||||
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
|
||||
const simple = decodeURIComponent(
|
||||
simplifySlug(destCanonical as FullSlug),
|
||||
) as SimpleSlug
|
||||
const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug
|
||||
const simple = simplifySlug(full)
|
||||
outgoing.add(simple)
|
||||
node.properties["data-slug"] = simple
|
||||
node.properties["data-slug"] = full
|
||||
}
|
||||
|
||||
// rewrite link internals if prettylinks is on
|
||||
|
||||
@@ -110,7 +110,10 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
||||
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
||||
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
||||
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
|
||||
export const wikilinkRegex = new RegExp(
|
||||
/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/,
|
||||
"g",
|
||||
)
|
||||
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
|
||||
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
||||
@@ -178,8 +181,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
||||
const [rawFp, rawHeader, rawAlias] = capture
|
||||
const fp = rawFp ?? ""
|
||||
const anchor = rawHeader?.trim().slice(1)
|
||||
const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : ""
|
||||
const anchor = rawHeader?.trim().replace(/^#+/, "")
|
||||
const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : ""
|
||||
const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : ""
|
||||
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
||||
const embedDisplay = value.startsWith("!") ? "!" : ""
|
||||
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
||||
@@ -436,7 +440,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
const blockTagTypes = new Set(["blockquote"])
|
||||
return (tree, file) => {
|
||||
file.data.blocks = {}
|
||||
file.data.htmlAst = tree
|
||||
|
||||
visit(tree, "element", (node, index, parent) => {
|
||||
if (blockTagTypes.has(node.tagName)) {
|
||||
@@ -478,6 +481,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
file.data.htmlAst = tree
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Root } from "mdast"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { toString } from "mdast-util-to-string"
|
||||
import Slugger from "github-slugger"
|
||||
import { wikilinkRegex } from "./ofm"
|
||||
|
||||
export interface Options {
|
||||
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
||||
@@ -24,6 +25,7 @@ interface TocEntry {
|
||||
slug: string // this is just the anchor (#some-slug), not the canonical slug
|
||||
}
|
||||
|
||||
const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g")
|
||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||
userOpts,
|
||||
) => {
|
||||
@@ -41,7 +43,16 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
||||
let highestDepth: number = opts.maxDepth
|
||||
visit(tree, "heading", (node) => {
|
||||
if (node.depth <= opts.maxDepth) {
|
||||
const text = toString(node)
|
||||
let text = toString(node)
|
||||
|
||||
// strip link formatting from toc entries
|
||||
text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => {
|
||||
const fp = rawFp?.trim() ?? ""
|
||||
const alias = rawAlias?.slice(1).trim()
|
||||
return alias ?? fp
|
||||
})
|
||||
text = text.replace(regexMdLinks, "$1")
|
||||
|
||||
highestDepth = Math.min(highestDepth, node.depth)
|
||||
toc.push({
|
||||
depth: node.depth,
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("transforms", () => {
|
||||
test("simplifySlug", () => {
|
||||
asserts(
|
||||
[
|
||||
["index", ""],
|
||||
["index", "/"],
|
||||
["abc", "abc"],
|
||||
["abc/index", "abc/"],
|
||||
["abc/def", "abc/def"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { slug } from "github-slugger"
|
||||
import type { Element as HastElement } from "hast"
|
||||
// this file must be isomorphic so it can't use node libs (e.g. path)
|
||||
|
||||
export const QUARTZ = "quartz"
|
||||
@@ -24,7 +25,7 @@ export function isFullSlug(s: string): s is FullSlug {
|
||||
/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
|
||||
export type SimpleSlug = SlugLike<"simple">
|
||||
export function isSimpleSlug(s: string): s is SimpleSlug {
|
||||
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
||||
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
|
||||
const validEnding = !(s.endsWith("/index") || s === "index")
|
||||
return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
||||
}
|
||||
@@ -65,7 +66,8 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
}
|
||||
|
||||
export function simplifySlug(fp: FullSlug): SimpleSlug {
|
||||
return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug
|
||||
const res = _stripSlashes(_trimSuffix(fp, "index"), true)
|
||||
return (res.length === 0 ? "/" : res) as SimpleSlug
|
||||
}
|
||||
|
||||
export function transformInternalLink(link: string): RelativeURL {
|
||||
@@ -84,6 +86,49 @@ export function transformInternalLink(link: string): RelativeURL {
|
||||
return res
|
||||
}
|
||||
|
||||
// from micromorph/src/utils.ts
|
||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||
const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => {
|
||||
const rebased = new URL(el.getAttribute(attr)!, newBase)
|
||||
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
||||
}
|
||||
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
||||
_rebaseHtmlElement(item, "href", destination),
|
||||
)
|
||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
||||
_rebaseHtmlElement(item, "src", destination),
|
||||
)
|
||||
}
|
||||
|
||||
const _rebaseHastElement = (
|
||||
el: HastElement,
|
||||
attr: string,
|
||||
curBase: FullSlug,
|
||||
newBase: FullSlug,
|
||||
) => {
|
||||
if (el.properties?.[attr]) {
|
||||
if (!isRelativeURL(String(el.properties[attr]))) {
|
||||
return
|
||||
}
|
||||
|
||||
const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string)
|
||||
el.properties[attr] = rel
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
||||
_rebaseHastElement(el, "src", curBase, newBase)
|
||||
_rebaseHastElement(el, "href", curBase, newBase)
|
||||
if (el.children) {
|
||||
el.children = el.children.map((child) =>
|
||||
normalizeHastElement(child as HastElement, curBase, newBase),
|
||||
)
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
// resolve /a/b/c to ../..
|
||||
export function pathToRoot(slug: FullSlug): RelativeURL {
|
||||
let rootPath = slug
|
||||
|
||||
Reference in New Issue
Block a user