protoml-parser
Version:
ProtoML is a lightweight, declarative markup language designed for writing and structuring meeting protocols, notes and task lists in a human-readable and machine-parseable format.
176 lines (156 loc) • 5.71 kB
JavaScript
function decodeEntities(text) {
return String(text || "")
.replace(/ /gi, " ")
.replace(/&/gi, "&")
.replace(/</gi, "<")
.replace(/>/gi, ">")
.replace(/"/gi, '"')
.replace(/'/gi, "'")
.replace(/'/gi, "'")
.replace(/…/gi, "...")
.replace(/–/gi, "-")
.replace(/—/gi, "-")
.replace(/€/gi, "EUR")
.replace(/ü/gi, "ü")
.replace(/Ü/gi, "Ü")
.replace(/ö/gi, "ö")
.replace(/Ö/gi, "Ö")
.replace(/ä/gi, "ä")
.replace(/Ä/gi, "Ä")
.replace(/ß/gi, "ß");
}
function renderTemplate(template, context = {}) {
return String(template || "").replace(/\{\{(.*?)\}\}/g, (_, key) => {
const normalizedKey = String(key).trim();
return context[normalizedKey] ?? "";
});
}
function isTruthySetting(value) {
const normalized = String(value == null ? "" : value).trim().toLowerCase();
return ["1", "true", "yes", "on", "hide", "hidden"].includes(normalized);
}
function shouldHideMeta(ast, options = {}) {
if (options.hideMeta) {
return true;
}
const meta = ast?.meta || {};
if (isTruthySetting(meta.hide_meta) || isTruthySetting(meta.hideMeta)) {
return true;
}
const outputMeta = String(meta.output_meta == null ? "" : meta.output_meta).trim().toLowerCase();
return ["hide", "hidden", "false", "none", "off"].includes(outputMeta);
}
function getVisibleMetaEntries(ast, options = {}) {
if (shouldHideMeta(ast, options)) {
return [];
}
return Object.entries(ast?.meta || {}).filter(([key]) =>
!["protocol", "meeting_title", "title", "theme", "hide_meta", "hideMeta", "output_meta"].includes(key)
);
}
function normalizeWhitespace(text) {
return String(text || "")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/[ \t]{2,}/g, " ")
.trim();
}
function stripHtml(text) {
return normalizeWhitespace(
decodeEntities(
String(text || "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p>/gi, "\n")
.replace(/<\/div>/gi, "\n")
.replace(/<\/section>/gi, "\n")
.replace(/<[^>]+>/g, "")
)
);
}
function convertHtmlToMarkdown(text) {
let output = String(text || "");
output = output
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<hr\s*\/?>/gi, "\n\n---\n\n")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, content) => {
const clean = stripHtml(content);
return clean ? `\n\n${"#".repeat(Number(level))} ${clean}\n\n` : "\n";
})
.replace(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, label) => {
const cleanLabel = stripHtml(label);
return cleanLabel ? `[${cleanLabel}](${href})` : href;
})
.replace(/<(b|strong)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, content) => `**${stripHtml(content)}**`)
.replace(/<(i|em)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, content) => `*${stripHtml(content)}*`)
.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
const clean = stripHtml(content);
if (!clean) return "";
return `\n\n> ${clean.split("\n").join("\n> ")}\n\n`;
})
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, content) => {
const clean = stripHtml(content);
return clean ? `- ${clean}\n` : "";
})
.replace(/<\/?(ul|ol|nav|section|article|div|p|table|thead|tbody|tr)[^>]*>/gi, "\n")
.replace(/<\/?(td|th)[^>]*>/gi, " ")
.replace(/<[^>]+>/g, "");
return normalizeWhitespace(decodeEntities(output));
}
function convertHtmlToText(text) {
let output = String(text || "");
output = output
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<hr\s*\/?>/gi, "\n\n--------------------\n\n")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, _level, content) => {
const clean = stripHtml(content);
return clean ? `\n\n${clean}\n\n` : "\n";
})
.replace(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, label) => {
const cleanLabel = stripHtml(label);
return cleanLabel ? `${cleanLabel} -> ${href}` : href;
})
.replace(/<(b|strong|i|em)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, content) => stripHtml(content))
.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
const clean = stripHtml(content);
if (!clean) return "";
return `\n\n> ${clean.split("\n").join("\n> ")}\n\n`;
})
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, content) => {
const clean = stripHtml(content);
return clean ? `- ${clean}\n` : "";
})
.replace(/<\/?(ul|ol|nav|section|article|div|p|table|thead|tbody|tr)[^>]*>/gi, "\n")
.replace(/<\/?(td|th)[^>]*>/gi, " ")
.replace(/<[^>]+>/g, "");
return normalizeWhitespace(decodeEntities(output));
}
function isProbablyHtml(text) {
return /<([a-z][a-z0-9-]*)(\s|>|\/)/i.test(String(text || ""));
}
function renderStructuredReference(entry, markdown = false) {
const match = String(entry || "").match(/^(.+?)\|(.+)$/);
if (!match) {
return stripHtml(entry);
}
const label = stripHtml(match[1].trim());
const target = match[2].trim();
return markdown ? `[${label}](${target})` : `${label} -> ${target}`;
}
module.exports = {
convertHtmlToMarkdown,
convertHtmlToText,
decodeEntities,
isProbablyHtml,
normalizeWhitespace,
renderStructuredReference,
renderTemplate,
getVisibleMetaEntries,
shouldHideMeta,
stripHtml,
};