pubid 2.0.19 → 2.0.21
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/iho/s4-reserved-specs.yaml +15 -0
- data/data/parg/bipm.json +1 -1
- data/data/parg/bipm.parg +46 -11
- data/data/parg/iho.json +1 -1
- data/data/parg/iho.parg +128 -11
- data/data/parg/plateau.json +1 -1
- data/lib/pubid/bipm/CLAUDE.md +6 -1
- data/lib/pubid/bipm/builder.rb +20 -1
- data/lib/pubid/bipm/renderer.rb +14 -2
- data/lib/pubid/bipm/urn_generator.rb +80 -14
- data/lib/pubid/bipm/urn_parser.rb +70 -6
- data/lib/pubid/ecma/CLAUDE.md +12 -0
- data/lib/pubid/ecma/identifier.rb +1 -2
- data/lib/pubid/ecma/parser.rb +49 -0
- data/lib/pubid/identifier.rb +4 -3
- data/lib/pubid/iec/CLAUDE.md +20 -11
- data/lib/pubid/iec/builder.rb +19 -1
- data/lib/pubid/iec/identifier.rb +19 -70
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +3 -3
- data/lib/pubid/iec/identifiers/test_report_form.rb +29 -0
- data/lib/pubid/iec/identifiers/working_document.rb +23 -6
- data/lib/pubid/iec/pubid_urn_schema.rb +15 -7
- data/lib/pubid/iec/renderer.rb +8 -4
- data/lib/pubid/iec/single_identifier.rb +69 -4
- data/lib/pubid/iec/supplement_identifier.rb +52 -76
- data/lib/pubid/iho/CLAUDE.md +52 -0
- data/lib/pubid/iho/builder.rb +105 -2
- data/lib/pubid/iho/identifiers/base.rb +52 -1
- data/lib/pubid/iho/pubid_urn_schema.rb +11 -4
- data/lib/pubid/iho/renderer.rb +39 -2
- data/lib/pubid/iho/reserved_specs.rb +54 -0
- data/lib/pubid/iho/urn_generator.rb +6 -0
- data/lib/pubid/iho/urn_parser.rb +50 -9
- data/lib/pubid/iho.rb +1 -0
- data/lib/pubid/nist/CLAUDE.md +11 -0
- data/lib/pubid/nist/preprocessor.rb +16 -0
- data/lib/pubid/plateau/CLAUDE.md +35 -0
- data/lib/pubid/plateau/builder.rb +4 -0
- data/lib/pubid/plateau/identifiers/handbook.rb +10 -2
- data/lib/pubid/plateau/pubid_urn_schema.rb +1 -0
- data/lib/pubid/plateau/renderer.rb +6 -1
- data/lib/pubid/sae/CLAUDE.md +11 -2
- data/lib/pubid/version.rb +1 -1
- metadata +18 -2
data/lib/pubid/iho/builder.rb
CHANGED
|
@@ -18,18 +18,34 @@ module Pubid
|
|
|
18
18
|
part_edition part_year supplement_edition
|
|
19
19
|
supplement_year annex_edition annex_year
|
|
20
20
|
appendix_edition appendix_year volume_edition
|
|
21
|
-
volume_year
|
|
21
|
+
volume_year
|
|
22
|
+
spec spec_sub spec_variant spec_part
|
|
23
|
+
spec_section].freeze
|
|
22
24
|
# The independently-publishable components' own editions; the
|
|
23
25
|
# base edition context is mutually exclusive with them (owner
|
|
24
26
|
# ruling on the two publication modes).
|
|
25
27
|
COMPONENT_EDITIONS = %i[part_edition supplement_edition annex_edition
|
|
26
28
|
appendix_edition volume_edition].freeze
|
|
29
|
+
# The S-4 anchor: the loose-leaf publication every spec citation
|
|
30
|
+
# resolves through (bare "B-401" carries it implicitly).
|
|
31
|
+
S4_TYPE = "S"
|
|
32
|
+
S4_NUMBER = "4"
|
|
33
|
+
# A citation letter is one of S-4's lettered Parts; the section
|
|
34
|
+
# vocabulary (100-600, Part C 100-500) is theirs.
|
|
35
|
+
SPEC_LETTERS = %w[A B C].freeze
|
|
36
|
+
# Components an S-4 spec citation cannot carry: the citation is a
|
|
37
|
+
# leaf of the Part, not a wrappable publication.
|
|
38
|
+
SPEC_FOREIGN_COMPONENTS = %i[supplement annex appendix
|
|
39
|
+
appendix_subnumber volume issue].freeze
|
|
27
40
|
|
|
28
41
|
def build(parsed)
|
|
29
42
|
hash = parsed.is_a?(Array) ? parsed.reduce({}, :merge) : parsed
|
|
30
43
|
attrs = stringified_attrs(hash)
|
|
44
|
+
resolve_spec_citation!(attrs)
|
|
45
|
+
type = type_letter_from(hash, attrs)
|
|
46
|
+
assert_spec_anchor!(attrs, type)
|
|
31
47
|
assert_edition_mode!(attrs)
|
|
32
|
-
klass = Pubid::Iho.identifier_klass_for_type_letter(
|
|
48
|
+
klass = Pubid::Iho.identifier_klass_for_type_letter(type)
|
|
33
49
|
klass.new(**attrs)
|
|
34
50
|
end
|
|
35
51
|
|
|
@@ -39,6 +55,93 @@ module Pubid
|
|
|
39
55
|
|
|
40
56
|
private
|
|
41
57
|
|
|
58
|
+
# The citation captures resolve onto the model (see the grammar
|
|
59
|
+
# header): the citation letter folds into `part` (a Part-rule
|
|
60
|
+
# capture must agree), and the long form's spelled section becomes
|
|
61
|
+
# the spec alone — beside a citation it only cross-checks the
|
|
62
|
+
# citation's derived section ("Part B, Section 400, B-401" stores
|
|
63
|
+
# spec 401; "Part B, Section 500, B-401" is rejected).
|
|
64
|
+
def resolve_spec_citation!(attrs)
|
|
65
|
+
letter = attrs.delete(:spec_part)
|
|
66
|
+
spelled = attrs.delete(:spec_section)
|
|
67
|
+
attrs[:part] = merge_citation_letter!(attrs[:part], letter) if letter
|
|
68
|
+
apply_spelled_section!(attrs, spelled)
|
|
69
|
+
assert_citation_shape!(attrs) if attrs[:spec]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_spelled_section!(attrs, spelled)
|
|
73
|
+
return if spelled.nil?
|
|
74
|
+
return attrs[:spec] = spelled unless attrs[:spec]
|
|
75
|
+
|
|
76
|
+
return if spelled == spec_section_of(attrs[:spec])
|
|
77
|
+
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"the spelled section #{spelled} disagrees with the " \
|
|
80
|
+
"citation #{attrs[:part]}-#{attrs[:spec]}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def spec_section_of(spec)
|
|
84
|
+
"#{spec[0]}00"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def merge_citation_letter!(part, letter)
|
|
88
|
+
return letter if part.nil? || part == letter
|
|
89
|
+
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"the citation letter #{letter} conflicts with part #{part}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assert_citation_shape!(attrs)
|
|
95
|
+
assert_citation_letter!(attrs)
|
|
96
|
+
assert_citation_leaf!(attrs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def assert_citation_letter!(attrs)
|
|
100
|
+
return if SPEC_LETTERS.include?(attrs[:part])
|
|
101
|
+
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"an S-4 spec citation needs a lettered Part (A/B/C), " \
|
|
104
|
+
"not #{attrs[:part].inspect}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The citation is a leaf of its Part: it carries no component
|
|
108
|
+
# edition and none of the wrappable components.
|
|
109
|
+
def assert_citation_leaf!(attrs)
|
|
110
|
+
if attrs[:part_edition] || attrs[:part_year]
|
|
111
|
+
raise ArgumentError,
|
|
112
|
+
"a spec citation rides the S-4 edition, not a part edition"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
foreign = attrs.slice(*SPEC_FOREIGN_COMPONENTS).compact.keys
|
|
116
|
+
return if foreign.empty?
|
|
117
|
+
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"an S-4 spec citation is a leaf of its Part and cannot " \
|
|
120
|
+
"carry #{foreign.join(', ')}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The series letter: a bare citation branch parse has no series
|
|
124
|
+
# capture, and its anchor is implicitly the S-4 publication (the
|
|
125
|
+
# number binds with it).
|
|
126
|
+
def type_letter_from(hash, attrs)
|
|
127
|
+
type = stringify(hash[:type])
|
|
128
|
+
return type if type
|
|
129
|
+
|
|
130
|
+
raise ArgumentError, "unknown IHO series" unless attrs[:spec]
|
|
131
|
+
|
|
132
|
+
attrs[:number] = S4_NUMBER
|
|
133
|
+
S4_TYPE
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def assert_spec_anchor!(attrs, type)
|
|
137
|
+
return unless attrs[:spec]
|
|
138
|
+
return if type == S4_TYPE && attrs[:number] == S4_NUMBER
|
|
139
|
+
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"an S-4 spec citation requires the S-4 anchor: " \
|
|
142
|
+
"#{type}-#{attrs[:number]} cannot carry spec #{attrs[:spec]}"
|
|
143
|
+
end
|
|
144
|
+
|
|
42
145
|
# The two publication modes are mutually exclusive: an identifier
|
|
43
146
|
# is either within-edition (a base edition its components ride) or
|
|
44
147
|
# separately-published (a component carries its own edition and
|
|
@@ -8,6 +8,11 @@ module Pubid
|
|
|
8
8
|
# IHO identifiers have the form:
|
|
9
9
|
# IHO {S|P|M|B|C}-{number}[ {edition}| Vol. N [(issue)]]
|
|
10
10
|
# [ Sup N][ Part X][ Annex A][ Appendix B[-2]] [(2024)]
|
|
11
|
+
# The S-4 loose-leaf publication adds the spec citation
|
|
12
|
+
# IHO S-4 [edition] <letter>-<hundreds><seq>[.sub][(variant)]
|
|
13
|
+
# ("IHO S-4 B-401", the long form "S-4 Part B, Section 400" and the
|
|
14
|
+
# bare "B-401" all parse to it; `spec` stores the leaf number, the
|
|
15
|
+
# citation letter rides `part`, the section is a derived reader).
|
|
11
16
|
#
|
|
12
17
|
# Every tail component is a first-class semantic component: a typed
|
|
13
18
|
# attribute, independently addressable, URN-encodable and matchable.
|
|
@@ -83,6 +88,15 @@ module Pubid
|
|
|
83
88
|
attribute :issue, :string
|
|
84
89
|
attribute :volume_edition, :string
|
|
85
90
|
attribute :volume_year, :string
|
|
91
|
+
# The S-4 loose-leaf spec citation (issue #691): `spec` is the leaf
|
|
92
|
+
# number ("401" of B-401), the citation letter rides `part` (the
|
|
93
|
+
# builder folds it in), the dotted sub and the parenthesized
|
|
94
|
+
# variant keep their own components. The SECTION (the hundreds
|
|
95
|
+
# digit) is never stored — a derived reader — and the `00` slot
|
|
96
|
+
# (B-400) IS the section-level citation.
|
|
97
|
+
attribute :spec, :string
|
|
98
|
+
attribute :spec_sub, :string
|
|
99
|
+
attribute :spec_variant, :string
|
|
86
100
|
|
|
87
101
|
# Polymorphic type map for lutaml::Model key_value serialization: maps
|
|
88
102
|
# each subclass's polymorphic_name to its class name so `from_hash` can
|
|
@@ -122,6 +136,9 @@ module Pubid
|
|
|
122
136
|
map "volume_edition", to: :volume_edition
|
|
123
137
|
map "volume_year", to: :volume_year
|
|
124
138
|
map "issue", to: :issue
|
|
139
|
+
map "spec", to: :spec
|
|
140
|
+
map "spec_sub", to: :spec_sub
|
|
141
|
+
map "spec_variant", to: :spec_variant
|
|
125
142
|
end
|
|
126
143
|
|
|
127
144
|
# from_hash is the shared polymorphic dispatch on Pubid::Identifier.
|
|
@@ -139,9 +156,40 @@ module Pubid
|
|
|
139
156
|
args = args.dup
|
|
140
157
|
args << :issue if args.include?(:volume)
|
|
141
158
|
args << :appendix_subnumber if args.include?(:appendix)
|
|
159
|
+
# The spec citation is the leaf: a reference that omits the spec
|
|
160
|
+
# nils its sub and variant with it.
|
|
161
|
+
args.push(:spec_sub, :spec_variant) if args.include?(:spec)
|
|
142
162
|
super
|
|
143
163
|
end
|
|
144
164
|
|
|
165
|
+
# The citation letter of an S-4 spec (nil unless the identifier IS
|
|
166
|
+
# a spec citation): the part carries the letter, the same way the
|
|
167
|
+
# long form spells it ("Part B" of "B-401").
|
|
168
|
+
def spec_part
|
|
169
|
+
spec && part
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The section an S-4 spec belongs to: the hundreds digit of the
|
|
173
|
+
# spec number, zero-padded (B-401 -> "400"; B-400 derives its own
|
|
174
|
+
# section). DERIVED, never stored, never emitted — the owner
|
|
175
|
+
# ruling keeps the spec number the leaf.
|
|
176
|
+
def spec_section
|
|
177
|
+
return nil unless spec
|
|
178
|
+
|
|
179
|
+
"#{spec[0]}00"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# True when the spec number is reserved ("NOT CURRENTLY USED") in
|
|
183
|
+
# the edition's S-4 contents (data/iho/s4-reserved-specs.yaml). An
|
|
184
|
+
# edition-less reference is reserved when any seeded edition
|
|
185
|
+
# reserves the number. A reserved citation parses and resolves AS
|
|
186
|
+
# RESERVED, never invalid (issue #691).
|
|
187
|
+
def reserved?
|
|
188
|
+
return false unless spec
|
|
189
|
+
|
|
190
|
+
ReservedSpecs.reserved?(edition, part, spec)
|
|
191
|
+
end
|
|
192
|
+
|
|
145
193
|
# Render the compact face: the code-adjacent components folded onto
|
|
146
194
|
# the code with separators — the supplement and the part hyphenated
|
|
147
195
|
# (`IHO M-2-2` for `IHO M-2 Sup 2`, `IHO S-5-B` for `IHO S-5 Part B`,
|
|
@@ -153,7 +201,10 @@ module Pubid
|
|
|
153
201
|
def render(format: :human, **opts)
|
|
154
202
|
compact = opts.delete(:compact)
|
|
155
203
|
result = super
|
|
156
|
-
|
|
204
|
+
# A spec citation's canonical face IS the compact form (the
|
|
205
|
+
# citation is already folded onto the code: "IHO S-4 B-401"), so
|
|
206
|
+
# the compact flag is a no-op for it.
|
|
207
|
+
compact && format == :human && !spec ? compact_face(result) : result
|
|
157
208
|
end
|
|
158
209
|
|
|
159
210
|
# The shared mr_string path reads `edition&.number` (the base
|
|
@@ -13,10 +13,11 @@ module Pubid
|
|
|
13
13
|
# edition/year right after it — supplement, part, annex, appendix (the
|
|
14
14
|
# two-level Appendix D-2 keeps the subnumber as its own atom), volume
|
|
15
15
|
# (the unified locator, yearbook volumes included: `vol-2024`), issue.
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# The S-4 spec citation sits inside its part (`spec-`, `specsub-`,
|
|
17
|
+
# `specvar-`). Letters stay case-preserved (`part-A`). The legacy
|
|
18
|
+
# `y.` yearbook marker is decodable in the official URN parser, never
|
|
19
|
+
# emitted here: a periodical has no `year-` atom value, its volume
|
|
20
|
+
# carries the year. The official urn:mrn:iho:… form stays on #to_urn.
|
|
20
21
|
PubidUrnSchema = Pubid::PubidUrn.define("iho") do
|
|
21
22
|
atom "edition", "ed"
|
|
22
23
|
atom "year", "year"
|
|
@@ -26,6 +27,12 @@ module Pubid
|
|
|
26
27
|
atom "part", "part"
|
|
27
28
|
atom "part_edition", "parted"
|
|
28
29
|
atom "part_year", "party"
|
|
30
|
+
# The S-4 loose-leaf spec citation (issue #691): a child of the
|
|
31
|
+
# part (the citation letter rides `part-`); the section is
|
|
32
|
+
# derivable from the spec's hundreds digit — never an atom.
|
|
33
|
+
atom "spec", "spec"
|
|
34
|
+
atom "spec_sub", "specsub"
|
|
35
|
+
atom "spec_variant", "specvar"
|
|
29
36
|
atom "annex", "annex"
|
|
30
37
|
atom "annex_edition", "anned"
|
|
31
38
|
atom "annex_year", "annyear"
|
data/lib/pubid/iho/renderer.rb
CHANGED
|
@@ -19,6 +19,13 @@ module Pubid
|
|
|
19
19
|
# beside an edition is the date
|
|
20
20
|
# qualifier on standards,
|
|
21
21
|
# parenthesized — the only year face
|
|
22
|
+
# "IHO S-4 B-401" the S-4 spec citation (the long
|
|
23
|
+
# form "S-4 Part B, Section 400"
|
|
24
|
+
# parses to the same identity and
|
|
25
|
+
# renders compact: the citation
|
|
26
|
+
# replaces the Part face; the
|
|
27
|
+
# section is derivable, never
|
|
28
|
+
# printed)
|
|
22
29
|
#
|
|
23
30
|
# The renderer is registered as the `:human` format in the IHO format
|
|
24
31
|
# registry and invoked via `render(format: :human)`; the compact face
|
|
@@ -32,12 +39,17 @@ module Pubid
|
|
|
32
39
|
# edition (+ parenthesized year) right after it; the appendix joins
|
|
33
40
|
# its subnumber; the issue rides parenthesized after its volume;
|
|
34
41
|
# the base year prints only as the parenthesized with-year
|
|
35
|
-
# qualifier.
|
|
42
|
+
# qualifier. The S-4 spec citation face (" B-401") sits where the
|
|
43
|
+
# Part face sits — it REPLACES the Part face when a spec is
|
|
44
|
+
# present (the citation letter is the part), printing
|
|
45
|
+
# "<letter>-<number>" with the dotted sub and the parenthesized
|
|
46
|
+
# variant folded on.
|
|
36
47
|
FACES = [
|
|
37
48
|
[:edition, " %s"],
|
|
38
49
|
[:supplement, " Sup %s"], [:supplement_edition, " %s"],
|
|
39
50
|
[:supplement_year, " (%s)"],
|
|
40
51
|
[:part, " Part %s"], [:part_edition, " %s"], [:part_year, " (%s)"],
|
|
52
|
+
[:spec, " %s"],
|
|
41
53
|
[:annex, " Annex %s"], [:annex_edition, " %s"], [:annex_year, " (%s)"],
|
|
42
54
|
[:appendix, " Appendix %s"], [:appendix_subnumber, "-%s"],
|
|
43
55
|
[:appendix_edition, " %s"], [:appendix_year, " (%s)"],
|
|
@@ -50,11 +62,36 @@ module Pubid
|
|
|
50
62
|
id = @id
|
|
51
63
|
rendered = "#{id.publisher} #{id.class.type[:short]}-#{id.number}"
|
|
52
64
|
FACES.each do |component, face|
|
|
53
|
-
value = id
|
|
65
|
+
value = face_value(id, component)
|
|
54
66
|
rendered << (face % value) if value
|
|
55
67
|
end
|
|
56
68
|
rendered
|
|
57
69
|
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# nil = the face does not print. The citation face replaces the
|
|
74
|
+
# Part face (same containment slot: the citation letter IS the
|
|
75
|
+
# part), and it prints only on a spec citation.
|
|
76
|
+
def face_value(id, component)
|
|
77
|
+
if component == :spec
|
|
78
|
+
return nil unless id.spec
|
|
79
|
+
|
|
80
|
+
return spec_citation(id)
|
|
81
|
+
end
|
|
82
|
+
return nil if component == :part && id.spec
|
|
83
|
+
|
|
84
|
+
id.public_send(component)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# "B-401", "B-122.1", "B-521(I)", "B-351.1(I)" — the citation the
|
|
88
|
+
# S-4 contents and cross-references print.
|
|
89
|
+
def spec_citation(id)
|
|
90
|
+
citation = "#{id.part}-#{id.spec}"
|
|
91
|
+
citation << ".#{id.spec_sub}" if id.spec_sub
|
|
92
|
+
citation << "(#{id.spec_variant})" if id.spec_variant
|
|
93
|
+
citation
|
|
94
|
+
end
|
|
58
95
|
end
|
|
59
96
|
end
|
|
60
97
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Pubid
|
|
6
|
+
module Iho
|
|
7
|
+
# The S-4 reserved specification registry
|
|
8
|
+
# (data/iho/s4-reserved-specs.yaml): the "NOT CURRENTLY USED"
|
|
9
|
+
# numbers, edition-keyed (owner rulings of 2026-11-11, issue #691).
|
|
10
|
+
# {Reserved?} answers from this table — a reserved number parses and
|
|
11
|
+
# resolves AS RESERVED, never invalid.
|
|
12
|
+
module ReservedSpecs
|
|
13
|
+
DATA_PATH =
|
|
14
|
+
File.expand_path("../../../data/iho/s4-reserved-specs.yaml", __dir__)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# True when the part-lettered spec number is reserved in the
|
|
18
|
+
# given S-4 edition. An edition-less reference is reserved when
|
|
19
|
+
# ANY seeded edition reserves the number: the number is out of
|
|
20
|
+
# circulation in every edition that names it.
|
|
21
|
+
def reserved?(edition, part, spec)
|
|
22
|
+
return false unless part && spec
|
|
23
|
+
|
|
24
|
+
if edition
|
|
25
|
+
specs_for(edition, part).include?(spec)
|
|
26
|
+
else
|
|
27
|
+
registry.values.any? do |parts|
|
|
28
|
+
specs_for_parts(parts, part).include?(spec)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The edition-keyed registry: edition => part letter => spec
|
|
34
|
+
# numbers.
|
|
35
|
+
def registry
|
|
36
|
+
@registry ||=
|
|
37
|
+
(YAML.safe_load_file(DATA_PATH) || {}).transform_values do |parts|
|
|
38
|
+
parts.transform_values { |specs| Array(specs) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def specs_for(edition, part)
|
|
45
|
+
specs_for_parts(registry.fetch(edition, {}), part)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def specs_for_parts(parts, part)
|
|
49
|
+
parts.fetch(part, [])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -31,6 +31,7 @@ module Pubid
|
|
|
31
31
|
# - urn:iho:m:2:sup-1 IHO M-2 Sup 1
|
|
32
32
|
# - urn:iho:p:5:vol-2024 IHO P-5 Vol. 2024
|
|
33
33
|
# - urn:iho:p:1:vol-30:issue-2 IHO P-1 Vol. 30 (2)
|
|
34
|
+
# - urn:iho:s:4:ed-4.10.0:part-B:spec-401 IHO S-4 4.10.0 B-401
|
|
34
35
|
class UrnGenerator
|
|
35
36
|
attr_reader :identifier
|
|
36
37
|
|
|
@@ -49,6 +50,11 @@ module Pubid
|
|
|
49
50
|
[:supplement, "sup"], [:supplement_edition, "ed"],
|
|
50
51
|
[:supplement_year, "year"],
|
|
51
52
|
[:part, "part"], [:part_edition, "ed"], [:part_year, "year"],
|
|
53
|
+
# the S-4 spec citation: a child of its part (the citation
|
|
54
|
+
# letter rides the part- atom), the dotted sub and the
|
|
55
|
+
# parenthesized variant its own children. The SECTION is NOT
|
|
56
|
+
# emitted — derivable from the spec's hundreds digit.
|
|
57
|
+
[:spec, "spec"], [:spec_sub, "specsub"], [:spec_variant, "specvar"],
|
|
52
58
|
[:annex, "annex"], [:annex_edition, "ed"], [:annex_year, "year"],
|
|
53
59
|
[:appendix, "appendix"], [:appendix_subnumber, nil],
|
|
54
60
|
[:appendix_edition, "ed"], [:appendix_year, "year"],
|
data/lib/pubid/iho/urn_parser.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Pubid
|
|
|
7
7
|
# the segment table lives in the grammar header and the flavor notes):
|
|
8
8
|
#
|
|
9
9
|
# urn:iho:<type>:<number>[:ed-<x.y.z>][:sup-<n>][:part-<X>]
|
|
10
|
-
# [:
|
|
11
|
-
# [:year-<yyyy>]
|
|
10
|
+
# [:spec-<n>[:specsub-<n>][:specvar-<X>]][:annex-<A>]
|
|
11
|
+
# [:appendix-<B>][:<sub>][:vol-<n>][:issue-<n>][:year-<yyyy>]
|
|
12
12
|
#
|
|
13
13
|
# Segment discipline (mirrors the ISO URN parser's strictness):
|
|
14
14
|
# - the docnumber occupies every leading bare number segment up to
|
|
@@ -22,7 +22,11 @@ module Pubid
|
|
|
22
22
|
# directly after its appendix segment (a year is always
|
|
23
23
|
# marker-tagged, so a bare digit there can only be the subnumber);
|
|
24
24
|
# - the issue is allowed only beside its volume and validated as
|
|
25
|
-
# digits
|
|
25
|
+
# digits;
|
|
26
|
+
# - the S-4 spec atom is a child of its part (the citation letter
|
|
27
|
+
# rides the part- atom): specsub-/specvar- only after spec-, and
|
|
28
|
+
# no ed-/year- after a spec node (the spec rides its publication's
|
|
29
|
+
# edition; the section is derivable, never emitted).
|
|
26
30
|
#
|
|
27
31
|
# Legacy 2.0.x spellings remain decodable but are never emitted:
|
|
28
32
|
# the bare a.b.c edition segment, the dotted keys part./annex./
|
|
@@ -38,6 +42,7 @@ module Pubid
|
|
|
38
42
|
# - urn:iho:p:5:vol-2024 → IHO P-5 Vol. 2024
|
|
39
43
|
# - urn:iho:p:1:vol-30:issue-2 → IHO P-1 Vol. 30 (2)
|
|
40
44
|
# - urn:iho:s:158:100 → IHO S-158:100
|
|
45
|
+
# - urn:iho:s:4:ed-4.10.0:part-B:spec-401 → IHO S-4 4.10.0 B-401
|
|
41
46
|
class UrnParser < Pubid::UrnParser::Base
|
|
42
47
|
TYPE_LETTERS = %w[s p m b c].freeze
|
|
43
48
|
|
|
@@ -50,6 +55,9 @@ module Pubid
|
|
|
50
55
|
"ed" => [:edition, /\A\d+\.\d+\.\d+\z/],
|
|
51
56
|
"sup" => [:supplement, /\A\d+\z/],
|
|
52
57
|
"part" => [:part, /\A(?:\d+[a-zA-Z]*|[A-Z]|[SPMBC]-\d+)\z/],
|
|
58
|
+
"spec" => [:spec, /\A[1-6]\d\d\z/],
|
|
59
|
+
"specsub" => [:spec_sub, /\A\d+\z/],
|
|
60
|
+
"specvar" => [:spec_variant, /\A[A-Z]+\z/],
|
|
53
61
|
"annex" => [:annex, /\A[A-Z]\z/],
|
|
54
62
|
"appendix" => [:appendix, /\A(?:[A-Z]|\d+)\z/],
|
|
55
63
|
"vol" => [:volume, /\A\d+\z/],
|
|
@@ -63,6 +71,12 @@ module Pubid
|
|
|
63
71
|
# follow (the component-edition duality).
|
|
64
72
|
POSITIONAL_MARKERS = %i[edition year].freeze
|
|
65
73
|
|
|
74
|
+
# The S-4 spec node and its children: specsub-/specvar- are only
|
|
75
|
+
# allowed after their parent, and an S-4 spec rides its
|
|
76
|
+
# publication's edition — no ed-/year- inside a spec.
|
|
77
|
+
SPEC_NODES = %i[spec spec_sub spec_variant].freeze
|
|
78
|
+
SPEC_PARENTS = %i[spec spec_sub].freeze
|
|
79
|
+
|
|
66
80
|
# Legacy dotted keys (deprecated; decodable, never emitted).
|
|
67
81
|
# "ap" is the pre-positional appendix tag; "suppl" the pre-positional
|
|
68
82
|
# supplement tag; "y" the pre-positional year tag — under the
|
|
@@ -97,11 +111,22 @@ module Pubid
|
|
|
97
111
|
# own edition).
|
|
98
112
|
ctx = { component: nil, edition_owner: nil }
|
|
99
113
|
consume_segments!(attrs, segments, ctx, urn)
|
|
100
|
-
|
|
114
|
+
build_identifier({ type: type_token.upcase, **attrs }, urn)
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
private
|
|
104
118
|
|
|
119
|
+
# The semantic validations the builder applies to the decoded shape
|
|
120
|
+
# (the S-4 anchor, the citation letter) are URN validation at this
|
|
121
|
+
# layer: a URN that asserts a citation it cannot carry is a
|
|
122
|
+
# malformed URN, and the raw ArgumentError stays as the cause.
|
|
123
|
+
def build_identifier(hash, urn)
|
|
124
|
+
Builder.build(hash)
|
|
125
|
+
rescue ArgumentError => e
|
|
126
|
+
raise Pubid::Errors::UrnParseError,
|
|
127
|
+
"#{e.message} in #{urn.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
105
130
|
def read_type!(segments, urn)
|
|
106
131
|
type_token = segments.shift.to_s.downcase
|
|
107
132
|
return type_token if TYPE_LETTERS.include?(type_token)
|
|
@@ -159,10 +184,9 @@ module Pubid
|
|
|
159
184
|
marker, value = MARKER_RE.match(segment).captures
|
|
160
185
|
key, pattern = MARKERS.fetch(marker)
|
|
161
186
|
raise_invalid!(marker, value, urn) unless value.match?(pattern)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
end
|
|
187
|
+
assert_spec_parent!(key, ctx, urn)
|
|
188
|
+
return consume_positional!(attrs, key, value, ctx, urn) if
|
|
189
|
+
POSITIONAL_MARKERS.include?(key)
|
|
166
190
|
|
|
167
191
|
store!(attrs, key, value, urn)
|
|
168
192
|
ctx[:component] = key
|
|
@@ -170,6 +194,16 @@ module Pubid
|
|
|
170
194
|
consume_subnumber!(attrs, key, segments, ctx, urn)
|
|
171
195
|
end
|
|
172
196
|
|
|
197
|
+
# specsub-/specvar- are children of the spec atom — allowed only
|
|
198
|
+
# in their parent's slot (the SEGMENTS table in the generator).
|
|
199
|
+
def assert_spec_parent!(key, ctx, urn)
|
|
200
|
+
return unless %i[spec_sub spec_variant].include?(key)
|
|
201
|
+
return if SPEC_PARENTS.include?(ctx[:component])
|
|
202
|
+
|
|
203
|
+
raise Pubid::Errors::UrnParseError,
|
|
204
|
+
"#{key}- is only allowed after spec- in #{urn.inspect}"
|
|
205
|
+
end
|
|
206
|
+
|
|
173
207
|
def raise_invalid!(marker, value, urn)
|
|
174
208
|
raise Pubid::Errors::UrnParseError,
|
|
175
209
|
"Invalid #{marker} value #{value.inspect} in #{urn.inspect}"
|
|
@@ -177,8 +211,15 @@ module Pubid
|
|
|
177
211
|
|
|
178
212
|
# ed- and year- are POSITIONAL markers (see the marker docs): the
|
|
179
213
|
# edition attaches to the component it follows, the year to the
|
|
180
|
-
# edition owner.
|
|
214
|
+
# edition owner. An S-4 spec carries neither — it rides its
|
|
215
|
+
# publication's edition.
|
|
181
216
|
def consume_positional!(attrs, key, value, ctx, urn)
|
|
217
|
+
if SPEC_NODES.include?(ctx[:component])
|
|
218
|
+
raise Pubid::Errors::UrnParseError,
|
|
219
|
+
"an S-4 spec rides its publication's edition: #{key}- " \
|
|
220
|
+
"cannot follow a spec segment in #{urn.inspect}"
|
|
221
|
+
end
|
|
222
|
+
|
|
182
223
|
if key == :edition
|
|
183
224
|
return store_component_scoped!(attrs, :edition, value, ctx, urn)
|
|
184
225
|
end
|
data/lib/pubid/iho.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Pubid
|
|
|
12
12
|
autoload :Identifier, "#{__dir__}/iho/identifiers/base"
|
|
13
13
|
autoload :Identifiers, "#{__dir__}/iho/identifiers"
|
|
14
14
|
autoload :Renderer, "#{__dir__}/iho/renderer"
|
|
15
|
+
autoload :ReservedSpecs, "#{__dir__}/iho/reserved_specs"
|
|
15
16
|
autoload :UrnGenerator, "#{__dir__}/iho/urn_generator"
|
|
16
17
|
autoload :UrnParser, "#{__dir__}/iho/urn_parser"
|
|
17
18
|
autoload :PubidUrnSchema, "#{__dir__}/iho/pubid_urn_schema"
|
data/lib/pubid/nist/CLAUDE.md
CHANGED
|
@@ -135,3 +135,14 @@ Read this before you change `lib/pubid/nist/` or `spec/pubid/nist/`. The root
|
|
|
135
135
|
`spec/pubid/nist/bare_series_reference_spec.rb`; the corpus round froze
|
|
136
136
|
the six rows (`NBS CS`, `NBS CSM`, `NBS CIS`, `NBS CS-E`, `NIST PS`,
|
|
137
137
|
`NIST DCI`) into the testsuite with the gem twin in lockstep.
|
|
138
|
+
|
|
139
|
+
- **"FIPS PUB <number>" is a preprocessor alias, not a grammar face
|
|
140
|
+
(pubid#697).** NIST prints the series as "FIPS PUB <number>" but the
|
|
141
|
+
grammar reads "NIST FIPS <number>", so `normalize_fips_pub_alias!`
|
|
142
|
+
(head-anchored, immediately after the publisher/series stage) rewrites
|
|
143
|
+
the printed prefix and the parse lands on exactly the canonical model —
|
|
144
|
+
hash-, render-, URN- and MR-identical to "NIST FIPS <number>". The
|
|
145
|
+
hyphenated numbers and the revision forms the grammar models
|
|
146
|
+
("FIPS PUB 199 Rev. 1" with its `original_prefix` facet, compact
|
|
147
|
+
"rev1") ride the prefix alias; bare "FIPS <number>"
|
|
148
|
+
(publisher_was_parsed: false) and NBS FIPS are untouched.
|
|
@@ -68,6 +68,7 @@ module Pubid
|
|
|
68
68
|
end
|
|
69
69
|
normalize_spurious_u_suffix!
|
|
70
70
|
normalize_publisher_and_series!
|
|
71
|
+
normalize_fips_pub_alias!
|
|
71
72
|
normalize_lcirc_supplement_contexts!
|
|
72
73
|
normalize_revision_spacing!
|
|
73
74
|
normalize_letter_suffix_casing!
|
|
@@ -124,6 +125,21 @@ module Pubid
|
|
|
124
125
|
@cleaned = @cleaned.gsub(/\bLC\b(?!IRC)/, "LCIRC")
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# The FIPS series' own printed face: NIST prints the series as
|
|
129
|
+
# "FIPS PUB <number>" (document cover and catalogue style), so the
|
|
130
|
+
# grammar accepts the publisher-prefixed canonical form only through
|
|
131
|
+
# this alias (pubid#697). Rewrites the prefix to "NIST FIPS", the
|
|
132
|
+
# spelling the grammar already reads, and the printed-form parse
|
|
133
|
+
# therefore lands on exactly the model "NIST FIPS <number>" builds
|
|
134
|
+
# (same to_hash, same render, revision spellings like
|
|
135
|
+
# "FIPS PUB 199 Rev. 1" included). Anchored at the head and run
|
|
136
|
+
# before every other stage so later digit/marker stages see the
|
|
137
|
+
# canonical face; "FIPS" alone (publisher_was_parsed: false) is
|
|
138
|
+
# untouched.
|
|
139
|
+
def normalize_fips_pub_alias!
|
|
140
|
+
@cleaned = @cleaned.sub(/\AFIPS PUB\b/i, "NIST FIPS")
|
|
141
|
+
end
|
|
142
|
+
|
|
127
143
|
# LCIRC series: combine with NBS prefix when a supplement marker
|
|
128
144
|
# follows, and convert MR-format dots to spaces so the grammar's
|
|
129
145
|
# circ_supplement_identifier rule can match.
|
data/lib/pubid/plateau/CLAUDE.md
CHANGED
|
@@ -11,3 +11,38 @@ Integer, so its core is `:int` (`core: [["number", :int]]`): `#00` is the core
|
|
|
11
11
|
`0`, and `00` in a URN is not canonical. The Annex type has no `number`; it
|
|
12
12
|
has no core, its document comes first along `base`, and its `letter` is an
|
|
13
13
|
atom. No input makes an Annex today, so the spec builds one by hand.
|
|
14
|
+
|
|
15
|
+
## Handbook volume label (本編 / 本編+付録)
|
|
16
|
+
|
|
17
|
+
MLIT publishes Handbook #00 edition 5.0 twice on one date: `第5.0版 本編`
|
|
18
|
+
(main volume) and `第5.0版 本編+付録` (main volume and appendix, one PDF);
|
|
19
|
+
edition 6.0 only as `本編+付録`. `Identifiers::Handbook#volume` keeps that
|
|
20
|
+
label as Japanese text, as `edition` keeps `第X.Y版`. The grammar accepts it
|
|
21
|
+
only after an edition, separated by an ASCII or a full-width (U+3000) space,
|
|
22
|
+
with `+` or ASCII `+`; the builder normalizes `+` to `+`, and `to_s` writes
|
|
23
|
+
one ASCII space before it. The renderer prints the volume only after an
|
|
24
|
+
edition, so `exclude(:edition)` still renders a string that parses.
|
|
25
|
+
`volume` is in `==` and is `subset_strict`, so `#00 第5.0版` matches neither
|
|
26
|
+
labelled publication. A nil volume leaves no key in `to_hash`, so the
|
|
27
|
+
existing index-v2 rows keep their hash. The pubid-scheme URN carries it as
|
|
28
|
+
the `vol-` atom; `to_urn` is unchanged (it has no edition either). Locked by
|
|
29
|
+
`spec/pubid/plateau/handbook_volume_spec.rb`.
|
|
30
|
+
|
|
31
|
+
Known gaps, not caused by this change: a Technical Report drops an edition
|
|
32
|
+
and a volume without a diagnostic (the `handbook` rule matches any
|
|
33
|
+
`doc_type`), and `to_mr_string` raises for every Handbook with an edition
|
|
34
|
+
(the base `mr_edition` calls `edition.number` on a String).
|
|
35
|
+
|
|
36
|
+
## The grammar is baked here ahead of pubid-grammar
|
|
37
|
+
|
|
38
|
+
`data/parg/plateau.json` was compiled here from its own embedded `source`
|
|
39
|
+
plus the volume rule, at parsanol **1.3.114** (the version of the earlier
|
|
40
|
+
bake; force it with `gem "parsanol", "1.3.114"`, as
|
|
41
|
+
`pubid-grammar/scripts/bake-one.rb` does, because the binstub resolves the
|
|
42
|
+
latest installed gem). Until pubid-grammar takes hand-off
|
|
43
|
+
`pubid__pubid-grammar__plateau-handbook-volume`, its `grammars/plateau.parg`
|
|
44
|
+
is the OLD grammar. To change the grammar, extract the source with
|
|
45
|
+
`jq -j .source data/parg/plateau.json` (`-j`: the source has no trailing
|
|
46
|
+
newline, and one extra byte changes the checksum), edit, re-bake, then run
|
|
47
|
+
`bundle exec parsanol parg test data/parg/plateau.json`. Read the `.parg`
|
|
48
|
+
file as UTF-8: under a US-ASCII locale the parsanol lexer raises on `第`.
|
|
@@ -34,6 +34,10 @@ module Pubid
|
|
|
34
34
|
if parsed[:edition] && klass == Identifiers::Handbook
|
|
35
35
|
params[:edition] = parsed[:edition].to_s
|
|
36
36
|
end
|
|
37
|
+
# The grammar accepts an ASCII "+"; MLIT prints the full-width "+".
|
|
38
|
+
if parsed[:volume] && klass == Identifiers::Handbook
|
|
39
|
+
params[:volume] = parsed[:volume].to_s.tr("+", "+")
|
|
40
|
+
end
|
|
37
41
|
|
|
38
42
|
klass.new(**params)
|
|
39
43
|
end
|
|
@@ -4,10 +4,18 @@ module Pubid
|
|
|
4
4
|
module Plateau
|
|
5
5
|
module Identifiers
|
|
6
6
|
# PLATEAU Handbook
|
|
7
|
-
# Format: PLATEAU Handbook #NN[-annex] [第X.Y版]
|
|
7
|
+
# Format: PLATEAU Handbook #NN[-annex] [第X.Y版[ volume]]
|
|
8
8
|
# Example: PLATEAU Handbook #00 第1.0版
|
|
9
|
+
# Example: PLATEAU Handbook #00 第5.0版 本編+付録
|
|
9
10
|
class Handbook < Identifier
|
|
10
11
|
attribute :edition, :string, default: -> {}
|
|
12
|
+
# The volume label MLIT prints after the edition: "本編" (the main
|
|
13
|
+
# volume alone) or "本編+付録" (main volume and appendix, one PDF).
|
|
14
|
+
attribute :volume, :string, default: -> {}
|
|
15
|
+
|
|
16
|
+
# A nil `volume` means the publication has no volume label:
|
|
17
|
+
# `#00 第5.0版` does not subsume `#00 第5.0版 本編+付録`.
|
|
18
|
+
subset_strict :volume
|
|
11
19
|
|
|
12
20
|
def type_string
|
|
13
21
|
"Handbook"
|
|
@@ -20,7 +28,7 @@ module Pubid
|
|
|
20
28
|
def ==(other)
|
|
21
29
|
return false unless other.is_a?(Handbook)
|
|
22
30
|
|
|
23
|
-
super && edition == other.edition
|
|
31
|
+
super && edition == other.edition && volume == other.volume
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
end
|