// @ts-nocheck /** * @module research/extractor/html-to-content/html-utils * @description Research library module. */ /** * Converts URL-safe escaped HTML codes like &"'`’ & to standard HTML or in reverse. * @param {string} str - The string to process. * @param {boolean} toStandardHTML default=true - If true, converts url-safe codes * to standard HTML. If false, converts standard HTML to url-safe codes. * @return {string} The processed string. * @category HTML Utilities * @example * var normalHTML = convertURLSafeHTMLToHTML('<p>This & that © 2023 '+ * '"Quotes"'Apostrophes' €100 ☺</p>', true) * console.log(normalHTML) // "
This & that \u00a9 2023 "Quotes" 'Apostrophes' \u20ac100 \u263a
" */ export function convertURLSafeHTMLToHTML(str, toStandardHTML = true) { const entityMap = { "&": "&", "<": "<", ">": ">", '"': """, " ": " ", "'": "'", "`": "`", "\u00a2": "¢", "\u00a3": "£", "\u00a5": "¥", "\u20ac": "€", "\u00a9": "©", "\u00ae": "®", "\u2122": "™", }; // Add numeric character references for Latin-1 Supplement characters for (let i = 160; i <= 255; i++) { entityMap[String.fromCharCode(i)] = `${i};`; } if (toStandardHTML) { // Create a reverse mapping for unescaping const reverseEntityMap = Object.fromEntries( Object.entries(entityMap).map(([k, v]) => [v, k]) ); // Add alternative representations reverseEntityMap["'"] = "'"; reverseEntityMap["«"] = "\u00ab"; reverseEntityMap["»"] = "\u00bb"; // Regex to match all types of HTML entities const entityRegex = new RegExp( Object.keys(reverseEntityMap).join("|") + "|[0-9]+;|[0-9a-fA-F]+;", "g" ); str = str.replace(entityRegex, (entity) => { if (entity.startsWith("")) { // Convert hexadecimal numeric character reference return String.fromCharCode(parseInt(entity.slice(3, -1), 16)); } else if (entity.startsWith("")) { // Convert decimal numeric character reference return String.fromCharCode(parseInt(entity.slice(2, -1), 10)); } // Convert named entity return reverseEntityMap[entity] || entity; }); str = str.replace(/[\u0300-\u036f]/g, ""); //special chars return str; } else { // Regex to match all characters that need to be escaped const charRegex = new RegExp(`[${Object.keys(entityMap).join("")}]`, "g"); return str.replace(charRegex, (char) => entityMap[char]); } } /** * Convert relative URL to absolute URL using base URL. * @param {string} base base url of the domain * @param {string} relative partial urls like ../images/image.jpg #hash * @returns {string} absolute URL * @example * var absoluteURL = convertURLToAbsoluteURL('https://example.com', 'images/image.jpg') * console.log(absoluteURL) // Returns: "https://example.com/images/image.jpg" * var absoluteURL = convertURLToAbsoluteURL('https://example.com', '//images/image.jpg') * console.log(absoluteURL) // Returns: "https:images/image.jpg" * @category HTML Utilities * @author [vtempest (2025)](https://github.com/vtempest) */ export function convertURLToAbsoluteURL(base, relative) { // remove the %20 codes like data:image/svg+xml,%3Csvg%20x relative = decodeURI(relative); base = decodeURI(base); if ( relative.includes("data:") || relative.startsWith("#") || relative.startsWith("http") ) return relative; // Remove hash from base URL base = base.replace(/#.*$/, ""); // If relative URL starts with '//', add scheme from base if (relative.startsWith("//")) return base.split("://")[0] + ":" + relative; // If relative URL starts with '/', replace everything after the host in base if (relative[0] === "/") { const matchdomain = base.match(/^(https?:\/\/[^\/]+)/i); const domain = matchdomain ? matchdomain[1] : null; return domain + relative; } // Handle relative URLs if (relative.startsWith("../")) { base = base.replace(/\/[^\/]+$/, ""); while (relative.substring(0, 3) === "../") { relative = relative.substring(3); base = base.replace(/\/[^\/]+$/, ""); } relative = relative.replace(/^\.\//, ""); } // Combine base and relative // if (relative.startsWith("/")) { base = base.replace(/\/[^\/]+$/, ""); return base.replace(/\/+$/, "") + relative; } else { return base.split("/").slice(0, -1).join("/") + "/" + relative; } } import { marked } from "marked"; import Prism from "prismjs"; import "prismjs/components/prism-markup"; import "prismjs/components/prism-css"; import "prismjs/components/prism-javascript"; import "prismjs/components/prism-typescript"; import "prismjs/components/prism-jsx"; import "prismjs/components/prism-tsx"; import "prismjs/components/prism-python"; import "prismjs/components/prism-bash"; import "prismjs/components/prism-json"; import "prismjs/components/prism-yaml"; import "prismjs/components/prism-markdown"; import "prismjs/components/prism-sql"; import "prismjs/components/prism-rust"; import "prismjs/components/prism-go"; import "prismjs/components/prism-java"; import "prismjs/components/prism-c"; import "prismjs/components/prism-cpp"; // Configure marked once at module load with Prism.js syntax highlighting. // marked v17 removed the `highlight` option from setOptions, so highlighting // is wired via a custom `code` renderer instead. marked.use({ renderer: { code({ text, lang }) { const language = lang && Prism.languages[lang] ? lang : null; const highlighted = language ? Prism.highlight(text, Prism.languages[language], language) : text .replace(/&/g, "&") .replace(//g, ">"); const cls = language ? ` class="language-${language}"` : ""; return `${highlighted}\n`;
},
},
});
/**
* Converts Markdown text to HTML. It handles the following Markdown elements:
* - Headers (h1 to h6)
* - Bold text
* - Italic text
* - Unordered lists
* - Ordered lists
* - Paragraphs
* - Images
* - Links
* - Code blocks
* @param {string} content - The Markdown or HTML content to be converted.
* @param {boolean} toHtml - default=true - If true, converts Markdown to HTML.
* If false, converts HTML to Markdown.
* @returns {string} The resulting HTML string.
* @category HTML Utilities
* @example
* const markdown = "# Header\n\nThis is **bold** and *italic* text.\n\n* List item 1\n* List item 2";
* const html = convertMarkdownToHTML(markdown);
* console.log(html);
* // Output:
* // This is bold and italic text.
* //Some bold text.
" */ export function convertMarkdownToFormattedHTML(markdown) { if (!markdown || typeof markdown !== "string") return ""; let text = markdown.replace(/\r\n?/g, "\n"); // 1. Pull fenced code blocks out first so their contents are never parsed // as Markdown. Each is replaced by a placeholder restored at the end. const codeBlocks = []; text = text.replace( /```([^\n`]*)\n([\s\S]*?)```/g, (_m, lang, code) => { const language = (lang || "").trim(); const cls = language ? ` class="language-${language}"` : ""; const body = escapeHTMLChars(code.replace(/\n$/, "")); codeBlocks.push(`${body}`);
return `\u0000CB${codeBlocks.length - 1}\u0000`;
}
);
// 2. Pull inline code spans out next for the same reason.
const inlineCodes = [];
text = text.replace(/`([^`\n]+)`/g, (_m, code) => {
inlineCodes.push(`${escapeHTMLChars(code)}`);
return `\u0000IC${inlineCodes.length - 1}\u0000`;
});
const lines = text.split("\n");
const out = [];
let inUl = false;
let inOl = false;
let inBlockquote = false;
let paragraph = [];
const flushParagraph = () => {
if (paragraph.length) {
out.push(`${applyInlineMarkdown(paragraph.join(" "))}
`); paragraph = []; } }; const closeLists = () => { if (inUl) { out.push(""); inUl = false; } if (inOl) { out.push(""); inOl = false; } }; const closeBlockquote = () => { if (inBlockquote) { out.push(""); inBlockquote = false; } }; for (const line of lines) { // Standalone fenced-code-block placeholder line const cb = line.match(/^\u0000CB(\d+)\u0000$/); if (cb) { flushParagraph(); closeLists(); closeBlockquote(); out.push(codeBlocks[Number(cb[1])]); continue; } // Blank line closes open blocks if (/^\s*$/.test(line)) { flushParagraph(); closeLists(); closeBlockquote(); continue; } // Horizontal rule: ---, ***, ___ (3+) if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line)) { flushParagraph(); closeLists(); closeBlockquote(); out.push(""); inBlockquote = true; } out.push(`${applyInlineMarkdown(bq[1])}
`); continue; } closeBlockquote(); // Unordered list item: -, *, + const ul = line.match(/^\s*[-*+]\s+(.+)$/); if (ul) { flushParagraph(); if (inOl) { out.push(""); inOl = false; } if (!inUl) { out.push(""); inUl = true; } out.push(`
"); inUl = false; } if (!inOl) { out.push("- ${applyInlineMarkdown(ul[1])}
`); continue; } // Ordered list item: 1. or 1) const ol = line.match(/^\s*\d+[.)]\s+(.+)$/); if (ol) { flushParagraph(); if (inUl) { out.push(""); inOl = true; } out.push(`
- ${applyInlineMarkdown(ol[1])}
`); continue; } // Otherwise accumulate into the current paragraph closeLists(); paragraph.push(line.trim()); } flushParagraph(); closeLists(); closeBlockquote(); let html = out.join("\n"); // Restore inline code placeholders html = html.replace(/\u0000IC(\d+)\u0000/g, (_m, i) => inlineCodes[Number(i)]); return html.trim(); } export function convertHTMLToMarkdown(html) { var markdown = html // Convert headers .replace(/(.*?)<\/h[1-6]>/g, (match, level, content) => { return "#".repeat(parseInt(level)) + " " + content.trim() + "\n\n"; }) // Convert bold text .replace(/(.*?)<\/strong>/g, "**$1**") .replace(/(.*?)<\/b>/g, "**$1**") // Convert italic text .replace(/(.*?)<\/em>/g, "*$1*") // Convert unordered lists .replace(/ (.*?)<\/ul>/gs, (match, content) => { return content.replace(/
- (.*?)<\/li>/g, "* $1\n") + "\n"; }) // Convert ordered lists .replace(/
(.*?)<\/ol>/gs, (match, content) => { let index = 1; return ( content.replace(/
- (.*?)<\/li>/g, () => `${index++}. $1\n`) + "\n" ); }) // Convert paragraphs .replace(/