@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.
- package/LICENSE +21 -0
- package/README.md +794 -0
- package/dist/ast-CIKXwSl5.mjs +41 -0
- package/dist/ast-CIKXwSl5.mjs.map +1 -0
- package/dist/ast-uYWtkHwT.cjs +52 -0
- package/dist/ast-uYWtkHwT.cjs.map +1 -0
- package/dist/compile.cjs +265 -0
- package/dist/compile.cjs.map +1 -0
- package/dist/compile.d.cts +135 -0
- package/dist/compile.d.cts.map +1 -0
- package/dist/compile.d.mts +135 -0
- package/dist/compile.d.mts.map +1 -0
- package/dist/compile.mjs +256 -0
- package/dist/compile.mjs.map +1 -0
- package/dist/create-compiler-CvlndKOA.d.cts +23 -0
- package/dist/create-compiler-CvlndKOA.d.cts.map +1 -0
- package/dist/create-compiler-jdA408ny.d.mts +23 -0
- package/dist/create-compiler-jdA408ny.d.mts.map +1 -0
- package/dist/image-url-YcTed8tg.cjs +68 -0
- package/dist/image-url-YcTed8tg.cjs.map +1 -0
- package/dist/image-url-eQ6X-oN0.mjs +51 -0
- package/dist/image-url-eQ6X-oN0.mjs.map +1 -0
- package/dist/index.cjs +716 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +77 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +709 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.cjs +0 -0
- package/dist/plugin.d.cts +4 -0
- package/dist/plugin.d.mts +4 -0
- package/dist/plugin.mjs +1 -0
- package/dist/schema.cjs +171 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +34 -0
- package/dist/schema.d.cts.map +1 -0
- package/dist/schema.d.mts +34 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +148 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/types-BXlnUFr0.d.mts +63 -0
- package/dist/types-BXlnUFr0.d.mts.map +1 -0
- package/dist/types-Bo_BrmvK.d.cts +230 -0
- package/dist/types-Bo_BrmvK.d.cts.map +1 -0
- package/dist/types-Bo_BrmvK.d.mts +230 -0
- package/dist/types-Bo_BrmvK.d.mts.map +1 -0
- package/dist/types-DVwlUtla.d.cts +63 -0
- package/dist/types-DVwlUtla.d.cts.map +1 -0
- package/dist/utils.cjs +80 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.cts +46 -0
- package/dist/utils.d.cts.map +1 -0
- package/dist/utils.d.mts +46 -0
- package/dist/utils.d.mts.map +1 -0
- package/dist/utils.mjs +72 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
# @cosense-toolbox/parser
|
|
2
|
+
|
|
3
|
+
Cosense (旧 Scrapbox) 記法のパーサー。
|
|
4
|
+
位置情報つきの unist 風 AST を返す。
|
|
5
|
+
|
|
6
|
+
> **beta**:公開 API はまだ変わりうる。
|
|
7
|
+
> 安定するまでは `^` ではなくバージョンを固定して使うほうが安全。
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @cosense-toolbox/parser@beta
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { parse } from '@cosense-toolbox/parser'
|
|
15
|
+
|
|
16
|
+
const page = parse('タイトル\nこれは [リンク] です')
|
|
17
|
+
|
|
18
|
+
page.children[1].children
|
|
19
|
+
// [ { type: 'text', value: 'これは ', position: {…} },
|
|
20
|
+
// { type: 'internalLink', label: 'リンク', target: 'リンク', position: {…} },
|
|
21
|
+
// { type: 'text', value: ' です', position: {…} } ]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 依存は `effect` だけで、DOM も Node の API も使わない。
|
|
25
|
+
- すべてのノードが位置情報を持つので、エディタのハイライトやキャレット連携に使える。
|
|
26
|
+
- AST は plain object なので、`JSON.stringify` と `JSON.parse` で往復でき、worklet や postMessage を跨げる。
|
|
27
|
+
- 記法と出力の両方をプラグインで拡張できる。
|
|
28
|
+
- サブパスごとに export が分かれていて tree-shaking が効く (`parse` だけなら gzip 約 8 KB)。
|
|
29
|
+
|
|
30
|
+
> このパッケージは Cosense (Scrapbox) の記法を解釈する非公式の実装である。
|
|
31
|
+
> 開発元である Helpfeel社 とは関係がなく、公認も受けていない。
|
|
32
|
+
|
|
33
|
+
## パース
|
|
34
|
+
|
|
35
|
+
### parse
|
|
36
|
+
|
|
37
|
+
ページ全文を受け取り、ブロックの並びを返す。
|
|
38
|
+
どんな入力でも例外を投げず、必ず `Page` を返す。
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { parse } from '@cosense-toolbox/parser'
|
|
42
|
+
|
|
43
|
+
const page = parse('タイトル\nこれは [リンク] です')
|
|
44
|
+
|
|
45
|
+
for (const block of page.children) {
|
|
46
|
+
switch (block.type) {
|
|
47
|
+
case 'title':
|
|
48
|
+
console.log('title:', block.value)
|
|
49
|
+
break
|
|
50
|
+
case 'line':
|
|
51
|
+
console.log('line:', block.children.map((node) => node.type))
|
|
52
|
+
break
|
|
53
|
+
case 'codeBlock':
|
|
54
|
+
console.log(block.filename, block.lines.map((line) => line.value))
|
|
55
|
+
break
|
|
56
|
+
case 'table':
|
|
57
|
+
console.log(block.name, block.rows.length)
|
|
58
|
+
break
|
|
59
|
+
default:
|
|
60
|
+
// ノード型は minor で増えうるので default を置く
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`code:` と `table:` は、ページの文脈があって初めてブロックにまとまる。
|
|
67
|
+
1 行目は無条件でタイトルとして扱う。
|
|
68
|
+
|
|
69
|
+
### parseLine
|
|
70
|
+
|
|
71
|
+
1 行だけを通常行としてパースする。
|
|
72
|
+
エディタのように行単位で扱うときに使う。
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { parseLine } from '@cosense-toolbox/parser'
|
|
76
|
+
|
|
77
|
+
parseLine(' > [リンク]')
|
|
78
|
+
// LineBlock { indent: 2, quote: true, monospace: false, children: [...] }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
ページの文脈がないので、`code:` と `table:` もブロックにはならず通常行になる。
|
|
82
|
+
その行がページの何行目かは `{ line, offset }` で渡せる。位置情報がページ全体と揃う。
|
|
83
|
+
|
|
84
|
+
### tokenizeInline
|
|
85
|
+
|
|
86
|
+
行の中だけを解析して、インラインノードの並びを返す。
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { tokenizeInline } from '@cosense-toolbox/parser'
|
|
90
|
+
|
|
91
|
+
tokenizeInline('[* 太字] と [リンク]') // → readonly InlineNode[]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
改行を含まない 1 行分の文字列を渡す。
|
|
95
|
+
|
|
96
|
+
### createParser
|
|
97
|
+
|
|
98
|
+
拡張を固定したパーサーを作る。
|
|
99
|
+
同じ拡張で何度もパースするときに、毎回 `extensions` を渡さずに済む。
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createParser } from '@cosense-toolbox/parser'
|
|
103
|
+
|
|
104
|
+
const parser = createParser({ extensions: [mentions] })
|
|
105
|
+
parser.parse(source)
|
|
106
|
+
parser.parseLine(line)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### asImageSrc
|
|
110
|
+
|
|
111
|
+
画像 URL を `<img src>` に入れられる形にする。
|
|
112
|
+
画像でなければ `null` を返す。
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { asImageSrc } from '@cosense-toolbox/parser'
|
|
116
|
+
|
|
117
|
+
asImageSrc('https://gyazo.com/503a911fea542532aa5aba0a88eb7b60')
|
|
118
|
+
// → 'https://i.gyazo.com/503a911fea542532aa5aba0a88eb7b60.png'
|
|
119
|
+
asImageSrc('https://example.test/page')
|
|
120
|
+
// → null
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Gyazo のページ URL は画像そのものではないので、ここでだけ画像 URL に差し替わる。
|
|
124
|
+
`parse` はこの変換をしない。AST はソースに書かれた文字列を保つ。
|
|
125
|
+
|
|
126
|
+
### normalizeLineEndings
|
|
127
|
+
|
|
128
|
+
CRLF と CR を LF に揃える。
|
|
129
|
+
`parse` は必ずこれを通してから解析するので、位置情報を自分で計算するときに使う。
|
|
130
|
+
|
|
131
|
+
## AST
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Page
|
|
135
|
+
├─ TitleBlock 1 行目。value に生テキスト、children にインラインノード
|
|
136
|
+
├─ CodeBlock code:filename とその配下 (CodeLine[])
|
|
137
|
+
├─ TableBlock table:name とその配下 (TableRow[] → TableCell[])
|
|
138
|
+
└─ LineBlock 通常の行 (indent / quote / monospace + InlineNode[])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
インラインノードは次の 10 種類で、すべて `type` で判別できる。
|
|
142
|
+
|
|
143
|
+
| type | 記法 |
|
|
144
|
+
| :--- | :--- |
|
|
145
|
+
| `text` | 記法にならなかった素のテキスト |
|
|
146
|
+
| `internalLink` | `[title]` |
|
|
147
|
+
| `externalLink` | 裸の URL、`[url]`、`[url label]`、`[label url]` |
|
|
148
|
+
| `projectLink` | `[/project/title]` (`project` と `title` に分解済み) |
|
|
149
|
+
| `hashtag` | `#tag` |
|
|
150
|
+
| `inlineCode` | `` `code` `` |
|
|
151
|
+
| `image` | `[url.png]`、`[[url.png]]` (large)、`[linkUrl imageUrl]` (link つき) |
|
|
152
|
+
| `icon` | `[user.icon]`、`[user.icon*5]` |
|
|
153
|
+
| `formula` | `[$ x^2]` |
|
|
154
|
+
| `decoration` | `[* 太字]` `[/ 斜体]` `[- 打消し]` `[_ 下線]` とその複合、`[[太字]]` |
|
|
155
|
+
|
|
156
|
+
装飾は Markdown のような入れ子の強調にならない。
|
|
157
|
+
Cosense では 1 つの記法が複数の装飾を同時に持つ (`[-/ x]` は打消しかつ斜体) ので、1 ノードが複数のフラグを持つ形にしている。
|
|
158
|
+
|
|
159
|
+
`image` の `src` はソースに書かれた URL のままで、パーサーは書き換えない。
|
|
160
|
+
Gyazo のページ URL (`https://gyazo.com/{hash}`) のように、そのままでは `<img>` に入れられない URL を表示用に直すのは描画側の仕事になる。
|
|
161
|
+
その変換は [`asImageSrc`](#asimagesrc) が担当する。
|
|
162
|
+
|
|
163
|
+
## 位置情報
|
|
164
|
+
|
|
165
|
+
すべてのノードが `position` を持つ。
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
interface Point { line: number; column: number; offset: number } // すべて 0-based
|
|
169
|
+
interface Position { start: Point; end: Point } // end は exclusive
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`line` はページ内の行番号で、0 がタイトル行になる。
|
|
173
|
+
`column` は行内の文字オフセット、`offset` はページ全文の中の文字オフセットを指す。
|
|
174
|
+
|
|
175
|
+
`source.slice(start.offset, end.offset)` は、その記法の生テキスト全体と一致する。
|
|
176
|
+
`[` や `#` などのマーカーも含む。
|
|
177
|
+
|
|
178
|
+
unist は 1-based だが、JS の `slice` とそのまま噛み合うよう 0-based を採用している。
|
|
179
|
+
CRLF と CR は LF に正規化されてから解析されるので、位置は正規化後の文字列が基準になる。
|
|
180
|
+
|
|
181
|
+
ノードの生テキストを切り出すには [`rawTextOf`](#rawtextof) を使う。
|
|
182
|
+
|
|
183
|
+
## utils
|
|
184
|
+
|
|
185
|
+
`@cosense-toolbox/parser/utils` は AST を走査して中身を取り出す。
|
|
186
|
+
パースはしないので、AST を作る側と使う側を分けて import できる。
|
|
187
|
+
|
|
188
|
+
### visit
|
|
189
|
+
|
|
190
|
+
木を深さ優先で辿る。
|
|
191
|
+
型を渡すとその型のノードだけが visitor に届く。
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { visit } from '@cosense-toolbox/parser/utils'
|
|
195
|
+
|
|
196
|
+
visit(page, 'internalLink', (node) => {
|
|
197
|
+
console.log(node.target)
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
visitor は `'skip'` を返すとそのノードの子を辿らず、`'exit'` を返すと走査全体を打ち切る。
|
|
202
|
+
第 2 引数を省くとすべてのノードが届く。
|
|
203
|
+
visitor の第 2 引数には、ルートからそのノードまでの祖先が配列で渡る。
|
|
204
|
+
|
|
205
|
+
### find
|
|
206
|
+
|
|
207
|
+
その型の最初のノードを返す。
|
|
208
|
+
無ければ `null`。
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { find } from '@cosense-toolbox/parser/utils'
|
|
212
|
+
|
|
213
|
+
find(page, 'codeBlock') // → CodeBlock | null
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### collect
|
|
217
|
+
|
|
218
|
+
その型のノードを出現順にすべて返す。
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { collect } from '@cosense-toolbox/parser/utils'
|
|
222
|
+
|
|
223
|
+
collect(page, 'icon') // → IconNode[]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
戻り値の型はノード型から導出されるので、`collect(page, 'icon')` は `IconNode[]` になる。
|
|
227
|
+
|
|
228
|
+
### collectLinks
|
|
229
|
+
|
|
230
|
+
内部リンクとハッシュタグの指す先を、出現順、重複なしで返す。
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { collectLinks } from '@cosense-toolbox/parser/utils'
|
|
234
|
+
|
|
235
|
+
collectLinks(parse('t\n[A] と #B と [A]')) // → ['A', 'B']
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### firstImage
|
|
239
|
+
|
|
240
|
+
最初の画像ノードを返す。
|
|
241
|
+
無ければ `null`。ページのサムネイルを選ぶときに使う。
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { firstImage } from '@cosense-toolbox/parser/utils'
|
|
245
|
+
|
|
246
|
+
firstImage(page)?.src
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### rawTextOf
|
|
250
|
+
|
|
251
|
+
そのノードがソースで占めていた生テキストを返す。
|
|
252
|
+
位置情報から切り出すので、記法のマーカーも含む。
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { rawTextOf } from '@cosense-toolbox/parser/utils'
|
|
256
|
+
|
|
257
|
+
const source = 'title\nこれは [リンク] です'
|
|
258
|
+
const link = parse(source).children[1].children[1]
|
|
259
|
+
rawTextOf(source, link) // → '[リンク]'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## compile
|
|
263
|
+
|
|
264
|
+
`@cosense-toolbox/parser/compile` は AST を別の形式に変える。
|
|
265
|
+
この層はパーサー本体を import しないので、変換だけを使う側のバンドルにパーサーは入らない。
|
|
266
|
+
|
|
267
|
+
### toHtml
|
|
268
|
+
|
|
269
|
+
AST を HTML 文字列にする。
|
|
270
|
+
引数はページ全体でなくてもよく、`parseLine` が返した 1 行でも AST の任意のノードでも受け取る。
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { parse } from '@cosense-toolbox/parser'
|
|
274
|
+
import { toHtml } from '@cosense-toolbox/parser/compile'
|
|
275
|
+
|
|
276
|
+
toHtml(parse('タイトル\nこれは [リンク] です'))
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```html
|
|
280
|
+
<div class="page">
|
|
281
|
+
<h1 class="title">タイトル</h1>
|
|
282
|
+
<div class="line">
|
|
283
|
+
これは <a class="link" href="/%E3%83%AA%E3%83%B3%E3%82%AF">リンク</a> です
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
以降の HTML は読みやすさのために字下げして示す。
|
|
289
|
+
実際の出力に要素間の空白は入らない。
|
|
290
|
+
|
|
291
|
+
既定の見た目は [`@cosense-toolbox/style`](https://github.com/qaynam/cosense-toolbox/blob/main/packages/style/README.md) が別パッケージとして持っている。
|
|
292
|
+
|
|
293
|
+
#### 出力の形
|
|
294
|
+
|
|
295
|
+
class 名は接頭辞を持たず、すべて [`classNames`](#classnames) で差し替えられる。
|
|
296
|
+
|
|
297
|
+
ブロックの対応は次のとおり。
|
|
298
|
+
|
|
299
|
+
| 記法 | HTML |
|
|
300
|
+
| :--- | :--- |
|
|
301
|
+
| 1 行目 | `<h1 class="title">タイトル</h1>` |
|
|
302
|
+
| 通常の行 | `<div class="line">本文</div>` |
|
|
303
|
+
| 字下げした行 | `<div class="line" data-indent="2">本文</div>` |
|
|
304
|
+
| 空行 | `<div class="line"><br></div>` |
|
|
305
|
+
| `> 引用` | `<blockquote class="quote">引用</blockquote>` |
|
|
306
|
+
| `$ ls` | `<code class="monospace">$ ls</code>` |
|
|
307
|
+
|
|
308
|
+
ページ全体は `<div class="page">` で包まれる。
|
|
309
|
+
インデントの深さは class ではなく `data-indent` 属性で表す。
|
|
310
|
+
中点は要素を持たず、CSS の擬似要素が描く。
|
|
311
|
+
本家と同じ `.indent-mark` と `.pad` と `.dot` の要素が要る場合は [`showPads`](#showpads) を渡す。
|
|
312
|
+
|
|
313
|
+
テーブルは `<table>` になる。
|
|
314
|
+
|
|
315
|
+
```html
|
|
316
|
+
<table class="table">
|
|
317
|
+
<caption>テーブル名</caption>
|
|
318
|
+
<tbody>
|
|
319
|
+
<tr>
|
|
320
|
+
<td>abc</td>
|
|
321
|
+
<td>def</td>
|
|
322
|
+
</tr>
|
|
323
|
+
</tbody>
|
|
324
|
+
</table>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
コードブロックは本家と同じく 1 行を 1 要素に切る。
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<div class="line code-block">
|
|
331
|
+
<code class="code-start">
|
|
332
|
+
<span class="code-block-start">a.js</span>
|
|
333
|
+
</code>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="line code-block" data-indent="1">
|
|
336
|
+
<code class="code-body">const a = 1</code>
|
|
337
|
+
</div>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
本体行はヘッダより 1 段深い `data-indent` を持つ。
|
|
341
|
+
それより深い字下げは中身の文字列に残る。
|
|
342
|
+
|
|
343
|
+
インラインの対応は次のとおり。
|
|
344
|
+
|
|
345
|
+
| 記法 | HTML |
|
|
346
|
+
| :--- | :--- |
|
|
347
|
+
| `[title]` | `<a class="link" href="/title">title</a>` |
|
|
348
|
+
| `https://a.test/x` | `<a class="link link-external" href="https://a.test/x">https://a.test/x</a>` |
|
|
349
|
+
| `[https://a.test/x label]` | `<a class="link link-external" href="https://a.test/x">label</a>` |
|
|
350
|
+
| `[/proj/page]` | `<a class="link link-project" href="/proj/page">/proj/page</a>` |
|
|
351
|
+
| `#tag` | `<a class="hashtag" href="/tag">#tag</a>` |
|
|
352
|
+
| `` `code` `` | `<code class="code">code</code>` |
|
|
353
|
+
| `[a.png]` | `<img class="image" src="a.png" alt="">` |
|
|
354
|
+
| `[[a.png]]` | `<img class="image" src="a.png" alt="" data-large="true">` |
|
|
355
|
+
| `[user.icon]` | `<a class="link icon" href="/user">user</a>` |
|
|
356
|
+
| `[$ x^2]` | `<span class="formula">x^2</span>` |
|
|
357
|
+
|
|
358
|
+
装飾はフラグの集合を入れ子の要素に開く。
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<!-- [* 太字] -->
|
|
362
|
+
<span class="decoration">
|
|
363
|
+
<strong>太字</strong>
|
|
364
|
+
</span>
|
|
365
|
+
|
|
366
|
+
<!-- [-/ x] は打消しかつ斜体 -->
|
|
367
|
+
<span class="decoration">
|
|
368
|
+
<em>
|
|
369
|
+
<s>x</s>
|
|
370
|
+
</em>
|
|
371
|
+
</span>
|
|
372
|
+
|
|
373
|
+
<!-- [*** 見出し] -->
|
|
374
|
+
<span class="decoration" data-size-level="2">
|
|
375
|
+
<strong>見出し</strong>
|
|
376
|
+
</span>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
見出しの段階は `data-size-level` 属性で表す。
|
|
380
|
+
|
|
381
|
+
数式は組版せず、記法を外した中身をそのまま置く。
|
|
382
|
+
KaTeX に渡したい場合は [`handlers`](#handlers) で差し替える。
|
|
383
|
+
|
|
384
|
+
アイコンは既定では画像を出さない。
|
|
385
|
+
[`iconImageUrl`](#iconimageurl) を渡すと `<img>` が入る。
|
|
386
|
+
|
|
387
|
+
#### オプション
|
|
388
|
+
|
|
389
|
+
| オプション | 型 | 既定 |
|
|
390
|
+
| :--- | :--- | :--- |
|
|
391
|
+
| [`pageUrl`](#pageurl) | `(title, node) => string` | `/{title}` |
|
|
392
|
+
| [`iconImageUrl`](#iconimageurl) | `(node) => string \| null` | 常に `null` |
|
|
393
|
+
| [`highlight`](#highlight) | `(code, language) => string` | 色付けしない |
|
|
394
|
+
| [`classNames`](#classnames) | `HtmlClassNames` | `defaultClassNames` |
|
|
395
|
+
| [`showPads`](#showpads) | `boolean` | `false` |
|
|
396
|
+
| [`handlers`](#handlers) | `NodeHandlers<string>` | 既定のハンドラ |
|
|
397
|
+
| [`style`](#style) | `string` | `<style>` を出さない |
|
|
398
|
+
|
|
399
|
+
上の 3 つは、記法に書かれていないので AST から導けない情報を外から渡す。
|
|
400
|
+
ページをどの URL で配信しているか、アイコン画像がどこにあるか、コードの構文がどう色分けされるかは、どれもソースには存在しない。
|
|
401
|
+
残りは出力の見た目と構造を調整する。
|
|
402
|
+
|
|
403
|
+
#### pageUrl
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
pageUrl?: (title: string, node: PageRefNode) => string
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
ページを指す記法の遷移先を決める。
|
|
410
|
+
対象は `[title]` と `[/proj/page]` と `#tag` と `[user.icon]` の 4 つ。
|
|
411
|
+
アイコンについてはリンク先だけを決め、画像は `iconImageUrl` が決める。
|
|
412
|
+
|
|
413
|
+
`title` は記法に書かれたタイトルそのままで、`[title]` なら `title`、`[/proj/page]` なら `/proj/page`、`#tag` なら `tag`、`[user.icon]` なら `user` が渡る。
|
|
414
|
+
ノード型で分岐する必要はない。
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
toHtml(page, { pageUrl: (title) => `/wiki/${encodeURIComponent(title)}` })
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
既定は `/{title}` で、区切りを含むタイトルは区切りを残して各段を encode する。
|
|
421
|
+
`defaultPageUrl` として export しているので、独自のハンドラからも呼べる。
|
|
422
|
+
|
|
423
|
+
外部リンクには効かない。
|
|
424
|
+
外部リンクは記法そのものが URL なので、解決するものがない。
|
|
425
|
+
|
|
426
|
+
#### iconImageUrl
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
iconImageUrl?: (node: IconNode) => string | null
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`[user.icon]` の画像 URL を決める。
|
|
433
|
+
`null` を返すと `<img>` を出さず、ユーザー名のテキストリンクになる。
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
toHtml(page, {
|
|
437
|
+
iconImageUrl: (node) => `/api/pages/help-jp/${encodeURIComponent(node.user)}/icon`,
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```html
|
|
442
|
+
<a class="link icon" href="/rakusai">
|
|
443
|
+
<img class="icon" src="/api/pages/help-jp/rakusai/icon" alt="rakusai" title="rakusai">
|
|
444
|
+
</a>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
既定が `null` なのは、本家の画像 URL がプロジェクト名を含む (`/api/pages/{project}/{user}/icon`) 一方で、記法にプロジェクト名が書かれていないためである。
|
|
448
|
+
|
|
449
|
+
`[/icons/name.icon]` のように別プロジェクトを指す場合、`node.user` には `/icons/name` が入る。
|
|
450
|
+
|
|
451
|
+
> **注意**:
|
|
452
|
+
> `https://scrapbox.io/api/pages/{project}/{user}/icon` は `Cross-Origin-Resource-Policy: same-origin` を返す。
|
|
453
|
+
> scrapbox.io 以外のページの `<img>` からは読めず、`Access-Control-Allow-Origin` も無いので `crossorigin` 属性でも回避できない。
|
|
454
|
+
> 別オリジンで表示するなら、自前のサーバーやワーカーで中継してそちらに向ける。
|
|
455
|
+
|
|
456
|
+
#### highlight
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
highlight?: (code: string, language: string) => string
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
コードブロックの中身を色付けする。
|
|
463
|
+
シグネチャは markdown-it の同名オプションと同じなので、たいていのハイライタがそのまま嵌る。
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
import hljs from 'highlight.js'
|
|
467
|
+
|
|
468
|
+
toHtml(page, {
|
|
469
|
+
highlight: (code, language) =>
|
|
470
|
+
hljs.highlight(code, { language: hljs.getLanguage(language) ? language : 'plaintext' }).value,
|
|
471
|
+
})
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`language` はファイル名から推測した名前で、拡張子があればそれ、無ければファイル名全体が渡る。
|
|
475
|
+
`code:hello.js` なら `js`、`code:python` なら `python` になる。
|
|
476
|
+
言語名の綴りはライブラリごとに違うので、必要なら受け取った側で読み替える。
|
|
477
|
+
|
|
478
|
+
戻り値は HTML としてそのまま埋め込まれるので、エスケープはハイライタの責任になる。
|
|
479
|
+
|
|
480
|
+
これを渡すと、コードブロックの本体は 1 行 1 要素ではなく 1 つの要素にまとまる。
|
|
481
|
+
ハイライタの出力が複数行にまたがるタグを含みうるためで、行で切るとタグが壊れる。
|
|
482
|
+
ヘッダ行とファイル名は変わらない。
|
|
483
|
+
|
|
484
|
+
```html
|
|
485
|
+
<div class="line code-block">
|
|
486
|
+
<code class="code-start">
|
|
487
|
+
<span class="code-block-start">a.js</span>
|
|
488
|
+
</code>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="line code-block" data-indent="1">
|
|
491
|
+
<code class="code-body highlight">…ハイライタの出力…</code>
|
|
492
|
+
</div>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
ライブラリごとの書きかたは次のとおり。
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
// Prism
|
|
499
|
+
highlight: (code, lang) => Prism.highlight(code, Prism.languages[lang] ?? Prism.languages.plain, lang)
|
|
500
|
+
|
|
501
|
+
// sugar-high
|
|
502
|
+
highlight: (code) => sugarHigh(code)
|
|
503
|
+
|
|
504
|
+
// Shiki は既定で <pre><code> ごと返すので、structure: 'inline' で中身だけにする
|
|
505
|
+
const shiki = await createHighlighter({ themes: ['github-light'], langs: ['js'] })
|
|
506
|
+
highlight: (code, lang) => shiki.codeToHtml(code, { lang, theme: 'github-light', structure: 'inline' })
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
ハイライタのテーマ CSS が特定の class を要求する場合は `classNames` で足せる。
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
toHtml(page, { highlight, classNames: { codeHighlight: 'highlight hljs' } })
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### classNames
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
classNames?: HtmlClassNames
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
出力する要素に付ける class 名を差し替える。
|
|
522
|
+
指定したキーだけが既定を上書きする。
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
toHtml(page, { classNames: { line: 'my-2 leading-7', internalLink: 'text-sky-600 underline' } })
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
値は置き換えであって追加ではない。
|
|
529
|
+
既定の名前を残したまま足すなら `'line my-2'` のように自分で並べる。
|
|
530
|
+
空文字を渡すと class 属性そのものを出さない。
|
|
531
|
+
|
|
532
|
+
既定の一覧は `defaultClassNames` として export している。
|
|
533
|
+
キーはノード型のほか、要素を持つ細部にも用意してある。
|
|
534
|
+
|
|
535
|
+
| キー | 対象 |
|
|
536
|
+
| :--- | :--- |
|
|
537
|
+
| `page` `title` `line` | ページ全体、1 行目、通常の行 |
|
|
538
|
+
| `quote` `monospace` | 引用行の `<blockquote>`、等幅行の `<code>` |
|
|
539
|
+
| `codeBlock` | コードブロックに属する行 (ヘッダと本体の両方) |
|
|
540
|
+
| `codeStart` `codeFilename` `codeBody` `codeHighlight` | ヘッダの `<code>`、ファイル名、本体の `<code>`、色付けした本体に足す class |
|
|
541
|
+
| `indentMark` `pad` `dot` | `showPads` のときだけ出る要素 |
|
|
542
|
+
| `internalLink` `externalLink` `projectLink` `hashtag` | 各リンク |
|
|
543
|
+
| `inlineCode` `image` `icon` `formula` `decoration` `table` | 各インライン記法とテーブル |
|
|
544
|
+
|
|
545
|
+
#### showPads
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
showPads?: boolean
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
インデントを本家と同じ要素として書き出す。
|
|
552
|
+
深さ 1 段につき `pad` が 1 つ並び、その右端に中点が付く。
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<div class="line" data-indent="2">
|
|
556
|
+
<span class="indent-mark">
|
|
557
|
+
<span class="pad"> </span>
|
|
558
|
+
<span class="pad"> </span>
|
|
559
|
+
<span class="dot"></span>
|
|
560
|
+
</span>
|
|
561
|
+
字下げ
|
|
562
|
+
</div>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
既定では要素を出さず、深さは `data-indent` 属性だけで表す。
|
|
566
|
+
中点は CSS の擬似要素で描けるので、見た目はどちらでも変わらない。
|
|
567
|
+
|
|
568
|
+
#### handlers
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
handlers?: NodeHandlers<string>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
ノード型ごとの出力そのものを差し替える。
|
|
575
|
+
既定のハンドラに自動で重ねられるので、変えたい型だけ書けばよい。
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
toHtml(page, {
|
|
579
|
+
handlers: { formula: (node) => katex.renderToString(node.value) },
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
ハンドラは `(node, ctx)` を受け取る。
|
|
584
|
+
`ctx.children(node)` で子ノードの変換結果が配列で得られる。
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
handlers: {
|
|
588
|
+
line: (node, ctx) => `<p>${ctx.children(node).join('')}</p>`,
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
既定のハンドラ一式は `createHtmlHandlers(options)` で取れる。
|
|
593
|
+
既定の出力を包みたいときに使う。
|
|
594
|
+
|
|
595
|
+
拡張が足した独自のノード型も、`InlineNodeMap` を declaration merging で拡張してあればここのキーになる。
|
|
596
|
+
ハンドラを書かなかった独自ノードは、子があればその中身が出力される。
|
|
597
|
+
|
|
598
|
+
#### style
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
style?: string
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
渡した CSS を `<style>` 要素として出力の先頭に差し込む。
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
import css from '@cosense-toolbox/style/style.css?raw'
|
|
608
|
+
|
|
609
|
+
toHtml(page, { style: css })
|
|
610
|
+
// <style>…</style><div class="page">…</div>
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
iframe の `srcdoc` のように、1 つの文字列で完結させたいときに使う。
|
|
614
|
+
CSS そのものはこのパッケージに含まれていない。
|
|
615
|
+
|
|
616
|
+
#### 独自のハンドラを書く
|
|
617
|
+
|
|
618
|
+
`handlers` で要素ごと差し替えると、既定のハンドラがやっているエスケープと URL の検査が無くなる。
|
|
619
|
+
同じことをするための部品を export している。
|
|
620
|
+
|
|
621
|
+
| export | 役割 |
|
|
622
|
+
| :--- | :--- |
|
|
623
|
+
| `escapeHtml(value)` | `& < > " '` を実体参照にする |
|
|
624
|
+
| `safeHref(url)` | href に入れて安全な URL だけを返す。script が動くスキームなら `null` |
|
|
625
|
+
| `safeSrc(url)` | src 版。`data:` 画像は許す |
|
|
626
|
+
| `defaultPageUrl(title)` | `pageUrl` の既定の実装 |
|
|
627
|
+
| `defaultClassNames` | 既定の class 名 |
|
|
628
|
+
|
|
629
|
+
外部リンクを別タブで開く例を示す。
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
import { escapeHtml, safeHref, toHtml } from '@cosense-toolbox/parser/compile'
|
|
633
|
+
|
|
634
|
+
toHtml(page, {
|
|
635
|
+
handlers: {
|
|
636
|
+
externalLink: (node) => {
|
|
637
|
+
const href = safeHref(node.target)
|
|
638
|
+
return `<a class="link link-external" href="${escapeHtml(href ?? '')}" target="_blank" rel="noreferrer">${escapeHtml(node.label)}</a>`
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
})
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
ハンドラは `classNames` の設定を受け取らない。
|
|
645
|
+
両方を使う場合、class 名は書いた側で決めることになる。
|
|
646
|
+
|
|
647
|
+
#### エスケープと URL の検査
|
|
648
|
+
|
|
649
|
+
既定のハンドラは、テキストと属性値をすべてエスケープする。
|
|
650
|
+
`javascript:` と `vbscript:` のスキームは href と src の両方から落とし、`data:` は href からだけ落とす。
|
|
651
|
+
`data:` 画像には正当な使い道があるためである。
|
|
652
|
+
|
|
653
|
+
スキームの判定では、先に空白と制御文字を落とす。
|
|
654
|
+
ブラウザは途中にタブや改行が挟まった `javascript:` もスキームとして解釈するので、それを潰す。
|
|
655
|
+
|
|
656
|
+
`handlers` と `highlight` が返した文字列はそのまま埋め込む。
|
|
657
|
+
そこでのエスケープは書いた人の責任になる。
|
|
658
|
+
|
|
659
|
+
### toPlainText
|
|
660
|
+
|
|
661
|
+
記法を外したテキストを返す。
|
|
662
|
+
オプションは無い。
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
import { toPlainText } from '@cosense-toolbox/parser/compile'
|
|
666
|
+
|
|
667
|
+
toPlainText(parse('タイトル\n[* 太字] と [リンク]'))
|
|
668
|
+
// 'タイトル\n太字 と リンク'
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
インデントは半角 2 文字、引用は `> ` として残る。
|
|
672
|
+
コードブロックとテーブルは中身がそのまま出る。
|
|
673
|
+
|
|
674
|
+
### createCompiler
|
|
675
|
+
|
|
676
|
+
HTML 以外を出すときに使う。
|
|
677
|
+
`toHtml` も `toPlainText` もこれで書かれている。
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
import { createCompiler } from '@cosense-toolbox/parser/compile'
|
|
681
|
+
|
|
682
|
+
const toMarkdown = createCompiler<string>({
|
|
683
|
+
handlers: {
|
|
684
|
+
internalLink: (node) => `[[${node.target}]]`,
|
|
685
|
+
decoration: (node, ctx) => `**${ctx.children(node).join('')}**`,
|
|
686
|
+
text: (node) => node.value,
|
|
687
|
+
},
|
|
688
|
+
fallback: (node, ctx) => ctx.children(node).join(''),
|
|
689
|
+
})
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
`handlers` の型はノード型のマップから導出されるので、ノード型が増えても型が追随する。
|
|
693
|
+
`fallback` はハンドラの無いノード型に使われる。
|
|
694
|
+
|
|
695
|
+
## schema
|
|
696
|
+
|
|
697
|
+
`@cosense-toolbox/parser/schema` は、worklet や postMessage を跨いで受け取った、本当に `Page` か分からない値を検証する。
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
import { decodePage } from '@cosense-toolbox/parser/schema'
|
|
701
|
+
import { Either } from 'effect'
|
|
702
|
+
|
|
703
|
+
const decoded = decodePage(JSON.parse(input))
|
|
704
|
+
if (Either.isRight(decoded)) {
|
|
705
|
+
// decoded.right は Page
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
パース自体は失敗しないので、このサブパスが要るのは外から来た値を扱うときだけになる。
|
|
710
|
+
|
|
711
|
+
## plugin
|
|
712
|
+
|
|
713
|
+
`@cosense-toolbox/parser/plugin` は、記法を足す `InlineConstruct` と `BracketRule` と `Extension`、出力を足す `NodeHandlers` の型を公開する。
|
|
714
|
+
このサブパスは型だけを持ち、実行時のコードを含まない。
|
|
715
|
+
|
|
716
|
+
### 記法を拡張する
|
|
717
|
+
|
|
718
|
+
`Extension` を作って `parse` か `tokenizeInline` に渡す。
|
|
719
|
+
既定のルールより先に試されるので、既存の記法を上書きすることもできる。
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import { Option } from 'effect'
|
|
723
|
+
import { parse } from '@cosense-toolbox/parser'
|
|
724
|
+
import type { Extension, InlineConstruct } from '@cosense-toolbox/parser/plugin'
|
|
725
|
+
|
|
726
|
+
const mention: InlineConstruct = (source, index) => {
|
|
727
|
+
if (source[index] !== '@') return Option.none()
|
|
728
|
+
const match = source.slice(index + 1).match(/^[A-Za-z0-9_-]+/)
|
|
729
|
+
if (!match) return Option.none()
|
|
730
|
+
return Option.some({
|
|
731
|
+
node: { type: 'internalLink', label: `@${match[0]}`, target: match[0] },
|
|
732
|
+
length: match[0].length + 1,
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const mentions: Extension = { constructs: [mention] }
|
|
737
|
+
parse(source, { extensions: [mentions] })
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
`[...]` の中身の解釈を足すなら `bracketRules` を使う。
|
|
741
|
+
同じ拡張で何度もパースするなら、`createParser({ extensions })` でパーサーを固定できる。
|
|
742
|
+
|
|
743
|
+
### 独自のノード型を足す
|
|
744
|
+
|
|
745
|
+
既存のノード型に寄せず新しい `type` を作る場合は、`InlineNodeMap` を declaration merging で拡張する。
|
|
746
|
+
mdast と同じ手法で、`NodeHandlers` のキーにも `visit` の型引数にも自動で現れるので、描画まで型が通る。
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
declare module '@cosense-toolbox/parser' {
|
|
750
|
+
interface InlineNodeMap {
|
|
751
|
+
mention: { type: 'mention'; user: string; position: Position }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 記法を足す
|
|
756
|
+
const mention: InlineConstruct = (source, index) => { /* → { type: 'mention', user } */ }
|
|
757
|
+
|
|
758
|
+
// 描画を足す
|
|
759
|
+
toHtml(page, {
|
|
760
|
+
handlers: { mention: (node) => `<a href="/u/${node.user}">@${escapeHtml(node.user)}</a>` },
|
|
761
|
+
})
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
ハンドラを書かなかった独自ノードは、子があればその中身が出力される。
|
|
765
|
+
|
|
766
|
+
## 設計
|
|
767
|
+
|
|
768
|
+
パースは総関数で、どんな入力でも例外を投げず必ず `Page` を返す。
|
|
769
|
+
記法として成立しない部分は素のテキストになるだけで、エラーにはならない。
|
|
770
|
+
|
|
771
|
+
I/O をしない。
|
|
772
|
+
oEmbed の取得や動画判定のような URL の意味解決は、このパッケージの外の仕事になる。
|
|
773
|
+
画像かどうかの判定だけは、記法の構造そのものを決めるので含んでいる。
|
|
774
|
+
|
|
775
|
+
構造は micromark の「位置つきノードと記法ハンドラの登録制」と、markdown-it の「優先順位付きルールを先頭から試す」形を参考にしている。
|
|
776
|
+
|
|
777
|
+
開発時の規約は [CLAUDE.md](./CLAUDE.md) にある。
|
|
778
|
+
|
|
779
|
+
## 互換性の方針
|
|
780
|
+
|
|
781
|
+
| 変更 | バージョン |
|
|
782
|
+
| :--- | :--- |
|
|
783
|
+
| 新しいノード `type` の追加 | minor |
|
|
784
|
+
| 既存ノードへの optional フィールド追加 | minor |
|
|
785
|
+
| オプションへの optional フィールド追加 | minor |
|
|
786
|
+
| 既存ノードのフィールドの削除、型変更、必須化 | major |
|
|
787
|
+
| `position` の意味論の変更 | major |
|
|
788
|
+
| ノード `type` 文字列のリネーム | major |
|
|
789
|
+
|
|
790
|
+
ノード型は minor で増えうるので、`switch (node.type)` には `default` を置いておくとよい。
|
|
791
|
+
|
|
792
|
+
## ライセンス
|
|
793
|
+
|
|
794
|
+
MIT
|