stego-cli 0.3.2 → 0.3.3
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/add-comment.js +224 -24
- package/dist/stego-cli.js +17 -4
- package/package.json +1 -1
|
@@ -20,10 +20,34 @@ export function addCommentToManuscript(request) {
|
|
|
20
20
|
throw new CommentsCommandError("NOT_STEGO_MANUSCRIPT", 3, "File is not recognized as a Stego manuscript (missing frontmatter and comments sentinels).");
|
|
21
21
|
}
|
|
22
22
|
const commentId = getNextCommentId(lines, sentinelState);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const createdAt = now.toISOString();
|
|
25
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
|
|
26
|
+
const timezoneOffsetMinutes = -now.getTimezoneOffset();
|
|
27
|
+
const paragraphIndex = request.range
|
|
28
|
+
? resolveParagraphIndex(lines, lineEnding, sentinelState, request.range)
|
|
29
|
+
: undefined;
|
|
30
|
+
const excerpt = request.range
|
|
31
|
+
? extractExcerptForRange(lines, request.range)
|
|
32
|
+
: undefined;
|
|
33
|
+
const meta = buildMetaPayload({
|
|
34
|
+
createdAt,
|
|
35
|
+
timezone,
|
|
36
|
+
timezoneOffsetMinutes,
|
|
37
|
+
paragraphIndex,
|
|
38
|
+
range: request.range,
|
|
39
|
+
sourceMeta: request.sourceMeta
|
|
40
|
+
});
|
|
25
41
|
const meta64 = Buffer.from(JSON.stringify(meta), "utf8").toString("base64url");
|
|
26
|
-
const entryLines = buildCommentEntryLines(
|
|
42
|
+
const entryLines = buildCommentEntryLines({
|
|
43
|
+
commentId,
|
|
44
|
+
createdAt,
|
|
45
|
+
author: request.author,
|
|
46
|
+
message: request.message,
|
|
47
|
+
meta64,
|
|
48
|
+
paragraphIndex,
|
|
49
|
+
excerpt
|
|
50
|
+
});
|
|
27
51
|
const nextLines = applyCommentEntry(lines, sentinelState, entryLines);
|
|
28
52
|
const nextContent = `${nextLines.join(lineEnding)}${lineEnding}`;
|
|
29
53
|
try {
|
|
@@ -51,37 +75,53 @@ export function addCommentToManuscript(request) {
|
|
|
51
75
|
};
|
|
52
76
|
return result;
|
|
53
77
|
}
|
|
54
|
-
function buildMetaPayload(
|
|
78
|
+
function buildMetaPayload(input) {
|
|
55
79
|
const payload = {
|
|
56
|
-
status: "open"
|
|
80
|
+
status: "open",
|
|
81
|
+
created_at: input.createdAt,
|
|
82
|
+
timezone_offset_minutes: input.timezoneOffsetMinutes
|
|
57
83
|
};
|
|
58
|
-
if (
|
|
59
|
-
payload.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end_line: range.endLine,
|
|
64
|
-
end_col: range.endCol
|
|
65
|
-
};
|
|
84
|
+
if (input.timezone) {
|
|
85
|
+
payload.timezone = input.timezone;
|
|
86
|
+
}
|
|
87
|
+
if (input.paragraphIndex !== undefined) {
|
|
88
|
+
payload.paragraph_index = input.paragraphIndex;
|
|
66
89
|
}
|
|
67
|
-
if (
|
|
68
|
-
payload.
|
|
90
|
+
if (input.range) {
|
|
91
|
+
payload.excerpt_start_line = input.range.startLine;
|
|
92
|
+
payload.excerpt_start_col = input.range.startCol;
|
|
93
|
+
payload.excerpt_end_line = input.range.endLine;
|
|
94
|
+
payload.excerpt_end_col = input.range.endCol;
|
|
95
|
+
}
|
|
96
|
+
if (input.sourceMeta && Object.keys(input.sourceMeta).length > 0) {
|
|
97
|
+
payload.signature = input.sourceMeta;
|
|
69
98
|
}
|
|
70
99
|
return payload;
|
|
71
100
|
}
|
|
72
|
-
function buildCommentEntryLines(
|
|
73
|
-
const safeAuthor = author.trim() || "Saurus";
|
|
74
|
-
const normalizedMessageLines = message
|
|
101
|
+
function buildCommentEntryLines(input) {
|
|
102
|
+
const safeAuthor = input.author.trim() || "Saurus";
|
|
103
|
+
const normalizedMessageLines = input.message
|
|
75
104
|
.replace(/\r\n/g, "\n")
|
|
76
105
|
.split("\n")
|
|
77
106
|
.map((line) => line.trimEnd());
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
const displayTimestamp = formatHumanTimestamp(input.createdAt);
|
|
108
|
+
const headerTimestamp = escapeThreadHeaderPart(displayTimestamp);
|
|
109
|
+
const headerAuthor = escapeThreadHeaderPart(safeAuthor);
|
|
110
|
+
const lines = [
|
|
111
|
+
`<!-- comment: ${input.commentId} -->`,
|
|
112
|
+
`<!-- meta64: ${input.meta64} -->`,
|
|
113
|
+
`> _${headerTimestamp} — ${headerAuthor}_`,
|
|
114
|
+
">"
|
|
84
115
|
];
|
|
116
|
+
if (input.paragraphIndex !== undefined && input.excerpt) {
|
|
117
|
+
const truncatedExcerpt = input.excerpt.length > 100
|
|
118
|
+
? `${input.excerpt.slice(0, 100).trimEnd()}…`
|
|
119
|
+
: input.excerpt;
|
|
120
|
+
lines.push(`> > “${truncatedExcerpt}”`);
|
|
121
|
+
lines.push(">");
|
|
122
|
+
}
|
|
123
|
+
lines.push(...normalizedMessageLines.map((line) => `> ${line}`));
|
|
124
|
+
return lines;
|
|
85
125
|
}
|
|
86
126
|
function applyCommentEntry(lines, sentinelState, entryLines) {
|
|
87
127
|
if (!sentinelState.hasSentinels) {
|
|
@@ -162,6 +202,166 @@ function getNextCommentId(lines, sentinelState) {
|
|
|
162
202
|
const next = maxId + 1;
|
|
163
203
|
return `CMT-${String(next).padStart(4, "0")}`;
|
|
164
204
|
}
|
|
205
|
+
function resolveParagraphIndex(lines, lineEnding, sentinelState, range) {
|
|
206
|
+
const markdownWithoutComments = stripCommentsAppendix(lines, sentinelState, lineEnding);
|
|
207
|
+
const { body, lineOffset } = splitFrontmatterForAnchors(markdownWithoutComments);
|
|
208
|
+
const paragraphs = extractParagraphs(body).map((paragraph) => ({
|
|
209
|
+
...paragraph,
|
|
210
|
+
startLine: paragraph.startLine + lineOffset,
|
|
211
|
+
endLine: paragraph.endLine + lineOffset
|
|
212
|
+
}));
|
|
213
|
+
const matched = findParagraphForLine(paragraphs, range.startLine) ?? findPreviousParagraphForLine(paragraphs, range.startLine);
|
|
214
|
+
return matched?.index;
|
|
215
|
+
}
|
|
216
|
+
function extractExcerptForRange(lines, range) {
|
|
217
|
+
const startLineIndex = range.startLine - 1;
|
|
218
|
+
const endLineIndex = range.endLine - 1;
|
|
219
|
+
if (startLineIndex < 0 || endLineIndex < 0 || startLineIndex >= lines.length || endLineIndex >= lines.length) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (startLineIndex > endLineIndex || (startLineIndex === endLineIndex && range.startCol >= range.endCol)) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const startLineText = lines[startLineIndex] ?? "";
|
|
226
|
+
const endLineText = lines[endLineIndex] ?? "";
|
|
227
|
+
const safeStartCol = clamp(range.startCol, 0, startLineText.length);
|
|
228
|
+
const safeEndCol = clamp(range.endCol, 0, endLineText.length);
|
|
229
|
+
let selected = "";
|
|
230
|
+
if (startLineIndex === endLineIndex) {
|
|
231
|
+
if (safeStartCol >= safeEndCol) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
selected = startLineText.slice(safeStartCol, safeEndCol);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const segments = [];
|
|
238
|
+
segments.push(startLineText.slice(safeStartCol));
|
|
239
|
+
for (let lineIndex = startLineIndex + 1; lineIndex < endLineIndex; lineIndex += 1) {
|
|
240
|
+
segments.push(lines[lineIndex] ?? "");
|
|
241
|
+
}
|
|
242
|
+
segments.push(endLineText.slice(0, safeEndCol));
|
|
243
|
+
selected = segments.join("\n");
|
|
244
|
+
}
|
|
245
|
+
const compact = compactExcerpt(selected);
|
|
246
|
+
return compact || undefined;
|
|
247
|
+
}
|
|
248
|
+
function clamp(value, min, max) {
|
|
249
|
+
if (value < min) {
|
|
250
|
+
return min;
|
|
251
|
+
}
|
|
252
|
+
if (value > max) {
|
|
253
|
+
return max;
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
function compactExcerpt(value, max = 180) {
|
|
258
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
259
|
+
if (compact.length <= max) {
|
|
260
|
+
return compact;
|
|
261
|
+
}
|
|
262
|
+
return `${compact.slice(0, max - 1)}…`;
|
|
263
|
+
}
|
|
264
|
+
function stripCommentsAppendix(lines, sentinelState, lineEnding) {
|
|
265
|
+
if (!sentinelState.hasSentinels) {
|
|
266
|
+
return lines.join(lineEnding);
|
|
267
|
+
}
|
|
268
|
+
let removeStart = sentinelState.startIndex;
|
|
269
|
+
if (removeStart > 0 && lines[removeStart - 1].trim().length === 0) {
|
|
270
|
+
removeStart -= 1;
|
|
271
|
+
}
|
|
272
|
+
const keptLines = [...lines.slice(0, removeStart), ...lines.slice(sentinelState.endIndex + 1)];
|
|
273
|
+
while (keptLines.length > 0 && keptLines[keptLines.length - 1].trim().length === 0) {
|
|
274
|
+
keptLines.pop();
|
|
275
|
+
}
|
|
276
|
+
return keptLines.join(lineEnding);
|
|
277
|
+
}
|
|
278
|
+
function splitFrontmatterForAnchors(markdownText) {
|
|
279
|
+
const frontmatterMatch = markdownText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
280
|
+
if (!frontmatterMatch) {
|
|
281
|
+
return {
|
|
282
|
+
body: markdownText,
|
|
283
|
+
lineOffset: 0
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const consumed = frontmatterMatch[0];
|
|
287
|
+
return {
|
|
288
|
+
body: markdownText.slice(consumed.length),
|
|
289
|
+
lineOffset: countLineBreaks(consumed)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function countLineBreaks(value) {
|
|
293
|
+
const matches = value.match(/\r?\n/g);
|
|
294
|
+
return matches ? matches.length : 0;
|
|
295
|
+
}
|
|
296
|
+
function extractParagraphs(markdownText) {
|
|
297
|
+
const lines = markdownText.split(/\r?\n/);
|
|
298
|
+
const paragraphs = [];
|
|
299
|
+
let currentStart = -1;
|
|
300
|
+
const currentLines = [];
|
|
301
|
+
const flush = (endIndex) => {
|
|
302
|
+
if (currentStart < 0 || currentLines.length === 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const joined = currentLines.join(" ").replace(/\s+/g, " ").trim();
|
|
306
|
+
if (joined.length > 0) {
|
|
307
|
+
paragraphs.push({
|
|
308
|
+
index: paragraphs.length,
|
|
309
|
+
startLine: currentStart + 1,
|
|
310
|
+
endLine: endIndex + 1,
|
|
311
|
+
text: joined
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
currentStart = -1;
|
|
315
|
+
currentLines.length = 0;
|
|
316
|
+
};
|
|
317
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
318
|
+
const trimmed = lines[index].trim();
|
|
319
|
+
if (!trimmed) {
|
|
320
|
+
flush(index - 1);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (currentStart < 0) {
|
|
324
|
+
currentStart = index;
|
|
325
|
+
}
|
|
326
|
+
currentLines.push(trimmed);
|
|
327
|
+
}
|
|
328
|
+
flush(lines.length - 1);
|
|
329
|
+
return paragraphs;
|
|
330
|
+
}
|
|
331
|
+
function findParagraphForLine(paragraphs, lineNumber) {
|
|
332
|
+
return paragraphs.find((paragraph) => lineNumber >= paragraph.startLine && lineNumber <= paragraph.endLine);
|
|
333
|
+
}
|
|
334
|
+
function findPreviousParagraphForLine(paragraphs, lineNumber) {
|
|
335
|
+
for (let index = paragraphs.length - 1; index >= 0; index -= 1) {
|
|
336
|
+
if (paragraphs[index].endLine < lineNumber) {
|
|
337
|
+
return paragraphs[index];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
function formatHumanTimestamp(raw) {
|
|
343
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(raw)) {
|
|
344
|
+
return raw;
|
|
345
|
+
}
|
|
346
|
+
const date = new Date(raw);
|
|
347
|
+
if (Number.isNaN(date.getTime())) {
|
|
348
|
+
return raw;
|
|
349
|
+
}
|
|
350
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
351
|
+
const month = months[date.getUTCMonth()];
|
|
352
|
+
const day = date.getUTCDate();
|
|
353
|
+
const year = date.getUTCFullYear();
|
|
354
|
+
const hours24 = date.getUTCHours();
|
|
355
|
+
const hours = hours24 % 12 || 12;
|
|
356
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
357
|
+
const ampm = hours24 < 12 ? "AM" : "PM";
|
|
358
|
+
return `${month} ${day}, ${year}, ${hours}:${minutes} ${ampm}`;
|
|
359
|
+
}
|
|
360
|
+
function escapeThreadHeaderPart(value) {
|
|
361
|
+
return value
|
|
362
|
+
.replace(/\\/g, "\\\\")
|
|
363
|
+
.replace(/_/g, "\\_");
|
|
364
|
+
}
|
|
165
365
|
function trimTrailingBlankLines(lines) {
|
|
166
366
|
const copy = [...lines];
|
|
167
367
|
while (copy.length > 0 && copy[copy.length - 1].trim().length === 0) {
|
package/dist/stego-cli.js
CHANGED
|
@@ -1476,13 +1476,13 @@ function parseStegoCommentThreads(lines, relativePath, baseLine, issues) {
|
|
|
1476
1476
|
}
|
|
1477
1477
|
const headerQuote = extractQuotedLine(rawRow);
|
|
1478
1478
|
if (headerQuote === undefined) {
|
|
1479
|
-
issues.push(makeIssue("error", "comments", `Invalid thread header '${trimmedRow}'. Expected blockquote header like '> _timestamp
|
|
1479
|
+
issues.push(makeIssue("error", "comments", `Invalid thread header '${trimmedRow}'. Expected blockquote header like '> _timestamp — author_'.`, relativePath, lineNumber));
|
|
1480
1480
|
rowIndex += 1;
|
|
1481
1481
|
continue;
|
|
1482
1482
|
}
|
|
1483
1483
|
const header = parseThreadHeader(headerQuote);
|
|
1484
1484
|
if (!header) {
|
|
1485
|
-
issues.push(makeIssue("error", "comments", `Invalid thread header '${headerQuote.trim()}'. Expected '> _timestamp
|
|
1485
|
+
issues.push(makeIssue("error", "comments", `Invalid thread header '${headerQuote.trim()}'. Expected '> _timestamp — author_'.`, relativePath, lineNumber));
|
|
1486
1486
|
rowIndex += 1;
|
|
1487
1487
|
continue;
|
|
1488
1488
|
}
|
|
@@ -1580,7 +1580,20 @@ function decodeCommentMeta64(encoded, commentId, relativePath, lineNumber, issue
|
|
|
1580
1580
|
issues.push(makeIssue("error", "comments", `Invalid meta64 object for comment ${commentId}.`, relativePath, lineNumber));
|
|
1581
1581
|
return undefined;
|
|
1582
1582
|
}
|
|
1583
|
-
const allowedKeys = new Set([
|
|
1583
|
+
const allowedKeys = new Set([
|
|
1584
|
+
"status",
|
|
1585
|
+
"created_at",
|
|
1586
|
+
"timezone",
|
|
1587
|
+
"timezone_offset_minutes",
|
|
1588
|
+
"paragraph_index",
|
|
1589
|
+
"excerpt_start_line",
|
|
1590
|
+
"excerpt_start_col",
|
|
1591
|
+
"excerpt_end_line",
|
|
1592
|
+
"excerpt_end_col",
|
|
1593
|
+
"anchor",
|
|
1594
|
+
"excerpt",
|
|
1595
|
+
"signature"
|
|
1596
|
+
]);
|
|
1584
1597
|
for (const key of Object.keys(parsed)) {
|
|
1585
1598
|
if (!allowedKeys.has(key)) {
|
|
1586
1599
|
issues.push(makeIssue("error", "comments", `meta64 for comment ${commentId} contains unsupported key '${key}'.`, relativePath, lineNumber));
|
|
@@ -1602,7 +1615,7 @@ function extractQuotedLine(raw) {
|
|
|
1602
1615
|
return quoteMatch[1];
|
|
1603
1616
|
}
|
|
1604
1617
|
function parseThreadHeader(value) {
|
|
1605
|
-
const match = value.trim().match(/^_(.+?)\s
|
|
1618
|
+
const match = value.trim().match(/^_(.+?)\s*(?:—|\|)\s*(.+?)_\s*$/);
|
|
1606
1619
|
if (!match) {
|
|
1607
1620
|
return undefined;
|
|
1608
1621
|
}
|