snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
89
packages/extension-table-of-contents/src/generate-toc-ids.ts
Normal file
89
packages/extension-table-of-contents/src/generate-toc-ids.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { Extensions, JSONContent } from '@tiptap/core'
|
||||
import { getSchema } from '@tiptap/core'
|
||||
import { Node as PMNode } from '@tiptap/pm/model'
|
||||
import { EditorState } from '@tiptap/pm/state'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { TableOfContents } from './tableOfContents.js'
|
||||
|
||||
/**
|
||||
* Creates a new document with `id` and `data-toc-id` attributes added to
|
||||
* anchor-type nodes (headings by default). Does the same job as the
|
||||
* `TableOfContents` ProseMirror plugin, but without needing an `Editor`
|
||||
* instance — so it can run on the server before `renderToHTMLString`.
|
||||
*
|
||||
* The configuration from the `TableOfContents` extension (in particular
|
||||
* `anchorTypes` and `getId`) is picked up automatically. Nodes that already
|
||||
* have a unique `data-toc-id` are left untouched; duplicates and missing IDs
|
||||
* are re-generated to match the plugin's behavior.
|
||||
*
|
||||
* @throws {Error} If the `TableOfContents` extension is not in the array.
|
||||
*
|
||||
* @example
|
||||
* const doc = {
|
||||
* type: 'doc',
|
||||
* content: [
|
||||
* { type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Intro' }] },
|
||||
* ],
|
||||
* }
|
||||
* const docWithIds = generateTocIds(doc, [StarterKit, TableOfContents])
|
||||
* // → heading now has `id` and `data-toc-id` attributes
|
||||
* const html = renderToHTMLString({ content: docWithIds, extensions: [...] })
|
||||
*
|
||||
* @param doc - A Tiptap JSON document to add anchor IDs to.
|
||||
* @param extensions - The extensions to use. Must include the `TableOfContents` extension (or one extending it).
|
||||
* @param extensionName - Optional name of the `TableOfContents` extension to look up. Defaults to `'tableOfContents'`. Override this if you have extended the base extension and changed its `name`.
|
||||
* @returns The updated Tiptap JSON document.
|
||||
*/
|
||||
export function generateTocIds(
|
||||
doc: JSONContent,
|
||||
extensions: Extensions,
|
||||
extensionName = 'tableOfContents',
|
||||
): JSONContent {
|
||||
const tocExtension = extensions.find(ext => ext.name === extensionName) as
|
||||
| typeof TableOfContents
|
||||
| undefined
|
||||
|
||||
if (!tocExtension) {
|
||||
throw new Error(
|
||||
`TableOfContents extension (name: "${extensionName}") not found in the extensions array`,
|
||||
)
|
||||
}
|
||||
|
||||
const schema = getSchema([...extensions.filter(ext => ext.name !== extensionName), tocExtension])
|
||||
const anchorTypes = tocExtension.options.anchorTypes ?? ['heading']
|
||||
const getId = tocExtension.options.getId ?? (() => uuidv4())
|
||||
|
||||
const contentNode = PMNode.fromJSON(schema, doc)
|
||||
|
||||
// Mirrors the TableOfContents plugin's appendTransaction in `plugin.ts`:
|
||||
// an anchor node gets a fresh id when its `data-toc-id` is missing or
|
||||
// already seen earlier in the document. We track original ids — including
|
||||
// null — to match the plugin's behavior exactly.
|
||||
type Update = { pos: number; attrs: Record<string, unknown> }
|
||||
const updates: Update[] = []
|
||||
const existingIds: Array<string | null | undefined> = []
|
||||
|
||||
contentNode.descendants((node, pos) => {
|
||||
if (!anchorTypes.includes(node.type.name) || node.textContent.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentId = node.attrs['data-toc-id'] as string | null | undefined
|
||||
|
||||
if (currentId == null || existingIds.includes(currentId)) {
|
||||
const id = getId(node.textContent)
|
||||
updates.push({ pos, attrs: { ...node.attrs, id, 'data-toc-id': id } })
|
||||
}
|
||||
|
||||
existingIds.push(currentId)
|
||||
})
|
||||
|
||||
const tr = EditorState.create({ doc: contentNode }).tr
|
||||
|
||||
updates.forEach(({ pos, attrs }) => {
|
||||
tr.setNodeMarkup(pos, undefined, attrs)
|
||||
})
|
||||
|
||||
return tr.doc.toJSON()
|
||||
}
|
||||
Reference in New Issue
Block a user