Fixes #263: Implement TOC
This commit is contained in:
parent
cd27739336
commit
1ac494765f
@ -58,3 +58,19 @@ li code {
|
|||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: #a5a4a4;
|
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;
|
||||||
|
}
|
26
plugs/index/asset/toc.js
Normal file
26
plugs/index/asset/toc.js
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
});
|
@ -175,6 +175,20 @@ functions:
|
|||||||
renderMentions:
|
renderMentions:
|
||||||
path: "./mentions_ps.ts: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:
|
lintYAML:
|
||||||
path: lint.ts:lintYAML
|
path: lint.ts:lintYAML
|
||||||
events:
|
events:
|
||||||
|
@ -26,8 +26,13 @@ export async function updateMentions() {
|
|||||||
|
|
||||||
// use internal navigation via syscall to prevent reloading the full page.
|
// use internal navigation via syscall to prevent reloading the full page.
|
||||||
export async function navigate(ref: string) {
|
export async function navigate(ref: string) {
|
||||||
|
const currentPage = await editor.getCurrentPage();
|
||||||
const [page, pos] = ref.split(/[@$]/);
|
const [page, pos] = ref.split(/[@$]/);
|
||||||
|
if (page === currentPage) {
|
||||||
|
await editor.moveCursor(+pos, true);
|
||||||
|
} else {
|
||||||
await editor.navigate(page, +pos);
|
await editor.navigate(page, +pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(unsafe: string) {
|
function escapeHtml(unsafe: string) {
|
||||||
|
85
plugs/index/toc_preface.ts
Normal file
85
plugs/index/toc_preface.ts
Normal file
@ -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,
|
||||||
|
` <style>${css}</style>
|
||||||
|
<div id="sb-main"><div id="sb-editor"><div class="cm-editor">
|
||||||
|
<div id="button-bar">
|
||||||
|
<button id="reload-button" title="Reload"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></button>
|
||||||
|
<button id="hide-button" title="Hide TOC"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="cm-line sb-line-h2">Table of Contents</div>
|
||||||
|
<ul id="link-ul">
|
||||||
|
${
|
||||||
|
headers.map((header) =>
|
||||||
|
`<li data-ref="${page}@${header.pos}" class="toc-header-${header.level}"><span class="sb-wiki-link-page">${header.name}</span></li>`
|
||||||
|
).join("")
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div></div></div>
|
||||||
|
`,
|
||||||
|
js,
|
||||||
|
);
|
||||||
|
}
|
@ -5,19 +5,34 @@ import { PanelConfig } from "../types.ts";
|
|||||||
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
||||||
|
|
||||||
class IFrameWidget extends WidgetType {
|
class IFrameWidget extends WidgetType {
|
||||||
|
widgetHeightCacheKey: string;
|
||||||
constructor(
|
constructor(
|
||||||
readonly editor: Client,
|
readonly editor: Client,
|
||||||
readonly panel: PanelConfig,
|
readonly panel: PanelConfig,
|
||||||
|
readonly className: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.widgetHeightCacheKey = `${this.editor.currentPage!}#${this.className}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM(): HTMLElement {
|
toDOM(): HTMLElement {
|
||||||
const iframe = createWidgetSandboxIFrame(this.editor, null, this.panel);
|
const iframe = createWidgetSandboxIFrame(
|
||||||
iframe.classList.add("sb-ps-iframe");
|
this.editor,
|
||||||
|
this.widgetHeightCacheKey,
|
||||||
|
this.panel,
|
||||||
|
);
|
||||||
|
iframe.classList.add(this.className);
|
||||||
return iframe;
|
return iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get estimatedHeight(): number {
|
||||||
|
const height = this.editor.space.getCachedWidgetHeight(
|
||||||
|
this.widgetHeightCacheKey,
|
||||||
|
);
|
||||||
|
console.log("GOt height", height);
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
eq(other: WidgetType): boolean {
|
eq(other: WidgetType): boolean {
|
||||||
return this.panel.html ===
|
return this.panel.html ===
|
||||||
(other as IFrameWidget).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) => {
|
return decoratorStateField((state: EditorState) => {
|
||||||
const widgets: any[] = [];
|
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) {
|
if (editor.ui.viewState.panels.ps.html) {
|
||||||
widgets.push(
|
widgets.push(
|
||||||
Decoration.widget({
|
Decoration.widget({
|
||||||
widget: new IFrameWidget(
|
widget: new IFrameWidget(
|
||||||
editor,
|
editor,
|
||||||
editor.ui.viewState.panels.ps,
|
editor.ui.viewState.panels.ps,
|
||||||
|
"sb-ps-iframe",
|
||||||
),
|
),
|
||||||
side: 1,
|
side: 1,
|
||||||
block: true,
|
block: true,
|
@ -40,7 +40,7 @@ import {
|
|||||||
pasteLinkExtension,
|
pasteLinkExtension,
|
||||||
} from "./cm_plugins/editor_paste.ts";
|
} from "./cm_plugins/editor_paste.ts";
|
||||||
import { TextChange } from "$sb/lib/change.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 { languageFor } from "../common/languages.ts";
|
||||||
import { plugLinter } from "./cm_plugins/lint.ts";
|
import { plugLinter } from "./cm_plugins/lint.ts";
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ export function createEditorState(
|
|||||||
plugLinter(client),
|
plugLinter(client),
|
||||||
// lintGutter(),
|
// lintGutter(),
|
||||||
// gutters(),
|
// gutters(),
|
||||||
postScriptPlugin(client),
|
postScriptPrefacePlugin(client),
|
||||||
lineWrapper([
|
lineWrapper([
|
||||||
{ selector: "ATXHeading1", class: "sb-line-h1" },
|
{ selector: "ATXHeading1", class: "sb-line-h1" },
|
||||||
{ selector: "ATXHeading2", class: "sb-line-h2" },
|
{ selector: "ATXHeading2", class: "sb-line-h2" },
|
||||||
|
@ -154,6 +154,13 @@ body {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sb-preface-iframe {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid var(--editor-directive-background-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#sb-main {
|
#sb-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -186,6 +186,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
editor.editorView.focus();
|
||||||
},
|
},
|
||||||
"editor.setSelection": (_ctx, from: number, to: number) => {
|
"editor.setSelection": (_ctx, from: number, to: number) => {
|
||||||
editor.editorView.dispatch({
|
editor.editorView.dispatch({
|
||||||
|
@ -91,6 +91,7 @@ export const initialViewState: AppViewState = {
|
|||||||
bhs: {},
|
bhs: {},
|
||||||
modal: {},
|
modal: {},
|
||||||
ps: {},
|
ps: {},
|
||||||
|
preface: {},
|
||||||
},
|
},
|
||||||
allPages: [],
|
allPages: [],
|
||||||
commands: new Map(),
|
commands: new Map(),
|
||||||
|
Loading…
Reference in New Issue
Block a user