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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ucode/repo/atomic_writes"
|
|
4
|
+
require "ucode/repo/paths"
|
|
5
|
+
|
|
6
|
+
module Ucode
|
|
7
|
+
module Repo
|
|
8
|
+
module Writers
|
|
9
|
+
# Writes `output/blocks/<ID>.json` for every block plus
|
|
10
|
+
# `output/blocks/index.json` as a summary.
|
|
11
|
+
#
|
|
12
|
+
# One of the eight per-concern writers split out from
|
|
13
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
14
|
+
class BlocksWriter
|
|
15
|
+
include AtomicWrites
|
|
16
|
+
|
|
17
|
+
# @param output_root [Pathname]
|
|
18
|
+
# @param blocks [Array<Ucode::Models::Block>]
|
|
19
|
+
# @param block_codepoint_ids [Hash{String => Array<String>}]
|
|
20
|
+
# block_id → sorted cp_id list, accumulated during the
|
|
21
|
+
# streaming pass
|
|
22
|
+
# @param block_ages [Hash{String => String}] block_id → earliest
|
|
23
|
+
# DerivedAge string; nil entries get written as nil
|
|
24
|
+
def initialize(output_root:, blocks:, block_codepoint_ids:, block_ages:)
|
|
25
|
+
@output_root = output_root
|
|
26
|
+
@blocks = blocks
|
|
27
|
+
@block_codepoint_ids = block_codepoint_ids
|
|
28
|
+
@block_ages = block_ages
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Integer] number of files written (one per block plus
|
|
32
|
+
# one for the index)
|
|
33
|
+
def write
|
|
34
|
+
count = @blocks.sum do |block|
|
|
35
|
+
block.age = @block_ages[block.id]
|
|
36
|
+
path = Paths.block_metadata_path(@output_root, block.id)
|
|
37
|
+
write_atomic(path, block_payload(block)) ? 1 : 0
|
|
38
|
+
end
|
|
39
|
+
count + write_blocks_index
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def write_blocks_index
|
|
45
|
+
path = Paths.blocks_index_path(@output_root)
|
|
46
|
+
summary = @blocks.map do |block|
|
|
47
|
+
{
|
|
48
|
+
"id" => block.id,
|
|
49
|
+
"name" => block.name,
|
|
50
|
+
"first_cp" => block.range_first,
|
|
51
|
+
"last_cp" => block.range_last,
|
|
52
|
+
"plane_number" => block.plane_number,
|
|
53
|
+
"age" => @block_ages[block.id],
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
write_atomic(path, to_pretty_json(summary)) ? 1 : 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def block_payload(block)
|
|
60
|
+
to_pretty_json(
|
|
61
|
+
"id" => block.id,
|
|
62
|
+
"name" => block.name,
|
|
63
|
+
"range_first" => block.range_first,
|
|
64
|
+
"range_last" => block.range_last,
|
|
65
|
+
"plane_number" => block.plane_number,
|
|
66
|
+
"age" => @block_ages[block.id],
|
|
67
|
+
"codepoint_ids" => (@block_codepoint_ids[block.id] || []),
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "ucode/repo/atomic_writes"
|
|
5
|
+
|
|
6
|
+
module Ucode
|
|
7
|
+
module Repo
|
|
8
|
+
module Writers
|
|
9
|
+
# Writes `output/enums.json` carrying the full property-alias
|
|
10
|
+
# and property-value-alias tables.
|
|
11
|
+
#
|
|
12
|
+
# One of the eight per-concern writers split out from
|
|
13
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
14
|
+
class EnumsWriter
|
|
15
|
+
include AtomicWrites
|
|
16
|
+
|
|
17
|
+
# @param output_root [Pathname]
|
|
18
|
+
# @param property_aliases [Array<Ucode::Models::PropertyAlias>]
|
|
19
|
+
# @param property_value_aliases [Array<Ucode::Models::PropertyValueAlias>]
|
|
20
|
+
def initialize(output_root:, property_aliases:, property_value_aliases:)
|
|
21
|
+
@output_root = output_root
|
|
22
|
+
@property_aliases = property_aliases
|
|
23
|
+
@property_value_aliases = property_value_aliases
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Integer] 1 if written, 0 otherwise
|
|
27
|
+
def write
|
|
28
|
+
path = Pathname(@output_root).join("enums.json")
|
|
29
|
+
payload = {
|
|
30
|
+
"properties" => @property_aliases.map(&:to_yaml_hash),
|
|
31
|
+
"property_values" => @property_value_aliases.map(&:to_yaml_hash),
|
|
32
|
+
}
|
|
33
|
+
write_atomic(path, to_pretty_json(payload)) ? 1 : 0
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "ucode/repo/atomic_writes"
|
|
5
|
+
require "ucode/repo/paths"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Repo
|
|
9
|
+
module Writers
|
|
10
|
+
# Writes the three lookup indexes:
|
|
11
|
+
#
|
|
12
|
+
# output/index/names.json (cp_id → name)
|
|
13
|
+
# output/index/labels.json (cp_id → {name, gc, sc, cc, bc, mir})
|
|
14
|
+
# output/index/codepoint_to_block.json (cp_id → block_id)
|
|
15
|
+
#
|
|
16
|
+
# One of the eight per-concern writers split out from
|
|
17
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
18
|
+
class IndexesWriter
|
|
19
|
+
include AtomicWrites
|
|
20
|
+
|
|
21
|
+
# @param output_root [Pathname]
|
|
22
|
+
# @param names [Hash{String => String}] cp_id → name
|
|
23
|
+
# @param labels [Hash{String => Hash}] cp_id → label fields
|
|
24
|
+
# @param cp_to_block [Hash{String => String}] cp_id → block_id
|
|
25
|
+
def initialize(output_root:, names:, labels:, cp_to_block:)
|
|
26
|
+
@output_root = output_root
|
|
27
|
+
@names = names
|
|
28
|
+
@labels = labels
|
|
29
|
+
@cp_to_block = cp_to_block
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer] number of index files written (always 3
|
|
33
|
+
# when the directory is reachable)
|
|
34
|
+
def write
|
|
35
|
+
count = 0
|
|
36
|
+
count += 1 if write_atomic(Paths.names_index_path(@output_root),
|
|
37
|
+
to_pretty_json(@names))
|
|
38
|
+
count += 1 if write_atomic(Paths.labels_index_path(@output_root),
|
|
39
|
+
to_pretty_json(@labels))
|
|
40
|
+
count += 1 if write_atomic(codepoint_to_block_path,
|
|
41
|
+
to_pretty_json(@cp_to_block))
|
|
42
|
+
count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def codepoint_to_block_path
|
|
48
|
+
Pathname(@output_root).join("index", "codepoint_to_block.json")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
require "ucode/repo/atomic_writes"
|
|
8
|
+
require "ucode/repo/paths"
|
|
9
|
+
|
|
10
|
+
module Ucode
|
|
11
|
+
module Repo
|
|
12
|
+
module Writers
|
|
13
|
+
# Writes `output/manifest.json`. The `generated_at` timestamp is
|
|
14
|
+
# preserved across no-op re-runs (same content keys → keep old
|
|
15
|
+
# timestamp) so the byte content is byte-idempotent.
|
|
16
|
+
#
|
|
17
|
+
# One of the eight per-concern writers split out from
|
|
18
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
19
|
+
class ManifestWriter
|
|
20
|
+
include AtomicWrites
|
|
21
|
+
|
|
22
|
+
# Fields that define the manifest's semantic content. When
|
|
23
|
+
# these match the existing manifest on disk, we preserve the
|
|
24
|
+
# old `generated_at` so re-runs are byte-idempotent.
|
|
25
|
+
CONTENT_KEYS = %w[
|
|
26
|
+
ucd_version codepoint_count glyph_count schema_version
|
|
27
|
+
].freeze
|
|
28
|
+
private_constant :CONTENT_KEYS
|
|
29
|
+
|
|
30
|
+
SCHEMA_VERSION = "1"
|
|
31
|
+
private_constant :SCHEMA_VERSION
|
|
32
|
+
|
|
33
|
+
# @param output_root [Pathname]
|
|
34
|
+
# @param ucd_version [String]
|
|
35
|
+
# @param codepoint_count [Integer]
|
|
36
|
+
# @param glyph_count [Integer]
|
|
37
|
+
def initialize(output_root:, ucd_version:, codepoint_count:, glyph_count:)
|
|
38
|
+
@output_root = output_root
|
|
39
|
+
@ucd_version = ucd_version
|
|
40
|
+
@codepoint_count = codepoint_count
|
|
41
|
+
@glyph_count = glyph_count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Integer] 1 if written, 0 otherwise
|
|
45
|
+
def write
|
|
46
|
+
path = Paths.manifest_path(@output_root)
|
|
47
|
+
content = {
|
|
48
|
+
"ucd_version" => @ucd_version,
|
|
49
|
+
"codepoint_count" => @codepoint_count,
|
|
50
|
+
"glyph_count" => @glyph_count,
|
|
51
|
+
"schema_version" => SCHEMA_VERSION,
|
|
52
|
+
}
|
|
53
|
+
ts = preserved_or_new_timestamp(path, content)
|
|
54
|
+
payload = content.merge("generated_at" => ts)
|
|
55
|
+
write_atomic(path, to_pretty_json(payload)) ? 1 : 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def preserved_or_new_timestamp(path, content)
|
|
61
|
+
existing = read_manifest(path)
|
|
62
|
+
return Time.now.utc.iso8601 unless existing
|
|
63
|
+
|
|
64
|
+
unchanged = CONTENT_KEYS.all? { |k| existing[k] == content[k] }
|
|
65
|
+
unchanged ? existing["generated_at"] : Time.now.utc.iso8601
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_manifest(path)
|
|
69
|
+
return nil unless path.exist?
|
|
70
|
+
|
|
71
|
+
JSON.parse(path.read)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "ucode/repo/atomic_writes"
|
|
5
|
+
|
|
6
|
+
module Ucode
|
|
7
|
+
module Repo
|
|
8
|
+
module Writers
|
|
9
|
+
# Writes one file per named sequence under
|
|
10
|
+
# `output/named_sequences/<slug>.json`. Empty input writes nothing.
|
|
11
|
+
#
|
|
12
|
+
# One of the eight per-concern writers split out from
|
|
13
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
14
|
+
class NamedSequencesWriter
|
|
15
|
+
include AtomicWrites
|
|
16
|
+
|
|
17
|
+
# @param output_root [Pathname]
|
|
18
|
+
# @param named_sequences [Array<Ucode::Models::NamedSequence>]
|
|
19
|
+
def initialize(output_root:, named_sequences:)
|
|
20
|
+
@output_root = output_root
|
|
21
|
+
@named_sequences = named_sequences
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Integer] number of named-sequence files written
|
|
25
|
+
def write
|
|
26
|
+
return 0 if @named_sequences.nil? || @named_sequences.empty?
|
|
27
|
+
|
|
28
|
+
dir = Pathname(@output_root).join("named_sequences")
|
|
29
|
+
@named_sequences.sum do |ns|
|
|
30
|
+
path = dir.join("#{slug_for(ns)}.json")
|
|
31
|
+
write_atomic(path, ns.to_json(pretty: true)) ? 1 : 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Slug derived from the name: downcase, non-alphanumerics → "_".
|
|
38
|
+
def slug_for(named_sequence)
|
|
39
|
+
named_sequence.name
|
|
40
|
+
.downcase
|
|
41
|
+
.gsub(/[^a-z0-9]+/, "_")
|
|
42
|
+
.gsub(/^_+|_+$/, "")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ucode/repo/atomic_writes"
|
|
4
|
+
require "ucode/repo/paths"
|
|
5
|
+
|
|
6
|
+
module Ucode
|
|
7
|
+
module Repo
|
|
8
|
+
module Writers
|
|
9
|
+
# Writes `output/planes/<n>.json` for every plane (0..16).
|
|
10
|
+
#
|
|
11
|
+
# One of the eight per-concern writers split out from
|
|
12
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
13
|
+
class PlanesWriter
|
|
14
|
+
include AtomicWrites
|
|
15
|
+
|
|
16
|
+
# Static metadata for the 17 Unicode planes. Planes 4–13 are
|
|
17
|
+
# unassigned in Unicode 17; their entries use placeholder names.
|
|
18
|
+
PLANE_TABLE = {
|
|
19
|
+
0 => ["Basic Multilingual Plane", "BMP"],
|
|
20
|
+
1 => ["Supplementary Multilingual Plane", "SMP"],
|
|
21
|
+
2 => ["Supplementary Ideographic Plane", "SIP"],
|
|
22
|
+
3 => ["Tertiary Ideographic Plane", "TIP"],
|
|
23
|
+
4 => ["Unassigned Plane 4", "—"],
|
|
24
|
+
5 => ["Unassigned Plane 5", "—"],
|
|
25
|
+
6 => ["Unassigned Plane 6", "—"],
|
|
26
|
+
7 => ["Unassigned Plane 7", "—"],
|
|
27
|
+
8 => ["Unassigned Plane 8", "—"],
|
|
28
|
+
9 => ["Unassigned Plane 9", "—"],
|
|
29
|
+
10 => ["Unassigned Plane 10", "—"],
|
|
30
|
+
11 => ["Unassigned Plane 11", "—"],
|
|
31
|
+
12 => ["Unassigned Plane 12", "—"],
|
|
32
|
+
13 => ["Unassigned Plane 13", "—"],
|
|
33
|
+
14 => ["Supplementary Special-purpose Plane", "SSP"],
|
|
34
|
+
15 => ["Supplementary Private Use Area-A", "SPUA-A"],
|
|
35
|
+
16 => ["Supplementary Private Use Area-B", "SPUA-B"],
|
|
36
|
+
}.freeze
|
|
37
|
+
private_constant :PLANE_TABLE
|
|
38
|
+
|
|
39
|
+
# @param output_root [Pathname]
|
|
40
|
+
# @param blocks [Array<Ucode::Models::Block>]
|
|
41
|
+
def initialize(output_root:, blocks:)
|
|
42
|
+
@output_root = output_root
|
|
43
|
+
@blocks = blocks
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Integer] number of plane files written (always 17
|
|
47
|
+
# when the directory is reachable; one per plane number)
|
|
48
|
+
def write
|
|
49
|
+
plane_block_ids = group_block_ids_by_plane
|
|
50
|
+
count = 0
|
|
51
|
+
(0..16).each do |n|
|
|
52
|
+
path = Paths.plane_metadata_path(@output_root, n)
|
|
53
|
+
count += 1 if write_atomic(path, plane_payload(n, plane_block_ids[n] || []))
|
|
54
|
+
end
|
|
55
|
+
count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def group_block_ids_by_plane
|
|
61
|
+
@blocks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |block, h|
|
|
62
|
+
h[block.plane_number] << block.id
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def plane_payload(plane_number, block_ids)
|
|
67
|
+
name, abbrev = PLANE_TABLE.fetch(plane_number)
|
|
68
|
+
range_first = plane_number * 0x10000
|
|
69
|
+
range_last = range_first + 0xFFFF
|
|
70
|
+
to_pretty_json(
|
|
71
|
+
"number" => plane_number,
|
|
72
|
+
"name" => name,
|
|
73
|
+
"abbrev" => abbrev,
|
|
74
|
+
"range_first" => range_first,
|
|
75
|
+
"range_last" => range_last,
|
|
76
|
+
"block_ids" => block_ids,
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "ucode/coordinator"
|
|
5
|
+
require "ucode/repo/atomic_writes"
|
|
6
|
+
require "ucode/repo/paths"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Repo
|
|
10
|
+
module Writers
|
|
11
|
+
# Writes one file per relationship table under
|
|
12
|
+
# `output/relationships/`. The set of tables is enumerated by
|
|
13
|
+
# `Coordinator::Indices#each_relationship` (see Candidate 1 of the
|
|
14
|
+
# 2026-06-29 review).
|
|
15
|
+
#
|
|
16
|
+
# One of the eight per-concern writers split out from
|
|
17
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
18
|
+
class RelationshipsWriter
|
|
19
|
+
include AtomicWrites
|
|
20
|
+
|
|
21
|
+
# @param output_root [Pathname]
|
|
22
|
+
# @param indices [Ucode::Coordinator::Indices]
|
|
23
|
+
def initialize(output_root:, indices:)
|
|
24
|
+
@output_root = output_root
|
|
25
|
+
@indices = indices
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Integer] number of relationship files written
|
|
29
|
+
def write
|
|
30
|
+
@indices.each_relationship.sum do |slug, records|
|
|
31
|
+
write_relationship_file(slug, records)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def write_relationship_file(slug, records)
|
|
38
|
+
return 0 if records.nil? || records.empty?
|
|
39
|
+
|
|
40
|
+
path = Pathname(@output_root).join("relationships", "#{slug}.json")
|
|
41
|
+
write_atomic(path, relationship_payload(records)) ? 1 : 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# records is Hash<Integer, Record>, Hash<Integer, Array<Record>>,
|
|
45
|
+
# Hash<String, Record>, or Hash<String, Array<Record>>.
|
|
46
|
+
def relationship_payload(records)
|
|
47
|
+
payload = records.each_with_object({}) do |(key, value), h|
|
|
48
|
+
h[key_to_cp_id(key)] = serialize_value(value)
|
|
49
|
+
end
|
|
50
|
+
to_pretty_json(payload)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Integer codepoint keys are formatted as "U+XXXX"; string id
|
|
54
|
+
# keys (cjk_radicals, standardized_variants) pass through.
|
|
55
|
+
def key_to_cp_id(key)
|
|
56
|
+
key.is_a?(Integer) ? Paths.cp_id(key) : key
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def serialize_value(value)
|
|
60
|
+
return value.map { |v| serialize_one(v) } if value.is_a?(Array)
|
|
61
|
+
|
|
62
|
+
serialize_one(value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def serialize_one(record)
|
|
66
|
+
record.to_yaml_hash
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ucode/repo/atomic_writes"
|
|
4
|
+
require "ucode/repo/paths"
|
|
5
|
+
|
|
6
|
+
module Ucode
|
|
7
|
+
module Repo
|
|
8
|
+
module Writers
|
|
9
|
+
# Writes `output/scripts/<code>.json` for every assigned script.
|
|
10
|
+
#
|
|
11
|
+
# One of the eight per-concern writers split out from
|
|
12
|
+
# AggregateWriter — see Candidate 5 of the 2026-06-29 review.
|
|
13
|
+
class ScriptsWriter
|
|
14
|
+
include AtomicWrites
|
|
15
|
+
|
|
16
|
+
# @param output_root [Pathname]
|
|
17
|
+
# @param scripts [Array<Ucode::Models::Script>] from
|
|
18
|
+
# Coordinator::Indices
|
|
19
|
+
# @param script_codepoint_ids [Hash{String => Array<String>}]
|
|
20
|
+
# ISO 15924 code → cp_id list, accumulated during the
|
|
21
|
+
# streaming pass
|
|
22
|
+
def initialize(output_root:, scripts:, script_codepoint_ids:)
|
|
23
|
+
@output_root = output_root
|
|
24
|
+
@scripts = scripts
|
|
25
|
+
@script_codepoint_ids = script_codepoint_ids
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Integer] number of script files written
|
|
29
|
+
def write
|
|
30
|
+
count = 0
|
|
31
|
+
@scripts.group_by(&:code).each do |code, ranges|
|
|
32
|
+
next if code.nil? || code.empty?
|
|
33
|
+
|
|
34
|
+
path = Paths.script_metadata_path(@output_root, code)
|
|
35
|
+
count += 1 if write_atomic(path, script_payload(code, ranges))
|
|
36
|
+
end
|
|
37
|
+
count
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def script_payload(code, ranges)
|
|
43
|
+
to_pretty_json(
|
|
44
|
+
"code" => code,
|
|
45
|
+
"name" => ranges.first&.name,
|
|
46
|
+
"range_first" => ranges.map(&:range_first).min,
|
|
47
|
+
"range_last" => ranges.map(&:range_last).max,
|
|
48
|
+
"codepoint_ids" => (@script_codepoint_ids[code] || []),
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Repo
|
|
5
|
+
# Per-concern writer classes, one per output file kind. Each
|
|
6
|
+
# conforms to the `#write → Integer` interface (returns the count
|
|
7
|
+
# of files written). Composed by AggregateWriter#flush — adding a
|
|
8
|
+
# new aggregate = one writer class + one line in AggregateWriter.
|
|
9
|
+
module Writers
|
|
10
|
+
autoload :PlanesWriter, "ucode/repo/writers/planes_writer"
|
|
11
|
+
autoload :BlocksWriter, "ucode/repo/writers/blocks_writer"
|
|
12
|
+
autoload :ScriptsWriter, "ucode/repo/writers/scripts_writer"
|
|
13
|
+
autoload :IndexesWriter, "ucode/repo/writers/indexes_writer"
|
|
14
|
+
autoload :RelationshipsWriter, "ucode/repo/writers/relationships_writer"
|
|
15
|
+
autoload :EnumsWriter, "ucode/repo/writers/enums_writer"
|
|
16
|
+
autoload :NamedSequencesWriter, "ucode/repo/writers/named_sequences_writer"
|
|
17
|
+
autoload :ManifestWriter, "ucode/repo/writers/manifest_writer"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/ucode/repo.rb
CHANGED
|
@@ -18,5 +18,10 @@ module Ucode
|
|
|
18
18
|
autoload :AtomicWrites, "ucode/repo/atomic_writes"
|
|
19
19
|
autoload :CodepointWriter, "ucode/repo/codepoint_writer"
|
|
20
20
|
autoload :AggregateWriter, "ucode/repo/aggregate_writer"
|
|
21
|
+
autoload :Writers, "ucode/repo/writers"
|
|
22
|
+
autoload :BuildReportAccumulator, "ucode/repo/build_report_accumulator"
|
|
23
|
+
autoload :BuildReportWriter, "ucode/repo/build_report_writer"
|
|
24
|
+
autoload :BuildValidator, "ucode/repo/build_validator"
|
|
25
|
+
autoload :BlockFeedEmitter, "ucode/repo/block_feed_emitter"
|
|
21
26
|
end
|
|
22
27
|
end
|
data/lib/ucode/version.rb
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ucode block-feed output schema
|
|
2
|
+
#
|
|
3
|
+
# This is the canonical shape of ucode's `block-feed` output — a compact
|
|
4
|
+
# per-block Unicode data feed derived from ucode's canonical output tree.
|
|
5
|
+
#
|
|
6
|
+
# ucode emits three files under the target directory:
|
|
7
|
+
#
|
|
8
|
+
# unicode-blocks.json
|
|
9
|
+
# unicode-version.json
|
|
10
|
+
# unicode/blocks/<slug>.json
|
|
11
|
+
#
|
|
12
|
+
# Block slugs are derived from the Unicode block display name via the
|
|
13
|
+
# standard slug algorithm:
|
|
14
|
+
#
|
|
15
|
+
# name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
16
|
+
#
|
|
17
|
+
# Field names are short (cp, n, c, s, cc, bc, mir) because the per-block
|
|
18
|
+
# files inline every codepoint — byte size matters at the long tail
|
|
19
|
+
# (CJK Unified Ideographs alone is 20,992 chars in one file).
|
|
20
|
+
#
|
|
21
|
+
# This schema describes Unicode data only. It is not specific to any
|
|
22
|
+
# consumer; any tooling that wants a compact per-block view of Unicode
|
|
23
|
+
# can consume it.
|
|
24
|
+
|
|
25
|
+
$schema: "https://json-schema.org/draft/2020-12/schema"
|
|
26
|
+
title: UcodeBlockFeed
|
|
27
|
+
description: Canonical shape of ucode's compact per-block Unicode data feed
|
|
28
|
+
|
|
29
|
+
definitions:
|
|
30
|
+
|
|
31
|
+
codepoint:
|
|
32
|
+
description: A single Unicode codepoint's compact metadata.
|
|
33
|
+
type: object
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
required: [cp]
|
|
36
|
+
properties:
|
|
37
|
+
cp:
|
|
38
|
+
type: integer
|
|
39
|
+
minimum: 0
|
|
40
|
+
maximum: 0x10FFFF
|
|
41
|
+
description: The Unicode scalar value (integer form of U+XXXX).
|
|
42
|
+
n:
|
|
43
|
+
type: string
|
|
44
|
+
minLength: 1
|
|
45
|
+
description: Codepoint name (e.g. "LATIN CAPITAL LETTER A"). Omitted when empty.
|
|
46
|
+
c:
|
|
47
|
+
type: string
|
|
48
|
+
minLength: 1
|
|
49
|
+
pattern: '^[A-Z][a-z]$'
|
|
50
|
+
description: General category short code (Lu, Ll, Mn, So, ...).
|
|
51
|
+
s:
|
|
52
|
+
type: string
|
|
53
|
+
minLength: 2
|
|
54
|
+
description: Script code (Latn, Grek, Hani, Zyyy, ...). 4-letter ISO 15924.
|
|
55
|
+
cc:
|
|
56
|
+
type: integer
|
|
57
|
+
minimum: 0
|
|
58
|
+
maximum: 255
|
|
59
|
+
description: Canonical combining class. Always present (0 for non-marks).
|
|
60
|
+
bc:
|
|
61
|
+
type: string
|
|
62
|
+
minLength: 1
|
|
63
|
+
description: Bidi class short code (L, R, AL, AN, NSM, BN, ...).
|
|
64
|
+
mir:
|
|
65
|
+
type: boolean
|
|
66
|
+
description: Bidi mirrored flag. Omitted when false.
|
|
67
|
+
|
|
68
|
+
block_summary:
|
|
69
|
+
description: One entry per Unicode block in unicode-blocks.json.
|
|
70
|
+
type: object
|
|
71
|
+
additionalProperties: false
|
|
72
|
+
required: [start, end, name, unicode_version]
|
|
73
|
+
properties:
|
|
74
|
+
start:
|
|
75
|
+
type: integer
|
|
76
|
+
minimum: 0
|
|
77
|
+
maximum: 0x10FFFF
|
|
78
|
+
description: First codepoint in the block (inclusive).
|
|
79
|
+
end:
|
|
80
|
+
type: integer
|
|
81
|
+
minimum: 0
|
|
82
|
+
maximum: 0x10FFFF
|
|
83
|
+
description: Last codepoint in the block (inclusive).
|
|
84
|
+
name:
|
|
85
|
+
type: string
|
|
86
|
+
minLength: 1
|
|
87
|
+
description: Unicode block display name (e.g. "Basic Latin", "Greek and Coptic").
|
|
88
|
+
unicode_version:
|
|
89
|
+
type: string
|
|
90
|
+
pattern: '^\d+\.\d+\.\d+$'
|
|
91
|
+
description: Earliest Unicode version of any codepoint in the block (e.g. "1.1", "17.0.0").
|
|
92
|
+
|
|
93
|
+
type: object
|
|
94
|
+
properties:
|
|
95
|
+
|
|
96
|
+
unicode-blocks.json:
|
|
97
|
+
type: array
|
|
98
|
+
description: Top-level index of all Unicode blocks (~346 entries for Unicode 17).
|
|
99
|
+
items:
|
|
100
|
+
$ref: '#/definitions/block_summary'
|
|
101
|
+
|
|
102
|
+
unicode-version.json:
|
|
103
|
+
type: object
|
|
104
|
+
additionalProperties: false
|
|
105
|
+
required: [version, blockCount, charCount, generatedAt]
|
|
106
|
+
properties:
|
|
107
|
+
version:
|
|
108
|
+
type: string
|
|
109
|
+
pattern: '^\d+\.\d+\.\d+$'
|
|
110
|
+
description: UCD version (e.g. "17.0.0").
|
|
111
|
+
blockCount:
|
|
112
|
+
type: integer
|
|
113
|
+
minimum: 1
|
|
114
|
+
description: Total number of blocks (must match length of unicode-blocks.json).
|
|
115
|
+
charCount:
|
|
116
|
+
type: integer
|
|
117
|
+
minimum: 1
|
|
118
|
+
description: Total assigned codepoints across all blocks.
|
|
119
|
+
generatedAt:
|
|
120
|
+
type: string
|
|
121
|
+
format: date-time
|
|
122
|
+
description: ISO 8601 timestamp of the emitter run.
|
|
123
|
+
|
|
124
|
+
unicode/blocks/<slug>.json:
|
|
125
|
+
type: object
|
|
126
|
+
additionalProperties: false
|
|
127
|
+
required: [chars]
|
|
128
|
+
properties:
|
|
129
|
+
chars:
|
|
130
|
+
type: array
|
|
131
|
+
minItems: 0
|
|
132
|
+
description: Codepoints in this block, ordered by cp ascending.
|
|
133
|
+
items:
|
|
134
|
+
$ref: '#/definitions/codepoint'
|