ucode 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +72 -0
- data/TODO.full/00-README.md +116 -0
- data/TODO.full/01-panglyph-vision.md +112 -0
- data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
- data/TODO.full/03-panglyph-font-builder.md +201 -0
- data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
- data/TODO.full/05-ucode-0-1-1-release.md +139 -0
- data/TODO.full/06-fontisan-remove-audit.md +142 -0
- data/TODO.full/07-fontisan-remove-ucd.md +125 -0
- data/TODO.full/08-archive-private-bin-build.md +143 -0
- data/TODO.full/09-archive-public-structure.md +164 -0
- data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
- data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
- data/TODO.full/12-implementation-order.md +216 -0
- data/TODO.full/13-fontisan-font-writer-api.md +189 -0
- data/TODO.full/14-fontisan-table-writers.md +66 -0
- data/TODO.full/15-panglyph-builder-real.md +82 -0
- data/TODO.full/16-archive-public-sync-workflows.md +167 -0
- data/TODO.full/17-fontist-org-font-picker.md +73 -0
- data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
- data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
- data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
- data/TODO.new/00-README.md +30 -0
- data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
- data/TODO.new/24-universal-glyph-set-build.md +189 -0
- data/TODO.new/25-font-audit-against-universal-set.md +195 -0
- data/TODO.new/26-missing-glyph-reporter.md +189 -0
- data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
- data/TODO.new/28-implementation-order-update.md +187 -0
- data/TODO.new/29-universal-set-curation-uc17.md +312 -0
- data/TODO.new/30-tier1-font-acquisition.md +241 -0
- data/TODO.new/31-universal-set-production-build.md +205 -0
- data/TODO.new/32-uc17-coverage-matrix.md +165 -0
- data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
- data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
- data/TODO.new/35-universal-set-production-run.md +160 -0
- data/TODO.new/36-per-font-coverage-audit.md +145 -0
- data/TODO.new/37-coverage-highlight-reporter.md +125 -0
- data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
- data/TODO.new/39-implementation-order-update-32-38.md +258 -0
- data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
- data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
- data/config/specialist_fonts.yml +102 -0
- data/config/unicode17_tier1_fonts.yml +42 -0
- data/config/unicode17_universal_glyph_set.yml +293 -0
- data/lib/ucode/audit/block_aggregator.rb +57 -29
- data/lib/ucode/audit/browser/face_page.rb +128 -0
- data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
- data/lib/ucode/audit/browser/library_page.rb +74 -0
- data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
- data/lib/ucode/audit/browser/template.rb +47 -0
- data/lib/ucode/audit/browser/templates/face.css +200 -0
- data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
- data/lib/ucode/audit/browser/templates/face.js +298 -0
- data/lib/ucode/audit/browser/templates/library.css +119 -0
- data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
- data/lib/ucode/audit/browser/templates/library.js +99 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
- data/lib/ucode/audit/browser.rb +32 -0
- data/lib/ucode/audit/context.rb +27 -1
- data/lib/ucode/audit/coverage_reference.rb +103 -0
- data/lib/ucode/audit/differ.rb +121 -0
- data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
- data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
- data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
- data/lib/ucode/audit/emitter/face_directory.rb +212 -0
- data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
- data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
- data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
- data/lib/ucode/audit/emitter/paths.rb +312 -0
- data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
- data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
- data/lib/ucode/audit/emitter.rb +29 -0
- data/lib/ucode/audit/extractors/aggregations.rb +31 -2
- data/lib/ucode/audit/face_auditor.rb +86 -0
- data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
- data/lib/ucode/audit/formatters/audit_text.rb +411 -0
- data/lib/ucode/audit/formatters/color.rb +48 -0
- data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
- data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
- data/lib/ucode/audit/formatters.rb +23 -0
- data/lib/ucode/audit/library_aggregator.rb +86 -0
- data/lib/ucode/audit/library_auditor.rb +105 -0
- data/lib/ucode/audit/reference_factory.rb +66 -0
- data/lib/ucode/audit/release/emitter.rb +152 -0
- data/lib/ucode/audit/release/face_card.rb +93 -0
- data/lib/ucode/audit/release/formula_audits.rb +50 -0
- data/lib/ucode/audit/release/library_index_builder.rb +78 -0
- data/lib/ucode/audit/release/manifest_builder.rb +127 -0
- data/lib/ucode/audit/release.rb +42 -0
- data/lib/ucode/audit/ucd_only_reference.rb +81 -0
- data/lib/ucode/audit/universal_set_reference.rb +136 -0
- data/lib/ucode/audit.rb +32 -0
- data/lib/ucode/cli.rb +366 -41
- data/lib/ucode/commands/audit/browser_command.rb +82 -0
- data/lib/ucode/commands/audit/collection_command.rb +103 -0
- data/lib/ucode/commands/audit/compare_command.rb +188 -0
- data/lib/ucode/commands/audit/font_command.rb +140 -0
- data/lib/ucode/commands/audit/library_command.rb +87 -0
- data/lib/ucode/commands/audit.rb +19 -0
- data/lib/ucode/commands/block_feed.rb +73 -0
- data/lib/ucode/commands/build.rb +4 -0
- data/lib/ucode/commands/canonical_build.rb +137 -0
- data/lib/ucode/commands/fetch.rb +49 -15
- data/lib/ucode/commands/glyphs.rb +25 -67
- data/lib/ucode/commands/lookup.rb +11 -11
- data/lib/ucode/commands/parse.rb +7 -5
- data/lib/ucode/commands/release.rb +114 -0
- data/lib/ucode/commands/universal_set.rb +207 -0
- data/lib/ucode/commands.rb +5 -0
- data/lib/ucode/coordinator/indices.rb +49 -2
- data/lib/ucode/coordinator.rb +138 -5
- data/lib/ucode/error.rb +30 -2
- data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
- data/lib/ucode/fetch/font_fetcher.rb +16 -0
- data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
- data/lib/ucode/fetch.rb +7 -3
- data/lib/ucode/glyphs/pipeline.rb +106 -0
- data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
- data/lib/ucode/glyphs/real_fonts.rb +1 -0
- data/lib/ucode/glyphs/resolver.rb +62 -0
- data/lib/ucode/glyphs/source.rb +48 -0
- data/lib/ucode/glyphs/source_builder.rb +61 -0
- data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
- data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
- data/lib/ucode/glyphs/source_config.rb +104 -0
- data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
- data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
- data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
- data/lib/ucode/glyphs/sources.rb +20 -0
- data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
- data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
- data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
- data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
- data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
- data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
- data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
- data/lib/ucode/glyphs/universal_set.rb +45 -0
- data/lib/ucode/glyphs.rb +7 -0
- data/lib/ucode/models/audit/baseline.rb +6 -0
- data/lib/ucode/models/audit/block_summary.rb +7 -0
- data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
- data/lib/ucode/models/audit/release_face.rb +42 -0
- data/lib/ucode/models/audit/release_formula.rb +33 -0
- data/lib/ucode/models/audit/release_manifest.rb +43 -0
- data/lib/ucode/models/audit/release_universal_set.rb +37 -0
- data/lib/ucode/models/audit.rb +9 -0
- data/lib/ucode/models/block.rb +2 -0
- data/lib/ucode/models/build_report.rb +109 -0
- data/lib/ucode/models/codepoint/glyph.rb +42 -0
- data/lib/ucode/models/codepoint.rb +3 -0
- data/lib/ucode/models/glyph_source.rb +86 -0
- data/lib/ucode/models/glyph_source_map.rb +138 -0
- data/lib/ucode/models/specialist_font.rb +70 -0
- data/lib/ucode/models/specialist_font_manifest.rb +48 -0
- data/lib/ucode/models/unihan_entry.rb +81 -9
- data/lib/ucode/models/unihan_field.rb +21 -0
- data/lib/ucode/models/universal_set_entry.rb +47 -0
- data/lib/ucode/models/universal_set_manifest.rb +78 -0
- data/lib/ucode/models/validation_report.rb +99 -0
- data/lib/ucode/models.rb +9 -0
- data/lib/ucode/parsers/named_sequences.rb +5 -5
- data/lib/ucode/parsers/unihan.rb +50 -19
- data/lib/ucode/repo/aggregate_writer.rb +82 -288
- data/lib/ucode/repo/block_feed_emitter.rb +153 -0
- data/lib/ucode/repo/build_report_accumulator.rb +138 -0
- data/lib/ucode/repo/build_report_writer.rb +46 -0
- data/lib/ucode/repo/build_validator.rb +229 -0
- data/lib/ucode/repo/codepoint_writer.rb +50 -1
- data/lib/ucode/repo/paths.rb +8 -0
- data/lib/ucode/repo/writers/blocks_writer.rb +73 -0
- data/lib/ucode/repo/writers/enums_writer.rb +38 -0
- data/lib/ucode/repo/writers/indexes_writer.rb +53 -0
- data/lib/ucode/repo/writers/manifest_writer.rb +78 -0
- data/lib/ucode/repo/writers/named_sequences_writer.rb +47 -0
- data/lib/ucode/repo/writers/planes_writer.rb +82 -0
- data/lib/ucode/repo/writers/relationships_writer.rb +71 -0
- data/lib/ucode/repo/writers/scripts_writer.rb +54 -0
- data/lib/ucode/repo/writers.rb +20 -0
- data/lib/ucode/repo.rb +5 -0
- data/lib/ucode/version.rb +1 -1
- data/schema/block-feed.output.schema.yml +134 -0
- metadata +158 -4
- data/Gemfile.lock +0 -406
data/lib/ucode/parsers/unihan.rb
CHANGED
|
@@ -37,12 +37,32 @@ module Ucode
|
|
|
37
37
|
Unihan_OtherMappings.txt
|
|
38
38
|
].freeze
|
|
39
39
|
|
|
40
|
+
# Filename → category symbol. The parser tags every Record
|
|
41
|
+
# with the category derived from its source file, so consumers
|
|
42
|
+
# (Coordinator → UnihanEntry) don't need to know the mapping.
|
|
43
|
+
# Unicode does not reorganize files across versions, so this
|
|
44
|
+
# mapping is stable without per-field hardcoding.
|
|
45
|
+
FILE_TO_CATEGORY = {
|
|
46
|
+
"Unihan_DictionaryIndices.txt" => :dictionary_indices,
|
|
47
|
+
"Unihan_DictionaryLikeData.txt" => :dictionary_like_data,
|
|
48
|
+
"Unihan_IRGSources.txt" => :irg_sources,
|
|
49
|
+
"Unihan_NumericValues.txt" => :numeric_values,
|
|
50
|
+
"Unihan_RadicalStrokeCounts.txt" => :radical_stroke_counts,
|
|
51
|
+
"Unihan_Readings.txt" => :readings,
|
|
52
|
+
"Unihan_Variants.txt" => :variants,
|
|
53
|
+
"Unihan_OtherMappings.txt" => :other_mappings,
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
40
56
|
# Stream record: one Unihan line. Internal pipeline data — a Struct
|
|
41
57
|
# avoids lutaml-model ceremony for transient values. The final
|
|
42
58
|
# `UnihanEntry` model carries the merged, persisted shape. The
|
|
43
59
|
# member is `field_values` (not `values`) to avoid overriding
|
|
44
60
|
# `Struct#values` (the array of all member values).
|
|
45
|
-
|
|
61
|
+
#
|
|
62
|
+
# `category` is the symbol UnihanEntry uses to bucket the field
|
|
63
|
+
# into its category attribute (readings / variants / etc.). Set
|
|
64
|
+
# by `each_in_dir` from the source filename via FILE_TO_CATEGORY.
|
|
65
|
+
Record = Struct.new(:cp, :field, :field_values, :category, keyword_init: true) do
|
|
46
66
|
def cp_id
|
|
47
67
|
format("U+%04X", cp)
|
|
48
68
|
end
|
|
@@ -50,25 +70,16 @@ module Ucode
|
|
|
50
70
|
|
|
51
71
|
class << self
|
|
52
72
|
# Yields one Record per non-comment line in a single Unihan file.
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
# The caller must pass the source filename so the Record carries
|
|
74
|
+
# its category. Returns a lazy Enumerator when no block is given.
|
|
75
|
+
def each_record(path, filename: nil)
|
|
76
|
+
return enum_for(:each_record, path, filename: filename) unless block_given?
|
|
56
77
|
|
|
57
78
|
path_str = path.to_s
|
|
58
|
-
|
|
79
|
+
category = FILE_TO_CATEGORY.fetch(filename || File.basename(path_str), nil)
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
line = raw.chomp
|
|
63
|
-
next if line.empty? || line.start_with?("#")
|
|
64
|
-
|
|
65
|
-
begin
|
|
66
|
-
yield parse_line(line)
|
|
67
|
-
rescue MalformedLineError => e
|
|
68
|
-
e.context[:file] ||= path_str
|
|
69
|
-
e.context[:line] ||= lineno
|
|
70
|
-
raise
|
|
71
|
-
end
|
|
81
|
+
each_line_with_lineno(path_str) do |line, lineno|
|
|
82
|
+
yield tagged_record(line, category, path_str, lineno)
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
nil
|
|
@@ -76,7 +87,8 @@ module Ucode
|
|
|
76
87
|
|
|
77
88
|
# Iterates every known Unihan file in `dir`, yielding one Record
|
|
78
89
|
# per data line across all files. Missing files are silently
|
|
79
|
-
# skipped (incremental runs, partial downloads).
|
|
90
|
+
# skipped (incremental runs, partial downloads). Each Record
|
|
91
|
+
# carries its category so callers don't need to re-derive it.
|
|
80
92
|
def each_in_dir(dir)
|
|
81
93
|
return enum_for(:each_in_dir, dir) unless block_given?
|
|
82
94
|
|
|
@@ -85,7 +97,7 @@ module Ucode
|
|
|
85
97
|
path = dir_path.join(filename)
|
|
86
98
|
next unless path.exist?
|
|
87
99
|
|
|
88
|
-
each_record(path) { |record| yield record }
|
|
100
|
+
each_record(path, filename: filename) { |record| yield record }
|
|
89
101
|
end
|
|
90
102
|
|
|
91
103
|
nil
|
|
@@ -93,6 +105,25 @@ module Ucode
|
|
|
93
105
|
|
|
94
106
|
private
|
|
95
107
|
|
|
108
|
+
def each_line_with_lineno(path_str)
|
|
109
|
+
lineno = 0
|
|
110
|
+
File.foreach(path_str) do |raw|
|
|
111
|
+
lineno += 1
|
|
112
|
+
line = raw.chomp
|
|
113
|
+
next if line.empty? || line.start_with?("#")
|
|
114
|
+
|
|
115
|
+
yield line, lineno
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def tagged_record(line, category, path_str, lineno)
|
|
120
|
+
parse_line(line).tap { |r| r.category = category }
|
|
121
|
+
rescue MalformedLineError => e
|
|
122
|
+
e.context[:file] ||= path_str
|
|
123
|
+
e.context[:line] ||= lineno
|
|
124
|
+
raise
|
|
125
|
+
end
|
|
126
|
+
|
|
96
127
|
# Parses one TAB-separated Unihan data line into a Record. The
|
|
97
128
|
# `split("\t", 3)` limit preserves any tabs inside the value
|
|
98
129
|
# (defensive — real Unihan data does not contain them).
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
|
-
require "json"
|
|
5
|
-
require "time"
|
|
6
4
|
|
|
7
|
-
require "ucode/
|
|
8
|
-
require "ucode/repo/
|
|
9
|
-
require "ucode/repo/
|
|
5
|
+
require "ucode/repo/writers/planes_writer"
|
|
6
|
+
require "ucode/repo/writers/blocks_writer"
|
|
7
|
+
require "ucode/repo/writers/scripts_writer"
|
|
8
|
+
require "ucode/repo/writers/indexes_writer"
|
|
9
|
+
require "ucode/repo/writers/relationships_writer"
|
|
10
|
+
require "ucode/repo/writers/enums_writer"
|
|
11
|
+
require "ucode/repo/writers/named_sequences_writer"
|
|
12
|
+
require "ucode/repo/writers/manifest_writer"
|
|
10
13
|
|
|
11
14
|
module Ucode
|
|
12
15
|
module Repo
|
|
@@ -14,71 +17,35 @@ module Ucode
|
|
|
14
17
|
#
|
|
15
18
|
# output/planes/<n>.json
|
|
16
19
|
# output/blocks/<ID>.json
|
|
17
|
-
# output/blocks/index.json
|
|
20
|
+
# output/blocks/index.json
|
|
18
21
|
# output/scripts/<code>.json
|
|
19
|
-
# output/index/names.json
|
|
20
|
-
# output/index/labels.json
|
|
21
|
-
# output/index/codepoint_to_block.json
|
|
22
|
-
# output/relationships/*.json
|
|
23
|
-
# output/enums.json
|
|
22
|
+
# output/index/names.json
|
|
23
|
+
# output/index/labels.json
|
|
24
|
+
# output/index/codepoint_to_block.json
|
|
25
|
+
# output/relationships/*.json
|
|
26
|
+
# output/enums.json
|
|
24
27
|
# output/named_sequences/<slug>.json
|
|
25
28
|
# output/manifest.json
|
|
26
29
|
#
|
|
27
|
-
# **Single pass**: callers feed one CodePoint at a time via `#add
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
+
# **Single pass**: callers feed one CodePoint at a time via `#add`,
|
|
31
|
+
# which folds into the streaming accumulators. `#flush` then
|
|
32
|
+
# composes eight per-concern writer classes (one per output kind)
|
|
33
|
+
# and runs them in order. Adding a new aggregate = adding one
|
|
34
|
+
# writer class + one line here. See Candidate 5 of the 2026-06-29
|
|
35
|
+
# architecture review.
|
|
30
36
|
#
|
|
31
37
|
# **MECE**:
|
|
32
38
|
# - paths: `Repo::Paths`
|
|
33
39
|
# - atomic writes: `Repo::AtomicWrites`
|
|
34
|
-
# - stream aggregation: this class
|
|
40
|
+
# - stream aggregation: this class (the `#add` half)
|
|
41
|
+
# - per-concern writers: `Repo::Writers::*`
|
|
35
42
|
# - serialization: lutaml-model `to_yaml_hash` / `to_json`
|
|
36
43
|
class AggregateWriter
|
|
37
|
-
include AtomicWrites
|
|
38
|
-
|
|
39
|
-
# Static metadata for the 17 Unicode planes. Planes 4–13 are
|
|
40
|
-
# unassigned in Unicode 17; their entries use placeholder names.
|
|
41
|
-
PLANE_TABLE = {
|
|
42
|
-
0 => ["Basic Multilingual Plane", "BMP"],
|
|
43
|
-
1 => ["Supplementary Multilingual Plane", "SMP"],
|
|
44
|
-
2 => ["Supplementary Ideographic Plane", "SIP"],
|
|
45
|
-
3 => ["Tertiary Ideographic Plane", "TIP"],
|
|
46
|
-
4 => ["Unassigned Plane 4", "—"],
|
|
47
|
-
5 => ["Unassigned Plane 5", "—"],
|
|
48
|
-
6 => ["Unassigned Plane 6", "—"],
|
|
49
|
-
7 => ["Unassigned Plane 7", "—"],
|
|
50
|
-
8 => ["Unassigned Plane 8", "—"],
|
|
51
|
-
9 => ["Unassigned Plane 9", "—"],
|
|
52
|
-
10 => ["Unassigned Plane 10", "—"],
|
|
53
|
-
11 => ["Unassigned Plane 11", "—"],
|
|
54
|
-
12 => ["Unassigned Plane 12", "—"],
|
|
55
|
-
13 => ["Unassigned Plane 13", "—"],
|
|
56
|
-
14 => ["Supplementary Special-purpose Plane", "SSP"],
|
|
57
|
-
15 => ["Supplementary Private Use Area-A", "SPUA-A"],
|
|
58
|
-
16 => ["Supplementary Private Use Area-B", "SPUA-B"],
|
|
59
|
-
}.freeze
|
|
60
|
-
private_constant :PLANE_TABLE
|
|
61
|
-
|
|
62
|
-
# Coordinator::Indices fields paired with the file slug used
|
|
63
|
-
# under `output/relationships/`. Each field is a Hash<Integer,
|
|
64
|
-
# Record> or Hash<Integer, Array<Record>>.
|
|
65
|
-
RELATIONSHIP_SOURCES = {
|
|
66
|
-
special_casing: "special_casing",
|
|
67
|
-
case_folding: "case_folding",
|
|
68
|
-
bidi_mirroring: "bidi_mirroring",
|
|
69
|
-
bidi_brackets: "bidi_brackets",
|
|
70
|
-
cjk_radicals: "cjk_radicals",
|
|
71
|
-
standardized_variants: "standardized_variants",
|
|
72
|
-
name_aliases: "name_aliases",
|
|
73
|
-
}.freeze
|
|
74
|
-
private_constant :RELATIONSHIP_SOURCES
|
|
75
|
-
|
|
76
|
-
attr_reader :codepoint_count
|
|
77
|
-
|
|
78
44
|
# @param output_root [String, Pathname]
|
|
79
45
|
def initialize(output_root)
|
|
80
46
|
@output_root = Pathname.new(output_root)
|
|
81
47
|
@block_codepoint_ids = Hash.new { |h, k| h[k] = [] }
|
|
48
|
+
@block_ages = Hash.new { |h, k| h[k] = nil }
|
|
82
49
|
@script_codepoint_ids = Hash.new { |h, k| h[k] = [] }
|
|
83
50
|
@names_index = {}
|
|
84
51
|
@labels_index = {}
|
|
@@ -86,6 +53,8 @@ module Ucode
|
|
|
86
53
|
@codepoint_count = 0
|
|
87
54
|
end
|
|
88
55
|
|
|
56
|
+
attr_reader :codepoint_count
|
|
57
|
+
|
|
89
58
|
# Fold one CodePoint into the stream accumulators. No-ops if the
|
|
90
59
|
# cp has no block_id (it has no home in the output tree).
|
|
91
60
|
# @param cp [Ucode::Models::CodePoint]
|
|
@@ -94,6 +63,7 @@ module Ucode
|
|
|
94
63
|
return if cp.block_id.nil?
|
|
95
64
|
|
|
96
65
|
@block_codepoint_ids[cp.block_id] << cp.id
|
|
66
|
+
track_block_age(cp)
|
|
97
67
|
if cp.script_code
|
|
98
68
|
@script_codepoint_ids[cp.script_code] << cp.id
|
|
99
69
|
end
|
|
@@ -105,10 +75,8 @@ module Ucode
|
|
|
105
75
|
@codepoint_count += 1
|
|
106
76
|
end
|
|
107
77
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# `sc` subset of PropertyValueAliases; the full alias tables and
|
|
111
|
-
# the named sequences are passed through from the CLI/parsers).
|
|
78
|
+
# Compose the eight per-concern writers, run them in order, and
|
|
79
|
+
# return the total number of files written.
|
|
112
80
|
#
|
|
113
81
|
# @param ucd_version [String]
|
|
114
82
|
# @param indices [Ucode::Coordinator::Indices]
|
|
@@ -116,19 +84,40 @@ module Ucode
|
|
|
116
84
|
# @param property_value_aliases [Array<Ucode::Models::PropertyValueAlias>]
|
|
117
85
|
# @param named_sequences [Array<Ucode::Models::NamedSequence>]
|
|
118
86
|
# @param glyph_count [Integer]
|
|
119
|
-
# @return [Integer]
|
|
87
|
+
# @return [Integer]
|
|
120
88
|
def flush(ucd_version:, indices:, property_aliases: [],
|
|
121
89
|
property_value_aliases: [], named_sequences: [], glyph_count: 0)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
90
|
+
writers(ucd_version, indices, property_aliases, property_value_aliases,
|
|
91
|
+
named_sequences, glyph_count).sum(&:write)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private — exposed for testing.
|
|
95
|
+
def writers(ucd_version, indices, property_aliases,
|
|
96
|
+
property_value_aliases, named_sequences, glyph_count)
|
|
97
|
+
[
|
|
98
|
+
Writers::PlanesWriter.new(output_root: @output_root, blocks: indices.blocks),
|
|
99
|
+
Writers::BlocksWriter.new(output_root: @output_root,
|
|
100
|
+
blocks: indices.blocks,
|
|
101
|
+
block_codepoint_ids: @block_codepoint_ids,
|
|
102
|
+
block_ages: @block_ages),
|
|
103
|
+
Writers::ScriptsWriter.new(output_root: @output_root,
|
|
104
|
+
scripts: indices.scripts,
|
|
105
|
+
script_codepoint_ids: @script_codepoint_ids),
|
|
106
|
+
Writers::IndexesWriter.new(output_root: @output_root,
|
|
107
|
+
names: @names_index,
|
|
108
|
+
labels: @labels_index,
|
|
109
|
+
cp_to_block: @cp_to_block),
|
|
110
|
+
Writers::RelationshipsWriter.new(output_root: @output_root, indices: indices),
|
|
111
|
+
Writers::EnumsWriter.new(output_root: @output_root,
|
|
112
|
+
property_aliases: property_aliases,
|
|
113
|
+
property_value_aliases: property_value_aliases),
|
|
114
|
+
Writers::NamedSequencesWriter.new(output_root: @output_root,
|
|
115
|
+
named_sequences: named_sequences),
|
|
116
|
+
Writers::ManifestWriter.new(output_root: @output_root,
|
|
117
|
+
ucd_version: ucd_version,
|
|
118
|
+
codepoint_count: @codepoint_count,
|
|
119
|
+
glyph_count: glyph_count),
|
|
120
|
+
]
|
|
132
121
|
end
|
|
133
122
|
|
|
134
123
|
private
|
|
@@ -136,229 +125,34 @@ module Ucode
|
|
|
136
125
|
# ---- Per-codepoint accumulator helpers ---------------------------
|
|
137
126
|
|
|
138
127
|
def build_label(cp)
|
|
139
|
-
label = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
plane_block_ids = group_block_ids_by_plane(blocks)
|
|
147
|
-
count = 0
|
|
148
|
-
(0..16).each do |n|
|
|
149
|
-
path = Paths.plane_metadata_path(@output_root, n)
|
|
150
|
-
count += 1 if write_atomic(path, plane_payload(n, plane_block_ids[n] || []))
|
|
151
|
-
end
|
|
152
|
-
count
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def group_block_ids_by_plane(blocks)
|
|
156
|
-
blocks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |block, h|
|
|
157
|
-
h[block.plane_number] << block.id
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def plane_payload(plane_number, block_ids)
|
|
162
|
-
name, abbrev = PLANE_TABLE.fetch(plane_number)
|
|
163
|
-
range_first = plane_number * 0x10000
|
|
164
|
-
range_last = range_first + 0xFFFF
|
|
165
|
-
to_pretty_json(
|
|
166
|
-
"number" => plane_number,
|
|
167
|
-
"name" => name,
|
|
168
|
-
"abbrev" => abbrev,
|
|
169
|
-
"range_first" => range_first,
|
|
170
|
-
"range_last" => range_last,
|
|
171
|
-
"block_ids" => block_ids,
|
|
172
|
-
)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# ---- Block files -------------------------------------------------
|
|
176
|
-
|
|
177
|
-
def write_blocks(blocks)
|
|
178
|
-
count = blocks.sum do |block|
|
|
179
|
-
path = Paths.block_metadata_path(@output_root, block.id)
|
|
180
|
-
write_atomic(path, block_payload(block)) ? 1 : 0
|
|
181
|
-
end
|
|
182
|
-
count + write_blocks_index(blocks)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def write_blocks_index(blocks)
|
|
186
|
-
path = Paths.blocks_index_path(@output_root)
|
|
187
|
-
summary = blocks.map do |block|
|
|
188
|
-
{
|
|
189
|
-
"id" => block.id,
|
|
190
|
-
"name" => block.name,
|
|
191
|
-
"first_cp" => block.range_first,
|
|
192
|
-
"last_cp" => block.range_last,
|
|
193
|
-
"plane_number" => block.plane_number,
|
|
194
|
-
}
|
|
195
|
-
end
|
|
196
|
-
write_atomic(path, to_pretty_json(summary)) ? 1 : 0
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def block_payload(block)
|
|
200
|
-
to_pretty_json(
|
|
201
|
-
"id" => block.id,
|
|
202
|
-
"name" => block.name,
|
|
203
|
-
"range_first" => block.range_first,
|
|
204
|
-
"range_last" => block.range_last,
|
|
205
|
-
"plane_number" => block.plane_number,
|
|
206
|
-
"codepoint_ids" => (@block_codepoint_ids[block.id] || []),
|
|
207
|
-
)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# ---- Script files ------------------------------------------------
|
|
211
|
-
|
|
212
|
-
def write_scripts(scripts)
|
|
213
|
-
count = 0
|
|
214
|
-
scripts.group_by(&:code).each do |code, ranges|
|
|
215
|
-
next if code.nil? || code.empty?
|
|
216
|
-
|
|
217
|
-
path = Paths.script_metadata_path(@output_root, code)
|
|
218
|
-
count += 1 if write_atomic(path, script_payload(code, ranges))
|
|
219
|
-
end
|
|
220
|
-
count
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def script_payload(code, ranges)
|
|
224
|
-
to_pretty_json(
|
|
225
|
-
"code" => code,
|
|
226
|
-
"name" => ranges.first&.name,
|
|
227
|
-
"range_first" => ranges.map(&:range_first).min,
|
|
228
|
-
"range_last" => ranges.map(&:range_last).max,
|
|
229
|
-
"codepoint_ids" => (@script_codepoint_ids[code] || []),
|
|
230
|
-
)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# ---- Lookup indexes ---------------------------------------------
|
|
234
|
-
|
|
235
|
-
def write_indexes
|
|
236
|
-
count = 0
|
|
237
|
-
count += 1 if write_atomic(Paths.names_index_path(@output_root), to_pretty_json(@names_index))
|
|
238
|
-
count += 1 if write_atomic(Paths.labels_index_path(@output_root), to_pretty_json(@labels_index))
|
|
239
|
-
count += 1 if write_atomic(codepoint_to_block_path, to_pretty_json(@cp_to_block))
|
|
240
|
-
count
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def codepoint_to_block_path
|
|
244
|
-
Pathname(@output_root).join("index", "codepoint_to_block.json")
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# ---- Relationships ----------------------------------------------
|
|
248
|
-
|
|
249
|
-
def write_relationships(indices)
|
|
250
|
-
RELATIONSHIP_SOURCES.sum do |field, slug|
|
|
251
|
-
records = indices.public_send(field)
|
|
252
|
-
write_relationship_file(slug, records)
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def write_relationship_file(slug, records)
|
|
257
|
-
return 0 if records.nil? || records.empty?
|
|
258
|
-
|
|
259
|
-
path = Pathname(@output_root).join("relationships", "#{slug}.json")
|
|
260
|
-
write_atomic(path, relationship_payload(records)) ? 1 : 0
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# records is Hash<Integer, Record>, Hash<Integer, Array<Record>>,
|
|
264
|
-
# Hash<String, Record>, or Hash<String, Array<Record>>.
|
|
265
|
-
# Output: { "U+XXXX" => record.to_yaml_hash, ... } or
|
|
266
|
-
# { "U+XXXX" => [record.to_yaml_hash, ...], ... }
|
|
267
|
-
def relationship_payload(records)
|
|
268
|
-
payload = records.each_with_object({}) do |(key, value), h|
|
|
269
|
-
h[key_to_cp_id(key)] = serialize_value(value)
|
|
270
|
-
end
|
|
271
|
-
to_pretty_json(payload)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Indices that are keyed by Integer codepoint (most of them) get
|
|
275
|
-
# formatted into "U+XXXX". Indices keyed by string ids already
|
|
276
|
-
# (cjk_radicals by ideograph_id, standardized_variants by base_id)
|
|
277
|
-
# are passed through verbatim.
|
|
278
|
-
def key_to_cp_id(key)
|
|
279
|
-
key.is_a?(Integer) ? Paths.cp_id(key) : key
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def serialize_value(value)
|
|
283
|
-
return value.map { |v| serialize_one(v) } if value.is_a?(Array)
|
|
284
|
-
|
|
285
|
-
serialize_one(value)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def serialize_one(record)
|
|
289
|
-
record.to_yaml_hash
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# ---- Enums -------------------------------------------------------
|
|
293
|
-
|
|
294
|
-
def write_enums(property_aliases, property_value_aliases)
|
|
295
|
-
path = Pathname(@output_root).join("enums.json")
|
|
296
|
-
payload = {
|
|
297
|
-
"properties" => property_aliases.map(&:to_yaml_hash),
|
|
298
|
-
"property_values" => property_value_aliases.map(&:to_yaml_hash),
|
|
299
|
-
}
|
|
300
|
-
write_atomic(path, to_pretty_json(payload)) ? 1 : 0
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# ---- Named sequences --------------------------------------------
|
|
304
|
-
|
|
305
|
-
def write_named_sequences(named_sequences)
|
|
306
|
-
return 0 if named_sequences.nil? || named_sequences.empty?
|
|
307
|
-
|
|
308
|
-
dir = Pathname(@output_root).join("named_sequences")
|
|
309
|
-
named_sequences.sum do |ns|
|
|
310
|
-
path = dir.join("#{slug_for(ns)}.json")
|
|
311
|
-
write_atomic(path, ns.to_json(pretty: true)) ? 1 : 0
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# Slug derived from the name: downcase, non-alphanumerics → "_".
|
|
316
|
-
def slug_for(named_sequence)
|
|
317
|
-
named_sequence.name
|
|
318
|
-
.downcase
|
|
319
|
-
.gsub(/[^a-z0-9]+/, "_")
|
|
320
|
-
.gsub(/^_+|_+$/, "")
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# ---- Manifest ---------------------------------------------------
|
|
324
|
-
|
|
325
|
-
# Fields that define the manifest's semantic content. When these
|
|
326
|
-
# match the existing manifest on disk, we preserve the old
|
|
327
|
-
# `generated_at` so that re-runs are byte-idempotent (no rewrite
|
|
328
|
-
# unless something actually changed).
|
|
329
|
-
MANIFEST_CONTENT_KEYS = %w[
|
|
330
|
-
ucd_version codepoint_count glyph_count schema_version
|
|
331
|
-
].freeze
|
|
332
|
-
private_constant :MANIFEST_CONTENT_KEYS
|
|
333
|
-
|
|
334
|
-
def write_manifest(ucd_version:, glyph_count:)
|
|
335
|
-
path = Paths.manifest_path(@output_root)
|
|
336
|
-
content = {
|
|
337
|
-
"ucd_version" => ucd_version,
|
|
338
|
-
"codepoint_count" => @codepoint_count,
|
|
339
|
-
"glyph_count" => glyph_count,
|
|
340
|
-
"schema_version" => "1",
|
|
128
|
+
label = {
|
|
129
|
+
"name" => cp.name,
|
|
130
|
+
"gc" => cp.general_category,
|
|
131
|
+
"sc" => cp.script_code,
|
|
132
|
+
"cc" => cp.combining_class,
|
|
133
|
+
"bc" => cp.bidi&.bidi_class,
|
|
134
|
+
"mir" => cp.bidi&.is_mirrored ? true : nil,
|
|
341
135
|
}
|
|
342
|
-
|
|
343
|
-
payload = content.merge("generated_at" => ts)
|
|
344
|
-
write_atomic(path, to_pretty_json(payload)) ? 1 : 0
|
|
136
|
+
label.reject { |_, v| v.nil? }
|
|
345
137
|
end
|
|
346
138
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
139
|
+
# Per-block `age` is the earliest DerivedAge of any codepoint in
|
|
140
|
+
# the block, compared as a Gem::Version. Stored as the original
|
|
141
|
+
# string (e.g. "1.1", "17.0.0").
|
|
142
|
+
def track_block_age(cp)
|
|
143
|
+
return if cp.age.nil? || cp.age.empty?
|
|
350
144
|
|
|
351
|
-
|
|
352
|
-
|
|
145
|
+
current = @block_ages[cp.block_id]
|
|
146
|
+
@block_ages[cp.block_id] = if current.nil?
|
|
147
|
+
cp.age
|
|
148
|
+
else
|
|
149
|
+
min_age(current, cp.age)
|
|
150
|
+
end
|
|
353
151
|
end
|
|
354
152
|
|
|
355
|
-
def
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
JSON.parse(path.read)
|
|
359
|
-
rescue JSON::ParserError
|
|
360
|
-
nil
|
|
153
|
+
def min_age(a, b)
|
|
154
|
+
Gem::Version.new(a) < Gem::Version.new(b) ? a : b
|
|
361
155
|
end
|
|
362
156
|
end
|
|
363
157
|
end
|
|
364
|
-
end
|
|
158
|
+
end
|