openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
195 lines (194 loc) • 6.77 kB
JavaScript
//#region packages/markdown-core/src/html-scanner.ts
const RAW_TEXT_TAGS = /* @__PURE__ */ new Set([
"script",
"style",
"noscript"
]);
function isAsciiWhitespace(value) {
return value === " " || value === "\n" || value === "\r" || value === " " || value === "\f";
}
function isTagNameChar(value) {
const code = value.charCodeAt(0);
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || value === "." || value === "-" || value === "_" || value === ":";
}
function isTagNameStartChar(value) {
const code = value.charCodeAt(0);
return code >= 65 && code <= 90 || code >= 97 && code <= 122;
}
function isTagBoundary(value) {
return !value || isAsciiWhitespace(value) || value === ">" || value === "/";
}
function asciiLower(value) {
const code = value.charCodeAt(0);
return code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : value;
}
function startsWithClosingTag(html, start, tagName) {
if (html[start] !== "<" || html[start + 1] !== "/") return false;
for (let offset = 0; offset < tagName.length; offset += 1) if (asciiLower(html[start + 2 + offset] ?? "") !== tagName[offset]) return false;
return isTagBoundary(html[start + 2 + tagName.length]);
}
function readRawTextOpenTagName(html, start) {
if (html[start] !== "<" || html[start + 1] === "/") return;
for (const tagName of RAW_TEXT_TAGS) {
let matches = true;
for (let offset = 0; offset < tagName.length; offset += 1) if (asciiLower(html[start + 1 + offset] ?? "") !== tagName[offset]) {
matches = false;
break;
}
if (matches && isTagBoundary(html[start + 1 + tagName.length])) return tagName;
}
}
function findRawTextOpenTagStart(html, start, end) {
const span = html.slice(start, end);
for (let offset = span.indexOf("<"); offset !== -1; offset = span.indexOf("<", offset + 1)) {
const i = start + offset;
if (readRawTextOpenTagName(html, i)) return i;
}
return -1;
}
function startsLikeHtmlTag(html, start) {
const next = html[start + 1];
return next === "!" || next === "?" || next === "/" || isTagNameStartChar(next ?? "");
}
function findTagEnd(html, start, mode = "render") {
const rendering = mode === "render";
let afterEquals = false;
let rawTextStartInQuote;
for (let i = start + 1; i < html.length; i += 1) {
const ch = html.charAt(i);
if (afterEquals && isAsciiWhitespace(ch)) continue;
if ((!rendering || afterEquals) && (ch === "\"" || ch === "'")) {
const quoteEnd = html.indexOf(ch, i + 1);
if (rendering && rawTextStartInQuote === void 0) {
const rawTextStart = findRawTextOpenTagStart(html, i + 1, quoteEnd === -1 ? html.length : quoteEnd);
if (rawTextStart !== -1) rawTextStartInQuote = rawTextStart;
}
if (quoteEnd === -1) return {
end: -1,
rawTextStart: rawTextStartInQuote
};
i = quoteEnd;
afterEquals = false;
continue;
}
afterEquals = false;
if (rendering && readRawTextOpenTagName(html, i)) return {
end: -1,
rawTextStart: i
};
if (ch === ">") return { end: i };
if (ch === "=") afterEquals = true;
}
return {
end: -1,
rawTextStart: rawTextStartInQuote
};
}
function isSelfClosingTagRaw(raw) {
const trimmed = raw.trimEnd();
if (!trimmed.endsWith("/")) return false;
const beforeSlash = trimmed.charAt(trimmed.length - 2);
return !beforeSlash || isAsciiWhitespace(beforeSlash) || beforeSlash === "\"" || beforeSlash === "'" || !/[ \n\r\t\f]/.test(trimmed.slice(0, -1));
}
function skipHtmlComment(html, start) {
if (html[start + 4] === ">") return start + 5;
if (html.startsWith("->", start + 4)) return start + 6;
for (let end = html.indexOf("--", start + 4); end !== -1; end = html.indexOf("--", end + 1)) {
const bracket = html[end + 2] === "!" ? end + 3 : end + 2;
if (html[bracket] === ">") return bracket + 1;
}
return html.length;
}
function readTagToken(html, start, mode = "render") {
const rendering = mode === "render";
if (rendering && html.startsWith("<!--", start)) return {
token: null,
next: skipHtmlComment(html, start)
};
const tagEnd = findTagEnd(html, start, mode);
const end = tagEnd.end;
if (end === -1) return tagEnd.rawTextStart === void 0 ? null : {
token: null,
next: tagEnd.rawTextStart
};
const raw = html.slice(start + 1, end);
let pos = 0;
const closing = raw[pos] === "/";
if (closing) pos += 1;
if (!isTagNameStartChar(raw[pos] ?? "")) return {
token: null,
next: end + 1
};
const nameStart = pos;
while (pos < raw.length && !isAsciiWhitespace(raw.charAt(pos)) && raw[pos] !== "/" && raw[pos] !== ">") pos += 1;
const attrs = closing ? "" : raw.slice(pos);
return {
token: {
closing,
name: raw.slice(nameStart, pos).replace(/\0|[A-Z]/g, (ch) => ch === "\0" ? "�" : asciiLower(ch)),
raw,
attrs,
selfClosing: rendering ? isSelfClosingTagRaw(raw) : !closing && attrs.trimEnd().endsWith("/")
},
next: end + 1
};
}
function readRawTextBounds(html, tagName, contentStart, mode = "render") {
if (tagName === "script") {
let state = "data";
for (let cursor = contentStart; cursor < html.length; cursor += 1) {
if (state === "data" && html.startsWith("<!--", cursor)) {
state = "escaped";
cursor += 1;
continue;
}
if (state !== "data" && html.startsWith("-->", cursor)) {
state = "data";
cursor += 2;
continue;
}
if (html[cursor] !== "<") continue;
if (startsWithClosingTag(html, cursor, tagName)) {
if (state === "double-escaped") {
state = "escaped";
cursor += tagName.length + 1;
continue;
}
const closeEnd = findTagEnd(html, cursor, mode).end;
return {
contentEnd: cursor,
end: closeEnd === -1 ? html.length : closeEnd + 1
};
}
if (state === "escaped" && readRawTextOpenTagName(html, cursor) === "script") {
state = "double-escaped";
cursor += tagName.length;
}
}
return {
contentEnd: html.length,
end: html.length
};
}
let closeStart = html.indexOf("</", contentStart);
while (closeStart !== -1) {
if (startsWithClosingTag(html, closeStart, tagName)) {
const closeEnd = findTagEnd(html, closeStart, mode).end;
return {
contentEnd: closeStart,
end: closeEnd === -1 ? html.length : closeEnd + 1
};
}
closeStart = html.indexOf("</", closeStart + 2);
}
return {
contentEnd: html.length,
end: html.length
};
}
function skipRawTextElement(html, start, tagName) {
const openerEnd = findTagEnd(html, start);
return readRawTextBounds(html, tagName, openerEnd.end === -1 ? start + tagName.length + 1 : openerEnd.end + 1).end;
}
//#endregion
export { readRawTextBounds as a, skipHtmlComment as c, isTagNameChar as i, skipRawTextElement as l, findRawTextOpenTagStart as n, readRawTextOpenTagName as o, isAsciiWhitespace as r, readTagToken as s, RAW_TEXT_TAGS as t, startsLikeHtmlTag as u };