mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 00:24:43 -04:00
feat: import markdown-it-plugins from https://github.com/hedgedoc/markdown-it-plugins
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1d90013344
commit
f5736dad0f
37 changed files with 2025 additions and 0 deletions
95
markdown-it-plugins/src/toc/__snapshots__/index.test.ts.snap
Normal file
95
markdown-it-plugins/src/toc/__snapshots__/index.test.ts.snap
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
/*
|
||||
* SPDX-FileCopyrightText: Original: (c) 2018 Fabio Zendhi Nagao / Modifications: (c) 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
exports[`toc ignores the levels in the placeholder if not sorted 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a><ol><li><a href="#head-4">Head 4</a></li></ol></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with a level number array 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-4">Head 4</a></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with a single level number 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom allowed token types 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#text"> text</a></li><li><a href="#head-2">Head 2</a></li></ol></nav><h1><code>Head 1</code> text</h1>
|
||||
<h1>Head 2</h1>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom classes 1`] = `
|
||||
"<nav id="containerId" class="containerClass"><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#head-1">Head 1</a><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#heading-2">Heading 2</a><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#heading-3">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom format function 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">HEAD 1</a><ol><li><a href="#heading-2">HEADING 2</a><ol><li><a href="#heading-3">HEADING 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom slugify 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#slug-Head 1-0">Head 1</a><ol><li><a href="#slug-Heading 2-0">Heading 2</a><ol><li><a href="#slug-Heading 3-0">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom unique slug start index 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1-10">Head 1</a><ol><li><a href="#heading-2-10">Heading 2</a><ol><li><a href="#heading-3-10">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with default settings 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#heading-2">Heading 2</a><ol><li><a href="#heading-3">Heading 3</a></li></ol></li></ol></li><li><a href="#head-1-1">Head 1</a></li><li><a href="#head-1-2">Head 1</a></li><li><a href="#head-1-3">Head 1</a></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h1>Head 1</h1>
|
||||
<h1>Head 1</h1>
|
||||
<h1>Head 1</h1>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with levels in the placeholder 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a><ol></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with ordered list 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#heading-2">Heading 2</a><ol><li><a href="#heading-3">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with unordered list 1`] = `
|
||||
"<nav class="table-of-contents"><ul><li><a href="#head-1">Head 1</a><ul><li><a href="#heading-2">Heading 2</a><ul><li><a href="#heading-3">Heading 3</a></li></ul></li></ul></li></ul></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
186
markdown-it-plugins/src/toc/index.test.ts
Normal file
186
markdown-it-plugins/src/toc/index.test.ts
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { toc } from './plugin.js'
|
||||
import { describe, expect, it, jest } from '@jest/globals'
|
||||
|
||||
describe('toc', () => {
|
||||
const simpleContent = `
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
`
|
||||
|
||||
it('renders a toc with default settings', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
# Head 1
|
||||
|
||||
# Head 1
|
||||
|
||||
# Head 1
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom slugify', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { slugify: (slug, index) => `slug-${slug}-${index}` })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom unique slug start index', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { uniqueSlugStartIndex: 10 })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom classes', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, {
|
||||
containerClass: 'containerClass',
|
||||
listClass: 'listClass',
|
||||
itemClass: 'itemClass',
|
||||
linkClass: 'linkClass',
|
||||
containerId: 'containerId'
|
||||
})
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with a single level number', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { level: 2 })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with a level number array', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { level: [2, 4] })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with levels in the placeholder', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc:2:3]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('ignores the levels in the placeholder if not sorted', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc:3:2]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with ordered list', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { listType: 'ol' })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with unordered list', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { listType: 'ul' })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom format function', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { format: (name) => name.toUpperCase() })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc and executes the callback', () => {
|
||||
const callback = jest.fn()
|
||||
const markdownIt = new MarkdownIt().use(toc, { callback })
|
||||
markdownIt.render(simpleContent)
|
||||
expect(callback).toBeCalledWith({
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
level: 3,
|
||||
name: 'Heading 3'
|
||||
}
|
||||
],
|
||||
level: 2,
|
||||
name: 'Heading 2'
|
||||
}
|
||||
],
|
||||
level: 1,
|
||||
name: 'Head 1'
|
||||
}
|
||||
],
|
||||
level: 0,
|
||||
name: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('renders a toc with custom allowed token types', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { allowedTokenTypes: ['text'] })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# \`Head 1\` text
|
||||
|
||||
# Head 2
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
10
markdown-it-plugins/src/toc/index.ts
Normal file
10
markdown-it-plugins/src/toc/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { toc } from './plugin.js'
|
||||
export { defaultOptions } from './toc-options.js'
|
||||
export type { TocAst } from './toc-ast.js'
|
||||
export type { TocOptions } from './toc-options.js'
|
157
markdown-it-plugins/src/toc/plugin.ts
Normal file
157
markdown-it-plugins/src/toc/plugin.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: Original: (c) 2018 Fabio Zendhi Nagao / Modifications: (c) 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import { encode as htmlencode } from 'html-entities'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import StateBlock from 'markdown-it/lib/rules_block/state_block.js'
|
||||
import Token from 'markdown-it/lib/token.js'
|
||||
import { TocAst } from './toc-ast.js'
|
||||
import { renderAstToHtml } from './toc-body-renderer.js'
|
||||
import { defaultOptions, TocOptions } from './toc-options.js'
|
||||
|
||||
class Plugin {
|
||||
private readonly tocOptions: TocOptions
|
||||
private currentAst?: TocAst
|
||||
public readonly START_LEVEL_ATTRIBUTE_NAME = 'startLevel'
|
||||
public readonly END_LEVEL_ATTRIBUTE_NAME = 'endLevel'
|
||||
|
||||
private readonly TOC_PLACEHOLDER = /^\[\[toc(?::(\d+):(\d+))?]]|\[toc(?::(\d+):(\d+))?]$/i
|
||||
|
||||
public constructor(md: MarkdownIt, tocOptions?: Partial<TocOptions>) {
|
||||
this.tocOptions = {
|
||||
...defaultOptions,
|
||||
...tocOptions
|
||||
}
|
||||
md.renderer.rules.tocOpen = this.renderTocOpen.bind(this)
|
||||
md.renderer.rules.tocClose = this.renderTocClose.bind(this)
|
||||
md.renderer.rules.tocBody = this.renderTocBody.bind(this)
|
||||
md.core.ruler.push('generateTocAst', (state) => this.generateTocAst(state.tokens))
|
||||
md.block.ruler.before('heading', 'toc', this.generateTocToken.bind(this), {
|
||||
alt: ['paragraph', 'reference', 'blockquote']
|
||||
})
|
||||
}
|
||||
|
||||
private generateTocToken(state: StateBlock, startLine: number, _endLine: number, silent: boolean): boolean {
|
||||
const pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
const max = state.eMarks[startLine]
|
||||
|
||||
// use whitespace as a line tokenizer and extract the first token
|
||||
// to test against the placeholder anchored pattern, rejecting if false
|
||||
const lineFirstToken = state.src.slice(pos, max).split(' ')[0]
|
||||
|
||||
const matches = this.TOC_PLACEHOLDER.exec(lineFirstToken)
|
||||
if (matches === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
state.line = startLine + 1
|
||||
const tocOpenToken = state.push('tocOpen', 'nav', 1)
|
||||
tocOpenToken.markup = ''
|
||||
tocOpenToken.map = [startLine, state.line]
|
||||
|
||||
const tocBodyToken = state.push('tocBody', '', 0)
|
||||
tocBodyToken.markup = ''
|
||||
tocBodyToken.map = [startLine, state.line]
|
||||
tocBodyToken.children = []
|
||||
|
||||
const startLevel = matches[3]
|
||||
const endLevel = matches[4]
|
||||
if (startLevel !== undefined && endLevel !== undefined) {
|
||||
tocBodyToken.attrSet(this.START_LEVEL_ATTRIBUTE_NAME, startLevel)
|
||||
tocBodyToken.attrSet(this.END_LEVEL_ATTRIBUTE_NAME, endLevel)
|
||||
}
|
||||
|
||||
const tocCloseToken = state.push('tocClose', 'nav', -1)
|
||||
tocCloseToken.markup = ''
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private generateTocAst(tokens: Token[]) {
|
||||
this.currentAst = this.headings2ast(tokens)
|
||||
this.tocOptions.callback?.(this.currentAst)
|
||||
}
|
||||
|
||||
private renderTocOpen(): string {
|
||||
const id = this.tocOptions.containerId ? ` id="${htmlencode(this.tocOptions.containerId)}"` : ''
|
||||
return `<nav${id} class="${htmlencode(this.tocOptions.containerClass)}">`
|
||||
}
|
||||
|
||||
private renderTocClose(): string {
|
||||
return '</nav>'
|
||||
}
|
||||
|
||||
private createNumberRangeArray(from: number, to: number): number[] {
|
||||
return Array.from(Array(to - from + 1).keys()).map((value) => value + from)
|
||||
}
|
||||
|
||||
private renderTocBody(tokens: Token[], index: number): string {
|
||||
const bodyToken = tokens[index]
|
||||
const startLevel = Optional.ofNullable(bodyToken?.attrGet(this.START_LEVEL_ATTRIBUTE_NAME))
|
||||
.map(parseInt)
|
||||
.filter(isFinite)
|
||||
.orElse(null)
|
||||
const endLevel = Optional.ofNullable(bodyToken?.attrGet(this.END_LEVEL_ATTRIBUTE_NAME))
|
||||
.map(parseInt)
|
||||
.filter(isFinite)
|
||||
.orElse(null)
|
||||
|
||||
const modifiedTocOptions =
|
||||
startLevel !== null && endLevel !== null && startLevel <= endLevel
|
||||
? { ...this.tocOptions, level: this.createNumberRangeArray(startLevel, endLevel) }
|
||||
: this.tocOptions
|
||||
|
||||
return this.currentAst ? renderAstToHtml(this.currentAst, modifiedTocOptions) : ''
|
||||
}
|
||||
|
||||
private headings2ast(tokens: Token[]): TocAst {
|
||||
const ast: TocAst = { level: 0, name: '', children: [] }
|
||||
const stack = [ast]
|
||||
|
||||
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) {
|
||||
const token = tokens[tokenIndex]
|
||||
if (token.type !== 'heading_open') {
|
||||
continue
|
||||
}
|
||||
const nextToken = tokens[tokenIndex + 1]
|
||||
const key = (nextToken?.children ?? [])
|
||||
.filter((token) => this.tocOptions.allowedTokenTypes.includes(token.type))
|
||||
.reduce((s, t) => s + t.content, '')
|
||||
|
||||
const node: TocAst = {
|
||||
level: parseInt(token.tag.slice(1), 10),
|
||||
name: key,
|
||||
children: []
|
||||
}
|
||||
if (node.level > stack[0].level) {
|
||||
stack[0].children.push(node)
|
||||
stack.unshift(node)
|
||||
} else if (node.level === stack[0].level) {
|
||||
stack[1].children.push(node)
|
||||
stack[0] = node
|
||||
} else {
|
||||
while (node.level <= stack[0].level) stack.shift()
|
||||
stack[0].children.push(node)
|
||||
stack.unshift(node)
|
||||
}
|
||||
}
|
||||
|
||||
return ast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TOC plugin.
|
||||
*
|
||||
* @param md The markdown-it instance that should be configured
|
||||
* @param options The additional options that configure the plugin
|
||||
*/
|
||||
export const toc: MarkdownIt.PluginWithOptions<Partial<TocOptions>> = (md, options) => new Plugin(md, options)
|
11
markdown-it-plugins/src/toc/toc-ast.ts
Normal file
11
markdown-it-plugins/src/toc/toc-ast.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface TocAst {
|
||||
level: number
|
||||
name: string
|
||||
children: TocAst[]
|
||||
}
|
63
markdown-it-plugins/src/toc/toc-body-renderer.ts
Normal file
63
markdown-it-plugins/src/toc/toc-body-renderer.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { encode as htmlencode } from 'html-entities'
|
||||
import { TocAst } from './toc-ast.js'
|
||||
import { TocOptions } from './toc-options.js'
|
||||
|
||||
/**
|
||||
* Renders an HTML listing of the given tree.
|
||||
*
|
||||
* @param tree The tree that should be represented as HTML tree
|
||||
* @param tocOptions additional options that configure the rendering
|
||||
*/
|
||||
export function renderAstToHtml(tree: TocAst, tocOptions: TocOptions): string {
|
||||
if (tree.children.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let buffer = ''
|
||||
const tag = htmlencode(tocOptions.listType)
|
||||
if (tree.level === 0 || isLevelSelected(tree.level, tocOptions.level)) {
|
||||
const listClass = tocOptions.listClass !== '' ? ` class="${htmlencode(tocOptions.listClass)}"` : ''
|
||||
buffer += `<${tag + listClass}>`
|
||||
}
|
||||
const usedSlugs: string[] = []
|
||||
const parts = tree.children.map((node) => {
|
||||
const subNodesHtml = renderAstToHtml(node, tocOptions)
|
||||
if (isLevelSelected(node.level, tocOptions.level)) {
|
||||
const anchorContent = htmlencode(tocOptions.format?.(node.name) ?? node.name)
|
||||
const anchorId = generateUniqueSlug(node.name, tocOptions, usedSlugs)
|
||||
usedSlugs.push(anchorId)
|
||||
const itemClass = tocOptions.itemClass !== '' ? ` class="${htmlencode(tocOptions.itemClass)}"` : ''
|
||||
const linkClass = tocOptions.linkClass !== '' ? ` class="${htmlencode(tocOptions.linkClass)}"` : ''
|
||||
return `<li${itemClass}><a${linkClass} href="#${anchorId}">${anchorContent}</a>${subNodesHtml}</li>`
|
||||
} else {
|
||||
return subNodesHtml
|
||||
}
|
||||
})
|
||||
buffer += parts.join('')
|
||||
if (tree.level === 0 || isLevelSelected(tree.level, tocOptions.level)) {
|
||||
buffer += `</${tag}>`
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
function isLevelSelected(level: number, levels: number | number[]): boolean {
|
||||
return Array.isArray(levels) ? levels.includes(level) : level >= levels
|
||||
}
|
||||
|
||||
function generateUniqueSlug(slug: string, tocOptions: TocOptions, usedSlugs: string[]): string {
|
||||
for (let index = tocOptions.uniqueSlugStartIndex; index < Number.MAX_VALUE; index += 1) {
|
||||
const slugCandidate: string = tocOptions.slugify(slug, index)
|
||||
const slugWithIndex = index === 0 ? slugCandidate : `${slugCandidate}-${index}`
|
||||
|
||||
if (!usedSlugs.includes(slugWithIndex)) {
|
||||
return slugWithIndex
|
||||
}
|
||||
}
|
||||
throw new Error('Too many slug with same name')
|
||||
}
|
42
markdown-it-plugins/src/toc/toc-options.ts
Normal file
42
markdown-it-plugins/src/toc/toc-options.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { TocAst } from './toc-ast.js'
|
||||
|
||||
export type TocOptions = {
|
||||
slugify: (name: string, index: number) => string
|
||||
uniqueSlugStartIndex: number
|
||||
containerClass: string
|
||||
containerId: string
|
||||
listClass: string
|
||||
itemClass: string
|
||||
linkClass: string
|
||||
level: number | number[]
|
||||
listType: 'ol' | 'ul'
|
||||
format?: (name: string) => string
|
||||
callback?: (ast: TocAst) => void
|
||||
allowedTokenTypes: string[]
|
||||
}
|
||||
|
||||
function defaultSlugify(name: string) {
|
||||
return encodeURIComponent(String(name).trim().toLowerCase().replace(/\s+/g, '-'))
|
||||
}
|
||||
|
||||
/**
|
||||
* The default options for the toc plugin.
|
||||
*/
|
||||
export const defaultOptions: TocOptions = {
|
||||
uniqueSlugStartIndex: 0,
|
||||
containerClass: 'table-of-contents',
|
||||
containerId: '',
|
||||
listClass: '',
|
||||
itemClass: '',
|
||||
linkClass: '',
|
||||
level: 1,
|
||||
listType: 'ol',
|
||||
allowedTokenTypes: ['text', 'code_inline'],
|
||||
slugify: defaultSlugify
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue