ucode 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/config/unicode17_universal_glyph_set.yml +1 -1
  4. data/lib/ucode/cli.rb +1 -35
  5. data/lib/ucode/code_chart/extractor.rb +1 -9
  6. data/lib/ucode/code_chart/writer.rb +1 -1
  7. data/lib/ucode/commands/build.rb +3 -26
  8. data/lib/ucode/commands/canonical_build.rb +5 -8
  9. data/lib/ucode/commands/universal_set.rb +5 -3
  10. data/lib/ucode/commands.rb +0 -1
  11. data/lib/ucode/coordinator/enrichment/bidi.rb +35 -0
  12. data/lib/ucode/coordinator/enrichment/binary.rb +38 -0
  13. data/lib/ucode/coordinator/enrichment/casing.rb +55 -0
  14. data/lib/ucode/coordinator/enrichment/cjk.rb +49 -0
  15. data/lib/ucode/coordinator/enrichment/display.rb +36 -0
  16. data/lib/ucode/coordinator/enrichment/emoji.rb +36 -0
  17. data/lib/ucode/coordinator/enrichment/identity.rb +42 -0
  18. data/lib/ucode/coordinator/enrichment/indic.rb +32 -0
  19. data/lib/ucode/coordinator/enrichment/names.rb +63 -0
  20. data/lib/ucode/coordinator/enrichment/segmentation.rb +34 -0
  21. data/lib/ucode/coordinator/enrichment.rb +51 -0
  22. data/lib/ucode/coordinator/range_lookup.rb +65 -0
  23. data/lib/ucode/coordinator.rb +4 -276
  24. data/lib/ucode/error.rb +0 -8
  25. data/lib/ucode/glyphs/embedded_fonts/catalog.rb +32 -299
  26. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper.rb +130 -0
  27. data/lib/ucode/glyphs/embedded_fonts/content_stream_correlator.rb +25 -124
  28. data/lib/ucode/glyphs/embedded_fonts/font_entry.rb +0 -1
  29. data/lib/ucode/glyphs/embedded_fonts/pdf_indexer.rb +236 -0
  30. data/lib/ucode/glyphs/embedded_fonts/{source.rb → pdf_location.rb} +5 -5
  31. data/lib/ucode/glyphs/embedded_fonts/positional_matcher.rb +162 -0
  32. data/lib/ucode/glyphs/embedded_fonts/raw_font_descriptor.rb +24 -0
  33. data/lib/ucode/glyphs/embedded_fonts/renderer.rb +0 -2
  34. data/lib/ucode/glyphs/embedded_fonts/trace_correlator.rb +116 -0
  35. data/lib/ucode/glyphs/embedded_fonts/trace_glyph.rb +27 -0
  36. data/lib/ucode/glyphs/embedded_fonts/trace_parser.rb +50 -0
  37. data/lib/ucode/glyphs/embedded_fonts/trace_runner.rb +53 -0
  38. data/lib/ucode/glyphs/embedded_fonts/writer.rb +0 -4
  39. data/lib/ucode/glyphs/embedded_fonts.rb +9 -1
  40. data/lib/ucode/glyphs/pdf_fetcher.rb +7 -50
  41. data/lib/ucode/glyphs/resolver_factory.rb +45 -0
  42. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +1 -1
  43. data/lib/ucode/glyphs.rb +5 -14
  44. data/lib/ucode/version.rb +1 -1
  45. data/lib/ucode.rb +0 -2
  46. metadata +24 -16
  47. data/lib/ucode/commands/glyphs.rb +0 -94
  48. data/lib/ucode/glyphs/cell_extractor.rb +0 -130
  49. data/lib/ucode/glyphs/dvisvgm_renderer.rb +0 -29
  50. data/lib/ucode/glyphs/grid.rb +0 -30
  51. data/lib/ucode/glyphs/grid_detector.rb +0 -165
  52. data/lib/ucode/glyphs/monolith_page_map.rb +0 -181
  53. data/lib/ucode/glyphs/mutool_renderer.rb +0 -28
  54. data/lib/ucode/glyphs/page_renderer.rb +0 -234
  55. data/lib/ucode/glyphs/path_bbox.rb +0 -62
  56. data/lib/ucode/glyphs/pdf2svg_renderer.rb +0 -26
  57. data/lib/ucode/glyphs/pdftocairo_renderer.rb +0 -32
  58. data/lib/ucode/glyphs/pipeline.rb +0 -105
  59. data/lib/ucode/glyphs/writer.rb +0 -250
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2e60f073662cc78885f8ee6a0333a2307a8b894ddb9789b1e206f6ae63d25e3
4
- data.tar.gz: 50b3984de26589d0aab193250e9c8dd3f56fd0d10a4a77a21ffdcb236c38c737
3
+ metadata.gz: a6674c0e1a8cab0ef2ea7782878be4571e68603843742de78a43da6a6499220c
4
+ data.tar.gz: 416b0965b6fa4e1e22ef9c431a7c27a16093ba90b708224b7e9c9e1f15ac2c49
5
5
  SHA512:
6
- metadata.gz: 88d9ef3df0f99af9b3897cd429666092d48a373d9c60d3c83a6e06a59404e0523c736a8d971fbb6d6d29c9b4b80d9610cad54a64cda439326e19a80670d67ba9
7
- data.tar.gz: 8146f6984defddb5b45204fe7a8852cd3051bf1474584eddc7489e5ec9fd4900d48452ac5462a1c6c2ac9b8213823ace68cebe6c5b5176dd0c25c922f8f1d4ba
6
+ metadata.gz: 361d385dd8b5bb04d3ce528ddda0bed64b8c5db46a1ef7e2f77c79ba5c4cf41f67235e8e55d5a38ec2957e1650f7fa4610b50722a299f2dfd67f900bd25e66c0
7
+ data.tar.gz: 1049d2b20c925da363339af6bcf1b98fd1d7fcf93a50adf9bde2240314eb272f89af6981a6604d8e7b5fa8b075b6c36deb68d42e691425f18fa70fca94a4286e
data/Rakefile CHANGED
@@ -26,4 +26,4 @@ begin
26
26
  rescue LoadError
27
27
  end
28
28
 
29
- task default: %i[spec rubocop]
29
+ task default: %i[spec rubocop]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  unicode_version: 17.0.0
3
- ucode_version: 0.2.1
3
+ ucode_version: 0.2.3
4
4
  generated_at: '2026-06-28T00:00:00Z'
5
5
  default_sources:
6
6
  - kind: fontist
data/lib/ucode/cli.rb CHANGED
@@ -109,34 +109,6 @@ module Ucode
109
109
  puts JSON.pretty_generate(result)
110
110
  end
111
111
 
112
- # ─────────────── glyphs ───────────────
113
- desc "glyphs [VERSION]", "Extract per-codepoint SVGs from Code Charts PDFs (experimental)"
114
- long_desc <<~LONG
115
- EXPERIMENTAL in v0.1. The cell extractor currently includes cell-border
116
- decorations alongside the actual character outline, so the output is not
117
- yet suitable for end-user display. Opt in with --include-glyphs to run
118
- the pipeline anyway; otherwise it returns a skipped payload.
119
- LONG
120
- option :to, type: :string, default: "./output"
121
- option :block, type: :array, desc: "Limit to these block ids"
122
- option :force, type: :boolean, default: false
123
- option :monolith, type: :string, default: "CodeCharts.pdf",
124
- desc: "Path to CodeCharts.pdf for fallback slicing"
125
- option :include_glyphs, type: :boolean, default: false,
126
- desc: "Opt into the experimental v0.1 pipeline"
127
- def glyphs(version = nil)
128
- result = Commands::GlyphsCommand.new.call(
129
- VersionResolver.resolve(version),
130
- output_root: options[:to],
131
- block_filter: options[:block],
132
- force: options[:force],
133
- monolith_path: options[:monolith],
134
- include_glyphs: options[:include_glyphs],
135
- warn: $stderr,
136
- )
137
- puts JSON.pretty_generate(result)
138
- end
139
-
140
112
  # ─────────────── site ───────────────
141
113
  class Site < Thor
142
114
  desc "init", "Copy the Vitepress scaffold into site/"
@@ -345,22 +317,16 @@ module Ucode
345
317
  subcommand "cache", Cache
346
318
 
347
319
  # ─────────────── build ───────────────
348
- desc "build [VERSION]", "Full pipeline: fetch + parse + (optional) glyphs + site"
320
+ desc "build [VERSION]", "Full pipeline: fetch + parse + site"
349
321
  option :to, type: :string, default: "./output"
350
322
  option :site, type: :string, default: nil, desc: "Build the site here (skipped if nil)"
351
- option :monolith, type: :string, default: "CodeCharts.pdf"
352
323
  option :force_fetch, type: :boolean, default: false
353
- option :include_glyphs, type: :boolean, default: false,
354
- desc: "Opt into the experimental v0.1 glyph step"
355
324
  def build(version = nil)
356
325
  result = Commands::BuildCommand.new.call(
357
326
  version,
358
327
  output_root: options[:to],
359
328
  site_root: options[:site],
360
- monolith_path: options[:monolith],
361
329
  force_fetch: options[:force_fetch],
362
- include_glyphs: options[:include_glyphs],
363
- warn: $stderr,
364
330
  )
365
331
  puts JSON.pretty_generate(result)
366
332
  end
@@ -2,14 +2,6 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
- require "ucode/error"
6
- require "ucode/glyphs/embedded_fonts/catalog"
7
- require "ucode/glyphs/embedded_fonts/renderer"
8
- require "ucode/glyphs/embedded_fonts/source"
9
- require "ucode/glyphs/resolver"
10
- require "ucode/glyphs/sources/pillar1_embedded_tounicode"
11
- require "ucode/glyphs/sources/tier1_real_font"
12
-
13
5
  module Ucode
14
6
  module CodeChart
15
7
  # Walks every assigned codepoint in a block and returns one
@@ -108,7 +100,7 @@ module Ucode
108
100
  end
109
101
 
110
102
  def embedded_pillar_sources
111
- embedded_source = Glyphs::EmbeddedFonts::Source.new(
103
+ embedded_source = Glyphs::EmbeddedFonts::PdfLocation.new(
112
104
  pdf: @pdf_path, cache_dir: @cache_dir,
113
105
  )
114
106
  catalog = Glyphs::EmbeddedFonts::Catalog.new(embedded_source)
@@ -47,7 +47,7 @@ module Ucode
47
47
  # @param ucd_version [String, nil] UCD version to stamp on
48
48
  # provenance. nil = resolved via {VersionResolver.resolve(nil)}.
49
49
  # @param cache_dir [Pathname, String, nil] font-stream cache
50
- # directory for the EmbeddedFonts::Source.
50
+ # directory for the EmbeddedFonts::PdfLocation.
51
51
  # @param now [Time, nil] timestamp override (for tests).
52
52
  # @param pillar3_source, tier1_sources: forwarded to the Extractor.
53
53
  def initialize(output_root:, pdf_path:, ucd_version: nil,
@@ -8,36 +8,23 @@ require "ucode/version_resolver"
8
8
  module Ucode
9
9
  module Commands
10
10
  # `ucode build` — full pipeline: fetch (ucd + unihan + charts) →
11
- # parse → (optional) glyphs → (optional) site. Resumable: each step
12
- # is idempotent and safe to re-run.
11
+ # parse → site. Resumable: each step is idempotent and safe to re-run.
13
12
  #
14
13
  # Resolves the version intent once at the top and threads the
15
- # resolved string through every sub-command. See Candidate 4 of the
16
- # 2026-06-29 architecture review.
17
- #
18
- # **Glyph step is opt-in as of v0.1** because the SVG cell extractor
19
- # is still experimental. Pass `include_glyphs: true` to enable it;
20
- # otherwise the glyphs step is recorded as skipped.
14
+ # resolved string through every sub-command.
21
15
  class BuildCommand
22
16
  # @param version_intent [nil, :default, :latest, String]
23
17
  # @param output_root [String, Pathname]
24
18
  # @param site_root [String, Pathname, nil] if nil, skip site build
25
- # @param monolith_path [String, Pathname, nil] CodeCharts.pdf fallback
26
19
  # @param force_fetch [Boolean] re-download sources
27
- # @param include_glyphs [Boolean] opt into the experimental glyph
28
- # step (default false)
29
- # @param warn [IO, nil] forwarded to GlyphsCommand when enabled
30
20
  # @return [Hash] aggregated step results
31
21
  def call(version_intent, output_root:, site_root: nil,
32
- monolith_path: nil, force_fetch: false,
33
- include_glyphs: false, warn: nil)
22
+ force_fetch: false)
34
23
  version = VersionResolver.resolve(version_intent)
35
24
  steps = {}
36
25
 
37
26
  steps[:fetch] = run_fetch(version, force: force_fetch)
38
27
  steps[:parse] = ParseCommand.new.call(version, output_root: output_root)
39
- steps[:glyphs] = run_glyphs(version, output_root, monolith_path,
40
- include_glyphs: include_glyphs, warn: warn)
41
28
  steps[:site] = run_site(output_root, site_root) if site_root
42
29
 
43
30
  { version: version, steps: steps }
@@ -54,16 +41,6 @@ module Ucode
54
41
  }
55
42
  end
56
43
 
57
- def run_glyphs(version, output_root, monolith_path, include_glyphs:, warn:)
58
- GlyphsCommand.new.call(
59
- version,
60
- output_root: output_root,
61
- monolith_path: monolith_path || "CodeCharts.pdf",
62
- include_glyphs: include_glyphs,
63
- warn: warn,
64
- )
65
- end
66
-
67
44
  def run_site(output_root, site_root)
68
45
  SiteCommand.new.build(output_root: output_root, site_root: site_root)
69
46
  end
@@ -20,10 +20,7 @@ module Ucode
20
20
  # `index.json` + `glyph.svg` atomically, accumulate per-tier +
21
21
  # per-block stats, and emit `output/build-report.json`.
22
22
  #
23
- # This is the v0.2 replacement for the v0.1 cell-extractor pipeline
24
- # in {GlyphsCommand}. The two coexist until the v0.1 pipeline is
25
- # removed (TODOs 17-19); CanonicalBuildCommand is the path forward
26
- # for production dataset runs.
23
+ # This is the production path for dataset runs.
27
24
  #
28
25
  # == Pre-conditions (per TODO 21)
29
26
  #
@@ -121,10 +118,10 @@ module Ucode
121
118
  end
122
119
 
123
120
  def build_resolver(version, source_config_path)
124
- database = Database.open(version)
125
- config = Glyphs::SourceConfig.new(path: source_config_path_or_default(source_config_path))
126
- builder = Glyphs::SourceBuilder.new(config: config, database: database)
127
- Glyphs::Resolver.new(sources: builder.tier1_sources(install: false))
121
+ Glyphs::ResolverFactory.build(
122
+ version: version,
123
+ source_config_path: source_config_path,
124
+ )
128
125
  end
129
126
 
130
127
  def source_config_path_or_default(path)
@@ -126,9 +126,11 @@ module Ucode
126
126
  end
127
127
 
128
128
  def build_resolver(_version, config_path, database)
129
- config = Glyphs::SourceConfig.new(path: config_path)
130
- builder = Glyphs::SourceBuilder.new(config: config, database: database)
131
- Glyphs::Resolver.new(sources: builder.tier1_sources(install: false))
129
+ Glyphs::ResolverFactory.build(
130
+ version: _version,
131
+ source_config_path: config_path,
132
+ database: database,
133
+ )
132
134
  end
133
135
 
134
136
  def codepoint_enum(version)
@@ -9,7 +9,6 @@ module Ucode
9
9
  module Commands
10
10
  autoload :FetchCommand, "ucode/commands/fetch"
11
11
  autoload :ParseCommand, "ucode/commands/parse"
12
- autoload :GlyphsCommand, "ucode/commands/glyphs"
13
12
  autoload :SiteCommand, "ucode/commands/site"
14
13
  autoload :LookupCommand, "ucode/commands/lookup"
15
14
  autoload :CacheCommand, "ucode/commands/cache"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Bidirectional behaviour: mirroring glyph and paired bracket info.
7
+ module Bidi
8
+ class << self
9
+ # @param cp [Ucode::Models::CodePoint]
10
+ # @param indices [Ucode::Coordinator::Indices]
11
+ def enrich(cp, indices)
12
+ mirroring = indices.bidi_mirroring[cp.cp]
13
+ brackets = indices.bidi_brackets[cp.cp]
14
+ return unless mirroring || brackets
15
+
16
+ cp.bidi ||= Ucode::Models::CodePoint::Bidi.new
17
+ apply_mirroring(cp, mirroring) if mirroring
18
+ apply_brackets(cp, brackets) if brackets
19
+ end
20
+
21
+ private
22
+
23
+ def apply_mirroring(cp, mirroring)
24
+ cp.bidi.mirroring_glyph_id = mirroring.mirrored_id
25
+ end
26
+
27
+ def apply_brackets(cp, brackets)
28
+ cp.bidi.paired_bracket_type = brackets.type
29
+ cp.bidi.paired_bracket_id = brackets.paired_id
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Binary properties: DerivedCoreProperties (core) + PropList (extra).
7
+ # Both contribute to the same `cp.binary_properties` array.
8
+ module Binary
9
+ class << self
10
+ def enrich(cp, indices)
11
+ assign_core(cp, indices)
12
+ assign_extra(cp, indices)
13
+ end
14
+
15
+ private
16
+
17
+ def assign_core(cp, indices)
18
+ records = indices.binary_properties[cp.cp]
19
+ return unless records && !records.empty?
20
+
21
+ cp.binary_properties = records.map(&:property_short)
22
+ end
23
+
24
+ # PropList carries binary properties beyond DerivedCoreProperties
25
+ # (White_Space, Hyphen, Variation_Selector, etc.). Merge into the
26
+ # same binary_properties list, deduped.
27
+ def assign_extra(cp, indices)
28
+ extras = RangeLookup.all_range_values(cp.cp, indices.extra_binary_properties)
29
+ return if extras.empty?
30
+
31
+ cp.binary_properties.concat(extras)
32
+ cp.binary_properties.uniq!
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Context-sensitive case mappings and case folding for comparison.
7
+ module Casing
8
+ class << self
9
+ def enrich(cp, indices)
10
+ assign_special_casing(cp, indices)
11
+ assign_case_folding(cp, indices)
12
+ end
13
+
14
+ private
15
+
16
+ # NOTE: do not uniq the *_ids arrays — a mapping like U+00DF → "SS"
17
+ # legitimately contains two U+0053 entries and they must be
18
+ # preserved in order. Conditions, by contrast, are categorical
19
+ # tags (Final_Sigma, tr, After_I) and deduping them is correct.
20
+ def assign_special_casing(cp, indices)
21
+ rules = indices.special_casing[cp.cp]
22
+ return unless rules && !rules.empty?
23
+
24
+ cp.casing ||= Ucode::Models::CodePoint::Casing.new
25
+ apply_casing_rules(cp.casing, rules)
26
+ end
27
+
28
+ def apply_casing_rules(casing, rules)
29
+ casing.full_upper_ids = rules.flat_map(&:upper_ids)
30
+ casing.full_lower_ids = rules.flat_map(&:lower_ids)
31
+ casing.full_title_ids = rules.flat_map(&:title_ids)
32
+ casing.conditions = rules.flat_map(&:conditions).uniq
33
+ end
34
+
35
+ def assign_case_folding(cp, indices)
36
+ rules = indices.case_folding[cp.cp]
37
+ return unless rules && !rules.empty?
38
+
39
+ cp.case_folding ||= Ucode::Models::CodePoint::CaseFolding.new
40
+ rules.each { |rule| apply_folding_rule(cp, rule) }
41
+ end
42
+
43
+ def apply_folding_rule(cp, rule)
44
+ case rule.status
45
+ when "C" then cp.case_folding.common_id = rule.mapping_ids.first
46
+ when "S" then cp.case_folding.simple_id = rule.mapping_ids.first
47
+ when "T" then cp.case_folding.turkic_id = rule.mapping_ids.first
48
+ when "F" then cp.case_folding.full_ids = rule.mapping_ids
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # CJK-specific data: Unihan readings, KangXi radical mapping,
7
+ # Hangul syllable type.
8
+ module CJK
9
+ class << self
10
+ def enrich(cp, indices)
11
+ assign_unihan(cp, indices)
12
+ assign_cjk_radical(cp, indices)
13
+ assign_hangul(cp, indices)
14
+ end
15
+
16
+ private
17
+
18
+ def assign_unihan(cp, indices)
19
+ entry = indices.unihan[cp.cp]
20
+ return unless entry
21
+
22
+ cp.unihan = entry
23
+ end
24
+
25
+ def assign_cjk_radical(cp, indices)
26
+ radicals = indices.cjk_radicals[cp.id]
27
+ return unless radicals && !radicals.empty?
28
+
29
+ radicals.each do |radical|
30
+ cp.relationships << Ucode::Models::Relationship::CrossReference.new(
31
+ target_ids: [radical.cjk_radical_id],
32
+ description: "KangXi radical ##{radical.radical_number}",
33
+ source: "cjk_radicals",
34
+ )
35
+ end
36
+ end
37
+
38
+ def assign_hangul(cp, indices)
39
+ tuple = RangeLookup.find_in_range(cp.cp, indices.hangul_syllable_type)
40
+ return unless tuple
41
+
42
+ cp.hangul ||= Ucode::Models::CodePoint::HangulSyllable.new
43
+ cp.hangul.type = tuple.value
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Display layout properties: Line Break class, East Asian Width,
7
+ # Vertical Orientation. All three are range+value files, looked up
8
+ # via bsearch on sorted arrays of ExtractedProperties::Tuple.
9
+ module Display
10
+ class << self
11
+ def enrich(cp, indices)
12
+ lb = lookup_value(cp, indices.line_break)
13
+ eaw = lookup_value(cp, indices.east_asian_width)
14
+ vo = lookup_value(cp, indices.vertical_orientation)
15
+ return if lb.nil? && eaw.nil? && vo.nil?
16
+
17
+ cp.display ||= Ucode::Models::CodePoint::Display.new
18
+ apply_values(cp.display, lb, eaw, vo)
19
+ end
20
+
21
+ private
22
+
23
+ def lookup_value(cp, ranges)
24
+ RangeLookup.find_in_range(cp.cp, ranges)&.value
25
+ end
26
+
27
+ def apply_values(display, lb, eaw, vo)
28
+ display.line_break_class = lb if lb
29
+ display.east_asian_width = eaw if eaw
30
+ display.vertical_orientation = vo if vo
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Emoji property bundle. Each Emoji_* property from emoji-data.txt
7
+ # flips the matching boolean on the Emoji sub-model.
8
+ module Emoji
9
+ class << self
10
+ def enrich(cp, indices)
11
+ return unless RangeLookup.find_in_range(cp.cp, indices.emoji_properties)
12
+
13
+ props = RangeLookup.all_range_values(cp.cp, indices.emoji_properties)
14
+ return if props.empty?
15
+
16
+ cp.emoji ||= Ucode::Models::CodePoint::Emoji.new
17
+ props.each { |prop| apply_property(cp, prop) }
18
+ end
19
+
20
+ private
21
+
22
+ def apply_property(cp, prop)
23
+ case prop
24
+ when "Emoji" then cp.emoji.is_emoji = true
25
+ when "Emoji_Presentation" then cp.emoji.is_presentation_default = true
26
+ when "Emoji_Modifier" then cp.emoji.is_modifier = true
27
+ when "Emoji_Modifier_Base" then cp.emoji.is_base = true
28
+ when "Emoji_Component" then cp.emoji.is_component = true
29
+ when "Extended_Pictographic" then cp.emoji.is_extended_pictographic = true
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Basic identity properties: primary script, script extensions,
7
+ # and the Unicode version when the codepoint was introduced.
8
+ module Identity
9
+ class << self
10
+ def enrich(cp, indices)
11
+ assign_script(cp, indices)
12
+ assign_script_extensions(cp, indices)
13
+ assign_age(cp, indices)
14
+ end
15
+
16
+ private
17
+
18
+ def assign_script(cp, indices)
19
+ script = RangeLookup.find_in_range(cp.cp, indices.scripts)
20
+ return unless script
21
+
22
+ cp.script_code = script.code || script.name
23
+ end
24
+
25
+ def assign_script_extensions(cp, indices)
26
+ tuples = indices.script_extensions[cp.cp]
27
+ return unless tuples && !tuples.empty?
28
+
29
+ tuples.each { |tuple| cp.script_extensions << tuple.script_code }
30
+ end
31
+
32
+ def assign_age(cp, indices)
33
+ record = indices.derived_age[cp.cp]
34
+ return unless record
35
+
36
+ cp.age = record.age
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Indic script shaping categories: positional and syllabic.
7
+ module Indic
8
+ class << self
9
+ def enrich(cp, indices)
10
+ positional = lookup_value(cp, indices.indic_positional)
11
+ syllabic = lookup_value(cp, indices.indic_syllabic)
12
+ return if positional.nil? && syllabic.nil?
13
+
14
+ cp.indic ||= Ucode::Models::CodePoint::Indic.new
15
+ apply_values(cp.indic, positional, syllabic)
16
+ end
17
+
18
+ private
19
+
20
+ def lookup_value(cp, ranges)
21
+ RangeLookup.find_in_range(cp.cp, ranges)&.value
22
+ end
23
+
24
+ def apply_values(indic, positional, syllabic)
25
+ indic.positional_category = positional if positional
26
+ indic.syllabic_category = syllabic if syllabic
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # Human-curated name annotations: cross-references, informal aliases,
7
+ # footnotes from NamesList.txt; formal name aliases from
8
+ # NameAliases.txt; standardized variation sequences.
9
+ module Names
10
+ class << self
11
+ def enrich(cp, indices)
12
+ assign_names_list(cp, indices)
13
+ assign_name_aliases(cp, indices)
14
+ assign_standardized_variants(cp, indices)
15
+ end
16
+
17
+ private
18
+
19
+ def assign_names_list(cp, indices)
20
+ entry = indices.names_list[cp.cp]
21
+ return unless entry
22
+
23
+ cp.names_list = entry
24
+ cp.relationships.concat(entry.cross_references)
25
+ cp.relationships.concat(entry.sample_sequences)
26
+ cp.relationships.concat(entry.compatibility_equivalents)
27
+ cp.relationships.concat(entry.informal_aliases)
28
+ cp.relationships.concat(entry.footnotes)
29
+ end
30
+
31
+ def assign_name_aliases(cp, indices)
32
+ aliases = indices.name_aliases[cp.cp]
33
+ return unless aliases && !aliases.empty?
34
+
35
+ aliases.each do |alias_record|
36
+ cp.relationships << Ucode::Models::Relationship::InformalAlias.new(
37
+ description: alias_record.text,
38
+ source: "name_aliases",
39
+ )
40
+ end
41
+ end
42
+
43
+ def assign_standardized_variants(cp, indices)
44
+ variants = indices.standardized_variants[cp.id]
45
+ return unless variants && !variants.empty?
46
+
47
+ cp.standardized_variants = variants
48
+ variants.each { |v| add_variant_relationship(cp, v) }
49
+ end
50
+
51
+ def add_variant_relationship(cp, variant)
52
+ cp.relationships << Ucode::Models::Relationship::VariationSequence.new(
53
+ target_ids: [variant.base_id, variant.variation_selector_id],
54
+ description: variant.description,
55
+ contexts: variant.contexts,
56
+ source: "standardized_variants",
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ucode
4
+ class Coordinator
5
+ module Enrichment
6
+ # UAX #29 text segmentation: Grapheme / Word / Sentence break class.
7
+ module Segmentation
8
+ class << self
9
+ def enrich(cp, indices)
10
+ grapheme = lookup_value(cp, indices.grapheme_break)
11
+ word = lookup_value(cp, indices.word_break)
12
+ sentence = lookup_value(cp, indices.sentence_break)
13
+ return if grapheme.nil? && word.nil? && sentence.nil?
14
+
15
+ cp.break_segmentation ||= Ucode::Models::CodePoint::BreakSegmentation.new
16
+ apply_values(cp.break_segmentation, grapheme, word, sentence)
17
+ end
18
+
19
+ private
20
+
21
+ def lookup_value(cp, ranges)
22
+ RangeLookup.find_in_range(cp.cp, ranges)&.value
23
+ end
24
+
25
+ def apply_values(seg, grapheme, word, sentence)
26
+ seg.grapheme = grapheme if grapheme
27
+ seg.word = word if word
28
+ seg.sentence = sentence if sentence
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end