fix: parsing wikilinks that have codeblock anchors, scroll to anchor
This commit is contained in:
parent
cdb9fd576e
commit
821553fd2a
@ -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) {
|
||||
<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>
|
||||
|
@ -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)"
|
||||
}
|
||||
|
@ -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' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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<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 !== "")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
padding: 1rem;
|
||||
|
||||
& > .popover-inner {
|
||||
position: relative;
|
||||
width: 30rem;
|
||||
height: 20rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
|
@ -14,9 +14,6 @@ export const Description: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
name: "Description",
|
||||
markdownPlugins() {
|
||||
return []
|
||||
},
|
||||
htmlPlugins() {
|
||||
return [
|
||||
() => {
|
||||
|
@ -33,9 +33,6 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||
}
|
||||
]
|
||||
},
|
||||
htmlPlugins() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,9 +53,6 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
|
||||
}
|
||||
]
|
||||
},
|
||||
htmlPlugins() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Partial<Options> | undefined> =
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
name: "LinkProcessing",
|
||||
markdownPlugins() {
|
||||
return []
|
||||
},
|
||||
htmlPlugins() {
|
||||
return [() => {
|
||||
return (tree, file) => {
|
||||
@ -34,7 +31,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | 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<Partial<Options> | 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'
|
||||
) {
|
||||
|
@ -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<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 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
|
||||
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<Partial<Options>
|
||||
|
||||
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) {
|
||||
|
@ -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<CodeOptions>]]
|
||||
}
|
||||
|
@ -52,9 +52,6 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
||||
}
|
||||
}]
|
||||
},
|
||||
htmlPlugins() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,10 @@ type OptionType = object | undefined
|
||||
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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user