diff --git a/plugs/index/asset/style.css b/plugs/index/asset/style.css index ced0557..f25326f 100644 --- a/plugs/index/asset/style.css +++ b/plugs/index/asset/style.css @@ -57,4 +57,20 @@ body { li code { font-size: 80%; color: #a5a4a4; +} + +li.toc-header-1 { + margin-left: 0; +} + +li.toc-header-2 { + margin-left: 2ch; +} + +li.toc-header-3 { + margin-left: 4ch; +} + +li.toc-header-4 { + margin-left: 6ch; } \ No newline at end of file diff --git a/plugs/index/asset/toc.js b/plugs/index/asset/toc.js new file mode 100644 index 0000000..48e99f3 --- /dev/null +++ b/plugs/index/asset/toc.js @@ -0,0 +1,26 @@ +function processClick(e) { + const dataEl = e.target.closest("[data-ref]"); + syscall( + "system.invokeFunction", + "index.navigateToMention", + dataEl.getAttribute("data-ref"), + ).catch(console.error); +} + +document.getElementById("link-ul").addEventListener("click", processClick); +document.getElementById("hide-button").addEventListener("click", () => { + syscall("system.invokeFunction", "index.toggleTOC").catch(console.error); +}); + +document.body.addEventListener("mouseenter", () => { + console.log("Refreshing on focus"); + syscall("system.invokeFunction", "index.renderTOC").catch( + console.error, + ); +}); + +document.getElementById("reload-button").addEventListener("click", () => { + syscall("system.invokeFunction", "index.renderTOC").catch( + console.error, + ); +}); diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index e5b7a4f..2013831 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -175,6 +175,20 @@ functions: renderMentions: path: "./mentions_ps.ts:renderMentions" + # TOC + toggleTOC: + path: toc_preface.ts:toggleTOC + command: + name: "Table of Contents: Toggle" + key: ctrl-alt-t + + renderTOC: + path: toc_preface.ts:renderTOC + env: client + events: + - plug:load + - editor:pageLoaded + lintYAML: path: lint.ts:lintYAML events: diff --git a/plugs/index/mentions_ps.ts b/plugs/index/mentions_ps.ts index 5fdd2be..29993c5 100644 --- a/plugs/index/mentions_ps.ts +++ b/plugs/index/mentions_ps.ts @@ -26,8 +26,13 @@ export async function updateMentions() { // use internal navigation via syscall to prevent reloading the full page. export async function navigate(ref: string) { + const currentPage = await editor.getCurrentPage(); const [page, pos] = ref.split(/[@$]/); - await editor.navigate(page, +pos); + if (page === currentPage) { + await editor.moveCursor(+pos, true); + } else { + await editor.navigate(page, +pos); + } } function escapeHtml(unsafe: string) { diff --git a/plugs/index/toc_preface.ts b/plugs/index/toc_preface.ts new file mode 100644 index 0000000..2b2ef3f --- /dev/null +++ b/plugs/index/toc_preface.ts @@ -0,0 +1,85 @@ +import { clientStore, editor, markdown } from "$sb/silverbullet-syscall/mod.ts"; +import { renderToText, traverseTree } from "$sb/lib/tree.ts"; +import { asset } from "$sb/syscalls.ts"; + +const hideTOCKey = "hideTOC"; +const headerThreshold = 3; + +type Header = { + name: string; + pos: number; + level: number; +}; + +let cachedTOC: string | undefined; + +export async function toggleTOC() { + cachedTOC = undefined; + let hideTOC = await clientStore.get(hideTOCKey); + hideTOC = !hideTOC; + await clientStore.set(hideTOCKey, hideTOC); + await renderTOC(); // This will hide it if needed +} + +export async function renderTOC(reload = false) { + if (await clientStore.get(hideTOCKey)) { + return editor.hidePanel("preface"); + } + const page = await editor.getCurrentPage(); + const text = await editor.getText(); + const tree = await markdown.parseMarkdown(text); + const headers: Header[] = []; + traverseTree(tree, (n) => { + if (n.type?.startsWith("ATXHeading")) { + headers.push({ + name: n.children!.slice(1).map(renderToText).join("").trim(), + pos: n.from!, + level: +n.type[n.type.length - 1], + }); + + return true; + } + return false; + }); + // console.log("All headers", headers); + if (!reload && cachedTOC === JSON.stringify(headers)) { + console.log("TOC is the same, not updating"); + return; + } + cachedTOC = JSON.stringify(headers); + if (headers.length < headerThreshold) { + console.log("Not enough headers, not showing TOC", headers.length); + await editor.hidePanel("preface"); + return; + } + let tocMarkdown = ""; + for (const header of headers) { + tocMarkdown += `${ + " ".repeat(3 * (header.level - 1)) + }* [${header.name}](/${page}@${header.pos})\n`; + } + const css = await asset.readAsset("asset/style.css"); + const js = await asset.readAsset("asset/toc.js"); + + await editor.showPanel( + "preface", + 1, + ` +