@quolu/lattice 0.52.2 → 0.52.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/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
import { unified } from 'unified';
|
|
2
|
-
import remarkGfm from 'remark-gfm';
|
|
3
|
-
import remarkParse from 'remark-parse';
|
|
4
|
-
|
|
5
|
-
export const TODO_MARKDOWN_SECTION_MAX_BYTES = 256 * 1024;
|
|
6
|
-
|
|
7
|
-
const markdownParser = unified().use(remarkParse).use(remarkGfm);
|
|
8
|
-
const unicodeDirectionControl = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
9
|
-
const unicodeDirectionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
|
|
10
|
-
const scriptUnsafeCharacters = /[<>&\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu;
|
|
11
|
-
|
|
12
|
-
export class TodoMarkdownRenderError extends Error {
|
|
13
|
-
constructor(code, message, detail = {}) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = 'TodoMarkdownRenderError';
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.detail = detail;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class TodoMarkdownSectionTooLargeError extends TodoMarkdownRenderError {
|
|
22
|
-
constructor(actualBytes) {
|
|
23
|
-
super(
|
|
24
|
-
'TODO_MARKDOWN_SECTION_TOO_LARGE',
|
|
25
|
-
`todo Markdown section is ${actualBytes} bytes; maximum is ${TODO_MARKDOWN_SECTION_MAX_BYTES}`,
|
|
26
|
-
{ actual_bytes: actualBytes, maximum_bytes: TODO_MARKDOWN_SECTION_MAX_BYTES },
|
|
27
|
-
);
|
|
28
|
-
this.name = 'TodoMarkdownSectionTooLargeError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function escapeHtml(value) {
|
|
33
|
-
return value.replaceAll('&', '&')
|
|
34
|
-
.replaceAll('<', '<')
|
|
35
|
-
.replaceAll('>', '>')
|
|
36
|
-
.replaceAll('"', '"')
|
|
37
|
-
.replaceAll("'", ''');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isNode(value) {
|
|
41
|
-
return value !== null && typeof value === 'object' && typeof value.type === 'string';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function safeHttpUrl(value) {
|
|
45
|
-
if (typeof value !== 'string' || !/^https?:\/\//iu.test(value)) return null;
|
|
46
|
-
if (/[\u0000-\u0020\u007f]/u.test(value)) return null;
|
|
47
|
-
if (unicodeDirectionControl.test(value)) return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(value);
|
|
50
|
-
if ((parsed.protocol !== 'https:' && parsed.protocol !== 'http:') || parsed.hostname === '') {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
return parsed.href;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function renderChildren(node, path, state, context = {}) {
|
|
60
|
-
if (!Array.isArray(node.children)) {
|
|
61
|
-
state.discarded.push({ path, type: node.type, reason: 'invalid_children' });
|
|
62
|
-
return '';
|
|
63
|
-
}
|
|
64
|
-
return node.children
|
|
65
|
-
.map((child, index) => renderNode(child, `${path}.children[${index}]`, state, context))
|
|
66
|
-
.join('');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function renderText(node, path, state) {
|
|
70
|
-
if (typeof node.value !== 'string') {
|
|
71
|
-
state.discarded.push({ path, type: node.type, reason: 'invalid_value' });
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
const withoutDirectionControls = node.value.replace(unicodeDirectionControls, '');
|
|
75
|
-
if (withoutDirectionControls !== node.value) {
|
|
76
|
-
state.discarded.push({ path, type: node.type, reason: 'unicode_direction_control' });
|
|
77
|
-
}
|
|
78
|
-
return escapeHtml(withoutDirectionControls);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function renderNode(node, path, state, context = {}) {
|
|
82
|
-
if (!isNode(node)) {
|
|
83
|
-
state.discarded.push({ path, type: 'invalid', reason: 'invalid_node' });
|
|
84
|
-
return '';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
switch (node.type) {
|
|
88
|
-
case 'root':
|
|
89
|
-
return renderChildren(node, path, state, context);
|
|
90
|
-
case 'text':
|
|
91
|
-
return renderText(node, path, state);
|
|
92
|
-
case 'heading': {
|
|
93
|
-
if (!Number.isInteger(node.depth) || node.depth < 1 || node.depth > 6) {
|
|
94
|
-
state.discarded.push({ path, type: node.type, reason: 'invalid_depth' });
|
|
95
|
-
return '';
|
|
96
|
-
}
|
|
97
|
-
return `<h${node.depth}>${renderChildren(node, path, state)}</h${node.depth}>`;
|
|
98
|
-
}
|
|
99
|
-
case 'paragraph': {
|
|
100
|
-
const content = renderChildren(node, path, state);
|
|
101
|
-
return content === '' ? '' : `<p>${content}</p>`;
|
|
102
|
-
}
|
|
103
|
-
case 'list': {
|
|
104
|
-
const tag = node.ordered === true ? 'ol' : 'ul';
|
|
105
|
-
let start = '';
|
|
106
|
-
if (tag === 'ol' && Number.isSafeInteger(node.start) && node.start > 1) {
|
|
107
|
-
start = ` start="${node.start}"`;
|
|
108
|
-
}
|
|
109
|
-
return `<${tag}${start}>${renderChildren(node, path, state, context)}</${tag}>`;
|
|
110
|
-
}
|
|
111
|
-
case 'listItem': {
|
|
112
|
-
const line = node.position?.start?.line;
|
|
113
|
-
const taskState = context.taskCheckboxes === true && typeof node.checked === 'boolean'
|
|
114
|
-
&& Number.isSafeInteger(line) ? context.taskStatesByLine?.get(line) : undefined;
|
|
115
|
-
const checkbox = taskState === undefined
|
|
116
|
-
? (context.taskCheckboxes === true && typeof node.checked === 'boolean'
|
|
117
|
-
? `<span title="状態表示(更新は lattice todo CLI)" class="markdown-checkbox" role="img" aria-label="${node.checked ? 'checked' : 'unchecked'}">${node.checked ? '☑' : '☐'}</span>` : '')
|
|
118
|
-
: `<span title="状態表示(更新は lattice todo CLI)" class="document-status status-${escapeHtml(String(taskState.status))}" data-narrative-key="${escapeHtml(String(taskState.narrativeKey))}" role="img" aria-label="${escapeHtml(String(taskState.label))}">${escapeHtml(String(taskState.mark))}</span>`;
|
|
119
|
-
const taskClass = checkbox === '' ? '' : ' class="markdown-task"';
|
|
120
|
-
const blockedReason = taskState?.status === 'blocked'
|
|
121
|
-
? `<span class="blocked-reason"> — ${escapeHtml(String(taskState.blockedReason ?? '理由未記録'))}</span>` : '';
|
|
122
|
-
return `<li${taskClass}>${checkbox}${renderChildren(node, path, state, context)}${blockedReason}</li>`;
|
|
123
|
-
}
|
|
124
|
-
case 'strong':
|
|
125
|
-
return `<strong>${renderChildren(node, path, state)}</strong>`;
|
|
126
|
-
case 'emphasis':
|
|
127
|
-
return `<em>${renderChildren(node, path, state)}</em>`;
|
|
128
|
-
case 'inlineCode':
|
|
129
|
-
return `<code>${renderText(node, path, state)}</code>`;
|
|
130
|
-
case 'code':
|
|
131
|
-
return `<pre><code>${renderText(node, path, state)}</code></pre>`;
|
|
132
|
-
case 'blockquote':
|
|
133
|
-
return `<blockquote>${renderChildren(node, path, state, context)}</blockquote>`;
|
|
134
|
-
case 'link': {
|
|
135
|
-
const href = safeHttpUrl(node.url);
|
|
136
|
-
const label = renderChildren(node, path, state);
|
|
137
|
-
if (href === null) {
|
|
138
|
-
state.discarded.push({ path, type: node.type, reason: 'unsafe_url' });
|
|
139
|
-
return label;
|
|
140
|
-
}
|
|
141
|
-
let title = '';
|
|
142
|
-
if (typeof node.title === 'string') {
|
|
143
|
-
const safeTitle = node.title.replace(unicodeDirectionControls, '');
|
|
144
|
-
if (safeTitle !== node.title) {
|
|
145
|
-
state.discarded.push({ path, type: node.type, reason: 'unicode_direction_control' });
|
|
146
|
-
}
|
|
147
|
-
title = ` title="${escapeHtml(safeTitle)}"`;
|
|
148
|
-
}
|
|
149
|
-
return `<a href="${escapeHtml(href)}"${title}>${label}</a>`;
|
|
150
|
-
}
|
|
151
|
-
case 'break':
|
|
152
|
-
return '<br>';
|
|
153
|
-
case 'thematicBreak':
|
|
154
|
-
return '<hr>';
|
|
155
|
-
case 'table': {
|
|
156
|
-
if (!Array.isArray(node.children)) {
|
|
157
|
-
state.discarded.push({ path, type: node.type, reason: 'invalid_children' });
|
|
158
|
-
return '';
|
|
159
|
-
}
|
|
160
|
-
const rows = node.children.map((child, index) => renderNode(
|
|
161
|
-
child,
|
|
162
|
-
`${path}.children[${index}]`,
|
|
163
|
-
state,
|
|
164
|
-
{ ...context, tableHeader: index === 0 },
|
|
165
|
-
)).join('');
|
|
166
|
-
return `<table><tbody>${rows}</tbody></table>`;
|
|
167
|
-
}
|
|
168
|
-
case 'tableRow':
|
|
169
|
-
return `<tr>${renderChildren(node, path, state, context)}</tr>`;
|
|
170
|
-
case 'tableCell': {
|
|
171
|
-
const tag = context.tableHeader ? 'th' : 'td';
|
|
172
|
-
return `<${tag}>${renderChildren(node, path, state, context)}</${tag}>`;
|
|
173
|
-
}
|
|
174
|
-
case 'html':
|
|
175
|
-
state.discarded.push({ path, type: node.type, reason: 'raw_html' });
|
|
176
|
-
return '';
|
|
177
|
-
case 'image':
|
|
178
|
-
state.discarded.push({ path, type: node.type, reason: 'image' });
|
|
179
|
-
return '';
|
|
180
|
-
default:
|
|
181
|
-
state.discarded.push({ path, type: node.type, reason: 'unknown_type' });
|
|
182
|
-
return '';
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function renderTodoMarkdownAst(tree) {
|
|
187
|
-
if (!isNode(tree) || tree.type !== 'root') {
|
|
188
|
-
throw new TodoMarkdownRenderError(
|
|
189
|
-
'TODO_MARKDOWN_INVALID_AST',
|
|
190
|
-
'todo Markdown AST must be an mdast root node',
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
const state = { discarded: [] };
|
|
194
|
-
return {
|
|
195
|
-
html: renderNode(tree, 'root', state),
|
|
196
|
-
discarded: state.discarded,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function renderTodoMarkdown(markdown) {
|
|
201
|
-
if (typeof markdown !== 'string') {
|
|
202
|
-
throw new TodoMarkdownRenderError(
|
|
203
|
-
'TODO_MARKDOWN_INVALID_INPUT',
|
|
204
|
-
'todo Markdown section must be a string',
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
const actualBytes = Buffer.byteLength(markdown, 'utf8');
|
|
208
|
-
if (actualBytes > TODO_MARKDOWN_SECTION_MAX_BYTES) {
|
|
209
|
-
throw new TodoMarkdownSectionTooLargeError(actualBytes);
|
|
210
|
-
}
|
|
211
|
-
return renderTodoMarkdownAst(markdownParser.parse(markdown));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function parseTodoMarkdownDocument(markdown) {
|
|
215
|
-
if (typeof markdown !== 'string') {
|
|
216
|
-
throw new TodoMarkdownRenderError(
|
|
217
|
-
'TODO_MARKDOWN_INVALID_INPUT',
|
|
218
|
-
'todo Markdown section must be a string',
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
const actualBytes = Buffer.byteLength(markdown, 'utf8');
|
|
222
|
-
if (actualBytes > TODO_MARKDOWN_SECTION_MAX_BYTES) {
|
|
223
|
-
throw new TodoMarkdownSectionTooLargeError(actualBytes);
|
|
224
|
-
}
|
|
225
|
-
return markdownParser.parse(markdown);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function renderTodoMarkdownDocument(markdown, { taskStatesByLine = new Map() } = {}) {
|
|
229
|
-
if (!(taskStatesByLine instanceof Map)) {
|
|
230
|
-
throw new TodoMarkdownRenderError(
|
|
231
|
-
'TODO_MARKDOWN_INVALID_OPTIONS',
|
|
232
|
-
'taskStatesByLine must be a Map',
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const tree = parseTodoMarkdownDocument(markdown);
|
|
236
|
-
if (!isNode(tree) || tree.type !== 'root') {
|
|
237
|
-
throw new TodoMarkdownRenderError(
|
|
238
|
-
'TODO_MARKDOWN_INVALID_AST',
|
|
239
|
-
'todo Markdown AST must be an mdast root node',
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
const state = { discarded: [] };
|
|
243
|
-
return {
|
|
244
|
-
html: renderNode(tree, 'root', state, { taskCheckboxes: true, taskStatesByLine }),
|
|
245
|
-
discarded: state.discarded,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function serializeJsonForScript(value) {
|
|
250
|
-
const serialized = JSON.stringify(value);
|
|
251
|
-
if (serialized === undefined) {
|
|
252
|
-
throw new TodoMarkdownRenderError(
|
|
253
|
-
'TODO_MARKDOWN_JSON_NOT_SERIALIZABLE',
|
|
254
|
-
'script JSON value must be JSON-serializable',
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return serialized.replace(scriptUnsafeCharacters, (character) => (
|
|
258
|
-
`\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`
|
|
259
|
-
));
|
|
260
|
-
}
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import remarkParse from 'remark-parse';
|
|
4
|
+
|
|
5
|
+
export const TODO_MARKDOWN_SECTION_MAX_BYTES = 256 * 1024;
|
|
6
|
+
|
|
7
|
+
const markdownParser = unified().use(remarkParse).use(remarkGfm);
|
|
8
|
+
const unicodeDirectionControl = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
9
|
+
const unicodeDirectionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
|
|
10
|
+
const scriptUnsafeCharacters = /[<>&\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu;
|
|
11
|
+
|
|
12
|
+
export class TodoMarkdownRenderError extends Error {
|
|
13
|
+
constructor(code, message, detail = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'TodoMarkdownRenderError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.detail = detail;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TodoMarkdownSectionTooLargeError extends TodoMarkdownRenderError {
|
|
22
|
+
constructor(actualBytes) {
|
|
23
|
+
super(
|
|
24
|
+
'TODO_MARKDOWN_SECTION_TOO_LARGE',
|
|
25
|
+
`todo Markdown section is ${actualBytes} bytes; maximum is ${TODO_MARKDOWN_SECTION_MAX_BYTES}`,
|
|
26
|
+
{ actual_bytes: actualBytes, maximum_bytes: TODO_MARKDOWN_SECTION_MAX_BYTES },
|
|
27
|
+
);
|
|
28
|
+
this.name = 'TodoMarkdownSectionTooLargeError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeHtml(value) {
|
|
33
|
+
return value.replaceAll('&', '&')
|
|
34
|
+
.replaceAll('<', '<')
|
|
35
|
+
.replaceAll('>', '>')
|
|
36
|
+
.replaceAll('"', '"')
|
|
37
|
+
.replaceAll("'", ''');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isNode(value) {
|
|
41
|
+
return value !== null && typeof value === 'object' && typeof value.type === 'string';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeHttpUrl(value) {
|
|
45
|
+
if (typeof value !== 'string' || !/^https?:\/\//iu.test(value)) return null;
|
|
46
|
+
if (/[\u0000-\u0020\u007f]/u.test(value)) return null;
|
|
47
|
+
if (unicodeDirectionControl.test(value)) return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(value);
|
|
50
|
+
if ((parsed.protocol !== 'https:' && parsed.protocol !== 'http:') || parsed.hostname === '') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed.href;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderChildren(node, path, state, context = {}) {
|
|
60
|
+
if (!Array.isArray(node.children)) {
|
|
61
|
+
state.discarded.push({ path, type: node.type, reason: 'invalid_children' });
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
return node.children
|
|
65
|
+
.map((child, index) => renderNode(child, `${path}.children[${index}]`, state, context))
|
|
66
|
+
.join('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderText(node, path, state) {
|
|
70
|
+
if (typeof node.value !== 'string') {
|
|
71
|
+
state.discarded.push({ path, type: node.type, reason: 'invalid_value' });
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
const withoutDirectionControls = node.value.replace(unicodeDirectionControls, '');
|
|
75
|
+
if (withoutDirectionControls !== node.value) {
|
|
76
|
+
state.discarded.push({ path, type: node.type, reason: 'unicode_direction_control' });
|
|
77
|
+
}
|
|
78
|
+
return escapeHtml(withoutDirectionControls);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderNode(node, path, state, context = {}) {
|
|
82
|
+
if (!isNode(node)) {
|
|
83
|
+
state.discarded.push({ path, type: 'invalid', reason: 'invalid_node' });
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
switch (node.type) {
|
|
88
|
+
case 'root':
|
|
89
|
+
return renderChildren(node, path, state, context);
|
|
90
|
+
case 'text':
|
|
91
|
+
return renderText(node, path, state);
|
|
92
|
+
case 'heading': {
|
|
93
|
+
if (!Number.isInteger(node.depth) || node.depth < 1 || node.depth > 6) {
|
|
94
|
+
state.discarded.push({ path, type: node.type, reason: 'invalid_depth' });
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
return `<h${node.depth}>${renderChildren(node, path, state)}</h${node.depth}>`;
|
|
98
|
+
}
|
|
99
|
+
case 'paragraph': {
|
|
100
|
+
const content = renderChildren(node, path, state);
|
|
101
|
+
return content === '' ? '' : `<p>${content}</p>`;
|
|
102
|
+
}
|
|
103
|
+
case 'list': {
|
|
104
|
+
const tag = node.ordered === true ? 'ol' : 'ul';
|
|
105
|
+
let start = '';
|
|
106
|
+
if (tag === 'ol' && Number.isSafeInteger(node.start) && node.start > 1) {
|
|
107
|
+
start = ` start="${node.start}"`;
|
|
108
|
+
}
|
|
109
|
+
return `<${tag}${start}>${renderChildren(node, path, state, context)}</${tag}>`;
|
|
110
|
+
}
|
|
111
|
+
case 'listItem': {
|
|
112
|
+
const line = node.position?.start?.line;
|
|
113
|
+
const taskState = context.taskCheckboxes === true && typeof node.checked === 'boolean'
|
|
114
|
+
&& Number.isSafeInteger(line) ? context.taskStatesByLine?.get(line) : undefined;
|
|
115
|
+
const checkbox = taskState === undefined
|
|
116
|
+
? (context.taskCheckboxes === true && typeof node.checked === 'boolean'
|
|
117
|
+
? `<span title="状態表示(更新は lattice todo CLI)" class="markdown-checkbox" role="img" aria-label="${node.checked ? 'checked' : 'unchecked'}">${node.checked ? '☑' : '☐'}</span>` : '')
|
|
118
|
+
: `<span title="状態表示(更新は lattice todo CLI)" class="document-status status-${escapeHtml(String(taskState.status))}" data-narrative-key="${escapeHtml(String(taskState.narrativeKey))}" role="img" aria-label="${escapeHtml(String(taskState.label))}">${escapeHtml(String(taskState.mark))}</span>`;
|
|
119
|
+
const taskClass = checkbox === '' ? '' : ' class="markdown-task"';
|
|
120
|
+
const blockedReason = taskState?.status === 'blocked'
|
|
121
|
+
? `<span class="blocked-reason"> — ${escapeHtml(String(taskState.blockedReason ?? '理由未記録'))}</span>` : '';
|
|
122
|
+
return `<li${taskClass}>${checkbox}${renderChildren(node, path, state, context)}${blockedReason}</li>`;
|
|
123
|
+
}
|
|
124
|
+
case 'strong':
|
|
125
|
+
return `<strong>${renderChildren(node, path, state)}</strong>`;
|
|
126
|
+
case 'emphasis':
|
|
127
|
+
return `<em>${renderChildren(node, path, state)}</em>`;
|
|
128
|
+
case 'inlineCode':
|
|
129
|
+
return `<code>${renderText(node, path, state)}</code>`;
|
|
130
|
+
case 'code':
|
|
131
|
+
return `<pre><code>${renderText(node, path, state)}</code></pre>`;
|
|
132
|
+
case 'blockquote':
|
|
133
|
+
return `<blockquote>${renderChildren(node, path, state, context)}</blockquote>`;
|
|
134
|
+
case 'link': {
|
|
135
|
+
const href = safeHttpUrl(node.url);
|
|
136
|
+
const label = renderChildren(node, path, state);
|
|
137
|
+
if (href === null) {
|
|
138
|
+
state.discarded.push({ path, type: node.type, reason: 'unsafe_url' });
|
|
139
|
+
return label;
|
|
140
|
+
}
|
|
141
|
+
let title = '';
|
|
142
|
+
if (typeof node.title === 'string') {
|
|
143
|
+
const safeTitle = node.title.replace(unicodeDirectionControls, '');
|
|
144
|
+
if (safeTitle !== node.title) {
|
|
145
|
+
state.discarded.push({ path, type: node.type, reason: 'unicode_direction_control' });
|
|
146
|
+
}
|
|
147
|
+
title = ` title="${escapeHtml(safeTitle)}"`;
|
|
148
|
+
}
|
|
149
|
+
return `<a href="${escapeHtml(href)}"${title}>${label}</a>`;
|
|
150
|
+
}
|
|
151
|
+
case 'break':
|
|
152
|
+
return '<br>';
|
|
153
|
+
case 'thematicBreak':
|
|
154
|
+
return '<hr>';
|
|
155
|
+
case 'table': {
|
|
156
|
+
if (!Array.isArray(node.children)) {
|
|
157
|
+
state.discarded.push({ path, type: node.type, reason: 'invalid_children' });
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
const rows = node.children.map((child, index) => renderNode(
|
|
161
|
+
child,
|
|
162
|
+
`${path}.children[${index}]`,
|
|
163
|
+
state,
|
|
164
|
+
{ ...context, tableHeader: index === 0 },
|
|
165
|
+
)).join('');
|
|
166
|
+
return `<table><tbody>${rows}</tbody></table>`;
|
|
167
|
+
}
|
|
168
|
+
case 'tableRow':
|
|
169
|
+
return `<tr>${renderChildren(node, path, state, context)}</tr>`;
|
|
170
|
+
case 'tableCell': {
|
|
171
|
+
const tag = context.tableHeader ? 'th' : 'td';
|
|
172
|
+
return `<${tag}>${renderChildren(node, path, state, context)}</${tag}>`;
|
|
173
|
+
}
|
|
174
|
+
case 'html':
|
|
175
|
+
state.discarded.push({ path, type: node.type, reason: 'raw_html' });
|
|
176
|
+
return '';
|
|
177
|
+
case 'image':
|
|
178
|
+
state.discarded.push({ path, type: node.type, reason: 'image' });
|
|
179
|
+
return '';
|
|
180
|
+
default:
|
|
181
|
+
state.discarded.push({ path, type: node.type, reason: 'unknown_type' });
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function renderTodoMarkdownAst(tree) {
|
|
187
|
+
if (!isNode(tree) || tree.type !== 'root') {
|
|
188
|
+
throw new TodoMarkdownRenderError(
|
|
189
|
+
'TODO_MARKDOWN_INVALID_AST',
|
|
190
|
+
'todo Markdown AST must be an mdast root node',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const state = { discarded: [] };
|
|
194
|
+
return {
|
|
195
|
+
html: renderNode(tree, 'root', state),
|
|
196
|
+
discarded: state.discarded,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function renderTodoMarkdown(markdown) {
|
|
201
|
+
if (typeof markdown !== 'string') {
|
|
202
|
+
throw new TodoMarkdownRenderError(
|
|
203
|
+
'TODO_MARKDOWN_INVALID_INPUT',
|
|
204
|
+
'todo Markdown section must be a string',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const actualBytes = Buffer.byteLength(markdown, 'utf8');
|
|
208
|
+
if (actualBytes > TODO_MARKDOWN_SECTION_MAX_BYTES) {
|
|
209
|
+
throw new TodoMarkdownSectionTooLargeError(actualBytes);
|
|
210
|
+
}
|
|
211
|
+
return renderTodoMarkdownAst(markdownParser.parse(markdown));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function parseTodoMarkdownDocument(markdown) {
|
|
215
|
+
if (typeof markdown !== 'string') {
|
|
216
|
+
throw new TodoMarkdownRenderError(
|
|
217
|
+
'TODO_MARKDOWN_INVALID_INPUT',
|
|
218
|
+
'todo Markdown section must be a string',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const actualBytes = Buffer.byteLength(markdown, 'utf8');
|
|
222
|
+
if (actualBytes > TODO_MARKDOWN_SECTION_MAX_BYTES) {
|
|
223
|
+
throw new TodoMarkdownSectionTooLargeError(actualBytes);
|
|
224
|
+
}
|
|
225
|
+
return markdownParser.parse(markdown);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function renderTodoMarkdownDocument(markdown, { taskStatesByLine = new Map() } = {}) {
|
|
229
|
+
if (!(taskStatesByLine instanceof Map)) {
|
|
230
|
+
throw new TodoMarkdownRenderError(
|
|
231
|
+
'TODO_MARKDOWN_INVALID_OPTIONS',
|
|
232
|
+
'taskStatesByLine must be a Map',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const tree = parseTodoMarkdownDocument(markdown);
|
|
236
|
+
if (!isNode(tree) || tree.type !== 'root') {
|
|
237
|
+
throw new TodoMarkdownRenderError(
|
|
238
|
+
'TODO_MARKDOWN_INVALID_AST',
|
|
239
|
+
'todo Markdown AST must be an mdast root node',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const state = { discarded: [] };
|
|
243
|
+
return {
|
|
244
|
+
html: renderNode(tree, 'root', state, { taskCheckboxes: true, taskStatesByLine }),
|
|
245
|
+
discarded: state.discarded,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function serializeJsonForScript(value) {
|
|
250
|
+
const serialized = JSON.stringify(value);
|
|
251
|
+
if (serialized === undefined) {
|
|
252
|
+
throw new TodoMarkdownRenderError(
|
|
253
|
+
'TODO_MARKDOWN_JSON_NOT_SERIALIZABLE',
|
|
254
|
+
'script JSON value must be JSON-serializable',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return serialized.replace(scriptUnsafeCharacters, (character) => (
|
|
258
|
+
`\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`
|
|
259
|
+
));
|
|
260
|
+
}
|