marked-caml
Version:
marked extension to add CAML (Colon Attribute Markup Language)
255 lines (229 loc) • 8.57 kB
JavaScript
// 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