├── .gitignore
├── img
├── 1.png
├── 10.png
├── 11.png
├── 12.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── examples.html
└── imagify.js
├── index.js
├── package.json
├── LICENSE.md
├── lib
├── ruby.js
└── furigana.js
├── test
└── test.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/img/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/1.png
--------------------------------------------------------------------------------
/img/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/10.png
--------------------------------------------------------------------------------
/img/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/11.png
--------------------------------------------------------------------------------
/img/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/12.png
--------------------------------------------------------------------------------
/img/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/2.png
--------------------------------------------------------------------------------
/img/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/3.png
--------------------------------------------------------------------------------
/img/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/4.png
--------------------------------------------------------------------------------
/img/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/5.png
--------------------------------------------------------------------------------
/img/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/6.png
--------------------------------------------------------------------------------
/img/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/7.png
--------------------------------------------------------------------------------
/img/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/8.png
--------------------------------------------------------------------------------
/img/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iltrof/furigana-markdown-it/HEAD/img/9.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function(options) {
4 | return function(md) {
5 | md.inline.ruler.push("furigana", require("./lib/furigana")(options));
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "furigana-markdown-it",
3 | "version": "1.0.3",
4 | "description": "Furigana extension for markdown-it.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/iltrof/furigana-markdown-it"
12 | },
13 | "keywords": [
14 | "markdown-it",
15 | "markdown",
16 | "furigana"
17 | ],
18 | "author": "iltrof",
19 | "license": "MIT",
20 | "dependencies": {},
21 | "devDependencies": {
22 | "markdown-it": "^10.0.0",
23 | "mocha": "^7.1.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/img/examples.html:
--------------------------------------------------------------------------------
1 | 漢字
2 | 漢字
3 | 取り返す
4 | 可愛い犬
5 | 可愛い犬
6 | 可愛い犬
7 | 食べる
8 | 食べる
9 | アクセラレータ
10 | accelerator
11 | あいうえお
12 | あいうえお
13 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ilya Trofimov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/img/imagify.js:
--------------------------------------------------------------------------------
1 | try {
2 | const root = require("child_process")
3 | .execSync("npm root -g")
4 | .toString()
5 | .trim();
6 | var puppeteer = require(root + "/puppeteer");
7 | } catch (err) {
8 | console.error(
9 | `Install puppeteer globally first with: npm install -g puppeteer`
10 | );
11 | process.exit(1);
12 | }
13 |
14 | const html = require("fs")
15 | .readFileSync("examples.html", { encoding: "utf8" })
16 | .split("\n");
17 |
18 | (async () => {
19 | const browser = await puppeteer.launch();
20 | const page = await browser.newPage();
21 |
22 | for (let i = 0; i < html.length; i++) {
23 | if (html[i] == "") {
24 | continue;
25 | }
26 |
27 | await page.setContent(
28 | `
${html[i]}
`
29 | );
30 |
31 | const rect = await page.evaluate(selector => {
32 | const element = document.querySelector(selector);
33 | if (!element) return null;
34 | const { x, y, width, height } = element.getBoundingClientRect();
35 | return { left: x, top: y, width, height, id: element.id };
36 | }, "div");
37 |
38 | await page.screenshot({
39 | path: `${i + 1}.png`,
40 | clip: {
41 | x: rect.left - 5,
42 | y: rect.top - 3,
43 | width: rect.width + 10,
44 | height: rect.height
45 | }
46 | });
47 | }
48 |
49 | await browser.close();
50 | })();
51 |
--------------------------------------------------------------------------------
/lib/ruby.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports.parse = parse;
4 | module.exports.addTag = addTag;
5 |
6 | /**
7 | * Parses the [body]{toptext} syntax and returns
8 | * the body and toptext parts. These are then processed
9 | * in furigana.js and turned into \ tags by
10 | * the {@link addTag} function.
11 | *
12 | * @param {*} state Markdown-it's inline state.
13 | * @returns {{body: string, toptext: string, nextPos: int}}
14 | * body: the main text part of the \ tag.
15 | *
16 | * toptext: the top part of the \ tag.
17 | *
18 | * nextPos: index of the next character in the markdown source.
19 | */
20 | function parse(state) {
21 | if (state.src.charAt(state.pos) !== "[") {
22 | return null;
23 | }
24 |
25 | const bodyStartBracket = state.pos;
26 | const bodyEndBracket = state.src.indexOf("]", bodyStartBracket);
27 |
28 | if (
29 | bodyEndBracket === -1 ||
30 | bodyEndBracket >= state.posMax ||
31 | state.src.charAt(bodyEndBracket + 1) !== "{"
32 | ) {
33 | return null;
34 | }
35 |
36 | const toptextStartBracket = bodyEndBracket + 1;
37 | const toptextEndBracket = state.src.indexOf("}", toptextStartBracket);
38 |
39 | if (toptextEndBracket === -1 || toptextEndBracket >= state.posMax) {
40 | return null;
41 | }
42 |
43 | const body = state.src.slice(bodyStartBracket + 1, bodyEndBracket);
44 | const toptext = state.src.slice(toptextStartBracket + 1, toptextEndBracket);
45 | if (body.trim() === "" || toptext.trim() === "") {
46 | return null;
47 | }
48 |
49 | return {
50 | body: body,
51 | toptext: toptext,
52 | nextPos: toptextEndBracket + 1
53 | };
54 | }
55 |
56 | /**
57 | * Takes as content a flat array of main parts of
58 | * the ruby, each followed immediately by the text
59 | * that should show up above these parts.
60 | *
61 | * That content is then stored in its appropriate
62 | * representation in a markdown-it's inline state,
63 | * eventually resulting in a \ tag.
64 | *
65 | * This function also gives you the option to add
66 | * fallback parentheses, should the \
67 | * tag be unsupported. In that case, the top text
68 | * of the ruby will instead be shown after the main
69 | * text, surrounded by these parentheses.
70 | *
71 | * @example
72 | * addTag(state, ['猫', 'ねこ', 'と', '', '犬', 'いぬ'])
73 | * // markdown-it will eventually produce a tag
74 | * // with 猫と犬 as its main text, with ねこ corresponding
75 | * // to the 猫 kanji, and いぬ corresponding to the 犬 kanji.
76 | *
77 | * @param {*} state Markdown-it's inline state.
78 | * @param {string[]} content Flat array of main parts of
79 | * the ruby, each followed by the text that should
80 | * be above those parts.
81 | * @param {string} fallbackParens Parentheses to use
82 | * as a fallback if the \ tag happens to be
83 | * unsupported. Example value: "【】".
84 | * "" disables fallback parentheses.
85 | */
86 | function addTag(state, content, fallbackParens = "") {
87 | function pushText(text) {
88 | const token = state.push("text", "", 0);
89 | token.content = text;
90 | }
91 |
92 | state.push("ruby_open", "ruby", 1);
93 |
94 | for (let i = 0; i < content.length; i += 2) {
95 | const body = content[i];
96 | const toptext = content[i + 1];
97 |
98 | pushText(body);
99 |
100 | if (toptext === "") {
101 | state.push("rt_open", "rt", 1);
102 | state.push("rt_close", "rt", -1);
103 | continue;
104 | }
105 |
106 | if (fallbackParens !== "") {
107 | state.push("rp_open", "rp", 1);
108 | pushText(fallbackParens.charAt(0));
109 | state.push("rp_close", "rp", -1);
110 | }
111 |
112 | state.push("rt_open", "rt", 1);
113 | pushText(toptext);
114 | state.push("rt_close", "rt", -1);
115 |
116 | if (fallbackParens !== "") {
117 | state.push("rp_open", "rp", 1);
118 | pushText(fallbackParens.charAt(1));
119 | state.push("rp_close", "rp", -1);
120 | }
121 | }
122 |
123 | state.push("ruby_close", "ruby", -1);
124 | }
125 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const assert = require("assert");
4 | const md = require("markdown-it")().use(require("../index")());
5 |
6 | describe("ruby", function() {
7 | it("should parse basic [body]{toptext}", function() {
8 | assert.equal(
9 | md.renderInline("[漢字]{かんじ}"),
10 | "漢字"
11 | );
12 | });
13 |
14 | it("should parse single [body]{toptext} in a sentence", function() {
15 | assert.equal(
16 | md.renderInline("Foo [漢字]{かんじ} bar."),
17 | "Foo 漢字 bar."
18 | );
19 | });
20 |
21 | it("should parse multiple [body]{toptext} in a sentence", function() {
22 | assert.equal(
23 | md.renderInline("Foo [漢字]{かんじ} bar [猫]{ねこ} baz."),
24 | "Foo 漢字 bar 猫 baz."
25 | );
26 | });
27 |
28 | it("should ignore empty body", function() {
29 | assert.equal(md.renderInline("[]{ねこ}"), "[]{ねこ}");
30 | assert.equal(md.renderInline("[ ]{ねこ}"), "[ ]{ねこ}");
31 | });
32 |
33 | it("should ignore empty toptext", function() {
34 | assert.equal(md.renderInline("[猫]{}"), "[猫]{}");
35 | assert.equal(md.renderInline("[猫]{ }"), "[猫]{ }");
36 | });
37 | });
38 |
39 | describe("furigana", function() {
40 | it("should be able to pattern match a single kanji+hiragana word", function() {
41 | assert.equal(
42 | md.renderInline("[食べる]{たべる}"),
43 | "食べる"
44 | );
45 | });
46 |
47 | it("should be able to pattern match a word with hiragana in the middle", function() {
48 | assert.equal(
49 | md.renderInline("[取り返す]{とりかえす}"),
50 | "取り返す"
51 | );
52 | });
53 |
54 | it("should be able to split furigana with a dot", function() {
55 | assert.equal(
56 | md.renderInline("[漢字]{かん.じ}"),
57 | "漢字"
58 | );
59 | });
60 |
61 | it("should be able to use dots to resolve ambiguities", function() {
62 | assert.equal(
63 | md.renderInline("[可愛い犬]{か.わい.い.いぬ}"),
64 | "可愛い犬"
65 | );
66 | });
67 |
68 | it("should be able to use pluses to resolve ambiguities without splitting furigana", function() {
69 | assert.equal(
70 | md.renderInline("[可愛い犬]{か+わい.い.いぬ}"),
71 | "可愛い犬"
72 | );
73 | });
74 |
75 | it("should be able to handle symbols other than kanji and kana in the body", function() {
76 | assert.equal(
77 | md.renderInline("[猫!?可愛い!!!w]{ねこ.かわいい}"),
78 | "猫!?可愛い!!!w"
79 | );
80 | });
81 |
82 | it("should apply the whole toptext to the whole body if it can't pattern match", function() {
83 | assert.equal(
84 | md.renderInline("[食べる]{たべべ}"),
85 | "食べる"
86 | );
87 | assert.equal(
88 | md.renderInline("[アクセラレーター]{accelerator}"),
89 | "アクセラレーター"
90 | );
91 | assert.equal(
92 | md.renderInline("[cat]{ねこ}"),
93 | "cat"
94 | );
95 | assert.equal(
96 | md.renderInline("[可愛い]{kawaii}"),
97 | "可愛い"
98 | );
99 | });
100 |
101 | it("should accept a few other separators other than ASCII dot", function() {
102 | assert.equal(
103 | md.renderInline(
104 | "[犬犬犬犬犬犬犬犬犬犬犬]{いぬ.いぬ.いぬ。いぬ・いぬ|いぬ|いぬ/いぬ/いぬ いぬ いぬ}"
105 | ),
106 | "" + "犬".repeat(11) + ""
107 | );
108 | });
109 |
110 | it("should accept full-width plus as combinator", function() {
111 | assert.equal(
112 | md.renderInline("[可愛い犬]{か+わい.い.いぬ}"),
113 | "可愛い犬"
114 | );
115 | });
116 |
117 | it("should accept furigana in romaji, as long as body is kanji-only", function() {
118 | assert.equal(
119 | md.renderInline("[漢字]{kan.ji}"),
120 | "漢字"
121 | );
122 | });
123 |
124 | it("should disable pattern matching if toptext starts with an equals sign", function() {
125 | assert.equal(
126 | md.renderInline("[食べる]{=たべる}"),
127 | "食べる"
128 | );
129 | assert.equal(
130 | md.renderInline("[食べる]{=たべる}"),
131 | "食べる"
132 | );
133 | });
134 |
135 | it("should NOT disable pattern matching if = appears not in the beginning", function() {
136 | assert.equal(
137 | md.renderInline("[猫だ]{ね=こだ}"),
138 | "猫だ"
139 | );
140 | assert.equal(
141 | md.renderInline("[猫だ]{ね=こだ}"),
142 | "猫だ"
143 | );
144 | });
145 |
146 | it("should pattern match katakana", function() {
147 | assert.equal(
148 | md.renderInline("[ダメな奴]{ダメなやつ}"),
149 | "ダメな奴"
150 | );
151 | });
152 |
153 | it("should pattern match half-width katakana", function() {
154 | assert.equal(
155 | md.renderInline("[ダメな奴]{ダメなやつ}"),
156 | "ダメな奴"
157 | );
158 | });
159 |
160 | it("should abort if body only partially matches the furigana", function() {
161 | assert.equal(
162 | md.renderInline("[猫だ]{ねこだよ}"),
163 | "猫だ"
164 | );
165 | assert.equal(
166 | md.renderInline("[は猫]{これはねこ}"),
167 | "は猫"
168 | );
169 | });
170 | });
171 |
172 | describe("emphasis dots", function() {
173 | it("should be applied with [body]{*}", function() {
174 | assert.equal(
175 | md.renderInline("[だから]{*}"),
176 | "だから"
177 | );
178 | });
179 |
180 | it("should accept a full-width asterisk as well", function() {
181 | assert.equal(
182 | md.renderInline("[だから]{*}"),
183 | "だから"
184 | );
185 | });
186 |
187 | it("should accept custom markers", function() {
188 | assert.equal(
189 | md.renderInline("[だから]{*+}"),
190 | "だから"
191 | );
192 | });
193 |
194 | it("should work on any character", function() {
195 | assert.equal(
196 | md.renderInline("[猫is❤]{*}"),
197 | "猫is❤"
198 | );
199 | });
200 |
201 | it("should NOT create emphasis dots if * appears not in the beginning", function() {
202 | assert.equal(
203 | md.renderInline("[猫だ]{ね*こだ}"),
204 | "猫だ"
205 | );
206 | assert.equal(
207 | md.renderInline("[猫だ]{ね*こだ}"),
208 | "猫だ"
209 | );
210 | });
211 | });
212 |
213 | describe("options", function() {
214 | it("should allow custom fallback parentheses", function() {
215 | let md = require("markdown-it")().use(
216 | require("../index")({ fallbackParens: "()" })
217 | );
218 |
219 | assert.equal(
220 | md.renderInline("[漢字]{かんじ}"),
221 | "漢字"
222 | );
223 | });
224 |
225 | it("should allow adding extra separators", function() {
226 | let md = require("markdown-it")().use(
227 | require("../index")({ extraSeparators: "_-\\]" })
228 | );
229 |
230 | assert.equal(
231 | md.renderInline("[犬犬犬犬犬犬犬]{いぬ.いぬ。いぬ_いぬ-いぬ\\いぬ]いぬ}"),
232 | "" + "犬".repeat(7) + ""
233 | );
234 | });
235 |
236 | it("should allow adding extra combinators", function() {
237 | let md = require("markdown-it")().use(
238 | require("../index")({ extraCombinators: "*" })
239 | );
240 |
241 | assert.equal(
242 | md.renderInline("[可愛い犬]{か+わい.い.いぬ}"),
243 | "可愛い犬"
244 | );
245 | assert.equal(
246 | md.renderInline("[可愛い犬]{か*わい.い.いぬ}"),
247 | "可愛い犬"
248 | );
249 | });
250 | });
251 |
--------------------------------------------------------------------------------
/lib/furigana.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = furigana;
4 |
5 | const rubyHelper = require("./ruby");
6 |
7 | const kanaRegex = /[\u3040-\u3096\u30a1-\u30fa\uff66-\uff9fー]/;
8 | const kanjiRegex = /[\u3400-\u9faf]/;
9 |
10 | /**
11 | * Furigana is marked using the [body]{furigana} syntax.
12 | * First step, performed by bodyToRegex, is to convert
13 | * the body to a regex, which can then be used to pattern
14 | * match on the furigana.
15 | *
16 | * In essence, every kanji needs to be converted to a
17 | * pattern similar to ".?", so that it can match some kana
18 | * from the furigana part. However, this alone is ambiguous.
19 | * Consider [可愛い犬]{かわいいいぬ}: in this case there are
20 | * three different ways to assign furigana in the body.
21 | *
22 | * Ambiguities can be resolved by adding separator characters
23 | * in the furigana. These are only matched at the
24 | * boundaries between kanji and other kanji/kana.
25 | * So a regex created from 可愛い犬 should be able to match
26 | * か・わい・い・いぬ, but a regex created from 美味しい shouldn't
27 | * be able to match おいし・い.
28 | *
29 | * For purposes of this function, only ASCII dot is a
30 | * separators. Other characters are converted to dots in
31 | * the {@link cleanFurigana} function.
32 | *
33 | * The notation [可愛い犬]{か・わい・い・いぬ} forces us to
34 | * have separate \