diff --git a/package.json b/package.json index 0d39c67..a2015ec 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@better-fetch/fetch": "^1.1.21", "@better-fetch/logger": "^1.1.21", "@intlify/unplugin-vue-i18n": "^6.0.8", + "@mdit/plugin-tab": "^0.24.2", "@octokit/core": "^7.0.6", "@octokit/rest": "^22.0.1", "@openpanel/web": "^1.3.0", @@ -40,8 +41,10 @@ "events": "^3.3.0", "font-color-contrast": "^11.1.0", "fontfaceobserver": "^2.3.0", + "github-slugger": "^2.0.0", "isomorphic-fetch": "^3.0.0", "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0", "markdown-it-block-embed": "^0.0.3", "markdown-it-checkbox": "^1.1.0", "markdown-it-github-alerts": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5a60dc..312cfe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@intlify/unplugin-vue-i18n': 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)) + '@mdit/plugin-tab': + specifier: ^0.24.2 + version: 0.24.2(markdown-it@14.1.0) '@octokit/core': specifier: ^7.0.6 version: 7.0.6 @@ -74,12 +77,18 @@ importers: fontfaceobserver: specifier: ^2.3.0 version: 2.3.0 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 isomorphic-fetch: specifier: ^3.0.0 version: 3.0.0 markdown-it: specifier: ^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: specifier: ^0.0.3 version: 0.0.3 @@ -1307,6 +1316,24 @@ packages: '@jridgewell/trace-mapping@0.3.31': 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': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} @@ -3871,6 +3898,9 @@ packages: getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-base@0.3.0: resolution: {integrity: sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==} engines: {node: '>=0.10.0'} @@ -4848,6 +4878,12 @@ packages: resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} 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: resolution: {integrity: sha512-coWuC/uZY6Z1Gp3wthhJo5yjkG3/gHErNF/emaiEvD98fKzEHNP6GCYGfJfk5o0n31xiaYjbDgef+XtabKOZzA==} @@ -7880,6 +7916,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@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': dependencies: langium: 3.3.1 @@ -10560,6 +10609,8 @@ snapshots: dependencies: assert-plus: 1.0.0 + github-slugger@2.0.0: {} + glob-base@0.3.0: dependencies: glob-parent: 2.0.0 @@ -11597,6 +11648,11 @@ snapshots: dependencies: 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-checkbox@1.1.0: diff --git a/src/bus/noteEventBus.ts b/src/bus/noteEventBus.ts index 62698a4..5a882f8 100644 --- a/src/bus/noteEventBus.ts +++ b/src/bus/noteEventBus.ts @@ -4,6 +4,7 @@ interface EventBusParams { user: string repo: string path: string + hash?: string currentNoteSHA?: string } diff --git a/src/hooks/useLinks.hook.ts b/src/hooks/useLinks.hook.ts index 62cdcbf..f5a6b3d 100644 --- a/src/hooks/useLinks.hook.ts +++ b/src/hooks/useLinks.hook.ts @@ -30,8 +30,13 @@ export const useLinks = ( 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({ - path: href, + path, + hash, currentNoteSHA: toValue(sha), user: store.user, repo: store.repo diff --git a/src/hooks/useMarkdown.hook.ts b/src/hooks/useMarkdown.hook.ts index 473ddd8..d9e8109 100644 --- a/src/hooks/useMarkdown.hook.ts +++ b/src/hooks/useMarkdown.hook.ts @@ -1,7 +1,10 @@ +import { tab } from "@mdit/plugin-tab" import markdownItKatex from "@vscode/markdown-it-katex" +import GithubSlugger from "github-slugger" import MarkdownIt, { Options } from "markdown-it" import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs" import type Token from "markdown-it/lib/token.mjs" +import markdownItAnchor from "markdown-it-anchor" import blockEmbedPlugin from "markdown-it-block-embed" import markdownItCheckbox from "markdown-it-checkbox" import MarkdownItGitHubAlerts from "markdown-it-github-alerts" @@ -45,6 +48,8 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => { } } +const slugger = new GithubSlugger() + const md = new MarkdownIt({ typographer: true, quotes: ["«\xA0", "\xA0»", "‹\xA0", "\xA0›"] @@ -64,6 +69,12 @@ const md = new MarkdownIt({ }) .use(MarkdownItGitHubAlerts) .use(markdownItTablerIcons) + .use(tab, { + name: "tabs" + }) + .use(markdownItAnchor, { + slugify: (s: string) => slugger.slug(s) + }) let shikijiInitialized = false @@ -123,11 +134,16 @@ const stripFrontmatter = (content: string): string => { return match ? content.slice(match[0].length) : content } +const renderMarkdown = (content: string, env?: Record) => { + slugger.reset() + return env ? md.render(content, env) : md.render(content) +} + export const markdownBuilder = (defaultPrefix?: Ref | string) => { const getRawContent = (content: string) => decodeBase64ToUTF8(content) const renderFromUTF8 = (content: string, prefix?: string) => { return content - ? md.render(stripFrontmatter(content), { + ? renderMarkdown(stripFrontmatter(content), { docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? "") }) : "" @@ -135,7 +151,7 @@ export const markdownBuilder = (defaultPrefix?: Ref | string) => { return { toHTML: (content: string) => - content ? md.render(stripFrontmatter(content)) : "", + content ? renderMarkdown(stripFrontmatter(content)) : "", render: (content: string, prefix?: string) => renderFromUTF8(decodeBase64ToUTF8(content), prefix), renderFromUTF8, diff --git a/src/hooks/useNoteView.hook.ts b/src/hooks/useNoteView.hook.ts index 942905a..7dd8886 100644 --- a/src/hooks/useNoteView.hook.ts +++ b/src/hooks/useNoteView.hook.ts @@ -25,7 +25,7 @@ export const useNoteView = () => { ) const unsubscribeLink = noteEventBus.addEventBusListener( - ({ path, currentNoteSHA }) => { + ({ path, hash, currentNoteSHA }) => { const currentFile = store.files.find( (file) => file.sha === currentNoteSHA ) @@ -38,7 +38,7 @@ export const useNoteView = () => { return } - addStackedNote(currentNoteSHA ?? "", file.sha) + addStackedNote(currentNoteSHA ?? "", file.sha, undefined, hash) } ) diff --git a/src/hooks/useRouteQueryStackedNotes.hook.ts b/src/hooks/useRouteQueryStackedNotes.hook.ts index 4808f56..fdd79db 100644 --- a/src/hooks/useRouteQueryStackedNotes.hook.ts +++ b/src/hooks/useRouteQueryStackedNotes.hook.ts @@ -20,9 +20,32 @@ export const useRouteQueryStackedNotes = () => { 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 = ( noteId: string | null = null, - notes: string[] = stackedNotes.value + notes: string[] = stackedNotes.value, + hash?: string ) => { nextTick(() => { const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0 @@ -47,16 +70,21 @@ export const useRouteQueryStackedNotes = () => { scrollToNote(0) } } + + if (hash && noteId) { + scrollToHashInNote(noteId.replaceAll(":", "-"), hash) + } }) } const addStackedNote = ( currentSha: string, sha: string, - selector?: string + selector?: string, + hash?: string ) => { if (stackedNotes.value.includes(sha)) { - scrollToFocusedNote(selector ?? sha) + scrollToFocusedNote(selector ?? sha, stackedNotes.value, hash) return } @@ -76,7 +104,7 @@ export const useRouteQueryStackedNotes = () => { stackedNotes.value = newStackedNotes } - scrollToFocusedNote(selector ?? sha, stackedNotes.value) + scrollToFocusedNote(selector ?? sha, stackedNotes.value, hash) } return {