@ssobig/writer-cli 0.3.0 → 0.3.2

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.
Files changed (26) hide show
  1. package/README.md +11 -4
  2. package/asset-repository.js +7 -2
  3. package/package.json +1 -1
  4. package/templates/mystery-v1/codemirror6-runtime.min.js +1 -1
  5. package/templates/mystery-v1/component-asset-operations.js +14 -2
  6. package/templates/mystery-v1/component-field-contracts.js +98 -2
  7. package/templates/mystery-v1/component-navigation-counts.js +6 -1
  8. package/templates/mystery-v1/component-storage-contract.js +28 -9
  9. package/templates/mystery-v1/markdown-document-model.js +444 -0
  10. package/templates/mystery-v1/markdown-image-editor.js +320 -0
  11. package/templates/mystery-v1/markdown-live-editor.js +979 -105
  12. package/templates/mystery-v1/timeline-model.js +235 -0
  13. package/tools/writer-cli/package-lock.json +2 -2
  14. package/tools/writer-cli/package.json +1 -1
  15. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +12 -13
  16. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
  17. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
  18. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
  19. package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
  20. package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
  21. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -1
  22. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
  23. package/tools/writer-cli/src/command-registry.cjs +24 -23
  24. package/tools/writer-cli/src/commands.cjs +22 -1
  25. package/tools/writer-cli/src/domain.cjs +46 -0
  26. package/tools/writer-cli/src/project-import.cjs +15 -1
@@ -0,0 +1,444 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === "object" && module.exports) module.exports = api;
4
+ if (root) root.WriterMarkdownDocumentModel = api;
5
+ })(typeof globalThis !== "undefined" ? globalThis : this, function () {
6
+ "use strict";
7
+
8
+ function normalizedSource(value, preserveBlankLines = false) {
9
+ const source = String(value ?? "").replace(/\r\n?/g, "\n");
10
+ if (preserveBlankLines) return source;
11
+ return source
12
+ .replace(/^(?:[ \t]*\n)+/, "")
13
+ .replace(/(?:\n[ \t]*)+$/, "");
14
+ }
15
+
16
+ function lineStarts(source) {
17
+ const starts = [0];
18
+ for (let index = 0; index < source.length; index += 1) {
19
+ if (source[index] === "\n") starts.push(index + 1);
20
+ }
21
+ return starts;
22
+ }
23
+
24
+ // Only project-owned asset references are renderable; URLs and paths stay text.
25
+ function imageReference(value) {
26
+ const match = String(value ?? "").match(/^ {0,3}!\[([^\]\n]*)\]\(asset:([a-zA-Z0-9_-]{1,160})\)[ \t]*$/);
27
+ return match ? { alt: match[1], assetId: match[2] } : null;
28
+ }
29
+
30
+ function imageMarkdown(assetId, alt = "") {
31
+ if (!/^[a-zA-Z0-9_-]{1,160}$/.test(String(assetId))) throw new Error("올바른 이미지 에셋 ID가 아닙니다.");
32
+ return `![${String(alt).replace(/[\[\]\r\n]/g, " ")}](asset:${assetId})`;
33
+ }
34
+
35
+ function parseLine(text, index = 0, from = 0) {
36
+ const source = String(text ?? "");
37
+ const heading = source.match(/^( {0,3})(#{1,4})(?:[ \t]+)(.*)$/);
38
+ const quote = source.match(/^( {0,3})>[ \t]?(.*)$/);
39
+ const unordered = source.match(/^( {0,3})([-*])([ \t]+)(.*)$/);
40
+ const ordered = source.match(/^( {0,3})(\d+)([.)])([ \t]+)(.*)$/);
41
+ const list = unordered || ordered;
42
+ const listType = unordered ? "unordered" : ordered ? "ordered" : "";
43
+ const marker = unordered ? unordered[2] : ordered ? `${ordered[2]}${ordered[3]}` : "";
44
+ const indent = list ? list[1] : "";
45
+ const separator = unordered ? unordered[3] : ordered ? ordered[4] : "";
46
+ const body = unordered ? unordered[4] : ordered ? ordered[5] : "";
47
+ const markerFrom = list ? indent.length : -1;
48
+ const markerTo = list ? markerFrom + marker.length : -1;
49
+
50
+ return Object.freeze({
51
+ index,
52
+ text: source,
53
+ from,
54
+ to: from + source.length,
55
+ empty: !source.trim(),
56
+ headingLevel: heading ? heading[2].length : 0,
57
+ headingMarkerFrom: heading ? heading[1].length : -1,
58
+ headingMarkerTo: heading ? source.length - heading[3].length : -1,
59
+ headingBody: heading ? heading[3] : "",
60
+ quoteMarkerFrom: quote ? quote[1].length : -1,
61
+ quoteMarkerTo: quote ? quote[0].length - quote[2].length : -1,
62
+ quoteBody: quote ? quote[2] : "",
63
+ listType,
64
+ listIndent: indent,
65
+ listIndentTo: list ? indent.length : -1,
66
+ listMarker: marker,
67
+ listMarkerFrom: markerFrom,
68
+ listMarkerTo: markerTo,
69
+ listPrefixTo: list ? markerTo + separator.length : -1,
70
+ listSeparator: separator,
71
+ listBody: body,
72
+ listNumberText: ordered ? ordered[2] : "",
73
+ listNumber: ordered ? Number.parseInt(ordered[2], 10) : null,
74
+ listDelimiter: ordered ? ordered[3] : unordered ? unordered[2] : ""
75
+ });
76
+ }
77
+
78
+ function markerCharacterWidth(marker) {
79
+ return Math.max(1, [...String(marker || "")].length);
80
+ }
81
+
82
+ function isEscaped(source, index) {
83
+ let backslashes = 0;
84
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) {
85
+ backslashes += 1;
86
+ }
87
+ return backslashes % 2 === 1;
88
+ }
89
+
90
+ function backtickRunLength(source, index) {
91
+ let cursor = index;
92
+ while (source[cursor] === "`") cursor += 1;
93
+ return cursor - index;
94
+ }
95
+
96
+ function closingCodeSpan(source, from, markerLength) {
97
+ let cursor = from;
98
+ while (cursor < source.length) {
99
+ if (source[cursor] !== "`") {
100
+ cursor += 1;
101
+ continue;
102
+ }
103
+ const runLength = backtickRunLength(source, cursor);
104
+ if (runLength === markerLength) return cursor + runLength;
105
+ cursor += runLength;
106
+ }
107
+ return -1;
108
+ }
109
+
110
+ function tablePipeOffsets(value) {
111
+ const source = String(value ?? "");
112
+ const offsets = [];
113
+ let index = 0;
114
+ while (index < source.length) {
115
+ if (source[index] === "`" && !isEscaped(source, index)) {
116
+ const markerLength = backtickRunLength(source, index);
117
+ const closeTo = closingCodeSpan(source, index + markerLength, markerLength);
118
+ if (closeTo >= 0) {
119
+ index = closeTo;
120
+ continue;
121
+ }
122
+ index += markerLength;
123
+ continue;
124
+ }
125
+ if (source[index] === "|" && !isEscaped(source, index)) offsets.push(index);
126
+ index += 1;
127
+ }
128
+ return offsets;
129
+ }
130
+
131
+ function unescapeTableCellPipes(value) {
132
+ const source = String(value ?? "");
133
+ let result = "";
134
+ let index = 0;
135
+ while (index < source.length) {
136
+ if (source[index] !== "\\") {
137
+ result += source[index];
138
+ index += 1;
139
+ continue;
140
+ }
141
+ let cursor = index;
142
+ while (source[cursor] === "\\") cursor += 1;
143
+ const count = cursor - index;
144
+ if (source[cursor] === "|" && count % 2 === 1) {
145
+ result += "\\".repeat(count - 1);
146
+ result += "|";
147
+ index = cursor + 1;
148
+ continue;
149
+ }
150
+ result += "\\".repeat(count);
151
+ index = cursor;
152
+ }
153
+ return result;
154
+ }
155
+
156
+ function tableCell(source, lineIndex, lineFrom, rawFrom, rawTo, columnIndex) {
157
+ let contentFrom = rawFrom;
158
+ let contentTo = rawTo;
159
+ while (contentFrom < contentTo && /[ \t]/.test(source[contentFrom])) contentFrom += 1;
160
+ while (contentTo > contentFrom && /[ \t]/.test(source[contentTo - 1])) contentTo -= 1;
161
+ const raw = source.slice(rawFrom, rawTo);
162
+ return {
163
+ type: "tableCell",
164
+ columnIndex,
165
+ lineIndex,
166
+ from: lineFrom + contentFrom,
167
+ to: lineFrom + contentTo,
168
+ rawFrom: lineFrom + rawFrom,
169
+ rawTo: lineFrom + rawTo,
170
+ raw,
171
+ text: unescapeTableCellPipes(source.slice(contentFrom, contentTo)),
172
+ synthetic: false
173
+ };
174
+ }
175
+
176
+ function parseTableRow(text, index = 0, from = 0) {
177
+ const source = String(text ?? "");
178
+ const firstContent = source.search(/[^ \t]/);
179
+ if (firstContent < 0 || firstContent > 3 || /\t/.test(source.slice(0, firstContent))) return null;
180
+ let lastContent = source.length - 1;
181
+ while (lastContent >= 0 && /[ \t]/.test(source[lastContent])) lastContent -= 1;
182
+
183
+ const offsets = tablePipeOffsets(source);
184
+ const offsetSet = new Set(offsets);
185
+ const hasLeadingPipe = source[firstContent] === "|" && offsetSet.has(firstContent);
186
+ const hasTrailingPipe = source[lastContent] === "|" && offsetSet.has(lastContent);
187
+ const contentFrom = hasLeadingPipe ? firstContent + 1 : 0;
188
+ const contentTo = hasTrailingPipe ? lastContent : source.length;
189
+ if (contentFrom > contentTo) return null;
190
+
191
+ const separators = offsets.filter(offset => offset >= contentFrom && offset < contentTo);
192
+ const cells = [];
193
+ let cellFrom = contentFrom;
194
+ separators.forEach((separator, columnIndex) => {
195
+ cells.push(tableCell(source, index, from, cellFrom, separator, columnIndex));
196
+ cellFrom = separator + 1;
197
+ });
198
+ cells.push(tableCell(source, index, from, cellFrom, contentTo, cells.length));
199
+
200
+ return {
201
+ type: "tableRow",
202
+ role: "",
203
+ lineIndex: index,
204
+ from,
205
+ to: from + source.length,
206
+ raw: source,
207
+ cells,
208
+ pipes: offsets.map(offset => ({ from: from + offset, to: from + offset + 1 })),
209
+ hasLeadingPipe,
210
+ hasTrailingPipe
211
+ };
212
+ }
213
+
214
+ function parseTableDelimiterCell(cell) {
215
+ const match = String(cell?.text ?? "").match(/^(:)?(-{3,})(:)?$/);
216
+ if (!match) return null;
217
+ const alignment = match[1] && match[3]
218
+ ? "center"
219
+ : match[1]
220
+ ? "left"
221
+ : match[3]
222
+ ? "right"
223
+ : null;
224
+ return { ...cell, type: "tableDelimiterCell", alignment };
225
+ }
226
+
227
+ function tableRowWithRole(row, role, columnCount = row.cells.length) {
228
+ const cells = row.cells.slice();
229
+ while (cells.length < columnCount) {
230
+ cells.push({
231
+ type: "tableCell",
232
+ columnIndex: cells.length,
233
+ lineIndex: row.lineIndex,
234
+ from: row.to,
235
+ to: row.to,
236
+ rawFrom: row.to,
237
+ rawTo: row.to,
238
+ raw: "",
239
+ text: "",
240
+ synthetic: true
241
+ });
242
+ }
243
+ return { ...row, role, cells };
244
+ }
245
+
246
+ function parseTableBlock(lines, startIndex, source) {
247
+ const headerLine = lines[startIndex];
248
+ const delimiterLine = lines[startIndex + 1];
249
+ if (!headerLine || !delimiterLine) return null;
250
+ if (
251
+ headerLine.empty || headerLine.headingLevel || headerLine.quoteMarkerFrom >= 0 || headerLine.listType ||
252
+ delimiterLine.empty || delimiterLine.headingLevel || delimiterLine.quoteMarkerFrom >= 0 || delimiterLine.listType
253
+ ) return null;
254
+
255
+ const parsedHeader = parseTableRow(headerLine.text, headerLine.index, headerLine.from);
256
+ const parsedDelimiter = parseTableRow(delimiterLine.text, delimiterLine.index, delimiterLine.from);
257
+ if (!parsedHeader || !parsedDelimiter) return null;
258
+ if (!parsedHeader.pipes.length && !parsedDelimiter.pipes.length) return null;
259
+ if (!parsedHeader.cells.length || parsedHeader.cells.length !== parsedDelimiter.cells.length) return null;
260
+
261
+ const delimiterCells = parsedDelimiter.cells.map(parseTableDelimiterCell);
262
+ if (delimiterCells.some(cell => !cell)) return null;
263
+ const columnCount = parsedHeader.cells.length;
264
+ const rows = [];
265
+ let nextIndex = startIndex + 2;
266
+ while (nextIndex < lines.length) {
267
+ const line = lines[nextIndex];
268
+ if (line.empty || line.headingLevel || line.quoteMarkerFrom >= 0 || line.listType) break;
269
+ const parsedRow = parseTableRow(line.text, line.index, line.from);
270
+ if (!parsedRow || !parsedRow.pipes.length) break;
271
+ if (parsedRow.cells.length > columnCount) return null;
272
+ rows.push(tableRowWithRole(parsedRow, "body", columnCount));
273
+ nextIndex += 1;
274
+ }
275
+
276
+ const header = tableRowWithRole(parsedHeader, "header");
277
+ const delimiter = {
278
+ ...parsedDelimiter,
279
+ role: "delimiter",
280
+ cells: delimiterCells
281
+ };
282
+ const allRows = [header, delimiter, ...rows];
283
+ const from = header.from;
284
+ const to = allRows.at(-1).to;
285
+ return {
286
+ block: {
287
+ type: "table",
288
+ from,
289
+ to,
290
+ raw: source.slice(from, to),
291
+ lineIndices: allRows.map(row => row.lineIndex),
292
+ columnCount,
293
+ alignments: delimiterCells.map(cell => cell.alignment),
294
+ header,
295
+ delimiter,
296
+ rows
297
+ },
298
+ nextIndex
299
+ };
300
+ }
301
+
302
+ function parseDocument(value, options = {}) {
303
+ const preserveBlankLines = options.preserveBlankLines === true;
304
+ const source = normalizedSource(value, preserveBlankLines);
305
+ if (!source && !preserveBlankLines) return Object.freeze({ source, lines: Object.freeze([]), blocks: Object.freeze([]) });
306
+
307
+ const starts = lineStarts(source);
308
+ const lines = source.split("\n").map((text, index) => parseLine(text, index, starts[index]));
309
+ const blocks = [];
310
+ let paragraph = null;
311
+ let quote = null;
312
+ let list = null;
313
+
314
+ const closeParagraph = () => { paragraph = null; };
315
+ const closeQuote = () => { quote = null; };
316
+ const closeList = () => {
317
+ if (list) {
318
+ list.markerWidth = Math.max(1, ...list.items.map(item => markerCharacterWidth(item.marker)));
319
+ }
320
+ list = null;
321
+ };
322
+ const closeAll = () => {
323
+ closeParagraph();
324
+ closeQuote();
325
+ closeList();
326
+ };
327
+ const appendParagraph = line => {
328
+ if (!paragraph) {
329
+ paragraph = { type: "paragraph", lines: [], lineIndices: [] };
330
+ blocks.push(paragraph);
331
+ }
332
+ paragraph.lines.push(line.text);
333
+ paragraph.lineIndices.push(line.index);
334
+ };
335
+ const appendQuote = (container, line) => {
336
+ let current = container.at(-1);
337
+ if (!current || current.type !== "quote") {
338
+ current = { type: "quote", lines: [], lineIndices: [] };
339
+ container.push(current);
340
+ }
341
+ current.lines.push(line.quoteBody);
342
+ current.lineIndices.push(line.index);
343
+ return current;
344
+ };
345
+
346
+ for (let lineIndex = 0; lineIndex < lines.length;) {
347
+ const line = lines[lineIndex];
348
+ const image = imageReference(line.text);
349
+ if (image) {
350
+ closeAll();
351
+ blocks.push({ type: "image", ...image, from: line.from, to: line.to, lineIndex });
352
+ lineIndex += 1;
353
+ continue;
354
+ }
355
+ const table = parseTableBlock(lines, lineIndex, source);
356
+ if (table) {
357
+ closeAll();
358
+ blocks.push(table.block);
359
+ lineIndex = table.nextIndex;
360
+ continue;
361
+ }
362
+
363
+ if (line.quoteMarkerFrom >= 0) {
364
+ closeParagraph();
365
+ if (list?.items.length) {
366
+ quote = null;
367
+ appendQuote(list.items.at(-1).children, line);
368
+ } else {
369
+ closeList();
370
+ if (!quote) {
371
+ quote = { type: "quote", lines: [], lineIndices: [] };
372
+ blocks.push(quote);
373
+ }
374
+ quote.lines.push(line.quoteBody);
375
+ quote.lineIndices.push(line.index);
376
+ }
377
+ lineIndex += 1;
378
+ continue;
379
+ }
380
+
381
+ closeQuote();
382
+ if (line.empty) {
383
+ closeAll();
384
+ blocks.push({ type: "blank", lineIndex: line.index });
385
+ lineIndex += 1;
386
+ continue;
387
+ }
388
+
389
+ if (line.headingLevel) {
390
+ closeAll();
391
+ blocks.push({ type: "heading", level: line.headingLevel, text: line.headingBody, lineIndex: line.index });
392
+ lineIndex += 1;
393
+ continue;
394
+ }
395
+
396
+ if (line.listType) {
397
+ closeParagraph();
398
+ if (list && list.listType !== line.listType) closeList();
399
+ if (!list) {
400
+ list = { type: "list", listType: line.listType, items: [], lineIndices: [], markerWidth: 1 };
401
+ blocks.push(list);
402
+ }
403
+ list.items.push({
404
+ type: "listItem",
405
+ marker: line.listMarker,
406
+ number: line.listNumber,
407
+ delimiter: line.listDelimiter,
408
+ indent: line.listIndent,
409
+ separator: line.listSeparator,
410
+ body: line.listBody,
411
+ numberText: line.listNumberText,
412
+ lineIndex: line.index,
413
+ children: []
414
+ });
415
+ list.lineIndices.push(line.index);
416
+ lineIndex += 1;
417
+ continue;
418
+ }
419
+
420
+ closeList();
421
+ appendParagraph(line);
422
+ lineIndex += 1;
423
+ }
424
+ closeList();
425
+
426
+ return Object.freeze({
427
+ source,
428
+ lines: Object.freeze(lines),
429
+ blocks: Object.freeze(blocks)
430
+ });
431
+ }
432
+
433
+ return Object.freeze({
434
+ imageReference,
435
+ imageMarkdown,
436
+ normalizedSource,
437
+ lineStarts,
438
+ parseLine,
439
+ parseTableRow,
440
+ parseTableDelimiterCell,
441
+ parseDocument,
442
+ markerCharacterWidth
443
+ });
444
+ });