@cosense-toolbox/parser 0.1.0-beta.0

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +794 -0
  3. package/dist/ast-CIKXwSl5.mjs +41 -0
  4. package/dist/ast-CIKXwSl5.mjs.map +1 -0
  5. package/dist/ast-uYWtkHwT.cjs +52 -0
  6. package/dist/ast-uYWtkHwT.cjs.map +1 -0
  7. package/dist/compile.cjs +265 -0
  8. package/dist/compile.cjs.map +1 -0
  9. package/dist/compile.d.cts +135 -0
  10. package/dist/compile.d.cts.map +1 -0
  11. package/dist/compile.d.mts +135 -0
  12. package/dist/compile.d.mts.map +1 -0
  13. package/dist/compile.mjs +256 -0
  14. package/dist/compile.mjs.map +1 -0
  15. package/dist/create-compiler-CvlndKOA.d.cts +23 -0
  16. package/dist/create-compiler-CvlndKOA.d.cts.map +1 -0
  17. package/dist/create-compiler-jdA408ny.d.mts +23 -0
  18. package/dist/create-compiler-jdA408ny.d.mts.map +1 -0
  19. package/dist/image-url-YcTed8tg.cjs +68 -0
  20. package/dist/image-url-YcTed8tg.cjs.map +1 -0
  21. package/dist/image-url-eQ6X-oN0.mjs +51 -0
  22. package/dist/image-url-eQ6X-oN0.mjs.map +1 -0
  23. package/dist/index.cjs +716 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +77 -0
  26. package/dist/index.d.cts.map +1 -0
  27. package/dist/index.d.mts +77 -0
  28. package/dist/index.d.mts.map +1 -0
  29. package/dist/index.mjs +709 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/plugin.cjs +0 -0
  32. package/dist/plugin.d.cts +4 -0
  33. package/dist/plugin.d.mts +4 -0
  34. package/dist/plugin.mjs +1 -0
  35. package/dist/schema.cjs +171 -0
  36. package/dist/schema.cjs.map +1 -0
  37. package/dist/schema.d.cts +34 -0
  38. package/dist/schema.d.cts.map +1 -0
  39. package/dist/schema.d.mts +34 -0
  40. package/dist/schema.d.mts.map +1 -0
  41. package/dist/schema.mjs +148 -0
  42. package/dist/schema.mjs.map +1 -0
  43. package/dist/types-BXlnUFr0.d.mts +63 -0
  44. package/dist/types-BXlnUFr0.d.mts.map +1 -0
  45. package/dist/types-Bo_BrmvK.d.cts +230 -0
  46. package/dist/types-Bo_BrmvK.d.cts.map +1 -0
  47. package/dist/types-Bo_BrmvK.d.mts +230 -0
  48. package/dist/types-Bo_BrmvK.d.mts.map +1 -0
  49. package/dist/types-DVwlUtla.d.cts +63 -0
  50. package/dist/types-DVwlUtla.d.cts.map +1 -0
  51. package/dist/utils.cjs +80 -0
  52. package/dist/utils.cjs.map +1 -0
  53. package/dist/utils.d.cts +46 -0
  54. package/dist/utils.d.cts.map +1 -0
  55. package/dist/utils.d.mts +46 -0
  56. package/dist/utils.d.mts.map +1 -0
  57. package/dist/utils.mjs +72 -0
  58. package/dist/utils.mjs.map +1 -0
  59. package/package.json +96 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,709 @@
1
+ import { n as hasImageExtension, r as isImageUrl, t as asImageSrc } from "./image-url-eQ6X-oN0.mjs";
2
+ import { Match, Option, pipe } from "effect";
3
+ //#region src/core/position.ts
4
+ const originOfLine = (line, offset) => ({
5
+ line,
6
+ column: 0,
7
+ offset
8
+ });
9
+ /** Origin を `by` 文字ぶん進める (部分文字列を再帰的に走査するときに使う)。 */
10
+ const shiftOrigin = (origin, by) => ({
11
+ line: origin.line,
12
+ column: origin.column + by,
13
+ offset: origin.offset + by
14
+ });
15
+ const pointAt = (origin, index) => ({
16
+ line: origin.line,
17
+ column: origin.column + index,
18
+ offset: origin.offset + index
19
+ });
20
+ /** 走査対象の `[start, end)` が占める Position。end は exclusive。 */
21
+ const spanAt = (origin, start, end) => ({
22
+ start: pointAt(origin, start),
23
+ end: pointAt(origin, end)
24
+ });
25
+ //#endregion
26
+ //#region src/inline/constructs/bare-url.ts
27
+ const URL_RE = /^https?:\/\/[^\s\]]+/i;
28
+ /**
29
+ * 角括弧で囲まれていない URL。常に外部リンクになり、画像 URL でも画像にはしない
30
+ * (本家準拠。インライン画像になるのは `[https://.../x.png]` の角括弧つきのみ)。
31
+ */
32
+ const bareUrlConstruct = (source, index) => {
33
+ const head = source[index];
34
+ if (head !== "h" && head !== "H") return Option.none();
35
+ const match = source.slice(index).match(URL_RE);
36
+ if (!match) return Option.none();
37
+ const url = match[0];
38
+ return Option.some({
39
+ node: {
40
+ type: "externalLink",
41
+ label: url,
42
+ target: url
43
+ },
44
+ length: url.length
45
+ });
46
+ };
47
+ //#endregion
48
+ //#region src/core/scan.ts
49
+ /**
50
+ * scan.ts — 記法の知識を持たない文字列走査のプリミティブ。
51
+ */
52
+ /** 行頭の空白 (半角スペース / タブ / 全角スペース)。インデント判定の単一ソース。 */
53
+ const LEADING_WHITESPACE_RE = /^[ \t ]*/;
54
+ const leadingWhitespace = (s) => LEADING_WHITESPACE_RE.exec(s)?.[0] ?? "";
55
+ /**
56
+ * `openIdx` の `[` に対応する `]` の位置。深さを数えるので
57
+ * `[* a [b] c]` のような入れ子でも外側の `]` を返す。対応が閉じなければ None。
58
+ */
59
+ const findClosingBracket = (s, openIdx) => {
60
+ let depth = 0;
61
+ for (let i = openIdx; i < s.length; i++) {
62
+ const ch = s[i];
63
+ if (ch === "[") depth++;
64
+ else if (ch === "]") {
65
+ depth--;
66
+ if (depth === 0) return Option.some(i);
67
+ }
68
+ }
69
+ return Option.none();
70
+ };
71
+ /** `#` がタグの開始位置か (行頭、または直前が空白)。単語の途中の `#` を除くため。 */
72
+ const isTagBoundary = (s, index) => {
73
+ if (index === 0) return true;
74
+ const prev = s[index - 1];
75
+ return prev === " " || prev === " " || prev === " ";
76
+ };
77
+ //#endregion
78
+ //#region src/inline/bracket-rules/decoration.ts
79
+ /** 先頭の装飾記号の run と、空白を挟んだ中身。 */
80
+ const DECORATION_RE = /^([*/\-_]+)\s+([\s\S]+)$/;
81
+ const MAX_SIZE_LEVEL = 4;
82
+ /**
83
+ * `[* 太字]` `[/ 斜体]` `[- 打消し]` `[_ 下線]` とその複合 (`[-/ x]`)。
84
+ *
85
+ * 中身はリンクやアイコンとして再帰的に解釈するが、**装飾の入れ子は不可**
86
+ * (本家準拠)。そのため子の走査は allowDecoration=false で行う。
87
+ * 例: `[* [* 太字]ですね]` の内側は装飾ではなく内部リンクになる。
88
+ */
89
+ const decorationRule = (inner, ctx) => {
90
+ if (!ctx.allowDecoration) return Option.none();
91
+ const match = inner.match(DECORATION_RE);
92
+ if (!match) return Option.none();
93
+ const marks = match[1] ?? "";
94
+ const value = match[2] ?? "";
95
+ const stars = (marks.match(/\*/g) ?? []).length;
96
+ const valueOffset = inner.length - value.length;
97
+ return Option.some({
98
+ type: "decoration",
99
+ value,
100
+ bold: stars > 0,
101
+ italic: marks.includes("/"),
102
+ strike: marks.includes("-"),
103
+ underline: marks.includes("_"),
104
+ sizeLevel: Math.min(Math.max(stars - 1, 0), MAX_SIZE_LEVEL),
105
+ children: ctx.tokenize(value, shiftOrigin(ctx.innerOrigin, valueOffset), false)
106
+ });
107
+ };
108
+ //#endregion
109
+ //#region src/inline/bracket-rules/formula.ts
110
+ /** `[$ x^2]` — 中身は解釈せず生のまま返す (KaTeX 等に渡す想定)。 */
111
+ const formulaRule = (inner) => inner.startsWith("$") ? Option.some({
112
+ type: "formula",
113
+ value: inner.slice(1).trim()
114
+ }) : Option.none();
115
+ //#endregion
116
+ //#region src/inline/bracket-rules/icon.ts
117
+ const ICON_RE = /^(.+)\.icon(?:\*(\d+))?$/;
118
+ const MIN_COUNT = 1;
119
+ const MAX_COUNT = 20;
120
+ /** `[user.icon]` と連打 `[user.icon*5]`。個数は 1..20 に収める (描画の暴走を防ぐため)。 */
121
+ const iconRule = (inner) => {
122
+ const match = inner.match(ICON_RE);
123
+ if (!match) return Option.none();
124
+ const raw = match[2];
125
+ const count = raw ? Math.min(Math.max(Number.parseInt(raw, 10), MIN_COUNT), MAX_COUNT) : MIN_COUNT;
126
+ return Option.some({
127
+ type: "icon",
128
+ user: match[1] ?? "",
129
+ count
130
+ });
131
+ };
132
+ //#endregion
133
+ //#region src/inline/bracket-rules/image-extension.ts
134
+ /**
135
+ * URL ではなく拡張子だけで画像と分かる中身 (`[a.png]`)。
136
+ *
137
+ * 装飾の中では画像にせずリンク扱いにする (本家準拠)。`[* [a.png]]` の内側は
138
+ * 画像ではなく `a.png` というタイトルの内部リンクになる。
139
+ */
140
+ const imageExtensionRule = (inner, ctx) => ctx.allowDecoration && hasImageExtension(inner) ? Option.some({
141
+ type: "image",
142
+ src: inner
143
+ }) : Option.none();
144
+ //#endregion
145
+ //#region src/inline/bracket-rules/internal-link.ts
146
+ /**
147
+ * `[title]` — 他のどのルールにも当たらなかった角括弧は内部リンクになる。
148
+ * 常に Some を返す catch-all なので、ルール配列の最後に置くこと。
149
+ */
150
+ const internalLinkRule = (inner) => Option.some({
151
+ type: "internalLink",
152
+ label: inner,
153
+ target: inner
154
+ });
155
+ //#endregion
156
+ //#region src/inline/bracket-rules/project-link.ts
157
+ /**
158
+ * `[/project/title]` — 別プロジェクトのページへのリンク。
159
+ *
160
+ * タイトルは `/` を含みうるので最初の `/` だけで分割する。
161
+ * `[/project]` のようにタイトルが無い書き方も記法としては成立するので、
162
+ * その場合は title を空文字にする (利用側が「プロジェクトそのものへのリンク」と判断できる)。
163
+ */
164
+ const projectLinkRule = (inner) => {
165
+ if (!inner.startsWith("/")) return Option.none();
166
+ const rest = inner.slice(1);
167
+ const slash = rest.indexOf("/");
168
+ const project = slash < 0 ? rest : rest.slice(0, slash);
169
+ const title = slash < 0 ? "" : rest.slice(slash + 1);
170
+ return Option.some({
171
+ type: "projectLink",
172
+ label: inner,
173
+ target: inner,
174
+ project,
175
+ title
176
+ });
177
+ };
178
+ //#endregion
179
+ //#region src/inline/bracket-rules/url.ts
180
+ const URLS_RE = /https?:\/\/[^\s\]]+/gi;
181
+ /** 画像 URL が複数あるとき最後のものを採るのは本家の挙動。 */
182
+ const lastImage = (urls) => {
183
+ const images = urls.filter(isImageUrl);
184
+ return Option.fromNullable(images[images.length - 1]);
185
+ };
186
+ /** 画像に添える遷移先。画像自身とは別の URL を優先し、無ければ先頭を使う。 */
187
+ const linkFor = (urls, src) => Option.fromNullable(urls.find((url) => url !== src) ?? urls[0]);
188
+ /**
189
+ * 中身に URL を含む角括弧。本家の挙動に合わせて次の順で決める:
190
+ *
191
+ * 1. URL 以外の文字が残る → ラベル付き外部リンク。URL が画像でも文字リンクにする
192
+ * (`[ラベル https://x/a.png]` は画像にならない)。
193
+ * 2. URL だけが 2 つ以上:
194
+ * - 画像 URL があれば「リンク付き画像」。画像が複数なら最後を表示し、それ以外の先頭をリンク先にする。
195
+ * - 画像が無ければ 先頭 = リンク先 / 2 番目 = 表示テキスト。
196
+ * 3. URL が 1 つだけ → 画像なら画像、違えば裸の外部リンク。
197
+ */
198
+ const urlRule = (inner) => {
199
+ const urls = inner.match(URLS_RE) ?? [];
200
+ if (urls.length === 0) return Option.none();
201
+ const label = urls.reduce((rest, url) => rest.replace(url, " "), inner).trim();
202
+ if (label !== "") return Option.some({
203
+ type: "externalLink",
204
+ label,
205
+ target: urls[0] ?? inner
206
+ });
207
+ if (urls.length >= 2) return Option.some(pipe(lastImage(urls), Option.match({
208
+ onSome: (src) => pipe(linkFor(urls, src), Option.match({
209
+ onNone: () => ({
210
+ type: "image",
211
+ src
212
+ }),
213
+ onSome: (link) => ({
214
+ type: "image",
215
+ src,
216
+ link
217
+ })
218
+ })),
219
+ onNone: () => ({
220
+ type: "externalLink",
221
+ label: urls[1] ?? urls[0] ?? "",
222
+ target: urls[0] ?? inner
223
+ })
224
+ })));
225
+ const only = urls[0] ?? inner;
226
+ return Option.some(pipe(Option.liftPredicate(only, isImageUrl), Option.match({
227
+ onSome: (src) => ({
228
+ type: "image",
229
+ src
230
+ }),
231
+ onNone: () => ({
232
+ type: "externalLink",
233
+ label: only,
234
+ target: only
235
+ })
236
+ })));
237
+ };
238
+ //#endregion
239
+ //#region src/inline/bracket-rules/index.ts
240
+ /** 中身に角括弧を含んでいても成立しうるルール。 */
241
+ const bracketRules = [formulaRule, decorationRule];
242
+ /**
243
+ * 「単純ターゲット」のルール。中身に `[` / `]` を含むときは試さない (本家準拠)。
244
+ * これにより `[[そうね] ですね]` の外側は記法にならず、先頭の `[` が素の文字になる。
245
+ * 末尾の internalLinkRule は常に成立する catch-all。
246
+ */
247
+ const simpleTargetRules = [
248
+ iconRule,
249
+ urlRule,
250
+ imageExtensionRule,
251
+ projectLinkRule,
252
+ internalLinkRule
253
+ ];
254
+ //#endregion
255
+ //#region src/inline/constructs/bracket.ts
256
+ /** ルールを順に試す。ジェネレータにしているのは最初に成立した時点で残りを評価しないため。 */
257
+ function* attempts$1(rules, inner, ctx) {
258
+ for (const rule of rules) yield rule(inner, ctx);
259
+ }
260
+ const parseInner = (inner, ctx) => pipe(Option.firstSomeOf(attempts$1(ctx.bracketRules, inner, ctx)), Option.orElse(() => Option.firstSomeOf(attempts$1(bracketRules, inner, ctx))), Option.orElse(() => inner.includes("[") || inner.includes("]") ? Option.none() : Option.firstSomeOf(attempts$1(simpleTargetRules, inner, ctx))));
261
+ /**
262
+ * `[...]` 全般。閉じ括弧は深さを数えて探すので `[* a [b] c]` でも外側で閉じる。
263
+ *
264
+ * 中身が空 (`[]` / `[ ]`) のときと、どのルールにも当たらなかったときは None を返す。
265
+ * 呼び出し側は先頭の `[` を素の文字として 1 文字進めるので、内側が改めて走査される。
266
+ */
267
+ const bracketConstruct = (source, index, ctx) => {
268
+ if (source[index] !== "[") return Option.none();
269
+ return pipe(findClosingBracket(source, index), Option.flatMap((end) => {
270
+ const inner = source.slice(index + 1, end);
271
+ if (inner.trim() === "") return Option.none();
272
+ const innerCtx = {
273
+ ...ctx,
274
+ innerOrigin: shiftOrigin(ctx.origin, index + 1)
275
+ };
276
+ return pipe(parseInner(inner, innerCtx), Option.map((node) => ({
277
+ node,
278
+ length: end + 1 - index
279
+ })));
280
+ }));
281
+ };
282
+ //#endregion
283
+ //#region src/inline/constructs/hashtag.ts
284
+ /** タグ名に使えない文字。空白と角括弧と `#` 自身で終わる。 */
285
+ const TAG_NAME_RE = /^[^\s[\]#]+/;
286
+ /** `#tag`。行頭または空白の直後でだけ成立する (単語の途中の `#` を拾わないため)。 */
287
+ const hashtagConstruct = (source, index) => {
288
+ if (source[index] !== "#" || !isTagBoundary(source, index)) return Option.none();
289
+ const match = source.slice(index + 1).match(TAG_NAME_RE);
290
+ if (!match) return Option.none();
291
+ const value = match[0];
292
+ return Option.some({
293
+ node: {
294
+ type: "hashtag",
295
+ value
296
+ },
297
+ length: value.length + 1
298
+ });
299
+ };
300
+ //#endregion
301
+ //#region src/inline/constructs/inline-code.ts
302
+ /** バッククォートで囲んだインラインコード。閉じるバッククォートが無ければ成立しない。 */
303
+ const inlineCodeConstruct = (source, index) => {
304
+ if (source[index] !== "`") return Option.none();
305
+ const end = source.indexOf("`", index + 1);
306
+ if (end < 0) return Option.none();
307
+ return Option.some({
308
+ node: {
309
+ type: "inlineCode",
310
+ value: source.slice(index + 1, end)
311
+ },
312
+ length: end + 1 - index
313
+ });
314
+ };
315
+ //#endregion
316
+ //#region src/inline/constructs/strong-bracket.ts
317
+ /**
318
+ * `[[...]]` — 本家の strong。`]]` で閉じるときだけ成立する
319
+ * (深さは数えない。`[[a] b]` のようなケースは bracketConstruct 側で処理される)。
320
+ *
321
+ * 中身が画像 URL なら大きい画像、そうでなければ太字装飾になる。
322
+ */
323
+ const strongBracketConstruct = (source, index, ctx) => {
324
+ if (source[index] !== "[" || source[index + 1] !== "[") return Option.none();
325
+ const end = source.indexOf("]]", index + 2);
326
+ if (end < 0) return Option.none();
327
+ const inner = source.slice(index + 2, end);
328
+ const length = end + 2 - index;
329
+ if (isImageUrl(inner)) {
330
+ const node = {
331
+ type: "image",
332
+ src: inner,
333
+ large: true
334
+ };
335
+ return Option.some({
336
+ node,
337
+ length
338
+ });
339
+ }
340
+ return Option.some({
341
+ node: {
342
+ type: "decoration",
343
+ value: inner,
344
+ bold: true,
345
+ italic: false,
346
+ strike: false,
347
+ underline: false,
348
+ sizeLevel: 0,
349
+ children: ctx.tokenize(inner, shiftOrigin(ctx.origin, index + 2), false)
350
+ },
351
+ length
352
+ });
353
+ };
354
+ //#endregion
355
+ //#region src/inline/constructs/index.ts
356
+ const inlineConstructs = [
357
+ strongBracketConstruct,
358
+ bracketConstruct,
359
+ inlineCodeConstruct,
360
+ hashtagConstruct,
361
+ bareUrlConstruct
362
+ ];
363
+ //#endregion
364
+ //#region src/inline/tokenize.ts
365
+ /**
366
+ * tokenize.ts — 行内の走査ループ。
367
+ *
368
+ * 1 文字ずつ進みながら construct を順に試し、成立したらノードにする。どれも成立しなければ
369
+ * その 1 文字はテキストとして貯めておき、次にノードが出たところ (と末尾) でまとめて
370
+ * text ノードにする。位置の付与はこのループだけが行う。
371
+ */
372
+ /** 拡張のルールを既定のルールの前に並べる。拡張が既定の記法を上書きできるのはこの順序による。 */
373
+ const resolveExtensions = (extensions) => {
374
+ if (extensions === void 0 || extensions.length === 0) return {
375
+ constructs: inlineConstructs,
376
+ bracketRules: []
377
+ };
378
+ const constructs = [];
379
+ const bracketRules = [];
380
+ for (const extension of extensions) {
381
+ if (extension.constructs) constructs.push(...extension.constructs);
382
+ if (extension.bracketRules) bracketRules.push(...extension.bracketRules);
383
+ }
384
+ return {
385
+ constructs: [...constructs, ...inlineConstructs],
386
+ bracketRules
387
+ };
388
+ };
389
+ /**
390
+ * ノード本体に位置を与える。判別共用体へのスプレッドは TS が型を保てないため
391
+ * ここだけキャストする (position 以外のフィールドは触っていない)。
392
+ */
393
+ const withPosition = (node, position) => ({
394
+ ...node,
395
+ position
396
+ });
397
+ /** ジェネレータにしているのは、最初に成立した時点で残りのルールを評価しないため。 */
398
+ function* attempts(constructs, source, index, ctx) {
399
+ for (const construct of constructs) yield construct(source, index, ctx);
400
+ }
401
+ const firstMatch = (constructs, source, index, ctx) => Option.firstSomeOf(attempts(constructs, source, index, ctx));
402
+ const scan = (source, origin, allowDecoration, rules) => {
403
+ const out = [];
404
+ const ctx = {
405
+ allowDecoration,
406
+ origin,
407
+ bracketRules: rules.bracketRules,
408
+ tokenize: (inner, innerOrigin, innerAllowDecoration) => scan(inner, innerOrigin, innerAllowDecoration, rules)
409
+ };
410
+ let textStart = 0;
411
+ let index = 0;
412
+ const flushText = (end) => {
413
+ if (end <= textStart) return;
414
+ out.push({
415
+ type: "text",
416
+ value: source.slice(textStart, end),
417
+ position: spanAt(origin, textStart, end)
418
+ });
419
+ };
420
+ while (index < source.length) {
421
+ const match = firstMatch(rules.constructs, source, index, ctx);
422
+ if (Option.isNone(match)) {
423
+ index++;
424
+ continue;
425
+ }
426
+ flushText(index);
427
+ const { node, length } = match.value;
428
+ out.push(withPosition(node, spanAt(origin, index, index + length)));
429
+ index += length;
430
+ textStart = index;
431
+ }
432
+ flushText(index);
433
+ return out;
434
+ };
435
+ const resolveOrigin = (origin) => ({
436
+ line: origin?.line ?? 0,
437
+ column: origin?.column ?? 0,
438
+ offset: origin?.offset ?? 0
439
+ });
440
+ /**
441
+ * インライン記法をノード列に分解する。改行を含まない 1 行分の文字列を渡すこと。
442
+ *
443
+ * 記法として成立しなかった文字は text ノードにまとまる。空文字列では空配列を返す。
444
+ */
445
+ const tokenizeInline = (source, options) => scan(source, resolveOrigin(options?.origin), options?.allowDecoration ?? true, resolveExtensions(options?.extensions));
446
+ /**
447
+ * 解決済みのルールで走査する内部向け入口。
448
+ * 行ごとに `tokenizeInline` を呼ぶと拡張の解決が毎回走るので、それを避けたいときに使う。
449
+ */
450
+ const tokenizeInlineWith = (source, origin, rules) => scan(source, origin, true, rules);
451
+ //#endregion
452
+ //#region src/block/classify.ts
453
+ /**
454
+ * classify.ts — 1 行が何の行かを判定する。インライン記法の中身には立ち入らない。
455
+ *
456
+ * 結果はタグ付きユニオンなので、行の種類を増やすと分岐している側が
457
+ * exhaustive チェックで対応漏れを教えてくれる。
458
+ */
459
+ const CODE_HEADER_RE = /^code:(.+)$/;
460
+ const TABLE_HEADER_RE = /^table:(.+)$/;
461
+ /** 引用記号と、その直後の空白 1 つ (あれば本文から除く)。 */
462
+ const QUOTE_RE = /^>\s?/;
463
+ const MONOSPACE_RE = /^[$%]/;
464
+ const codeHeader = (rest, indent) => {
465
+ const match = rest.match(CODE_HEADER_RE);
466
+ return match ? Option.some({
467
+ _tag: "codeHeader",
468
+ indent,
469
+ filename: (match[1] ?? "").trimEnd()
470
+ }) : Option.none();
471
+ };
472
+ const tableHeader = (rest, indent) => {
473
+ const match = rest.match(TABLE_HEADER_RE);
474
+ return match ? Option.some({
475
+ _tag: "tableHeader",
476
+ indent,
477
+ name: (match[1] ?? "").trimEnd()
478
+ }) : Option.none();
479
+ };
480
+ const content = (rest, indent) => {
481
+ const quoteMark = rest.match(QUOTE_RE)?.[0] ?? "";
482
+ const body = rest.slice(quoteMark.length);
483
+ return {
484
+ _tag: "content",
485
+ indent,
486
+ quote: quoteMark !== "",
487
+ monospace: MONOSPACE_RE.test(body),
488
+ contentOffset: indent + quoteMark.length
489
+ };
490
+ };
491
+ /**
492
+ * 行の役割を判定する。`code:` / `table:` は行の意味がページの文脈
493
+ * (コードブロックの中かどうか) で変わるので、ここでは「ヘッダの形をしている」ことだけを見る。
494
+ */
495
+ const classifyLine = (raw) => {
496
+ const indent = leadingWhitespace(raw).length;
497
+ const rest = raw.slice(indent);
498
+ return pipe(Option.firstSomeOf([codeHeader(rest, indent), tableHeader(rest, indent)]), Option.getOrElse(() => content(rest, indent)));
499
+ };
500
+ /** 行頭の空白の文字数。全角スペースも 1 文字として数える。 */
501
+ const indentOf = (raw) => leadingWhitespace(raw).length;
502
+ //#endregion
503
+ //#region src/block/build.ts
504
+ /**
505
+ * build.ts — 行の並びをブロックに畳む。
506
+ *
507
+ * `code:` / `table:` は「ヘッダ行 + それより深いインデントの行」で 1 つのブロックになる。
508
+ * この複数行のまとまりを作るのがここの仕事で、行の中身の解釈は注入された tokenize に任せる
509
+ * (どの記法ルールを使うかを知らずに済むので、拡張入りのパーサーでもここは変わらない)。
510
+ */
511
+ const lineOrigin = (line) => originOfLine(line.index, line.offset);
512
+ const wholeLine = (line) => spanAt(lineOrigin(line), 0, line.text.length);
513
+ /** インデントを `amount` 文字ぶん浅くする。空白より多くは削らない。 */
514
+ const dedent = (text, amount) => text.slice(Math.min(amount, indentOf(text)));
515
+ const titleBlock = (line, tokenize) => ({
516
+ type: "title",
517
+ value: line.text,
518
+ children: tokenize(line.text, lineOrigin(line)),
519
+ position: wholeLine(line)
520
+ });
521
+ const lineBlock = (line, role, tokenize) => ({
522
+ type: "line",
523
+ indent: role.indent,
524
+ quote: role.quote,
525
+ monospace: role.monospace,
526
+ children: tokenize(line.text.slice(role.contentOffset), {
527
+ line: line.index,
528
+ column: role.contentOffset,
529
+ offset: line.offset + role.contentOffset
530
+ }),
531
+ position: wholeLine(line)
532
+ });
533
+ const codeLine = (line, headerIndent) => ({
534
+ type: "codeLine",
535
+ value: dedent(line.text, headerIndent + 1),
536
+ position: wholeLine(line)
537
+ });
538
+ const tableCells = (line) => {
539
+ const indent = indentOf(line.text);
540
+ const origin = lineOrigin(line);
541
+ const cells = [];
542
+ let start = indent;
543
+ for (const value of line.text.slice(indent).split(" ")) {
544
+ cells.push({
545
+ type: "tableCell",
546
+ value,
547
+ position: spanAt(origin, start, start + value.length)
548
+ });
549
+ start += value.length + 1;
550
+ }
551
+ return cells;
552
+ };
553
+ const tableRow = (line) => ({
554
+ type: "tableRow",
555
+ cells: tableCells(line),
556
+ position: wholeLine(line)
557
+ });
558
+ /** ヘッダより深いインデントが続く範囲の終端 (exclusive)。 */
559
+ const bodyEnd = (lines, start, headerIndent) => {
560
+ let end = start;
561
+ while (end < lines.length) {
562
+ const line = lines[end];
563
+ if (line === void 0 || indentOf(line.text) <= headerIndent) break;
564
+ end++;
565
+ }
566
+ return end;
567
+ };
568
+ const blockPosition = (header, body) => ({
569
+ start: wholeLine(header).start,
570
+ end: wholeLine(body[body.length - 1] ?? header).end
571
+ });
572
+ const codeBlock = (header, body, filename, indent) => ({
573
+ type: "codeBlock",
574
+ filename,
575
+ indent,
576
+ lines: body.map((line) => codeLine(line, indent)),
577
+ position: blockPosition(header, body)
578
+ });
579
+ const tableBlock = (header, body, name, indent) => ({
580
+ type: "table",
581
+ name,
582
+ indent,
583
+ rows: body.map(tableRow),
584
+ position: blockPosition(header, body)
585
+ });
586
+ /**
587
+ * 行の並びをブロックの並びに畳む。1 行目は無条件でタイトルになる
588
+ * (Cosense ではタイトル行が `code:` や `table:` として解釈されることはない)。
589
+ */
590
+ const buildBlocks = (lines, tokenize) => {
591
+ const blocks = [];
592
+ const head = lines[0];
593
+ if (head === void 0) return blocks;
594
+ blocks.push(titleBlock(head, tokenize));
595
+ let index = 1;
596
+ while (index < lines.length) {
597
+ const line = lines[index];
598
+ if (line === void 0) break;
599
+ index = Match.value(classifyLine(line.text)).pipe(Match.tag("codeHeader", (role) => {
600
+ const end = bodyEnd(lines, index + 1, role.indent);
601
+ blocks.push(codeBlock(line, lines.slice(index + 1, end), role.filename, role.indent));
602
+ return end;
603
+ }), Match.tag("tableHeader", (role) => {
604
+ const end = bodyEnd(lines, index + 1, role.indent);
605
+ blocks.push(tableBlock(line, lines.slice(index + 1, end), role.name, role.indent));
606
+ return end;
607
+ }), Match.tag("content", (role) => {
608
+ blocks.push(lineBlock(line, role, tokenize));
609
+ return index + 1;
610
+ }), Match.exhaustive);
611
+ }
612
+ return blocks;
613
+ };
614
+ /**
615
+ * 1 行だけを通常行として組み立てる。
616
+ * ページの文脈が無いので `code:` / `table:` ヘッダもブロックにはせず、通常行として扱う。
617
+ */
618
+ const buildLineBlock = (line, tokenize) => {
619
+ const role = classifyLine(line.text);
620
+ return lineBlock(line, role._tag === "content" ? role : {
621
+ _tag: "content",
622
+ indent: role.indent,
623
+ quote: false,
624
+ monospace: false,
625
+ contentOffset: role.indent
626
+ }, tokenize);
627
+ };
628
+ //#endregion
629
+ //#region src/parse.ts
630
+ /**
631
+ * parse.ts — ページ全文の入口。
632
+ */
633
+ /**
634
+ * CR / CRLF を LF に揃える。
635
+ *
636
+ * `parse` は必ずこれを通してから解析するので、報告される位置は**正規化後**の
637
+ * 文字列を基準にする。CRLF のソースでは元のオフセットと 1 行につき 1 文字ずれる。
638
+ */
639
+ const normalizeLineEndings = (source) => source.replace(/\r\n?/g, "\n");
640
+ const toSourceLines = (source) => {
641
+ const lines = [];
642
+ let offset = 0;
643
+ for (const [index, text] of source.split("\n").entries()) {
644
+ lines.push({
645
+ index,
646
+ text,
647
+ offset
648
+ });
649
+ offset += text.length + 1;
650
+ }
651
+ return lines;
652
+ };
653
+ /**
654
+ * ページ全文をパースする。**失敗しない**: どんな入力でも Page を返す。
655
+ * 記法として成立しない部分は素のテキストになるだけで、エラーにはならない。
656
+ *
657
+ * 1 行目はタイトルとして扱われる (Cosense のページは 1 行目がタイトル)。
658
+ */
659
+ const parse = (source, options) => {
660
+ const normalized = normalizeLineEndings(source);
661
+ const lines = toSourceLines(normalized);
662
+ const rules = resolveExtensions(options?.extensions);
663
+ const last = lines[lines.length - 1];
664
+ return {
665
+ type: "page",
666
+ children: buildBlocks(lines, (text, origin) => tokenizeInlineWith(text, origin, rules)),
667
+ position: {
668
+ start: {
669
+ line: 0,
670
+ column: 0,
671
+ offset: 0
672
+ },
673
+ end: {
674
+ line: last?.index ?? 0,
675
+ column: last?.text.length ?? 0,
676
+ offset: normalized.length
677
+ }
678
+ }
679
+ };
680
+ };
681
+ /**
682
+ * 1 行だけを通常行としてパースする。
683
+ *
684
+ * `code:` / `table:` はページの文脈があって初めてブロックになるので、
685
+ * ここでは通常行として扱う。
686
+ */
687
+ const parseLine = (raw, options) => {
688
+ const rules = resolveExtensions(options?.extensions);
689
+ return buildLineBlock({
690
+ index: options?.line ?? 0,
691
+ text: raw,
692
+ offset: options?.offset ?? 0
693
+ }, (text, origin) => tokenizeInlineWith(text, origin, rules));
694
+ };
695
+ /**
696
+ * 拡張を固定したパーサーを作る。同じ拡張で何度もパースするときに、
697
+ * 呼び出しごとに options を渡さずに済む。
698
+ */
699
+ const createParser = (options) => ({
700
+ parse: (source) => parse(source, options),
701
+ parseLine: (raw, lineOptions) => parseLine(raw, {
702
+ ...lineOptions,
703
+ ...options
704
+ })
705
+ });
706
+ //#endregion
707
+ export { asImageSrc, createParser, isImageUrl, normalizeLineEndings, parse, parseLine, tokenizeInline };
708
+
709
+ //# sourceMappingURL=index.mjs.map