Skip to content

Commit

Permalink
chore(markdownit): inline taskLists plugin
Browse files Browse the repository at this point in the history
The upstream version was unmaintained and removed from NPM some time ago
and no longer compatible with latest markdownit.

Taken from https://github.com/hedgedoc/hedgedoc/blob/f5736dad0f2593b44d4db687e09f3f0247ee3ce4/markdown-it-plugins/src/task-lists/index.ts

Signed-off-by: Jonas <jonas@freesources.org>
  • Loading branch information
mejo- committed Nov 26, 2024
1 parent 8103570 commit 1f0ae0d
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 22 deletions.
15 changes: 0 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@hedgedoc/markdown-it-task-lists": "^2.0.1",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.4.0",
"@nextcloud/axios": "^2.5.1",
Expand Down
4 changes: 2 additions & 2 deletions src/markdownit/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*/

import type MarkdownIt from 'markdown-it'
import type StateBlock from 'markdown-it/lib/rules_block/state_block'
import type Token from 'markdown-it/lib/token'
import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs'
import type Token from 'markdown-it/lib/token.mjs'

const DETAILS_START_REGEX = /^<details>\s*$/
const DETAILS_END_REGEX = /^<\/details>\s*$/
Expand Down
4 changes: 2 additions & 2 deletions src/markdownit/hardbreak.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import markdownitNewline from 'markdown-it/lib/rules_inline/newline.js'
import markdownitEscape from 'markdown-it/lib/rules_inline/escape.js'
import markdownitNewline from 'markdown-it/lib/rules_inline/newline.mjs'
import markdownitEscape from 'markdown-it/lib/rules_inline/escape.mjs'

/**
* Add information about used markdown syntax to HTML hard breaks
Expand Down
4 changes: 2 additions & 2 deletions src/markdownit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import MarkdownIt from 'markdown-it'
import taskLists from '@hedgedoc/markdown-it-task-lists'
import taskLists from './taskLists.ts'
import markdownitMentions from '@quartzy/markdown-it-mentions'
import underline from './underline.js'
import splitMixedLists from './splitMixedLists.js'
Expand All @@ -15,7 +15,7 @@ import hardbreak from './hardbreak.js'
import keepSyntax from './keepSyntax.js'
import frontMatter from 'markdown-it-front-matter'
import implicitFigures from 'markdown-it-image-figures'
import { escapeHtml } from 'markdown-it/lib/common/utils.js'
import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'

const markdownit = MarkdownIt('commonmark', { html: false, breaks: false })
.enable('strikethrough')
Expand Down
159 changes: 159 additions & 0 deletions src/markdownit/taskLists.ts
Original file line number Diff line number Diff line change
@@ -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'
import StateCore from 'markdown-it/lib/rules_core/state_core.mjs'
import Token from 'markdown-it/lib/token.mjs'

interface TaskListsOptions {
enabled: boolean
label: boolean
lineNumber: boolean
}

const checkboxRegex = /^ *\[([\sx])] /i

export default 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)
}

0 comments on commit 1f0ae0d

Please sign in to comment.