ucode 0.1.1 → 0.2.1
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/TODO.extract-code-chart/01-pdf-fetch-validation.md +80 -0
- data/TODO.extract-code-chart/02-block-name-resolver.md +68 -0
- data/TODO.extract-code-chart/03-codechart-namespace.md +82 -0
- data/TODO.extract-code-chart/04-codechart-extractor.md +154 -0
- data/TODO.extract-code-chart/05-provenance-and-sidecar.md +147 -0
- data/TODO.extract-code-chart/06-codechart-writer.md +134 -0
- data/TODO.extract-code-chart/07-codechart-cli.md +135 -0
- data/TODO.extract-code-chart/08-specs.md +87 -0
- data/lib/ucode/audit/reference_factory.rb +66 -0
- data/lib/ucode/audit.rb +1 -0
- data/lib/ucode/cli.rb +134 -16
- data/lib/ucode/code_chart/extractor.rb +122 -0
- data/lib/ucode/code_chart/provenance.rb +81 -0
- data/lib/ucode/code_chart/sidecar.rb +52 -0
- data/lib/ucode/code_chart/writer.rb +128 -0
- data/lib/ucode/code_chart.rb +39 -0
- data/lib/ucode/commands/audit.rb +0 -1
- data/lib/ucode/commands/build.rb +4 -0
- data/lib/ucode/commands/canonical_build.rb +2 -3
- data/lib/ucode/commands/fetch.rb +12 -14
- 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 +0 -1
- data/lib/ucode/commands/universal_set.rb +10 -14
- data/lib/ucode/coordinator/indices.rb +38 -2
- data/lib/ucode/error.rb +11 -0
- data/lib/ucode/fetch/code_charts.rb +1 -1
- data/lib/ucode/fetch/http.rb +84 -14
- data/lib/ucode/glyphs/pipeline.rb +106 -0
- data/lib/ucode/glyphs.rb +1 -0
- data/lib/ucode/parsers/blocks.rb +34 -0
- data/lib/ucode/repo/aggregate_writer.rb +60 -298
- 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 +1 -0
- data/lib/ucode/version.rb +1 -1
- data/lib/ucode.rb +3 -0
- data/ucode.gemspec +56 -0
- metadata +31 -5
- data/Gemfile.lock +0 -406
- data/lib/ucode/commands/audit/reference_builder.rb +0 -64
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# TODO 07 — CodeChart CLI
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Pending. Depends on TODO 06 (Writer), TODO 04 (Extractor),
|
|
6
|
+
TODO 02 (block name resolver).
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
`ucode code-chart fetch | extract | list` — the REQ (R4) commands.
|
|
11
|
+
Thin Thor wrappers that delegate to the existing `CodeChart::*`
|
|
12
|
+
modules. No orchestration logic in the CLI; every command is a
|
|
13
|
+
single delegation.
|
|
14
|
+
|
|
15
|
+
## Files
|
|
16
|
+
|
|
17
|
+
- `lib/ucode/cli.rb` — add the `CodeChartCmd` Thor subcommand class.
|
|
18
|
+
- `spec/ucode/cli_spec.rb` (extend existing) — verify the new
|
|
19
|
+
subcommand wires up.
|
|
20
|
+
|
|
21
|
+
## Design
|
|
22
|
+
|
|
23
|
+
### Subcommand shape
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class Cli < Thor
|
|
27
|
+
# …existing commands…
|
|
28
|
+
|
|
29
|
+
class CodeChartCmd < Thor
|
|
30
|
+
desc "fetch --block BLOCK", "Download the Code Charts PDF for a block"
|
|
31
|
+
option :block, type: :string, required: true,
|
|
32
|
+
desc: "Block identifier (e.g. Sidetic)"
|
|
33
|
+
def fetch
|
|
34
|
+
puts JSON.pretty_generate(
|
|
35
|
+
Commands::FetchCommand.new.fetch_charts(
|
|
36
|
+
VersionResolver.resolve(nil),
|
|
37
|
+
block_first_cps: [block_first_cp!(options[:block])],
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "extract --block BLOCK --to DIR", "Extract per-codepoint SVG + provenance sidecars"
|
|
43
|
+
option :block, type: :string, required: true,
|
|
44
|
+
desc: "Block identifier (e.g. Sidetic)"
|
|
45
|
+
option :to, type: :string, required: true,
|
|
46
|
+
desc: "Output directory"
|
|
47
|
+
def extract
|
|
48
|
+
# ...
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "list", "List blocks that have Code Charts PDFs available locally"
|
|
52
|
+
def list
|
|
53
|
+
# ...
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "code-chart", "Extract per-codepoint SVG glyphs from Unicode Code Charts PDFs"
|
|
58
|
+
subcommand "code-chart", CodeChartCmd
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `extract` flow
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
def extract
|
|
66
|
+
Ucode::Commands::FetchCommand.new.fetch_charts(
|
|
67
|
+
VersionResolver.resolve(nil),
|
|
68
|
+
block_first_cps: [block_first_cp!(options[:block])],
|
|
69
|
+
)
|
|
70
|
+
blocks_txt = Ucode::Cache.ucd_dir(VersionResolver.resolve(nil)).join("Blocks.txt")
|
|
71
|
+
block = Parsers::Blocks.find_by_name(blocks_txt, options[:block]) or
|
|
72
|
+
raise Thor::Error, "Unknown block: #{options[:block].inspect}"
|
|
73
|
+
pdf = Ucode::Glyphs::PdfFetcher.new(
|
|
74
|
+
VersionResolver.resolve(nil),
|
|
75
|
+
monolith_path: nil,
|
|
76
|
+
blocks: [block],
|
|
77
|
+
).fetch(block_first_cp: block.range_first, force: false) or
|
|
78
|
+
raise Thor::Error, "PDF unavailable for block #{options[:block]}"
|
|
79
|
+
|
|
80
|
+
writer = Ucode::CodeChart::Writer.new(
|
|
81
|
+
output_root: Pathname.new(options[:to]),
|
|
82
|
+
pdf_path: pdf,
|
|
83
|
+
blocks_txt: blocks_txt,
|
|
84
|
+
)
|
|
85
|
+
summary = writer.write(block)
|
|
86
|
+
puts JSON.pretty_generate(summary.to_h.compact)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def block_first_cp!(block_id)
|
|
90
|
+
cache = Ucode::Cache.ucd_dir(VersionResolver.resolve(nil))
|
|
91
|
+
block = Ucode::Parsers::Blocks.find_by_name(cache.join("Blocks.txt"), block_id)
|
|
92
|
+
raise Thor::Error, "Unknown block: #{block_id.inspect}" unless block
|
|
93
|
+
block.range_first
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Why `Ucode::Commands::FetchCommand.new.fetch_charts` for fetch
|
|
98
|
+
|
|
99
|
+
`fetch_charts` is the existing CLI hook for "download a Code Charts
|
|
100
|
+
PDF for these block first-cps." We just call it with the block's
|
|
101
|
+
first cp. Reuse, don't reimplement.
|
|
102
|
+
|
|
103
|
+
### Why resolve version once at the top of `extract`
|
|
104
|
+
|
|
105
|
+
Per Candidate 4 of the architecture review (`refactor/build-context-resolve-version-once`,
|
|
106
|
+
merged): every CLI method resolves the version once and threads it
|
|
107
|
+
through. This CLI method does the same — one call per invocation.
|
|
108
|
+
|
|
109
|
+
### Why no separate `Commands::CodeChartCommand` class
|
|
110
|
+
|
|
111
|
+
Following the existing pattern (e.g. `Cli::Audit` calls
|
|
112
|
+
`Commands::Audit::*Command` *only* when the logic is non-trivial).
|
|
113
|
+
The CodeChart commands are trivial delegations — a one-liner each.
|
|
114
|
+
The CLI methods call `CodeChart::Writer` and `CodeChart::Extractor`
|
|
115
|
+
directly. Adding a `Commands::CodeChartCommand` class would be
|
|
116
|
+
indirection without a payoff.
|
|
117
|
+
|
|
118
|
+
If the extract logic grows (e.g. progress reporting, partial
|
|
119
|
+
extraction), extract it into a Command class at that point.
|
|
120
|
+
|
|
121
|
+
## Acceptance
|
|
122
|
+
|
|
123
|
+
- `ucode code-chart fetch --block Sidetic` downloads the PDF.
|
|
124
|
+
- `ucode code-chart extract --block Sidetic --to /tmp/s/` extracts
|
|
125
|
+
to the given directory.
|
|
126
|
+
- `ucode code-chart list` prints available blocks.
|
|
127
|
+
- Unknown block names produce a clean error, not a stack trace.
|
|
128
|
+
- The CLI matches the REQ's signature exactly.
|
|
129
|
+
|
|
130
|
+
## Out of scope
|
|
131
|
+
|
|
132
|
+
- `--version` flag (the REQ doesn't specify, and existing commands
|
|
133
|
+
default to the configured version).
|
|
134
|
+
- `--format svg|glif` (the REQ specifies SVG; `.glif` output is a
|
|
135
|
+
future extension).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# TODO 08 — CodeChart specs
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Pending. Depends on TODOs 01–07.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
Comprehensive spec coverage for every new module. Per the project
|
|
10
|
+
rules: real model instances (no doubles), tight focused tests,
|
|
11
|
+
behavior assertions (not interaction counts).
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
Already enumerated per TODO. Final spec coverage:
|
|
16
|
+
|
|
17
|
+
- `spec/ucode/fetch/code_charts_spec.rb` — happy path + HTTP 4xx +
|
|
18
|
+
wrong content-type + non-PDF body. (TODO 01)
|
|
19
|
+
- `spec/ucode/parsers/blocks_spec.rb` (extend) — `find_by_name`
|
|
20
|
+
happy path + nil on miss. (TODO 02)
|
|
21
|
+
- `spec/ucode/code_chart/extractor_spec.rb` — constructor invariants,
|
|
22
|
+
Resolver wiring, integration test against fixture PDF. (TODO 04)
|
|
23
|
+
- `spec/ucode/code_chart/provenance_spec.rb` — value object
|
|
24
|
+
construction + `to_h` schema. (TODO 05)
|
|
25
|
+
- `spec/ucode/code_chart/sidecar_spec.rb` — write sidecar, idempotent
|
|
26
|
+
re-write. (TODO 05)
|
|
27
|
+
- `spec/ucode/code_chart/writer_spec.rb` — full lifecycle:
|
|
28
|
+
extract → write → summary. Idempotent re-run produces byte-identical
|
|
29
|
+
files. (TODO 06)
|
|
30
|
+
- `spec/ucode/cli_spec.rb` (extend) — verify `ucode code-chart fetch`,
|
|
31
|
+
`extract`, `list` wire up. (TODO 07)
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
### Fixture strategy
|
|
36
|
+
|
|
37
|
+
The existing `spec/fixtures/pdfs/basic_latin.pdf` is the only PDF
|
|
38
|
+
fixture in the repo. It's tiny and validates the integration path.
|
|
39
|
+
The Sidetic + Egyptian Ext-B PDFs are large (whole-block) and would
|
|
40
|
+
inflate the repo. The integration spec uses `basic_latin.pdf` to
|
|
41
|
+
exercise the full pipeline; per-codepoint assertions cover
|
|
42
|
+
representative cases.
|
|
43
|
+
|
|
44
|
+
If Sidetic-specific behavior must be tested, a smaller fixture PDF
|
|
45
|
+
cropped to ~5 codepoints would be the right tool — out of scope for
|
|
46
|
+
this TODO.
|
|
47
|
+
|
|
48
|
+
### No doubles policy
|
|
49
|
+
|
|
50
|
+
The project's `~/.claude/CLAUDE.md` rule: no doubles. All specs use
|
|
51
|
+
real instances:
|
|
52
|
+
- `Ucode::Models::Block.new(...)` for test blocks.
|
|
53
|
+
- A temp directory + real `Blocks.txt` text for parser specs.
|
|
54
|
+
- The real `Ucode::Glyphs::Resolver` for extractor specs.
|
|
55
|
+
|
|
56
|
+
### Idempotency assertion pattern
|
|
57
|
+
|
|
58
|
+
`Writer#write` idempotency is asserted via byte-equality:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
first_run = writer.write(block)
|
|
62
|
+
first_size = File.stat(svg_path).size
|
|
63
|
+
sleep 0.01 # ensure mtime changes would be detectable
|
|
64
|
+
second_run = writer.write(block)
|
|
65
|
+
second_size = File.stat(svg_path).size
|
|
66
|
+
expect(second_size).to eq(first_size)
|
|
67
|
+
expect(File.read(svg_path)).to eq(expected_svg_bytes)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This is the existing pattern from `spec/ucode/repo/aggregate_writer_spec.rb`
|
|
71
|
+
(idiempotency spec there). Reuse.
|
|
72
|
+
|
|
73
|
+
## Acceptance
|
|
74
|
+
|
|
75
|
+
- `bundle exec rspec spec/ucode/code_chart/ spec/ucode/fetch/code_charts_spec.rb spec/ucode/parsers/blocks_spec.rb`
|
|
76
|
+
passes 100%.
|
|
77
|
+
- Coverage for the new files is ≥ 95% (per the project's per-file
|
|
78
|
+
floor of 30% + the overall 80% minimum).
|
|
79
|
+
- No doubles are introduced (verify with `grep -r "double(" spec/ucode/code_chart/`).
|
|
80
|
+
- The integration spec exercises both the Extractor and Writer
|
|
81
|
+
together end-to-end.
|
|
82
|
+
|
|
83
|
+
## Out of scope
|
|
84
|
+
|
|
85
|
+
- Performance benchmarks — separate concern.
|
|
86
|
+
- Sidetic-specific fixtures — requires PDF curation beyond the
|
|
87
|
+
scope of this feature.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Audit
|
|
7
|
+
# Translates CLI flags into a {Ucode::Audit::CoverageReference}.
|
|
8
|
+
#
|
|
9
|
+
# The audit CLI exposes the universal-set reference via a
|
|
10
|
+
# `--reference-universal-set=<path>` flag (and a default lookup
|
|
11
|
+
# at `output/universal_glyph_set/manifest.json`). This factory
|
|
12
|
+
# resolves the flag value into a concrete reference instance
|
|
13
|
+
# backed by a freshly-opened {Ucode::Database}, so the command
|
|
14
|
+
# classes don't repeat the same branching.
|
|
15
|
+
#
|
|
16
|
+
# Behavior:
|
|
17
|
+
#
|
|
18
|
+
# - flag = "none" → nil (force UCD-only even if a default manifest exists)
|
|
19
|
+
# - flag = path to .json → {Ucode::Audit::UniversalSetReference}
|
|
20
|
+
# - flag = nil → look at DEFAULT_MANIFEST_PATH; use it if present,
|
|
21
|
+
# else nil (UCD-only)
|
|
22
|
+
#
|
|
23
|
+
# Lives in the {Ucode::Audit} namespace (not {Ucode::Commands::Audit})
|
|
24
|
+
# so the Audit module owns its own entry point — programmatic callers
|
|
25
|
+
# don't need to round-trip through the CLI to obtain a reference.
|
|
26
|
+
module ReferenceFactory
|
|
27
|
+
DEFAULT_MANIFEST_PATH = Pathname.new("output/universal_glyph_set/manifest.json")
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @param flag [String, nil] value of the
|
|
32
|
+
# `--reference-universal-set` CLI option.
|
|
33
|
+
# @param version [String, nil] UCD version for the database
|
|
34
|
+
# that backs the reference. When nil, the default UCD
|
|
35
|
+
# version is resolved.
|
|
36
|
+
# @return [Ucode::Audit::CoverageReference, nil]
|
|
37
|
+
def build_from_cli(flag:, version: nil)
|
|
38
|
+
return nil if flag == "none"
|
|
39
|
+
|
|
40
|
+
path = resolve_manifest_path(flag)
|
|
41
|
+
return nil unless path && File.exist?(path)
|
|
42
|
+
|
|
43
|
+
database = open_database(version)
|
|
44
|
+
return nil unless database
|
|
45
|
+
|
|
46
|
+
Ucode::Audit::UniversalSetReference.new(
|
|
47
|
+
manifest: path, database: database,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_manifest_path(flag)
|
|
52
|
+
return Pathname.new(flag) if flag && flag != "none"
|
|
53
|
+
return DEFAULT_MANIFEST_PATH if DEFAULT_MANIFEST_PATH.exist?
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def open_database(version)
|
|
59
|
+
resolved = version || Ucode::VersionResolver.resolve(nil)
|
|
60
|
+
Ucode::Database.open(resolved)
|
|
61
|
+
rescue Ucode::UnknownVersionError, Ucode::DatabaseMissingError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/ucode/audit.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Ucode
|
|
|
32
32
|
autoload :CoverageReference, "ucode/audit/coverage_reference"
|
|
33
33
|
autoload :UcdOnlyReference, "ucode/audit/ucd_only_reference"
|
|
34
34
|
autoload :UniversalSetReference, "ucode/audit/universal_set_reference"
|
|
35
|
+
autoload :ReferenceFactory, "ucode/audit/reference_factory"
|
|
35
36
|
|
|
36
37
|
# Per-face orchestrator (TODO 11) — shared by LibraryAuditor and
|
|
37
38
|
# the future CLI AuditCommand.
|
data/lib/ucode/cli.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
|
|
5
5
|
require "ucode/commands"
|
|
6
|
+
require "ucode/code_chart"
|
|
7
|
+
require "ucode/version_resolver"
|
|
6
8
|
|
|
7
9
|
module Ucode
|
|
8
10
|
# Top-level CLI entry.
|
|
@@ -10,6 +12,12 @@ module Ucode
|
|
|
10
12
|
# **Thin Thor**: every method delegates to a `Commands::*Command`
|
|
11
13
|
# class and only formats the result. The Command classes are pure
|
|
12
14
|
# and testable in-process — Thor never holds business logic.
|
|
15
|
+
#
|
|
16
|
+
# **Version resolution lives here** — each top-level command resolves
|
|
17
|
+
# the user-supplied intent (nil / :default / :latest / explicit string)
|
|
18
|
+
# exactly once via `VersionResolver.resolve` and threads the resolved
|
|
19
|
+
# string into the dispatched Command. Sub-commands never re-resolve.
|
|
20
|
+
# See Candidate 4 of the 2026-06-29 architecture review.
|
|
13
21
|
class Cli < Thor
|
|
14
22
|
package_name "ucode"
|
|
15
23
|
|
|
@@ -28,13 +36,17 @@ module Ucode
|
|
|
28
36
|
desc "ucd [VERSION]", "Download UCD.zip"
|
|
29
37
|
option :force, type: :boolean, default: false, desc: "Re-download even if cached"
|
|
30
38
|
def ucd(version = nil)
|
|
31
|
-
puts format_result Commands::FetchCommand.new.fetch_ucd(
|
|
39
|
+
puts format_result Commands::FetchCommand.new.fetch_ucd(
|
|
40
|
+
VersionResolver.resolve(version), force: options[:force],
|
|
41
|
+
)
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
desc "unihan [VERSION]", "Download Unihan.zip"
|
|
35
45
|
option :force, type: :boolean, default: false
|
|
36
46
|
def unihan(version = nil)
|
|
37
|
-
puts format_result Commands::FetchCommand.new.fetch_unihan(
|
|
47
|
+
puts format_result Commands::FetchCommand.new.fetch_unihan(
|
|
48
|
+
VersionResolver.resolve(version), force: options[:force],
|
|
49
|
+
)
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
desc "charts [VERSION]", "Download per-block Code Charts PDFs"
|
|
@@ -43,7 +55,8 @@ module Ucode
|
|
|
43
55
|
def charts(version = nil)
|
|
44
56
|
cps = options[:block]&.map { |id| block_id_to_first_cp(id) }&.compact
|
|
45
57
|
puts format_result Commands::FetchCommand.new
|
|
46
|
-
.fetch_charts(version,
|
|
58
|
+
.fetch_charts(VersionResolver.resolve(version),
|
|
59
|
+
block_first_cps: cps, force: options[:force])
|
|
47
60
|
end
|
|
48
61
|
|
|
49
62
|
desc "fonts", "Download specialist Tier 1 fonts (config/specialist_fonts.yml)"
|
|
@@ -90,7 +103,9 @@ module Ucode
|
|
|
90
103
|
desc "parse [VERSION]", "Stream UCD → output/"
|
|
91
104
|
option :to, type: :string, default: "./output", desc: "Output directory"
|
|
92
105
|
def parse(version = nil)
|
|
93
|
-
result = Commands::ParseCommand.new.call(
|
|
106
|
+
result = Commands::ParseCommand.new.call(
|
|
107
|
+
VersionResolver.resolve(version), output_root: options[:to],
|
|
108
|
+
)
|
|
94
109
|
puts JSON.pretty_generate(result)
|
|
95
110
|
end
|
|
96
111
|
|
|
@@ -111,7 +126,7 @@ module Ucode
|
|
|
111
126
|
desc: "Opt into the experimental v0.1 pipeline"
|
|
112
127
|
def glyphs(version = nil)
|
|
113
128
|
result = Commands::GlyphsCommand.new.call(
|
|
114
|
-
version,
|
|
129
|
+
VersionResolver.resolve(version),
|
|
115
130
|
output_root: options[:to],
|
|
116
131
|
block_filter: options[:block],
|
|
117
132
|
force: options[:force],
|
|
@@ -143,6 +158,104 @@ module Ucode
|
|
|
143
158
|
desc "site", "Generate the Vitepress site"
|
|
144
159
|
subcommand "site", Site
|
|
145
160
|
|
|
161
|
+
# ─────────────── code-chart ───────────────
|
|
162
|
+
# Extract per-codepoint SVG glyphs from a Unicode Code Charts PDF.
|
|
163
|
+
# One folder per block under --to, with <U+XXXX>.svg + .json pairs.
|
|
164
|
+
class CodeChartCmd < Thor
|
|
165
|
+
desc "fetch --block BLOCK [VERSION]", "Download the Code Charts PDF for a block"
|
|
166
|
+
option :block, type: :string, required: true,
|
|
167
|
+
desc: "Block identifier (e.g. Sidetic, Basic_Latin)"
|
|
168
|
+
def fetch(version = nil)
|
|
169
|
+
with_codechart_errors do
|
|
170
|
+
block_first_cp = resolve_block_first_cp!(options[:block], version)
|
|
171
|
+
result = Commands::FetchCommand.new.fetch_charts(
|
|
172
|
+
VersionResolver.resolve(version),
|
|
173
|
+
block_first_cps: [block_first_cp],
|
|
174
|
+
)
|
|
175
|
+
puts JSON.pretty_generate(result)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
desc "extract --block BLOCK --to DIR [VERSION]",
|
|
180
|
+
"Extract per-codepoint SVG + provenance sidecars from a Code Charts PDF"
|
|
181
|
+
option :block, type: :string, required: true,
|
|
182
|
+
desc: "Block identifier (e.g. Sidetic)"
|
|
183
|
+
option :to, type: :string, required: true,
|
|
184
|
+
desc: "Output directory (will contain <block_id>/<U+XXXX>.svg + .json)"
|
|
185
|
+
def extract(version = nil)
|
|
186
|
+
with_codechart_errors do
|
|
187
|
+
version_str = VersionResolver.resolve(version)
|
|
188
|
+
block = resolve_block!(options[:block], version_str)
|
|
189
|
+
block_first_cp = block.range_first
|
|
190
|
+
|
|
191
|
+
# Download (idempotent — re-runs skip when the PDF is cached).
|
|
192
|
+
Commands::FetchCommand.new.fetch_charts(version_str, block_first_cps: [block_first_cp])
|
|
193
|
+
|
|
194
|
+
pdf = Ucode::Glyphs::PdfFetcher.new(version_str)
|
|
195
|
+
.fetch(block_first_cp: block_first_cp)
|
|
196
|
+
raise Ucode::CodeChartNotFoundError.new(
|
|
197
|
+
"Code Charts PDF unavailable for block #{block.id.inspect}",
|
|
198
|
+
context: { block_id: block.id, version: version_str },
|
|
199
|
+
) unless pdf
|
|
200
|
+
|
|
201
|
+
writer = Ucode::CodeChart::Writer.new(
|
|
202
|
+
output_root: Pathname.new(options[:to]),
|
|
203
|
+
pdf_path: pdf,
|
|
204
|
+
ucd_version: version_str,
|
|
205
|
+
)
|
|
206
|
+
summary = writer.write(block)
|
|
207
|
+
puts JSON.pretty_generate(summary.to_h.compact)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc "list", "List cached Code Charts PDFs under the version's cache"
|
|
212
|
+
def list
|
|
213
|
+
version = VersionResolver.resolve(nil)
|
|
214
|
+
pdfs_dir = Ucode::Cache.pdfs_dir(version)
|
|
215
|
+
files = pdfs_dir.exist? ? pdfs_dir.children.sort : []
|
|
216
|
+
if files.empty?
|
|
217
|
+
puts "(no cached Code Charts PDFs)"
|
|
218
|
+
return
|
|
219
|
+
end
|
|
220
|
+
files.each do |f|
|
|
221
|
+
puts f.basename.to_s
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
# Resolve a block name to its first codepoint via the cached
|
|
228
|
+
# Blocks.txt. Raises {Ucode::UnknownBlockError} on miss.
|
|
229
|
+
def resolve_block!(block_id, version)
|
|
230
|
+
blocks_txt = Ucode::Cache.ucd_dir(VersionResolver.resolve(version)).join("Blocks.txt")
|
|
231
|
+
Ucode::Parsers::Blocks.find_by_id!(blocks_txt, block_id)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def resolve_block_first_cp!(block_id, version)
|
|
235
|
+
resolve_block!(block_id, version).range_first
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Convert semantic Ucode errors into Thor errors so Thor's
|
|
239
|
+
# dispatch prints the message cleanly instead of a stack trace.
|
|
240
|
+
# Thor's `start` rescues only `Thor::Error`; without this bridge,
|
|
241
|
+
# any `Ucode::Error` subclass propagates as an uncaught exception.
|
|
242
|
+
def with_codechart_errors
|
|
243
|
+
yield
|
|
244
|
+
rescue Ucode::Error => e
|
|
245
|
+
raise Thor::Error, e.message
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Register the subcommand under the underscored method name
|
|
250
|
+
# (`code_chart`). Thor's `normalize_command_name` converts the
|
|
251
|
+
# user's hyphenated form (`code-chart`) to the underscored form
|
|
252
|
+
# before lookup, so `ucode code-chart <cmd>` dispatches correctly.
|
|
253
|
+
# `desc` first registers the method as a Thor command so the
|
|
254
|
+
# dispatch table has an entry; `subcommand` then attaches the
|
|
255
|
+
# CodeChartCmd class to it.
|
|
256
|
+
desc "code_chart <command>", "Extract SVG glyphs from Unicode Code Charts PDFs"
|
|
257
|
+
subcommand "code_chart", CodeChartCmd
|
|
258
|
+
|
|
146
259
|
# ─────────────── lookup ───────────────
|
|
147
260
|
class Lookup < Thor
|
|
148
261
|
desc "block CODEPOINT", "Block name covering CODEPOINT (integer or 0xNNNN)"
|
|
@@ -150,7 +263,9 @@ module Ucode
|
|
|
150
263
|
def block(codepoint)
|
|
151
264
|
cp = parse_cp(codepoint)
|
|
152
265
|
with_db_handling do
|
|
153
|
-
result = Commands::LookupCommand.new.lookup_block(
|
|
266
|
+
result = Commands::LookupCommand.new.lookup_block(
|
|
267
|
+
VersionResolver.resolve(options[:version]), codepoint: cp,
|
|
268
|
+
)
|
|
154
269
|
puts "#{format("U+%04X", cp)} → #{result.block || "(unassigned)"}"
|
|
155
270
|
end
|
|
156
271
|
end
|
|
@@ -160,7 +275,9 @@ module Ucode
|
|
|
160
275
|
def script(codepoint)
|
|
161
276
|
cp = parse_cp(codepoint)
|
|
162
277
|
with_db_handling do
|
|
163
|
-
result = Commands::LookupCommand.new.lookup_script(
|
|
278
|
+
result = Commands::LookupCommand.new.lookup_script(
|
|
279
|
+
VersionResolver.resolve(options[:version]), codepoint: cp,
|
|
280
|
+
)
|
|
164
281
|
puts "#{format("U+%04X", cp)} → #{result.script || "(none)"}"
|
|
165
282
|
end
|
|
166
283
|
end
|
|
@@ -172,7 +289,8 @@ module Ucode
|
|
|
172
289
|
cp = parse_cp(codepoint)
|
|
173
290
|
with_db_handling do
|
|
174
291
|
result = Commands::LookupCommand.new
|
|
175
|
-
.lookup_char(options[:version],
|
|
292
|
+
.lookup_char(VersionResolver.resolve(options[:version]),
|
|
293
|
+
codepoint: cp, output_root: options[:from])
|
|
176
294
|
puts "#{format("U+%04X", cp)} block=#{result.block_id} glyph=#{result.glyph_path}"
|
|
177
295
|
end
|
|
178
296
|
end
|
|
@@ -274,7 +392,7 @@ module Ucode
|
|
|
274
392
|
desc: "Emit per-block missing-glyph galleries " \
|
|
275
393
|
"(requires --browse + --universal-set-root)"
|
|
276
394
|
def font(path)
|
|
277
|
-
reference =
|
|
395
|
+
reference = Ucode::Audit::ReferenceFactory.build_from_cli(
|
|
278
396
|
flag: options[:reference_universal_set],
|
|
279
397
|
version: options[:unicode_version],
|
|
280
398
|
)
|
|
@@ -312,7 +430,7 @@ module Ucode
|
|
|
312
430
|
option :with_missing_glyph_pages, type: :boolean, default: false,
|
|
313
431
|
desc: "Emit per-block missing-glyph galleries"
|
|
314
432
|
def collection(path)
|
|
315
|
-
reference =
|
|
433
|
+
reference = Ucode::Audit::ReferenceFactory.build_from_cli(
|
|
316
434
|
flag: options[:reference_universal_set],
|
|
317
435
|
version: options[:unicode_version],
|
|
318
436
|
)
|
|
@@ -349,7 +467,7 @@ module Ucode
|
|
|
349
467
|
option :with_missing_glyph_pages, type: :boolean, default: false,
|
|
350
468
|
desc: "Emit per-block missing-glyph galleries"
|
|
351
469
|
def library(dir)
|
|
352
|
-
reference =
|
|
470
|
+
reference = Ucode::Audit::ReferenceFactory.build_from_cli(
|
|
353
471
|
flag: options[:reference_universal_set],
|
|
354
472
|
version: options[:unicode_version],
|
|
355
473
|
)
|
|
@@ -430,7 +548,7 @@ module Ucode
|
|
|
430
548
|
desc: "Worker pool size (default: Ucode.configuration.parallel_workers)"
|
|
431
549
|
def build(version = nil)
|
|
432
550
|
result = Commands::UniversalSet::BuildCommand.new.call(
|
|
433
|
-
version,
|
|
551
|
+
VersionResolver.resolve(version),
|
|
434
552
|
output_root: options[:to],
|
|
435
553
|
source_config_path: options[:source_config],
|
|
436
554
|
block_filter: options[:block],
|
|
@@ -448,7 +566,7 @@ module Ucode
|
|
|
448
566
|
desc: "Path to a Tier 1 source config YAML"
|
|
449
567
|
def pre_check(version = nil)
|
|
450
568
|
report = Commands::UniversalSet::PreCheckCommand.new.call(
|
|
451
|
-
version,
|
|
569
|
+
VersionResolver.resolve(version),
|
|
452
570
|
source_config_path: options[:source_config],
|
|
453
571
|
)
|
|
454
572
|
puts JSON.pretty_generate(report.to_h)
|
|
@@ -463,7 +581,7 @@ module Ucode
|
|
|
463
581
|
desc: "Output directory holding manifest.json"
|
|
464
582
|
def report(version = nil)
|
|
465
583
|
result = Commands::UniversalSet::ReportCommand.new.call(
|
|
466
|
-
version,
|
|
584
|
+
VersionResolver.resolve(version),
|
|
467
585
|
output_root: options[:from],
|
|
468
586
|
)
|
|
469
587
|
puts JSON.pretty_generate(result)
|
|
@@ -475,7 +593,7 @@ module Ucode
|
|
|
475
593
|
def validate(output_root = "./output/universal_glyph_set")
|
|
476
594
|
result = Commands::UniversalSet::ValidateCommand.new.call(
|
|
477
595
|
output_root,
|
|
478
|
-
|
|
596
|
+
version: options[:version] && VersionResolver.resolve(options[:version]),
|
|
479
597
|
)
|
|
480
598
|
puts JSON.pretty_generate(result)
|
|
481
599
|
exit 1 unless result[:passed]
|
|
@@ -518,7 +636,7 @@ module Ucode
|
|
|
518
636
|
desc: "Path to universal-set manifest (or 'none') " \
|
|
519
637
|
"for the per-face coverage reference"
|
|
520
638
|
def release
|
|
521
|
-
reference =
|
|
639
|
+
reference = Ucode::Audit::ReferenceFactory.build_from_cli(
|
|
522
640
|
flag: options[:reference_universal_set],
|
|
523
641
|
version: options[:unicode_version],
|
|
524
642
|
)
|