@oh-my-pi/pi-coding-agent 16.0.7 → 16.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/cli.js +4752 -12462
- package/dist/types/cli/update-cli.d.ts +11 -0
- package/dist/types/debug/remote-debugger.d.ts +45 -0
- package/dist/types/internal-urls/docs-index.d.ts +19 -0
- package/dist/types/markit/converters/docx.d.ts +6 -0
- package/dist/types/markit/converters/epub.d.ts +15 -0
- package/dist/types/markit/converters/pdf/columns.d.ts +35 -0
- package/dist/types/markit/converters/pdf/extract.d.ts +10 -0
- package/dist/types/markit/converters/pdf/grid.d.ts +25 -0
- package/dist/types/markit/converters/pdf/headers.d.ts +24 -0
- package/dist/types/markit/converters/pdf/index.d.ts +6 -0
- package/dist/types/markit/converters/pdf/render.d.ts +24 -0
- package/dist/types/markit/converters/pdf/types.d.ts +75 -0
- package/dist/types/markit/converters/pptx.d.ts +57 -0
- package/dist/types/markit/converters/xlsx.d.ts +25 -0
- package/dist/types/markit/index.d.ts +2 -0
- package/dist/types/markit/registry.d.ts +16 -0
- package/dist/types/markit/types.d.ts +30 -0
- package/dist/types/session/agent-session.d.ts +7 -8
- package/dist/types/session/auth-storage.d.ts +3 -2
- package/dist/types/session/yield-queue.d.ts +3 -1
- package/dist/types/tools/browser/attach.d.ts +1 -1
- package/dist/types/utils/markit.d.ts +0 -8
- package/dist/types/utils/mupdf-wasm-embed.d.ts +1 -0
- package/dist/types/utils/turndown.d.ts +15 -0
- package/dist/types/utils/zip.d.ts +119 -0
- package/package.json +20 -18
- package/scripts/build-binary.ts +7 -3
- package/scripts/bundle-dist.ts +28 -12
- package/scripts/embed-mupdf-wasm.ts +67 -0
- package/scripts/generate-docs-index.ts +48 -32
- package/scripts/omp +1 -1
- package/src/advisor/__tests__/advisor.test.ts +83 -0
- package/src/advisor/runtime.ts +16 -1
- package/src/cli/auth-broker-cli.ts +1 -3
- package/src/cli/auth-gateway-cli.ts +2 -5
- package/src/cli/update-cli.ts +63 -3
- package/src/config/model-discovery.ts +20 -8
- package/src/config/models-config-schema.ts +8 -1
- package/src/debug/index.ts +44 -0
- package/src/debug/remote-debugger.ts +151 -0
- package/src/debug/report-bundle.ts +2 -1
- package/src/internal-urls/docs-index.generated.txt +2 -0
- package/src/internal-urls/docs-index.ts +102 -0
- package/src/internal-urls/omp-protocol.ts +10 -9
- package/src/markit/NOTICE +32 -0
- package/src/markit/converters/docx.ts +56 -0
- package/src/markit/converters/epub.ts +136 -0
- package/src/markit/converters/mammoth.d.ts +24 -0
- package/src/markit/converters/pdf/columns.ts +103 -0
- package/src/markit/converters/pdf/extract.ts +574 -0
- package/src/markit/converters/pdf/grid.ts +780 -0
- package/src/markit/converters/pdf/headers.ts +106 -0
- package/src/markit/converters/pdf/index.ts +146 -0
- package/src/markit/converters/pdf/render.ts +501 -0
- package/src/markit/converters/pdf/types.ts +84 -0
- package/src/markit/converters/pptx.ts +325 -0
- package/src/markit/converters/xlsx.ts +173 -0
- package/src/markit/index.ts +2 -0
- package/src/markit/registry.ts +59 -0
- package/src/markit/types.ts +35 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +14 -7
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/controllers/input-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +26 -9
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +5 -9
- package/src/session/agent-session.ts +62 -40
- package/src/session/auth-storage.ts +2 -11
- package/src/session/yield-queue.ts +7 -1
- package/src/tools/browser/attach.ts +2 -2
- package/src/tools/fetch.ts +25 -60
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +1 -6
- package/src/tools/write.ts +25 -65
- package/src/utils/markit.ts +25 -9
- package/src/utils/mupdf-wasm-embed.ts +12 -0
- package/src/utils/tools-manager.ts +2 -11
- package/src/utils/turndown.ts +83 -0
- package/src/{tools/archive-reader.ts → utils/zip.ts} +453 -83
- package/src/web/scrapers/types.ts +3 -46
- package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
- package/dist/types/tools/archive-reader.d.ts +0 -49
- package/src/internal-urls/docs-index.generated.ts +0 -120
package/src/tools/write.ts
CHANGED
|
@@ -20,8 +20,14 @@ import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
|
20
20
|
import type { ToolSession } from "../sdk";
|
|
21
21
|
import { fileHyperlink, framedBlock, renderStatusLine } from "../tui";
|
|
22
22
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
23
|
+
import {
|
|
24
|
+
type ArchiveMemberContent,
|
|
25
|
+
archiveFormatFromPath,
|
|
26
|
+
parseArchivePathCandidates,
|
|
27
|
+
readArchiveEntries,
|
|
28
|
+
writeArchive,
|
|
29
|
+
} from "../utils/zip";
|
|
23
30
|
import { truncateForPrompt } from "./approval";
|
|
24
|
-
import { parseArchivePathCandidates } from "./archive-reader";
|
|
25
31
|
import { assertEditableFile } from "./auto-generated-guard";
|
|
26
32
|
import {
|
|
27
33
|
type ConflictEntry,
|
|
@@ -65,12 +71,6 @@ import { toolResult } from "./tool-result";
|
|
|
65
71
|
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
66
72
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
67
73
|
|
|
68
|
-
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
69
|
-
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
70
|
-
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
71
|
-
return fflateModulePromise;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
74
|
const writeSchema = type({
|
|
75
75
|
path: type("string").describe("file path"),
|
|
76
76
|
content: type("string").describe("file content"),
|
|
@@ -369,9 +369,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
369
369
|
const finalPath = resolvedArchivePath.exists
|
|
370
370
|
? await fs.realpath(resolvedArchivePath.absolutePath).catch(() => resolvedArchivePath.absolutePath)
|
|
371
371
|
: resolvedArchivePath.absolutePath;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
// A realpath swap can land on a name without an archive extension; a
|
|
373
|
+
// whole-archive rewrite then defaults to an uncompressed tar, matching the
|
|
374
|
+
// previous `isZip`/`isGzip`/else fallthrough.
|
|
375
|
+
const format = archiveFormatFromPath(finalPath) ?? "tar";
|
|
375
376
|
// Rewrites are whole-archive: write to a temp file and rename so a
|
|
376
377
|
// crash/disk-full mid-write can't destroy the original archive.
|
|
377
378
|
const tmpPath = `${finalPath}.tmp-${process.pid}`;
|
|
@@ -381,67 +382,26 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
381
382
|
await fs.mkdir(parentDir, { recursive: true });
|
|
382
383
|
}
|
|
383
384
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (resolvedArchivePath.exists) {
|
|
388
|
-
try {
|
|
389
|
-
const bytes = await Bun.file(resolvedArchivePath.absolutePath).bytes();
|
|
390
|
-
const { unzipSync } = await loadFflate();
|
|
391
|
-
const existing = unzipSync(new Uint8Array(bytes));
|
|
392
|
-
for (const [entryPath, data] of Object.entries(existing)) {
|
|
393
|
-
zipEntries[entryPath.replace(/\\/g, "/")] = data;
|
|
394
|
-
}
|
|
395
|
-
} catch (error) {
|
|
396
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
zipEntries[resolvedArchivePath.archiveSubPath] = new TextEncoder().encode(content);
|
|
401
|
-
|
|
385
|
+
const entries = new Map<string, ArchiveMemberContent>();
|
|
386
|
+
if (resolvedArchivePath.exists) {
|
|
402
387
|
try {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
await fs.rename(tmpPath, finalPath);
|
|
407
|
-
} catch (error) {
|
|
408
|
-
await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
409
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
const archiveEntries: Record<string, string | File> = {};
|
|
413
|
-
if (resolvedArchivePath.exists) {
|
|
414
|
-
let archive: Bun.Archive;
|
|
415
|
-
try {
|
|
416
|
-
archive = new Bun.Archive(await Bun.file(resolvedArchivePath.absolutePath).bytes());
|
|
417
|
-
} catch (error) {
|
|
418
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
388
|
+
const existing = await readArchiveEntries({ bytes: await Bun.file(finalPath).bytes(), format });
|
|
389
|
+
for (const [entryPath, data] of existing) {
|
|
390
|
+
entries.set(entryPath, data);
|
|
419
391
|
}
|
|
420
|
-
|
|
421
|
-
let files: Map<string, File>;
|
|
422
|
-
try {
|
|
423
|
-
files = await archive.files();
|
|
424
|
-
} catch (error) {
|
|
425
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
for (const [entryPath, file] of files) {
|
|
429
|
-
archiveEntries[entryPath.replace(/\\/g, "/")] = file;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
archiveEntries[resolvedArchivePath.archiveSubPath] = content;
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
// `Bun.Archive.write` never infers compression from the extension;
|
|
437
|
-
// request gzip explicitly so `.tar.gz`/`.tgz` stay compressed.
|
|
438
|
-
await Bun.Archive.write(tmpPath, archiveEntries, isGzip ? { compress: "gzip" } : undefined);
|
|
439
|
-
await fs.rename(tmpPath, finalPath);
|
|
440
392
|
} catch (error) {
|
|
441
|
-
await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
442
393
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
443
394
|
}
|
|
444
395
|
}
|
|
396
|
+
entries.set(resolvedArchivePath.archiveSubPath, content);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
await writeArchive(tmpPath, format, entries);
|
|
400
|
+
await fs.rename(tmpPath, finalPath);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
403
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
404
|
+
}
|
|
445
405
|
|
|
446
406
|
invalidateFsScanAfterWrite(resolvedArchivePath.absolutePath);
|
|
447
407
|
const outputPath = `${formatPathRelativeToCwd(resolvedArchivePath.absolutePath, this.session.cwd)}:${
|
package/src/utils/markit.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import type { Markit, StreamInfo } from "markit
|
|
2
|
+
import type { Markit, StreamInfo } from "../markit";
|
|
3
3
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
|
+
import { loadEmbeddedMupdfWasm } from "./mupdf-wasm-embed";
|
|
4
5
|
|
|
5
6
|
export interface MarkitConversionResult {
|
|
6
7
|
content: string;
|
|
@@ -21,10 +22,7 @@ export interface MarkitFileConversionOptions {
|
|
|
21
22
|
interface MuPdfWasmModuleConfig {
|
|
22
23
|
print?: (...values: unknown[]) => void;
|
|
23
24
|
printErr?: (...values: unknown[]) => void;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
declare global {
|
|
27
|
-
var $libmupdf_wasm_Module: MuPdfWasmModuleConfig | undefined;
|
|
25
|
+
wasmBinary?: Uint8Array;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): void {
|
|
@@ -32,17 +30,35 @@ function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): voi
|
|
|
32
30
|
logger.debug("mupdf wasm output", { stream, message });
|
|
33
31
|
}
|
|
34
32
|
|
|
33
|
+
// `$libmupdf_wasm_Module` is declared globally (as `any`) by the mupdf package.
|
|
34
|
+
// Install print hooks before the WASM module initializes so its stdout/stderr
|
|
35
|
+
// route to the file logger instead of corrupting the TUI.
|
|
35
36
|
function installMuPdfWasmLogger(): void {
|
|
36
|
-
const moduleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
37
|
-
moduleConfig.print = (...values) => logMuPdfWasmOutput("stdout", values);
|
|
38
|
-
moduleConfig.printErr = (...values) => logMuPdfWasmOutput("stderr", values);
|
|
37
|
+
const moduleConfig: MuPdfWasmModuleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
38
|
+
moduleConfig.print = (...values: unknown[]) => logMuPdfWasmOutput("stdout", values);
|
|
39
|
+
moduleConfig.printErr = (...values: unknown[]) => logMuPdfWasmOutput("stderr", values);
|
|
40
|
+
globalThis.$libmupdf_wasm_Module = moduleConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hand the WASM module its bytes directly when the compiled binary embedded them
|
|
44
|
+
// (scripts/embed-mupdf-wasm.ts); a single-file binary has no node_modules for
|
|
45
|
+
// mupdf to read `mupdf-wasm.wasm` from. Source/npm builds get undefined here and
|
|
46
|
+
// mupdf loads its own wasm. Must run before the mupdf module evaluates.
|
|
47
|
+
function installEmbeddedMupdfWasm(): void {
|
|
48
|
+
const wasmBinary = loadEmbeddedMupdfWasm();
|
|
49
|
+
if (!wasmBinary) return;
|
|
50
|
+
const moduleConfig: MuPdfWasmModuleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
51
|
+
moduleConfig.wasmBinary = wasmBinary;
|
|
39
52
|
globalThis.$libmupdf_wasm_Module = moduleConfig;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
installMuPdfWasmLogger();
|
|
43
56
|
|
|
44
57
|
let markit: () => Markit | Promise<Markit> = async () => {
|
|
45
|
-
|
|
58
|
+
// Lazy: keep the document engine (mammoth/mupdf) off the startup
|
|
59
|
+
// import graph — it loads only when a document is first converted.
|
|
60
|
+
installEmbeddedMupdfWasm();
|
|
61
|
+
const promise = import("../markit").then(({ Markit }) => {
|
|
46
62
|
const instance = new Markit();
|
|
47
63
|
markit = () => instance;
|
|
48
64
|
return instance;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit by hand.
|
|
2
|
+
//
|
|
3
|
+
// Compiled single-file binaries cannot let mupdf resolve its `mupdf-wasm.wasm`
|
|
4
|
+
// sibling from the read-only bunfs, so the binary build (scripts/build-binary.ts
|
|
5
|
+
// and scripts/ci-release-build-binaries.ts) regenerates this module to embed the
|
|
6
|
+
// wasm bytes via `with { type: "file" }` and copies the wasm next to it. Source
|
|
7
|
+
// checkouts, `bun test`, and the npm `dist/cli.js` bundle keep mupdf external and
|
|
8
|
+
// load the wasm from node_modules, so this placeholder returns undefined and the
|
|
9
|
+
// build resets back to it afterward.
|
|
10
|
+
export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { $which, APP_NAME, getToolsDir, logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { extractArchive } from "./zip";
|
|
5
6
|
|
|
6
7
|
const TOOLS_DIR = getToolsDir();
|
|
7
8
|
const TOOL_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
@@ -220,17 +221,7 @@ async function downloadTool(tool: ToolName, signal?: AbortSignal): Promise<strin
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
try {
|
|
223
|
-
|
|
224
|
-
const files = await archive.files();
|
|
225
|
-
const extractRoot = path.resolve(tmp.path());
|
|
226
|
-
|
|
227
|
-
for (const [filePath, file] of files) {
|
|
228
|
-
const outputPath = path.resolve(extractRoot, filePath);
|
|
229
|
-
if (!outputPath.startsWith(extractRoot + path.sep)) {
|
|
230
|
-
throw new Error(`Archive entry escapes extraction dir: ${filePath}`);
|
|
231
|
-
}
|
|
232
|
-
await Bun.write(outputPath, file);
|
|
233
|
-
}
|
|
224
|
+
await extractArchive(archivePath, tmp.path());
|
|
234
225
|
} catch (err) {
|
|
235
226
|
throw new Error(`Failed to extract ${assetName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
227
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import TurndownService from "turndown";
|
|
2
|
+
import { gfm } from "turndown-plugin-gfm";
|
|
3
|
+
|
|
4
|
+
type TurndownListParent = {
|
|
5
|
+
nodeName: string;
|
|
6
|
+
getAttribute(name: string): string | null;
|
|
7
|
+
children: ArrayLike<unknown>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a Turndown instance configured for GFM with the fixes omp relies on:
|
|
12
|
+
* `~~strikethrough~~`, unescaped heading periods, and single-space list markers.
|
|
13
|
+
*
|
|
14
|
+
* Shared by the web scrapers (HTML → markdown) and the markit document engine
|
|
15
|
+
* (`src/markit`). The rule set must stay identical across both call sites.
|
|
16
|
+
*/
|
|
17
|
+
export function createTurndown(): TurndownService {
|
|
18
|
+
const turndown = new TurndownService({
|
|
19
|
+
headingStyle: "atx",
|
|
20
|
+
codeBlockStyle: "fenced",
|
|
21
|
+
bulletListMarker: "-",
|
|
22
|
+
});
|
|
23
|
+
turndown.use(gfm);
|
|
24
|
+
// GFM spec uses ~~ (double tilde), not ~ (single)
|
|
25
|
+
turndown.addRule("strikethrough", {
|
|
26
|
+
filter: ["del", "s", "strike"],
|
|
27
|
+
replacement(content) {
|
|
28
|
+
return `~~${content}~~`;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Unescape the backslash turndown inserts before periods in headings ("1." -> "1\.")
|
|
32
|
+
turndown.addRule("heading", {
|
|
33
|
+
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
34
|
+
replacement(content, node) {
|
|
35
|
+
const level = Number(node.nodeName.charAt(1));
|
|
36
|
+
const prefix = "#".repeat(level);
|
|
37
|
+
const cleaned = content.replace(/\\([.])/g, "$1").trim();
|
|
38
|
+
return `\n\n${prefix} ${cleaned}\n\n`;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
// Single space after the marker (turndown hardcodes three)
|
|
42
|
+
turndown.addRule("listItem", {
|
|
43
|
+
filter: "li",
|
|
44
|
+
replacement(content, node, options) {
|
|
45
|
+
const body = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n ");
|
|
46
|
+
const parent = node.parentNode as unknown as TurndownListParent | null;
|
|
47
|
+
let prefix = `${options.bulletListMarker} `;
|
|
48
|
+
if (parent?.nodeName === "OL") {
|
|
49
|
+
const start = parent.getAttribute("start");
|
|
50
|
+
const index = Array.prototype.indexOf.call(parent.children, node);
|
|
51
|
+
prefix = `${(start ? Number(start) : 1) + index}. `;
|
|
52
|
+
}
|
|
53
|
+
return prefix + body + (node.nextSibling ? "\n" : "");
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
return turndown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize HTML tables so turndown-plugin-gfm can render them:
|
|
61
|
+
* - strip `<p>` tags inside `<td>`/`<th>` cells (joining paragraphs with a space)
|
|
62
|
+
* - wrap the first row in `<thead>` when missing
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeTablesHtml(html: string): string {
|
|
65
|
+
let result = html.replace(
|
|
66
|
+
/<(td|th)([^>]*)>([\s\S]*?)<\/(td|th)>/gi,
|
|
67
|
+
(_match, tag: string, attrs: string, inner: string, closeTag: string) => {
|
|
68
|
+
const stripped = inner
|
|
69
|
+
.replace(/^\s*<p>/i, "")
|
|
70
|
+
.replace(/<\/p>\s*$/i, "")
|
|
71
|
+
.replace(/<\/p>\s*<p>/gi, " ");
|
|
72
|
+
return `<${tag}${attrs}>${stripped}</${closeTag}>`;
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
result = result.replace(
|
|
76
|
+
/<table([^>]*)>\s*(?:<tbody>\s*)?(<tr[\s\S]*?<\/tr>)([\s\S]*?)<\/(?:tbody>\s*<\/)?table>/gi,
|
|
77
|
+
(_match, attrs: string, firstRow: string, rest: string) => {
|
|
78
|
+
const theadRow = firstRow.replace(/<td/gi, "<th").replace(/<\/td>/gi, "</th>");
|
|
79
|
+
return `<table${attrs}><thead>${theadRow}</thead><tbody>${rest}</tbody></table>`;
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return result;
|
|
83
|
+
}
|