pubid 2.0.2 → 2.0.3
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/data/ieee/update_codes.yaml +2 -2
- data/data/parg/aiee.json +1 -1
- data/data/parg/aiee.parg +99 -9
- data/data/parg/iala.json +1 -1
- data/data/parg/ieee.json +1 -1
- data/data/parg/ire.json +1 -1
- data/data/parg/ire.parg +51 -12
- data/data/parg/iso.json +1 -1
- data/data/parg/iso.parg +751 -0
- data/data/parg/jis.json +1 -1
- data/data/parg/nesc.json +1 -1
- data/data/parg/nesc.parg +97 -12
- data/data/parg/parg.json +1 -1
- data/lib/pubid/iala/builder.rb +48 -39
- data/lib/pubid/iala/identifier.rb +1 -1
- data/lib/pubid/ieee/CLAUDE.md +2 -0
- data/lib/pubid/ieee/aiee/identifier.rb +1 -1
- data/lib/pubid/ieee/builder.rb +175 -28
- data/lib/pubid/ieee/components/code.rb +46 -7
- data/lib/pubid/ieee/identifiers/base.rb +4 -1
- data/lib/pubid/ieee/identifiers/joint_development.rb +7 -3
- data/lib/pubid/ieee/ire/identifier.rb +1 -1
- data/lib/pubid/ieee/nesc/builder.rb +1 -1
- data/lib/pubid/ieee/project_renderer.rb +5 -1
- data/lib/pubid/ieee/renderer.rb +4 -2
- data/lib/pubid/ieee/urn_generator.rb +4 -1
- data/lib/pubid/iho/CLAUDE.md +15 -6
- data/lib/pubid/iho/urn_generator.rb +9 -6
- data/lib/pubid/iho/urn_parser.rb +43 -40
- data/lib/pubid/iso/CLAUDE.md +66 -0
- data/lib/pubid/iso/builder.rb +228 -14
- data/lib/pubid/iso/identifier.rb +22 -12
- data/lib/pubid/iso/normalizer.rb +5 -4
- data/lib/pubid/iso/urn_parser.rb +13 -8
- data/lib/pubid/jis/CLAUDE.md +36 -0
- data/lib/pubid/jis/builder.rb +12 -12
- data/lib/pubid/jis/identifier.rb +11 -1
- data/lib/pubid/jis/identifiers/welding_engineering_standard.rb +35 -0
- data/lib/pubid/jis/identifiers.rb +2 -0
- data/lib/pubid/jis/renderer.rb +1 -1
- data/lib/pubid/jis/supplement_identifier.rb +5 -5
- data/lib/pubid/jis/urn_parser.rb +2 -1
- data/lib/pubid/jis.rb +3 -2
- data/lib/pubid/version.rb +1 -1
- metadata +5 -2
|
@@ -94,7 +94,7 @@ module Pubid
|
|
|
94
94
|
return Identifiers::Nesc::Preprint if parsed_hash[:preprint]
|
|
95
95
|
|
|
96
96
|
# Draft identifiers
|
|
97
|
-
return Identifiers::Nesc::Draft if parsed_hash[:draft]
|
|
97
|
+
return Identifiers::Nesc::Draft if parsed_hash[:draft] || parsed_hash[:nesc_draft_marker]
|
|
98
98
|
|
|
99
99
|
# Variant-based identifiers
|
|
100
100
|
if parsed_hash[:variant]
|
|
@@ -14,8 +14,12 @@ module Pubid
|
|
|
14
14
|
|
|
15
15
|
def render(context: nil, **_opts)
|
|
16
16
|
id = @id
|
|
17
|
+
# Canonical code: a project draft's code is reassembled from
|
|
18
|
+
# parse-tree captures, and the assembled separators are synthetic
|
|
19
|
+
# — the printed face is the uniform spelling (the same document
|
|
20
|
+
# spelled with all dashes or all dots renders identically).
|
|
17
21
|
body = if id.respond_to?(:code_obj) && id.code_obj
|
|
18
|
-
id.code_obj.
|
|
22
|
+
id.code_obj.canonical_to_s
|
|
19
23
|
else
|
|
20
24
|
id.number.to_s
|
|
21
25
|
end
|
data/lib/pubid/ieee/renderer.rb
CHANGED
|
@@ -472,8 +472,10 @@ module Pubid
|
|
|
472
472
|
end
|
|
473
473
|
|
|
474
474
|
# Code (number) with draft version for PSI — the mark goes after the
|
|
475
|
-
# code, before the draft and the date.
|
|
476
|
-
|
|
475
|
+
# code, before the draft and the date. Canonical code: the PSI
|
|
476
|
+
# code is reassembled from parse-tree captures (synthetic
|
|
477
|
+
# separators — see Components::Code#canonical_to_s).
|
|
478
|
+
code_part = id.code.canonical_to_s
|
|
477
479
|
code_part += mark(id.code_obj&.number, id.code_obj&.prefix,
|
|
478
480
|
publishers: publishers_of(id))
|
|
479
481
|
if id.draft_obj
|
|
@@ -190,7 +190,10 @@ module Pubid
|
|
|
190
190
|
|
|
191
191
|
return nil unless identifier.code_obj
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
# The URN is spelling-stable: codes printed with mixed part
|
|
194
|
+
# separators ("7.4-3-2") share one identity with their uniform
|
|
195
|
+
# spelling, so the segment renders canonically, not as printed.
|
|
196
|
+
identifier.code_obj.canonical_to_s
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
# A multi-designation wrapper (AdoptedStandard, DualPublished,
|
data/lib/pubid/iho/CLAUDE.md
CHANGED
|
@@ -62,12 +62,21 @@ and volume/issue only ride part-less ids, which cannot join
|
|
|
62
62
|
`spec/pubid/all_parts_edition_keys_audit_spec.rb` covers the
|
|
63
63
|
version-bearing S-series collapse.
|
|
64
64
|
|
|
65
|
-
**URN conventions:**
|
|
66
|
-
(`urn:iho:p:5:2024`); a volume is a
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
grammar as a string.
|
|
65
|
+
**URN conventions:** only the dotted `version` is an untagged
|
|
66
|
+
segment. A year is tagged `y.N` (`urn:iho:p:5:y.2024`); a volume is a
|
|
67
|
+
`vol.N` segment with an optional `issue.N` segment after it
|
|
68
|
+
(`urn:iho:p:1:vol.30:issue.2`). The URN parser walks the tagged
|
|
69
|
+
segments as a run and hands the rebuilt text back through the flavor
|
|
70
|
+
parse, so a URN round-trip exercises the same grammar as a string.
|
|
71
|
+
**The number is not escaped and can hold a colon** (`IHO S-158:100`
|
|
72
|
+
gives `urn:iho:s:158:100`), so the parser joins every untagged segment
|
|
73
|
+
that is not an a.b.c version back into the number. That is why the
|
|
74
|
+
year must carry a tag: with the bare year that pubid 2.0.2 emitted,
|
|
75
|
+
`IHO S-158:2024` and `IHO S-158 2024` both gave `urn:iho:s:158:2024`,
|
|
76
|
+
and no colon number round-tripped at all. A 2.0.2 year URN such as
|
|
77
|
+
`urn:iho:p:5:2024` now decodes as the number `5:2024`; it was emitted
|
|
78
|
+
only by 2.0.2. A tagged segment with an unknown tag raises
|
|
79
|
+
`Pubid::Errors::UrnParseError`.
|
|
71
80
|
|
|
72
81
|
## Fixtures run through the classify pipeline
|
|
73
82
|
|
|
@@ -4,13 +4,16 @@ module Pubid
|
|
|
4
4
|
module Iho
|
|
5
5
|
# Generates URN strings for IHO identifiers.
|
|
6
6
|
#
|
|
7
|
-
# Format: urn:iho:{type-letter-lowercase}:{number}[:ap.{appendix}][:part.{part}][:annex.{annex}][:suppl.{supplement}][:
|
|
7
|
+
# Format: urn:iho:{type-letter-lowercase}:{number}[:ap.{appendix}][:part.{part}][:annex.{annex}][:suppl.{supplement}][:vol.{volume}[:issue.{issue}]][:{version}][:y.{year}]
|
|
8
8
|
# Example: urn:iho:s:44:5.0.0 for "IHO S-44 5.0.0".
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# optional issue.N after it ("urn:iho:p:1:vol.30.issue.2" for
|
|
9
|
+
# Only the dotted version is a bare segment. A year is tagged
|
|
10
|
+
# ("urn:iho:p:5:y.2024" for "IHO P-5 2024"), and so is a volume with
|
|
11
|
+
# its optional issue ("urn:iho:p:1:vol.30:issue.2" for
|
|
13
12
|
# "IHO P-1 Vol. 30 (2)").
|
|
13
|
+
#
|
|
14
|
+
# The number is not escaped, so "IHO S-158:100" gives
|
|
15
|
+
# "urn:iho:s:158:100". A bare year would then make "IHO S-158:2024"
|
|
16
|
+
# and "IHO S-158 2024" share one URN; the y. tag keeps them apart.
|
|
14
17
|
class UrnGenerator
|
|
15
18
|
attr_reader :identifier
|
|
16
19
|
|
|
@@ -31,7 +34,7 @@ module Pubid
|
|
|
31
34
|
parts << "vol.#{identifier.volume}" if identifier.volume
|
|
32
35
|
parts << "issue.#{identifier.issue}" if identifier.issue
|
|
33
36
|
parts << identifier.version.to_s if identifier.version
|
|
34
|
-
parts << identifier.year
|
|
37
|
+
parts << "y.#{identifier.year}" if identifier.year
|
|
35
38
|
parts.join(":")
|
|
36
39
|
end
|
|
37
40
|
end
|
data/lib/pubid/iho/urn_parser.rb
CHANGED
|
@@ -5,63 +5,66 @@ module Pubid
|
|
|
5
5
|
# Parses IHO URNs back into identifiers.
|
|
6
6
|
#
|
|
7
7
|
# UrnGenerator emits:
|
|
8
|
-
# urn:iho:{type-lower}:{number}[:{
|
|
8
|
+
# urn:iho:{type-lower}:{number}[:{tag}.{value}]*[:{version}][:y.{year}]
|
|
9
9
|
#
|
|
10
10
|
# type-lower is the lowercase series letter (s, p, m, b, c).
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# tag is a lowercase label: part, ap, annex, suppl, vol, issue or y.
|
|
12
|
+
# version is the dotted a.b.c edition, the only untagged tail.
|
|
13
|
+
#
|
|
14
|
+
# The number is not escaped, so it can span segments
|
|
15
|
+
# ("IHO S-158:100" gives "urn:iho:s:158:100"). Every untagged
|
|
16
|
+
# segment that is not a version therefore belongs to the number.
|
|
15
17
|
#
|
|
16
18
|
# Examples:
|
|
17
19
|
# - urn:iho:s:44:5.0.0 → IHO S-44 5.0.0
|
|
18
20
|
# - urn:iho:s:100:part.4a:1.0.0 → IHO S-100 Part 4a 1.0.0
|
|
19
21
|
# - urn:iho:s:65:ap.A:1.0.0 → IHO S-65 Ap. A 1.0.0
|
|
20
|
-
# - urn:iho:
|
|
22
|
+
# - urn:iho:s:158:100:1.0.0 → IHO S-158:100 1.0.0
|
|
23
|
+
# - urn:iho:p:5:y.2024 → IHO P-5 2024
|
|
21
24
|
# - urn:iho:p:1:vol.30 → IHO P-1 Vol. 30
|
|
22
|
-
# - urn:iho:p:1:vol.30
|
|
25
|
+
# - urn:iho:p:1:vol.30:issue.2 → IHO P-1 Vol. 30 (2)
|
|
23
26
|
class UrnParser < Pubid::UrnParser::Base
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
27
|
+
# Tag => the text that the human-readable form prints for it.
|
|
28
|
+
TAG_TEXT = {
|
|
29
|
+
"part" => ->(v) { " Part #{v}" },
|
|
30
|
+
"ap" => ->(v) { " Ap. #{v}" },
|
|
31
|
+
"annex" => ->(v) { " Annex #{v}" },
|
|
32
|
+
"suppl" => ->(v) { " Suppl #{v}" },
|
|
33
|
+
"vol" => ->(v) { " Vol. #{v}" },
|
|
34
|
+
"issue" => ->(v) { " (#{v})" },
|
|
35
|
+
"y" => ->(v) { " #{v}" },
|
|
30
36
|
}.freeze
|
|
31
37
|
|
|
38
|
+
VERSION_SEGMENT = /\A\d+\.\d+\.\d+\z/
|
|
39
|
+
|
|
32
40
|
def parse_urn(urn)
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
type_token, first, *rest = split_parts(strip_namespace(urn))
|
|
42
|
+
number = [first, *rest.take_while { |s| untagged_number?(s) }]
|
|
43
|
+
tail = rest.drop(number.size - 1).map { |s| segment_text(s, urn) }
|
|
44
|
+
flavor_parse("IHO #{type_token.upcase}-#{number.join(':')}#{tail.join}")
|
|
45
|
+
end
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
number = parts.fetch(1)
|
|
38
|
-
idx = 2
|
|
39
|
-
text = "IHO #{type_token}-#{number}"
|
|
40
|
-
# Part-kind tokens come in any run (a volume may carry an issue
|
|
41
|
-
# beside it); "issue.N" prints as the parenthesized issue.
|
|
42
|
-
while parts[idx] && part_kind?(parts[idx])
|
|
43
|
-
kind, value = parts[idx].split(".", 2)
|
|
44
|
-
if kind.downcase == "issue"
|
|
45
|
-
text += " (#{value})"
|
|
46
|
-
else
|
|
47
|
-
label = PART_LABELS.fetch(kind.downcase) { kind.capitalize }
|
|
48
|
-
text += " #{label} #{value}"
|
|
49
|
-
end
|
|
50
|
-
idx += 1
|
|
51
|
-
end
|
|
52
|
-
tail = parts[idx]
|
|
47
|
+
private
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
def untagged_number?(segment)
|
|
50
|
+
!tagged?(segment) && !segment.match?(VERSION_SEGMENT)
|
|
56
51
|
end
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
def tagged?(segment)
|
|
54
|
+
segment.match?(/\A[a-z]+\./i)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def segment_text(segment, urn)
|
|
58
|
+
return " #{segment}" if segment.match?(VERSION_SEGMENT)
|
|
59
|
+
|
|
60
|
+
tag, value = segment.split(".", 2)
|
|
61
|
+
render = TAG_TEXT[tag.downcase] if tagged?(segment)
|
|
62
|
+
unless render
|
|
63
|
+
raise Pubid::Errors::UrnParseError,
|
|
64
|
+
"Unexpected segment #{segment.inspect} in #{urn.inspect}"
|
|
65
|
+
end
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
# "part.4a", "ap.A", "annex.A", "suppl.1". A version like "5.0.0"
|
|
62
|
-
# starts with a digit.
|
|
63
|
-
def part_kind?(token)
|
|
64
|
-
token.match?(/^[a-z]+\./i)
|
|
67
|
+
render.call(value)
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
data/lib/pubid/iso/CLAUDE.md
CHANGED
|
@@ -45,3 +45,69 @@
|
|
|
45
45
|
needs a lookbehind, because `tc_number.value` and `edition.number.value` are
|
|
46
46
|
different attributes — the first pass stripped `edition.number.value` too,
|
|
47
47
|
and two corrigendum examples caught it.
|
|
48
|
+
|
|
49
|
+
- **Typed model bindings (Phase D): the builder consumes the grammar's bound
|
|
50
|
+
map, not a parse tree.** `Identifier.parse` and `Normalizer` parse through
|
|
51
|
+
`Pubid::Parg::Model.bound(:iso, …)`; `UrnParser#build_identifier` constructs
|
|
52
|
+
the same bound-map dialect. The grammar is edition 2 with every
|
|
53
|
+
builder-consumed capture bound (`grammars/iso.parg`, `bindings identifier`).
|
|
54
|
+
The conversion preserved byte fidelity: `to_s` and `to_hash` are identical
|
|
55
|
+
for all 7,859 fixture inputs replayed against a `main` baseline, and the
|
|
56
|
+
wasm engine agrees with Ruby on every tree and bound map (the
|
|
57
|
+
`corpus-parity` gate). **What to know before editing either side:**
|
|
58
|
+
|
|
59
|
+
- **Supplement layers capture into position-scoped prefixes** — `supp_*`
|
|
60
|
+
(first supplement), `supp2_*`/`supp3_*` (chain layers), `isor_supp_*`
|
|
61
|
+
(ISO/R supplement), `dsupp_*` (directive supplement), `bs1_/bs2_/bs3_`
|
|
62
|
+
(bundle items), `idf_*/idf_supp_*` (the joint's IDF side, whose rules are
|
|
63
|
+
inlined from the imported idf grammar so the names never collide with the
|
|
64
|
+
ISO ones). Bindings.collect_captures collects captures globally by name
|
|
65
|
+
and delivers the FIRST occurrence, so shared names across layers would
|
|
66
|
+
collapse a layer's number/date onto the base's. `Builder#build` peels the
|
|
67
|
+
layers outermost-first, re-keying each prefix onto the builder-input
|
|
68
|
+
names and nesting the rest as the layer's `:base`.
|
|
69
|
+
- **The base document's own edition captures as `base_edition`** wherever a
|
|
70
|
+
supplement layer's third_part can carry its own edition
|
|
71
|
+
(`supplement_base_no_third`, the joint base). The builder re-keys it onto
|
|
72
|
+
`:edition` only at the innermost base — moving it inside a peel step
|
|
73
|
+
mis-assigns the edition to the wrapper on multi-layer chains (the
|
|
74
|
+
`ISO/IEC Guide 98-3 ED1/Suppl 1:2008/Cor 1:2009` spec caught that).
|
|
75
|
+
- **No array bindings.** The wasm engine's array-group harvest pads
|
|
76
|
+
non-firing reps with empty hashes (parsanol-ruby#399), so Ruby and wasm
|
|
77
|
+
bound maps disagree on any input whose parse backtracks past the array
|
|
78
|
+
branch. Two consequences: copublishers capture as ONE scalar span
|
|
79
|
+
(`"/IEC/IEEE"`, at-least-one item, absent when none) that the builder
|
|
80
|
+
splits, and bundle items capture into `bs1_/bs2_/bs3_` prefixes the
|
|
81
|
+
builder assembles into `supplements`. Verified to hold unchanged at the
|
|
82
|
+
1.3.121 pin (the capture-flatten release): the re-baked artifact's
|
|
83
|
+
grammar/entries/bindings/tests/tables are byte-identical to the 1.3.114
|
|
84
|
+
bake. A newer engine's `(raw)` wrapper interiors would replace both
|
|
85
|
+
shapes — re-visit when such a wrapper ships.
|
|
86
|
+
- **The artifact exposes `["identifier", "idf.identifier"]`** — the
|
|
87
|
+
`use idf` import survives the conversion so the joint composition's
|
|
88
|
+
entry surface stays available (the fleet gate asserts the entry list).
|
|
89
|
+
The joint's IDF side itself binds as `idf_*` scalars; the pilot's
|
|
90
|
+
`publisher -> publisher_name (string) preprocess: publisher_names`
|
|
91
|
+
binding is also kept (the T1 materialization asserts the full
|
|
92
|
+
organization name), and the Ruby builder's `cast` returns nil for
|
|
93
|
+
`:publisher_name` — the Ruby identifier has no such attribute. The
|
|
94
|
+
Ruby artifact load resolves `use idf` against `data/parg/idf.parg`,
|
|
95
|
+
which sits beside the vendored artifact.
|
|
96
|
+
- **Captures must inline their bodies to raw terms.** A capture wrapping a
|
|
97
|
+
bare rule ref exposes the ref's child nodes and cannot bind as a string
|
|
98
|
+
(`cannot bind a Array capture`), and date/language bodies are inlined at
|
|
99
|
+
all capture sites for that reason. Multi-line rules must be brace bodies
|
|
100
|
+
(line-mode rejects newlines inside groups).
|
|
101
|
+
- **The URN parser emits the same dialect**: its supplement loop writes
|
|
102
|
+
`supp_`/`supp2_`/`supp3_`-prefixed flat keys (the prefixes the builder
|
|
103
|
+
peels), not `{base:, **supp}` nesting; the builder peels `supp_year` and
|
|
104
|
+
`supp_supplement_version` the same way by prefix. Its raw-stage fallback
|
|
105
|
+
(`:stage`), `:document_type` and `:supplement_version` keys are
|
|
106
|
+
builder-input names, not grammar captures, and pass through `cast`
|
|
107
|
+
unchanged.
|
|
108
|
+
- **Grammar probes**: `ruby -e 'gem "parsanol", "1.3.121"; …'` against the
|
|
109
|
+
artifact (`parse_and_bind`) shows the bound map; `Model.bound(:iso, …)`
|
|
110
|
+
shows the builder's actual input. The artifact and its vendored twin
|
|
111
|
+
(`data/parg/iso.parg`) must move together — the twin is the embedded
|
|
112
|
+
source's provenance copy.
|
|
113
|
+
|
data/lib/pubid/iso/builder.rb
CHANGED
|
@@ -66,6 +66,84 @@ module Pubid
|
|
|
66
66
|
Pubid::Iso.locate_type(typed_stage.type_code)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Bound-hash layer fields: the grammar captures every supplement
|
|
70
|
+
# layer into a disjoint prefix (supp_ on the first supplement,
|
|
71
|
+
# supp2_ / supp3_ on the chain's later layers, isor_supp_ on an
|
|
72
|
+
# ISO/R supplement, dsupp_ on a directive supplement), so each
|
|
73
|
+
# wrapper's fields peel off the flat bound hash by prefix and
|
|
74
|
+
# re-key onto the builder-input names.
|
|
75
|
+
SUPPLEMENT_LAYERS = [
|
|
76
|
+
{
|
|
77
|
+
prefix: "supp3_",
|
|
78
|
+
keys: %i[supp3_type_with_stage supp3_number_with_part supp3_stage_iteration
|
|
79
|
+
supp3_number supp3_date supp3_undated_marker supp3_edition
|
|
80
|
+
supp3_languages supp3_all_parts],
|
|
81
|
+
plain_third_part: false,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
prefix: "supp2_",
|
|
85
|
+
keys: %i[supp2_type_with_stage supp2_number_with_part supp2_stage_iteration
|
|
86
|
+
supp2_number supp2_date supp2_undated_marker supp2_edition
|
|
87
|
+
supp2_languages supp2_all_parts],
|
|
88
|
+
plain_third_part: false,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
prefix: "supp_",
|
|
92
|
+
keys: %i[supp_type_with_stage supp_number_with_part supp_stage_iteration
|
|
93
|
+
supp_number supp_date supp_undated_marker supp_edition
|
|
94
|
+
supp_languages supp_all_parts],
|
|
95
|
+
# The first supplement's third_part keeps the plain names: the
|
|
96
|
+
# base document's own edition captures as base_edition, so the
|
|
97
|
+
# plain edition/languages/all_parts belong to the supplement.
|
|
98
|
+
plain_third_part: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
prefix: "isor_supp_",
|
|
102
|
+
keys: %i[isor_supp_type_with_stage isor_supp_number_with_part
|
|
103
|
+
isor_supp_stage_iteration isor_supp_date isor_supp_undated_marker],
|
|
104
|
+
plain_third_part: true,
|
|
105
|
+
},
|
|
106
|
+
].freeze
|
|
107
|
+
PLAIN_THIRD_PART_KEYS = %i[edition languages all_parts].freeze
|
|
108
|
+
|
|
109
|
+
# Directive-supplement layer (ISO/IEC DIR 1 ISO SUP): dsupp_* fields
|
|
110
|
+
# re-key onto the supplement hash, the base directive's fields stay
|
|
111
|
+
# plain.
|
|
112
|
+
DSUPP_KEYS = %i[dsupp_publisher dsupp_type_with_stage dsupp_date
|
|
113
|
+
dsupp_undated_marker dsupp_edition dsupp_languages
|
|
114
|
+
dsupp_all_parts].freeze
|
|
115
|
+
|
|
116
|
+
# Bundled directive items capture into position-scoped prefixes
|
|
117
|
+
# (one per bundle item — the engine's array-group harvest pads
|
|
118
|
+
# non-firing reps with empty hashes, parsanol-ruby#399, so bundle
|
|
119
|
+
# items cannot collect through an array binding).
|
|
120
|
+
BUNDLE_PREFIXES = %w[bs1_ bs2_ bs3_].freeze
|
|
121
|
+
|
|
122
|
+
# Joint identifier (ISO x|IDF y): the IDF side captures under idf_*
|
|
123
|
+
# names; they re-key onto the tree-shape hash Pubid::Idf.build_from_parse
|
|
124
|
+
# consumes.
|
|
125
|
+
IDF_KEYS = %i[idf_publisher idf_type_with_stage idf_stage_iteration
|
|
126
|
+
idf_number_with_part idf_date idf_languages idf_all_parts
|
|
127
|
+
idf_supp_type_with_stage idf_supp_number_with_part2
|
|
128
|
+
idf_supp_stage_iteration2 idf_supp_date2
|
|
129
|
+
idf_supp_undated_marker].freeze
|
|
130
|
+
IDF_REKEY = {
|
|
131
|
+
idf_publisher: :publisher,
|
|
132
|
+
idf_type_with_stage: :type_with_stage,
|
|
133
|
+
idf_stage_iteration: :stage_iteration,
|
|
134
|
+
idf_number_with_part: :number_with_part,
|
|
135
|
+
idf_date: :date,
|
|
136
|
+
idf_languages: :languages,
|
|
137
|
+
idf_all_parts: :all_parts,
|
|
138
|
+
}.freeze
|
|
139
|
+
IDF_SUPP_REKEY = {
|
|
140
|
+
idf_supp_type_with_stage: :type_with_stage,
|
|
141
|
+
idf_supp_number_with_part2: :number_with_part,
|
|
142
|
+
idf_supp_stage_iteration2: :stage_iteration,
|
|
143
|
+
idf_supp_date2: :date,
|
|
144
|
+
idf_supp_undated_marker: :undated_marker,
|
|
145
|
+
}.freeze
|
|
146
|
+
|
|
69
147
|
def build(parsed_hash)
|
|
70
148
|
# "(all parts)" and the ":ser" URN name every part of the document,
|
|
71
149
|
# so they build an AllPartsIdentifier around the document (the URN
|
|
@@ -73,6 +151,51 @@ module Pubid
|
|
|
73
151
|
# reason). The document itself holds no all-parts mark.
|
|
74
152
|
all_parts = parsed_hash.delete(:all_parts)
|
|
75
153
|
|
|
154
|
+
# The copublisher tail captures as one scalar span ("/IEC/IEEE");
|
|
155
|
+
# split it into the {copublisher:} element hashes the merge and
|
|
156
|
+
# the :copublishers cast consume. A pre-built array (the URN
|
|
157
|
+
# parser) passes through.
|
|
158
|
+
if parsed_hash[:copublishers].is_a?(String)
|
|
159
|
+
parsed_hash[:copublishers] = parsed_hash.delete(:copublishers)
|
|
160
|
+
.split("/").map(&:strip).reject(&:empty?)
|
|
161
|
+
.map { |cp| { copublisher: cp } }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Peel supplement layers outermost-first: each layer re-keys its
|
|
165
|
+
# prefixed fields onto the builder-input names, nests the rest as
|
|
166
|
+
# its :base, and re-enters build — the same shape the grammar's
|
|
167
|
+
# tree used to nest.
|
|
168
|
+
SUPPLEMENT_LAYERS.each do |layer|
|
|
169
|
+
next unless parsed_hash.keys.any? do |key|
|
|
170
|
+
key.to_s.start_with?(layer[:prefix])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return build_supplement_layer(parsed_hash, layer, all_parts)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Joint identifier (ISO x|IDF y): the base designation's fields
|
|
177
|
+
# stay plain, the IDF side re-keys into the tree-shape hash the
|
|
178
|
+
# IDF builder consumes, and the pair routes through the
|
|
179
|
+
# CombinedIdentifier via the :joint_identifier key.
|
|
180
|
+
if parsed_hash.key?(:idf_publisher)
|
|
181
|
+
return build_joint_identifier(parsed_hash, all_parts)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Directive supplement (ISO/IEC DIR 1 ISO SUP): the dsupp_* fields
|
|
185
|
+
# are the supplement's, the plain fields its base directive's.
|
|
186
|
+
if parsed_hash.key?(:dsupp_type_with_stage)
|
|
187
|
+
return build_directive_supplement(parsed_hash, all_parts)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Bundled directives ("DIR 1:2022 + IEC SUP:2022"): the plain
|
|
191
|
+
# fields are the base document's; each bundle item captures under
|
|
192
|
+
# its bs1_/bs2_/bs3_ prefix.
|
|
193
|
+
if BUNDLE_PREFIXES.any? do |prefix|
|
|
194
|
+
parsed_hash.keys.any? { |key| key.to_s.start_with?(prefix) }
|
|
195
|
+
end
|
|
196
|
+
return build_bundled(parsed_hash, all_parts)
|
|
197
|
+
end
|
|
198
|
+
|
|
76
199
|
# For ISO/R legacy format, split into publisher and type
|
|
77
200
|
if parsed_hash[:iso_r_prefix]
|
|
78
201
|
parsed_hash[:publisher] = "ISO"
|
|
@@ -80,6 +203,12 @@ module Pubid
|
|
|
80
203
|
parsed_hash.delete(:iso_r_prefix)
|
|
81
204
|
end
|
|
82
205
|
|
|
206
|
+
# The base document's own edition captures as base_edition (the
|
|
207
|
+
# supplement layers' third_part editions own the plain key), so
|
|
208
|
+
# it re-keys onto :edition only here — at the innermost base,
|
|
209
|
+
# after every wrapper layer has been peeled off.
|
|
210
|
+
parsed_hash[:edition] = parsed_hash.delete(:base_edition) if parsed_hash.key?(:base_edition)
|
|
211
|
+
|
|
83
212
|
# For NSB-prefixed identifiers (FprISO, WD/ISO), set stage
|
|
84
213
|
if parsed_hash[:nsb_stage]
|
|
85
214
|
nsb_stage = parsed_hash.delete(:nsb_stage)
|
|
@@ -94,12 +223,11 @@ module Pubid
|
|
|
94
223
|
|
|
95
224
|
# For French GUIDE entries: "Guide ISO/CEI 37:1995". The rename
|
|
96
225
|
# must happen BEFORE class selection — locate_identifier_klass
|
|
97
|
-
# reads :type_with_stage, and the
|
|
98
|
-
# carries
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
if type_with_stage_fr = parsed_hash.delete(:type_with_stage_fr)
|
|
226
|
+
# reads :type_with_stage, and the bound map of a guide-first
|
|
227
|
+
# spelling carries the type under :type_with_stage_fr, so
|
|
228
|
+
# deferring the rename selected the default International
|
|
229
|
+
# Standard class for every guide-first reference.
|
|
230
|
+
if (type_with_stage_fr = parsed_hash.delete(:type_with_stage_fr))
|
|
103
231
|
parsed_hash[:type_with_stage] = type_with_stage_fr
|
|
104
232
|
end
|
|
105
233
|
|
|
@@ -136,6 +264,89 @@ module Pubid
|
|
|
136
264
|
all_parts ? identifier.to_all_parts : identifier
|
|
137
265
|
end
|
|
138
266
|
|
|
267
|
+
# "ISO 5537|IDF 26": the ISO side builds as the combined
|
|
268
|
+
# identifier's base, the IDF side re-keys into the tree-shape hash
|
|
269
|
+
# Pubid::Idf.build_from_parse consumes and lands in
|
|
270
|
+
# :joint_identifier.
|
|
271
|
+
def build_joint_identifier(hash, all_parts)
|
|
272
|
+
base_fields = hash.except(*IDF_KEYS)
|
|
273
|
+
idf_fields = rekey(hash.slice(*IDF_REKEY.keys), IDF_REKEY)
|
|
274
|
+
|
|
275
|
+
if (supp_keys = IDF_SUPP_REKEY.keys & hash.keys).any?
|
|
276
|
+
idf_fields = {
|
|
277
|
+
base: idf_fields,
|
|
278
|
+
**rekey(hash.slice(*supp_keys), IDF_SUPP_REKEY),
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
inner = { base: base_fields, joint_identifier: idf_fields }
|
|
283
|
+
inner[:all_parts] = all_parts if all_parts
|
|
284
|
+
build(inner)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# One supplement layer: re-key the layer's prefixed fields, move
|
|
288
|
+
# the shared plain third-part fields onto the layer where the
|
|
289
|
+
# grammar captures them there, nest everything else as the layer's
|
|
290
|
+
# base, and re-enter build.
|
|
291
|
+
def build_supplement_layer(hash, layer, all_parts)
|
|
292
|
+
prefix = layer[:prefix]
|
|
293
|
+
layer_hash = hash.select { |key, _| key.to_s.start_with?(prefix) }
|
|
294
|
+
fields = rekey_prefix(layer_hash, prefix)
|
|
295
|
+
|
|
296
|
+
if layer[:plain_third_part]
|
|
297
|
+
third_part = hash.slice(*PLAIN_THIRD_PART_KEYS)
|
|
298
|
+
fields = { **third_part, **fields }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
rest = hash.except(*layer_hash.keys)
|
|
302
|
+
rest = rest.except(*PLAIN_THIRD_PART_KEYS) if layer[:plain_third_part]
|
|
303
|
+
|
|
304
|
+
inner = { **fields, base: rest }
|
|
305
|
+
inner[:all_parts] = all_parts if all_parts
|
|
306
|
+
build(inner)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def build_directive_supplement(hash, all_parts)
|
|
310
|
+
fields = rekey_prefix(hash.slice(*DSUPP_KEYS), "dsupp_")
|
|
311
|
+
rest = hash.except(*DSUPP_KEYS)
|
|
312
|
+
|
|
313
|
+
inner = { **fields, base: rest }
|
|
314
|
+
inner[:all_parts] = all_parts if all_parts
|
|
315
|
+
build(inner)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Bundled directives: each bundle item's prefixed fields re-key to
|
|
319
|
+
# the supplement hash and build a DirectivesSupplement, the plain
|
|
320
|
+
# fields build the base document, and the pair lands on a
|
|
321
|
+
# BundledIdentifier.
|
|
322
|
+
def build_bundled(hash, all_parts)
|
|
323
|
+
bundle_keys = BUNDLE_PREFIXES.flat_map do |prefix|
|
|
324
|
+
hash.keys.select { |key| key.to_s.start_with?(prefix) }
|
|
325
|
+
end
|
|
326
|
+
supplements = BUNDLE_PREFIXES.filter_map do |prefix|
|
|
327
|
+
keys = hash.keys.select { |key| key.to_s.start_with?(prefix) }
|
|
328
|
+
next if keys.empty?
|
|
329
|
+
|
|
330
|
+
build(rekey_prefix(hash.slice(*keys), prefix))
|
|
331
|
+
end
|
|
332
|
+
identifier = BundledIdentifier.new
|
|
333
|
+
identifier.base_document = build(hash.except(*bundle_keys))
|
|
334
|
+
identifier.supplements = supplements
|
|
335
|
+
all_parts ? identifier.to_all_parts : identifier
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Strip a layer prefix from every key ("supp_" / "supp2_" /
|
|
339
|
+
# "supp3_" / "isor_supp_" / "dsupp_").
|
|
340
|
+
def rekey_prefix(hash, prefix)
|
|
341
|
+
hash.to_h do |key, value|
|
|
342
|
+
[key.to_s.delete_prefix(prefix).to_sym, value]
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def rekey(hash, map)
|
|
347
|
+
hash.to_h { |key, value| [map[key], value] }
|
|
348
|
+
end
|
|
349
|
+
|
|
139
350
|
def handle_key(identifier, key, value)
|
|
140
351
|
if key == :joint_identifier
|
|
141
352
|
identifier.additional_identifiers ||= []
|
|
@@ -150,8 +361,8 @@ module Pubid
|
|
|
150
361
|
# "4037/1979", "4037-1979", and "ISO/IEC DIR 1 IEC" all split cleanly.
|
|
151
362
|
def normalize_number_with_part(value)
|
|
152
363
|
value.to_s
|
|
153
|
-
|
|
154
|
-
|
|
364
|
+
.tr("#{Parser::DASH_CHARS.join}/", "-")
|
|
365
|
+
.gsub(" ", "-")
|
|
155
366
|
end
|
|
156
367
|
|
|
157
368
|
# LEGACY: "ISO 4037-1979" used a hyphen where the modern form is
|
|
@@ -190,7 +401,7 @@ module Pubid
|
|
|
190
401
|
# (e.g. a TC document, which skips the copublisher merge) matches
|
|
191
402
|
# the [] convention used by copublisher-merged parses and by
|
|
192
403
|
# Identifier.create. Otherwise equality fails on [] vs nil.
|
|
193
|
-
Pubid::Iso::Components::Publisher.new(publisher: value,
|
|
404
|
+
Pubid::Iso::Components::Publisher.new(publisher: value,
|
|
194
405
|
copublisher: [])
|
|
195
406
|
end
|
|
196
407
|
|
|
@@ -285,10 +496,17 @@ module Pubid
|
|
|
285
496
|
# Handle JTC 1 subgroup in directives (ISO/IEC JTC 1 DIR)
|
|
286
497
|
value.to_s
|
|
287
498
|
|
|
499
|
+
when :publisher_name
|
|
500
|
+
# The grammar's publisher_names preprocess binding (the ISO
|
|
501
|
+
# organization's full name, consumed by the ts artifact's
|
|
502
|
+
# schema materialization). The Ruby identifier carries only
|
|
503
|
+
# the Publisher component, so the field assigns nothing.
|
|
504
|
+
nil
|
|
505
|
+
|
|
288
506
|
when :supplements
|
|
289
507
|
# Handle bundled supplements (+ operator)
|
|
290
508
|
# Each supplement is a hash that needs to be built
|
|
291
|
-
value.map { |supplement_hash| build(supplement_hash
|
|
509
|
+
value.map { |supplement_hash| build(supplement_hash) }
|
|
292
510
|
|
|
293
511
|
when :base_document
|
|
294
512
|
# For bundled identifiers, build the base document
|
|
@@ -301,10 +519,6 @@ module Pubid
|
|
|
301
519
|
when :tc_number, :sc_number, :wg_number
|
|
302
520
|
value.to_s
|
|
303
521
|
|
|
304
|
-
when :year
|
|
305
|
-
# For TC documents with year, convert to Date
|
|
306
|
-
Pubid::Components::Date.new(year: value.to_s)
|
|
307
|
-
|
|
308
522
|
when :number
|
|
309
523
|
# For TC documents, number is the document number (N number)
|
|
310
524
|
# For regular identifiers, this is handled in :number_with_part
|