feat(markdown): support task list

This commit is contained in:
EGOIST 2022-05-23 17:59:15 +08:00
parent 486f6451fe
commit 6bfb48db4c
3 changed files with 148 additions and 0 deletions

View File

@ -89,3 +89,11 @@
.prose table tbody tr:nth-child(2n-1) {
@apply bg-zinc-100;
}
.prose .task-list {
@apply list-none;
}
.prose > .task-list {
@apply pl-0;
}

View File

@ -3,6 +3,7 @@ import { pluginCodeBlock } from "./plugin-code-block"
import { pluginExcerpt } from "./plugin-excerpt"
import { pluginImage } from "./plugin-image"
import { pluginTable } from "./plugin-table"
import { pluginTaskList } from "./plugin-task-list"
export type MarkdownEnv = {
excerpt: string
@ -24,6 +25,7 @@ export const renderPageContent = async (content: string): Promise<Rendered> => {
md.use(pluginCodeBlock)
md.use(pluginExcerpt)
md.use(pluginTable)
md.use(pluginTaskList)
const env: MarkdownEnv = { excerpt: "", __internal: {} }
const contentHTML = md.render(content, env)

View File

@ -0,0 +1,138 @@
import MarkdownIt from "markdown-it"
import Token from "markdown-it/lib/token"
export const pluginTaskList = (md: MarkdownIt, options?: any) => {
options = Object.assign(
{
disabled: true,
divWrap: false,
divClass: "checkbox",
idPrefix: "cbx_",
ulClass: "task-list",
liClass: "task-list-item",
},
options,
)
md.core.ruler.after("inline", "github-task-lists", (state) => {
const { tokens } = state
let lastId = 0
for (let i = 2; i < tokens.length; i++) {
if (isTodoItem(tokens, i)) {
todoify(tokens[i], `${lastId}`, options, state.Token)
lastId += 1
const token = tokens[i - 2]
token.attrSet("class", options.liClass)
const parentTokenIndex = getParentTokenIndex(tokens, i - 2)
const parentToken = tokens[parentTokenIndex]
if (parentToken) {
parentToken.attrSet("class", options.ulClass)
}
}
}
})
}
function getParentTokenIndex(tokens: Token[], index: number) {
const targetLevel = tokens[index].level - 1
for (let i = index - 1; i >= 0; i--) {
if (tokens[i].level === targetLevel) {
return i
}
}
return -1
}
function isTodoItem(tokens: Token[], index: number) {
return (
isInline(tokens[index]) &&
isParagraph(tokens[index - 1]) &&
isListItem(tokens[index - 2]) &&
startsWithTodoMarkdown(tokens[index])
)
}
function todoify(
token: Token,
lastId: string,
options: any,
TokenConstructor: typeof Token,
) {
const id = `${options.idPrefix}${lastId}`
token.children![0].content = token.children![0].content.slice(3)
// label
token.children!.unshift(beginLabel(id, TokenConstructor))
token.children!.push(endLabel(TokenConstructor))
// checkbox
token.children!.unshift(makeCheckbox(token, id, options, TokenConstructor))
if (options.divWrap) {
token.children!.unshift(beginWrap(options, TokenConstructor))
token.children!.push(endWrap(TokenConstructor))
}
}
function makeCheckbox(
token: Token,
id: string,
options: any,
TokenConstructor: typeof Token,
) {
const checkbox = new TokenConstructor("checkbox_input", "input", 0)
checkbox.attrs = [
["type", "checkbox"],
["id", id],
]
const checked = /^\[[xX]\][ \u00A0]/.test(token.content) // if token.content starts with '[x] ' or '[X] '
if (checked === true) {
checkbox.attrs.push(["checked", "true"])
}
if (options.disabled === true) {
checkbox.attrs.push(["disabled", "true"])
}
return checkbox
}
function beginLabel(id: string, TokenConstructor: typeof Token) {
const label = new TokenConstructor("label_open", "label", 1)
label.attrs = [["for", id]]
return label
}
function endLabel(TokenConstructor: typeof Token) {
return new TokenConstructor("label_close", "label", -1)
}
// these next two functions are kind of hacky; probably should really be a
// true block-level token with .tag=='label'
function beginWrap(options: any, TokenConstructor: typeof Token) {
const token = new TokenConstructor("checkbox_open", "div", 0)
token.attrs = [["class", options.divClass]]
return token
}
function endWrap(TokenConstructor: typeof Token) {
const token = new TokenConstructor("checkbox_close", "div", -1)
// token.content = '</label>';
return token
}
function isInline(token: Token) {
return token.type === "inline"
}
function isParagraph(token: Token) {
return token.type === "paragraph_open"
}
function isListItem(token: Token) {
return token.type === "list_item_open"
}
function startsWithTodoMarkdown(token: Token) {
// The leading whitespace in a list item (token.content) is already trimmed off by markdown-it.
// The regex below checks for '[ ] ' or '[x] ' or '[X] ' at the start of the string token.content,
// where the space is either a normal space or a non-breaking space (character 160 = \u00A0).
return /^\[[xX \u00A0]\][ \u00A0]/.test(token.content)
}