@cosense-toolbox/parser 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -758
- 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 -212
- package/dist/compile.cjs.map +1 -1
- package/dist/compile.d.cts +17 -125
- package/dist/compile.d.cts.map +1 -1
- package/dist/compile.d.mts +17 -125
- package/dist/compile.d.mts.map +1 -1
- package/dist/compile.mjs +3 -206
- package/dist/compile.mjs.map +1 -1
- package/dist/decoration-BB1cS4lH.mjs +72 -0
- package/dist/decoration-BB1cS4lH.mjs.map +1 -0
- package/dist/decoration-BYXIXxCA.cjs +107 -0
- package/dist/decoration-BYXIXxCA.cjs.map +1 -0
- package/dist/extensions.cjs +32 -0
- package/dist/extensions.cjs.map +1 -0
- package/dist/extensions.d.cts +26 -0
- package/dist/extensions.d.cts.map +1 -0
- package/dist/extensions.d.mts +26 -0
- package/dist/extensions.d.mts.map +1 -0
- package/dist/extensions.mjs +30 -0
- package/dist/extensions.mjs.map +1 -0
- 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 +118 -94
- 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 +110 -86
- package/dist/index.mjs.map +1 -1
- package/dist/schema.cjs +2 -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 +2 -0
- package/dist/schema.mjs.map +1 -1
- package/dist/{types-BXlnUFr0.d.mts → types-CdiHju1P.d.mts} +15 -12
- package/dist/types-CdiHju1P.d.mts.map +1 -0
- package/dist/{types-DVwlUtla.d.cts → types-_-ZAyNkz.d.cts} +15 -12
- package/dist/types-_-ZAyNkz.d.cts.map +1 -0
- package/dist/{types-Bo_BrmvK.d.cts → types-gWMrsXPX.d.cts} +31 -22
- package/dist/types-gWMrsXPX.d.cts.map +1 -0
- package/dist/{types-Bo_BrmvK.d.mts → types-gWMrsXPX.d.mts} +31 -22
- package/dist/types-gWMrsXPX.d.mts.map +1 -0
- package/dist/utils.cjs +2 -2
- 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 +2 -2
- package/dist/utils.mjs.map +1 -1
- package/package.json +33 -10
- package/dist/ast-CIKXwSl5.mjs.map +0 -1
- package/dist/ast-uYWtkHwT.cjs.map +0 -1
- package/dist/create-compiler-CvlndKOA.d.cts +0 -23
- package/dist/create-compiler-CvlndKOA.d.cts.map +0 -1
- package/dist/create-compiler-jdA408ny.d.mts +0 -23
- package/dist/create-compiler-jdA408ny.d.mts.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/plugin.cjs +0 -0
- package/dist/plugin.d.cts +0 -4
- package/dist/plugin.d.mts +0 -4
- package/dist/plugin.mjs +0 -1
- package/dist/types-BXlnUFr0.d.mts.map +0 -1
- package/dist/types-Bo_BrmvK.d.cts.map +0 -1
- package/dist/types-Bo_BrmvK.d.mts.map +0 -1
- package/dist/types-DVwlUtla.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,34 +1,12 @@
|
|
|
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-BYXIXxCA.cjs");
|
|
3
4
|
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
|
|
27
5
|
//#region src/inline/constructs/bare-url.ts
|
|
28
6
|
const URL_RE = /^https?:\/\/[^\s\]]+/i;
|
|
29
7
|
/**
|
|
30
8
|
* 角括弧で囲まれていない URL。常に外部リンクになり、画像 URL でも画像にはしない
|
|
31
|
-
* (
|
|
9
|
+
* (Cosense Web に合わせている。インライン画像になるのは `[https://.../x.png]` の角括弧つきのみ)。
|
|
32
10
|
*/
|
|
33
11
|
const bareUrlConstruct = (source, index) => {
|
|
34
12
|
const head = source[index];
|
|
@@ -51,7 +29,7 @@ const bareUrlConstruct = (source, index) => {
|
|
|
51
29
|
* scan.ts — 記法の知識を持たない文字列走査のプリミティブ。
|
|
52
30
|
*/
|
|
53
31
|
/** 行頭の空白 (半角スペース / タブ / 全角スペース)。インデント判定の単一ソース。 */
|
|
54
|
-
const LEADING_WHITESPACE_RE = /^[ \t
|
|
32
|
+
const LEADING_WHITESPACE_RE = /^[ \t\u3000]*/;
|
|
55
33
|
const leadingWhitespace = (s) => LEADING_WHITESPACE_RE.exec(s)?.[0] ?? "";
|
|
56
34
|
/**
|
|
57
35
|
* `openIdx` の `[` に対応する `]` の位置。深さを数えるので
|
|
@@ -76,37 +54,6 @@ const isTagBoundary = (s, index) => {
|
|
|
76
54
|
return prev === " " || prev === " " || prev === " ";
|
|
77
55
|
};
|
|
78
56
|
//#endregion
|
|
79
|
-
//#region src/inline/bracket-rules/decoration.ts
|
|
80
|
-
/** 先頭の装飾記号の run と、空白を挟んだ中身。 */
|
|
81
|
-
const DECORATION_RE = /^([*/\-_]+)\s+([\s\S]+)$/;
|
|
82
|
-
const MAX_SIZE_LEVEL = 4;
|
|
83
|
-
/**
|
|
84
|
-
* `[* 太字]` `[/ 斜体]` `[- 打消し]` `[_ 下線]` とその複合 (`[-/ x]`)。
|
|
85
|
-
*
|
|
86
|
-
* 中身はリンクやアイコンとして再帰的に解釈するが、**装飾の入れ子は不可**
|
|
87
|
-
* (本家準拠)。そのため子の走査は allowDecoration=false で行う。
|
|
88
|
-
* 例: `[* [* 太字]ですね]` の内側は装飾ではなく内部リンクになる。
|
|
89
|
-
*/
|
|
90
|
-
const decorationRule = (inner, ctx) => {
|
|
91
|
-
if (!ctx.allowDecoration) return effect.Option.none();
|
|
92
|
-
const match = inner.match(DECORATION_RE);
|
|
93
|
-
if (!match) return effect.Option.none();
|
|
94
|
-
const marks = match[1] ?? "";
|
|
95
|
-
const value = match[2] ?? "";
|
|
96
|
-
const stars = (marks.match(/\*/g) ?? []).length;
|
|
97
|
-
const valueOffset = inner.length - value.length;
|
|
98
|
-
return effect.Option.some({
|
|
99
|
-
type: "decoration",
|
|
100
|
-
value,
|
|
101
|
-
bold: stars > 0,
|
|
102
|
-
italic: marks.includes("/"),
|
|
103
|
-
strike: marks.includes("-"),
|
|
104
|
-
underline: marks.includes("_"),
|
|
105
|
-
sizeLevel: Math.min(Math.max(stars - 1, 0), MAX_SIZE_LEVEL),
|
|
106
|
-
children: ctx.tokenize(value, shiftOrigin(ctx.innerOrigin, valueOffset), false)
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
//#endregion
|
|
110
57
|
//#region src/inline/bracket-rules/formula.ts
|
|
111
58
|
/** `[$ x^2]` — 中身は解釈せず生のまま返す (KaTeX 等に渡す想定)。 */
|
|
112
59
|
const formulaRule = (inner) => inner.startsWith("$") ? effect.Option.some({
|
|
@@ -135,7 +82,7 @@ const iconRule = (inner) => {
|
|
|
135
82
|
/**
|
|
136
83
|
* URL ではなく拡張子だけで画像と分かる中身 (`[a.png]`)。
|
|
137
84
|
*
|
|
138
|
-
* 装飾の中では画像にせずリンク扱いにする (
|
|
85
|
+
* 装飾の中では画像にせずリンク扱いにする (Cosense Web に合わせている)。`[* [a.png]]` の内側は
|
|
139
86
|
* 画像ではなく `a.png` というタイトルの内部リンクになる。
|
|
140
87
|
*/
|
|
141
88
|
const imageExtensionRule = (inner, ctx) => ctx.allowDecoration && require_image_url.hasImageExtension(inner) ? effect.Option.some({
|
|
@@ -179,7 +126,7 @@ const projectLinkRule = (inner) => {
|
|
|
179
126
|
//#endregion
|
|
180
127
|
//#region src/inline/bracket-rules/url.ts
|
|
181
128
|
const URLS_RE = /https?:\/\/[^\s\]]+/gi;
|
|
182
|
-
/** 画像 URL
|
|
129
|
+
/** 画像 URL が複数あるとき最後のものを採るのはCosense Web の挙動。 */
|
|
183
130
|
const lastImage = (urls) => {
|
|
184
131
|
const images = urls.filter(require_image_url.isImageUrl);
|
|
185
132
|
return effect.Option.fromNullable(images[images.length - 1]);
|
|
@@ -187,7 +134,7 @@ const lastImage = (urls) => {
|
|
|
187
134
|
/** 画像に添える遷移先。画像自身とは別の URL を優先し、無ければ先頭を使う。 */
|
|
188
135
|
const linkFor = (urls, src) => effect.Option.fromNullable(urls.find((url) => url !== src) ?? urls[0]);
|
|
189
136
|
/**
|
|
190
|
-
* 中身に URL
|
|
137
|
+
* 中身に URL を含む角括弧。Cosense Web の挙動に合わせて次の順で決める:
|
|
191
138
|
*
|
|
192
139
|
* 1. URL 以外の文字が残る → ラベル付き外部リンク。URL が画像でも文字リンクにする
|
|
193
140
|
* (`[ラベル https://x/a.png]` は画像にならない)。
|
|
@@ -239,9 +186,9 @@ const urlRule = (inner) => {
|
|
|
239
186
|
//#endregion
|
|
240
187
|
//#region src/inline/bracket-rules/index.ts
|
|
241
188
|
/** 中身に角括弧を含んでいても成立しうるルール。 */
|
|
242
|
-
const bracketRules = [formulaRule, decorationRule];
|
|
189
|
+
const bracketRules = [formulaRule, require_decoration.decorationRule];
|
|
243
190
|
/**
|
|
244
|
-
* 「単純ターゲット」のルール。中身に `[` / `]` を含むときは試さない (
|
|
191
|
+
* 「単純ターゲット」のルール。中身に `[` / `]` を含むときは試さない (Cosense Web に合わせている)。
|
|
245
192
|
* これにより `[[そうね] ですね]` の外側は記法にならず、先頭の `[` が素の文字になる。
|
|
246
193
|
* 末尾の internalLinkRule は常に成立する catch-all。
|
|
247
194
|
*/
|
|
@@ -272,7 +219,7 @@ const bracketConstruct = (source, index, ctx) => {
|
|
|
272
219
|
if (inner.trim() === "") return effect.Option.none();
|
|
273
220
|
const innerCtx = {
|
|
274
221
|
...ctx,
|
|
275
|
-
innerOrigin: shiftOrigin(ctx.origin, index + 1)
|
|
222
|
+
innerOrigin: require_decoration.shiftOrigin(ctx.origin, index + 1)
|
|
276
223
|
};
|
|
277
224
|
return (0, effect.pipe)(parseInner(inner, innerCtx), effect.Option.map((node) => ({
|
|
278
225
|
node,
|
|
@@ -316,7 +263,7 @@ const inlineCodeConstruct = (source, index) => {
|
|
|
316
263
|
//#endregion
|
|
317
264
|
//#region src/inline/constructs/strong-bracket.ts
|
|
318
265
|
/**
|
|
319
|
-
* `[[...]]` —
|
|
266
|
+
* `[[...]]` — Cosense Web の strong。`]]` で閉じるときだけ成立する
|
|
320
267
|
* (深さは数えない。`[[a] b]` のようなケースは bracketConstruct 側で処理される)。
|
|
321
268
|
*
|
|
322
269
|
* 中身が画像 URL なら大きい画像、そうでなければ太字装飾になる。
|
|
@@ -342,12 +289,13 @@ const strongBracketConstruct = (source, index, ctx) => {
|
|
|
342
289
|
node: {
|
|
343
290
|
type: "decoration",
|
|
344
291
|
value: inner,
|
|
292
|
+
markers: ["*"],
|
|
345
293
|
bold: true,
|
|
346
294
|
italic: false,
|
|
347
295
|
strike: false,
|
|
348
296
|
underline: false,
|
|
349
297
|
sizeLevel: 0,
|
|
350
|
-
children: ctx.tokenize(inner, shiftOrigin(ctx.origin, index + 2), false)
|
|
298
|
+
children: ctx.tokenize(inner, require_decoration.shiftOrigin(ctx.origin, index + 2), false)
|
|
351
299
|
},
|
|
352
300
|
length
|
|
353
301
|
});
|
|
@@ -370,21 +318,20 @@ const inlineConstructs = [
|
|
|
370
318
|
* その 1 文字はテキストとして貯めておき、次にノードが出たところ (と末尾) でまとめて
|
|
371
319
|
* text ノードにする。位置の付与はこのループだけが行う。
|
|
372
320
|
*/
|
|
321
|
+
/** 拡張のルールは null で返すので、中のルールと同じく Option で返す形に包む。 */
|
|
322
|
+
const fromConstruct = (construct) => (source, index, ctx) => effect.Option.fromNullable(construct(source, index, ctx));
|
|
323
|
+
const fromBracketRule = (rule) => (inner, ctx) => effect.Option.fromNullable(rule(inner, ctx));
|
|
373
324
|
/** 拡張のルールを既定のルールの前に並べる。拡張が既定の記法を上書きできるのはこの順序による。 */
|
|
374
325
|
const resolveExtensions = (extensions) => {
|
|
375
326
|
if (extensions === void 0 || extensions.length === 0) return {
|
|
376
327
|
constructs: inlineConstructs,
|
|
377
328
|
bracketRules: []
|
|
378
329
|
};
|
|
379
|
-
const constructs = [];
|
|
380
|
-
const bracketRules = [];
|
|
381
|
-
for (const extension of extensions) {
|
|
382
|
-
if (extension.constructs) constructs.push(...extension.constructs);
|
|
383
|
-
if (extension.bracketRules) bracketRules.push(...extension.bracketRules);
|
|
384
|
-
}
|
|
330
|
+
const constructs = extensions.flatMap((extension) => extension.constructs ?? []);
|
|
331
|
+
const bracketRules = extensions.flatMap((extension) => extension.bracketRules ?? []);
|
|
385
332
|
return {
|
|
386
|
-
constructs: [...constructs, ...inlineConstructs],
|
|
387
|
-
bracketRules
|
|
333
|
+
constructs: [...constructs.map(fromConstruct), ...inlineConstructs],
|
|
334
|
+
bracketRules: bracketRules.map(fromBracketRule)
|
|
388
335
|
};
|
|
389
336
|
};
|
|
390
337
|
/**
|
|
@@ -415,7 +362,7 @@ const scan = (source, origin, allowDecoration, rules) => {
|
|
|
415
362
|
out.push({
|
|
416
363
|
type: "text",
|
|
417
364
|
value: source.slice(textStart, end),
|
|
418
|
-
position: spanAt(origin, textStart, end)
|
|
365
|
+
position: require_decoration.spanAt(origin, textStart, end)
|
|
419
366
|
});
|
|
420
367
|
};
|
|
421
368
|
while (index < source.length) {
|
|
@@ -426,7 +373,7 @@ const scan = (source, origin, allowDecoration, rules) => {
|
|
|
426
373
|
}
|
|
427
374
|
flushText(index);
|
|
428
375
|
const { node, length } = match.value;
|
|
429
|
-
out.push(withPosition(node, spanAt(origin, index, index + length)));
|
|
376
|
+
out.push(withPosition(node, require_decoration.spanAt(origin, index, index + length)));
|
|
430
377
|
index += length;
|
|
431
378
|
textStart = index;
|
|
432
379
|
}
|
|
@@ -509,8 +456,8 @@ const indentOf = (raw) => leadingWhitespace(raw).length;
|
|
|
509
456
|
* この複数行のまとまりを作るのがここの仕事で、行の中身の解釈は注入された tokenize に任せる
|
|
510
457
|
* (どの記法ルールを使うかを知らずに済むので、拡張入りのパーサーでもここは変わらない)。
|
|
511
458
|
*/
|
|
512
|
-
const lineOrigin = (line) => originOfLine(line.index, line.offset);
|
|
513
|
-
const wholeLine = (line) => spanAt(lineOrigin(line), 0, line.text.length);
|
|
459
|
+
const lineOrigin = (line) => require_decoration.originOfLine(line.index, line.offset);
|
|
460
|
+
const wholeLine = (line) => require_decoration.spanAt(lineOrigin(line), 0, line.text.length);
|
|
514
461
|
/** インデントを `amount` 文字ぶん浅くする。空白より多くは削らない。 */
|
|
515
462
|
const dedent = (text, amount) => text.slice(Math.min(amount, indentOf(text)));
|
|
516
463
|
const titleBlock = (line, tokenize) => ({
|
|
@@ -536,24 +483,23 @@ const codeLine = (line, headerIndent) => ({
|
|
|
536
483
|
value: dedent(line.text, headerIndent + 1),
|
|
537
484
|
position: wholeLine(line)
|
|
538
485
|
});
|
|
539
|
-
const tableCells = (line) => {
|
|
540
|
-
const indent = indentOf(line.text);
|
|
486
|
+
const tableCells = (line, tokenize) => {
|
|
541
487
|
const origin = lineOrigin(line);
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
488
|
+
const indent = indentOf(line.text);
|
|
489
|
+
const values = line.text.slice(indent).split(" ");
|
|
490
|
+
return values.map((value, index) => {
|
|
491
|
+
const start = indent + values.slice(0, index).reduce((sum, cell) => sum + cell.length + 1, 0);
|
|
492
|
+
return {
|
|
546
493
|
type: "tableCell",
|
|
547
494
|
value,
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
return cells;
|
|
495
|
+
children: tokenize(value, require_decoration.shiftOrigin(origin, start)),
|
|
496
|
+
position: require_decoration.spanAt(origin, start, start + value.length)
|
|
497
|
+
};
|
|
498
|
+
});
|
|
553
499
|
};
|
|
554
|
-
const tableRow = (line) => ({
|
|
500
|
+
const tableRow = (line, tokenize) => ({
|
|
555
501
|
type: "tableRow",
|
|
556
|
-
cells: tableCells(line),
|
|
502
|
+
cells: tableCells(line, tokenize),
|
|
557
503
|
position: wholeLine(line)
|
|
558
504
|
});
|
|
559
505
|
/** ヘッダより深いインデントが続く範囲の終端 (exclusive)。 */
|
|
@@ -577,18 +523,19 @@ const codeBlock = (header, body, filename, indent) => ({
|
|
|
577
523
|
lines: body.map((line) => codeLine(line, indent)),
|
|
578
524
|
position: blockPosition(header, body)
|
|
579
525
|
});
|
|
580
|
-
const tableBlock = (header, body, name, indent) => ({
|
|
526
|
+
const tableBlock = (header, body, name, indent, tokenize) => ({
|
|
581
527
|
type: "table",
|
|
582
528
|
name,
|
|
583
529
|
indent,
|
|
584
|
-
rows: body.map(tableRow),
|
|
530
|
+
rows: body.map((line) => tableRow(line, tokenize)),
|
|
585
531
|
position: blockPosition(header, body)
|
|
586
532
|
});
|
|
587
533
|
/**
|
|
588
534
|
* 行の並びをブロックの並びに畳む。1 行目は無条件でタイトルになる
|
|
589
535
|
* (Cosense ではタイトル行が `code:` や `table:` として解釈されることはない)。
|
|
590
536
|
*/
|
|
591
|
-
const buildBlocks = (lines,
|
|
537
|
+
const buildBlocks = (lines, tokenizers) => {
|
|
538
|
+
const tokenize = tokenizers.line;
|
|
592
539
|
const blocks = [];
|
|
593
540
|
const head = lines[0];
|
|
594
541
|
if (head === void 0) return blocks;
|
|
@@ -603,7 +550,7 @@ const buildBlocks = (lines, tokenize) => {
|
|
|
603
550
|
return end;
|
|
604
551
|
}), effect.Match.tag("tableHeader", (role) => {
|
|
605
552
|
const end = bodyEnd(lines, index + 1, role.indent);
|
|
606
|
-
blocks.push(tableBlock(line, lines.slice(index + 1, end), role.name, role.indent));
|
|
553
|
+
blocks.push(tableBlock(line, lines.slice(index + 1, end), role.name, role.indent, tokenizers.tableCell));
|
|
607
554
|
return end;
|
|
608
555
|
}), effect.Match.tag("content", (role) => {
|
|
609
556
|
blocks.push(lineBlock(line, role, tokenize));
|
|
@@ -627,6 +574,79 @@ const buildLineBlock = (line, tokenize) => {
|
|
|
627
574
|
}, tokenize);
|
|
628
575
|
};
|
|
629
576
|
//#endregion
|
|
577
|
+
//#region src/inline/table-cell.ts
|
|
578
|
+
/**
|
|
579
|
+
* table-cell.ts — テーブルのセルの中の記法。
|
|
580
|
+
*
|
|
581
|
+
* Cosense Web はセルの中ではリンクの記法 (`[title]` / `[https://…]` / `[/project/page]` /
|
|
582
|
+
* 裸の URL / `#tag`) だけを読み、それ以外は書いたままの文字として出す。
|
|
583
|
+
* ここでは行と同じ規則でいったん読み、残さないノードを書いたままの文字に戻す。
|
|
584
|
+
* 最初からリンクの規則だけで読むと、`[* 太字]` が `* 太字` というページへのリンクになってしまうため。
|
|
585
|
+
*
|
|
586
|
+
* どのノードを残すかは拡張の `keepInTableCell` で足せる (`tableCellNotation`)。
|
|
587
|
+
*/
|
|
588
|
+
/** Cosense Web がセルの中でも読む記法。 */
|
|
589
|
+
const LINK_TYPES = /* @__PURE__ */ new Set([
|
|
590
|
+
"internalLink",
|
|
591
|
+
"externalLink",
|
|
592
|
+
"projectLink",
|
|
593
|
+
"hashtag"
|
|
594
|
+
]);
|
|
595
|
+
/** リンクの記法は必ず残し、それに加えて拡張のどれかが残すと言ったノードを残す。 */
|
|
596
|
+
const keepInTableCellOf = (extensions = []) => {
|
|
597
|
+
const keeps = extensions.flatMap((extension) => extension.keepInTableCell === void 0 ? [] : [extension.keepInTableCell]);
|
|
598
|
+
return (node) => LINK_TYPES.has(node.type) || keeps.some((keep) => keep(node));
|
|
599
|
+
};
|
|
600
|
+
const joinTexts = (previous, next) => ({
|
|
601
|
+
type: "text",
|
|
602
|
+
value: previous.value + next.value,
|
|
603
|
+
position: {
|
|
604
|
+
start: previous.position.start,
|
|
605
|
+
end: next.position.end
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
const isText = (node) => node.type === "text";
|
|
609
|
+
/** `start` から続く text ノードの並び。 */
|
|
610
|
+
const textRunFrom = (nodes, start) => {
|
|
611
|
+
const end = nodes.findIndex((node, index) => index > start && !isText(node));
|
|
612
|
+
return nodes.slice(start, end === -1 ? nodes.length : end).filter(isText);
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* 隣り合う text ノードを 1 つにまとめる。行を読んだときと同じ形にするため。
|
|
616
|
+
* text の並びの先頭で並び全体をまとめ、続きの text は先頭に含まれているので出さない。
|
|
617
|
+
*/
|
|
618
|
+
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)]);
|
|
619
|
+
/**
|
|
620
|
+
* 行と同じ規則で読んだセルのノード列から、`keep` が残さないノードを書いたままの文字に戻す。
|
|
621
|
+
* `source` はセルの中身、`origin` はその先頭のソース上の位置。
|
|
622
|
+
*
|
|
623
|
+
* 装飾を残さないときは記号の部分だけを文字に戻し、中身は同じように辿る。
|
|
624
|
+
* `[* [リンク]]` の中のリンクは残す。装飾を残すときも、その中身は同じ規則で辿る。
|
|
625
|
+
*/
|
|
626
|
+
const keepNotation = (nodes, source, origin, keep) => {
|
|
627
|
+
/** ソース上の `[start, end)` を、書いたままの文字のノードにする。 */
|
|
628
|
+
const literal = (start, end) => start >= end ? [] : [{
|
|
629
|
+
type: "text",
|
|
630
|
+
value: source.slice(start - origin.offset, end - origin.offset),
|
|
631
|
+
position: require_decoration.spanAt(origin, start - origin.offset, end - origin.offset)
|
|
632
|
+
}];
|
|
633
|
+
const inside = (children) => mergeTexts(children.flatMap(demote));
|
|
634
|
+
/** 装飾の記号だけを文字に戻し、中身は辿る。中身が空なら丸ごと文字にする。 */
|
|
635
|
+
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({
|
|
636
|
+
onNone: () => literal(node.position.start.offset, node.position.end.offset),
|
|
637
|
+
onSome: ([first, last]) => [
|
|
638
|
+
...literal(node.position.start.offset, first.position.start.offset),
|
|
639
|
+
...node.children.flatMap(demote),
|
|
640
|
+
...literal(last.position.end.offset, node.position.end.offset)
|
|
641
|
+
]
|
|
642
|
+
}));
|
|
643
|
+
const demote = (node) => effect.Match.value(node).pipe(effect.Match.when({ type: "text" }, (text) => [text]), effect.Match.when({ type: "decoration" }, (decoration) => keep(decoration) ? [{
|
|
644
|
+
...decoration,
|
|
645
|
+
children: inside(decoration.children)
|
|
646
|
+
}] : unwrap(decoration)), effect.Match.when(keep, (kept) => [kept]), effect.Match.orElse((other) => literal(other.position.start.offset, other.position.end.offset)));
|
|
647
|
+
return inside(nodes);
|
|
648
|
+
};
|
|
649
|
+
//#endregion
|
|
630
650
|
//#region src/parse.ts
|
|
631
651
|
/**
|
|
632
652
|
* parse.ts — ページ全文の入口。
|
|
@@ -661,10 +681,14 @@ const parse = (source, options) => {
|
|
|
661
681
|
const normalized = normalizeLineEndings(source);
|
|
662
682
|
const lines = toSourceLines(normalized);
|
|
663
683
|
const rules = resolveExtensions(options?.extensions);
|
|
684
|
+
const keepInTableCell = keepInTableCellOf(options?.extensions);
|
|
664
685
|
const last = lines[lines.length - 1];
|
|
665
686
|
return {
|
|
666
687
|
type: "page",
|
|
667
|
-
children: buildBlocks(lines,
|
|
688
|
+
children: buildBlocks(lines, {
|
|
689
|
+
line: (text, origin) => tokenizeInlineWith(text, origin, rules),
|
|
690
|
+
tableCell: (text, origin) => keepNotation(tokenizeInlineWith(text, origin, rules), text, origin, keepInTableCell)
|
|
691
|
+
}),
|
|
668
692
|
position: {
|
|
669
693
|
start: {
|
|
670
694
|
line: 0,
|