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

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# @tiptap/extension-table-of-contents
[![Version](https://img.shields.io/npm/v/@tiptap/extension-table-of-contents.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-table-of-contents)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-table-of-contents.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-table-of-contents.svg)](https://www.npmjs.com/package/@tiptap/extension-table-of-contents)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
## Official Documentation
Documentation can be found on the [Tiptap website](https://tiptap.dev).
## License
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).

View File

@@ -0,0 +1,55 @@
{
"name": "@tiptap/extension-table-of-contents",
"version": "3.30.3",
"description": "table of contents extension for Tiptap",
"keywords": [
"tiptap",
"tiptap extension"
],
"homepage": "https://tiptap.dev/docs/editor/extensions/functionality/table-of-contents",
"bugs": {
"url": "https://github.com/ueberdosis/tiptap/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/extension-table-of-contents"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"files": [
"src",
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.ts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "vp pack"
},
"dependencies": {
"uuid": "^14.0.0"
},
"devDependencies": {
"@tiptap/core": "workspace:^",
"@tiptap/pm": "workspace:^"
},
"peerDependencies": {
"@tiptap/core": "workspace:*",
"@tiptap/pm": "workspace:*"
}
}

View File

@@ -0,0 +1,146 @@
import type { JSONContent } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Heading from '@tiptap/extension-heading'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { describe, expect, it } from 'vite-plus/test'
import { generateTocIds } from './generate-toc-ids.js'
import { TableOfContents } from './tableOfContents.js'
const baseExtensions = [Document, Paragraph, Text, Heading]
const docWithHeadings = (...texts: string[]): JSONContent => ({
type: 'doc',
content: texts.map(text => ({
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text }],
})),
})
describe('generateTocIds', () => {
it('throws when TableOfContents extension is missing', () => {
expect(() => generateTocIds(docWithHeadings('a'), baseExtensions)).toThrow(
'TableOfContents extension (name: "tableOfContents") not found in the extensions array',
)
})
it('accepts a custom extension name for users who extend TableOfContents', () => {
const CustomToc = TableOfContents.extend({ name: 'myCustomToc' })
const result = generateTocIds(
docWithHeadings('Intro'),
[...baseExtensions, CustomToc],
'myCustomToc',
)
const heading = result.content?.[0]
expect(heading?.attrs?.id).toEqual(expect.any(String))
expect(heading?.attrs?.['data-toc-id']).toEqual(expect.any(String))
})
it('reports the requested name in the not-found error when a custom name is missing', () => {
expect(() => generateTocIds(docWithHeadings('a'), baseExtensions, 'myCustomToc')).toThrow(
'TableOfContents extension (name: "myCustomToc") not found in the extensions array',
)
})
it('assigns id and data-toc-id to anchor nodes that have none', () => {
const result = generateTocIds(docWithHeadings('Intro'), [...baseExtensions, TableOfContents])
const heading = result.content?.[0]
expect(heading?.attrs?.id).toEqual(expect.any(String))
expect(heading?.attrs?.['data-toc-id']).toEqual(expect.any(String))
expect(heading?.attrs?.id).toBe(heading?.attrs?.['data-toc-id'])
})
it('skips anchor nodes with empty text content', () => {
const doc: JSONContent = {
type: 'doc',
content: [{ type: 'heading', attrs: { level: 1 } }],
}
const result = generateTocIds(doc, [...baseExtensions, TableOfContents])
const heading = result.content?.[0]
expect(heading?.attrs?.id).toBeFalsy()
expect(heading?.attrs?.['data-toc-id']).toBeFalsy()
})
it('preserves unique existing data-toc-id values', () => {
const doc: JSONContent = {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1, id: 'existing', 'data-toc-id': 'existing' },
content: [{ type: 'text', text: 'Intro' }],
},
],
}
const result = generateTocIds(doc, [...baseExtensions, TableOfContents])
expect(result.content?.[0].attrs?.['data-toc-id']).toBe('existing')
expect(result.content?.[0].attrs?.id).toBe('existing')
})
it('regenerates ids for duplicate data-toc-id values', () => {
const doc: JSONContent = {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1, id: 'dup', 'data-toc-id': 'dup' },
content: [{ type: 'text', text: 'First' }],
},
{
type: 'heading',
attrs: { level: 1, id: 'dup', 'data-toc-id': 'dup' },
content: [{ type: 'text', text: 'Second' }],
},
],
}
const result = generateTocIds(doc, [...baseExtensions, TableOfContents])
const [first, second] = result.content ?? []
expect(first.attrs?.['data-toc-id']).toBe('dup')
expect(second.attrs?.['data-toc-id']).not.toBe('dup')
expect(second.attrs?.['data-toc-id']).toEqual(expect.any(String))
})
it('does not modify non-anchor nodes', () => {
const doc: JSONContent = {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] },
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Intro' }],
},
],
}
const result = generateTocIds(doc, [...baseExtensions, TableOfContents])
expect(result.content?.[0].attrs?.id).toBeUndefined()
expect(result.content?.[1].attrs?.id).toEqual(expect.any(String))
})
it('respects a custom getId option', () => {
const result = generateTocIds(docWithHeadings('Hello World'), [
...baseExtensions,
TableOfContents.configure({
getId: textContent => textContent.toLowerCase().replace(/\s+/g, '-'),
}),
])
expect(result.content?.[0].attrs?.id).toBe('hello-world')
expect(result.content?.[0].attrs?.['data-toc-id']).toBe('hello-world')
})
it('returns an empty content array unchanged', () => {
const doc: JSONContent = { type: 'doc', content: [] }
const result = generateTocIds(doc, [...baseExtensions, TableOfContents])
expect(result).toEqual({ type: 'doc' })
})
})

View 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()
}

View File

@@ -0,0 +1,8 @@
import { TableOfContents } from './tableOfContents.js'
export * from './generate-toc-ids.js'
export * from './plugin.js'
export * from './tableOfContents.js'
export * from './utils.js'
export default TableOfContents

View File

@@ -0,0 +1,65 @@
import type { NodeType } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { v4 as uuidv4 } from 'uuid'
export const TableOfContentsPlugin = ({
getId,
anchorTypes = ['heading'],
}: {
getId?: (textContent: string) => string
anchorTypes?: Array<string | NodeType>
}) => {
return new Plugin({
key: new PluginKey('tableOfContent'),
appendTransaction(transactions, _oldState, newState) {
// Avoid running on the server where `window` / DOM is not available.
if (typeof window === 'undefined') {
return null
}
// Skip updating table of contents during IME composition
// to avoid interfering with input method editor
if (transactions.some(tr => tr.getMeta('composition'))) {
return
}
const tr = newState.tr
let modified = false
if (transactions.some(transaction => transaction.docChanged)) {
const existingIds: string[] = []
newState.doc.descendants((node, pos) => {
const nodeId = node.attrs['data-toc-id']
if (!anchorTypes.includes(node.type.name) || node.textContent.length === 0) {
return
}
if (nodeId === null || nodeId === undefined || existingIds.includes(nodeId)) {
let id = ''
if (getId) {
id = getId(node.textContent)
} else {
id = uuidv4()
}
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
'data-toc-id': id,
id,
})
modified = true
}
existingIds.push(nodeId)
})
}
return modified ? tr : null
},
})
}

View File

@@ -0,0 +1,373 @@
import type { Editor } from '@tiptap/core'
import { Extension } from '@tiptap/core'
import type { Node as TiptapNode } from '@tiptap/pm/model'
import { v4 as uuidv4 } from 'uuid'
import { TableOfContentsPlugin } from './plugin.js'
import type {
GetTableOfContentIndexFunction,
GetTableOfContentLevelFunction,
TableOfContentData,
TableOfContentDataItem,
TableOfContentsOptions,
TableOfContentsStorage,
} from './types.js'
import { getHeadlineLevel, getLinearIndexes } from './utils.js'
export * from './types.js'
const addTocActiveStatesAndGetItems = (
content: TableOfContentDataItem[],
options: {
editor: Editor
anchorTypes: Array<string> | undefined
storage: TableOfContentsStorage
onUpdate?: (data: TableOfContentData, isCreate?: boolean) => void | undefined
},
) => {
const { editor } = options
const headlines: Array<{ node: TiptapNode; pos: number }> = []
const scrolledOverIds: string[] = []
let activeId: string | null = null
if (editor.isDestroyed) {
return content
}
editor.state.doc.descendants((node, pos) => {
const isValidType = options.anchorTypes?.includes(node.type.name)
if (!isValidType) {
return
}
headlines.push({ node, pos })
})
headlines.forEach(headline => {
const domElement = editor.view.domAtPos(headline.pos + 1).node as
| HTMLHeadingElement
| HTMLElement
const scrolledOver = options.storage.scrollPosition >= domElement.offsetTop
if (scrolledOver) {
activeId = headline.node.attrs['data-toc-id']
scrolledOverIds.push(headline.node.attrs['data-toc-id'])
}
})
content = content.map(heading => ({
...heading,
isActive: heading.id === activeId,
isScrolledOver: scrolledOverIds.includes(heading.id),
}))
if (options.onUpdate) {
const isInitialCreation = options.storage.content.length === 0
options.onUpdate(content, isInitialCreation)
}
return content
}
const setTocData = (options: {
editor: Editor
anchorTypes: Array<string> | undefined
getIndexFn: GetTableOfContentIndexFunction
getLevelFn: GetTableOfContentLevelFunction
storage: TableOfContentsStorage
onUpdate?: (data: TableOfContentData, isCreate?: boolean) => void | undefined
}) => {
const { editor } = options
if (editor.isDestroyed) {
return
}
const headlines: Array<{ node: TiptapNode; pos: number }> = []
let anchors: TableOfContentDataItem[] = []
const anchorEls: Array<HTMLHeadingElement | HTMLElement> = []
editor.state.doc.descendants((node, pos) => {
const isValidType = options.anchorTypes?.includes(node.type.name)
if (!isValidType) {
return
}
headlines.push({ node, pos })
})
headlines.forEach((headline, i) => {
if (headline.node.textContent.length === 0) {
return
}
const domElement = editor.view.domAtPos(headline.pos + 1).node as HTMLHeadingElement
const scrolledOver = options.storage.scrollPosition >= domElement.offsetTop
anchorEls.push(domElement)
const originalLevel = headline.node.attrs.level
const prevHeadline = headlines[i - 1]
const level = options.getLevelFn(headline, anchors)
const itemIndex = options.getIndexFn(headline, anchors, level)
if (!prevHeadline) {
anchors = [
...anchors,
{
itemIndex,
id: headline.node.attrs['data-toc-id'],
originalLevel,
level,
textContent: headline.node.textContent,
pos: headline.pos,
editor,
isActive: false,
isScrolledOver: scrolledOver,
node: headline.node,
dom: domElement,
},
]
return
}
anchors = [
...anchors,
{
itemIndex,
id: headline.node.attrs['data-toc-id'],
originalLevel,
level,
textContent: headline.node.textContent,
pos: headline.pos,
editor,
isActive: false,
isScrolledOver: false,
node: headline.node,
dom: domElement,
},
]
})
anchors = addTocActiveStatesAndGetItems(anchors, options)
options.storage.anchors = anchorEls
options.storage.content = anchors
editor.state.tr.setMeta('toc', anchors)
editor.view.dispatch(editor.state.tr)
}
export const TableOfContents = Extension.create<TableOfContentsOptions, TableOfContentsStorage>({
name: 'tableOfContents',
addStorage() {
return {
content: [],
anchors: [],
scrollHandler: () => null,
scrollPosition: 0,
}
},
addGlobalAttributes() {
return [
{
types: (this.options.anchorTypes as string[]) || ['headline'],
attributes: {
id: {
default: null,
renderHTML: attributes => {
return {
id: attributes.id,
}
},
parseHTML: element => {
return element.id || null
},
},
'data-toc-id': {
default: null,
renderHTML: attributes => {
return {
'data-toc-id': attributes['data-toc-id'],
}
},
parseHTML: element => {
return element.dataset.tocId || null
},
},
},
},
]
},
addOptions() {
const defaultScrollParent = typeof window !== 'undefined' ? () => window : undefined
return {
// oxlint-disable-next-line
onUpdate: () => {},
// oxlint-disable-next-line
getId: _textContent => uuidv4(),
scrollParent: defaultScrollParent,
anchorTypes: ['heading'],
}
},
addCommands() {
return {
updateTableOfContents:
() =>
({ dispatch }) => {
if (dispatch) {
setTocData({
editor: this.editor,
storage: this.storage,
onUpdate: this.options.onUpdate?.bind(this),
getIndexFn: this.options.getIndex || getLinearIndexes,
getLevelFn: this.options.getLevel || getHeadlineLevel,
anchorTypes: this.options.anchorTypes,
})
}
return true
},
}
},
onTransaction({ transaction }) {
if (transaction.docChanged && !transaction.getMeta('toc')) {
setTocData({
editor: this.editor,
storage: this.storage,
onUpdate: this.options.onUpdate?.bind(this),
getIndexFn: this.options.getIndex || getLinearIndexes,
getLevelFn: this.options.getLevel || getHeadlineLevel,
anchorTypes: this.options.anchorTypes,
})
}
},
onCreate() {
// Avoid mutating the document during server-side rendering.
if (typeof window === 'undefined' || !this.editor.view) {
return
}
const { tr } = this.editor.state
const existingIds: string[] = []
if (this.options.scrollParent && typeof this.options.scrollParent !== 'function') {
console.warn(
"[Tiptap Table of Contents Deprecation Notice]: The 'scrollParent' option must now be provided as a callback function that returns the 'scrollParent' element. The ability to pass this option directly will be deprecated in future releases.",
)
}
this.editor.state.doc.descendants((node, pos) => {
const nodeId = node.attrs['data-toc-id']
const isValidType = this.options.anchorTypes?.includes(node.type.name)
if (!isValidType || node.textContent.length === 0) {
return
}
if (nodeId === null || nodeId === undefined || existingIds.includes(nodeId)) {
let id = ''
if (this.options.getId) {
id = this.options.getId(node.textContent)
} else {
id = uuidv4()
}
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
'data-toc-id': id,
id,
})
}
existingIds.push(nodeId)
})
this.editor.view.dispatch(tr)
setTocData({
editor: this.editor,
storage: this.storage,
onUpdate: this.options.onUpdate?.bind(this),
getIndexFn: this.options.getIndex || getLinearIndexes,
getLevelFn: this.options.getLevel || getHeadlineLevel,
anchorTypes: this.options.anchorTypes,
})
this.storage.scrollHandler = () => {
if (!this.options.scrollParent) {
return
}
// if scrollParent is HTML Element, use scrollTop - otherwise use scrollY
const scrollParent =
typeof this.options.scrollParent === 'function'
? this.options.scrollParent()
: this.options.scrollParent
const scrollPosition =
scrollParent instanceof HTMLElement ? scrollParent.scrollTop : scrollParent.scrollY
this.storage.scrollPosition = scrollPosition || 0
const updatedItems = addTocActiveStatesAndGetItems(this.storage.content, {
editor: this.editor,
anchorTypes: this.options.anchorTypes,
storage: this.storage,
onUpdate: this.options.onUpdate?.bind(this),
})
this.storage.content = updatedItems
}
if (!this.options.scrollParent) {
return
}
const scrollParent =
typeof this.options.scrollParent === 'function'
? this.options.scrollParent()
: this.options.scrollParent
if (scrollParent) {
scrollParent.addEventListener('scroll', this.storage.scrollHandler)
}
},
onDestroy() {
if (!this.options.scrollParent) {
return
}
const scrollParent =
typeof this.options.scrollParent === 'function'
? this.options.scrollParent()
: this.options.scrollParent
if (scrollParent) {
scrollParent.removeEventListener('scroll', this.storage.scrollHandler)
}
},
addProseMirrorPlugins() {
return [
TableOfContentsPlugin({ getId: this.options.getId, anchorTypes: this.options.anchorTypes }),
]
},
})

View File

@@ -0,0 +1,57 @@
import type { Editor } from '@tiptap/core'
import type { Node as PMNode } from '@tiptap/pm/model'
export type GetTableOfContentLevelFunction = (
headline: { node: PMNode; pos: number },
previousItems: TableOfContentDataItem[],
) => number
export type GetTableOfContentIndexFunction = (
headline: { node: PMNode; pos: number },
previousItems: TableOfContentDataItem[],
currentLevel?: number,
) => number
export type TableOfContentsOptions = {
getId?: (textContent: string) => string
onUpdate?: (data: TableOfContentData, isCreate?: boolean) => void
getLevel?: GetTableOfContentLevelFunction
getIndex?: GetTableOfContentIndexFunction
scrollParent?: (() => HTMLElement | Window) | HTMLElement | Window
anchorTypes?: Array<string>
}
export type TableOfContentsStorage = {
content: TableOfContentData
anchors: Array<HTMLHeadingElement | HTMLElement>
scrollHandler: () => void
scrollPosition: number
}
export type TableOfContentData = Array<TableOfContentDataItem>
export type TableOfContentDataItem = {
dom: HTMLHeadingElement
editor: Editor
id: string
isActive: boolean
isScrolledOver: boolean
itemIndex: number
level: number
node: PMNode
originalLevel: number
pos: number
textContent: string
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
tableOfContents: {
updateTableOfContents: () => ReturnType
}
}
interface Storage {
tableOfContents: TableOfContentsStorage
}
}

View File

@@ -0,0 +1,85 @@
import type {
GetTableOfContentIndexFunction,
GetTableOfContentLevelFunction,
TableOfContentDataItem,
} from './types.js'
export const getLastHeadingOnLevel = (
headings: TableOfContentDataItem[],
level: number,
): TableOfContentDataItem | undefined => {
let heading = headings.filter(currentHeading => currentHeading.level === level).pop()
if (level === 0) {
return undefined
}
if (!heading) {
heading = getLastHeadingOnLevel(headings, level - 1)
}
return heading
}
export const getHeadlineLevel: GetTableOfContentLevelFunction = (headline, previousItems) => {
let level = 1
const previousHeadline = previousItems.at(-1)
const highestHeadlineAbove = [...previousItems]
.reverse()
.find(h => h.originalLevel <= headline.node.attrs.level)
const highestLevelAbove = highestHeadlineAbove?.level || 1
if (headline.node.attrs.level > (previousHeadline?.originalLevel || 1)) {
level = (previousHeadline?.level || 1) + 1
} else if (headline.node.attrs.level < (previousHeadline?.originalLevel || 1)) {
level = highestLevelAbove
} else {
level = previousHeadline?.level || 1
}
return level
}
export const getLinearIndexes: GetTableOfContentIndexFunction = (_headline, previousHeadlines) => {
const previousHeadline = previousHeadlines.at(-1)
if (!previousHeadline) {
return 1
}
return (previousHeadline?.itemIndex || 1) + 1
}
export const getHierarchicalIndexes: GetTableOfContentIndexFunction = (
headline,
previousHeadlines,
currentLevel,
) => {
const level = currentLevel || headline.node.attrs.level || 1
let itemIndex = 1
const previousHeadlinesOnLevelOrBelow = previousHeadlines.filter(h => h.level <= level)
if (previousHeadlinesOnLevelOrBelow.at(-1)?.level === level) {
itemIndex = (previousHeadlinesOnLevelOrBelow.at(-1)?.itemIndex || 1) + 1
} else {
itemIndex = 1
}
return itemIndex
}
export function debounce(func: (...args: any[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null = null
return (...args: any[]) => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
func(...args)
}, wait)
}
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite-plus'
import { basePackConfig } from '../../pack.config.mjs'
export default defineConfig({
pack: {
entry: ['src/index.ts'],
...basePackConfig(),
},
})