cjk_index 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c9ffe634562d740738f6d9b9cf97ade5f5ab6d32a80b6bfd349496435d9e88b
4
- data.tar.gz: 6cc0aebdedd14b07cdcf9dda3dcc9b849b80d135149444f057daed4cc620c004
3
+ metadata.gz: b8780b3265cb3e02580edd66360721d6a9ea245c3b8465c49e499e0b1bb6d420
4
+ data.tar.gz: a9c1233d9e57d170ba90c25e7e0ecf9af6b10e89640584855dbe6fae5b358418
5
5
  SHA512:
6
- metadata.gz: ee41c34d99efe6e17f8936f3211b62aaab6fdafa54b714f685458c641a56263a46b506de9f703a4cd1c6f470c32e0d336b1914547655a433641ecbf9cdc5fcc0
7
- data.tar.gz: 608805cba3a84e8c1bb8425e09c8ff3bdcb2bd4da6443f6ca63eeb6d2e3952e75b0166db871e853383d3531cb40d7f48052cc70540f2ae7fd8bd5ae372424af7
6
+ metadata.gz: 205fff662f662ae191a3bb437351531473bcbb8943758d13b7b6593aeabb425814fb8a4cc779f5a028b3d0ff2c6c895ab90efb49ed6d9a6423d31f63f62bb8ed
7
+ data.tar.gz: 4a82cf20daf35f34bb5596645e373e4c1902722f5fd91c6541a77c8039f6d97a97d8421c68d20761c29914140d4e8201aa3d5409b883458ff2417deb633dde6b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 (2026-10-28)
4
+
5
+ - `variants` also takes table files of your own: one group per line,
6
+ tab-separated, the form to fold to first (the format of the itaiji lists
7
+ that collection databases keep). Paths are relative to the site's folder
8
+ (Jekyll, Bridgetown) or the current folder; the index records the path as
9
+ written and a digest of the contents. `Builder.load` and `Builder.read`
10
+ take `normalizer:` to read such an index from another folder.
11
+ - `Searcher.new(builder)` folds with the builder's normalizer.
12
+
3
13
  ## 1.0.0 (2026-10-28)
4
14
 
5
15
  Indexes built with 0.x should be rebuilt: the `ja` table changed, and the
data/README.md CHANGED
@@ -366,6 +366,35 @@ by Unicode normalization. `ko` is for old Hangul stored in the Private Use
366
366
  Area by HWP and the Hanyang fonts; a query typed in standard jamo then finds
367
367
  and highlights it.
368
368
 
369
+ ### A table of your own
370
+
371
+ Any entry of `variants` that is not one of the names above is a table file
372
+ of your own, relative to the site's folder (Jekyll, Bridgetown) or the
373
+ current folder (CLI, Rake, Ruby). One group per line, tab-separated: the
374
+ form to fold to, then its variants, one character each. This is the format
375
+ of the itaiji lists that collection databases often keep already.
376
+
377
+ ```text
378
+ # itaiji.tsv
379
+ 与 與
380
+ 宴 醼 讌
381
+ 嶋 嶌
382
+ ```
383
+
384
+ ```yaml
385
+ cjk_index:
386
+ variants: [ja, _data/itaiji.tsv]
387
+ ```
388
+
389
+ Each cell is folded first (NFKC, lowercase, katakana → hiragana), and the
390
+ table listed first still picks the form a group folds to. The index records
391
+ the entry as written (`_data/itaiji.tsv`), not the full path, and its digest
392
+ covers the file's contents, so editing the file means rebuilding the index.
393
+ To read such an index from another folder in Ruby, pass the normalizer:
394
+ `Builder.load(json, normalizer: CJKIndex::Normalizer.new(tables: [...], base: dir))`.
395
+ Wide lists merge more than spelling: a list that puts 燕 with 宴 makes
396
+ a search for 燕 (swallow) find 宴 (banquet). Check a list against your own material first.
397
+
369
398
  Browser script size, gzipped: no table 6.3 KB, `ja` 9.0 KB, `ja, zh-safe`
370
399
  24.1 KB, `ja, zh` 25.9 KB, `ja, ko` 36.1 KB.
371
400
 
@@ -11,7 +11,7 @@ module CJKIndex
11
11
  # Bridgetown.configure do |config|
12
12
  # init :cjk_index,
13
13
  # output: "cjk-index", # folder inside the output (default)
14
- # variants: ["ja"], # variant tables: ja (default), zh
14
+ # variants: ["ja"], # variant tables: ja (default), zh-safe, zh, ko, or a file
15
15
  # indexes: [
16
16
  # { name: "posts", # -> output/cjk-index/posts.json
17
17
  # collection: "posts", # a collection (or a list of them), or...
@@ -37,7 +37,7 @@ module CJKIndex
37
37
  def write(site, config)
38
38
  config = stringify(config)
39
39
  output = config.fetch("output", DEFAULT_OUTPUT).to_s.gsub(%r{\A/+|/+\z}, "")
40
- normalizer = Normalizer.new(tables: Array(config.fetch("variants", Normalizer::DEFAULT_TABLES)))
40
+ normalizer = Normalizer.new(tables: Array(config.fetch("variants", Normalizer::DEFAULT_TABLES)), base: site.root_dir)
41
41
  put(site, output, RUNTIME_NAME, Runtime.source(normalizer: normalizer))
42
42
  Array(config["indexes"]).each do |spec|
43
43
  builder = build(site, stringify(spec), normalizer)
@@ -129,7 +129,9 @@ module CJKIndex
129
129
  # Reads an index written by to_json (String) or to_h back, to update it.
130
130
  # Reads formats 2, 3 and 4; a split index is read with read.
131
131
  # A value filter keeps the form of each value that the index stored.
132
- def self.load(index)
132
+ # normalizer: needed when the index uses a table file of your own and
133
+ # the current folder is not the one it was built from.
134
+ def self.load(index, normalizer: nil)
133
135
  data = index.is_a?(String) ? JSON.parse(index) : index
134
136
  unless data.is_a?(Hash) && FORMATS.include?(data["format"])
135
137
  raise ArgumentError, "cjk_index: unsupported index format #{data.is_a?(Hash) ? data['format'].inspect : data.class}"
@@ -137,7 +139,7 @@ module CJKIndex
137
139
  raise ArgumentError, "cjk_index: a split index is read with Builder.read(directory)" unless data["tokens"]
138
140
 
139
141
  filters = (data["filters"] || {}).transform_values { |f| f.slice("kind", "separator") }
140
- normalizer = Normalizer.new(tables: data["variants"])
142
+ normalizer ||= Normalizer.new(tables: data["variants"])
141
143
  normalizer.check!(data)
142
144
  builder = new(fields: data["fields"], boosts: data["fields"].zip(data["boosts"]).to_h,
143
145
  normalizer: normalizer, filters: filters)
@@ -147,8 +149,8 @@ module CJKIndex
147
149
 
148
150
  # Reads an index from a file (written by write or to_json) or from the
149
151
  # directory of a split index (written by write_split), to update it.
150
- def self.read(path)
151
- load(Builder.read_data(path))
152
+ def self.read(path, normalizer: nil)
153
+ load(Builder.read_data(path), normalizer: normalizer)
152
154
  end
153
155
 
154
156
  # The index at path as one Hash with "tokens", for a file or a split
data/lib/cjk_index/cli.rb CHANGED
@@ -73,7 +73,7 @@ module CJKIndex
73
73
  name, spec = parse_filter(v)
74
74
  opts[:filters][name] = spec
75
75
  end
76
- o.on("--variants LIST", Array, "variant tables: ja (default), zh-safe, zh, ko") { |v| opts[:variants] = v }
76
+ o.on("--variants LIST", Array, "variant tables: ja (default), zh-safe, zh, ko, or table files") { |v| opts[:variants] = v }
77
77
  o.on("-o", "--output PATH", "index file (default: standard output)") { |v| output = v }
78
78
  o.on("--runtime PATH", "also write the browser script for these variants") { |v| runtime = v }
79
79
  end
@@ -196,7 +196,7 @@ module CJKIndex
196
196
  output = nil
197
197
  OptionParser.new do |o|
198
198
  o.banner = "Usage: cjk_index runtime [options]"
199
- o.on("--variants LIST", Array, "variant tables: ja (default), zh-safe, zh, ko; must match the index") { |v| variants = v }
199
+ o.on("--variants LIST", Array, "variant tables: ja (default), zh-safe, zh, ko, or table files; must match the index") { |v| variants = v }
200
200
  o.on("-o", "--output PATH", "script file (default: standard output)") { |v| output = v }
201
201
  end.parse!(argv)
202
202
  raise Error, "too many arguments: #{argv.join(' ')}" unless argv.empty?
@@ -9,7 +9,7 @@ module CJKIndex
9
9
  #
10
10
  # cjk_index:
11
11
  # output: assets/cjk-index # where files are written (default)
12
- # variants: [ja] # variant tables: ja (default), zh (Traditional -> Simplified)
12
+ # variants: [ja] # variant tables: ja (default), zh-safe, zh, ko, or a file of your own
13
13
  # indexes:
14
14
  # - name: posts # -> assets/cjk-index/posts.json
15
15
  # collection: posts # a Jekyll collection, or...
@@ -64,7 +64,7 @@ module CJKIndex
64
64
  return unless config.is_a?(Hash)
65
65
 
66
66
  output = config.fetch("output", DEFAULT_OUTPUT).sub(%r{\A/+}, "").sub(%r{/+\z}, "")
67
- normalizer = Normalizer.new(tables: Array(config.fetch("variants", Normalizer::DEFAULT_TABLES)))
67
+ normalizer = Normalizer.new(tables: Array(config.fetch("variants", Normalizer::DEFAULT_TABLES)), base: site.source)
68
68
  site.static_files << GeneratedFile.new(site, output, RUNTIME_NAME, Runtime.source(normalizer: normalizer))
69
69
  Array(config["indexes"]).each do |spec|
70
70
  builder = build(site, spec, normalizer)
@@ -24,10 +24,19 @@ module CJKIndex
24
24
  # With several tables, characters linked by any table fold to one form,
25
25
  # preferring the target of the table listed first: with [ja, zh],
26
26
  # 圖, 図 and 图 all become 図.
27
+ # Any other entry names a table file of your own, relative to base (the
28
+ # site's folder for Jekyll and Bridgetown, the current folder otherwise).
29
+ # The index records the entry as written, not the resolved path.
27
30
  #
28
31
  # Table format: UTF-8 lines "variant<TAB>folded", optionally followed by
29
32
  # another tab and a comment column; lines starting with "#" are comments.
30
33
  # "folded" may be several characters.
34
+ #
35
+ # Your own table is one group per line, tab-separated: the form to fold to,
36
+ # then its variants, one character each (the format of the itaiji lists
37
+ # that collection databases keep). Each cell is folded first (NFKC,
38
+ # lowercase, katakana -> hiragana), so a compatibility ideograph such as
39
+ # U+F900 joins the group of the character NFKC makes of it.
31
40
  class Normalizer
32
41
  DATA_DIR = File.expand_path("../../data", __dir__)
33
42
  TABLES = {
@@ -48,11 +57,38 @@ module CJKIndex
48
57
  @default ||= new
49
58
  end
50
59
 
51
- def self.read_table(name)
52
- file = TABLES.fetch(name) { raise ArgumentError, "unknown variant table #{name.inspect} (#{TABLES.keys.join(', ')})" }
53
- File.readlines(File.join(DATA_DIR, file), chomp: true, encoding: "UTF-8")
54
- .reject { |line| line.empty? || line.start_with?("#") }
55
- .map { |line| line.split("\t")[0, 2] }
60
+ # Pairs [variant, folded] of a built-in table, or of a table file of your
61
+ # own (a name that is not built in, resolved against base).
62
+ def self.read_table(name, base: Dir.pwd)
63
+ return read_groups(name, File.expand_path(name, base)) unless TABLES.key?(name)
64
+
65
+ lines(File.join(DATA_DIR, TABLES[name])).map { |_, line| line.split("\t")[0, 2] }
66
+ end
67
+
68
+ def self.lines(path) # :nodoc:
69
+ File.readlines(path, chomp: true, encoding: "UTF-8").each_with_index
70
+ .reject { |line, _| line.strip.empty? || line.start_with?("#") }
71
+ .map { |line, i| [i + 1, line.delete_prefix("\uFEFF")] }
72
+ end
73
+
74
+ def self.read_groups(name, path) # :nodoc:
75
+ unless File.file?(path)
76
+ raise ArgumentError, "unknown variant table #{name.inspect}: not one of #{TABLES.keys.join(', ')}, " \
77
+ "and no file at #{path}"
78
+ end
79
+
80
+ plain = new(tables: [])
81
+ lines(path).flat_map do |number, line|
82
+ target, *variants = line.split("\t").map(&:strip).reject(&:empty?).map { |cell| plain.normalize(cell) }
83
+ variants = variants.uniq.reject { |v| v == target } # NFKC may already fold a cell (㍿)
84
+ variants.each do |v|
85
+ next if v.length == 1
86
+
87
+ raise ArgumentError, "#{name}:#{number}: #{v.inspect} is not one character; " \
88
+ "only the first cell (the form to fold to) may be longer"
89
+ end
90
+ variants.map { |v| [v, target] }
91
+ end
56
92
  end
57
93
 
58
94
  attr_reader :tables, :variants, :digest
@@ -60,8 +96,9 @@ module CJKIndex
60
96
  KATAKANA = "#{KATAKANA_FIRST.chr(Encoding::UTF_8)}-#{KATAKANA_LAST.chr(Encoding::UTF_8)}"
61
97
  HIRAGANA = "#{(KATAKANA_FIRST - KANA_OFFSET).chr(Encoding::UTF_8)}-#{(KATAKANA_LAST - KANA_OFFSET).chr(Encoding::UTF_8)}"
62
98
 
63
- def initialize(tables: DEFAULT_TABLES)
99
+ def initialize(tables: DEFAULT_TABLES, base: Dir.pwd)
64
100
  @tables = tables.map(&:to_s).uniq.freeze
101
+ @base = base
65
102
  @variants = build_variants.freeze
66
103
  keys = @variants.keys.select { |k| k.length == 1 }
67
104
  @variant_chars = keys.empty? ? nil : Regexp.new("[#{keys.map { |k| format('\\u{%x}', k.ord) }.join}]")
@@ -122,7 +159,7 @@ module CJKIndex
122
159
  end
123
160
  rank = {}
124
161
  @tables.each_with_index do |name, priority|
125
- self.class.read_table(name).each do |from, to|
162
+ self.class.read_table(name, base: @base).each do |from, to|
126
163
  parent[find.call(from)] = find.call(to)
127
164
  rank[to] = [rank.fetch(to, [priority, to.ord]), [priority, to.ord]].min
128
165
  end
@@ -57,7 +57,7 @@ module CJKIndex
57
57
  end
58
58
  end
59
59
 
60
- @normalizer = normalizer || Normalizer.new(tables: data["variants"])
60
+ @normalizer = normalizer || @builder&.normalizer || Normalizer.new(tables: data["variants"])
61
61
  @normalizer.check!(data)
62
62
 
63
63
  if data["shards"] && !@load_shard
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CJKIndex
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cjk_index
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Satoru Nakamura