mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 10:45:20 -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
|
@ -0,0 +1,67 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
exports[`markdown-it-task-lists renders bullet correctly 1`] = `
|
||||
"<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-1" />unchecked item 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-2" />unchecked item 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-3" />unchecked item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-4" />checked item 4</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders dirty correctly 1`] = `
|
||||
"<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-1" />unchecked todo item 1</li>
|
||||
<li>[ ]</li>
|
||||
<li>[ ] not a todo item 2</li>
|
||||
<li>[ x] not a todo item 3</li>
|
||||
<li>[x ] not a todo item 4</li>
|
||||
<li>[ x ] not a todo item 5</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-7" />todo item 6</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders mixedNested correctly 1`] = `
|
||||
"<h1>Test 1</h1>
|
||||
<ol>
|
||||
<li>foo
|
||||
<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-4" />nested unchecked item 1</li>
|
||||
<li>not a todo item 2</li>
|
||||
<li>not a todo item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-7" />nested checked item 4</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>bar</li>
|
||||
<li>spam</li>
|
||||
</ol>
|
||||
<h1>Test 2</h1>
|
||||
<ul>
|
||||
<li>foo
|
||||
<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-14" />nested unchecked item 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-15" />nested unchecked item 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-16" />nested checked item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-17" />nested checked item 4</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders ordered correctly 1`] = `
|
||||
"<ol class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-1" />checked ordered 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-2" />unchecked ordered 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-3" />checked ordered 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-4" />unchecked ordered 4</li>
|
||||
</ol>
|
||||
"
|
||||
`;
|
74
markdown-it-plugins/src/task-lists/index.test.ts
Normal file
74
markdown-it-plugins/src/task-lists/index.test.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { taskLists } from './index.js'
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
|
||||
describe('markdown-it-task-lists', () => {
|
||||
it('renders bullet correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
- [ ] unchecked item 1
|
||||
- [ ] unchecked item 2
|
||||
- [ ] unchecked item 3
|
||||
- [x] checked item 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders dirty correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
- [ ] unchecked todo item 1
|
||||
- [ ]
|
||||
- [ ] not a todo item 2
|
||||
- [ x] not a todo item 3
|
||||
- [x ] not a todo item 4
|
||||
- [ x ] not a todo item 5
|
||||
- [x] todo item 6
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders mixedNested correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
# Test 1
|
||||
|
||||
1. foo
|
||||
* [ ] nested unchecked item 1
|
||||
* not a todo item 2
|
||||
* not a todo item 3
|
||||
* [x] nested checked item 4
|
||||
2. bar
|
||||
3. spam
|
||||
|
||||
# Test 2
|
||||
|
||||
- foo
|
||||
- [ ] nested unchecked item 1
|
||||
- [ ] nested unchecked item 2
|
||||
- [x] nested checked item 3
|
||||
- [X] nested checked item 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
it('renders ordered correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
1. [x] checked ordered 1
|
||||
2. [ ] unchecked ordered 2
|
||||
3. [x] checked ordered 3
|
||||
4. [ ] unchecked ordered 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
159
markdown-it-plugins/src/task-lists/index.ts
Normal file
159
markdown-it-plugins/src/task-lists/index.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
// Markdown-it plugin to render GitHub-style task lists; see
|
||||
//
|
||||
// https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments
|
||||
// https://github.com/blog/1825-task-lists-in-all-markdown-documents
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import StateCore from 'markdown-it/lib/rules_core/state_core.js'
|
||||
import Token from 'markdown-it/lib/token.js'
|
||||
|
||||
interface TaskListsOptions {
|
||||
enabled: boolean
|
||||
label: boolean
|
||||
lineNumber: boolean
|
||||
}
|
||||
|
||||
const checkboxRegex = /^ *\[([\sx])] /i
|
||||
|
||||
export function taskLists(
|
||||
md: MarkdownIt,
|
||||
options: TaskListsOptions = { enabled: false, label: false, lineNumber: false }
|
||||
): void {
|
||||
md.core.ruler.after('inline', 'task-lists', (state) => processToken(state, options))
|
||||
md.renderer.rules.taskListItemCheckbox = (tokens) => {
|
||||
const token = tokens[0]
|
||||
const checkedAttribute = token.attrGet('checked') ? 'checked="" ' : ''
|
||||
const disabledAttribute = token.attrGet('disabled') ? 'disabled="" ' : ''
|
||||
const line = token.attrGet('line')
|
||||
const idAttribute = `id="${token.attrGet('id')}" `
|
||||
const dataLineAttribute = line && options.lineNumber ? `data-line="${line}" ` : ''
|
||||
|
||||
return `<input class="task-list-item-checkbox" type="checkbox" ${checkedAttribute}${disabledAttribute}${dataLineAttribute}${idAttribute}/>`
|
||||
}
|
||||
|
||||
md.renderer.rules.taskListItemLabel_close = () => {
|
||||
return '</label>'
|
||||
}
|
||||
|
||||
md.renderer.rules.taskListItemLabel_open = (tokens: Token[]) => {
|
||||
const token = tokens[0]
|
||||
const id = token.attrGet('id')
|
||||
return `<label for="${id}">`
|
||||
}
|
||||
}
|
||||
|
||||
function processToken(state: StateCore, options: TaskListsOptions): boolean {
|
||||
const allTokens = state.tokens
|
||||
for (let i = 2; i < allTokens.length; i++) {
|
||||
if (!isTodoItem(allTokens, i)) {
|
||||
continue
|
||||
}
|
||||
|
||||
todoify(allTokens[i], options)
|
||||
allTokens[i - 2].attrJoin('class', `task-list-item ${options.enabled ? ' enabled' : ''}`)
|
||||
|
||||
const parentToken = findParentToken(allTokens, i - 2)
|
||||
if (parentToken) {
|
||||
const classes = parentToken.attrGet('class') ?? ''
|
||||
if (!classes.match(/(^| )contains-task-list/)) {
|
||||
parentToken.attrJoin('class', 'contains-task-list')
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findParentToken(tokens: Token[], index: number): Token | undefined {
|
||||
const targetLevel = tokens[index].level - 1
|
||||
for (let currentTokenIndex = index - 1; currentTokenIndex >= 0; currentTokenIndex--) {
|
||||
if (tokens[currentTokenIndex].level === targetLevel) {
|
||||
return tokens[currentTokenIndex]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isTodoItem(tokens: Token[], index: number): boolean {
|
||||
return (
|
||||
isInline(tokens[index]) &&
|
||||
isParagraph(tokens[index - 1]) &&
|
||||
isListItem(tokens[index - 2]) &&
|
||||
startsWithTodoMarkdown(tokens[index])
|
||||
)
|
||||
}
|
||||
|
||||
function todoify(token: Token, options: TaskListsOptions): void {
|
||||
if (token.children == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = generateIdForToken(token)
|
||||
|
||||
token.children.splice(0, 0, createCheckboxToken(token, options.enabled, id))
|
||||
token.children[1].content = token.children[1].content.replace(checkboxRegex, '')
|
||||
|
||||
if (options.label) {
|
||||
token.children.splice(1, 0, createLabelBeginToken(id))
|
||||
token.children.push(createLabelEndToken())
|
||||
}
|
||||
}
|
||||
|
||||
function generateIdForToken(token: Token): string {
|
||||
if (token.map) {
|
||||
return `task-item-${token.map[0]}`
|
||||
} else {
|
||||
return `task-item-${Math.ceil(Math.random() * (10000 * 1000) - 1000)}`
|
||||
}
|
||||
}
|
||||
|
||||
function createCheckboxToken(token: Token, enabled: boolean, id: string): Token {
|
||||
const checkbox = new Token('taskListItemCheckbox', '', 0)
|
||||
if (!enabled) {
|
||||
checkbox.attrSet('disabled', 'true')
|
||||
}
|
||||
if (token.map) {
|
||||
checkbox.attrSet('line', token.map[0].toString())
|
||||
}
|
||||
|
||||
checkbox.attrSet('id', id)
|
||||
|
||||
const checkboxRegexResult = checkboxRegex.exec(token.content)
|
||||
const isChecked = checkboxRegexResult?.[1].toLowerCase() === 'x'
|
||||
if (isChecked) {
|
||||
checkbox.attrSet('checked', 'true')
|
||||
}
|
||||
|
||||
return checkbox
|
||||
}
|
||||
|
||||
function createLabelBeginToken(id: string): Token {
|
||||
const labelBeginToken = new Token('taskListItemLabel_open', '', 1)
|
||||
labelBeginToken.attrSet('id', id)
|
||||
return labelBeginToken
|
||||
}
|
||||
|
||||
function createLabelEndToken(): Token {
|
||||
return new Token('taskListItemLabel_close', '', -1)
|
||||
}
|
||||
|
||||
function isInline(token: Token): boolean {
|
||||
return token.type === 'inline'
|
||||
}
|
||||
|
||||
function isParagraph(token: Token): boolean {
|
||||
return token.type === 'paragraph_open'
|
||||
}
|
||||
|
||||
function isListItem(token: Token): boolean {
|
||||
return token.type === 'list_item_open'
|
||||
}
|
||||
|
||||
function startsWithTodoMarkdown(token: Token): boolean {
|
||||
return checkboxRegex.test(token.content)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue