From 1ac494765f064548cee706fc4f29fe8e653f69c6 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sat, 25 Nov 2023 13:40:56 +0100 Subject: [PATCH] Fixes #263: Implement TOC --- plugs/index/asset/style.css | 16 ++++ plugs/index/asset/toc.js | 26 ++++++ plugs/index/index.plug.yaml | 14 +++ plugs/index/mentions_ps.ts | 7 +- plugs/index/toc_preface.ts | 85 +++++++++++++++++++ .../{post_script.ts => preface_ps.ts} | 35 +++++++- web/editor_state.ts | 4 +- web/styles/main.scss | 7 ++ web/syscalls/editor.ts | 1 + web/types.ts | 1 + 10 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 plugs/index/asset/toc.js create mode 100644 plugs/index/toc_preface.ts rename web/cm_plugins/{post_script.ts => preface_ps.ts} (54%) 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, + ` +
+
+ + +
+
Table of Contents
+ +
+ `, + js, + ); +} diff --git a/web/cm_plugins/post_script.ts b/web/cm_plugins/preface_ps.ts similarity index 54% rename from web/cm_plugins/post_script.ts rename to web/cm_plugins/preface_ps.ts index 3ac9d3d..0d4b07b 100644 --- a/web/cm_plugins/post_script.ts +++ b/web/cm_plugins/preface_ps.ts @@ -5,19 +5,34 @@ import { PanelConfig } from "../types.ts"; import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts"; class IFrameWidget extends WidgetType { + widgetHeightCacheKey: string; constructor( readonly editor: Client, readonly panel: PanelConfig, + readonly className: string, ) { super(); + this.widgetHeightCacheKey = `${this.editor.currentPage!}#${this.className}`; } toDOM(): HTMLElement { - const iframe = createWidgetSandboxIFrame(this.editor, null, this.panel); - iframe.classList.add("sb-ps-iframe"); + const iframe = createWidgetSandboxIFrame( + this.editor, + this.widgetHeightCacheKey, + this.panel, + ); + iframe.classList.add(this.className); return iframe; } + get estimatedHeight(): number { + const height = this.editor.space.getCachedWidgetHeight( + this.widgetHeightCacheKey, + ); + console.log("GOt height", height); + return height; + } + eq(other: WidgetType): boolean { return this.panel.html === (other as IFrameWidget).panel.html && @@ -26,15 +41,29 @@ class IFrameWidget extends WidgetType { } } -export function postScriptPlugin(editor: Client) { +export function postScriptPrefacePlugin(editor: Client) { return decoratorStateField((state: EditorState) => { const widgets: any[] = []; + if (editor.ui.viewState.panels.preface.html) { + widgets.push( + Decoration.widget({ + widget: new IFrameWidget( + editor, + editor.ui.viewState.panels.preface, + "sb-preface-iframe", + ), + side: -1, + block: true, + }).range(0), + ); + } if (editor.ui.viewState.panels.ps.html) { widgets.push( Decoration.widget({ widget: new IFrameWidget( editor, editor.ui.viewState.panels.ps, + "sb-ps-iframe", ), side: 1, block: true, diff --git a/web/editor_state.ts b/web/editor_state.ts index 4cb482f..fb41912 100644 --- a/web/editor_state.ts +++ b/web/editor_state.ts @@ -40,7 +40,7 @@ import { pasteLinkExtension, } from "./cm_plugins/editor_paste.ts"; import { TextChange } from "$sb/lib/change.ts"; -import { postScriptPlugin } from "./cm_plugins/post_script.ts"; +import { postScriptPrefacePlugin } from "./cm_plugins/preface_ps.ts"; import { languageFor } from "../common/languages.ts"; import { plugLinter } from "./cm_plugins/lint.ts"; @@ -144,7 +144,7 @@ export function createEditorState( plugLinter(client), // lintGutter(), // gutters(), - postScriptPlugin(client), + postScriptPrefacePlugin(client), lineWrapper([ { selector: "ATXHeading1", class: "sb-line-h1" }, { selector: "ATXHeading2", class: "sb-line-h2" }, diff --git a/web/styles/main.scss b/web/styles/main.scss index 7d61180..f190eef 100644 --- a/web/styles/main.scss +++ b/web/styles/main.scss @@ -154,6 +154,13 @@ body { border-radius: 5px; } +.sb-preface-iframe { + width: 100%; + margin-top: 10px; + border: 1px solid var(--editor-directive-background-color); + border-radius: 5px; +} + #sb-main { display: flex; diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index 6dda6b5..167be92 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -186,6 +186,7 @@ export function editorSyscalls(editor: Client): SysCallMapping { ], }); } + editor.editorView.focus(); }, "editor.setSelection": (_ctx, from: number, to: number) => { editor.editorView.dispatch({ diff --git a/web/types.ts b/web/types.ts index 5b7e469..6dc0e24 100644 --- a/web/types.ts +++ b/web/types.ts @@ -91,6 +91,7 @@ export const initialViewState: AppViewState = { bhs: {}, modal: {}, ps: {}, + preface: {}, }, allPages: [], commands: new Map(),