@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.
- package/README.md +45 -31
- package/dist/{ast-uYWtkHwT.cjs → ast-C0BGz7X2.cjs} +2 -2
- package/dist/ast-C0BGz7X2.cjs.map +1 -0
- package/dist/{ast-CIKXwSl5.mjs → ast-CMQYawYl.mjs} +2 -2
- package/dist/ast-CMQYawYl.mjs.map +1 -0
- package/dist/compile.cjs +2 -213
- package/dist/compile.cjs.map +1 -1
- package/dist/compile.d.cts +2 -129
- package/dist/compile.d.cts.map +1 -1
- package/dist/compile.d.mts +2 -129
- package/dist/compile.d.mts.map +1 -1
- package/dist/compile.mjs +3 -207
- package/dist/compile.mjs.map +1 -1
- package/dist/extensions.cjs +10 -7
- package/dist/extensions.cjs.map +1 -1
- package/dist/extensions.d.cts +12 -8
- package/dist/extensions.d.cts.map +1 -1
- package/dist/extensions.d.mts +12 -8
- package/dist/extensions.d.mts.map +1 -1
- package/dist/extensions.mjs +10 -7
- package/dist/extensions.mjs.map +1 -1
- package/dist/html.cjs +458 -0
- package/dist/html.cjs.map +1 -0
- package/dist/html.d.cts +274 -0
- package/dist/html.d.cts.map +1 -0
- package/dist/html.d.mts +274 -0
- package/dist/html.d.mts.map +1 -0
- package/dist/html.mjs +447 -0
- package/dist/html.mjs.map +1 -0
- package/dist/image-url-YcTed8tg.cjs.map +1 -1
- package/dist/image-url-eQ6X-oN0.mjs.map +1 -1
- package/dist/index-CrIlOW1k.d.mts +78 -0
- package/dist/index-CrIlOW1k.d.mts.map +1 -0
- package/dist/index-_o1STNhP.d.cts +78 -0
- package/dist/index-_o1STNhP.d.cts.map +1 -0
- package/dist/index.cjs +188 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -77
- package/dist/index.d.mts +4 -77
- package/dist/index.mjs +180 -27
- package/dist/index.mjs.map +1 -1
- package/dist/schema.cjs +1 -0
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.cts.map +1 -1
- package/dist/schema.d.mts +1 -1
- package/dist/schema.d.mts.map +1 -1
- package/dist/schema.mjs +1 -0
- package/dist/schema.mjs.map +1 -1
- package/dist/{types-CiwPj2R8.d.cts → types-CdiHju1P.d.mts} +15 -12
- package/dist/types-CdiHju1P.d.mts.map +1 -0
- package/dist/{types-9vAAFHka.d.mts → types-_-ZAyNkz.d.cts} +15 -12
- package/dist/types-_-ZAyNkz.d.cts.map +1 -0
- package/dist/{types-DmfFumcw.d.cts → types-gWMrsXPX.d.cts} +28 -21
- package/dist/{types-DmfFumcw.d.cts.map → types-gWMrsXPX.d.cts.map} +1 -1
- package/dist/{types-DmfFumcw.d.mts → types-gWMrsXPX.d.mts} +28 -21
- package/dist/{types-DmfFumcw.d.mts.map → types-gWMrsXPX.d.mts.map} +1 -1
- package/dist/utils.cjs +1 -1
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +2 -2
- package/dist/utils.d.cts.map +1 -1
- package/dist/utils.d.mts +2 -2
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +1 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +27 -5
- package/dist/ast-CIKXwSl5.mjs.map +0 -1
- package/dist/ast-uYWtkHwT.cjs.map +0 -1
- package/dist/decoration-DODaJKeA.cjs +0 -103
- package/dist/decoration-DODaJKeA.cjs.map +0 -1
- package/dist/decoration-Dq_5e20j.mjs +0 -68
- package/dist/decoration-Dq_5e20j.mjs.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/types-9vAAFHka.d.mts.map +0 -1
- 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,
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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) =>
|
|
461
|
-
const wholeLine = (line) =>
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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,
|
|
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,
|
|
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,
|