yanagi 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/data/combinatorial.yml +59 -0
- data/data/exceptions.yml +15 -0
- data/data/exonyms.yml +14 -0
- data/data/mora.yml +790 -0
- data/exe/yanagi +7 -0
- data/lib/yanagi/audit.rb +301 -0
- data/lib/yanagi/cli.rb +345 -0
- data/lib/yanagi/cyrillic.rb +310 -0
- data/lib/yanagi/doc_sync.rb +194 -0
- data/lib/yanagi/lexicon.rb +211 -0
- data/lib/yanagi/mora.rb +81 -0
- data/lib/yanagi/normalize.rb +26 -0
- data/lib/yanagi/romaji.rb +55 -0
- data/lib/yanagi/rules.rb +89 -0
- data/lib/yanagi/version.rb +5 -0
- data/lib/yanagi.rb +15 -0
- data/yanagi.gemspec +34 -0
- metadata +94 -0
data/lib/yanagi/mora.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "normalize"
|
|
4
|
+
require_relative "rules"
|
|
5
|
+
|
|
6
|
+
module Yanagi
|
|
7
|
+
Mora = Struct.new(:kana, :kind, :onset, :nucleus, keyword_init: true) do
|
|
8
|
+
def sokuon?
|
|
9
|
+
kind == :sokuon
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def chouonpu?
|
|
13
|
+
kind == :chouonpu
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def moraic_n?
|
|
17
|
+
kind == :moraic_n
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def syllable?
|
|
21
|
+
kind == :syllable
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def passthrough?
|
|
25
|
+
kind == :passthrough
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module Tokenizer
|
|
30
|
+
def self.tokenize(input)
|
|
31
|
+
hira = Normalize.to_hiragana(Normalize.nfkc(input.to_s))
|
|
32
|
+
moras = []
|
|
33
|
+
mora_map = Rules.mora_map
|
|
34
|
+
|
|
35
|
+
i = 0
|
|
36
|
+
len = hira.length
|
|
37
|
+
while i < len
|
|
38
|
+
c1 = hira[i]
|
|
39
|
+
c2 = hira[i, 2]
|
|
40
|
+
|
|
41
|
+
if c1 == "っ"
|
|
42
|
+
moras << Mora.new(kana: "っ", kind: :sokuon, onset: nil, nucleus: nil)
|
|
43
|
+
i += 1
|
|
44
|
+
elsif c1 == "ー"
|
|
45
|
+
moras << Mora.new(kana: "ー", kind: :chouonpu, onset: nil, nucleus: nil)
|
|
46
|
+
i += 1
|
|
47
|
+
elsif c1 == "ん"
|
|
48
|
+
moras << Mora.new(kana: "ん", kind: :moraic_n, onset: "n", nucleus: "n")
|
|
49
|
+
i += 1
|
|
50
|
+
elsif c2 && mora_map.key?(c2.to_sym)
|
|
51
|
+
info = mora_map[c2.to_sym]
|
|
52
|
+
moras << Mora.new(
|
|
53
|
+
kana: c2,
|
|
54
|
+
kind: :syllable,
|
|
55
|
+
onset: info[:onset]&.to_s || "",
|
|
56
|
+
nucleus: info[:nucleus]&.to_s || ""
|
|
57
|
+
)
|
|
58
|
+
i += 2
|
|
59
|
+
elsif mora_map.key?(c1.to_sym)
|
|
60
|
+
info = mora_map[c1.to_sym]
|
|
61
|
+
moras << Mora.new(
|
|
62
|
+
kana: c1,
|
|
63
|
+
kind: :syllable,
|
|
64
|
+
onset: info[:onset]&.to_s || "",
|
|
65
|
+
nucleus: info[:nucleus]&.to_s || ""
|
|
66
|
+
)
|
|
67
|
+
i += 1
|
|
68
|
+
else
|
|
69
|
+
moras << Mora.new(kana: c1, kind: :passthrough, onset: nil, nucleus: nil)
|
|
70
|
+
i += 1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
moras
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.tokenize(input)
|
|
79
|
+
Tokenizer.tokenize(input)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yanagi
|
|
4
|
+
module Normalize
|
|
5
|
+
KATA_START = 0x30A1
|
|
6
|
+
KATA_END = 0x30F6
|
|
7
|
+
KATA_SHIFT = 0x60
|
|
8
|
+
|
|
9
|
+
# NFKC-normalise a string (compatibility decomposition then canonical composition).
|
|
10
|
+
# Input may arrive tagged ASCII-8BIT (e.g. from ARGV), which unicode_normalize
|
|
11
|
+
# rejects, so coerce to UTF-8 first.
|
|
12
|
+
def self.nfkc(str)
|
|
13
|
+
s = str.to_s
|
|
14
|
+
s = s.dup.force_encoding(Encoding::UTF_8) unless s.encoding == Encoding::UTF_8
|
|
15
|
+
s.unicode_normalize(:nfkc)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convert a katakana string to hiragana (passthrough for everything else).
|
|
19
|
+
def self.to_hiragana(str)
|
|
20
|
+
str.to_s.chars.map do |ch|
|
|
21
|
+
cp = ch.ord
|
|
22
|
+
(cp >= KATA_START && cp <= KATA_END) ? (cp - KATA_SHIFT).chr(Encoding::UTF_8) : ch
|
|
23
|
+
end.join
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mora"
|
|
4
|
+
require_relative "rules"
|
|
5
|
+
|
|
6
|
+
module Yanagi
|
|
7
|
+
module Romaji
|
|
8
|
+
def self.call(input)
|
|
9
|
+
moras = input.is_a?(Array) ? input : Tokenizer.tokenize(input)
|
|
10
|
+
mora_map = Rules.mora_map
|
|
11
|
+
res = []
|
|
12
|
+
|
|
13
|
+
moras.each_with_index do |mora, idx|
|
|
14
|
+
case mora.kind
|
|
15
|
+
when :sokuon
|
|
16
|
+
# Find next non-sokuon/chouonpu mora
|
|
17
|
+
next_mora = moras[(idx + 1)..].find { |m| m.syllable? || m.moraic_n? }
|
|
18
|
+
if next_mora
|
|
19
|
+
next_rom = mora_map.dig(next_mora.kana.to_sym, :romaji)&.to_s || next_mora.kana.to_s
|
|
20
|
+
consonant = next_rom.start_with?("ch") ? "t" : next_rom[0]
|
|
21
|
+
res << consonant if consonant
|
|
22
|
+
end
|
|
23
|
+
when :chouonpu
|
|
24
|
+
prev_char = res.last&.chars&.last
|
|
25
|
+
res << prev_char if prev_char
|
|
26
|
+
when :moraic_n
|
|
27
|
+
res << "n"
|
|
28
|
+
when :syllable
|
|
29
|
+
rom = mora_map.dig(mora.kana.to_sym, :romaji)&.to_s || mora.kana.to_s
|
|
30
|
+
res << rom
|
|
31
|
+
else
|
|
32
|
+
res << mora.kana.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
res.join
|
|
37
|
+
end
|
|
38
|
+
def self.matches?(reading, romaji)
|
|
39
|
+
expected = call(reading)
|
|
40
|
+
return true if romaji == expected
|
|
41
|
+
|
|
42
|
+
norm_actual = romaji.to_s.gsub(/\d+$/, "").gsub(/([aeiou])-/, '\1\1').gsub("oo", "ou").gsub("dewa", "deha")
|
|
43
|
+
norm_exp = expected.gsub("oo", "ou")
|
|
44
|
+
norm_actual == norm_exp
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.romaji(input)
|
|
49
|
+
Romaji.call(input)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.romaji_matches?(reading, romaji)
|
|
53
|
+
Romaji.matches?(reading, romaji)
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/yanagi/rules.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Yanagi
|
|
6
|
+
module Rules
|
|
7
|
+
DATA_DIR = File.expand_path("../../data", __dir__).freeze
|
|
8
|
+
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
# Policy rules ship with the gem. Corpus-derived data (the lexicon and the
|
|
12
|
+
# native-Ukrainian allowlist) is generated from a specific translation
|
|
13
|
+
# project and is NOT distributed: point YANAGI_DATA_DIR at a directory
|
|
14
|
+
# holding those files, or build them with `yanagi lexicon build`.
|
|
15
|
+
CORPUS_FILES = %w[lexicon.yml native_ua_allowlist.yml].freeze
|
|
16
|
+
|
|
17
|
+
def self.data_dir
|
|
18
|
+
DATA_DIR
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Where corpus-derived files are read from. Defaults to the gem's data
|
|
22
|
+
# directory so a local checkout keeps working without configuration.
|
|
23
|
+
def self.corpus_dir
|
|
24
|
+
ENV.fetch("YANAGI_DATA_DIR", DATA_DIR)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.path_for(filename)
|
|
28
|
+
dir = CORPUS_FILES.include?(filename) ? corpus_dir : DATA_DIR
|
|
29
|
+
File.join(dir, filename)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.load_yaml(filename)
|
|
33
|
+
path = path_for(filename)
|
|
34
|
+
return {}.freeze unless File.exist?(path)
|
|
35
|
+
|
|
36
|
+
data = YAML.safe_load_file(path, permitted_classes: [Symbol, Date], symbolize_names: true) || {}
|
|
37
|
+
deep_freeze(data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.mora_map
|
|
41
|
+
@mora_map ||= load_yaml("mora.yml")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.combinatorial
|
|
45
|
+
@combinatorial ||= load_yaml("combinatorial.yml")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.exonyms
|
|
49
|
+
@exonyms ||= load_yaml("exonyms.yml")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.lexicon
|
|
53
|
+
@lexicon ||= load_yaml("lexicon.yml")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.native_ua_allowlist
|
|
57
|
+
@native_ua_allowlist ||= load_yaml("native_ua_allowlist.yml")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.exceptions
|
|
61
|
+
@exceptions ||= load_yaml("exceptions.yml")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.reload!
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
@mora_map = nil
|
|
67
|
+
@combinatorial = nil
|
|
68
|
+
@exonyms = nil
|
|
69
|
+
@lexicon = nil
|
|
70
|
+
@native_ua_allowlist = nil
|
|
71
|
+
@exceptions = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.deep_freeze(obj)
|
|
76
|
+
case obj
|
|
77
|
+
when Hash
|
|
78
|
+
obj.transform_keys(&:freeze).transform_values { |v| deep_freeze(v) }.freeze
|
|
79
|
+
when Array
|
|
80
|
+
obj.map { |v| deep_freeze(v) }.freeze
|
|
81
|
+
when String
|
|
82
|
+
obj.dup.freeze
|
|
83
|
+
else
|
|
84
|
+
obj.freeze
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
private_class_method :deep_freeze
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/yanagi.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "yanagi/version"
|
|
4
|
+
require_relative "yanagi/normalize"
|
|
5
|
+
require_relative "yanagi/rules"
|
|
6
|
+
require_relative "yanagi/mora"
|
|
7
|
+
require_relative "yanagi/romaji"
|
|
8
|
+
require_relative "yanagi/cyrillic"
|
|
9
|
+
require_relative "yanagi/doc_sync"
|
|
10
|
+
require_relative "yanagi/lexicon"
|
|
11
|
+
require_relative "yanagi/audit"
|
|
12
|
+
|
|
13
|
+
module Yanagi
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
end
|
data/yanagi.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/yanagi/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "yanagi"
|
|
7
|
+
spec.version = Yanagi::VERSION
|
|
8
|
+
spec.authors = ["shogi-dojo"]
|
|
9
|
+
spec.email = ["play@shogi-dojo.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Deterministic Japanese to Ukrainian transliteration engine"
|
|
12
|
+
spec.description = "Zero-dependency Japanese to Ukrainian transliteration and policy enforcement gem."
|
|
13
|
+
spec.homepage = "https://github.com/shogi-dojo/yanagi"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/shogi-dojo/yanagi/tree/main"
|
|
19
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/shogi-dojo/yanagi/issues"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/shogi-dojo/yanagi/blob/main/CHANGELOG.md"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
# Corpus-derived data (lexicon, native-Ukrainian allowlist) is generated
|
|
24
|
+
# from a specific translation project and is not distributed.
|
|
25
|
+
Dir["{lib,data,exe}/**/*", "LICENSE*", "README*", "CHANGELOG*", "yanagi.gemspec"] -
|
|
26
|
+
Dir["data/{lexicon,native_ua_allowlist}.yml"]
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = ["yanagi"]
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_development_dependency "minitest", ">= 5.16"
|
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: yanagi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- shogi-dojo
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.16'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.16'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
description: Zero-dependency Japanese to Ukrainian transliteration and policy enforcement
|
|
41
|
+
gem.
|
|
42
|
+
email:
|
|
43
|
+
- play@shogi-dojo.com
|
|
44
|
+
executables:
|
|
45
|
+
- yanagi
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- data/combinatorial.yml
|
|
53
|
+
- data/exceptions.yml
|
|
54
|
+
- data/exonyms.yml
|
|
55
|
+
- data/mora.yml
|
|
56
|
+
- exe/yanagi
|
|
57
|
+
- lib/yanagi.rb
|
|
58
|
+
- lib/yanagi/audit.rb
|
|
59
|
+
- lib/yanagi/cli.rb
|
|
60
|
+
- lib/yanagi/cyrillic.rb
|
|
61
|
+
- lib/yanagi/doc_sync.rb
|
|
62
|
+
- lib/yanagi/lexicon.rb
|
|
63
|
+
- lib/yanagi/mora.rb
|
|
64
|
+
- lib/yanagi/normalize.rb
|
|
65
|
+
- lib/yanagi/romaji.rb
|
|
66
|
+
- lib/yanagi/rules.rb
|
|
67
|
+
- lib/yanagi/version.rb
|
|
68
|
+
- yanagi.gemspec
|
|
69
|
+
homepage: https://github.com/shogi-dojo/yanagi
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata:
|
|
73
|
+
homepage_uri: https://github.com/shogi-dojo/yanagi
|
|
74
|
+
source_code_uri: https://github.com/shogi-dojo/yanagi/tree/main
|
|
75
|
+
bug_tracker_uri: https://github.com/shogi-dojo/yanagi/issues
|
|
76
|
+
changelog_uri: https://github.com/shogi-dojo/yanagi/blob/main/CHANGELOG.md
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.0.0
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 4.0.16
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Deterministic Japanese to Ukrainian transliteration engine
|
|
94
|
+
test_files: []
|