@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
916 lines (913 loc) • 27.4 kB
JavaScript
import { K as logVerbose, P as resolveUserPath, Y as shouldLogVerbose } from "./registry-dD2_jBuv.js";
import { b as maxBytesForKind, c as hasAlphaChannel, d as detectMime, f as extensionForMime, l as optimizeImageToPng, o as convertHeicToJpeg, r as readLocalFileSafely, t as SafeOpenError, u as resizeToJpeg, x as mediaKindFromMime } from "./fs-safe-GrTh3Ydq.js";
import { a as chunkText } from "./chunk-YQGDBsHo.js";
import { i as fetchRemoteMedia, n as getDefaultMediaLocalRoots } from "./local-roots-BBJH5K5T.js";
import path from "node:path";
import fs from "node:fs/promises";
import { fileURLToPath } from "node:url";
import MarkdownIt from "markdown-it";
//#region src/web/media.ts
var LocalMediaAccessError = class extends Error {
constructor(code, message, options) {
super(message, options);
this.code = code;
this.name = "LocalMediaAccessError";
}
};
function getDefaultLocalRoots() {
return getDefaultMediaLocalRoots();
}
async function assertLocalMediaAllowed(mediaPath, localRoots) {
if (localRoots === "any") return;
const roots = localRoots ?? getDefaultLocalRoots();
let resolved;
try {
resolved = await fs.realpath(mediaPath);
} catch {
resolved = path.resolve(mediaPath);
}
if (localRoots === void 0) {
const workspaceRoot = roots.find((root) => path.basename(root) === "workspace");
if (workspaceRoot) {
const stateDir = path.dirname(workspaceRoot);
const rel = path.relative(stateDir, resolved);
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
if ((rel.split(path.sep)[0] ?? "").startsWith("workspace-")) throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`);
}
}
}
for (const root of roots) {
let resolvedRoot;
try {
resolvedRoot = await fs.realpath(root);
} catch {
resolvedRoot = path.resolve(root);
}
if (resolvedRoot === path.parse(resolvedRoot).root) throw new LocalMediaAccessError("invalid-root", `Invalid localRoots entry (refuses filesystem root): ${root}. Pass a narrower directory.`);
if (resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep)) return;
}
throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`);
}
const HEIC_MIME_RE = /^image\/hei[cf]$/i;
const HEIC_EXT_RE = /\.(heic|heif)$/i;
const MB = 1024 * 1024;
function formatMb(bytes, digits = 2) {
return (bytes / MB).toFixed(digits);
}
function formatCapLimit(label, cap, size) {
return `${label} exceeds ${formatMb(cap, 0)}MB limit (got ${formatMb(size)}MB)`;
}
function formatCapReduce(label, cap, size) {
return `${label} could not be reduced below ${formatMb(cap, 0)}MB (got ${formatMb(size)}MB)`;
}
function isHeicSource(opts) {
if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) return true;
if (opts.fileName && HEIC_EXT_RE.test(opts.fileName.trim())) return true;
return false;
}
function toJpegFileName(fileName) {
if (!fileName) return;
const trimmed = fileName.trim();
if (!trimmed) return fileName;
const parsed = path.parse(trimmed);
if (!parsed.ext || HEIC_EXT_RE.test(parsed.ext)) return path.format({
dir: parsed.dir,
name: parsed.name || trimmed,
ext: ".jpg"
});
return path.format({
dir: parsed.dir,
name: parsed.name,
ext: ".jpg"
});
}
function logOptimizedImage(params) {
if (!shouldLogVerbose()) return;
if (params.optimized.optimizedSize >= params.originalSize) return;
if (params.optimized.format === "png") {
logVerbose(`Optimized PNG (preserving alpha) from ${formatMb(params.originalSize)}MB to ${formatMb(params.optimized.optimizedSize)}MB (side≤${params.optimized.resizeSide}px)`);
return;
}
logVerbose(`Optimized media from ${formatMb(params.originalSize)}MB to ${formatMb(params.optimized.optimizedSize)}MB (side≤${params.optimized.resizeSide}px, q=${params.optimized.quality})`);
}
async function optimizeImageWithFallback(params) {
const { buffer, cap, meta } = params;
if ((meta?.contentType === "image/png" || meta?.fileName?.toLowerCase().endsWith(".png")) && await hasAlphaChannel(buffer)) {
const optimized = await optimizeImageToPng(buffer, cap);
if (optimized.buffer.length <= cap) return {
...optimized,
format: "png"
};
if (shouldLogVerbose()) logVerbose(`PNG with alpha still exceeds ${formatMb(cap, 0)}MB after optimization; falling back to JPEG`);
}
return {
...await optimizeImageToJpeg(buffer, cap, meta),
format: "jpeg"
};
}
async function loadWebMediaInternal(mediaUrl, options = {}) {
const { maxBytes, optimizeImages = true, ssrfPolicy, localRoots, sandboxValidated = false, readFile: readFileOverride } = options;
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
if (mediaUrl.startsWith("file://")) try {
mediaUrl = fileURLToPath(mediaUrl);
} catch {
throw new LocalMediaAccessError("invalid-file-url", `Invalid file:// URL: ${mediaUrl}`);
}
const optimizeAndClampImage = async (buffer, cap, meta) => {
const originalSize = buffer.length;
const optimized = await optimizeImageWithFallback({
buffer,
cap,
meta
});
logOptimizedImage({
originalSize,
optimized
});
if (optimized.buffer.length > cap) throw new Error(formatCapReduce("Media", cap, optimized.buffer.length));
const contentType = optimized.format === "png" ? "image/png" : "image/jpeg";
const fileName = optimized.format === "jpeg" && meta && isHeicSource(meta) ? toJpegFileName(meta.fileName) : meta?.fileName;
return {
buffer: optimized.buffer,
contentType,
kind: "image",
fileName
};
};
const clampAndFinalize = async (params) => {
const cap = maxBytes !== void 0 ? maxBytes : maxBytesForKind(params.kind);
if (params.kind === "image") {
const isGif = params.contentType === "image/gif";
if (isGif || !optimizeImages) {
if (params.buffer.length > cap) throw new Error(formatCapLimit(isGif ? "GIF" : "Media", cap, params.buffer.length));
return {
buffer: params.buffer,
contentType: params.contentType,
kind: params.kind,
fileName: params.fileName
};
}
return { ...await optimizeAndClampImage(params.buffer, cap, {
contentType: params.contentType,
fileName: params.fileName
}) };
}
if (params.buffer.length > cap) throw new Error(formatCapLimit("Media", cap, params.buffer.length));
return {
buffer: params.buffer,
contentType: params.contentType ?? void 0,
kind: params.kind,
fileName: params.fileName
};
};
if (/^https?:\/\//i.test(mediaUrl)) {
const defaultFetchCap = maxBytesForKind("unknown");
const { buffer, contentType, fileName } = await fetchRemoteMedia({
url: mediaUrl,
maxBytes: maxBytes === void 0 ? defaultFetchCap : optimizeImages ? Math.max(maxBytes, defaultFetchCap) : maxBytes,
ssrfPolicy
});
return await clampAndFinalize({
buffer,
contentType,
kind: mediaKindFromMime(contentType),
fileName
});
}
if (mediaUrl.startsWith("~")) mediaUrl = resolveUserPath(mediaUrl);
if ((sandboxValidated || localRoots === "any") && !readFileOverride) throw new LocalMediaAccessError("unsafe-bypass", "Refusing localRoots bypass without readFile override. Use sandboxValidated with readFile, or pass explicit localRoots.");
if (!(sandboxValidated || localRoots === "any")) await assertLocalMediaAllowed(mediaUrl, localRoots);
let data;
if (readFileOverride) data = await readFileOverride(mediaUrl);
else try {
data = (await readLocalFileSafely({ filePath: mediaUrl })).buffer;
} catch (err) {
if (err instanceof SafeOpenError) {
if (err.code === "not-found") throw new LocalMediaAccessError("not-found", `Local media file not found: ${mediaUrl}`, { cause: err });
if (err.code === "not-file") throw new LocalMediaAccessError("not-file", `Local media path is not a file: ${mediaUrl}`, { cause: err });
throw new LocalMediaAccessError("invalid-path", `Local media path is not safe to read: ${mediaUrl}`, { cause: err });
}
throw err;
}
const mime = await detectMime({
buffer: data,
filePath: mediaUrl
});
const kind = mediaKindFromMime(mime);
let fileName = path.basename(mediaUrl) || void 0;
if (fileName && !path.extname(fileName) && mime) {
const ext = extensionForMime(mime);
if (ext) fileName = `${fileName}${ext}`;
}
return await clampAndFinalize({
buffer: data,
contentType: mime,
kind,
fileName
});
}
async function loadWebMedia(mediaUrl, maxBytesOrOptions, options) {
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === void 0) return await loadWebMediaInternal(mediaUrl, {
maxBytes: maxBytesOrOptions,
optimizeImages: true,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots
});
return await loadWebMediaInternal(mediaUrl, {
...maxBytesOrOptions,
optimizeImages: maxBytesOrOptions.optimizeImages ?? true
});
}
async function loadWebMediaRaw(mediaUrl, maxBytesOrOptions, options) {
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === void 0) return await loadWebMediaInternal(mediaUrl, {
maxBytes: maxBytesOrOptions,
optimizeImages: false,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots
});
return await loadWebMediaInternal(mediaUrl, {
...maxBytesOrOptions,
optimizeImages: false
});
}
async function optimizeImageToJpeg(buffer, maxBytes, opts = {}) {
let source = buffer;
if (isHeicSource(opts)) try {
source = await convertHeicToJpeg(buffer);
} catch (err) {
throw new Error(`HEIC image conversion failed: ${String(err)}`, { cause: err });
}
const sides = [
2048,
1536,
1280,
1024,
800
];
const qualities = [
80,
70,
60,
50,
40
];
let smallest = null;
for (const side of sides) for (const quality of qualities) try {
const out = await resizeToJpeg({
buffer: source,
maxSide: side,
quality,
withoutEnlargement: true
});
const size = out.length;
if (!smallest || size < smallest.size) smallest = {
buffer: out,
size,
resizeSide: side,
quality
};
if (size <= maxBytes) return {
buffer: out,
optimizedSize: size,
resizeSide: side,
quality
};
} catch {}
if (smallest) return {
buffer: smallest.buffer,
optimizedSize: smallest.size,
resizeSide: smallest.resizeSide,
quality: smallest.quality
};
throw new Error("Failed to optimize image");
}
//#endregion
//#region src/markdown/ir.ts
function createMarkdownIt(options) {
const md = new MarkdownIt({
html: false,
linkify: options.linkify ?? true,
breaks: false,
typographer: false
});
md.enable("strikethrough");
if (options.tableMode && options.tableMode !== "off") md.enable("table");
else md.disable("table");
if (options.autolink === false) md.disable("autolink");
return md;
}
function getAttr(token, name) {
if (token.attrGet) return token.attrGet(name);
if (token.attrs) {
for (const [key, value] of token.attrs) if (key === name) return value;
}
return null;
}
function createTextToken(base, content) {
return {
...base,
type: "text",
content,
children: void 0
};
}
function applySpoilerTokens(tokens) {
for (const token of tokens) if (token.children && token.children.length > 0) token.children = injectSpoilersIntoInline(token.children);
}
function injectSpoilersIntoInline(tokens) {
const result = [];
const state = { spoilerOpen: false };
for (const token of tokens) {
if (token.type !== "text") {
result.push(token);
continue;
}
const content = token.content ?? "";
if (!content.includes("||")) {
result.push(token);
continue;
}
let index = 0;
while (index < content.length) {
const next = content.indexOf("||", index);
if (next === -1) {
if (index < content.length) result.push(createTextToken(token, content.slice(index)));
break;
}
if (next > index) result.push(createTextToken(token, content.slice(index, next)));
state.spoilerOpen = !state.spoilerOpen;
result.push({ type: state.spoilerOpen ? "spoiler_open" : "spoiler_close" });
index = next + 2;
}
}
return result;
}
function initRenderTarget() {
return {
text: "",
styles: [],
openStyles: [],
links: [],
linkStack: []
};
}
function resolveRenderTarget(state) {
return state.table?.currentCell ?? state;
}
function appendText(state, value) {
if (!value) return;
const target = resolveRenderTarget(state);
target.text += value;
}
function openStyle(state, style) {
const target = resolveRenderTarget(state);
target.openStyles.push({
style,
start: target.text.length
});
}
function closeStyle(state, style) {
const target = resolveRenderTarget(state);
for (let i = target.openStyles.length - 1; i >= 0; i -= 1) if (target.openStyles[i]?.style === style) {
const start = target.openStyles[i].start;
target.openStyles.splice(i, 1);
const end = target.text.length;
if (end > start) target.styles.push({
start,
end,
style
});
return;
}
}
function appendParagraphSeparator(state) {
if (state.env.listStack.length > 0) return;
if (state.table) return;
state.text += "\n\n";
}
function appendListPrefix(state) {
const stack = state.env.listStack;
const top = stack[stack.length - 1];
if (!top) return;
top.index += 1;
const indent = " ".repeat(Math.max(0, stack.length - 1));
const prefix = top.type === "ordered" ? `${top.index}. ` : "• ";
state.text += `${indent}${prefix}`;
}
function renderInlineCode(state, content) {
if (!content) return;
const target = resolveRenderTarget(state);
const start = target.text.length;
target.text += content;
target.styles.push({
start,
end: start + content.length,
style: "code"
});
}
function renderCodeBlock(state, content) {
let code = content ?? "";
if (!code.endsWith("\n")) code = `${code}\n`;
const target = resolveRenderTarget(state);
const start = target.text.length;
target.text += code;
target.styles.push({
start,
end: start + code.length,
style: "code_block"
});
if (state.env.listStack.length === 0) target.text += "\n";
}
function handleLinkClose(state) {
const target = resolveRenderTarget(state);
const link = target.linkStack.pop();
if (!link?.href) return;
const href = link.href.trim();
if (!href) return;
const start = link.labelStart;
const end = target.text.length;
if (end <= start) {
target.links.push({
start,
end,
href
});
return;
}
target.links.push({
start,
end,
href
});
}
function initTableState() {
return {
headers: [],
rows: [],
currentRow: [],
currentCell: null,
inHeader: false
};
}
function finishTableCell(cell) {
closeRemainingStyles(cell);
return {
text: cell.text,
styles: cell.styles,
links: cell.links
};
}
function trimCell(cell) {
const text = cell.text;
let start = 0;
let end = text.length;
while (start < end && /\s/.test(text[start] ?? "")) start += 1;
while (end > start && /\s/.test(text[end - 1] ?? "")) end -= 1;
if (start === 0 && end === text.length) return cell;
const trimmedText = text.slice(start, end);
const trimmedLength = trimmedText.length;
const trimmedStyles = [];
for (const span of cell.styles) {
const sliceStart = Math.max(0, span.start - start);
const sliceEnd = Math.min(trimmedLength, span.end - start);
if (sliceEnd > sliceStart) trimmedStyles.push({
start: sliceStart,
end: sliceEnd,
style: span.style
});
}
const trimmedLinks = [];
for (const span of cell.links) {
const sliceStart = Math.max(0, span.start - start);
const sliceEnd = Math.min(trimmedLength, span.end - start);
if (sliceEnd > sliceStart) trimmedLinks.push({
start: sliceStart,
end: sliceEnd,
href: span.href
});
}
return {
text: trimmedText,
styles: trimmedStyles,
links: trimmedLinks
};
}
function appendCell(state, cell) {
if (!cell.text) return;
const start = state.text.length;
state.text += cell.text;
for (const span of cell.styles) state.styles.push({
start: start + span.start,
end: start + span.end,
style: span.style
});
for (const link of cell.links) state.links.push({
start: start + link.start,
end: start + link.end,
href: link.href
});
}
function appendCellTextOnly(state, cell) {
if (!cell.text) return;
state.text += cell.text;
}
function renderTableAsBullets(state) {
if (!state.table) return;
const headers = state.table.headers.map(trimCell);
const rows = state.table.rows.map((row) => row.map(trimCell));
if (headers.length === 0 && rows.length === 0) return;
if (headers.length > 1 && rows.length > 0) for (const row of rows) {
if (row.length === 0) continue;
const rowLabel = row[0];
if (rowLabel?.text) {
const labelStart = state.text.length;
appendCell(state, rowLabel);
const labelEnd = state.text.length;
if (labelEnd > labelStart) state.styles.push({
start: labelStart,
end: labelEnd,
style: "bold"
});
state.text += "\n";
}
for (let i = 1; i < row.length; i++) {
const header = headers[i];
const value = row[i];
if (!value?.text) continue;
state.text += "• ";
if (header?.text) {
appendCell(state, header);
state.text += ": ";
} else state.text += `Column ${i}: `;
appendCell(state, value);
state.text += "\n";
}
state.text += "\n";
}
else for (const row of rows) {
for (let i = 0; i < row.length; i++) {
const header = headers[i];
const value = row[i];
if (!value?.text) continue;
state.text += "• ";
if (header?.text) {
appendCell(state, header);
state.text += ": ";
}
appendCell(state, value);
state.text += "\n";
}
state.text += "\n";
}
}
function renderTableAsCode(state) {
if (!state.table) return;
const headers = state.table.headers.map(trimCell);
const rows = state.table.rows.map((row) => row.map(trimCell));
const columnCount = Math.max(headers.length, ...rows.map((row) => row.length));
if (columnCount === 0) return;
const widths = Array.from({ length: columnCount }, () => 0);
const updateWidths = (cells) => {
for (let i = 0; i < columnCount; i += 1) {
const width = cells[i]?.text.length ?? 0;
if (widths[i] < width) widths[i] = width;
}
};
updateWidths(headers);
for (const row of rows) updateWidths(row);
const codeStart = state.text.length;
const appendRow = (cells) => {
state.text += "|";
for (let i = 0; i < columnCount; i += 1) {
state.text += " ";
const cell = cells[i];
if (cell) appendCellTextOnly(state, cell);
const pad = widths[i] - (cell?.text.length ?? 0);
if (pad > 0) state.text += " ".repeat(pad);
state.text += " |";
}
state.text += "\n";
};
const appendDivider = () => {
state.text += "|";
for (let i = 0; i < columnCount; i += 1) {
const dashCount = Math.max(3, widths[i]);
state.text += ` ${"-".repeat(dashCount)} |`;
}
state.text += "\n";
};
appendRow(headers);
appendDivider();
for (const row of rows) appendRow(row);
const codeEnd = state.text.length;
if (codeEnd > codeStart) state.styles.push({
start: codeStart,
end: codeEnd,
style: "code_block"
});
if (state.env.listStack.length === 0) state.text += "\n";
}
function renderTokens(tokens, state) {
for (const token of tokens) switch (token.type) {
case "inline":
if (token.children) renderTokens(token.children, state);
break;
case "text":
appendText(state, token.content ?? "");
break;
case "em_open":
openStyle(state, "italic");
break;
case "em_close":
closeStyle(state, "italic");
break;
case "strong_open":
openStyle(state, "bold");
break;
case "strong_close":
closeStyle(state, "bold");
break;
case "s_open":
openStyle(state, "strikethrough");
break;
case "s_close":
closeStyle(state, "strikethrough");
break;
case "code_inline":
renderInlineCode(state, token.content ?? "");
break;
case "spoiler_open":
if (state.enableSpoilers) openStyle(state, "spoiler");
break;
case "spoiler_close":
if (state.enableSpoilers) closeStyle(state, "spoiler");
break;
case "link_open": {
const href = getAttr(token, "href") ?? "";
const target = resolveRenderTarget(state);
target.linkStack.push({
href,
labelStart: target.text.length
});
break;
}
case "link_close":
handleLinkClose(state);
break;
case "image":
appendText(state, token.content ?? "");
break;
case "softbreak":
case "hardbreak":
appendText(state, "\n");
break;
case "paragraph_close":
appendParagraphSeparator(state);
break;
case "heading_open":
if (state.headingStyle === "bold") openStyle(state, "bold");
break;
case "heading_close":
if (state.headingStyle === "bold") closeStyle(state, "bold");
appendParagraphSeparator(state);
break;
case "blockquote_open":
if (state.blockquotePrefix) state.text += state.blockquotePrefix;
openStyle(state, "blockquote");
break;
case "blockquote_close":
closeStyle(state, "blockquote");
break;
case "bullet_list_open":
if (state.env.listStack.length > 0) state.text += "\n";
state.env.listStack.push({
type: "bullet",
index: 0
});
break;
case "bullet_list_close":
state.env.listStack.pop();
if (state.env.listStack.length === 0) state.text += "\n";
break;
case "ordered_list_open": {
if (state.env.listStack.length > 0) state.text += "\n";
const start = Number(getAttr(token, "start") ?? "1");
state.env.listStack.push({
type: "ordered",
index: start - 1
});
break;
}
case "ordered_list_close":
state.env.listStack.pop();
if (state.env.listStack.length === 0) state.text += "\n";
break;
case "list_item_open":
appendListPrefix(state);
break;
case "list_item_close":
if (!state.text.endsWith("\n")) state.text += "\n";
break;
case "code_block":
case "fence":
renderCodeBlock(state, token.content ?? "");
break;
case "html_block":
case "html_inline":
appendText(state, token.content ?? "");
break;
case "table_open":
if (state.tableMode !== "off") {
state.table = initTableState();
state.hasTables = true;
}
break;
case "table_close":
if (state.table) {
if (state.tableMode === "bullets") renderTableAsBullets(state);
else if (state.tableMode === "code") renderTableAsCode(state);
}
state.table = null;
break;
case "thead_open":
if (state.table) state.table.inHeader = true;
break;
case "thead_close":
if (state.table) state.table.inHeader = false;
break;
case "tbody_open":
case "tbody_close": break;
case "tr_open":
if (state.table) state.table.currentRow = [];
break;
case "tr_close":
if (state.table) {
if (state.table.inHeader) state.table.headers = state.table.currentRow;
else state.table.rows.push(state.table.currentRow);
state.table.currentRow = [];
}
break;
case "th_open":
case "td_open":
if (state.table) state.table.currentCell = initRenderTarget();
break;
case "th_close":
case "td_close":
if (state.table?.currentCell) {
state.table.currentRow.push(finishTableCell(state.table.currentCell));
state.table.currentCell = null;
}
break;
case "hr":
state.text += "───\n\n";
break;
default:
if (token.children) renderTokens(token.children, state);
break;
}
}
function closeRemainingStyles(target) {
for (let i = target.openStyles.length - 1; i >= 0; i -= 1) {
const open = target.openStyles[i];
const end = target.text.length;
if (end > open.start) target.styles.push({
start: open.start,
end,
style: open.style
});
}
target.openStyles = [];
}
function clampStyleSpans(spans, maxLength) {
const clamped = [];
for (const span of spans) {
const start = Math.max(0, Math.min(span.start, maxLength));
const end = Math.max(start, Math.min(span.end, maxLength));
if (end > start) clamped.push({
start,
end,
style: span.style
});
}
return clamped;
}
function clampLinkSpans(spans, maxLength) {
const clamped = [];
for (const span of spans) {
const start = Math.max(0, Math.min(span.start, maxLength));
const end = Math.max(start, Math.min(span.end, maxLength));
if (end > start) clamped.push({
start,
end,
href: span.href
});
}
return clamped;
}
function mergeStyleSpans(spans) {
const sorted = [...spans].toSorted((a, b) => {
if (a.start !== b.start) return a.start - b.start;
if (a.end !== b.end) return a.end - b.end;
return a.style.localeCompare(b.style);
});
const merged = [];
for (const span of sorted) {
const prev = merged[merged.length - 1];
if (prev && prev.style === span.style && (span.start < prev.end || span.start === prev.end && span.style !== "blockquote")) {
prev.end = Math.max(prev.end, span.end);
continue;
}
merged.push({ ...span });
}
return merged;
}
function sliceStyleSpans(spans, start, end) {
if (spans.length === 0) return [];
const sliced = [];
for (const span of spans) {
const sliceStart = Math.max(span.start, start);
const sliceEnd = Math.min(span.end, end);
if (sliceEnd > sliceStart) sliced.push({
start: sliceStart - start,
end: sliceEnd - start,
style: span.style
});
}
return mergeStyleSpans(sliced);
}
function sliceLinkSpans(spans, start, end) {
if (spans.length === 0) return [];
const sliced = [];
for (const span of spans) {
const sliceStart = Math.max(span.start, start);
const sliceEnd = Math.min(span.end, end);
if (sliceEnd > sliceStart) sliced.push({
start: sliceStart - start,
end: sliceEnd - start,
href: span.href
});
}
return sliced;
}
function markdownToIR(markdown, options = {}) {
return markdownToIRWithMeta(markdown, options).ir;
}
function markdownToIRWithMeta(markdown, options = {}) {
const env = { listStack: [] };
const tokens = createMarkdownIt(options).parse(markdown ?? "", env);
if (options.enableSpoilers) applySpoilerTokens(tokens);
const tableMode = options.tableMode ?? "off";
const state = {
text: "",
styles: [],
openStyles: [],
links: [],
linkStack: [],
env,
headingStyle: options.headingStyle ?? "none",
blockquotePrefix: options.blockquotePrefix ?? "",
enableSpoilers: options.enableSpoilers ?? false,
tableMode,
table: null,
hasTables: false
};
renderTokens(tokens, state);
closeRemainingStyles(state);
const trimmedLength = state.text.trimEnd().length;
let codeBlockEnd = 0;
for (const span of state.styles) {
if (span.style !== "code_block") continue;
if (span.end > codeBlockEnd) codeBlockEnd = span.end;
}
const finalLength = Math.max(trimmedLength, codeBlockEnd);
return {
ir: {
text: finalLength === state.text.length ? state.text : state.text.slice(0, finalLength),
styles: mergeStyleSpans(clampStyleSpans(state.styles, finalLength)),
links: clampLinkSpans(state.links, finalLength)
},
hasTables: state.hasTables
};
}
function chunkMarkdownIR(ir, limit) {
if (!ir.text) return [];
if (limit <= 0 || ir.text.length <= limit) return [ir];
const chunks = chunkText(ir.text, limit);
const results = [];
let cursor = 0;
chunks.forEach((chunk, index) => {
if (!chunk) return;
if (index > 0) while (cursor < ir.text.length && /\s/.test(ir.text[cursor] ?? "")) cursor += 1;
const start = cursor;
const end = Math.min(ir.text.length, start + chunk.length);
results.push({
text: chunk,
styles: sliceStyleSpans(ir.styles, start, end),
links: sliceLinkSpans(ir.links, start, end)
});
cursor = end;
});
return results;
}
//#endregion
export { loadWebMedia as a, getDefaultLocalRoots as i, markdownToIR as n, loadWebMediaRaw as o, markdownToIRWithMeta as r, chunkMarkdownIR as t };