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.

136 lines (115 loc) 5.32 kB
/** * Custom variables plugin for markdown-it * Processes {{varname [arg1=value1, arg2=value2]}} * * Standard arguments recognised by the default handler in customNode.js: * - before : text (or HTML) prepended to the value; only rendered when a value is present * - after : text (or HTML) appended to the value; only rendered when a value is present * - prefix : alias for `before` * - default : fallback value displayed when the frontmatter variable is missing * * When `before` / `after` are supplied they are also set as `data-before` / `data-after` * attributes on the rendered `<fm-var>` element, mirroring the `<uib-var>` component behaviour. * @param {object} md - markdown-it instance * @param {function} handler - Function to handle variables * @returns {void} */ function fmVariablesPlugin(md, handler) { // Regular expression to match variables // Matches: {{varname [arg1=value1, arg2=value2]}} const VARIABLE_RE = /\{\{(\w+)\s*(?:\[([^\]]*)\])?\}\}/ // eslint-disable-line security/detect-unsafe-regex /** Parse arguments from variable 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. `before="<b>Title</b>: " after=" end"` or `key=val, key2=val2` * @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 variables * @param {object} state - Current state * @param {boolean} silent - Silent mode flag * @returns {boolean} Success status */ function fmVariablesRule(state, silent) { const start = state.pos const max = state.posMax // Quick fail if we don't have {{ if (state.src.charCodeAt(start) !== 0x7B || /* { */ state.src.charCodeAt(start + 1) !== 0x7B /* { */) { 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 = VARIABLE_RE.exec(state.src.slice(start)) if (!match) return false const fmVariable = 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) args.varName = fnName // Include the variable name in args for convenience // Create token const token = state.push('fmVariables', '', 0) token.meta = { args, raw: fmVariable, } // Advance position state.pos += fmVariable.length return true } /** Renderer for variable tokens * NB: Errors are wrapped in a dummy component, <fm-var class="variable-error">, so that they can be easily styled. * @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 renderFmVariables(tokens, idx, options, env, self) { const token = tokens[idx] const { args, raw, } = token.meta // Check if handler exists if (handler && typeof handler === 'function') { try { return handler(args, env) } catch (err) { console.error(`Error executing variable handler '${args.varName}': ${err.message}`, err) return `<fm-var class="fm-${args.varName} variable-error" data-fmvar="${args.varName}">Error in variable: ${args.varName}</fm-var><p>${err.message}</p>` } } // No handler found, return as-is or error return `<fm-var class="fm-${args.varName} variable-unknown" data-fmvar="${args.varName}">Unknown variable: ${args.varName}</fm-var>` } // Register the inline rule md.inline.ruler.before('escape', 'fmVariables', fmVariablesRule) // Register the renderer md.renderer.rules.fmVariables = renderFmVariables } export { fmVariablesPlugin }