fix(navigation): support anchor fragments in note links
Links like `path/to/note.md#heading` previously errored with "Note not found" because the full href (including `#hash`) was matched against file paths. Split the fragment off in the link handler, plumb it through the event bus, and scroll the matching heading into view once the target note is in place. Headings now get GitHub-style ids via markdown-it-anchor + github-slugger so the anchors actually exist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
"@better-fetch/fetch": "^1.1.21",
|
"@better-fetch/fetch": "^1.1.21",
|
||||||
"@better-fetch/logger": "^1.1.21",
|
"@better-fetch/logger": "^1.1.21",
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||||
|
"@mdit/plugin-tab": "^0.24.2",
|
||||||
"@octokit/core": "^7.0.6",
|
"@octokit/core": "^7.0.6",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@openpanel/web": "^1.3.0",
|
"@openpanel/web": "^1.3.0",
|
||||||
@@ -40,8 +41,10 @@
|
|||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"font-color-contrast": "^11.1.0",
|
"font-color-contrast": "^11.1.0",
|
||||||
"fontfaceobserver": "^2.3.0",
|
"fontfaceobserver": "^2.3.0",
|
||||||
|
"github-slugger": "^2.0.0",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
"markdown-it-block-embed": "^0.0.3",
|
"markdown-it-block-embed": "^0.0.3",
|
||||||
"markdown-it-checkbox": "^1.1.0",
|
"markdown-it-checkbox": "^1.1.0",
|
||||||
"markdown-it-github-alerts": "^1.0.0",
|
"markdown-it-github-alerts": "^1.0.0",
|
||||||
|
|||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@intlify/unplugin-vue-i18n':
|
'@intlify/unplugin-vue-i18n':
|
||||||
specifier: ^6.0.8
|
specifier: ^6.0.8
|
||||||
version: 6.0.8(@vue/compiler-dom@3.5.28)(eslint@8.57.1)(rollup@2.79.2)(typescript@5.9.3)(vue-i18n@11.1.11(vue@3.5.18(typescript@5.9.3)))(vue@3.5.18(typescript@5.9.3))
|
version: 6.0.8(@vue/compiler-dom@3.5.28)(eslint@8.57.1)(rollup@2.79.2)(typescript@5.9.3)(vue-i18n@11.1.11(vue@3.5.18(typescript@5.9.3)))(vue@3.5.18(typescript@5.9.3))
|
||||||
|
'@mdit/plugin-tab':
|
||||||
|
specifier: ^0.24.2
|
||||||
|
version: 0.24.2(markdown-it@14.1.0)
|
||||||
'@octokit/core':
|
'@octokit/core':
|
||||||
specifier: ^7.0.6
|
specifier: ^7.0.6
|
||||||
version: 7.0.6
|
version: 7.0.6
|
||||||
@@ -74,12 +77,18 @@ importers:
|
|||||||
fontfaceobserver:
|
fontfaceobserver:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
|
github-slugger:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
isomorphic-fetch:
|
isomorphic-fetch:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
markdown-it:
|
markdown-it:
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0
|
version: 14.1.0
|
||||||
|
markdown-it-anchor:
|
||||||
|
specifier: ^9.2.0
|
||||||
|
version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0)
|
||||||
markdown-it-block-embed:
|
markdown-it-block-embed:
|
||||||
specifier: ^0.0.3
|
specifier: ^0.0.3
|
||||||
version: 0.0.3
|
version: 0.0.3
|
||||||
@@ -1307,6 +1316,24 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@mdit/helper@0.23.2':
|
||||||
|
resolution: {integrity: sha512-w4oja7kZYnkSiodfn4Neg1gmlIkvQtmCBJTLvLFOaET7xt8KomDNPQeumpGobQ9dWkXFqBKHlxjTYgroPH+CvA==}
|
||||||
|
engines: {node: '>= 20'}
|
||||||
|
peerDependencies:
|
||||||
|
markdown-it: ^14.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
markdown-it:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@mdit/plugin-tab@0.24.2':
|
||||||
|
resolution: {integrity: sha512-9rN23SP4beO0shBOuSGLGR+Ia7fminVSH6xl5Rb6rh6rRYQ6R3NR2KkIfLZvoMCRiN2uDwhXT/R9LyXHOdRMUQ==}
|
||||||
|
engines: {node: '>= 20'}
|
||||||
|
peerDependencies:
|
||||||
|
markdown-it: ^14.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
markdown-it:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@mermaid-js/parser@0.6.3':
|
'@mermaid-js/parser@0.6.3':
|
||||||
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
||||||
|
|
||||||
@@ -3871,6 +3898,9 @@ packages:
|
|||||||
getpass@0.1.7:
|
getpass@0.1.7:
|
||||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||||
|
|
||||||
|
github-slugger@2.0.0:
|
||||||
|
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||||
|
|
||||||
glob-base@0.3.0:
|
glob-base@0.3.0:
|
||||||
resolution: {integrity: sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==}
|
resolution: {integrity: sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4848,6 +4878,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
|
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
markdown-it-anchor@9.2.0:
|
||||||
|
resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/markdown-it': '*'
|
||||||
|
markdown-it: '*'
|
||||||
|
|
||||||
markdown-it-block-embed@0.0.3:
|
markdown-it-block-embed@0.0.3:
|
||||||
resolution: {integrity: sha512-coWuC/uZY6Z1Gp3wthhJo5yjkG3/gHErNF/emaiEvD98fKzEHNP6GCYGfJfk5o0n31xiaYjbDgef+XtabKOZzA==}
|
resolution: {integrity: sha512-coWuC/uZY6Z1Gp3wthhJo5yjkG3/gHErNF/emaiEvD98fKzEHNP6GCYGfJfk5o0n31xiaYjbDgef+XtabKOZzA==}
|
||||||
|
|
||||||
@@ -7880,6 +7916,19 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mdit/helper@0.23.2(markdown-it@14.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/markdown-it': 14.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
markdown-it: 14.1.0
|
||||||
|
|
||||||
|
'@mdit/plugin-tab@0.24.2(markdown-it@14.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@mdit/helper': 0.23.2(markdown-it@14.1.0)
|
||||||
|
'@types/markdown-it': 14.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
markdown-it: 14.1.0
|
||||||
|
|
||||||
'@mermaid-js/parser@0.6.3':
|
'@mermaid-js/parser@0.6.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
langium: 3.3.1
|
langium: 3.3.1
|
||||||
@@ -10560,6 +10609,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
assert-plus: 1.0.0
|
||||||
|
|
||||||
|
github-slugger@2.0.0: {}
|
||||||
|
|
||||||
glob-base@0.3.0:
|
glob-base@0.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-parent: 2.0.0
|
glob-parent: 2.0.0
|
||||||
@@ -11597,6 +11648,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit: 1.0.1
|
object-visit: 1.0.1
|
||||||
|
|
||||||
|
markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@types/markdown-it': 14.1.2
|
||||||
|
markdown-it: 14.1.0
|
||||||
|
|
||||||
markdown-it-block-embed@0.0.3: {}
|
markdown-it-block-embed@0.0.3: {}
|
||||||
|
|
||||||
markdown-it-checkbox@1.1.0:
|
markdown-it-checkbox@1.1.0:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface EventBusParams {
|
|||||||
user: string
|
user: string
|
||||||
repo: string
|
repo: string
|
||||||
path: string
|
path: string
|
||||||
|
hash?: string
|
||||||
currentNoteSHA?: string
|
currentNoteSHA?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ export const useLinks = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hashIndex = href.indexOf("#")
|
||||||
|
const path = hashIndex === -1 ? href : href.slice(0, hashIndex)
|
||||||
|
const hash = hashIndex === -1 ? undefined : href.slice(hashIndex + 1)
|
||||||
|
|
||||||
noteEventBus.emit({
|
noteEventBus.emit({
|
||||||
path: href,
|
path,
|
||||||
|
hash,
|
||||||
currentNoteSHA: toValue(sha),
|
currentNoteSHA: toValue(sha),
|
||||||
user: store.user,
|
user: store.user,
|
||||||
repo: store.repo
|
repo: store.repo
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { tab } from "@mdit/plugin-tab"
|
||||||
import markdownItKatex from "@vscode/markdown-it-katex"
|
import markdownItKatex from "@vscode/markdown-it-katex"
|
||||||
|
import GithubSlugger from "github-slugger"
|
||||||
import MarkdownIt, { Options } from "markdown-it"
|
import MarkdownIt, { Options } from "markdown-it"
|
||||||
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
|
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
|
||||||
import type Token from "markdown-it/lib/token.mjs"
|
import type Token from "markdown-it/lib/token.mjs"
|
||||||
|
import markdownItAnchor from "markdown-it-anchor"
|
||||||
import blockEmbedPlugin from "markdown-it-block-embed"
|
import blockEmbedPlugin from "markdown-it-block-embed"
|
||||||
import markdownItCheckbox from "markdown-it-checkbox"
|
import markdownItCheckbox from "markdown-it-checkbox"
|
||||||
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
||||||
@@ -45,6 +48,8 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slugger = new GithubSlugger()
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
typographer: true,
|
typographer: true,
|
||||||
quotes: ["«\xA0", "\xA0»", "‹\xA0", "\xA0›"]
|
quotes: ["«\xA0", "\xA0»", "‹\xA0", "\xA0›"]
|
||||||
@@ -64,6 +69,12 @@ const md = new MarkdownIt({
|
|||||||
})
|
})
|
||||||
.use(MarkdownItGitHubAlerts)
|
.use(MarkdownItGitHubAlerts)
|
||||||
.use(markdownItTablerIcons)
|
.use(markdownItTablerIcons)
|
||||||
|
.use(tab, {
|
||||||
|
name: "tabs"
|
||||||
|
})
|
||||||
|
.use(markdownItAnchor, {
|
||||||
|
slugify: (s: string) => slugger.slug(s)
|
||||||
|
})
|
||||||
|
|
||||||
let shikijiInitialized = false
|
let shikijiInitialized = false
|
||||||
|
|
||||||
@@ -123,11 +134,16 @@ const stripFrontmatter = (content: string): string => {
|
|||||||
return match ? content.slice(match[0].length) : content
|
return match ? content.slice(match[0].length) : content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderMarkdown = (content: string, env?: Record<string, unknown>) => {
|
||||||
|
slugger.reset()
|
||||||
|
return env ? md.render(content, env) : md.render(content)
|
||||||
|
}
|
||||||
|
|
||||||
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
||||||
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
||||||
const renderFromUTF8 = (content: string, prefix?: string) => {
|
const renderFromUTF8 = (content: string, prefix?: string) => {
|
||||||
return content
|
return content
|
||||||
? md.render(stripFrontmatter(content), {
|
? renderMarkdown(stripFrontmatter(content), {
|
||||||
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? "")
|
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? "")
|
||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
@@ -135,7 +151,7 @@ export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
toHTML: (content: string) =>
|
toHTML: (content: string) =>
|
||||||
content ? md.render(stripFrontmatter(content)) : "",
|
content ? renderMarkdown(stripFrontmatter(content)) : "",
|
||||||
render: (content: string, prefix?: string) =>
|
render: (content: string, prefix?: string) =>
|
||||||
renderFromUTF8(decodeBase64ToUTF8(content), prefix),
|
renderFromUTF8(decodeBase64ToUTF8(content), prefix),
|
||||||
renderFromUTF8,
|
renderFromUTF8,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const useNoteView = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const unsubscribeLink = noteEventBus.addEventBusListener(
|
const unsubscribeLink = noteEventBus.addEventBusListener(
|
||||||
({ path, currentNoteSHA }) => {
|
({ path, hash, currentNoteSHA }) => {
|
||||||
const currentFile = store.files.find(
|
const currentFile = store.files.find(
|
||||||
(file) => file.sha === currentNoteSHA
|
(file) => file.sha === currentNoteSHA
|
||||||
)
|
)
|
||||||
@@ -38,7 +38,7 @@ export const useNoteView = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addStackedNote(currentNoteSHA ?? "", file.sha)
|
addStackedNote(currentNoteSHA ?? "", file.sha, undefined, hash)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,32 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
|
|
||||||
const { scrollToNote, isMobile } = useOverlay(false)
|
const { scrollToNote, isMobile } = useOverlay(false)
|
||||||
|
|
||||||
|
const scrollToHashInNote = (
|
||||||
|
cleanSha: string,
|
||||||
|
hash: string,
|
||||||
|
attempts = 30
|
||||||
|
) => {
|
||||||
|
if (attempts <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = document.querySelector(
|
||||||
|
`.note-${cleanSha} #${CSS.escape(hash)}`
|
||||||
|
)
|
||||||
|
if (heading) {
|
||||||
|
heading.scrollIntoView({ block: "start", inline: "nearest" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToHashInNote(cleanSha, hash, attempts - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const scrollToFocusedNote = (
|
const scrollToFocusedNote = (
|
||||||
noteId: string | null = null,
|
noteId: string | null = null,
|
||||||
notes: string[] = stackedNotes.value
|
notes: string[] = stackedNotes.value,
|
||||||
|
hash?: string
|
||||||
) => {
|
) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
||||||
@@ -47,16 +70,21 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
scrollToNote(0)
|
scrollToNote(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hash && noteId) {
|
||||||
|
scrollToHashInNote(noteId.replaceAll(":", "-"), hash)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStackedNote = (
|
const addStackedNote = (
|
||||||
currentSha: string,
|
currentSha: string,
|
||||||
sha: string,
|
sha: string,
|
||||||
selector?: string
|
selector?: string,
|
||||||
|
hash?: string
|
||||||
) => {
|
) => {
|
||||||
if (stackedNotes.value.includes(sha)) {
|
if (stackedNotes.value.includes(sha)) {
|
||||||
scrollToFocusedNote(selector ?? sha)
|
scrollToFocusedNote(selector ?? sha, stackedNotes.value, hash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +104,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
stackedNotes.value = newStackedNotes
|
stackedNotes.value = newStackedNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToFocusedNote(selector ?? sha, stackedNotes.value)
|
scrollToFocusedNote(selector ?? sha, stackedNotes.value, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user