rehype-ruby-annotator 1.0.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 +9 -0
- package/README.md +250 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/normalize.d.ts +4 -0
- package/dist/normalize.js +16 -0
- package/dist/transform.d.ts +5 -0
- package/dist/transform.js +101 -0
- package/dist/trie.d.ts +15 -0
- package/dist/trie.js +95 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +2 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yukidoke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# rehype-ruby-annotator
|
|
2
|
+
|
|
3
|
+
`rehype-ruby-annotator` は、HTML AST 内のテキストに `<ruby>` 要素を追加する rehype プラグインです。
|
|
4
|
+
本文中の日本語に、あらかじめ定義したふりがなを自動で付与する用途を想定しています。
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<p>私は日本語を読む。</p>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<p>私は<ruby>日本語<rp>(</rp><rt>にほんご</rt><rp>)</rp></ruby>を読む。</p>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 特徴
|
|
15
|
+
|
|
16
|
+
- 登録した語句に一致したテキストへ `<ruby>` / `<rt>` / `<rp>` を挿入します。
|
|
17
|
+
- 重複する候補がある場合は、左から最長一致で処理します。
|
|
18
|
+
- `code`、`pre`、見出し、既存の `ruby` など、注釈を付けたくない要素を既定でスキップします。
|
|
19
|
+
- ひとつの語句の一部だけにルビを付ける指定もできます。
|
|
20
|
+
- `Intl.Segmenter` を使い、絵文字などの grapheme cluster も壊さず扱います。
|
|
21
|
+
|
|
22
|
+
## インストール
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add rehype-ruby-annotator
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install rehype-ruby-annotator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
yarn add rehype-ruby-annotator
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 動作環境
|
|
37
|
+
|
|
38
|
+
- Node.js 18 以降を推奨します。
|
|
39
|
+
- 実行環境で `Intl.Segmenter` が利用できる必要があります。
|
|
40
|
+
- 古い Node.js やブラウザで使う場合は、事前に `Intl.Segmenter` の対応状況を確認してください。
|
|
41
|
+
|
|
42
|
+
## 基本的な使い方
|
|
43
|
+
|
|
44
|
+
以下は Markdown を HTML に変換する unified pipeline の最小例です。
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import rehypeRuby from "rehype-ruby-annotator";
|
|
48
|
+
import rehypeStringify from "rehype-stringify";
|
|
49
|
+
import remarkParse from "remark-parse";
|
|
50
|
+
import remarkRehype from "remark-rehype";
|
|
51
|
+
import { unified } from "unified";
|
|
52
|
+
|
|
53
|
+
const options = {
|
|
54
|
+
entries: [
|
|
55
|
+
{
|
|
56
|
+
segments: [
|
|
57
|
+
{ base: "日本語", reading: "にほんご" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
segments: [
|
|
62
|
+
{ base: "東京駅", reading: "とうきょうえき" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const file = await unified()
|
|
69
|
+
.use(remarkParse)
|
|
70
|
+
.use(remarkRehype)
|
|
71
|
+
.use(rehypeRuby, options)
|
|
72
|
+
.use(rehypeStringify)
|
|
73
|
+
.process("私は日本語を読む。");
|
|
74
|
+
|
|
75
|
+
console.log(String(file));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`segments` に指定した `base` が本文に見つかると、対応する `reading` が `<rt>` として挿入されます。
|
|
79
|
+
|
|
80
|
+
## Astro で使う例
|
|
81
|
+
|
|
82
|
+
`astro.config.mjs` の Markdown 設定に追加します。
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { defineConfig } from "astro/config";
|
|
86
|
+
import rehypeRuby from "rehype-ruby-annotator";
|
|
87
|
+
|
|
88
|
+
export default defineConfig({
|
|
89
|
+
markdown: {
|
|
90
|
+
rehypePlugins: [
|
|
91
|
+
[
|
|
92
|
+
rehypeRuby,
|
|
93
|
+
{
|
|
94
|
+
entries: [
|
|
95
|
+
{
|
|
96
|
+
segments: [
|
|
97
|
+
{ base: "日本語", reading: "にほんご" },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
segments: [
|
|
102
|
+
{ base: "東京駅", reading: "とうきょうえき" },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## エントリ形式
|
|
114
|
+
|
|
115
|
+
### 語句全体にルビを付ける
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
{
|
|
119
|
+
segments: [
|
|
120
|
+
{ base: "銀河鉄道", reading: "ぎんがてつどう" },
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
本文の `銀河鉄道` が、次のような要素に変換されます。
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<ruby>銀河鉄道<rp>(</rp><rt>ぎんがてつどう</rt><rp>)</rp></ruby>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 語句の一部だけにルビを付ける
|
|
132
|
+
|
|
133
|
+
`segments` には文字列も混ぜられます。文字列部分は一致には含まれますが、ルビは付きません。
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
{
|
|
137
|
+
segments: [
|
|
138
|
+
"銀",
|
|
139
|
+
{ base: "河", reading: "が" },
|
|
140
|
+
"鉄道",
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
この場合、本文の `銀河鉄道` に一致し、`河` の部分だけが `<ruby>` になります。
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
銀<ruby>河<rp>(</rp><rt>が</rt><rp>)</rp></ruby>鉄道
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `match` で一致文字列を明示する
|
|
152
|
+
|
|
153
|
+
通常、照合に使う文字列は `segments` から自動で組み立てられます。
|
|
154
|
+
意図しない指定ミスを検出したい場合は `match` を書けます。
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
{
|
|
158
|
+
match: "日本語",
|
|
159
|
+
segments: [
|
|
160
|
+
{ base: "日本語", reading: "にほんご" },
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`match` と `segments` から推測される文字列が一致しない場合はエラーになります。
|
|
166
|
+
|
|
167
|
+
## オプション
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import type { RehypeRubyOptions } from "rehype-ruby-annotator";
|
|
171
|
+
|
|
172
|
+
const options: RehypeRubyOptions = {
|
|
173
|
+
entries: [],
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| オプション | 型 | 既定値 | 説明 |
|
|
178
|
+
| --- | --- | --- | --- |
|
|
179
|
+
| `entries` | `RubyEntryInput[]` | なし | ルビを付ける語句の一覧です。必須です。 |
|
|
180
|
+
| `additionalSkipTags` | `string[]` | `[]` | 既定のスキップ対象に追加するタグ名です。 |
|
|
181
|
+
| `skipTags` | `string[]` | `DEFAULT_SKIP_TAGS` | スキップ対象のタグ一覧を置き換えます。指定すると既定値は使われません。 |
|
|
182
|
+
| `verbose` | `boolean` | `false` | `true` のとき、初期化と変換結果のログを出します。 |
|
|
183
|
+
| `logger` | `RubyLogger \| false` | `console` | `verbose: true` のログ出力先です。`false` なら出力しません。 |
|
|
184
|
+
|
|
185
|
+
## 既定でスキップされるタグ
|
|
186
|
+
|
|
187
|
+
次のタグの中にあるテキストには、既定ではルビを付けません。
|
|
188
|
+
|
|
189
|
+
```txt
|
|
190
|
+
a, h1, h2, h3, h4, h5, h6,
|
|
191
|
+
code, pre, kbd, samp,
|
|
192
|
+
script, style, textarea,
|
|
193
|
+
ruby, rt, rp,
|
|
194
|
+
math, svg
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
既定のリストに追加する場合は `additionalSkipTags` を使います。
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
{
|
|
201
|
+
entries,
|
|
202
|
+
additionalSkipTags: ["span"],
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
既定のリストを使わず、完全に置き換えたい場合は `skipTags` を使います。
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
{
|
|
210
|
+
entries,
|
|
211
|
+
skipTags: ["span"],
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## 一致ルール
|
|
216
|
+
|
|
217
|
+
- テキストノードごとに処理します。HTML 要素をまたいだ語句には一致しません。
|
|
218
|
+
- 同じ位置で複数の候補が一致する場合は、より長い語句を優先します。
|
|
219
|
+
- 同じ `match` を持つエントリを複数登録するとエラーになります。
|
|
220
|
+
- 空文字列に一致するエントリは登録できません。
|
|
221
|
+
|
|
222
|
+
## 補助 API
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import {
|
|
226
|
+
DEFAULT_SKIP_TAGS,
|
|
227
|
+
getEntryText,
|
|
228
|
+
normalizeEntry,
|
|
229
|
+
} from "rehype-ruby-annotator";
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| API | 説明 |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `DEFAULT_SKIP_TAGS` | 既定のスキップ対象タグ一覧です。 |
|
|
235
|
+
| `getEntryText(segments)` | `segments` から照合用テキストを組み立てます。 |
|
|
236
|
+
| `normalizeEntry(entry)` | `RubyEntryInput` を検証し、照合用の形式に正規化します。 |
|
|
237
|
+
|
|
238
|
+
## 開発
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
pnpm install
|
|
242
|
+
pnpm run build
|
|
243
|
+
pnpm test
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`pnpm test` は TypeScript のビルド後に `node --test` を実行します。
|
|
247
|
+
|
|
248
|
+
## ライセンス
|
|
249
|
+
|
|
250
|
+
MIT
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const DEFAULT_SKIP_TAGS = [
|
|
2
|
+
"a",
|
|
3
|
+
"h1",
|
|
4
|
+
"h2",
|
|
5
|
+
"h3",
|
|
6
|
+
"h4",
|
|
7
|
+
"h5",
|
|
8
|
+
"h6",
|
|
9
|
+
"code",
|
|
10
|
+
"pre",
|
|
11
|
+
"kbd",
|
|
12
|
+
"samp",
|
|
13
|
+
"script",
|
|
14
|
+
"style",
|
|
15
|
+
"textarea",
|
|
16
|
+
"ruby",
|
|
17
|
+
"rt",
|
|
18
|
+
"rp",
|
|
19
|
+
"math",
|
|
20
|
+
"svg",
|
|
21
|
+
];
|
|
22
|
+
//# sourceMappingURL=defaults.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default } from "./transform.js";
|
|
2
|
+
export { DEFAULT_SKIP_TAGS } from "./defaults.js";
|
|
3
|
+
export { getEntryText, normalizeEntry, } from "./normalize.js";
|
|
4
|
+
export type { RubySegment, RubyEntrySegment, RubyEntryInput, NormalizedRubyEntry, RehypeRubyOptions, } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NormalizedRubyEntry, RubyEntryInput, RubyEntrySegment } from "./types.js";
|
|
2
|
+
export declare function getEntryText(segments: RubyEntrySegment[]): string;
|
|
3
|
+
export declare function normalizeEntry(entry: RubyEntryInput): NormalizedRubyEntry;
|
|
4
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getEntryText(segments) {
|
|
2
|
+
return segments
|
|
3
|
+
.map((segment) => typeof segment === "string" ? segment : segment.base)
|
|
4
|
+
.join("");
|
|
5
|
+
}
|
|
6
|
+
export function normalizeEntry(entry) {
|
|
7
|
+
const inferredMatch = getEntryText(entry.segments);
|
|
8
|
+
if (entry.match !== undefined && entry.match !== inferredMatch) {
|
|
9
|
+
throw new Error(`Ruby entry match mismatch: match="${entry.match}", segments="${inferredMatch}"`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
match: inferredMatch,
|
|
13
|
+
segments: entry.segments,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { DEFAULT_SKIP_TAGS } from './defaults.js';
|
|
2
|
+
import { Trie } from './trie.js';
|
|
3
|
+
import { normalizeEntry } from './normalize.js';
|
|
4
|
+
import { visitParents } from 'unist-util-visit-parents';
|
|
5
|
+
function rubyNode(segment) {
|
|
6
|
+
return {
|
|
7
|
+
type: "element",
|
|
8
|
+
tagName: "ruby",
|
|
9
|
+
properties: {},
|
|
10
|
+
children: [
|
|
11
|
+
{ type: "text", value: segment.base },
|
|
12
|
+
{
|
|
13
|
+
type: "element",
|
|
14
|
+
tagName: "rp",
|
|
15
|
+
properties: {},
|
|
16
|
+
children: [
|
|
17
|
+
{ type: "text", value: "(" }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: "element",
|
|
22
|
+
tagName: "rt",
|
|
23
|
+
properties: {},
|
|
24
|
+
children: [
|
|
25
|
+
{ type: "text", value: segment.reading }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "element",
|
|
30
|
+
tagName: "rp",
|
|
31
|
+
properties: {},
|
|
32
|
+
children: [
|
|
33
|
+
{ type: "text", value: ")" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function textNode(text) {
|
|
40
|
+
return {
|
|
41
|
+
type: "text",
|
|
42
|
+
value: text,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export default function rehypeRuby(options) {
|
|
46
|
+
const logger = options.verbose === true && options.logger !== false
|
|
47
|
+
? options.logger ?? console
|
|
48
|
+
: undefined;
|
|
49
|
+
logger?.info("[rehype-ruby] Annotator initialize start.");
|
|
50
|
+
const skipTags = new Set((options.skipTags ?? DEFAULT_SKIP_TAGS).map((tag) => tag.toLowerCase()));
|
|
51
|
+
for (const tag of options.additionalSkipTags ?? []) {
|
|
52
|
+
skipTags.add(tag.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
const trie = new Trie();
|
|
55
|
+
for (const entry of options.entries) {
|
|
56
|
+
trie.addData(normalizeEntry(entry));
|
|
57
|
+
}
|
|
58
|
+
logger?.info("[rehype-ruby] Annotator initialized.");
|
|
59
|
+
return (tree, file) => {
|
|
60
|
+
const startTime = performance.now();
|
|
61
|
+
let rubyCount = 0;
|
|
62
|
+
visitParents(tree, "text", (node, ancestors) => {
|
|
63
|
+
if (ancestors.some((n) => {
|
|
64
|
+
return n.type === "element" && skipTags.has(n.tagName);
|
|
65
|
+
})) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const nre = trie.searchAll(node.value);
|
|
69
|
+
if (!nre.segments.some((seg) => typeof seg !== "string")) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const parent = ancestors.at(-1);
|
|
73
|
+
if (!parent) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const index = parent.children.indexOf(node);
|
|
77
|
+
const newNodes = [];
|
|
78
|
+
let textBuffer = "";
|
|
79
|
+
for (const seg of nre.segments) {
|
|
80
|
+
if (typeof seg === "string") {
|
|
81
|
+
textBuffer += seg;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (textBuffer !== "") {
|
|
85
|
+
newNodes.push(textNode(textBuffer));
|
|
86
|
+
textBuffer = "";
|
|
87
|
+
}
|
|
88
|
+
newNodes.push(rubyNode(seg));
|
|
89
|
+
rubyCount++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (textBuffer !== "") {
|
|
93
|
+
newNodes.push(textNode(textBuffer));
|
|
94
|
+
}
|
|
95
|
+
parent.children.splice(index, 1, ...newNodes);
|
|
96
|
+
});
|
|
97
|
+
const durationMs = performance.now() - startTime;
|
|
98
|
+
logger?.debug(`[rehype-ruby] ${file.path || "(unknown)"}: ${rubyCount} words, ${durationMs.toFixed(3)} ms.`);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=transform.js.map
|
package/dist/trie.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NormalizedRubyEntry } from "./types.js";
|
|
2
|
+
interface TrieNode {
|
|
3
|
+
data: NormalizedRubyEntry | null;
|
|
4
|
+
child?: Map<string, TrieNode>;
|
|
5
|
+
}
|
|
6
|
+
export declare class Trie {
|
|
7
|
+
root: TrieNode;
|
|
8
|
+
segmenter: Intl.Segmenter;
|
|
9
|
+
constructor();
|
|
10
|
+
addData(data: NormalizedRubyEntry): void;
|
|
11
|
+
private search;
|
|
12
|
+
searchAll(text: string): NormalizedRubyEntry;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=trie.d.ts.map
|
package/dist/trie.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function appendSegment(segments, segment) {
|
|
2
|
+
if (typeof segment === "string") {
|
|
3
|
+
const last = segments.at(-1);
|
|
4
|
+
if (typeof last === "string") {
|
|
5
|
+
segments[segments.length - 1] = last + segment;
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
segments.push(segment);
|
|
10
|
+
}
|
|
11
|
+
export class Trie {
|
|
12
|
+
root;
|
|
13
|
+
segmenter;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.root = {
|
|
16
|
+
data: null,
|
|
17
|
+
};
|
|
18
|
+
this.segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
|
|
19
|
+
}
|
|
20
|
+
addData(data) {
|
|
21
|
+
if (data.match.length === 0) {
|
|
22
|
+
throw new Error("Empty match is not allowed.");
|
|
23
|
+
}
|
|
24
|
+
let current = this.root;
|
|
25
|
+
const segments = this.segmenter.segment(data.match);
|
|
26
|
+
for (const c of segments) {
|
|
27
|
+
current.child ??= new Map();
|
|
28
|
+
let nextNode = current.child.get(c.segment);
|
|
29
|
+
if (nextNode === undefined) {
|
|
30
|
+
nextNode = {
|
|
31
|
+
data: null,
|
|
32
|
+
};
|
|
33
|
+
current.child.set(c.segment, nextNode);
|
|
34
|
+
}
|
|
35
|
+
current = nextNode;
|
|
36
|
+
}
|
|
37
|
+
if (current.data !== null) {
|
|
38
|
+
throw new Error(`${data.match} already exists.`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
current.data = data;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
search(segments, index) {
|
|
45
|
+
let current = this.root;
|
|
46
|
+
let result = null;
|
|
47
|
+
let matchedEnd = 0;
|
|
48
|
+
for (let i = index; i < segments.length; i++) {
|
|
49
|
+
if (!current.child) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const seg = segments[i];
|
|
53
|
+
if (!seg)
|
|
54
|
+
break;
|
|
55
|
+
const nextNode = current.child.get(seg.segment);
|
|
56
|
+
if (nextNode === undefined) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (nextNode.data !== null) {
|
|
60
|
+
result = nextNode.data;
|
|
61
|
+
matchedEnd = i;
|
|
62
|
+
}
|
|
63
|
+
current = nextNode;
|
|
64
|
+
}
|
|
65
|
+
return result === null ? [null, 0] : [result, matchedEnd + 1 - index];
|
|
66
|
+
}
|
|
67
|
+
searchAll(text) {
|
|
68
|
+
const segments = [...this.segmenter.segment(text)];
|
|
69
|
+
const result = {
|
|
70
|
+
match: "",
|
|
71
|
+
segments: [],
|
|
72
|
+
};
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < segments.length) {
|
|
75
|
+
const [res, len] = this.search(segments, i);
|
|
76
|
+
if (res === null) {
|
|
77
|
+
const seg = segments[i];
|
|
78
|
+
if (!seg)
|
|
79
|
+
break;
|
|
80
|
+
result.match += seg.segment;
|
|
81
|
+
appendSegment(result.segments, seg.segment);
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
result.match += res.match;
|
|
86
|
+
for (const s of res.segments) {
|
|
87
|
+
appendSegment(result.segments, s);
|
|
88
|
+
}
|
|
89
|
+
i += len;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=trie.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RubySegment {
|
|
2
|
+
base: string;
|
|
3
|
+
reading: string;
|
|
4
|
+
}
|
|
5
|
+
export type RubyEntrySegment = RubySegment | string;
|
|
6
|
+
export interface RubyEntryInput {
|
|
7
|
+
/**
|
|
8
|
+
* Optional assertion that must match the text inferred from segments.
|
|
9
|
+
*/
|
|
10
|
+
match?: string;
|
|
11
|
+
segments: RubyEntrySegment[];
|
|
12
|
+
}
|
|
13
|
+
export interface NormalizedRubyEntry {
|
|
14
|
+
match: string;
|
|
15
|
+
segments: RubyEntrySegment[];
|
|
16
|
+
}
|
|
17
|
+
export interface RubyLogger {
|
|
18
|
+
info(message?: unknown, ...optionalParams: unknown[]): void;
|
|
19
|
+
debug(message?: unknown, ...optionalParams: unknown[]): void;
|
|
20
|
+
}
|
|
21
|
+
export interface RehypeRubyOptions {
|
|
22
|
+
entries: RubyEntryInput[];
|
|
23
|
+
additionalSkipTags?: string[];
|
|
24
|
+
skipTags?: string[];
|
|
25
|
+
logger?: RubyLogger | false;
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rehype-ruby-annotator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A rehype plugin that adds HTML ruby annotations using user-provided entries.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"rehype",
|
|
8
|
+
"ruby",
|
|
9
|
+
"furigana",
|
|
10
|
+
"japanese",
|
|
11
|
+
"annotation",
|
|
12
|
+
"unified",
|
|
13
|
+
"hast"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/yukidoke/rehype-ruby-annotator#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/yukidoke/rehype-ruby-annotator/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/yukidoke/rehype-ruby-annotator.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"author": "yukidoke",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**/*.js",
|
|
39
|
+
"dist/**/*.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^6.0.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@types/hast": "^3.0.4",
|
|
46
|
+
"unist-util-visit-parents": "^6.0.2",
|
|
47
|
+
"vfile": "^6.0.3"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc",
|
|
51
|
+
"test": "pnpm run build && node --test test/rehype-ruby-annotator.test.mjs"
|
|
52
|
+
}
|
|
53
|
+
}
|