From 24348b24a94c5f9ca285642b751e6798b92eedd9 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Tue, 20 Jun 2023 05:50:25 +0000
Subject: [PATCH] fix: parsing wikilinks that have codeblock anchors, scroll to anchor

---
 quartz/plugins/types.ts                     |    7 +
 quartz/components/Backlinks.tsx             |    3 
 quartz/components/scripts/util.ts           |   38 +++++++++
 quartz/plugins/transformers/description.ts  |    3 
 quartz/components/scripts/popover.inline.ts |   30 +++++--
 quartz/plugins/transformers/frontmatter.ts  |    3 
 quartz/components/scripts/search.inline.ts  |   12 --
 quartz/worker.ts                            |    2 
 quartz/processors/parse.ts                  |   18 ++-
 quartz/components/scripts/graph.inline.ts   |   15 ---
 quartz/plugins/transformers/links.ts        |   12 +-
 /dev/null                                   |   19 ----
 quartz/plugins/transformers/syntax.ts       |    7 -
 quartz/plugins/transformers/ofm.ts          |   41 +++++++--
 quartz/plugins/transformers/toc.ts          |    3 
 quartz/plugins/transformers/lastmod.ts      |    3 
 quartz/components/styles/popover.scss       |    1 
 17 files changed, 118 insertions(+), 99 deletions(-)

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 @@
     <h3>Backlinks</h3>
     <ul>
       {backlinkFiles.length > 0 ?
-        backlinkFiles.map(f => <li><a href={relativeToRoot(slug, f.slug!)} class="internal">{f.frontmatter?.title}</a></li>)
+        backlinkFiles.map(f => <li><a href={stripIndex(relativeToRoot(slug, f.slug!))} class="internal">{f.frontmatter?.title}</a></li>)
         : <li>No backlinks found</li>}
     </ul>
   </div> 
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 @@
   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 @@
 
   // 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/handler.ts b/quartz/components/scripts/handler.ts
deleted file mode 100644
index c806a8b..0000000
--- a/quartz/components/scripts/handler.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) {
-  if (!outsideContainer) return
-  function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
-    if (e.target !== this) return
-    e.preventDefault()
-    cb()
-  }
-
-  function esc(e: HTMLElementEventMap["keydown"]) {
-    if (!e.key.startsWith("Esc")) return
-    e.preventDefault()
-    cb()
-  }
-
-  outsideContainer?.removeEventListener("click", click)
-  outsideContainer?.addEventListener("click", click)
-  document.removeEventListener("keydown", esc)
-  document.addEventListener('keydown', esc)
-}
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 @@
     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 @@
         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 @@
 
       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 @@
       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 @@
 }
 let index: Document<Item> | 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/util.ts b/quartz/components/scripts/util.ts
new file mode 100644
index 0000000..e94929b
--- /dev/null
+++ b/quartz/components/scripts/util.ts
@@ -0,0 +1,38 @@
+export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) {
+  if (!outsideContainer) return
+  function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
+    if (e.target !== this) return
+    e.preventDefault()
+    cb()
+  }
+
+  function esc(e: HTMLElementEventMap["keydown"]) {
+    if (!e.key.startsWith("Esc")) return
+    e.preventDefault()
+    cb()
+  }
+
+  outsideContainer?.removeEventListener("click", click)
+  outsideContainer?.addEventListener("click", click)
+  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 @@
   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 @@
         }
       ]
     },
-    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 @@
         }
       ]
     },
-    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 @@
   const opts = { ...defaultOptions, ...userOpts }
   return {
     name: "LinkProcessing",
-    markdownPlugins() {
-      return []
-    },
     htmlPlugins() {
       return [() => {
         return (tree, file) => {
@@ -34,7 +31,8 @@
           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 @@
               }
             }
 
-            // 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 { 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 @@
   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<Partial<Options> | 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 @@
 
       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 @@
 
       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 @@
 
 export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
   name: "SyntaxHighlighting",
-  markdownPlugins() {
-    return []
-  },
   htmlPlugins() {
     return [[rehypePrettyCode, {
       theme: 'css-variables',
@@ -15,10 +12,12 @@
         }
       },
       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<CodeOptions>]]
   }
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 @@
         }
       }]
     },
-    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 @@
 export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzTransformerPluginInstance
 export type QuartzTransformerPluginInstance = {
   name: string
-  markdownPlugins(): PluggableList
-  htmlPlugins(): PluggableList
-  externalResources?(): Partial<StaticResources>
+  textTransform?: (src: string | Buffer) => string | Buffer
+  markdownPlugins?: () => PluggableList
+  htmlPlugins?: () => PluggableList
+  externalResources?: () => Partial<StaticResources>
 }
 
 export type QuartzFilterPlugin<Options extends OptionType = undefined> = (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 @@
   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 @@
 
 
   // 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 @@
   })
 }
 
-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 @@
 
   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 @@
 
 // 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)
 }

--
Gitblit v1.10.0