@ssobig/writer-cli 0.2.2 → 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 (39) hide show
  1. package/README.md +19 -0
  2. package/asset-repository.js +7 -2
  3. package/config.js +2 -1
  4. package/package.json +1 -1
  5. package/templates/mystery-v1/authoring-view-preference.js +22 -5
  6. package/templates/mystery-v1/character-perspective-preview.js +3 -3
  7. package/templates/mystery-v1/codemirror6-runtime.min.js +28 -0
  8. package/templates/mystery-v1/component-asset-operations.js +19 -6
  9. package/templates/mystery-v1/component-catalog-contract.js +26 -35
  10. package/templates/mystery-v1/component-draft-operations.js +19 -8
  11. package/templates/mystery-v1/component-field-contracts.js +127 -51
  12. package/templates/mystery-v1/component-id-policy.js +1 -1
  13. package/templates/mystery-v1/component-manager.js +21 -11
  14. package/templates/mystery-v1/component-navigation-counts.js +9 -9
  15. package/templates/mystery-v1/component-registry.js +11 -5
  16. package/templates/mystery-v1/component-renderers.js +12 -8
  17. package/templates/mystery-v1/component-storage-contract.js +28 -9
  18. package/templates/mystery-v1/markdown-document-model.js +444 -0
  19. package/templates/mystery-v1/markdown-image-editor.js +320 -0
  20. package/templates/mystery-v1/markdown-live-editor.js +1224 -0
  21. package/templates/mystery-v1/page-header.js +2 -1
  22. package/templates/mystery-v1/timeline-model.js +235 -0
  23. package/tools/writer-cli/package-lock.json +2 -2
  24. package/tools/writer-cli/package.json +1 -1
  25. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +13 -14
  26. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
  27. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
  28. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
  29. package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
  30. package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
  31. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +8 -2
  32. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
  33. package/tools/writer-cli/src/agent-service.cjs +3 -1
  34. package/tools/writer-cli/src/command-registry.cjs +24 -20
  35. package/tools/writer-cli/src/commands.cjs +106 -3
  36. package/tools/writer-cli/src/domain.cjs +296 -1
  37. package/tools/writer-cli/src/gateway.cjs +14 -0
  38. package/tools/writer-cli/src/mutations.cjs +167 -1
  39. package/tools/writer-cli/src/project-import.cjs +27 -22
@@ -10,11 +10,8 @@
10
10
  }
11
11
 
12
12
  function commonCount(data) {
13
- const sections = data?.sections && typeof data.sections === "object" ? data.sections : {};
14
- const deleted = new Set(Array.isArray(data?.deletedBaseSections) ? data.deletedBaseSections : []);
15
- const customIds = new Set((Array.isArray(data?.customSections) ? data.customSections : []).map(item => item?.id).filter(Boolean));
16
- return (Array.isArray(data?.sectionOrder) ? data.sectionOrder : [])
17
- .filter(id => !deleted.has(id) && (Object.hasOwn(sections, id) || customIds.has(id))).length;
13
+ const deleted = new Set(Array.isArray(data?.deletedDocumentIds) ? data.deletedDocumentIds : []);
14
+ return (Array.isArray(data?.documents) ? data.documents : []).filter(document => !deleted.has(document?.id)).length;
18
15
  }
19
16
 
20
17
  function characterCount(data) {
@@ -24,6 +21,11 @@
24
21
  .filter(id => !deleted.has(id) && Object.hasOwn(characters, id)).length;
25
22
  }
26
23
 
24
+ // v2는 항목이, legacy는 사건이 세는 단위다.
25
+ function timelineCount(data) {
26
+ return Array.isArray(data?.entries) ? data.entries.length : listLength(data?.events);
27
+ }
28
+
27
29
  function endingCount(data) {
28
30
  const conditions = Array.isArray(data?.conditions) ? data.conditions : [];
29
31
  const outcomes = Array.isArray(data?.outcomes) ? data.outcomes : [];
@@ -38,16 +40,14 @@
38
40
  function componentItemCount(instance, data = instance?.data) {
39
41
  switch (String(instance?.templateId || "")) {
40
42
  case "ssobig.progress": return listLength(data?.steps);
41
- case "ssobig.common":
42
- case "ssobig.choice": return commonCount(data);
43
+ case "ssobig.common": return commonCount(data);
43
44
  case "ssobig.character": return characterCount(data);
44
45
  case "ssobig.clues": return listLength(data?.clues);
45
46
  case "ssobig.clue-combinations": return listLength(data?.combinations);
46
47
  case "ssobig.ai-response": return listLength(data?.responses);
47
- case "ssobig.timeline": return listLength(data?.events);
48
+ case "ssobig.timeline": return timelineCount(data);
48
49
  case "ssobig.investigation-board": return listLength(data?.nodes);
49
50
  case "ssobig.ending": return endingCount(data);
50
- case "ssobig.postgame": return listLength(data?.truth) + listLength(data?.epilogue);
51
51
  case "ssobig.author-notes": return listLength(data?.notes);
52
52
  case "ssobig.documents": return listLength(data?.blocks);
53
53
  default: return 0;
@@ -48,8 +48,7 @@
48
48
  { key: "image", label: "이미지", description: "단서 이미지 등록과 미리보기" },
49
49
  { key: "location", label: "위치", description: "단서를 발견하거나 사용하는 위치" },
50
50
  { key: "tags", label: "태그", description: "단서 분류와 미리보기 필터" },
51
- { key: "color", label: "컬러", description: "단서 분류용 대표 색상" },
52
- { key: "secret", label: "작가 노트", description: "제작자만 확인하는 단서 메모" }
51
+ { key: "color", label: "컬러", description: "단서 분류용 대표 색상" }
53
52
  ]);
54
53
 
55
54
  const BASIC_FIELD_CONFIGURATION = deepFreeze([
@@ -73,8 +72,7 @@
73
72
  { key: "containers", label: "원고 Container", required: true, description: "캐릭터 기본정보와 원고 section의 묶음·순서" },
74
73
  { key: "image", label: "캐릭터 이미지", required: true, description: "캐릭터 이미지 등록과 미리보기" },
75
74
  { key: "color", label: "컬러", required: true, description: "캐릭터 카드의 대표 색상" },
76
- { key: "tag", label: "태그", required: true, description: "캐릭터의 역할과 분류 태그" },
77
- { key: "authorNote", label: "작가 노트", description: "연기 방향과 설계 의도 메모" }
75
+ { key: "tag", label: "태그", required: true, description: "캐릭터의 역할과 분류 태그" }
78
76
  ]);
79
77
 
80
78
  function template(input) {
@@ -113,7 +111,7 @@
113
111
 
114
112
  const INITIAL_TEMPLATE_CATALOG = deepFreeze(catalogContract.list().map(descriptor => template({
115
113
  templateId: descriptor.templateId,
116
- name: descriptor.tabLabel,
114
+ name: descriptor.displayLabel,
117
115
  required: descriptor.required,
118
116
  internal: descriptor.internal,
119
117
  allowMultiple: descriptor.allowMultiple,
@@ -186,12 +184,20 @@
186
184
  return selected.allowMultiple || !instances.some(instance => instance?.templateId === selected.templateId);
187
185
  }
188
186
 
187
+ function displayLabelFor(instance, defaultLabel = "") {
188
+ const selected = get(instance?.templateId);
189
+ const instanceLabel = String(instance?.tab || "").trim();
190
+ if (selected?.allowMultiple && instanceLabel) return instanceLabel;
191
+ return String(selected?.name || instanceLabel || defaultLabel || instance?.instanceId || instance?.templateId || "");
192
+ }
193
+
189
194
  initialTemplates.forEach(item => register(item));
190
195
  return Object.freeze({
191
196
  register,
192
197
  get,
193
198
  has: id => Boolean(get(id)),
194
199
  canInstantiate,
200
+ displayLabelFor,
195
201
  list: () => [...templates.values()].sort((left, right) => left.templateId.localeCompare(right.templateId))
196
202
  });
197
203
  }
@@ -10,7 +10,6 @@
10
10
  aiResponse: root.WriterWorkbenchAiResponseRenderer,
11
11
  timeline: root.WriterWorkbenchTimelineRenderer,
12
12
  ending: root.WriterWorkbenchEndingRenderer,
13
- postgame: root.WriterWorkbenchPostgameRenderer,
14
13
  authorNotes: root.WriterWorkbenchAuthorNotesRenderer,
15
14
  investigationBoard: root.WriterWorkbenchInvestigationBoardRenderer,
16
15
  documents: root.WriterWorkbenchDocumentRenderers
@@ -26,7 +25,6 @@
26
25
  aiResponse: require("./renderers/ai-response.js"),
27
26
  timeline: require("./renderers/timeline.js"),
28
27
  ending: require("./renderers/ending.js"),
29
- postgame: require("./renderers/postgame.js"),
30
28
  authorNotes: require("./renderers/author-notes.js"),
31
29
  investigationBoard: require("./renderers/investigation-board.js"),
32
30
  documents: require("./renderers/documents.js")
@@ -36,8 +34,8 @@
36
34
  if (root) root.WriterWorkbenchComponentRenderers = api;
37
35
  })(typeof globalThis !== "undefined" ? globalThis : this, function (dependencies) {
38
36
  "use strict";
39
- const { appearance, shared, basic, common, character, clues, clueCombinations, aiResponse, timeline, ending, postgame, authorNotes, investigationBoard, documents } = dependencies;
40
- if (!appearance || !shared || !basic || !common || !character || !clues || !clueCombinations || !aiResponse || !timeline || !ending || !postgame || !authorNotes || !investigationBoard || !documents) throw new Error("Component renderer modules are incomplete");
37
+ const { appearance, shared, basic, common, character, clues, clueCombinations, aiResponse, timeline, ending, authorNotes, investigationBoard, documents } = dependencies;
38
+ if (!appearance || !shared || !basic || !common || !character || !clues || !clueCombinations || !aiResponse || !timeline || !ending || !authorNotes || !investigationBoard || !documents) throw new Error("Component renderer modules are incomplete");
41
39
 
42
40
  const renderers = new Map();
43
41
  function register(renderer) {
@@ -45,9 +43,10 @@
45
43
  if (renderers.has(key)) throw new Error(`중복 Component 렌더러입니다: ${key}`);
46
44
  renderers.set(key, Object.freeze({ ...renderer }));
47
45
  }
48
- [basic, common, { ...common, templateId: "ssobig.choice" }, character, clues, clueCombinations, aiResponse, timeline, ending, postgame, authorNotes, investigationBoard].forEach(register);
46
+ [basic, common, character, clues, clueCombinations, aiResponse, timeline, ending, authorNotes, investigationBoard].forEach(register);
49
47
  Object.entries(documents.definitions).forEach(([templateId, definition]) => register({
50
48
  templateId,
49
+ unifiedAuthoring: definition.unifiedAuthoring === true,
51
50
  renderAuthoring: documents.renderAuthoring,
52
51
  renderPreview: documents.renderPreview,
53
52
  bind: documents.bind
@@ -71,6 +70,8 @@
71
70
  return renderer;
72
71
  }
73
72
  function defaultView() { return "preview"; }
73
+ function usesUnifiedAuthoring(instance) { return resolve(instance).unifiedAuthoring === true; }
74
+ function usesUnifiedAuthoringView(viewModel) { return resolveView(viewModel).unifiedAuthoring === true; }
74
75
  function context(instance, data, extra = {}) {
75
76
  return { instance, data, ...extra, view: shared.rendererView(extra, defaultView(instance)) };
76
77
  }
@@ -90,8 +91,9 @@
90
91
  .replace(/data-renderer-view="(?:input|preview)"/, `data-renderer-view="${view}"`);
91
92
  }
92
93
  function renderAuthoring(instance, data, extra) {
93
- const rendererContext = context(instance, data, extra);
94
- return applyRendererView(resolve(instance).renderAuthoring(rendererContext), rendererContext.view);
94
+ const renderer = resolve(instance);
95
+ const rendererContext = context(instance, data, renderer.unifiedAuthoring === true ? { ...extra, view: "preview" } : extra);
96
+ return applyRendererView(renderer.renderAuthoring(rendererContext), rendererContext.view);
95
97
  }
96
98
  function renderPreview(instance, data = instance?.data, extra) { return resolve(instance).renderPreview(context(instance, data, extra)); }
97
99
  function bind(instance, root, data, extra) {
@@ -102,7 +104,7 @@
102
104
  }
103
105
  function renderAuthoringView(viewModel, extra) {
104
106
  const renderer = resolveView(viewModel);
105
- const rendererContext = viewContext(viewModel, extra);
107
+ const rendererContext = viewContext(viewModel, renderer.unifiedAuthoring === true ? { ...extra, view: "preview" } : extra);
106
108
  return applyRendererView(renderer.renderAuthoring(rendererContext), rendererContext.view);
107
109
  }
108
110
  function renderPreviewView(viewModel, extra) {
@@ -119,6 +121,8 @@
119
121
  resolve,
120
122
  resolveView,
121
123
  defaultView,
124
+ usesUnifiedAuthoring,
125
+ usesUnifiedAuthoringView,
122
126
  renderAuthoring,
123
127
  renderPreview,
124
128
  bind,
@@ -123,20 +123,39 @@
123
123
  || (component.internal && instance.config?.hidden !== true ? "에셋 Component는 내부 전용으로 숨김 처리되어야 합니다." : null);
124
124
  }
125
125
 
126
+ function legacyTimelineError(timeline, characterIds) {
127
+ for (const event of timeline.data.events) {
128
+ const missing = Object.keys(event.memories).find(id => !characterIds.has(id));
129
+ if (missing) return `${timeline.instanceId} timeline memory가 존재하지 않는 character(${missing})를 가리킵니다.`;
130
+ const unassigned = Object.keys(event.unassignedMemories || {});
131
+ const assigned = unassigned.find(id => characterIds.has(id));
132
+ if (assigned) return `${timeline.instanceId} unassigned memory(${assigned})는 현재 character에 연결할 수 있습니다.`;
133
+ const overlap = unassigned.find(id => Object.hasOwn(event.memories, id));
134
+ if (overlap) return `${timeline.instanceId} memory(${overlap})가 assigned와 unassigned에 중복 저장되어 있습니다.`;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ function timelineLaneCharacterError(timeline, characterIds) {
140
+ for (const entry of timeline.data.entries) {
141
+ if (entry.lane.kind === "character" && !characterIds.has(entry.lane.characterId)) {
142
+ return `${timeline.instanceId} timeline 항목이 존재하지 않는 character(${entry.lane.characterId})를 가리킵니다.`;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
126
148
  function compositionRelationError(instances) {
127
149
  const character = instances.find(instance => instance.templateId === "ssobig.character");
128
150
  if (character) {
129
151
  const characterIds = new Set(Object.keys(character.data.characters || {}));
152
+ // 타임라인의 모양 배타, 시간 레벨 참조, lane/when 종류별 규칙은 validateComponentData가 이미 검증한다.
153
+ // 여기서는 다른 Component를 봐야 판정할 수 있는 character 참조만 확인한다.
130
154
  for (const timeline of instances.filter(instance => instance.templateId === "ssobig.timeline")) {
131
- for (const event of timeline.data.events) {
132
- const missing = Object.keys(event.memories).find(id => !characterIds.has(id));
133
- if (missing) return `${timeline.instanceId} timeline memory가 존재하지 않는 character(${missing})를 가리킵니다.`;
134
- const unassigned = Object.keys(event.unassignedMemories || {});
135
- const assigned = unassigned.find(id => characterIds.has(id));
136
- if (assigned) return `${timeline.instanceId} unassigned memory(${assigned})는 현재 character에 연결할 수 있습니다.`;
137
- const overlap = unassigned.find(id => Object.hasOwn(event.memories, id));
138
- if (overlap) return `${timeline.instanceId} memory(${overlap})가 assigned와 unassigned에 중복 저장되어 있습니다.`;
139
- }
155
+ const invalid = Array.isArray(timeline.data.events)
156
+ ? legacyTimelineError(timeline, characterIds)
157
+ : timelineLaneCharacterError(timeline, characterIds);
158
+ if (invalid) return invalid;
140
159
  }
141
160
  }
142
161
  const clues = instances.find(instance => instance.templateId === "ssobig.clues");
@@ -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
+ });