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