kuroshiro-enhance
Version:
kuroshiro is a Japanese language library for converting Japanese sentence to Hiragana, Katakana or Romaji with furigana and okurigana modes supported.
376 lines (357 loc) • 17.4 kB
JavaScript
import {
ROMANIZATION_SYSTEM,
getStrType,
patchTokens,
isHiragana,
isKatakana,
isKana,
isKanji,
isJapanese,
hasHiragana,
hasKatakana,
hasKana,
hasKanji,
hasJapanese,
toRawHiragana,
toRawKatakana,
toRawRomaji,
kanaToHiragna,
kanaToKatakana,
kanaToRomaji
} from "./util";
/**
* Kuroshiro Class
*/
class Kuroshiro {
/**
* Constructor
* @constructs Kuroshiro
*/
constructor() {
this._analyzer = null;
}
/**
* Initialize Kuroshiro
* @memberOf Kuroshiro
* @instance
* @returns {Promise} Promise object represents the result of initialization
*/
async init(analyzer) {
if (!analyzer || typeof analyzer !== "object" || typeof analyzer.init !== "function" || typeof analyzer.parse !== "function") {
throw new Error("Invalid initialization parameter.");
}
else if (this._analyzer == null) {
await analyzer.init();
this._analyzer = analyzer;
}
else {
throw new Error("Kuroshiro has already been initialized.");
}
}
/**
* Convert given string to target syllabary with options available
* @memberOf Kuroshiro
* @instance
* @param {string} str Given String
* @param {Object} [options] Settings Object
* @param {string} [options.to="hiragana"] Target syllabary ["hiragana"|"katakana"|"romaji"]
* @param {string} [options.mode="normal"] Convert mode ["normal"|"spaced"|"okurigana"|"furigana"]
* @param {string} [options.includeKatakana=false] Whether to include Katakana in Furigana mode
* @param {string} [options.romajiSystem="hepburn"] Romanization System ["nippon"|"passport"|"hepburn"]
* @param {string} [options.delimiter_start="("] Delimiter(Start)
* @param {string} [options.delimiter_end=")"] Delimiter(End)
* @returns {Promise} Promise object represents the result of conversion
*/
async convert(str, options) {
options = options || {};
options.to = options.to || "hiragana";
options.mode = options.mode || "normal";
options.includeKatakana = options.includeKatakana || false;
options.romajiSystem = options.romajiSystem || ROMANIZATION_SYSTEM.HEPBURN;
options.delimiter_start = options.delimiter_start || "(";
options.delimiter_end = options.delimiter_end || ")";
str = str || "";
if (["hiragana", "katakana", "romaji"].indexOf(options.to) === -1) {
throw new Error("Invalid Target Syllabary.");
}
if (["normal", "spaced", "okurigana", "furigana", "furigana_map"].indexOf(options.mode) === -1) {
throw new Error("Invalid Conversion Mode.");
}
const ROMAJI_SYSTEMS = Object.keys(ROMANIZATION_SYSTEM).map(e => ROMANIZATION_SYSTEM[e]);
if (ROMAJI_SYSTEMS.indexOf(options.romajiSystem) === -1) {
throw new Error("Invalid Romanization System.");
}
const rawTokens = await this._analyzer.parse(str);
const tokens = patchTokens(rawTokens);
if (options.mode === "normal" || options.mode === "spaced") {
switch (options.to) {
case "katakana":
if (options.mode === "normal") {
return tokens.map(token => token.reading).join("");
}
return tokens.map(token => token.reading).join(" ");
case "romaji":
const romajiConv = (token) => {
let preToken;
if (hasJapanese(token.surface_form)) {
preToken = token.pronunciation || token.reading;
}
else {
preToken = token.surface_form;
}
return toRawRomaji(preToken, options.romajiSystem);
};
if (options.mode === "normal") {
return tokens.map(romajiConv).join("");
}
return tokens.map(romajiConv).join(" ");
case "hiragana":
for (let hi = 0; hi < tokens.length; hi++) {
if (hasKanji(tokens[hi].surface_form)) {
if (!hasKatakana(tokens[hi].surface_form)) {
tokens[hi].reading = toRawHiragana(tokens[hi].reading);
}
else {
// handle katakana-kanji-mixed tokens
tokens[hi].reading = toRawHiragana(tokens[hi].reading);
let tmp = "";
let hpattern = "";
for (let hc = 0; hc < tokens[hi].surface_form.length; hc++) {
if (isKanji(tokens[hi].surface_form[hc])) {
hpattern += "(.*)";
}
else {
hpattern += isKatakana(tokens[hi].surface_form[hc]) ? toRawHiragana(tokens[hi].surface_form[hc]) : tokens[hi].surface_form[hc];
}
}
const hreg = new RegExp(hpattern);
const hmatches = hreg.exec(tokens[hi].reading);
if (hmatches) {
let pickKJ = 0;
for (let hc1 = 0; hc1 < tokens[hi].surface_form.length; hc1++) {
if (isKanji(tokens[hi].surface_form[hc1])) {
tmp += hmatches[pickKJ + 1];
pickKJ++;
}
else {
tmp += tokens[hi].surface_form[hc1];
}
}
tokens[hi].reading = tmp;
}
}
}
else {
tokens[hi].reading = tokens[hi].surface_form;
}
}
if (options.mode === "normal") {
return tokens.map(token => token.reading).join("");
}
return tokens.map(token => token.reading).join(" ");
default:
throw new Error("Unknown option.to param");
}
}
else if (options.mode === "okurigana" || options.mode === "furigana" || options.mode === "furigana_map") {
const notations = []; // [basic, basic_type[1=kanji,2=kana,3=others], notation, pronunciation]
for (let i = 0; i < tokens.length; i++) {
const strType = getStrType(tokens[i].surface_form);
switch (strType) {
case 0:
notations.push([tokens[i].surface_form, 1, toRawHiragana(tokens[i].reading), tokens[i].pronunciation || tokens[i].reading]);
break;
case 1:
let pattern = "";
let isLastTokenKanji = false;
const subs = []; // recognize kanjis and group them
for (let c = 0; c < tokens[i].surface_form.length; c++) {
if (isKanji(tokens[i].surface_form[c])) {
if (!isLastTokenKanji) { // ignore successive kanji tokens (#10)
isLastTokenKanji = true;
pattern += "(.+)";
subs.push(tokens[i].surface_form[c]);
}
else {
subs[subs.length - 1] += tokens[i].surface_form[c];
}
}
else {
isLastTokenKanji = false;
subs.push(tokens[i].surface_form[c]);
pattern += isKatakana(tokens[i].surface_form[c]) ? toRawHiragana(tokens[i].surface_form[c]) : tokens[i].surface_form[c];
}
}
const reg = new RegExp(`^${pattern}$`);
const matches = reg.exec(toRawHiragana(tokens[i].reading));
if (matches) {
let pickKanji = 1;
for (let c1 = 0; c1 < subs.length; c1++) {
if (isKanji(subs[c1][0])) {
notations.push([subs[c1], 1, matches[pickKanji], toRawKatakana(matches[pickKanji])]);
pickKanji += 1;
}
else {
notations.push([subs[c1], 2, toRawHiragana(subs[c1]), toRawKatakana(subs[c1])]);
}
}
}
else {
notations.push([tokens[i].surface_form, 1, toRawHiragana(tokens[i].reading), tokens[i].pronunciation || tokens[i].reading]);
}
break;
case 2:
for (let c2 = 0; c2 < tokens[i].surface_form.length; c2++) {
notations.push([tokens[i].surface_form[c2], 2, toRawHiragana(tokens[i].reading[c2]), (tokens[i].pronunciation && tokens[i].pronunciation[c2]) || tokens[i].reading[c2]]);
}
break;
case 3:
for (let c3 = 0; c3 < tokens[i].surface_form.length; c3++) {
notations.push([tokens[i].surface_form[c3], 3, tokens[i].surface_form[c3], tokens[i].surface_form[c3]]);
}
break;
default:
throw new Error("Unknown strType");
}
}
// Special mode: return structured ruby span map { text, ruby: [{s,e,rt}] }
if (options.mode === "furigana_map") {
// Build the plain text and span indices first
let text = "";
const ruby = [];
let cursor = 0;
for (let n = 0; n < notations.length; n++) {
const base = notations[n][0];
const typ = notations[n][1]; // 1=kanji, 2=kana, 3=others
const hira = notations[n][2];
const pron = notations[n][3];
const start = cursor;
const end = start + base.length;
// Decide whether this segment should carry ruby
let shouldRuby = false;
if (typ === 1) {
// Kanji always get ruby
shouldRuby = true;
}
else if (typ === 2) {
// Kana only when includeKatakana and the original was katakana
const isKatakanaLocal = base !== hira; // katakana differs from hiragana form
shouldRuby = !!options.includeKatakana && isKatakanaLocal;
}
if (shouldRuby) {
let rt;
switch (options.to) {
case "katakana":
rt = toRawKatakana(hira);
break;
case "romaji":
rt = toRawRomaji(pron, options.romajiSystem);
break;
case "hiragana":
default:
rt = hira;
break;
}
ruby.push({ s: start, e: end, rt });
}
text += base;
cursor = end;
}
return { text, ruby };
}
let result = "";
switch (options.to) {
case "katakana":
if (options.mode === "okurigana") {
for (let n0 = 0; n0 < notations.length; n0++) {
if (notations[n0][1] !== 1) {
result += notations[n0][0];
}
else {
result += notations[n0][0] + options.delimiter_start + toRawKatakana(notations[n0][2]) + options.delimiter_end;
}
}
}
else { // furigana
for (let n1 = 0; n1 < notations.length; n1++) {
if (notations[n1][1] !== 1) {
result += notations[n1][0];
}
else {
result += `<ruby>${notations[n1][0]}<rp>${options.delimiter_start}</rp><rt>${toRawKatakana(notations[n1][2])}</rt><rp>${options.delimiter_end}</rp></ruby>`;
}
}
}
return result;
case "romaji":
if (options.mode === "okurigana") {
for (let n2 = 0; n2 < notations.length; n2++) {
if (notations[n2][1] !== 1) {
result += notations[n2][0];
}
else {
result += notations[n2][0] + options.delimiter_start + toRawRomaji(notations[n2][3], options.romajiSystem) + options.delimiter_end;
}
}
}
else { // furigana
for (let n3 = 0; n3 < notations.length; n3++) {
if (notations[n3][1] === 3) {
// For non-Japanese characters (like punctuation), don't add ruby
result += notations[n3][0];
}
else {
// For all Japanese characters (kanji and kana), add ruby with romaji
result += `<ruby>${notations[n3][0]}<rp>${options.delimiter_start}</rp><rt>${toRawRomaji(notations[n3][3], options.romajiSystem)}</rt><rp>${options.delimiter_end}</rp></ruby>`;
}
}
}
return result;
case "hiragana":
if (options.mode === "okurigana") {
for (let n4 = 0; n4 < notations.length; n4++) {
if (notations[n4][1] !== 1) {
result += notations[n4][0];
}
else {
result += notations[n4][0] + options.delimiter_start + notations[n4][2] + options.delimiter_end;
}
}
}
else { // furigana
for (let n5 = 0; n5 < notations.length; n5++) {
const isKatakanaLocal = notations[n5][0] !== notations[n5][2];
const shouldUseRuby = (options.includeKatakana && isKatakanaLocal);
const isKanjiLocal = notations[n5][1] === 1;
if (shouldUseRuby || isKanjiLocal) {
result += `<ruby>${notations[n5][0]}<rp>${options.delimiter_start}</rp><rt>${notations[n5][2]}</rt><rp>${options.delimiter_end}</rp></ruby>`;
}
else {
result += notations[n5][0];
}
}
}
return result;
default:
throw new Error("Invalid Target Syllabary.");
}
}
}
}
const Util = {
isHiragana,
isKatakana,
isKana,
isKanji,
isJapanese,
hasHiragana,
hasKatakana,
hasKana,
hasKanji,
hasJapanese,
kanaToHiragna,
kanaToKatakana,
kanaToRomaji
};
Kuroshiro.Util = Util;
export default Kuroshiro;