@cosense-toolbox/parser 0.1.0-beta.1 → 0.1.0-beta.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 (76) hide show
  1. package/README.md +45 -31
  2. package/dist/{ast-uYWtkHwT.cjs → ast-C0BGz7X2.cjs} +2 -2
  3. package/dist/ast-C0BGz7X2.cjs.map +1 -0
  4. package/dist/{ast-CIKXwSl5.mjs → ast-CMQYawYl.mjs} +2 -2
  5. package/dist/ast-CMQYawYl.mjs.map +1 -0
  6. package/dist/compile.cjs +2 -213
  7. package/dist/compile.cjs.map +1 -1
  8. package/dist/compile.d.cts +2 -129
  9. package/dist/compile.d.cts.map +1 -1
  10. package/dist/compile.d.mts +2 -129
  11. package/dist/compile.d.mts.map +1 -1
  12. package/dist/compile.mjs +3 -207
  13. package/dist/compile.mjs.map +1 -1
  14. package/dist/extensions.cjs +10 -7
  15. package/dist/extensions.cjs.map +1 -1
  16. package/dist/extensions.d.cts +12 -8
  17. package/dist/extensions.d.cts.map +1 -1
  18. package/dist/extensions.d.mts +12 -8
  19. package/dist/extensions.d.mts.map +1 -1
  20. package/dist/extensions.mjs +10 -7
  21. package/dist/extensions.mjs.map +1 -1
  22. package/dist/html.cjs +458 -0
  23. package/dist/html.cjs.map +1 -0
  24. package/dist/html.d.cts +274 -0
  25. package/dist/html.d.cts.map +1 -0
  26. package/dist/html.d.mts +274 -0
  27. package/dist/html.d.mts.map +1 -0
  28. package/dist/html.mjs +447 -0
  29. package/dist/html.mjs.map +1 -0
  30. package/dist/image-url-YcTed8tg.cjs.map +1 -1
  31. package/dist/image-url-eQ6X-oN0.mjs.map +1 -1
  32. package/dist/index-CrIlOW1k.d.mts +78 -0
  33. package/dist/index-CrIlOW1k.d.mts.map +1 -0
  34. package/dist/index-_o1STNhP.d.cts +78 -0
  35. package/dist/index-_o1STNhP.d.cts.map +1 -0
  36. package/dist/index.cjs +188 -35
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +4 -77
  39. package/dist/index.d.mts +4 -77
  40. package/dist/index.mjs +180 -27
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/schema.cjs +1 -0
  43. package/dist/schema.cjs.map +1 -1
  44. package/dist/schema.d.cts +1 -1
  45. package/dist/schema.d.cts.map +1 -1
  46. package/dist/schema.d.mts +1 -1
  47. package/dist/schema.d.mts.map +1 -1
  48. package/dist/schema.mjs +1 -0
  49. package/dist/schema.mjs.map +1 -1
  50. package/dist/{types-CiwPj2R8.d.cts → types-CdiHju1P.d.mts} +15 -12
  51. package/dist/types-CdiHju1P.d.mts.map +1 -0
  52. package/dist/{types-9vAAFHka.d.mts → types-_-ZAyNkz.d.cts} +15 -12
  53. package/dist/types-_-ZAyNkz.d.cts.map +1 -0
  54. package/dist/{types-DmfFumcw.d.cts → types-gWMrsXPX.d.cts} +28 -21
  55. package/dist/{types-DmfFumcw.d.cts.map → types-gWMrsXPX.d.cts.map} +1 -1
  56. package/dist/{types-DmfFumcw.d.mts → types-gWMrsXPX.d.mts} +28 -21
  57. package/dist/{types-DmfFumcw.d.mts.map → types-gWMrsXPX.d.mts.map} +1 -1
  58. package/dist/utils.cjs +1 -1
  59. package/dist/utils.cjs.map +1 -1
  60. package/dist/utils.d.cts +2 -2
  61. package/dist/utils.d.cts.map +1 -1
  62. package/dist/utils.d.mts +2 -2
  63. package/dist/utils.d.mts.map +1 -1
  64. package/dist/utils.mjs +1 -1
  65. package/dist/utils.mjs.map +1 -1
  66. package/package.json +27 -5
  67. package/dist/ast-CIKXwSl5.mjs.map +0 -1
  68. package/dist/ast-uYWtkHwT.cjs.map +0 -1
  69. package/dist/decoration-DODaJKeA.cjs +0 -103
  70. package/dist/decoration-DODaJKeA.cjs.map +0 -1
  71. package/dist/decoration-Dq_5e20j.mjs +0 -68
  72. package/dist/decoration-Dq_5e20j.mjs.map +0 -1
  73. package/dist/index.d.cts.map +0 -1
  74. package/dist/index.d.mts.map +0 -1
  75. package/dist/types-9vAAFHka.d.mts.map +0 -1
  76. package/dist/types-CiwPj2R8.d.cts.map +0 -1
@@ -0,0 +1,78 @@
1
+ import { g as InlineNode, w as Page, x as LineBlock } from "./types-gWMrsXPX.mjs";
2
+ import { i as Extension } from "./types-CdiHju1P.mjs";
3
+ import { Option } from "effect";
4
+ //#region src/core/image-url.d.ts
5
+ /** URL が画像として表示されるものか。判定だけを行い、URL は書き換えない。 */
6
+ declare const isImageUrl: (url: string) => boolean;
7
+ /**
8
+ * 画像 URL なら `<img src>` に入れられる形にして返す。画像でなければ null。
9
+ *
10
+ * Gyazo のページ URL はここでだけ `https://i.gyazo.com/{hash}.png` に差し替わる。
11
+ * `parse()` はこの変換を行わない (AST はソースに書かれた文字列を保つ)。
12
+ */
13
+ declare const asImageSrc: (url: string) => string | null;
14
+ //#endregion
15
+ //#region src/inline/tokenize.d.ts
16
+ interface TokenizeInlineOptions {
17
+ /** 装飾記法を解釈するか (既定: true)。装飾の中身を解析するときだけ false になる */
18
+ readonly allowDecoration?: boolean;
19
+ /** この文字列がページ上のどこにあるか。省略時は 0 行目の先頭 */
20
+ readonly origin?: {
21
+ readonly line?: number;
22
+ readonly column?: number;
23
+ readonly offset?: number;
24
+ };
25
+ /** 記法の拡張。既定のルールより先に試される */
26
+ readonly extensions?: readonly Extension[];
27
+ }
28
+ /**
29
+ * インライン記法をノード列に分解する。改行を含まない 1 行分の文字列を渡すこと。
30
+ *
31
+ * 記法として成立しなかった文字は text ノードにまとまる。空文字列では空配列を返す。
32
+ */
33
+ declare const tokenizeInline: (source: string, options?: TokenizeInlineOptions) => readonly InlineNode[];
34
+ //#endregion
35
+ //#region src/parse.d.ts
36
+ /**
37
+ * CR / CRLF を LF に揃える。
38
+ *
39
+ * `parse` は必ずこれを通してから解析するので、報告される位置は**正規化後**の
40
+ * 文字列を基準にする。CRLF のソースでは元のオフセットと 1 行につき 1 文字ずれる。
41
+ */
42
+ declare const normalizeLineEndings: (source: string) => string;
43
+ interface ParseOptions {
44
+ /** 記法の拡張。既定のルールより先に試される */
45
+ readonly extensions?: readonly Extension[];
46
+ }
47
+ /**
48
+ * ページ全文をパースする。**失敗しない**: どんな入力でも Page を返す。
49
+ * 記法として成立しない部分は素のテキストになるだけで、エラーにはならない。
50
+ *
51
+ * 1 行目はタイトルとして扱われる (Cosense のページは 1 行目がタイトル)。
52
+ */
53
+ declare const parse: (source: string, options?: ParseOptions) => Page;
54
+ interface ParseLineOptions extends ParseOptions {
55
+ /** この行がページ内の何行目か (既定: 0) */
56
+ readonly line?: number;
57
+ /** この行の行頭がソース全文のどこにあるか (既定: 0) */
58
+ readonly offset?: number;
59
+ }
60
+ /**
61
+ * 1 行だけを通常行としてパースする。
62
+ *
63
+ * `code:` / `table:` はページの文脈があって初めてブロックになるので、
64
+ * ここでは通常行として扱う。
65
+ */
66
+ declare const parseLine: (raw: string, options?: ParseLineOptions) => LineBlock;
67
+ interface Parser {
68
+ readonly parse: (source: string) => Page;
69
+ readonly parseLine: (raw: string, options?: Omit<ParseLineOptions, "extensions">) => LineBlock;
70
+ }
71
+ /**
72
+ * 拡張を固定したパーサーを作る。同じ拡張で何度もパースするときに、
73
+ * 呼び出しごとに options を渡さずに済む。
74
+ */
75
+ declare const createParser: (options?: ParseOptions) => Parser;
76
+ //#endregion
77
+ export { normalizeLineEndings as a, TokenizeInlineOptions as c, isImageUrl as d, createParser as i, tokenizeInline as l, ParseOptions as n, parse as o, Parser as r, parseLine as s, ParseLineOptions as t, asImageSrc as u };
78
+ //# sourceMappingURL=index-CrIlOW1k.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CrIlOW1k.d.mts","names":[],"sources":["../src/core/image-url.ts","../src/inline/tokenize.ts","../src/parse.ts"],"mappings":";;;;;cAwDa,aAAc;;;;;;;cAQd,aAAc;;;UCjDV;;WAEN;;WAEA;aACE;aACA;aACA;;;WAGF,sBAAsB;;;;;;;cAgHpB,iBACX,gBACA,UAAU,mCACA;;;;;;;;;cC7HC,uBAAwB;UAEpB;;WAEN,sBAAsB;;;;;;;;cAmBpB,QAAS,gBAAgB,UAAU,iBAAe;UAyB9C,yBAAyB;;WAE/B;;WAEA;;;;;;;;cASE,YAAa,aAAa,UAAU,qBAAmB;UAQnD;WACN,QAAQ,mBAAmB;WAC3B,YAAY,aAAa,UAAU,KAAK,oCAAoC;;;;;;cAO1E,eAAgB,UAAU,iBAAe"}
@@ -0,0 +1,78 @@
1
+ import { g as InlineNode, w as Page, x as LineBlock } from "./types-gWMrsXPX.cjs";
2
+ import { i as Extension } from "./types-_-ZAyNkz.cjs";
3
+ import "effect";
4
+ //#region src/core/image-url.d.ts
5
+ /** URL が画像として表示されるものか。判定だけを行い、URL は書き換えない。 */
6
+ declare const isImageUrl: (url: string) => boolean;
7
+ /**
8
+ * 画像 URL なら `<img src>` に入れられる形にして返す。画像でなければ null。
9
+ *
10
+ * Gyazo のページ URL はここでだけ `https://i.gyazo.com/{hash}.png` に差し替わる。
11
+ * `parse()` はこの変換を行わない (AST はソースに書かれた文字列を保つ)。
12
+ */
13
+ declare const asImageSrc: (url: string) => string | null;
14
+ //#endregion
15
+ //#region src/inline/tokenize.d.ts
16
+ interface TokenizeInlineOptions {
17
+ /** 装飾記法を解釈するか (既定: true)。装飾の中身を解析するときだけ false になる */
18
+ readonly allowDecoration?: boolean;
19
+ /** この文字列がページ上のどこにあるか。省略時は 0 行目の先頭 */
20
+ readonly origin?: {
21
+ readonly line?: number;
22
+ readonly column?: number;
23
+ readonly offset?: number;
24
+ };
25
+ /** 記法の拡張。既定のルールより先に試される */
26
+ readonly extensions?: readonly Extension[];
27
+ }
28
+ /**
29
+ * インライン記法をノード列に分解する。改行を含まない 1 行分の文字列を渡すこと。
30
+ *
31
+ * 記法として成立しなかった文字は text ノードにまとまる。空文字列では空配列を返す。
32
+ */
33
+ declare const tokenizeInline: (source: string, options?: TokenizeInlineOptions) => readonly InlineNode[];
34
+ //#endregion
35
+ //#region src/parse.d.ts
36
+ /**
37
+ * CR / CRLF を LF に揃える。
38
+ *
39
+ * `parse` は必ずこれを通してから解析するので、報告される位置は**正規化後**の
40
+ * 文字列を基準にする。CRLF のソースでは元のオフセットと 1 行につき 1 文字ずれる。
41
+ */
42
+ declare const normalizeLineEndings: (source: string) => string;
43
+ interface ParseOptions {
44
+ /** 記法の拡張。既定のルールより先に試される */
45
+ readonly extensions?: readonly Extension[];
46
+ }
47
+ /**
48
+ * ページ全文をパースする。**失敗しない**: どんな入力でも Page を返す。
49
+ * 記法として成立しない部分は素のテキストになるだけで、エラーにはならない。
50
+ *
51
+ * 1 行目はタイトルとして扱われる (Cosense のページは 1 行目がタイトル)。
52
+ */
53
+ declare const parse: (source: string, options?: ParseOptions) => Page;
54
+ interface ParseLineOptions extends ParseOptions {
55
+ /** この行がページ内の何行目か (既定: 0) */
56
+ readonly line?: number;
57
+ /** この行の行頭がソース全文のどこにあるか (既定: 0) */
58
+ readonly offset?: number;
59
+ }
60
+ /**
61
+ * 1 行だけを通常行としてパースする。
62
+ *
63
+ * `code:` / `table:` はページの文脈があって初めてブロックになるので、
64
+ * ここでは通常行として扱う。
65
+ */
66
+ declare const parseLine: (raw: string, options?: ParseLineOptions) => LineBlock;
67
+ interface Parser {
68
+ readonly parse: (source: string) => Page;
69
+ readonly parseLine: (raw: string, options?: Omit<ParseLineOptions, "extensions">) => LineBlock;
70
+ }
71
+ /**
72
+ * 拡張を固定したパーサーを作る。同じ拡張で何度もパースするときに、
73
+ * 呼び出しごとに options を渡さずに済む。
74
+ */
75
+ declare const createParser: (options?: ParseOptions) => Parser;
76
+ //#endregion
77
+ export { normalizeLineEndings as a, TokenizeInlineOptions as c, isImageUrl as d, createParser as i, tokenizeInline as l, ParseOptions as n, parse as o, Parser as r, parseLine as s, ParseLineOptions as t, asImageSrc as u };
78
+ //# sourceMappingURL=index-_o1STNhP.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-_o1STNhP.d.cts","names":[],"sources":["../src/core/image-url.ts","../src/inline/tokenize.ts","../src/parse.ts"],"mappings":";;;;;cAwDa,aAAc;;;;;;;cAQd,aAAc;;;UCjDV;;WAEN;;WAEA;aACE;aACA;aACA;;;WAGF,sBAAsB;;;;;;;cAgHpB,iBACX,gBACA,UAAU,mCACA;;;;;;;;;cC7HC,uBAAwB;UAEpB;;WAEN,sBAAsB;;;;;;;;cAmBpB,QAAS,gBAAgB,UAAU,iBAAe;UAyB9C,yBAAyB;;WAE/B;;WAEA;;;;;;;;cASE,YAAa,aAAa,UAAU,qBAAmB;UAQnD;WACN,QAAQ,mBAAmB;WAC3B,YAAY,aAAa,UAAU,KAAK,oCAAoC;;;;;;cAO1E,eAAgB,UAAU,iBAAe"}
package/dist/index.cjs CHANGED
@@ -1,7 +1,29 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_image_url = require("./image-url-YcTed8tg.cjs");
3
- const require_decoration = require("./decoration-DODaJKeA.cjs");
4
3
  let effect = require("effect");
4
+ //#region src/core/position.ts
5
+ const originOfLine = (line, offset) => ({
6
+ line,
7
+ column: 0,
8
+ offset
9
+ });
10
+ /** Origin を `by` 文字ぶん進める (部分文字列を再帰的に走査するときに使う)。 */
11
+ const shiftOrigin = (origin, by) => ({
12
+ line: origin.line,
13
+ column: origin.column + by,
14
+ offset: origin.offset + by
15
+ });
16
+ const pointAt = (origin, index) => ({
17
+ line: origin.line,
18
+ column: origin.column + index,
19
+ offset: origin.offset + index
20
+ });
21
+ /** 走査対象の `[start, end)` が占める Position。end は exclusive。 */
22
+ const spanAt = (origin, start, end) => ({
23
+ start: pointAt(origin, start),
24
+ end: pointAt(origin, end)
25
+ });
26
+ //#endregion
5
27
  //#region src/inline/constructs/bare-url.ts
6
28
  const URL_RE = /^https?:\/\/[^\s\]]+/i;
7
29
  /**
@@ -29,7 +51,7 @@ const bareUrlConstruct = (source, index) => {
29
51
  * scan.ts — 記法の知識を持たない文字列走査のプリミティブ。
30
52
  */
31
53
  /** 行頭の空白 (半角スペース / タブ / 全角スペース)。インデント判定の単一ソース。 */
32
- const LEADING_WHITESPACE_RE = /^[ \t ]*/;
54
+ const LEADING_WHITESPACE_RE = /^[ \t\u3000]*/;
33
55
  const leadingWhitespace = (s) => LEADING_WHITESPACE_RE.exec(s)?.[0] ?? "";
34
56
  /**
35
57
  * `openIdx` の `[` に対応する `]` の位置。深さを数えるので
@@ -54,6 +76,61 @@ const isTagBoundary = (s, index) => {
54
76
  return prev === " " || prev === " " || prev === " ";
55
77
  };
56
78
  //#endregion
79
+ //#region src/inline/bracket-rules/decoration.ts
80
+ /**
81
+ * `[<記号の並び> 中身]`。記号は Cosense の文字装飾の記号 `` !"#%&'()*+,-./{|}<>_~= `` で、
82
+ * Cosense Web はこの集合からなる並びをすべて装飾として読む (help-jp「文字装飾記法」)。
83
+ * `=` は help-jp が将来の別用途のために使わないよう求めているが、今の Cosense Web は
84
+ * 装飾として読むので含める。
85
+ * どの記号が装飾になるかは構文なので、設定で変えられるようにしない。
86
+ * `$` と `[` はこの一覧に無い。`[$ x]` は数式、`[[x]]` は太字の別の記法で、どちらも別のルールが読む。
87
+ *
88
+ * 文字列から組み立てずに正規表現のリテラルで持つのは、トップレベルで関数を呼ばないため
89
+ * (`sideEffects: false`)。
90
+ */
91
+ const DECORATION_PATTERN = /^([!"#%&'()*+,\-./{|}<>_~=]+)\s+([\s\S]+)$/;
92
+ /**
93
+ * 見た目が決まっている記号。bold などのフラグはこれだけから決める。
94
+ * ほかの記号は `markers` に残るだけで、見た目はプロジェクトの UserCSS が付ける。
95
+ */
96
+ const STYLED_MARKERS = {
97
+ bold: "*",
98
+ italic: "/",
99
+ strike: "-",
100
+ underline: "_"
101
+ };
102
+ const MAX_SIZE_LEVEL = 4;
103
+ /** 出現順を保ったまま重複を落とす。`[*** x]` の markers は `['*']` になる。 */
104
+ const uniqueChars = (marks) => [...new Set(marks)];
105
+ /**
106
+ * `[* 太字]` `[/ 斜体]` `[- 打消し]` `[_ 下線]` とその複合 (`[-/ x]`)、
107
+ * および見た目の付かない記号の装飾 (`[! 注意]`)。
108
+ *
109
+ * 中身はリンクやアイコンとして再帰的に解釈するが、**装飾の入れ子は不可**。
110
+ * そのため子の走査は allowDecoration=false で行う。
111
+ * 例: `[* [* 太字]ですね]` の内側は装飾ではなく内部リンクになる。
112
+ */
113
+ const decorationRule = (inner, ctx) => {
114
+ if (!ctx.allowDecoration) return effect.Option.none();
115
+ const match = inner.match(DECORATION_PATTERN);
116
+ if (!match) return effect.Option.none();
117
+ const marks = match[1] ?? "";
118
+ const value = match[2] ?? "";
119
+ const stars = [...marks].filter((mark) => mark === STYLED_MARKERS.bold).length;
120
+ const valueOffset = inner.length - value.length;
121
+ return effect.Option.some({
122
+ type: "decoration",
123
+ value,
124
+ markers: uniqueChars(marks),
125
+ bold: stars > 0,
126
+ italic: marks.includes(STYLED_MARKERS.italic),
127
+ strike: marks.includes(STYLED_MARKERS.strike),
128
+ underline: marks.includes(STYLED_MARKERS.underline),
129
+ sizeLevel: Math.min(Math.max(stars - 1, 0), MAX_SIZE_LEVEL),
130
+ children: ctx.tokenize(value, shiftOrigin(ctx.innerOrigin, valueOffset), false)
131
+ });
132
+ };
133
+ //#endregion
57
134
  //#region src/inline/bracket-rules/formula.ts
58
135
  /** `[$ x^2]` — 中身は解釈せず生のまま返す (KaTeX 等に渡す想定)。 */
59
136
  const formulaRule = (inner) => inner.startsWith("$") ? effect.Option.some({
@@ -186,7 +263,7 @@ const urlRule = (inner) => {
186
263
  //#endregion
187
264
  //#region src/inline/bracket-rules/index.ts
188
265
  /** 中身に角括弧を含んでいても成立しうるルール。 */
189
- const bracketRules = [formulaRule, require_decoration.decorationRule];
266
+ const bracketRules = [formulaRule, decorationRule];
190
267
  /**
191
268
  * 「単純ターゲット」のルール。中身に `[` / `]` を含むときは試さない (Cosense Web に合わせている)。
192
269
  * これにより `[[そうね] ですね]` の外側は記法にならず、先頭の `[` が素の文字になる。
@@ -219,7 +296,7 @@ const bracketConstruct = (source, index, ctx) => {
219
296
  if (inner.trim() === "") return effect.Option.none();
220
297
  const innerCtx = {
221
298
  ...ctx,
222
- innerOrigin: require_decoration.shiftOrigin(ctx.origin, index + 1)
299
+ innerOrigin: shiftOrigin(ctx.origin, index + 1)
223
300
  };
224
301
  return (0, effect.pipe)(parseInner(inner, innerCtx), effect.Option.map((node) => ({
225
302
  node,
@@ -295,7 +372,7 @@ const strongBracketConstruct = (source, index, ctx) => {
295
372
  strike: false,
296
373
  underline: false,
297
374
  sizeLevel: 0,
298
- children: ctx.tokenize(inner, require_decoration.shiftOrigin(ctx.origin, index + 2), false)
375
+ children: ctx.tokenize(inner, shiftOrigin(ctx.origin, index + 2), false)
299
376
  },
300
377
  length
301
378
  });
@@ -318,21 +395,20 @@ const inlineConstructs = [
318
395
  * その 1 文字はテキストとして貯めておき、次にノードが出たところ (と末尾) でまとめて
319
396
  * text ノードにする。位置の付与はこのループだけが行う。
320
397
  */
398
+ /** 拡張のルールは null で返すので、中のルールと同じく Option で返す形に包む。 */
399
+ const fromConstruct = (construct) => (source, index, ctx) => effect.Option.fromNullable(construct(source, index, ctx));
400
+ const fromBracketRule = (rule) => (inner, ctx) => effect.Option.fromNullable(rule(inner, ctx));
321
401
  /** 拡張のルールを既定のルールの前に並べる。拡張が既定の記法を上書きできるのはこの順序による。 */
322
402
  const resolveExtensions = (extensions) => {
323
403
  if (extensions === void 0 || extensions.length === 0) return {
324
404
  constructs: inlineConstructs,
325
405
  bracketRules: []
326
406
  };
327
- const constructs = [];
328
- const bracketRules = [];
329
- for (const extension of extensions) {
330
- if (extension.constructs) constructs.push(...extension.constructs);
331
- if (extension.bracketRules) bracketRules.push(...extension.bracketRules);
332
- }
407
+ const constructs = extensions.flatMap((extension) => extension.constructs ?? []);
408
+ const bracketRules = extensions.flatMap((extension) => extension.bracketRules ?? []);
333
409
  return {
334
- constructs: [...constructs, ...inlineConstructs],
335
- bracketRules
410
+ constructs: [...constructs.map(fromConstruct), ...inlineConstructs],
411
+ bracketRules: bracketRules.map(fromBracketRule)
336
412
  };
337
413
  };
338
414
  /**
@@ -363,7 +439,7 @@ const scan = (source, origin, allowDecoration, rules) => {
363
439
  out.push({
364
440
  type: "text",
365
441
  value: source.slice(textStart, end),
366
- position: require_decoration.spanAt(origin, textStart, end)
442
+ position: spanAt(origin, textStart, end)
367
443
  });
368
444
  };
369
445
  while (index < source.length) {
@@ -374,7 +450,7 @@ const scan = (source, origin, allowDecoration, rules) => {
374
450
  }
375
451
  flushText(index);
376
452
  const { node, length } = match.value;
377
- out.push(withPosition(node, require_decoration.spanAt(origin, index, index + length)));
453
+ out.push(withPosition(node, spanAt(origin, index, index + length)));
378
454
  index += length;
379
455
  textStart = index;
380
456
  }
@@ -457,8 +533,8 @@ const indentOf = (raw) => leadingWhitespace(raw).length;
457
533
  * この複数行のまとまりを作るのがここの仕事で、行の中身の解釈は注入された tokenize に任せる
458
534
  * (どの記法ルールを使うかを知らずに済むので、拡張入りのパーサーでもここは変わらない)。
459
535
  */
460
- const lineOrigin = (line) => require_decoration.originOfLine(line.index, line.offset);
461
- const wholeLine = (line) => require_decoration.spanAt(lineOrigin(line), 0, line.text.length);
536
+ const lineOrigin = (line) => originOfLine(line.index, line.offset);
537
+ const wholeLine = (line) => spanAt(lineOrigin(line), 0, line.text.length);
462
538
  /** インデントを `amount` 文字ぶん浅くする。空白より多くは削らない。 */
463
539
  const dedent = (text, amount) => text.slice(Math.min(amount, indentOf(text)));
464
540
  const titleBlock = (line, tokenize) => ({
@@ -484,24 +560,23 @@ const codeLine = (line, headerIndent) => ({
484
560
  value: dedent(line.text, headerIndent + 1),
485
561
  position: wholeLine(line)
486
562
  });
487
- const tableCells = (line) => {
488
- const indent = indentOf(line.text);
563
+ const tableCells = (line, tokenize) => {
489
564
  const origin = lineOrigin(line);
490
- const cells = [];
491
- let start = indent;
492
- for (const value of line.text.slice(indent).split(" ")) {
493
- cells.push({
565
+ const indent = indentOf(line.text);
566
+ const values = line.text.slice(indent).split(" ");
567
+ return values.map((value, index) => {
568
+ const start = indent + values.slice(0, index).reduce((sum, cell) => sum + cell.length + 1, 0);
569
+ return {
494
570
  type: "tableCell",
495
571
  value,
496
- position: require_decoration.spanAt(origin, start, start + value.length)
497
- });
498
- start += value.length + 1;
499
- }
500
- return cells;
572
+ children: tokenize(value, shiftOrigin(origin, start)),
573
+ position: spanAt(origin, start, start + value.length)
574
+ };
575
+ });
501
576
  };
502
- const tableRow = (line) => ({
577
+ const tableRow = (line, tokenize) => ({
503
578
  type: "tableRow",
504
- cells: tableCells(line),
579
+ cells: tableCells(line, tokenize),
505
580
  position: wholeLine(line)
506
581
  });
507
582
  /** ヘッダより深いインデントが続く範囲の終端 (exclusive)。 */
@@ -525,18 +600,19 @@ const codeBlock = (header, body, filename, indent) => ({
525
600
  lines: body.map((line) => codeLine(line, indent)),
526
601
  position: blockPosition(header, body)
527
602
  });
528
- const tableBlock = (header, body, name, indent) => ({
603
+ const tableBlock = (header, body, name, indent, tokenize) => ({
529
604
  type: "table",
530
605
  name,
531
606
  indent,
532
- rows: body.map(tableRow),
607
+ rows: body.map((line) => tableRow(line, tokenize)),
533
608
  position: blockPosition(header, body)
534
609
  });
535
610
  /**
536
611
  * 行の並びをブロックの並びに畳む。1 行目は無条件でタイトルになる
537
612
  * (Cosense ではタイトル行が `code:` や `table:` として解釈されることはない)。
538
613
  */
539
- const buildBlocks = (lines, tokenize) => {
614
+ const buildBlocks = (lines, tokenizers) => {
615
+ const tokenize = tokenizers.line;
540
616
  const blocks = [];
541
617
  const head = lines[0];
542
618
  if (head === void 0) return blocks;
@@ -551,7 +627,7 @@ const buildBlocks = (lines, tokenize) => {
551
627
  return end;
552
628
  }), effect.Match.tag("tableHeader", (role) => {
553
629
  const end = bodyEnd(lines, index + 1, role.indent);
554
- blocks.push(tableBlock(line, lines.slice(index + 1, end), role.name, role.indent));
630
+ blocks.push(tableBlock(line, lines.slice(index + 1, end), role.name, role.indent, tokenizers.tableCell));
555
631
  return end;
556
632
  }), effect.Match.tag("content", (role) => {
557
633
  blocks.push(lineBlock(line, role, tokenize));
@@ -575,6 +651,79 @@ const buildLineBlock = (line, tokenize) => {
575
651
  }, tokenize);
576
652
  };
577
653
  //#endregion
654
+ //#region src/inline/table-cell.ts
655
+ /**
656
+ * table-cell.ts — テーブルのセルの中の記法。
657
+ *
658
+ * Cosense Web はセルの中ではリンクの記法 (`[title]` / `[https://…]` / `[/project/page]` /
659
+ * 裸の URL / `#tag`) だけを読み、それ以外は書いたままの文字として出す。
660
+ * ここでは行と同じ規則でいったん読み、残さないノードを書いたままの文字に戻す。
661
+ * 最初からリンクの規則だけで読むと、`[* 太字]` が `* 太字` というページへのリンクになってしまうため。
662
+ *
663
+ * どのノードを残すかは拡張の `keepInTableCell` で足せる (`tableCellNotation`)。
664
+ */
665
+ /** Cosense Web がセルの中でも読む記法。 */
666
+ const LINK_TYPES = /* @__PURE__ */ new Set([
667
+ "internalLink",
668
+ "externalLink",
669
+ "projectLink",
670
+ "hashtag"
671
+ ]);
672
+ /** リンクの記法は必ず残し、それに加えて拡張のどれかが残すと言ったノードを残す。 */
673
+ const keepInTableCellOf = (extensions = []) => {
674
+ const keeps = extensions.flatMap((extension) => extension.keepInTableCell === void 0 ? [] : [extension.keepInTableCell]);
675
+ return (node) => LINK_TYPES.has(node.type) || keeps.some((keep) => keep(node));
676
+ };
677
+ const joinTexts = (previous, next) => ({
678
+ type: "text",
679
+ value: previous.value + next.value,
680
+ position: {
681
+ start: previous.position.start,
682
+ end: next.position.end
683
+ }
684
+ });
685
+ const isText = (node) => node.type === "text";
686
+ /** `start` から続く text ノードの並び。 */
687
+ const textRunFrom = (nodes, start) => {
688
+ const end = nodes.findIndex((node, index) => index > start && !isText(node));
689
+ return nodes.slice(start, end === -1 ? nodes.length : end).filter(isText);
690
+ };
691
+ /**
692
+ * 隣り合う text ノードを 1 つにまとめる。行を読んだときと同じ形にするため。
693
+ * text の並びの先頭で並び全体をまとめ、続きの text は先頭に含まれているので出さない。
694
+ */
695
+ const mergeTexts = (nodes) => nodes.flatMap((node, index) => !isText(node) ? [node] : (0, effect.pipe)(effect.Option.fromNullable(nodes[index - 1]), effect.Option.exists(isText)) ? [] : [textRunFrom(nodes, index).reduce(joinTexts)]);
696
+ /**
697
+ * 行と同じ規則で読んだセルのノード列から、`keep` が残さないノードを書いたままの文字に戻す。
698
+ * `source` はセルの中身、`origin` はその先頭のソース上の位置。
699
+ *
700
+ * 装飾を残さないときは記号の部分だけを文字に戻し、中身は同じように辿る。
701
+ * `[* [リンク]]` の中のリンクは残す。装飾を残すときも、その中身は同じ規則で辿る。
702
+ */
703
+ const keepNotation = (nodes, source, origin, keep) => {
704
+ /** ソース上の `[start, end)` を、書いたままの文字のノードにする。 */
705
+ const literal = (start, end) => start >= end ? [] : [{
706
+ type: "text",
707
+ value: source.slice(start - origin.offset, end - origin.offset),
708
+ position: spanAt(origin, start - origin.offset, end - origin.offset)
709
+ }];
710
+ const inside = (children) => mergeTexts(children.flatMap(demote));
711
+ /** 装飾の記号だけを文字に戻し、中身は辿る。中身が空なら丸ごと文字にする。 */
712
+ const unwrap = (node) => (0, effect.pipe)(effect.Option.all([effect.Option.fromNullable(node.children[0]), effect.Option.fromNullable(node.children[node.children.length - 1])]), effect.Option.match({
713
+ onNone: () => literal(node.position.start.offset, node.position.end.offset),
714
+ onSome: ([first, last]) => [
715
+ ...literal(node.position.start.offset, first.position.start.offset),
716
+ ...node.children.flatMap(demote),
717
+ ...literal(last.position.end.offset, node.position.end.offset)
718
+ ]
719
+ }));
720
+ const demote = (node) => effect.Match.value(node).pipe(effect.Match.when({ type: "text" }, (text) => [text]), effect.Match.when({ type: "decoration" }, (decoration) => keep(decoration) ? [{
721
+ ...decoration,
722
+ children: inside(decoration.children)
723
+ }] : unwrap(decoration)), effect.Match.when(keep, (kept) => [kept]), effect.Match.orElse((other) => literal(other.position.start.offset, other.position.end.offset)));
724
+ return inside(nodes);
725
+ };
726
+ //#endregion
578
727
  //#region src/parse.ts
579
728
  /**
580
729
  * parse.ts — ページ全文の入口。
@@ -609,10 +758,14 @@ const parse = (source, options) => {
609
758
  const normalized = normalizeLineEndings(source);
610
759
  const lines = toSourceLines(normalized);
611
760
  const rules = resolveExtensions(options?.extensions);
761
+ const keepInTableCell = keepInTableCellOf(options?.extensions);
612
762
  const last = lines[lines.length - 1];
613
763
  return {
614
764
  type: "page",
615
- children: buildBlocks(lines, (text, origin) => tokenizeInlineWith(text, origin, rules)),
765
+ children: buildBlocks(lines, {
766
+ line: (text, origin) => tokenizeInlineWith(text, origin, rules),
767
+ tableCell: (text, origin) => keepNotation(tokenizeInlineWith(text, origin, rules), text, origin, keepInTableCell)
768
+ }),
616
769
  position: {
617
770
  start: {
618
771
  line: 0,