UNPKG

@intlayer/core

Version:

Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.

369 lines (367 loc) 13 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_markdown_constants = require('./constants.cjs'); //#region src/markdown/utils.ts /** * Trim trailing whitespace from a string. */ const trimEnd = (str) => { let end = str.length; while (end > 0 && str[end - 1] <= " ") end--; return str.slice(0, end); }; /** * Check if string starts with prefix. */ const startsWith = (str, prefix) => { return str.startsWith(prefix); }; /** * Remove symmetrical leading and trailing quotes. */ const unquote = (str) => { const first = str[0]; if ((first === "\"" || first === "'") && str.length >= 2 && str[str.length - 1] === first) return str.slice(1, -1); return str; }; /** * Unescape backslash-escaped characters. */ const unescapeString = (rawString) => rawString ? rawString.replace(require_markdown_constants.UNESCAPE_R, "$1") : rawString; /** * Join class names, filtering out falsy values. */ const cx = (...args) => args.filter(Boolean).join(" "); /** * Get a nested property from an object using dot notation. */ const get = (src, path, fb) => { let ptr = src; const frags = path.split("."); while (frags.length) { ptr = ptr[frags[0]]; if (ptr === void 0) break; else frags.shift(); } return ptr ?? fb; }; /** * Convert a string to a URL-safe slug. * Based on https://stackoverflow.com/a/18123682/1141611 */ const slugify = (str) => str.replace(/[ÀÁÂÃÄÅàáâãä忯]/g, "a").replace(/[çÇ]/g, "c").replace(/[ðÐ]/g, "d").replace(/[ÈÉÊËéèêë]/g, "e").replace(/[ÏïÎîÍíÌì]/g, "i").replace(/[Ññ]/g, "n").replace(/[øØœŒÕõÔôÓóÒò]/g, "o").replace(/[ÜüÛûÚúÙù]/g, "u").replace(/[ŸÿÝý]/g, "y").replace(/[^a-z0-9- ]/gi, "").replace(/ /gi, "-").toLowerCase(); const SANITIZE_R = /(javascript|vbscript|data(?!:image)):/i; /** * Sanitize URLs to prevent XSS attacks. * Returns null if the URL is unsafe. */ const sanitizer = (input) => { try { const decoded = decodeURIComponent(input).replace(/[^A-Za-z0-9/:]/g, ""); if (SANITIZE_R.test(decoded)) { console.warn("Input contains an unsafe JavaScript/VBScript/data expression, it will not be rendered.", decoded); return null; } } catch (_e) { console.warn("Input could not be decoded due to malformed syntax or characters, it will not be rendered.", input); return null; } return input; }; /** * Normalize whitespace in source string. */ const normalizeWhitespace = (source) => { const start = performance.now(); const result = source.replace(require_markdown_constants.CR_NEWLINE_R, "\n").replace(require_markdown_constants.FORMFEED_R, "").replace(require_markdown_constants.TAB_R, " "); const duration = performance.now() - start; if (duration > 20) console.log(`normalizeWhitespace: ${duration.toFixed(3)}ms, source length: ${source.length}`); return result; }; /** * Safely remove a uniform leading indentation from lines, but do NOT touch * the content inside fenced code blocks (``` or ~~~). */ const trimLeadingWhitespaceOutsideFences = (text, whitespace) => { const start = performance.now(); if (!whitespace) return text; const lines = text.split("\n"); const result = lines.map((line) => line.startsWith(whitespace) ? line.slice(whitespace.length) : line).join("\n"); const duration = performance.now() - start; if (duration > 20) console.log(`trimLeadingWhitespaceOutsideFences: ${duration.toFixed(3)}ms, text length: ${text.length}, lines count: ${lines.length}`); return result; }; /** * Normalize HTML attribute key to JSX prop name. */ const normalizeAttributeKey = (key) => { if (key.indexOf("-") !== -1 && key.match(require_markdown_constants.HTML_CUSTOM_ATTR_R) === null) key = key.replace(require_markdown_constants.CAPTURE_LETTER_AFTER_HYPHEN, (_, letter) => { return letter.toUpperCase(); }); return key; }; /** * Parse a CSS style string into an array of [key, value] tuples. */ const parseStyleAttribute = (styleString) => { const start = performance.now(); const styles = []; let buffer = ""; let inUrl = false; let inQuotes = false; let quoteChar = ""; if (!styleString) return styles; for (let i = 0; i < styleString.length; i++) { const char = styleString[i]; if ((char === "\"" || char === "'") && !inUrl) { if (!inQuotes) { inQuotes = true; quoteChar = char; } else if (char === quoteChar) { inQuotes = false; quoteChar = ""; } } if (char === "(" && buffer.endsWith("url")) inUrl = true; else if (char === ")" && inUrl) inUrl = false; if (char === ";" && !inQuotes && !inUrl) { const declaration = buffer.trim(); if (declaration) { const colonIndex = declaration.indexOf(":"); if (colonIndex > 0) { const key = declaration.slice(0, colonIndex).trim(); const value = declaration.slice(colonIndex + 1).trim(); styles.push([key, value]); } } buffer = ""; } else buffer += char; } const declaration = buffer.trim(); if (declaration) { const colonIndex = declaration.indexOf(":"); if (colonIndex > 0) { const key = declaration.slice(0, colonIndex).trim(); const value = declaration.slice(colonIndex + 1).trim(); styles.push([key, value]); } } const duration = performance.now() - start; if (duration > 20) console.log(`parseStyleAttribute: ${duration.toFixed(3)}ms, styleString length: ${styleString.length}, styles count: ${styles.length}`); return styles; }; /** * Convert an attribute value to a Node prop value. */ const attributeValueToNodePropValue = (tag, key, value, sanitizeUrlFn) => { if (key === "style") return parseStyleAttribute(value).reduce((styles, [styleKey, styleValue]) => { const camelCasedKey = styleKey.replace(/(-[a-z])/g, (substr) => substr[1].toUpperCase()); styles[camelCasedKey] = sanitizeUrlFn(styleValue, tag, styleKey); return styles; }, {}); else if (require_markdown_constants.ATTRIBUTES_TO_SANITIZE.indexOf(key) !== -1) return sanitizeUrlFn(unescapeString(value), tag, key); else if (value.match(require_markdown_constants.INTERPOLATION_R)) value = unescapeString(value.slice(1, value.length - 1)); if (value === "true") return true; else if (value === "false") return false; return value; }; /** * Parse table alignment from a separator row. */ const parseTableAlignCapture = (alignCapture) => { if (require_markdown_constants.TABLE_RIGHT_ALIGN.test(alignCapture)) return "right"; else if (require_markdown_constants.TABLE_CENTER_ALIGN.test(alignCapture)) return "center"; else if (require_markdown_constants.TABLE_LEFT_ALIGN.test(alignCapture)) return "left"; return "left"; }; /** * Parse table alignment row. */ const parseTableAlign = (source) => { return source.replace(require_markdown_constants.TABLE_TRIM_PIPES, "").split("|").map(parseTableAlignCapture); }; /** * Parse a single table row. */ const parseTableRow = (source, parse, state, tableOutput) => { const start = performance.now(); const prevInTable = state.inTable; state.inTable = true; const cells = [[]]; let acc = ""; const flush = () => { if (!acc) return; const cell = cells[cells.length - 1]; cell.push.apply(cell, parse(acc, state)); acc = ""; }; source.trim().split(/(`[^`]*`|\\\||\|)/).filter(Boolean).forEach((fragment, i, arr) => { if (fragment.trim() === "|") { flush(); if (tableOutput) { if (i !== 0 && i !== arr.length - 1) cells.push([]); return; } } acc += fragment; }); flush(); state.inTable = prevInTable; const duration = performance.now() - start; if (duration > 20) console.log(`parseTableRow: ${duration.toFixed(3)}ms, source length: ${source.length}, cells count: ${cells.length}`); return cells; }; /** * Parse table cells (multiple rows). */ const parseTableCells = (source, parse, state) => { const start = performance.now(); const rowsText = source.trim().split("\n"); const result = rowsText.map((rowText) => parseTableRow(rowText, parse, state, true)); const duration = performance.now() - start; if (duration > 20) console.log(`parseTableCells: ${duration.toFixed(3)}ms, source length: ${source.length}, rows count: ${rowsText.length}`); return result; }; /** * Check if a rule qualifies for the current source and state. */ const qualifies = (source, state, qualify) => { if (Array.isArray(qualify)) { for (let i = 0; i < qualify.length; i++) if (startsWith(source, qualify[i])) return true; return false; } return qualify(source, state); }; /** * Marks a matcher function as eligible for being run inside an inline context. */ const allowInline = (fn) => { fn.inline = 1; return fn; }; /** * Creates a match function for an inline scoped element from a regex. */ const inlineRegex = (regex) => allowInline((source, state) => { if (state.inline) return regex.exec(source); else return null; }); /** * Creates a match function for inline elements except links. */ const simpleInlineRegex = (regex) => allowInline((source, state) => { if (state.inline || state.simple) return regex.exec(source); else return null; }); /** * Creates a match function for a block scoped element from a regex. */ const blockRegex = (regex) => (source, state) => { if (state.inline || state.simple) return null; else return regex.exec(source); }; /** * Creates a match function from a regex, ignoring block/inline scope. */ const anyScopeRegex = (fn) => allowInline((source, state) => { if (typeof fn === "function") return fn(source, state); return fn.exec(source); }); /** * Parse inline content (including links). */ const parseInline = (parse, children, state) => { const start = performance.now(); const isCurrentlyInline = state.inline ?? false; const isCurrentlySimple = state.simple ?? false; state.inline = true; state.simple = true; const result = parse(children, state); state.inline = isCurrentlyInline; state.simple = isCurrentlySimple; const duration = performance.now() - start; if (duration > 20) console.log(`parseInline: ${duration.toFixed(3)}ms, children length: ${children.length}, result count: ${result.length}`); return result; }; /** * Parse simple inline content (no links). */ const parseSimpleInline = (parse, children, state) => { const start = performance.now(); const isCurrentlyInline = state.inline ?? false; const isCurrentlySimple = state.simple ?? false; state.inline = false; state.simple = true; const result = parse(children, state); state.inline = isCurrentlyInline; state.simple = isCurrentlySimple; const duration = performance.now() - start; if (duration > 20) console.log(`parseSimpleInline: ${duration.toFixed(3)}ms, children length: ${children.length}, result count: ${result.length}`); return result; }; /** * Parse block content. */ const parseBlock = (parse, children, state = {}) => { const start = performance.now(); const isCurrentlyInline = state.inline || false; state.inline = false; const normalizedChildren = trimEnd(children); const result = parse(/\n\n$/.test(normalizedChildren) === false ? normalizedChildren.endsWith("\n") ? `${normalizedChildren}\n` : `${normalizedChildren}\n\n` : normalizedChildren, state); state.inline = isCurrentlyInline; const duration = performance.now() - start; if (duration > 20) console.log(`parseBlock: ${duration.toFixed(3)}ms, children length: ${children.length}, result count: ${result.length}`); return result; }; /** * Helper to parse capture group 2 as inline content. */ const parseCaptureInline = (capture, parse, state) => { return { children: parseInline(parse, capture[2], state) }; }; /** * Helper that captures nothing (empty object). */ const captureNothing = () => ({}); /** * Helper that renders nothing (null). */ const renderNothing = () => null; /** * Check if any regex in a list matches the input. */ const some = (regexes, input) => { for (let i = 0; i < regexes.length; i++) if (regexes[i].test(input)) return true; return false; }; //#endregion exports.allowInline = allowInline; exports.anyScopeRegex = anyScopeRegex; exports.attributeValueToNodePropValue = attributeValueToNodePropValue; exports.blockRegex = blockRegex; exports.captureNothing = captureNothing; exports.cx = cx; exports.get = get; exports.inlineRegex = inlineRegex; exports.normalizeAttributeKey = normalizeAttributeKey; exports.normalizeWhitespace = normalizeWhitespace; exports.parseBlock = parseBlock; exports.parseCaptureInline = parseCaptureInline; exports.parseInline = parseInline; exports.parseSimpleInline = parseSimpleInline; exports.parseStyleAttribute = parseStyleAttribute; exports.parseTableAlign = parseTableAlign; exports.parseTableAlignCapture = parseTableAlignCapture; exports.parseTableCells = parseTableCells; exports.parseTableRow = parseTableRow; exports.qualifies = qualifies; exports.renderNothing = renderNothing; exports.sanitizer = sanitizer; exports.simpleInlineRegex = simpleInlineRegex; exports.slugify = slugify; exports.some = some; exports.startsWith = startsWith; exports.trimEnd = trimEnd; exports.trimLeadingWhitespaceOutsideFences = trimLeadingWhitespaceOutsideFences; exports.unescapeString = unescapeString; exports.unquote = unquote; //# sourceMappingURL=utils.cjs.map