stego-cli 0.3.2 → 0.3.4
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/dist/comments/comment-domain.js +919 -0
- package/dist/comments/comments-command.js +356 -64
- package/dist/stego-cli.js +19 -241
- package/package.json +1 -1
- package/dist/comments/add-comment.js +0 -182
package/dist/stego-cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { markdownExporter } from "./exporters/markdown-exporter.js";
|
|
10
10
|
import { createPandocExporter } from "./exporters/pandoc-exporter.js";
|
|
11
|
+
import { parseCommentAppendix } from "./comments/comment-domain.js";
|
|
11
12
|
import { runCommentsCommand } from "./comments/comments-command.js";
|
|
12
13
|
import { CommentsCommandError } from "./comments/errors.js";
|
|
13
14
|
const STATUS_RANK = {
|
|
@@ -767,7 +768,7 @@ function writeInitRootPackageJson(targetRoot) {
|
|
|
767
768
|
fs.writeFileSync(path.join(targetRoot, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
768
769
|
}
|
|
769
770
|
function printUsage() {
|
|
770
|
-
console.log(`Stego CLI\n\nCommands:\n init [--force]\n list-projects [--root <path>]\n new-project --project <project-id> [--title <title>] [--root <path>]\n new --project <project-id> [--i <prefix>|-i <prefix>] [--root <path>]\n validate --project <project-id> [--file <project-relative-manuscript-path>] [--root <path>]\n build --project <project-id> [--root <path>]\n check-stage --project <project-id> --stage <draft|revise|line-edit|proof|final> [--file <project-relative-manuscript-path>] [--root <path>]\n lint --project <project-id> [--manuscript|--spine] [--root <path>]\n export --project <project-id> --format <md|docx|pdf|epub> [--output <path>] [--root <path>]\n comments add <manuscript> [--message <text> | --input <path|->] [--author <name>] [--start-line <n> --start-col <n> --end-line <n> --end-col <n>] [--format <text|json>]\n`);
|
|
771
|
+
console.log(`Stego CLI\n\nCommands:\n init [--force]\n list-projects [--root <path>]\n new-project --project <project-id> [--title <title>] [--root <path>]\n new --project <project-id> [--i <prefix>|-i <prefix>] [--root <path>]\n validate --project <project-id> [--file <project-relative-manuscript-path>] [--root <path>]\n build --project <project-id> [--root <path>]\n check-stage --project <project-id> --stage <draft|revise|line-edit|proof|final> [--file <project-relative-manuscript-path>] [--root <path>]\n lint --project <project-id> [--manuscript|--spine] [--root <path>]\n export --project <project-id> --format <md|docx|pdf|epub> [--output <path>] [--root <path>]\n comments read <manuscript> [--format <text|json>]\n comments add <manuscript> [--message <text> | --input <path|->] [--author <name>] [--start-line <n> --start-col <n> --end-line <n> --end-col <n>] [--cursor-line <n>] [--format <text|json>]\n comments reply <manuscript> --comment-id <CMT-####> [--message <text> | --input <path|->] [--author <name>] [--format <text|json>]\n comments set-status <manuscript> --comment-id <CMT-####> --status <open|resolved> [--thread] [--format <text|json>]\n comments delete <manuscript> --comment-id <CMT-####> [--format <text|json>]\n comments clear-resolved <manuscript> [--format <text|json>]\n comments sync-anchors <manuscript> --input <path|-> [--format <text|json>]\n`);
|
|
771
772
|
}
|
|
772
773
|
function listProjects() {
|
|
773
774
|
const ids = getProjectIds();
|
|
@@ -1375,252 +1376,29 @@ function parseMetadata(raw, chapterPath, required) {
|
|
|
1375
1376
|
};
|
|
1376
1377
|
}
|
|
1377
1378
|
function parseStegoCommentsAppendix(body, relativePath, bodyStartLine) {
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
if (startIndexes.length === 0 && endIndexes.length === 0) {
|
|
1386
|
-
return { bodyWithoutComments: body, comments: [], issues };
|
|
1387
|
-
}
|
|
1388
|
-
if (startIndexes.length !== 1 || endIndexes.length !== 1) {
|
|
1389
|
-
if (startIndexes.length !== 1) {
|
|
1390
|
-
issues.push(makeIssue("error", "comments", `Expected exactly one '${startMarker}' marker.`, relativePath));
|
|
1391
|
-
}
|
|
1392
|
-
if (endIndexes.length !== 1) {
|
|
1393
|
-
issues.push(makeIssue("error", "comments", `Expected exactly one '${endMarker}' marker.`, relativePath));
|
|
1394
|
-
}
|
|
1395
|
-
return { bodyWithoutComments: body, comments: [], issues };
|
|
1396
|
-
}
|
|
1397
|
-
const start = startIndexes[0];
|
|
1398
|
-
const end = endIndexes[0];
|
|
1399
|
-
if (end <= start) {
|
|
1400
|
-
issues.push(makeIssue("error", "comments", `'${endMarker}' must appear after '${startMarker}'.`, relativePath, bodyStartLine + end));
|
|
1401
|
-
return { bodyWithoutComments: body, comments: [], issues };
|
|
1402
|
-
}
|
|
1403
|
-
const blockLines = lines.slice(start + 1, end);
|
|
1404
|
-
const comments = parseStegoCommentThreads(blockLines, relativePath, bodyStartLine + start + 1, issues);
|
|
1405
|
-
let removeStart = start;
|
|
1406
|
-
if (removeStart > 0 && lines[removeStart - 1].trim().length === 0) {
|
|
1407
|
-
removeStart -= 1;
|
|
1408
|
-
}
|
|
1409
|
-
const kept = [...lines.slice(0, removeStart), ...lines.slice(end + 1)];
|
|
1410
|
-
while (kept.length > 0 && kept[kept.length - 1].trim().length === 0) {
|
|
1411
|
-
kept.pop();
|
|
1412
|
-
}
|
|
1379
|
+
const parsed = parseCommentAppendix(body);
|
|
1380
|
+
const issues = parsed.errors.map((error) => parseCommentIssueFromParserError(error, relativePath, bodyStartLine));
|
|
1381
|
+
const comments = parsed.comments.map((comment) => ({
|
|
1382
|
+
id: comment.id,
|
|
1383
|
+
resolved: comment.status === "resolved",
|
|
1384
|
+
thread: comment.thread
|
|
1385
|
+
}));
|
|
1413
1386
|
return {
|
|
1414
|
-
bodyWithoutComments:
|
|
1387
|
+
bodyWithoutComments: parsed.contentWithoutComments,
|
|
1415
1388
|
comments,
|
|
1416
1389
|
issues
|
|
1417
1390
|
};
|
|
1418
1391
|
}
|
|
1419
|
-
function
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const trimmed = lines[index].trim();
|
|
1424
|
-
if (!trimmed) {
|
|
1425
|
-
index += 1;
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
const id = parseCommentThreadDelimiter(trimmed);
|
|
1429
|
-
if (!id) {
|
|
1430
|
-
issues.push(makeIssue("error", "comments", "Invalid comments appendix line. Expected comment delimiter '<!-- comment: CMT-0001 -->'.", relativePath, baseLine + index));
|
|
1431
|
-
index += 1;
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
index += 1;
|
|
1435
|
-
const rowLines = [];
|
|
1436
|
-
const rowLineNumbers = [];
|
|
1437
|
-
while (index < lines.length) {
|
|
1438
|
-
const nextTrimmed = lines[index].trim();
|
|
1439
|
-
if (parseCommentThreadDelimiter(nextTrimmed)) {
|
|
1440
|
-
break;
|
|
1441
|
-
}
|
|
1442
|
-
rowLines.push(lines[index]);
|
|
1443
|
-
rowLineNumbers.push(baseLine + index);
|
|
1444
|
-
index += 1;
|
|
1445
|
-
}
|
|
1446
|
-
let resolved;
|
|
1447
|
-
let sawMeta64 = false;
|
|
1448
|
-
const thread = [];
|
|
1449
|
-
let rowIndex = 0;
|
|
1450
|
-
while (rowIndex < rowLines.length) {
|
|
1451
|
-
const rawRow = rowLines[rowIndex];
|
|
1452
|
-
const lineNumber = rowLineNumbers[rowIndex];
|
|
1453
|
-
const trimmedRow = rawRow.trim();
|
|
1454
|
-
if (!trimmedRow) {
|
|
1455
|
-
rowIndex += 1;
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
if (thread.length > 0) {
|
|
1459
|
-
issues.push(makeIssue("error", "comments", `Multiple message blocks found for ${id}. Create a new CMT id for each reply.`, relativePath, lineNumber));
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
if (!sawMeta64) {
|
|
1463
|
-
const metaMatch = trimmedRow.match(/^<!--\s*meta64:\s*(\S+)\s*-->\s*$/);
|
|
1464
|
-
if (!metaMatch) {
|
|
1465
|
-
issues.push(makeIssue("error", "comments", `Invalid comment metadata row '${trimmedRow}'. Expected '<!-- meta64: <base64url-json> -->'.`, relativePath, lineNumber));
|
|
1466
|
-
rowIndex += 1;
|
|
1467
|
-
continue;
|
|
1468
|
-
}
|
|
1469
|
-
sawMeta64 = true;
|
|
1470
|
-
const decoded = decodeCommentMeta64(metaMatch[1], id, relativePath, lineNumber, issues);
|
|
1471
|
-
if (decoded) {
|
|
1472
|
-
resolved = decoded.resolved;
|
|
1473
|
-
}
|
|
1474
|
-
rowIndex += 1;
|
|
1475
|
-
continue;
|
|
1476
|
-
}
|
|
1477
|
-
const headerQuote = extractQuotedLine(rawRow);
|
|
1478
|
-
if (headerQuote === undefined) {
|
|
1479
|
-
issues.push(makeIssue("error", "comments", `Invalid thread header '${trimmedRow}'. Expected blockquote header like '> _timestamp | author_'.`, relativePath, lineNumber));
|
|
1480
|
-
rowIndex += 1;
|
|
1481
|
-
continue;
|
|
1482
|
-
}
|
|
1483
|
-
const header = parseThreadHeader(headerQuote);
|
|
1484
|
-
if (!header) {
|
|
1485
|
-
issues.push(makeIssue("error", "comments", `Invalid thread header '${headerQuote.trim()}'. Expected '> _timestamp | author_'.`, relativePath, lineNumber));
|
|
1486
|
-
rowIndex += 1;
|
|
1487
|
-
continue;
|
|
1488
|
-
}
|
|
1489
|
-
rowIndex += 1;
|
|
1490
|
-
while (rowIndex < rowLines.length) {
|
|
1491
|
-
const separatorRaw = rowLines[rowIndex];
|
|
1492
|
-
const separatorTrimmed = separatorRaw.trim();
|
|
1493
|
-
if (!separatorTrimmed) {
|
|
1494
|
-
rowIndex += 1;
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
const separatorQuote = extractQuotedLine(separatorRaw);
|
|
1498
|
-
if (separatorQuote !== undefined && separatorQuote.trim().length === 0) {
|
|
1499
|
-
rowIndex += 1;
|
|
1500
|
-
}
|
|
1501
|
-
break;
|
|
1502
|
-
}
|
|
1503
|
-
const messageLines = [];
|
|
1504
|
-
while (rowIndex < rowLines.length) {
|
|
1505
|
-
const messageRaw = rowLines[rowIndex];
|
|
1506
|
-
const messageLineNumber = rowLineNumbers[rowIndex];
|
|
1507
|
-
const messageTrimmed = messageRaw.trim();
|
|
1508
|
-
if (!messageTrimmed) {
|
|
1509
|
-
rowIndex += 1;
|
|
1510
|
-
if (messageLines.length > 0) {
|
|
1511
|
-
break;
|
|
1512
|
-
}
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
const messageQuote = extractQuotedLine(messageRaw);
|
|
1516
|
-
if (messageQuote === undefined) {
|
|
1517
|
-
issues.push(makeIssue("error", "comments", `Invalid thread line '${messageTrimmed}'. Expected blockquote content starting with '>'.`, relativePath, messageLineNumber));
|
|
1518
|
-
rowIndex += 1;
|
|
1519
|
-
if (messageLines.length > 0) {
|
|
1520
|
-
break;
|
|
1521
|
-
}
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
if (parseThreadHeader(messageQuote)) {
|
|
1525
|
-
break;
|
|
1526
|
-
}
|
|
1527
|
-
messageLines.push(messageQuote);
|
|
1528
|
-
rowIndex += 1;
|
|
1529
|
-
}
|
|
1530
|
-
while (messageLines.length > 0 && messageLines[messageLines.length - 1].trim().length === 0) {
|
|
1531
|
-
messageLines.pop();
|
|
1532
|
-
}
|
|
1533
|
-
if (messageLines.length === 0) {
|
|
1534
|
-
issues.push(makeIssue("error", "comments", `Thread entry for comment ${id} is missing message text.`, relativePath, lineNumber));
|
|
1535
|
-
continue;
|
|
1536
|
-
}
|
|
1537
|
-
const message = messageLines.join("\n").trim();
|
|
1538
|
-
thread.push(`${header.timestamp} | ${header.author} | ${message}`);
|
|
1539
|
-
}
|
|
1540
|
-
if (!sawMeta64) {
|
|
1541
|
-
issues.push(makeIssue("error", "comments", `Comment ${id} is missing metadata row ('<!-- meta64: <base64url-json> -->').`, relativePath));
|
|
1542
|
-
resolved = false;
|
|
1543
|
-
}
|
|
1544
|
-
if (thread.length === 0) {
|
|
1545
|
-
issues.push(makeIssue("error", "comments", `Comment ${id} is missing valid blockquote thread entries.`, relativePath));
|
|
1546
|
-
}
|
|
1547
|
-
comments.push({ id, resolved: Boolean(resolved), thread });
|
|
1548
|
-
}
|
|
1549
|
-
return comments;
|
|
1550
|
-
}
|
|
1551
|
-
function parseCommentThreadDelimiter(line) {
|
|
1552
|
-
const markerMatch = line.match(/^<!--\s*comment:\s*(CMT-\d{4,})\s*-->\s*$/i);
|
|
1553
|
-
if (markerMatch?.[1]) {
|
|
1554
|
-
return markerMatch[1].toUpperCase();
|
|
1555
|
-
}
|
|
1556
|
-
const legacyHeadingMatch = line.match(/^###\s+(CMT-\d{4,})\s*$/i);
|
|
1557
|
-
if (legacyHeadingMatch?.[1]) {
|
|
1558
|
-
return legacyHeadingMatch[1].toUpperCase();
|
|
1559
|
-
}
|
|
1560
|
-
return undefined;
|
|
1561
|
-
}
|
|
1562
|
-
function decodeCommentMeta64(encoded, commentId, relativePath, lineNumber, issues) {
|
|
1563
|
-
let rawJson = "";
|
|
1564
|
-
try {
|
|
1565
|
-
rawJson = Buffer.from(encoded, "base64url").toString("utf8");
|
|
1566
|
-
}
|
|
1567
|
-
catch {
|
|
1568
|
-
issues.push(makeIssue("error", "comments", `Invalid meta64 payload for comment ${commentId}; expected base64url-encoded JSON.`, relativePath, lineNumber));
|
|
1569
|
-
return undefined;
|
|
1570
|
-
}
|
|
1571
|
-
let parsed;
|
|
1572
|
-
try {
|
|
1573
|
-
parsed = JSON.parse(rawJson);
|
|
1574
|
-
}
|
|
1575
|
-
catch {
|
|
1576
|
-
issues.push(makeIssue("error", "comments", `Invalid meta64 JSON for comment ${commentId}.`, relativePath, lineNumber));
|
|
1577
|
-
return undefined;
|
|
1578
|
-
}
|
|
1579
|
-
if (!isPlainObject(parsed)) {
|
|
1580
|
-
issues.push(makeIssue("error", "comments", `Invalid meta64 object for comment ${commentId}.`, relativePath, lineNumber));
|
|
1581
|
-
return undefined;
|
|
1582
|
-
}
|
|
1583
|
-
const allowedKeys = new Set(["status", "anchor", "paragraph_index", "signature", "excerpt"]);
|
|
1584
|
-
for (const key of Object.keys(parsed)) {
|
|
1585
|
-
if (!allowedKeys.has(key)) {
|
|
1586
|
-
issues.push(makeIssue("error", "comments", `meta64 for comment ${commentId} contains unsupported key '${key}'.`, relativePath, lineNumber));
|
|
1587
|
-
return undefined;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
const status = typeof parsed.status === "string" ? parsed.status.trim().toLowerCase() : "";
|
|
1591
|
-
if (status !== "open" && status !== "resolved") {
|
|
1592
|
-
issues.push(makeIssue("error", "comments", `meta64 for comment ${commentId} must include status 'open' or 'resolved'.`, relativePath, lineNumber));
|
|
1593
|
-
return undefined;
|
|
1594
|
-
}
|
|
1595
|
-
return { resolved: status === "resolved" };
|
|
1596
|
-
}
|
|
1597
|
-
function extractQuotedLine(raw) {
|
|
1598
|
-
const quoteMatch = raw.match(/^\s*>\s?(.*)$/);
|
|
1599
|
-
if (!quoteMatch) {
|
|
1600
|
-
return undefined;
|
|
1601
|
-
}
|
|
1602
|
-
return quoteMatch[1];
|
|
1603
|
-
}
|
|
1604
|
-
function parseThreadHeader(value) {
|
|
1605
|
-
const match = value.trim().match(/^_(.+?)\s*\|\s*(.+?)_\s*$/);
|
|
1606
|
-
if (!match) {
|
|
1607
|
-
return undefined;
|
|
1608
|
-
}
|
|
1609
|
-
const timestamp = match[1].trim();
|
|
1610
|
-
const author = match[2].trim();
|
|
1611
|
-
if (!timestamp || !author) {
|
|
1612
|
-
return undefined;
|
|
1613
|
-
}
|
|
1614
|
-
return { timestamp, author };
|
|
1615
|
-
}
|
|
1616
|
-
function findTrimmedLineIndexes(lines, marker) {
|
|
1617
|
-
const indexes = [];
|
|
1618
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
1619
|
-
if (lines[index].trim() === marker) {
|
|
1620
|
-
indexes.push(index);
|
|
1621
|
-
}
|
|
1392
|
+
function parseCommentIssueFromParserError(error, relativePath, bodyStartLine) {
|
|
1393
|
+
const lineMatch = error.match(/^Line\\s+(\\d+):\\s+([\\s\\S]+)$/);
|
|
1394
|
+
if (!lineMatch) {
|
|
1395
|
+
return makeIssue("error", "comments", error, relativePath);
|
|
1622
1396
|
}
|
|
1623
|
-
|
|
1397
|
+
const relativeLine = Number.parseInt(lineMatch[1], 10);
|
|
1398
|
+
const absoluteLine = Number.isFinite(relativeLine)
|
|
1399
|
+
? bodyStartLine + relativeLine - 1
|
|
1400
|
+
: undefined;
|
|
1401
|
+
return makeIssue("error", "comments", lineMatch[2], relativePath, absoluteLine);
|
|
1624
1402
|
}
|
|
1625
1403
|
function coerceMetadataValue(value) {
|
|
1626
1404
|
if (!value) {
|
package/package.json
CHANGED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { CommentsCommandError } from "./errors.js";
|
|
4
|
-
const COMMENT_DELIMITER_REGEX = /^<!--\s*comment:\s*(CMT-(\d{4,}))\s*-->\s*$/i;
|
|
5
|
-
const LEGACY_COMMENT_HEADING_REGEX = /^###\s+(CMT-(\d{4,}))\s*$/i;
|
|
6
|
-
const START_SENTINEL = "<!-- stego-comments:start -->";
|
|
7
|
-
const END_SENTINEL = "<!-- stego-comments:end -->";
|
|
8
|
-
/** Adds one stego comment entry to a manuscript and writes the updated file. */
|
|
9
|
-
export function addCommentToManuscript(request) {
|
|
10
|
-
const absolutePath = path.resolve(request.cwd, request.manuscriptPath);
|
|
11
|
-
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
12
|
-
throw new CommentsCommandError("INVALID_USAGE", 2, `Manuscript file not found: ${request.manuscriptPath}`);
|
|
13
|
-
}
|
|
14
|
-
const raw = fs.readFileSync(absolutePath, "utf8");
|
|
15
|
-
const lineEnding = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
16
|
-
const lines = raw.split(/\r?\n/);
|
|
17
|
-
const sentinelState = resolveSentinelState(lines);
|
|
18
|
-
const hasYamlFrontmatter = raw.startsWith(`---${lineEnding}`) || raw.startsWith("---\n");
|
|
19
|
-
if (!sentinelState.hasSentinels && !hasYamlFrontmatter) {
|
|
20
|
-
throw new CommentsCommandError("NOT_STEGO_MANUSCRIPT", 3, "File is not recognized as a Stego manuscript (missing frontmatter and comments sentinels).");
|
|
21
|
-
}
|
|
22
|
-
const commentId = getNextCommentId(lines, sentinelState);
|
|
23
|
-
const createdAt = new Date().toISOString();
|
|
24
|
-
const meta = buildMetaPayload(request.range, request.sourceMeta);
|
|
25
|
-
const meta64 = Buffer.from(JSON.stringify(meta), "utf8").toString("base64url");
|
|
26
|
-
const entryLines = buildCommentEntryLines(commentId, createdAt, request.author, request.message, meta64);
|
|
27
|
-
const nextLines = applyCommentEntry(lines, sentinelState, entryLines);
|
|
28
|
-
const nextContent = `${nextLines.join(lineEnding)}${lineEnding}`;
|
|
29
|
-
try {
|
|
30
|
-
fs.writeFileSync(absolutePath, nextContent, "utf8");
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
-
throw new CommentsCommandError("WRITE_FAILURE", 6, `Failed to update manuscript: ${message}`);
|
|
35
|
-
}
|
|
36
|
-
const result = {
|
|
37
|
-
ok: true,
|
|
38
|
-
manuscript: absolutePath,
|
|
39
|
-
commentId,
|
|
40
|
-
status: "open",
|
|
41
|
-
anchor: request.range
|
|
42
|
-
? {
|
|
43
|
-
type: "selection",
|
|
44
|
-
excerptStartLine: request.range.startLine,
|
|
45
|
-
excerptStartCol: request.range.startCol,
|
|
46
|
-
excerptEndLine: request.range.endLine,
|
|
47
|
-
excerptEndCol: request.range.endCol
|
|
48
|
-
}
|
|
49
|
-
: { type: "file" },
|
|
50
|
-
createdAt
|
|
51
|
-
};
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
function buildMetaPayload(range, sourceMeta) {
|
|
55
|
-
const payload = {
|
|
56
|
-
status: "open"
|
|
57
|
-
};
|
|
58
|
-
if (range) {
|
|
59
|
-
payload.anchor = "selection";
|
|
60
|
-
payload.excerpt = {
|
|
61
|
-
start_line: range.startLine,
|
|
62
|
-
start_col: range.startCol,
|
|
63
|
-
end_line: range.endLine,
|
|
64
|
-
end_col: range.endCol
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
if (sourceMeta && Object.keys(sourceMeta).length > 0) {
|
|
68
|
-
payload.signature = sourceMeta;
|
|
69
|
-
}
|
|
70
|
-
return payload;
|
|
71
|
-
}
|
|
72
|
-
function buildCommentEntryLines(commentId, timestamp, author, message, meta64) {
|
|
73
|
-
const safeAuthor = author.trim() || "Saurus";
|
|
74
|
-
const normalizedMessageLines = message
|
|
75
|
-
.replace(/\r\n/g, "\n")
|
|
76
|
-
.split("\n")
|
|
77
|
-
.map((line) => line.trimEnd());
|
|
78
|
-
return [
|
|
79
|
-
`<!-- comment: ${commentId} -->`,
|
|
80
|
-
`<!-- meta64: ${meta64} -->`,
|
|
81
|
-
`> _${timestamp} | ${safeAuthor}_`,
|
|
82
|
-
">",
|
|
83
|
-
...normalizedMessageLines.map((line) => `> ${line}`)
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
function applyCommentEntry(lines, sentinelState, entryLines) {
|
|
87
|
-
if (!sentinelState.hasSentinels) {
|
|
88
|
-
const baseLines = trimTrailingBlankLines(lines);
|
|
89
|
-
return [
|
|
90
|
-
...baseLines,
|
|
91
|
-
"",
|
|
92
|
-
START_SENTINEL,
|
|
93
|
-
"",
|
|
94
|
-
...entryLines,
|
|
95
|
-
"",
|
|
96
|
-
END_SENTINEL
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
const startIndex = sentinelState.startIndex;
|
|
100
|
-
const endIndex = sentinelState.endIndex;
|
|
101
|
-
const block = lines.slice(startIndex + 1, endIndex);
|
|
102
|
-
const trimmedBlock = trimOuterBlankLines(block);
|
|
103
|
-
const nextBlock = trimmedBlock.length > 0
|
|
104
|
-
? [...trimmedBlock, "", ...entryLines]
|
|
105
|
-
: [...entryLines];
|
|
106
|
-
return [
|
|
107
|
-
...lines.slice(0, startIndex + 1),
|
|
108
|
-
...nextBlock,
|
|
109
|
-
...lines.slice(endIndex)
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
function resolveSentinelState(lines) {
|
|
113
|
-
const startIndexes = findTrimmedLineIndexes(lines, START_SENTINEL);
|
|
114
|
-
const endIndexes = findTrimmedLineIndexes(lines, END_SENTINEL);
|
|
115
|
-
const hasAnySentinel = startIndexes.length > 0 || endIndexes.length > 0;
|
|
116
|
-
if (!hasAnySentinel) {
|
|
117
|
-
return {
|
|
118
|
-
hasSentinels: false,
|
|
119
|
-
startIndex: -1,
|
|
120
|
-
endIndex: -1
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
if (startIndexes.length !== 1 || endIndexes.length !== 1) {
|
|
124
|
-
throw new CommentsCommandError("COMMENT_APPENDIX_INVALID", 5, `Expected exactly one '${START_SENTINEL}' and one '${END_SENTINEL}' marker.`);
|
|
125
|
-
}
|
|
126
|
-
const startIndex = startIndexes[0];
|
|
127
|
-
const endIndex = endIndexes[0];
|
|
128
|
-
if (endIndex <= startIndex) {
|
|
129
|
-
throw new CommentsCommandError("COMMENT_APPENDIX_INVALID", 5, `'${END_SENTINEL}' must appear after '${START_SENTINEL}'.`);
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
hasSentinels: true,
|
|
133
|
-
startIndex,
|
|
134
|
-
endIndex
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function findTrimmedLineIndexes(lines, target) {
|
|
138
|
-
const indexes = [];
|
|
139
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
140
|
-
if (lines[index].trim() === target) {
|
|
141
|
-
indexes.push(index);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return indexes;
|
|
145
|
-
}
|
|
146
|
-
function getNextCommentId(lines, sentinelState) {
|
|
147
|
-
const candidateLines = sentinelState.hasSentinels
|
|
148
|
-
? lines.slice(sentinelState.startIndex + 1, sentinelState.endIndex)
|
|
149
|
-
: lines;
|
|
150
|
-
let maxId = 0;
|
|
151
|
-
for (const line of candidateLines) {
|
|
152
|
-
const trimmed = line.trim();
|
|
153
|
-
const match = trimmed.match(COMMENT_DELIMITER_REGEX) ?? trimmed.match(LEGACY_COMMENT_HEADING_REGEX);
|
|
154
|
-
if (!match || !match[2]) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const numeric = Number.parseInt(match[2], 10);
|
|
158
|
-
if (Number.isFinite(numeric) && numeric > maxId) {
|
|
159
|
-
maxId = numeric;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const next = maxId + 1;
|
|
163
|
-
return `CMT-${String(next).padStart(4, "0")}`;
|
|
164
|
-
}
|
|
165
|
-
function trimTrailingBlankLines(lines) {
|
|
166
|
-
const copy = [...lines];
|
|
167
|
-
while (copy.length > 0 && copy[copy.length - 1].trim().length === 0) {
|
|
168
|
-
copy.pop();
|
|
169
|
-
}
|
|
170
|
-
return copy;
|
|
171
|
-
}
|
|
172
|
-
function trimOuterBlankLines(lines) {
|
|
173
|
-
let start = 0;
|
|
174
|
-
let end = lines.length;
|
|
175
|
-
while (start < end && lines[start].trim().length === 0) {
|
|
176
|
-
start += 1;
|
|
177
|
-
}
|
|
178
|
-
while (end > start && lines[end - 1].trim().length === 0) {
|
|
179
|
-
end -= 1;
|
|
180
|
-
}
|
|
181
|
-
return lines.slice(start, end);
|
|
182
|
-
}
|