refactor: modularize auth transport and markdown
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m4s
Testing / test (push) Successful in 1m22s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m7s
Package Extension / package-extension (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 1m5s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m4s
Testing / test (push) Successful in 1m22s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m7s
Package Extension / package-extension (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 1m5s
- Split auth into focused package modules for agent keys, file keys, signing, and post-quantum transport helpers while keeping the public browser_cli.auth import surface intact. - Move transport encoding internals into a package with separate codec and binary-hoisting helpers, preserving browser_cli.transport compatibility. - Extract remote TCP auth/socket helpers and serve challenge setup out of the runtime paths to make connection handling easier to reason about. - Move the extension markdown extractor into a dedicated content/markdown folder with separate root selection, code normalization, renderer, and utils. - Centralize CLI Rich rendering helpers for tab/window tree and table output, and add rendering tests for the shared builders. - Remove local typing ignores in SDK/decorator/script plumbing and bump the package and extension version to 0.15.3.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { normalizeCodeBlock } from './code';
|
||||
import {
|
||||
absoluteUrl,
|
||||
BLOCK_TAGS,
|
||||
collapseBlankLines,
|
||||
escapeMarkdown,
|
||||
escapeTableCell,
|
||||
isNoiseElement,
|
||||
normalizeInline,
|
||||
normalizeText,
|
||||
} from './utils';
|
||||
|
||||
function inlineText(node: Node): string {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return escapeMarkdown(node.textContent || "");
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
||||
if (isNoiseElement(node)) return "";
|
||||
|
||||
const el = node as HTMLElement;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === "br") return "\n";
|
||||
if (tag === "img") {
|
||||
const img = el as HTMLImageElement;
|
||||
const src = absoluteUrl(img.getAttribute("src"), img.src);
|
||||
if (!src) return "";
|
||||
const alt = normalizeText(img.getAttribute("alt") || "");
|
||||
return alt ? `` : ``;
|
||||
}
|
||||
if (tag === "a") {
|
||||
const text = normalizeInline(Array.from(el.childNodes).map(inlineText).join(""));
|
||||
const href = absoluteUrl(el.getAttribute("href"), (el as HTMLAnchorElement).href);
|
||||
if (!href) return text;
|
||||
return `[${text || href}](${href})`;
|
||||
}
|
||||
if (tag === "code") {
|
||||
const text = normalizeInline(Array.from(el.childNodes).map(inlineText).join(""));
|
||||
return text ? `\`${text.replace(/`/g, "\\`")}\`` : "";
|
||||
}
|
||||
if (tag === "strong" || tag === "b") {
|
||||
const text = normalizeInline(Array.from(el.childNodes).map(inlineText).join(""));
|
||||
return text ? `**${text}**` : "";
|
||||
}
|
||||
if (tag === "em" || tag === "i") {
|
||||
const text = normalizeInline(Array.from(el.childNodes).map(inlineText).join(""));
|
||||
return text ? `*${text}*` : "";
|
||||
}
|
||||
|
||||
const chunks: string[] = [];
|
||||
for (const child of el.childNodes) {
|
||||
const rendered = inlineText(child);
|
||||
if (!rendered) continue;
|
||||
chunks.push(rendered);
|
||||
if (child.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has((child as Element).tagName.toLowerCase())) {
|
||||
chunks.push("\n");
|
||||
}
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
function textBlock(node: Node): string {
|
||||
return collapseBlankLines(normalizeInline(Array.from(node.childNodes).map(inlineText).join("")));
|
||||
}
|
||||
|
||||
function preserveNodeText(node: Node): string {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent || "";
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
||||
|
||||
const el = node as HTMLElement;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === "br") return "\n";
|
||||
|
||||
const parts: string[] = [];
|
||||
for (const child of el.childNodes) {
|
||||
const rendered = preserveNodeText(child);
|
||||
if (!rendered) continue;
|
||||
parts.push(rendered);
|
||||
}
|
||||
|
||||
if (["div", "p", "li"].includes(tag)) {
|
||||
return `${parts.join("")}\n`;
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function tableToMarkdown(table: Element) {
|
||||
const rows = Array.from(table.querySelectorAll("tr"))
|
||||
.map(row => Array.from(row.children)
|
||||
.filter(cell => cell.tagName === "TD" || cell.tagName === "TH")
|
||||
.map(cell => escapeTableCell(textBlock(cell)))
|
||||
)
|
||||
.filter(cells => cells.length > 0);
|
||||
if (!rows.length) return "";
|
||||
|
||||
const widths = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
||||
const normalizedRows = rows.map(row => {
|
||||
const next = row.slice();
|
||||
while (next.length < widths) next.push("");
|
||||
return next;
|
||||
});
|
||||
|
||||
let headers = normalizedRows[0];
|
||||
let bodyRows = normalizedRows.slice(1);
|
||||
const firstRowIsBlank = headers.every(cell => !cell.trim());
|
||||
if (firstRowIsBlank && normalizedRows.length > 1) {
|
||||
headers = normalizedRows[1];
|
||||
bodyRows = normalizedRows.slice(2);
|
||||
}
|
||||
|
||||
const firstRow = table.querySelector("tr");
|
||||
const thead = table.querySelector("thead");
|
||||
const firstRowHasTh = firstRow && Array.from(firstRow.children).some(cell => cell.tagName === "TH");
|
||||
if (!(thead || firstRowHasTh || firstRowIsBlank)) {
|
||||
headers = new Array(widths).fill("");
|
||||
bodyRows = normalizedRows;
|
||||
}
|
||||
|
||||
const separator = new Array(widths).fill("---");
|
||||
const lines = [
|
||||
`| ${headers.join(" | ")} |`,
|
||||
`| ${separator.join(" | ")} |`,
|
||||
];
|
||||
for (const row of bodyRows) {
|
||||
lines.push(`| ${row.join(" | ")} |`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function listToMarkdown(list: Element, depth = 0): string {
|
||||
const ordered = list.tagName.toLowerCase() === "ol";
|
||||
const items: string[] = [];
|
||||
const children = Array.from(list.children).filter(child => child.tagName === "LI");
|
||||
children.forEach((item, index) => {
|
||||
const marker = ordered ? `${index + 1}. ` : "- ";
|
||||
const indent = " ".repeat(depth);
|
||||
const nested: string[] = [];
|
||||
const content: string[] = [];
|
||||
|
||||
for (const child of item.childNodes) {
|
||||
const childEl = child as Element;
|
||||
if (child.nodeType === Node.ELEMENT_NODE && (childEl.tagName === "UL" || childEl.tagName === "OL")) {
|
||||
nested.push(listToMarkdown(childEl, depth + 1));
|
||||
} else {
|
||||
content.push(inlineText(child));
|
||||
}
|
||||
}
|
||||
|
||||
const line = collapseBlankLines(normalizeInline(content.join("")));
|
||||
if (line) {
|
||||
const lineParts = line.split("\n");
|
||||
items.push(`${indent}${marker}${lineParts[0]}`);
|
||||
const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
|
||||
lineParts.slice(1).forEach(part => items.push(`${continuationIndent}${part}`));
|
||||
}
|
||||
nested.filter(Boolean).forEach(block => items.push(block));
|
||||
});
|
||||
return items.join("\n");
|
||||
}
|
||||
|
||||
function blockToMarkdown(node: Node): string {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return normalizeText(node.textContent || "");
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
||||
if (isNoiseElement(node)) return "";
|
||||
|
||||
const el = node as HTMLElement;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === "table") return tableToMarkdown(el);
|
||||
if (tag === "ul" || tag === "ol") return listToMarkdown(el);
|
||||
if (el.matches(".cm-editor[data-is-code-block-view='true']")) {
|
||||
const lines = Array.from(el.querySelectorAll(".cm-line")).map(line => {
|
||||
const text = preserveNodeText(line);
|
||||
return text === "\n" ? "" : text.replace(/\n$/, "");
|
||||
});
|
||||
const code = normalizeCodeBlock(lines.join("\n"));
|
||||
return code ? `\`\`\`\n${code}\n\`\`\`` : "";
|
||||
}
|
||||
if (tag === "pre") {
|
||||
const code = normalizeCodeBlock(preserveNodeText(el));
|
||||
return code ? `\`\`\`\n${code}\n\`\`\`` : "";
|
||||
}
|
||||
if (tag === "blockquote") {
|
||||
const content = collapseBlankLines(Array.from(el.childNodes).map(blockToMarkdown).join("\n\n"));
|
||||
return content
|
||||
.split("\n")
|
||||
.map(line => line ? `> ${line}` : ">")
|
||||
.join("\n");
|
||||
}
|
||||
if (/^h[1-6]$/.test(tag)) {
|
||||
const level = Number(tag.slice(1));
|
||||
const text = textBlock(el);
|
||||
return text ? `${"#".repeat(level)} ${text}` : "";
|
||||
}
|
||||
if (tag === "p" || tag === "figcaption") {
|
||||
return textBlock(el);
|
||||
}
|
||||
if (tag === "hr") {
|
||||
return "---";
|
||||
}
|
||||
if (tag === "img") {
|
||||
return inlineText(el);
|
||||
}
|
||||
|
||||
const childBlocks = Array.from(el.childNodes)
|
||||
.map(child => blockToMarkdown(child))
|
||||
.filter(Boolean);
|
||||
if (childBlocks.length) return collapseBlankLines(childBlocks.join("\n\n"));
|
||||
|
||||
return textBlock(node);
|
||||
}
|
||||
|
||||
export function renderMarkdown(root: Element): string {
|
||||
return collapseBlankLines(blockToMarkdown(root));
|
||||
}
|
||||
Reference in New Issue
Block a user