arisa 2.3.55 → 3.0.0
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/AGENTS.md +102 -0
- package/README.md +120 -165
- package/cli/openai-transcribe/index.js +51 -0
- package/cli/openai-transcribe/package.json +6 -0
- package/cli/openai-transcribe/tool.manifest.json +15 -0
- package/cli/openai-tts/index.js +58 -0
- package/cli/openai-tts/package.json +6 -0
- package/cli/openai-tts/tool.manifest.json +20 -0
- package/cli/web-browser/index.js +146 -0
- package/cli/web-browser/package.json +6 -0
- package/cli/web-browser/tool.manifest.json +8 -0
- package/package.json +26 -44
- package/src/core/agent/agent-manager.js +218 -0
- package/src/core/artifacts/artifact-store.js +102 -0
- package/src/core/config/config-store.js +20 -0
- package/src/core/tools/tool-registry.js +117 -0
- package/src/index.js +27 -0
- package/src/runtime/bootstrap.js +213 -0
- package/src/runtime/create-app.js +22 -0
- package/src/transport/telegram/auth.js +13 -0
- package/src/transport/telegram/bot.js +214 -0
- package/src/transport/telegram/media.js +75 -0
- package/CLAUDE.md +0 -191
- package/SOUL.md +0 -36
- package/bin/arisa.js +0 -644
- package/scripts/dump-commands.ts +0 -26
- package/scripts/test-secrets.ts +0 -22
- package/src/core/attachments.ts +0 -104
- package/src/core/auth.ts +0 -58
- package/src/core/context.ts +0 -30
- package/src/core/file-detector.ts +0 -39
- package/src/core/format.ts +0 -159
- package/src/core/index.ts +0 -456
- package/src/core/intent.ts +0 -119
- package/src/core/media.ts +0 -144
- package/src/core/onboarding.ts +0 -102
- package/src/core/processor.ts +0 -305
- package/src/core/router.ts +0 -64
- package/src/core/scheduler.ts +0 -193
- package/src/daemon/agent-cli.ts +0 -130
- package/src/daemon/auto-install.ts +0 -158
- package/src/daemon/autofix.ts +0 -116
- package/src/daemon/bridge.ts +0 -166
- package/src/daemon/channels/base.ts +0 -10
- package/src/daemon/channels/telegram.ts +0 -306
- package/src/daemon/claude-login.ts +0 -218
- package/src/daemon/codex-login.ts +0 -172
- package/src/daemon/fallback.ts +0 -73
- package/src/daemon/index.ts +0 -272
- package/src/daemon/lifecycle.ts +0 -313
- package/src/daemon/setup.ts +0 -329
- package/src/shared/ai-cli.ts +0 -165
- package/src/shared/config.ts +0 -137
- package/src/shared/db.ts +0 -304
- package/src/shared/deepbase-secure.ts +0 -39
- package/src/shared/ink-shim.js +0 -14
- package/src/shared/logger.ts +0 -42
- package/src/shared/paths.ts +0 -90
- package/src/shared/ports.ts +0 -120
- package/src/shared/secrets.ts +0 -136
- package/src/shared/types.ts +0 -103
- package/tsconfig.json +0 -19
package/src/core/attachments.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module core/attachments
|
|
3
|
-
* @role Persist media attachments so the model can access them later.
|
|
4
|
-
* @responsibilities
|
|
5
|
-
* - Save base64 attachments to runtime attachments/{chatId}/
|
|
6
|
-
* - Track metadata in deepbase (collection: "attachments")
|
|
7
|
-
* - Clean up files older than configured max age
|
|
8
|
-
* @dependencies shared/config, shared/db
|
|
9
|
-
* @effects Disk I/O in runtime attachments dir, deepbase writes
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { mkdirSync, existsSync, unlinkSync, rmdirSync, readdirSync } from "fs";
|
|
13
|
-
import { join, dirname, relative } from "path";
|
|
14
|
-
import { config } from "../shared/config";
|
|
15
|
-
import { createLogger } from "../shared/logger";
|
|
16
|
-
import { addAttachment, getExpiredAttachments, deleteAttachment, cleanupOldMessages } from "../shared/db";
|
|
17
|
-
import type { AttachmentRecord } from "../shared/types";
|
|
18
|
-
|
|
19
|
-
const log = createLogger("core");
|
|
20
|
-
|
|
21
|
-
const EXT_MAP: Record<string, string> = {
|
|
22
|
-
image: "jpg",
|
|
23
|
-
audio: "ogg",
|
|
24
|
-
document: "bin",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export async function initAttachments(): Promise<void> {
|
|
28
|
-
if (!existsSync(config.attachmentsDir)) {
|
|
29
|
-
mkdirSync(config.attachmentsDir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
await cleanupOldAttachments();
|
|
32
|
-
const msgsCleaned = await cleanupOldMessages(config.attachmentMaxAgeDays);
|
|
33
|
-
if (msgsCleaned > 0) {
|
|
34
|
-
log.info(`Cleaned up ${msgsCleaned} expired message record(s)`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function saveAttachment(
|
|
39
|
-
chatId: string,
|
|
40
|
-
type: "image" | "audio" | "document",
|
|
41
|
-
base64: string,
|
|
42
|
-
filename?: string,
|
|
43
|
-
mimeType?: string,
|
|
44
|
-
): Promise<string> {
|
|
45
|
-
const chatDir = join(config.attachmentsDir, chatId);
|
|
46
|
-
if (!existsSync(chatDir)) {
|
|
47
|
-
mkdirSync(chatDir, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const ext = filename ? filename.split(".").pop() || EXT_MAP[type] : EXT_MAP[type];
|
|
51
|
-
const hex = Math.random().toString(16).slice(2, 6);
|
|
52
|
-
const prefix = type === "image" ? "img" : type === "audio" ? "aud" : "doc";
|
|
53
|
-
const outName = `${prefix}_${Date.now()}_${hex}.${ext}`;
|
|
54
|
-
const outPath = join(chatDir, outName);
|
|
55
|
-
|
|
56
|
-
const buffer = Buffer.from(base64, "base64");
|
|
57
|
-
await Bun.write(outPath, buffer);
|
|
58
|
-
|
|
59
|
-
let relPath = relative(config.projectDir, outPath).replace(/\\/g, "/");
|
|
60
|
-
if (relPath.startsWith("..")) {
|
|
61
|
-
relPath = outPath;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const record: AttachmentRecord = {
|
|
65
|
-
id: `${chatId}_${outName}`,
|
|
66
|
-
chatId,
|
|
67
|
-
type,
|
|
68
|
-
filename: filename || outName,
|
|
69
|
-
relPath,
|
|
70
|
-
mimeType,
|
|
71
|
-
sizeBytes: buffer.length,
|
|
72
|
-
createdAt: Date.now(),
|
|
73
|
-
};
|
|
74
|
-
await addAttachment(record);
|
|
75
|
-
|
|
76
|
-
log.info(`Saved ${type} attachment: ${relPath} (${buffer.length} bytes)`);
|
|
77
|
-
return relPath;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function cleanupOldAttachments(): Promise<void> {
|
|
81
|
-
const expired = await getExpiredAttachments(config.attachmentMaxAgeDays);
|
|
82
|
-
if (expired.length === 0) return;
|
|
83
|
-
|
|
84
|
-
let cleaned = 0;
|
|
85
|
-
for (const record of expired) {
|
|
86
|
-
const absPath = join(config.projectDir, record.relPath);
|
|
87
|
-
try {
|
|
88
|
-
if (existsSync(absPath)) {
|
|
89
|
-
unlinkSync(absPath);
|
|
90
|
-
}
|
|
91
|
-
// Remove empty chat dir
|
|
92
|
-
const dir = dirname(absPath);
|
|
93
|
-
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
94
|
-
rmdirSync(dir);
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// File already gone — just clean the record
|
|
98
|
-
}
|
|
99
|
-
await deleteAttachment(record.id);
|
|
100
|
-
cleaned++;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
log.info(`Cleaned up ${cleaned} expired attachment(s)`);
|
|
104
|
-
}
|
package/src/core/auth.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module core/auth
|
|
3
|
-
* @role Gate access to the bot via a one-time token shown in the console.
|
|
4
|
-
* @responsibilities
|
|
5
|
-
* - Generate and persist an auth token via deepbase (settings.auth_token)
|
|
6
|
-
* - Track authorized chat IDs via deepbase (authorized collection)
|
|
7
|
-
* - Validate tokens from new chats
|
|
8
|
-
* @dependencies shared/db
|
|
9
|
-
* @effects deepbase writes, console output
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createLogger } from "../shared/logger";
|
|
13
|
-
import { getAuthorizedUsers, addAuthorized, getSetting, setSetting } from "../shared/db";
|
|
14
|
-
|
|
15
|
-
const log = createLogger("auth");
|
|
16
|
-
|
|
17
|
-
let authToken = "";
|
|
18
|
-
let authorizedChats: Set<string> = new Set();
|
|
19
|
-
|
|
20
|
-
async function loadToken(): Promise<string> {
|
|
21
|
-
const existing = await getSetting("auth_token");
|
|
22
|
-
if (existing) return existing;
|
|
23
|
-
|
|
24
|
-
const token = crypto.randomUUID().split("-")[0];
|
|
25
|
-
await setSetting("auth_token", token);
|
|
26
|
-
return token;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function loadAuthorized(): Promise<Set<string>> {
|
|
30
|
-
try {
|
|
31
|
-
const users = await getAuthorizedUsers();
|
|
32
|
-
return new Set(users);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
log.error(`Failed to load authorized users: ${error}`);
|
|
35
|
-
return new Set();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function initAuth() {
|
|
40
|
-
authToken = await loadToken();
|
|
41
|
-
authorizedChats = await loadAuthorized();
|
|
42
|
-
log.info(`Auth token: ${authToken}`);
|
|
43
|
-
console.log(`\n🔑 Auth token: ${authToken}\n Send this token to the bot on Telegram to authorize a chat.\n`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function isAuthorized(chatId: string): boolean {
|
|
47
|
-
return authorizedChats.has(chatId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function tryAuthorize(chatId: string, message: string): Promise<boolean> {
|
|
51
|
-
if (message.trim() === authToken) {
|
|
52
|
-
authorizedChats.add(chatId);
|
|
53
|
-
await addAuthorized(chatId);
|
|
54
|
-
log.info(`Chat ${chatId} authorized`);
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
package/src/core/context.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module core/context
|
|
3
|
-
* @role Manage Claude conversation continuity via the -c flag and reset_flag.
|
|
4
|
-
* @responsibilities
|
|
5
|
-
* - Check if reset_flag exists (user sent /reset)
|
|
6
|
-
* - Return whether to use -c (continue) flag
|
|
7
|
-
* - Clear reset_flag after consuming it
|
|
8
|
-
* @dependencies shared/config
|
|
9
|
-
* @effects Reads/deletes reset_flag from runtime data dir
|
|
10
|
-
* @contract shouldContinue() => boolean
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, unlinkSync } from "fs";
|
|
14
|
-
import { config } from "../shared/config";
|
|
15
|
-
import { createLogger } from "../shared/logger";
|
|
16
|
-
|
|
17
|
-
const log = createLogger("core");
|
|
18
|
-
|
|
19
|
-
export function shouldContinue(): boolean {
|
|
20
|
-
if (existsSync(config.resetFlagPath)) {
|
|
21
|
-
log.info("Reset flag found — starting fresh conversation");
|
|
22
|
-
try {
|
|
23
|
-
unlinkSync(config.resetFlagPath);
|
|
24
|
-
} catch {
|
|
25
|
-
// Already deleted, race condition
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module core/file-detector
|
|
3
|
-
* @role Detect file paths mentioned in Claude responses that exist on disk.
|
|
4
|
-
* @responsibilities
|
|
5
|
-
* - Scan response text for absolute file paths
|
|
6
|
-
* - Verify they exist and are sendable (< 50MB, not directories)
|
|
7
|
-
* - Return list of unique valid file paths
|
|
8
|
-
* @dependencies None
|
|
9
|
-
* @effects Reads file system to check existence/size
|
|
10
|
-
* @contract detectFiles(text) => string[]
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { statSync } from "fs";
|
|
14
|
-
|
|
15
|
-
const FILE_PATH_REGEX = /(\/[\w./-]+\.\w{1,10})/gm;
|
|
16
|
-
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
17
|
-
|
|
18
|
-
export function detectFiles(text: string): string[] {
|
|
19
|
-
const matches = [...text.matchAll(FILE_PATH_REGEX)];
|
|
20
|
-
const seen = new Set<string>();
|
|
21
|
-
const files: string[] = [];
|
|
22
|
-
|
|
23
|
-
for (const match of matches) {
|
|
24
|
-
const filePath = match[1];
|
|
25
|
-
if (seen.has(filePath)) continue;
|
|
26
|
-
seen.add(filePath);
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const stat = statSync(filePath);
|
|
30
|
-
if (stat.isFile() && stat.size < MAX_FILE_SIZE) {
|
|
31
|
-
files.push(filePath);
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// File doesn't exist or can't be read
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return files;
|
|
39
|
-
}
|
package/src/core/format.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module core/format
|
|
3
|
-
* @role Format responses for Telegram (HTML) and chunk long messages.
|
|
4
|
-
* @responsibilities
|
|
5
|
-
* - Split text into chunks respecting Telegram's 4096 char limit
|
|
6
|
-
* - Safe HTML sending with plain-text fallback marker
|
|
7
|
-
* @dependencies None
|
|
8
|
-
* @effects None (pure functions)
|
|
9
|
-
* @contract chunkMessage(text) => string[]
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const MAX_TELEGRAM_LENGTH = 4096;
|
|
13
|
-
|
|
14
|
-
function escapeHtml(s: string): string {
|
|
15
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Decode HTML entities so we don't double-encode them when escapeHtml runs. */
|
|
19
|
-
function unescapeHtml(s: string): string {
|
|
20
|
-
return s
|
|
21
|
-
.replace(/&/g, "&")
|
|
22
|
-
.replace(/</g, "<")
|
|
23
|
-
.replace(/>/g, ">")
|
|
24
|
-
.replace(/"/g, '"')
|
|
25
|
-
.replace(/'/g, "'");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function protectTelegramHtmlTags(text: string): { text: string; tags: string[] } {
|
|
29
|
-
const tags: string[] = [];
|
|
30
|
-
const tagPattern = /<\/?(?:b|i|u|s|code|pre|blockquote)>|<a\s+href="[^"]+">|<\/a>/gi;
|
|
31
|
-
const protectedText = text.replace(tagPattern, (tag) => {
|
|
32
|
-
tags.push(tag);
|
|
33
|
-
return `\x00HTMLTAG${tags.length - 1}\x00`;
|
|
34
|
-
});
|
|
35
|
-
return { text: protectedText, tags };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Convert Markdown (from Claude CLI) to Telegram-safe HTML.
|
|
40
|
-
* Telegram supports: <b>, <i>, <code>, <pre>, <a>, <s>, <u>, <blockquote>
|
|
41
|
-
* Must also escape HTML entities in non-tag content.
|
|
42
|
-
*/
|
|
43
|
-
export function markdownToTelegramHtml(text: string): string {
|
|
44
|
-
// Step 0: Decode any pre-escaped HTML entities to avoid double-encoding
|
|
45
|
-
// (e.g. <code> → <code> so protectTelegramHtmlTags can detect them)
|
|
46
|
-
text = unescapeHtml(text);
|
|
47
|
-
|
|
48
|
-
// Step 1: Extract code blocks and links before escaping to protect them
|
|
49
|
-
const codeBlocks: string[] = [];
|
|
50
|
-
const inlineCodes: string[] = [];
|
|
51
|
-
const links: string[] = [];
|
|
52
|
-
|
|
53
|
-
// Preserve already-valid Telegram HTML tags instead of escaping them.
|
|
54
|
-
const protectedHtml = protectTelegramHtmlTags(text);
|
|
55
|
-
|
|
56
|
-
// Protect fenced code blocks: ```lang\n...\n``` or ```...```
|
|
57
|
-
let result = protectedHtml.text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => {
|
|
58
|
-
codeBlocks.push(code.trimEnd());
|
|
59
|
-
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Protect inline code: `...`
|
|
63
|
-
result = result.replace(/`([^`\n]+)`/g, (_match, code) => {
|
|
64
|
-
inlineCodes.push(code);
|
|
65
|
-
return `\x00INLINE${inlineCodes.length - 1}\x00`;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Protect links: [text](url)
|
|
69
|
-
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
|
|
70
|
-
links.push(`<a href="${url}">${escapeHtml(linkText)}</a>`);
|
|
71
|
-
return `\x00LINK${links.length - 1}\x00`;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Step 2: Now escape HTML entities in the remaining text
|
|
75
|
-
result = escapeHtml(result);
|
|
76
|
-
|
|
77
|
-
// Step 3: Convert markdown patterns to HTML tags
|
|
78
|
-
|
|
79
|
-
// Bold+Italic: ***text*** or ___text___
|
|
80
|
-
result = result.replace(/\*{3}(.+?)\*{3}/g, "<b><i>$1</i></b>");
|
|
81
|
-
result = result.replace(/_{3}(.+?)_{3}/g, "<b><i>$1</i></b>");
|
|
82
|
-
|
|
83
|
-
// Bold: **text** or __text__
|
|
84
|
-
result = result.replace(/\*{2}(.+?)\*{2}/g, "<b>$1</b>");
|
|
85
|
-
result = result.replace(/_{2}(.+?)_{2}/g, "<b>$1</b>");
|
|
86
|
-
|
|
87
|
-
// Italic: *text* or _text_ (not inside words for underscore)
|
|
88
|
-
result = result.replace(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, "<i>$1</i>");
|
|
89
|
-
result = result.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, "<i>$1</i>");
|
|
90
|
-
|
|
91
|
-
// Strikethrough: ~~text~~
|
|
92
|
-
result = result.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
93
|
-
|
|
94
|
-
// Step 4: Restore protected elements
|
|
95
|
-
result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, i) => `<pre>${escapeHtml(codeBlocks[+i])}</pre>`);
|
|
96
|
-
result = result.replace(/\x00INLINE(\d+)\x00/g, (_m, i) => `<code>${escapeHtml(inlineCodes[+i])}</code>`);
|
|
97
|
-
result = result.replace(/\x00LINK(\d+)\x00/g, (_m, i) => links[+i]);
|
|
98
|
-
result = result.replace(/\x00HTMLTAG(\d+)\x00/g, (_m, i) => protectedHtml.tags[+i]);
|
|
99
|
-
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function chunkMessage(text: string): string[] {
|
|
104
|
-
if (text.length <= MAX_TELEGRAM_LENGTH) {
|
|
105
|
-
return [text];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const chunks: string[] = [];
|
|
109
|
-
let remaining = text;
|
|
110
|
-
|
|
111
|
-
// Tags that Telegram supports and we use
|
|
112
|
-
const tagNames = ["pre", "code", "b", "i", "s", "a"];
|
|
113
|
-
|
|
114
|
-
while (remaining.length > 0) {
|
|
115
|
-
if (remaining.length <= MAX_TELEGRAM_LENGTH) {
|
|
116
|
-
chunks.push(remaining);
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let splitAt = remaining.lastIndexOf("\n", MAX_TELEGRAM_LENGTH);
|
|
121
|
-
if (splitAt === -1 || splitAt < MAX_TELEGRAM_LENGTH * 0.5) {
|
|
122
|
-
splitAt = MAX_TELEGRAM_LENGTH;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check if splitting here would break an HTML tag
|
|
126
|
-
let candidate = remaining.substring(0, splitAt);
|
|
127
|
-
|
|
128
|
-
// Find unclosed tags in the candidate chunk
|
|
129
|
-
const openTags: string[] = [];
|
|
130
|
-
const tagRegex = /<\/?([a-z]+)(?:\s[^>]*)?\/?>/gi;
|
|
131
|
-
let match: RegExpExecArray | null;
|
|
132
|
-
while ((match = tagRegex.exec(candidate)) !== null) {
|
|
133
|
-
const fullTag = match[0];
|
|
134
|
-
const tagName = match[1].toLowerCase();
|
|
135
|
-
if (!tagNames.includes(tagName)) continue;
|
|
136
|
-
if (fullTag.startsWith("</")) {
|
|
137
|
-
// Closing tag - pop if matching
|
|
138
|
-
const idx = openTags.lastIndexOf(tagName);
|
|
139
|
-
if (idx !== -1) openTags.splice(idx, 1);
|
|
140
|
-
} else if (!fullTag.endsWith("/>")) {
|
|
141
|
-
openTags.push(tagName);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// If there are unclosed tags, close them at end of this chunk and reopen in next
|
|
146
|
-
if (openTags.length > 0) {
|
|
147
|
-
const closingTags = [...openTags].reverse().map(t => `</${t}>`).join("");
|
|
148
|
-
const openingTags = openTags.map(t => `<${t}>`).join("");
|
|
149
|
-
candidate = candidate + closingTags;
|
|
150
|
-
remaining = openingTags + remaining.substring(splitAt).trimStart();
|
|
151
|
-
} else {
|
|
152
|
-
remaining = remaining.substring(splitAt).trimStart();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
chunks.push(candidate);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return chunks;
|
|
159
|
-
}
|