import {escapeText} from "fwtoolkit"
import {Plugin, PluginKey} from "prosemirror-state"
import {Decoration, DecorationSet} from "prosemirror-view"
import type {Node} from "prosemirror-model"
import type {EditorView} from "prosemirror-view"
const key = new PluginKey("tocRender")
interface TocItem {
type: Node
id: string
textContent: string
}
function getTocItems(decorations: DecorationSet) {
let tocItems: TocItem[] | undefined
decorations.find(undefined, undefined, (deco: Decoration) => {
if (deco.spec.tocItems) {
tocItems = deco.spec.tocItems as TocItem[]
return true
}
return false
})
return tocItems
}
function tocHTML(tocItems: TocItem[], title: string) {
return `
${escapeText(title)}
${tocItems
.map(item => {
const level = (item.type as unknown as {name: string}).name.substr(-1)
return `${escapeText(item.textContent)}`
})
.join("")}`
}
class ToCView {
node: Node
view: EditorView
getPos: () => number
dom: HTMLDivElement
innerView: HTMLDivElement
contentDOM: HTMLDivElement
constructor(node: Node, view: EditorView, getPos: () => number) {
this.node = node
this.view = view
this.getPos = getPos
this.dom = document.createElement("div")
this.dom.classList.add("content-container", "toc")
this.dom.id = node.attrs.id as string
this.innerView = document.createElement("div")
this.dom.appendChild(this.innerView)
this.contentDOM = document.createElement("div")
this.dom.appendChild(this.contentDOM)
this.update(node)
}
update(node: Node) {
if (node.type !== this.node.type) {
return false
}
this.node = node
const tocPlugin = this.view.state.plugins.find(
plugin => plugin.spec.key === key
)
const tocItems = tocPlugin
? getTocItems(key.getState(this.view.state) as DecorationSet)
: undefined
if (tocItems) {
this.innerView.innerHTML = tocHTML(
tocItems,
node.attrs.title as string
)
}
return true
}
stopEvent() {
return true
}
}
function findTocItems(doc: Node): TocItem[] {
const items: TocItem[] = []
doc.descendants(node => {
if (node.type.name.startsWith("heading") && node.attrs.id) {
items.push({
type: node,
id: node.attrs.id as string,
textContent: node.textContent
})
}
})
return items
}
export const tocRenderPlugin = (_options: {editor: unknown}) =>
new Plugin({
key,
state: {
init(_config, state) {
const tocItems = findTocItems(state.doc)
return DecorationSet.create(state.doc, [
Decoration.widget(1, () => document.createElement("span"), {
tocItems
})
])
},
apply(tr, value, _oldState, newState) {
value = value.map(tr.mapping, tr.doc)
if (!tr.docChanged) {
return value
}
const tocItems = findTocItems(newState.doc)
const deco = Decoration.widget(
1,
() => document.createElement("span"),
{
tocItems
}
)
return DecorationSet.create(newState.doc, [deco])
}
},
props: {
nodeViews: {
toc: (node: Node, view: EditorView, getPos: () => number | undefined) =>
new ToCView(node, view, getPos as () => number)
}
}
})