UNPKG

marked-caml

Version:

marked extension to add CAML (Colon Attribute Markup Language)

255 lines (229 loc) 8.57 kB
// marked-caml v0.0.1 - https://github.com/caml-mkdn/marked-caml.git import { merge } from 'lodash'; import * as CAML from 'caml-mkdn'; const attributeCollection = {}; function caml() { let opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Helper to add attributes to collection function addToCollection(key, value) { if (!attributeCollection[key]) { attributeCollection[key] = []; } attributeCollection[key].push(value); // Add metadata if callback provided if (opts.addAttr) { opts.addAttr(key, value.string); } } // Render the attribute box function renderAttributeBox() { var _opts$attrs; if (Object.keys(attributeCollection).length === 0) { return ''; } const cssNames = opts.cssNames || {}; let attrboxHtml = `<aside class="${cssNames.attrbox || 'attrbox'}">\n`; attrboxHtml += `<span class="${cssNames.attrboxTitle || 'attrbox-title'}">${((_opts$attrs = opts.attrs) === null || _opts$attrs === void 0 ? void 0 : _opts$attrs.title) || 'Attributes'}</span>\n`; attrboxHtml += '<dl>\n'; // Render each attribute type and its values for (const key in attributeCollection) { attrboxHtml += `<dt>${key}</dt>\n`; for (const item of attributeCollection[key]) { const keySlug = key.trim().toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); attrboxHtml += `<dd><span class="${cssNames.attr || 'attr'} ${item.type} ${keySlug}">${item.string}</span></dd>\n`; } } attrboxHtml += '</dl>\n</aside>\n'; return attrboxHtml; } /** * Checks if a matched text is in a valid context for wiki attributes * (top of the document: not inside a list item, blockquote, code block, etc.) */ function isTop(content, position, matchedText) { // If position is invalid, return false if (position < 0 || position >= content.length) { return false; } // Get the first line of the matched text (this is what we check for context patterns) const firstLine = matchedText.split('\n')[0]; // Check for invalid contexts in the first line // Bullet list item (- * +) if (/^ *[-*+]\s/.test(firstLine)) { return false; } // Numbered list item (1. 2. etc) if (/^ *\d+[.)]\s/.test(firstLine)) { return false; } // Blockquote if (/^ *>\s/.test(firstLine)) { return false; } // Indented code block (4+ spaces or tab) if (/^ {4,}|\t/.test(firstLine)) { return false; } // Fenced code block (```) if (content.includes('```')) { const codeFenceStr = '```'; // Simplify for reliability const beforeMatch = content.substring(0, position).lastIndexOf(codeFenceStr); if (beforeMatch >= 0) { const afterMatch = content.indexOf(codeFenceStr, position); // If we found an opening ``` before and a closing ``` after if (afterMatch > position) { // Simple check: odd number of ``` before position means we're in a code block const blockMarkers = (content.substring(0, position).match(/```/g) || []).length; if (blockMarkers % 2 !== 0) { return false; } } } } // Code span (inline code with backticks) if (content.includes('`')) { const charsBeforePosition = content.substring(0, position); const backticksBefore = (charsBeforePosition.match(/`/g) || []).length; const charsAfterPosition = content.substring(position); const backticksAfter = (charsAfterPosition.match(/`/g) || []).length; // If we have an odd number before and at least one after, we're in a code span if (backticksBefore % 2 !== 0 && backticksAfter > 0) { return false; } } return true; } return { hooks: { preprocess(markdown) { // Clear the attribute collection Object.keys(attributeCollection).forEach(key => { delete attributeCollection[key]; }); // Find all CAML attributes const lines = markdown.split('\n'); const modified = []; let i = 0; while (i < lines.length) { const line = lines[i]; // Match CAML attribute line using the package's regex const match = CAML.RGX.LINE.KEY.exec(line); if (match) { // Check if this is a valid context for attributes if (isTop(markdown, markdown.indexOf(line), line)) { const key = match[1].trim(); const value = match[2]; let valuesFound = false; // Process the value if (value && value.trim() !== '') { // Parse the value respecting quotes const items = parseCommaSeparatedValues(value); if (items.length > 0) { valuesFound = true; for (const item of items) { addToCollection(key, CAML.resolve(item.trim())); } } } else { // Check for list items following this attribute definition let j = i + 1; let foundListItems = false; while (j < lines.length) { const nextLine = lines[j]; // Check if line is a list item const listMatch = /^\s*[-*+]\s+(.+)$/.exec(nextLine); if (listMatch) { foundListItems = true; valuesFound = true; // Add list item as a value for this attribute addToCollection(key, CAML.resolve(listMatch[1].trim())); j++; } else if (nextLine.trim() === '') { // Empty line terminates the list j++; break; } else { // Not a list item or empty line, end of list break; } } if (foundListItems) { // Skip the lines we processed i = j - 1; } } // If we found values, skip this line; otherwise keep it if (valuesFound) { i++; continue; } } } // Keep all other lines modified.push(line); i++; } return modified.join('\n'); }, postprocess(html) { var _opts$cssNames; // Render attribute box const attrboxHtml = renderAttributeBox(); const hasAttrbox = html.includes(`class="${((_opts$cssNames = opts.cssNames) === null || _opts$cssNames === void 0 ? void 0 : _opts$cssNames.attrbox) || 'attrbox'}"`); const doRender = !!(opts.attrs && opts.attrs.render !== false); // No need to "restore" anything since we didn't escape in the first place return doRender && attrboxHtml && !hasAttrbox ? attrboxHtml + html : html; } } }; } // Helper function to parse comma-separated values while preserving quotes function parseCommaSeparatedValues(input) { const result = []; let current = ''; let inDoubleQuote = false; let inSingleQuote = false; for (let i = 0; i < input.length; i++) { const char = input[i]; // Handle quotes if (char === '"' && (i === 0 || input[i - 1] !== '\\')) { inDoubleQuote = !inDoubleQuote; current += char; // Keep the quote in the output } else if (char === '\'' && (i === 0 || input[i - 1] !== '\\')) { inSingleQuote = !inSingleQuote; current += char; // Keep the quote in the output // Handle commas (only split if not in quotes) } else if (char === ',' && !inDoubleQuote && !inSingleQuote) { // End of a value result.push(current.trim()); current = ''; } else { current += char; } } // Add the last value if (current.trim() !== '') { result.push(current.trim()); } return result; } function camlExtension() { let opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Set default options const defaults = { attrs: { render: true, title: 'Attributes' }, cssNames: { attrbox: 'attrbox', attrboxTitle: 'attrbox-title', attr: 'attr' } }; const fullOpts = merge({}, defaults, opts); const extension = caml(fullOpts); return extension; } export { camlExtension as default }; //# sourceMappingURL=index.umd.js.map