node-red-contrib-uibuilder
Version:
Easily create data-driven web UI's for Node-RED. Single- & Multi-page. Multiple UI's. Work with existing web development workflows or mix and match with no-code/low-code features.
196 lines (172 loc) • 7.34 kB
JavaScript
/** Reworked replica of markdown-it-task-lists
* https://github.com/revin/markdown-it-task-lists
* https://github.com/Anson2251/markdown-it-task-lists
* Brought more up-to-date and with added uibuilder features.
*/
let disableCheckboxes = true
let useLabelWrapper = false
/** Sets an attribute on a token, pushing it if new or replacing if it exists.
* @param {object} token - The markdown-it token to modify
* @param {string} name - The attribute name to set
* @param {string} value - The attribute value to set
*/
function attrSet(token, name, value) {
const index = token.attrIndex(name)
const attr = [name, value]
if (index < 0) {
token.attrPush(attr)
} else {
token.attrs[index] = attr
}
}
/** Finds the parent token index by walking backwards through tokens.
* @param {Array<object>} tokens - Array of markdown-it tokens
* @param {number} index - The current token index to find parent for
* @returns {number} The index of the parent token, or -1 if not found
*/
function parentToken(tokens, index) {
const targetLevel = tokens[index].level - 1
for (let i = index - 1; i >= 0; i--) {
if (tokens[i].level === targetLevel) {
return i
}
}
return -1
}
/** Checks if the token at the given index represents a todo/task list item.
* @param {Array<object>} tokens - Array of markdown-it tokens
* @param {number} index - The token index to check
* @returns {boolean} True if the token is a todo item
*/
function isTodoItem(tokens, index) {
return isInline(tokens[index])
&& isParagraph(tokens[index - 1])
&& isListItem(tokens[index - 2])
&& startsWithTodoMarkdown(tokens[index])
}
/** Converts a token into a todo/task item by adding checkbox and optional label elements.
* @param {object} token - The markdown-it token to convert
* @param {Function} TokenConstructor - The Token constructor from markdown-it state
*/
function todoify(token, TokenConstructor) {
const isChecked = startsWithCheckedTodoMarkdown(token)
stripTodoPrefix(token)
if (useLabelWrapper) {
// Wrap existing parsed children to preserve inline markdown rendering
const labelOpenToken = new TokenConstructor('html_inline', '', 0)
const labelCloseToken = new TokenConstructor('html_inline', '', 0)
const textWrapOpenToken = new TokenConstructor('html_inline', '', 0)
const textWrapCloseToken = new TokenConstructor('html_inline', '', 0)
const checkbox = makeCheckbox(TokenConstructor, isChecked)
labelOpenToken.content = '<label class="task-list-item-label">'
textWrapOpenToken.content = '<span class="task-list-item-text">'
textWrapCloseToken.content = '</span>'
labelCloseToken.content = '</label>'
token.children = [
labelOpenToken,
checkbox,
textWrapOpenToken,
...token.children,
textWrapCloseToken,
labelCloseToken,
]
} else {
// Original behaviour: checkbox followed by parsed text content
token.children.unshift(makeCheckbox(TokenConstructor, isChecked))
}
}
/** Creates a checkbox input token based on the task item state.
* @param {Function} TokenConstructor - The Token constructor from markdown-it state
* @param {boolean} isChecked - Whether the task item is checked
* @returns {object} A new html_inline token containing the checkbox input
*/
function makeCheckbox(TokenConstructor, isChecked) {
const checkbox = new TokenConstructor('html_inline', '', 0)
const disabledAttr = disableCheckboxes ? ' disabled="" ' : ''
const checkedAttr = isChecked ? ' checked="" ' : ''
const uibActionAttr = ''
// if (!disableCheckboxes) uibActionAttr = ' onclick="uibuilder.eventSend(event)" '
checkbox.content = `<input class="task-list-item-checkbox"${checkedAttr}${disabledAttr}${uibActionAttr}type="checkbox">`
return checkbox
}
/** Checks if a token is an inline token.
* @param {object} token - The markdown-it token to check
* @returns {boolean} True if the token type is 'inline'
*/
function isInline(token) {
return token.type === 'inline'
}
/** Checks if a token is a paragraph open token.
* @param {object} token - The markdown-it token to check
* @returns {boolean} True if the token type is 'paragraph_open'
*/
function isParagraph(token) {
return token.type === 'paragraph_open'
}
/** Checks if a token is a list item open token.
* @param {object} token - The markdown-it token to check
* @returns {boolean} True if the token type is 'list_item_open'
*/
function isListItem(token) {
return token.type === 'list_item_open'
}
/** Checks if a token's content starts with todo markdown syntax ([ ], [x], or [X]).
* @param {object} token - The markdown-it token to check
* @returns {boolean} True if the token content starts with todo markdown
*/
function startsWithTodoMarkdown(token) {
// leading whitespace in a list item is already trimmed off by markdown-it
return token.content.indexOf('[ ] ') === 0 || token.content.indexOf('[x] ') === 0 || token.content.indexOf('[X] ') === 0
}
/** Checks whether todo markdown starts with a checked marker.
* @param {object} token - The markdown-it token to check
* @returns {boolean} True if token starts with checked todo markdown
*/
function startsWithCheckedTodoMarkdown(token) {
return token.content.indexOf('[x] ') === 0 || token.content.indexOf('[X] ') === 0
}
/** Removes the todo marker prefix from inline token content and first matching child token.
* Keeps markdown-it children intact so inline markdown (e.g., code spans) still renders.
* @param {object} token - The markdown-it inline token to modify
*/
function stripTodoPrefix(token) {
token.content = token.content.slice(4)
if (!Array.isArray(token.children)) return
for (const child of token.children) {
if (typeof child?.content !== 'string') continue
if (!startsWithTodoMarkdown(child)) continue
child.content = child.content.slice(4)
if (child.position) {
child.position += 4
}
if (child.size) {
child.size -= 4
}
break
}
}
/** markdown-it plugin to render GitHub-style task lists.
* @param {object} md - The markdown-it instance
* @param {object} [options] - Plugin configuration options
* @param {boolean} [options.label] - Whether to wrap checkbox and text in a label element
* @param {boolean|Function} [options.enabled] - Whether checkboxes are enabled (editable)
*/
function taskLists(md, options) {
if (options) {
useLabelWrapper = !!options.label
}
md.core.ruler.after('inline', 'github-task-lists', function(state) {
if (options) {
disableCheckboxes = (typeof options.enabled === 'function') ? !options.enabled() : !options.enabled
}
const tokens = state.tokens
for (let i = 2; i < tokens.length; i++) {
if (isTodoItem(tokens, i)) {
todoify(tokens[i], state.Token)
attrSet(tokens[i - 2], 'class', 'task-list-item' + (!disableCheckboxes ? ' enabled' : ''))
attrSet(tokens[parentToken(tokens, i - 2)], 'class', 'contains-task-list')
}
}
})
}
export { taskLists }