UNPKG

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.

172 lines (143 loc) 6.31 kB
/** * Custom directive plugin for markdown-it * Processes %%fnName [arg1=value1, arg2=value2]%% directives * @param {object} md - markdown-it instance * @param {object} handlers - Object containing handler functions keyed by directive name * @returns {void} */ function directivePlugin(md, handlers = {}) { // Regular expression to match directives // Matches: %%functionName [arg1=value1, arg2=value2]%% const DIRECTIVE_RE = /%%(\w+)\s*(?:\[([^\]]*)\])?%%/ // eslint-disable-line security/detect-unsafe-regex /** * Parse arguments from directive string * @param {string} argsStr - Arguments string like "arg1=value1, arg2=value2" * @returns {object} Parsed arguments as key-value pairs */ /** Parse arguments from directive string. * Supports space- or comma-separated key=value pairs. * Values may be double-quoted, single-quoted, or unquoted. * Quoted values may contain spaces and HTML. * @param {string} argsStr - Arguments string, e.g. `key="val with spaces" key2=simple` * @returns {object} Parsed arguments as key-value pairs */ function parseArgs(argsStr) { if (!argsStr || !argsStr.trim()) return {} const args = {} // key=value where value is: double-quoted, single-quoted, or an unquoted non-whitespace token const re = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g // eslint-disable-line security/detect-unsafe-regex let match while ((match = re.exec(argsStr)) !== null) { // groups: 1=key, 2=double-quoted, 3=single-quoted, 4=unquoted args[match[1]] = match[2] ?? match[3] ?? match[4] ?? '' } return args } /** * Inline rule to process directives * @param {object} state - Current state * @param {boolean} silent - Silent mode flag * @returns {boolean} Success status */ function directiveRule(state, silent) { const start = state.pos const max = state.posMax // Quick fail if we don't have %% if (state.src.charCodeAt(start) !== 0x25 || /* % */ state.src.charCodeAt(start + 1) !== 0x25 /* % */) { return false } // Check if we're inside code // markdown-it tracks code state, but we need to check manually const marker = state.src.slice(start, start + 2) if (marker !== '%%') return false // Additional safety: check if we're in a code block by examining previous tokens // const tokens = state.tokens // for (let i = tokens.length - 1; i >= 0; i--) { // const token = tokens[i] // if (token.type === 'code_inline' || token.type === 'fence') { // return false // } // } // Find the closing %% const match = DIRECTIVE_RE.exec(state.src.slice(start)) if (!match) return false const directive = match[0] const fnName = match[1] const argsStr = match[2] || '' // If in silent mode, just return true to indicate we matched if (silent) return true // Parse arguments const args = parseArgs(argsStr) // Create token const token = state.push('directive', '', 0) token.meta = { fnName, args, raw: directive, } // Advance position state.pos += directive.length return true } /** * Renderer for directive tokens * @param {Array} tokens - Token array * @param {number} idx - Current token index * @param {object} options - Options * @param {object} env - Environment * @param {object} self - Renderer instance * @returns {string} Rendered HTML */ function renderDirective(tokens, idx, options, env, self) { const token = tokens[idx] const { fnName, args, raw, } = token.meta // Check if handler exists if (handlers[fnName] && typeof handlers[fnName] === 'function') { try { return handlers[fnName](args, env) } catch (err) { console.error(`Error executing directive handler '${fnName}': ${err.message}`, err) return `<span class="directive-error" data-directive="${fnName}">Error in directive: ${fnName}</span><p>${err.message}</p>` } } // No handler found, return as-is or error return `<span class="directive-unknown" data-directive="${fnName}">${md.utils.escapeHtml(raw)}</span>` } /** Block rule to process directives that are the sole content of a paragraph. * This prevents block-level output (e.g. <ul>) from being wrapped in <p> tags. * @param {object} state - Block state * @param {number} startLine - Start line index * @param {number} endLine - End line index * @param {boolean} silent - Silent mode flag * @returns {boolean} Success status */ function directiveBlockRule(state, startLine, endLine, silent) { const pos = state.bMarks[startLine] + state.tShift[startLine] const max = state.eMarks[startLine] const line = state.src.slice(pos, max).trim() // Must match the entire line as a single directive const match = line.match(/^%%(\w+)\s*(?:\[([^\]]*)\])?\s*%%$/) // eslint-disable-line security/detect-unsafe-regex if (!match) return false if (silent) return true const fnName = match[1] const argsStr = match[2] || '' state.line = startLine + 1 const token = state.push('directive_block', '', 0) token.meta = { fnName, args: parseArgs(argsStr), raw: match[0], } token.map = [startLine, state.line] return true } // Register block rule (higher priority) - handles directives on their own line md.block.ruler.before('paragraph', 'directive_block', directiveBlockRule) // Register the inline rule - handles directives embedded in text md.inline.ruler.before('escape', 'directive', directiveRule) // Both block and inline directive tokens use the same renderer md.renderer.rules.directive_block = renderDirective md.renderer.rules.directive = renderDirective } export { directivePlugin }