/* Safe, format-neutral document parsing helpers. */
(function (root, factory) {
if (typeof module === "object" && module.exports) module.exports = factory();
else root.Documents = factory();
})(typeof self !== "undefined" ? self : globalThis, function () {
"use strict";
const SAFE_LINK = /^(https?:|mailto:)/i;
const SAFE_DATA_IMAGE = /^data:image\/(?:png|jpe?g|webp|gif|bmp);base64,/i;
function safeUrl(value) {
const url = String(value || "").trim();
return SAFE_LINK.test(url) ? url : null;
}
function decodeUtf8(buffer) {
try {
return new TextDecoder("utf-8", { fatal: true }).decode(buffer).replace(/^\uFEFF/, "");
} catch {
throw new Error("isn't valid UTF-8. Save it as UTF-8 and try again.");
}
}
function parseDelimited(text, delimiter) {
const rows = [];
let row = [];
let field = "";
let quoted = false;
for (let index = 0; index < text.length; index += 1) {
const char = text[index];
if (quoted) {
if (char === '"' && text[index + 1] === '"') {
field += '"';
index += 1;
} else if (char === '"') {
quoted = false;
} else {
field += char;
}
} else if (char === '"' && field.length === 0) {
quoted = true;
} else if (char === delimiter) {
row.push(field);
field = "";
} else if (char === "\n" || char === "\r") {
if (char === "\r" && text[index + 1] === "\n") index += 1;
row.push(field);
rows.push(row);
row = [];
field = "";
} else {
field += char;
}
}
if (quoted) throw new Error("has an unclosed quoted field.");
if (field.length || row.length || !rows.length) {
row.push(field);
rows.push(row);
}
return rows;
}
function detectDelimiter(text) {
const candidates = [",", "\t", ";", "|"];
let best = { delimiter: ",", score: -Infinity };
for (const delimiter of candidates) {
let rows;
try {
rows = parseDelimited(text.slice(0, 65536), delimiter).slice(0, 12);
} catch {
continue;
}
const counts = rows.filter((row) => row.some((cell) => cell !== "")).map((row) => row.length);
const common = counts.reduce((map, count) => map.set(count, (map.get(count) || 0) + 1), new Map());
const consistency = Math.max(0, ...common.values());
const width = Math.max(1, ...counts);
const score = width > 1 ? consistency * 100 + width : 0;
if (score > best.score) best = { delimiter, score };
}
return best.delimiter;
}
function parseCsv(text) {
let inQuotes = false;
for (let index = 0; index < text.length; index += 1) {
if (text[index] !== '"') continue;
if (inQuotes && text[index + 1] === '"') index += 1;
else inQuotes = !inQuotes;
}
if (inQuotes) throw new Error("has an unclosed quoted field.");
const delimiter = detectDelimiter(text);
const rows = parseDelimited(text, delimiter);
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
rows.forEach((row) => {
while (row.length < width) row.push("");
});
return { delimiter, rows, cells: rows.length * width };
}
function cleanText(value) {
return String(value || "")
.replace(/\u00a0/g, " ")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n[ \t]+/g, "\n")
.trim();
}
function htmlToBlocks(html) {
if (typeof DOMParser === "undefined") throw new Error("This browser cannot read document content.");
const parsed = new DOMParser().parseFromString(String(html || ""), "text/html");
const blocks = [];
const warnings = [];
function inline(node, style, into) {
const output = into || [];
const current = style || {};
if (node.nodeType === 3) {
if (node.nodeValue) output.push({ text: node.nodeValue, ...current });
return output;
}
if (node.nodeType !== 1) return output;
const tag = node.tagName.toLowerCase();
if (tag === "br") {
output.push({ text: "\n", ...current });
return output;
}
if (tag === "img") return output;
const next = { ...current };
if (tag === "strong" || tag === "b") next.bold = true;
if (tag === "em" || tag === "i") next.italic = true;
if (tag === "code") next.code = true;
if (tag === "s" || tag === "del" || tag === "strike") next.strike = true;
if (tag === "a") {
const href = safeUrl(node.getAttribute("href"));
if (href) next.link = href;
else if (node.getAttribute("href")) warnings.push(`Skipped an unsafe link: ${node.getAttribute("href")}`);
}
Array.from(node.childNodes).forEach((child) => inline(child, next, output));
return output;
}
function imagesFrom(element) {
return Array.from(element.querySelectorAll("img")).map((img) => {
const src = String(img.getAttribute("src") || "");
const alt = cleanText(img.getAttribute("alt") || "Image");
if (SAFE_DATA_IMAGE.test(src)) return { type: "image", src, alt };
const label = src ? `${alt} — ${src}` : alt;
warnings.push(`Did not load linked image: ${src || alt}`);
return { type: "paragraph", spans: [{ text: label, italic: true }] };
});
}
function addTextBlock(type, element, extra) {
const spans = inline(element).filter((span) => span.text);
const text = cleanText(spans.map((span) => span.text).join(""));
if (text) blocks.push({ type, spans, ...(extra || {}) });
blocks.push(...imagesFrom(element));
}
function walk(element) {
if (element.nodeType !== 1) return;
const tag = element.tagName.toLowerCase();
if (/^h[1-6]$/.test(tag)) return addTextBlock("heading", element, { level: Number(tag[1]) });
if (tag === "p") return addTextBlock("paragraph", element);
if (tag === "pre") {
const text = element.textContent || "";
if (text) blocks.push({ type: "code", text: text.replace(/\n$/, "") });
return;
}
if (tag === "blockquote") {
const text = cleanText(element.textContent);
if (text) blocks.push({ type: "quote", text });
return;
}
if (tag === "hr") {
blocks.push({ type: "rule" });
return;
}
if (tag === "img") {
blocks.push(...imagesFrom(element.parentElement || element));
return;
}
if (tag === "ul" || tag === "ol") {
const items = Array.from(element.children)
.filter((child) => child.tagName.toLowerCase() === "li")
.map((item) => {
const clone = item.cloneNode(true);
clone.querySelectorAll("ul,ol").forEach((nested) => nested.remove());
return cleanText(clone.textContent);
})
.filter(Boolean);
if (items.length) blocks.push({ type: "list", ordered: tag === "ol", items });
return;
}
if (tag === "table") {
const rows = Array.from(element.querySelectorAll("tr")).map((row) =>
Array.from(row.children)
.filter((cell) => /^(td|th)$/i.test(cell.tagName))
.map((cell) => ({
text: cleanText(cell.textContent),
header: cell.tagName.toLowerCase() === "th",
colSpan: Math.max(1, Number(cell.getAttribute("colspan")) || 1),
rowSpan: Math.max(1, Number(cell.getAttribute("rowspan")) || 1),
}))
);
if (rows.length) blocks.push({ type: "table", rows });
return;
}
if (["body", "main", "article", "section", "div", "figure", "figcaption"].includes(tag)) {
Array.from(element.children).forEach(walk);
} else if (cleanText(element.textContent)) {
addTextBlock("paragraph", element);
}
}
Array.from(parsed.body.children).forEach(walk);
return { blocks, warnings };
}
return {
SAFE_DATA_IMAGE,
safeUrl,
decodeUtf8,
detectDelimiter,
parseCsv,
htmlToBlocks,
};
});