diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index 584746c..5096977 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -1,6 +1,7 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss"
import { relativeToRoot } from "../path"
+import { stripIndex } from "./scripts/util"
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
const slug = fileData.slug!
@@ -9,7 +10,7 @@ function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
Backlinks
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index 1c1149d..4ff2dfe 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -1,6 +1,6 @@
import { ContentDetails } from "../../plugins/emitters/contentIndex"
import * as d3 from 'd3'
-import { registerEscapeHandler } from "./handler"
+import { registerEscapeHandler, relative, removeAllChildren } from "./util"
type NodeData = {
id: string,
@@ -13,18 +13,6 @@ type LinkData = {
target: string
}
-function relative(from: string, to: string) {
- const pieces = [location.protocol, '//', location.host, location.pathname]
- const url = pieces.join('').slice(0, -from.length) + to
- return url
-}
-
-function removeAllChildren(node: HTMLElement) {
- while (node.firstChild) {
- node.removeChild(node.firstChild)
- }
-}
-
async function renderGraph(container: string, slug: string) {
const graph = document.getElementById(container)
if (!graph) return
@@ -117,7 +105,6 @@ async function renderGraph(container: string, slug: string) {
// calculate radius
const color = (d: NodeData) => {
- // TODO: does this handle the index page
const isCurrent = d.id === slug
return isCurrent ? "var(--secondary)" : "var(--gray)"
}
diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts
index f7cd998..655831d 100644
--- a/quartz/components/scripts/popover.inline.ts
+++ b/quartz/components/scripts/popover.inline.ts
@@ -7,10 +7,11 @@ document.addEventListener("nav", () => {
link.addEventListener("mouseenter", async ({ clientX, clientY }) => {
async function setPosition(popoverElement: HTMLElement) {
const { x, y } = await computePosition(link, popoverElement, {
- middleware: [inline({
- x: clientX,
- y: clientY
- }), shift(), flip()]
+ middleware: [
+ inline({ x: clientX, y: clientY }),
+ shift(),
+ flip()
+ ]
})
Object.assign(popoverElement.style, {
left: `${x}px`,
@@ -22,11 +23,17 @@ document.addEventListener("nav", () => {
return setPosition(link.lastChild as HTMLElement)
}
- const url = link.href
- const anchor = new URL(url).hash
- if (anchor.startsWith("#")) return
+ const thisUrl = new URL(document.location.href)
+ thisUrl.hash = ""
+ thisUrl.search = ""
+ const targetUrl = new URL(link.href)
+ const hash = targetUrl.hash
+ targetUrl.hash = ""
+ targetUrl.search = ""
+ // prevent hover of the same page
+ if (thisUrl.toString() === targetUrl.toString()) return
- const contents = await fetch(`${url}`)
+ const contents = await fetch(`${targetUrl}`)
.then((res) => res.text())
.catch((err) => {
console.error(err)
@@ -39,7 +46,6 @@ document.addEventListener("nav", () => {
const popoverElement = document.createElement("div")
popoverElement.classList.add("popover")
- // TODO: scroll this element if we specify a header/anchor to jump to
const popoverInner = document.createElement("div")
popoverInner.classList.add("popover-inner")
popoverElement.appendChild(popoverInner)
@@ -48,6 +54,12 @@ document.addEventListener("nav", () => {
setPosition(popoverElement)
link.appendChild(popoverElement)
link.dataset.fetchedPopover = "true"
+
+ const heading = popoverInner.querySelector(hash) as HTMLElement | null
+ if (heading) {
+ // leave ~12px of buffer when scrolling to a heading
+ popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
+ }
})
}
})
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts
index b1c6265..78517fe 100644
--- a/quartz/components/scripts/search.inline.ts
+++ b/quartz/components/scripts/search.inline.ts
@@ -1,6 +1,6 @@
import { Document } from "flexsearch"
import { ContentDetails } from "../../plugins/emitters/contentIndex"
-import { registerEscapeHandler } from "./handler"
+import { registerEscapeHandler, relative, removeAllChildren } from "./util"
interface Item {
slug: string,
@@ -9,16 +9,6 @@ interface Item {
}
let index: Document- | undefined = undefined
-function relative(from: string, to: string) {
- const pieces = [location.protocol, '//', location.host, location.pathname]
- const url = pieces.join('').slice(0, -from.length) + to
- return url
-}
-
-function removeAllChildren(node: HTMLElement) {
- node.innerHTML = ``
-}
-
const contextWindowWords = 30
function highlight(searchTerm: string, text: string, trim?: boolean) {
const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "")
diff --git a/quartz/components/scripts/handler.ts b/quartz/components/scripts/util.ts
similarity index 51%
rename from quartz/components/scripts/handler.ts
rename to quartz/components/scripts/util.ts
index c806a8b..e94929b 100644
--- a/quartz/components/scripts/handler.ts
+++ b/quartz/components/scripts/util.ts
@@ -17,3 +17,22 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
document.removeEventListener("keydown", esc)
document.addEventListener('keydown', esc)
}
+
+export function stripIndex(s: string): string {
+ return s.endsWith("index") ? s.slice(0, -"index".length) : s
+}
+
+export function relative(from: string, to: string) {
+ from = encodeURI(stripIndex(from))
+ to = encodeURI(stripIndex(to))
+ const start = [location.protocol, '//', location.host, location.pathname].join('')
+ const trimEnd = from.length === 0 ? start.length : -from.length
+ const url = start.slice(0, trimEnd) + to
+ return url
+}
+
+export function removeAllChildren(node: HTMLElement) {
+ while (node.firstChild) {
+ node.removeChild(node.firstChild)
+ }
+}
diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss
index 9b79447..f95dc7b 100644
--- a/quartz/components/styles/popover.scss
+++ b/quartz/components/styles/popover.scss
@@ -19,6 +19,7 @@
padding: 1rem;
& > .popover-inner {
+ position: relative;
width: 30rem;
height: 20rem;
padding: 0 1rem 1rem 1rem;
diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts
index ed59f82..0e41c5b 100644
--- a/quartz/plugins/transformers/description.ts
+++ b/quartz/plugins/transformers/description.ts
@@ -14,9 +14,6 @@ export const Description: QuartzTransformerPlugin | undefined>
const opts = { ...defaultOptions, ...userOpts }
return {
name: "Description",
- markdownPlugins() {
- return []
- },
htmlPlugins() {
return [
() => {
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index 5568463..fd91755 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/quartz/plugins/transformers/frontmatter.ts
@@ -33,9 +33,6 @@ export const FrontMatter: QuartzTransformerPlugin | undefined>
}
]
},
- htmlPlugins() {
- return []
- }
}
}
diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts
index b7514e4..f3a8904 100644
--- a/quartz/plugins/transformers/lastmod.ts
+++ b/quartz/plugins/transformers/lastmod.ts
@@ -53,9 +53,6 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und
}
]
},
- htmlPlugins() {
- return []
- }
}
}
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index 3083ce7..1391452 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types"
-import { relative, relativeToRoot, slugify, trimPathSuffix } from "../../path"
+import { relativeToRoot, slugify, trimPathSuffix } from "../../path"
import path from "path"
import { visit } from 'unist-util-visit'
import isAbsoluteUrl from "is-absolute-url"
@@ -24,9 +24,6 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> =
const opts = { ...defaultOptions, ...userOpts }
return {
name: "LinkProcessing",
- markdownPlugins() {
- return []
- },
htmlPlugins() {
return [() => {
return (tree, file) => {
@@ -34,7 +31,8 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> =
const transformLink = (target: string) => {
const targetSlug = slugify(decodeURI(target).trim())
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
- return './' + relative(curSlug, targetSlug)
+ // TODO
+ // return './' + relative(curSlug, targetSlug)
} else {
return './' + relativeToRoot(curSlug, targetSlug)
}
@@ -77,9 +75,9 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> =
}
}
- // transform all images
+ // transform all other resources that may use links
if (
- node.tagName === 'img' &&
+ ["img", "video", "audio", "iframe"].includes(node.tagName) &&
node.properties &&
typeof node.properties.src === 'string'
) {
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 3742d4b..0deec4b 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -3,6 +3,7 @@ import { QuartzTransformerPlugin } from "../types"
import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast'
import { findAndReplace } from "mdast-util-find-and-replace"
import { slugify } from "../../path"
+import { slug as slugAnchor } from 'github-slugger'
import rehypeRaw from "rehype-raw"
import { visit } from "unist-util-visit"
import path from "path"
@@ -94,21 +95,43 @@ const capitalize = (s: string): string => {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
+// Match wikilinks
+// !? -> optional embedding
+// \[\[ -> open brace
+// ([^\[\]\|\#]+) -> 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 backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
+
+// Match highlights
+const highlightRegex = new RegExp(/==(.+)==/, "g")
+
+// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
+const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
+
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "ObsidianFlavoredMarkdown",
+ textTransform(src) {
+ // pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
+ if (opts.wikilinks) {
+ src = src.toString()
+ return src.replaceAll(backlinkRegex, (value, ...capture) => {
+ const [fp, rawHeader, rawAlias] = capture
+ const anchor = rawHeader?.trim().slice(1)
+ const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : ""
+ const displayAlias = rawAlias ?? ""
+ const embedDisplay = value.startsWith("!") ? "!" : ""
+ return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
+ })
+ }
+ return src
+ },
markdownPlugins() {
const plugins: PluggableList = []
if (opts.wikilinks) {
plugins.push(() => {
- // Match wikilinks
- // !? -> optional embedding
- // \[\[ -> open brace
- // ([^\[\]\|\#]+) -> 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 backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
return (tree: Root, _file) => {
findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
const [fp, rawHeader, rawAlias] = capture
@@ -170,8 +193,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin
if (opts.highlight) {
plugins.push(() => {
- // Match highlights
- const highlightRegex = new RegExp(/==(.+)==/, "g")
return (tree: Root, _file) => {
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
const [inner] = capture
@@ -186,8 +207,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin
if (opts.callouts) {
plugins.push(() => {
- // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
- const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
return (tree: Root, _file) => {
visit(tree, "blockquote", (node) => {
if (node.children.length === 0) {
diff --git a/quartz/plugins/transformers/syntax.ts b/quartz/plugins/transformers/syntax.ts
index 16424ec..0f46519 100644
--- a/quartz/plugins/transformers/syntax.ts
+++ b/quartz/plugins/transformers/syntax.ts
@@ -3,9 +3,6 @@ import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
name: "SyntaxHighlighting",
- markdownPlugins() {
- return []
- },
htmlPlugins() {
return [[rehypePrettyCode, {
theme: 'css-variables',
@@ -15,10 +12,12 @@ export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
}
},
onVisitHighlightedLine(node) {
+ node.properties.className ??= []
node.properties.className.push('highlighted')
},
onVisitHighlightedWord(node) {
- node.properties.className = ['word']
+ node.properties.className ??= []
+ node.properties.className.push('word')
},
} satisfies Partial]]
}
diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts
index 172f082..8d37def 100644
--- a/quartz/plugins/transformers/toc.ts
+++ b/quartz/plugins/transformers/toc.ts
@@ -52,9 +52,6 @@ export const TableOfContents: QuartzTransformerPlugin | undefin
}
}]
},
- htmlPlugins() {
- return []
- }
}
}
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index 444fcff..f74b3c9 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -14,9 +14,10 @@ type OptionType = object | undefined
export type QuartzTransformerPlugin = (opts?: Options) => QuartzTransformerPluginInstance
export type QuartzTransformerPluginInstance = {
name: string
- markdownPlugins(): PluggableList
- htmlPlugins(): PluggableList
- externalResources?(): Partial
+ textTransform?: (src: string | Buffer) => string | Buffer
+ markdownPlugins?: () => PluggableList
+ htmlPlugins?: () => PluggableList
+ externalResources?: () => Partial
}
export type QuartzFilterPlugin = (opts?: Options) => QuartzFilterPluginInstance
diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts
index f937701..6560bf6 100644
--- a/quartz/processors/parse.ts
+++ b/quartz/processors/parse.ts
@@ -21,8 +21,8 @@ export function createProcessor(transformers: QuartzTransformerPluginInstance[])
let processor = unified().use(remarkParse)
// MD AST -> MD AST transforms
- for (const plugin of transformers) {
- processor = processor.use(plugin.markdownPlugins())
+ for (const plugin of transformers.filter(p => p.markdownPlugins)) {
+ processor = processor.use(plugin.markdownPlugins!())
}
// MD AST -> HTML AST
@@ -30,8 +30,8 @@ export function createProcessor(transformers: QuartzTransformerPluginInstance[])
// HTML AST -> HTML AST transforms
- for (const plugin of transformers) {
- processor = processor.use(plugin.htmlPlugins())
+ for (const plugin of transformers.filter(p => p.htmlPlugins)) {
+ processor = processor.use(plugin.htmlPlugins!())
}
return processor
@@ -73,13 +73,18 @@ async function transpileWorkerScript() {
})
}
-export function createFileParser(baseDir: string, fps: string[], verbose: boolean) {
+export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: string[], verbose: boolean) {
return async (processor: QuartzProcessor) => {
const res: ProcessedContent[] = []
for (const fp of fps) {
try {
const file = await read(fp)
+ // Text -> Text transforms
+ for (const plugin of transformers.filter(p => p.textTransform)) {
+ file.value = plugin.textTransform!(file.value)
+ }
+
// base data properties that plugins may use
file.data.slug = slugify(path.relative(baseDir, file.path))
file.data.filePath = fp
@@ -111,9 +116,8 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
log.start(`Parsing input files using ${concurrency} threads`)
if (concurrency === 1) {
- // single-thread
const processor = createProcessor(transformers)
- const parse = createFileParser(baseDir, fps, verbose)
+ const parse = createFileParser(transformers, baseDir, fps, verbose)
res = await parse(processor)
} else {
await transpileWorkerScript()
diff --git a/quartz/worker.ts b/quartz/worker.ts
index e2c278f..be2c0d1 100644
--- a/quartz/worker.ts
+++ b/quartz/worker.ts
@@ -6,6 +6,6 @@ const processor = createProcessor(transformers)
// only called from worker thread
export async function parseFiles(baseDir: string, fps: string[], verbose: boolean) {
- const parse = createFileParser(baseDir, fps, verbose)
+ const parse = createFileParser(transformers, baseDir, fps, verbose)
return parse(processor)
}