pubid 2.0.1 → 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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/data/ieee/update_codes.yaml +2 -2
  3. data/data/parg/adobe.json +1 -1
  4. data/data/parg/aiee.json +1 -1
  5. data/data/parg/aiee.parg +99 -9
  6. data/data/parg/amca.json +1 -1
  7. data/data/parg/ansi.json +1 -1
  8. data/data/parg/api.json +1 -1
  9. data/data/parg/ashrae.json +1 -1
  10. data/data/parg/asme.json +1 -1
  11. data/data/parg/astm.json +1 -1
  12. data/data/parg/bipm.json +1 -1
  13. data/data/parg/bsi.json +1 -1
  14. data/data/parg/ccsds.json +1 -1
  15. data/data/parg/cen_cenelec.json +1 -1
  16. data/data/parg/cie.json +1 -1
  17. data/data/parg/csa.json +1 -1
  18. data/data/parg/ecma.json +1 -1
  19. data/data/parg/etsi.json +1 -1
  20. data/data/parg/evs.json +1 -1
  21. data/data/parg/gost.json +1 -1
  22. data/data/parg/iala.json +1 -1
  23. data/data/parg/iana.json +1 -1
  24. data/data/parg/idf.json +1 -1
  25. data/data/parg/iec.json +1 -1
  26. data/data/parg/ieee.json +1 -1
  27. data/data/parg/iho.json +1 -0
  28. data/data/parg/ire.json +1 -1
  29. data/data/parg/ire.parg +51 -12
  30. data/data/parg/isbn.json +1 -1
  31. data/data/parg/iso.json +1 -1
  32. data/data/parg/iso.parg +751 -0
  33. data/data/parg/itu.json +1 -1
  34. data/data/parg/jcgm.json +1 -1
  35. data/data/parg/jis.json +1 -1
  36. data/data/parg/nesc.json +1 -1
  37. data/data/parg/nesc.parg +97 -12
  38. data/data/parg/nist.json +1 -1
  39. data/data/parg/oiml.json +1 -1
  40. data/data/parg/omg.json +1 -1
  41. data/data/parg/parg.json +1 -1
  42. data/data/parg/plateau.json +1 -1
  43. data/data/parg/tgpp.json +1 -1
  44. data/data/parg/un.json +1 -1
  45. data/lib/pubid/adobe/identifier.rb +1 -1
  46. data/lib/pubid/adobe/parser.rb +2 -113
  47. data/lib/pubid/amca/identifiers/base.rb +1 -1
  48. data/lib/pubid/amca/parser.rb +1 -145
  49. data/lib/pubid/ansi/identifier.rb +1 -1
  50. data/lib/pubid/ansi/parser.rb +1 -82
  51. data/lib/pubid/api/builder.rb +32 -1
  52. data/lib/pubid/api/identifier.rb +1 -1
  53. data/lib/pubid/api/parser.rb +1 -131
  54. data/lib/pubid/ashrae/identifiers/base.rb +1 -1
  55. data/lib/pubid/ashrae/parser.rb +3 -710
  56. data/lib/pubid/asme/builder.rb +6 -4
  57. data/lib/pubid/asme/identifier.rb +1 -2
  58. data/lib/pubid/asme/parser.rb +1 -328
  59. data/lib/pubid/astm/builder.rb +12 -3
  60. data/lib/pubid/astm/identifier.rb +1 -2
  61. data/lib/pubid/astm/parser.rb +1 -227
  62. data/lib/pubid/bipm/identifier.rb +8 -1
  63. data/lib/pubid/bipm/parser.rb +2 -224
  64. data/lib/pubid/bsi/builder.rb +2 -4
  65. data/lib/pubid/bsi/parser.rb +1 -666
  66. data/lib/pubid/bsi/single_identifier.rb +38 -1
  67. data/lib/pubid/calconnect/parser.rb +2 -40
  68. data/lib/pubid/ccsds/identifier.rb +1 -1
  69. data/lib/pubid/ccsds/parser.rb +1 -66
  70. data/lib/pubid/cen_cenelec/identifier.rb +16 -1
  71. data/lib/pubid/cen_cenelec/parser.rb +3 -136
  72. data/lib/pubid/cie/identifier.rb +1 -1
  73. data/lib/pubid/cie/parser.rb +3 -378
  74. data/lib/pubid/conformance.rb +1 -1
  75. data/lib/pubid/csa/builder.rb +41 -18
  76. data/lib/pubid/csa/identifier.rb +12 -4
  77. data/lib/pubid/csa/identifiers/amendment.rb +28 -0
  78. data/lib/pubid/csa/identifiers.rb +1 -0
  79. data/lib/pubid/csa/parser.rb +16 -397
  80. data/lib/pubid/doi/parser.rb +2 -37
  81. data/lib/pubid/easc/builder.rb +1 -1
  82. data/lib/pubid/easc/parser.rb +2 -70
  83. data/lib/pubid/ecma/CLAUDE.md +8 -1
  84. data/lib/pubid/ecma/identifier.rb +13 -8
  85. data/lib/pubid/ecma/parser.rb +2 -54
  86. data/lib/pubid/ecma/renderer.rb +3 -7
  87. data/lib/pubid/errors.rb +18 -13
  88. data/lib/pubid/etsi/builder.rb +8 -5
  89. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  90. data/lib/pubid/etsi/parser.rb +1 -132
  91. data/lib/pubid/evs/identifier.rb +7 -0
  92. data/lib/pubid/evs/parser.rb +1 -8
  93. data/lib/pubid/evs.rb +1 -1
  94. data/lib/pubid/gb/identifier.rb +6 -0
  95. data/lib/pubid/gb/parser.rb +2 -51
  96. data/lib/pubid/gost/identifier.rb +9 -1
  97. data/lib/pubid/gost/parser.rb +4 -81
  98. data/lib/pubid/iala/builder.rb +48 -39
  99. data/lib/pubid/iala/identifier.rb +1 -1
  100. data/lib/pubid/iala/parser.rb +2 -121
  101. data/lib/pubid/iana/identifier.rb +1 -1
  102. data/lib/pubid/iana/parser.rb +2 -24
  103. data/lib/pubid/identifier.rb +78 -1
  104. data/lib/pubid/idf/builder.rb +42 -0
  105. data/lib/pubid/idf/identifier.rb +1 -1
  106. data/lib/pubid/idf/parser.rb +1 -137
  107. data/lib/pubid/idf.rb +1 -2
  108. data/lib/pubid/iec/builder.rb +158 -50
  109. data/lib/pubid/iec/identifier.rb +1 -1
  110. data/lib/pubid/iec/parser.rb +3 -430
  111. data/lib/pubid/ieee/CLAUDE.md +2 -0
  112. data/lib/pubid/ieee/aiee/identifier.rb +2 -1
  113. data/lib/pubid/ieee/aiee.rb +0 -1
  114. data/lib/pubid/ieee/builder.rb +177 -30
  115. data/lib/pubid/ieee/components/code.rb +46 -7
  116. data/lib/pubid/ieee/identifiers/base.rb +5 -2
  117. data/lib/pubid/ieee/identifiers/joint_development.rb +7 -3
  118. data/lib/pubid/ieee/ire/identifier.rb +2 -1
  119. data/lib/pubid/ieee/ire.rb +0 -1
  120. data/lib/pubid/ieee/nesc/builder.rb +1 -1
  121. data/lib/pubid/ieee/nesc.rb +0 -1
  122. data/lib/pubid/ieee/parser.rb +1 -1257
  123. data/lib/pubid/ieee/project_renderer.rb +5 -1
  124. data/lib/pubid/ieee/renderer.rb +4 -2
  125. data/lib/pubid/ieee/urn_generator.rb +4 -1
  126. data/lib/pubid/ietf/identifiers/base.rb +17 -1
  127. data/lib/pubid/ietf/parser.rb +2 -47
  128. data/lib/pubid/ietf/renderer.rb +17 -2
  129. data/lib/pubid/iho/CLAUDE.md +98 -0
  130. data/lib/pubid/iho/builder.rb +4 -1
  131. data/lib/pubid/iho/identifiers/base.rb +28 -2
  132. data/lib/pubid/iho/renderer.rb +8 -0
  133. data/lib/pubid/iho/urn_generator.rb +14 -1
  134. data/lib/pubid/iho/urn_parser.rb +45 -32
  135. data/lib/pubid/iho.rb +0 -1
  136. data/lib/pubid/isbn/identifier.rb +1 -1
  137. data/lib/pubid/isbn/parser.rb +2 -31
  138. data/lib/pubid/iso/CLAUDE.md +66 -0
  139. data/lib/pubid/iso/builder.rb +229 -15
  140. data/lib/pubid/iso/identifier.rb +22 -12
  141. data/lib/pubid/iso/identifiers/directives.rb +6 -3
  142. data/lib/pubid/iso/identifiers/guide.rb +6 -3
  143. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +6 -3
  144. data/lib/pubid/iso/normalizer.rb +5 -4
  145. data/lib/pubid/iso/parser.rb +1 -539
  146. data/lib/pubid/iso/supplement_identifier.rb +6 -3
  147. data/lib/pubid/iso/urn_parser.rb +13 -8
  148. data/lib/pubid/iso/utilities.rb +4 -5
  149. data/lib/pubid/iso.rb +1 -19
  150. data/lib/pubid/itu/builder.rb +36 -20
  151. data/lib/pubid/itu/identifiers/base.rb +15 -2
  152. data/lib/pubid/itu/parser.rb +1 -775
  153. data/lib/pubid/jcgm/builder.rb +35 -0
  154. data/lib/pubid/jcgm/parser.rb +1 -151
  155. data/lib/pubid/jcgm.rb +1 -2
  156. data/lib/pubid/jis/CLAUDE.md +36 -0
  157. data/lib/pubid/jis/builder.rb +30 -41
  158. data/lib/pubid/jis/identifier.rb +12 -2
  159. data/lib/pubid/jis/identifiers/welding_engineering_standard.rb +35 -0
  160. data/lib/pubid/jis/identifiers.rb +2 -0
  161. data/lib/pubid/jis/parser.rb +1 -128
  162. data/lib/pubid/jis/renderer.rb +1 -1
  163. data/lib/pubid/jis/supplement_identifier.rb +5 -5
  164. data/lib/pubid/jis/urn_parser.rb +2 -1
  165. data/lib/pubid/jis.rb +3 -2
  166. data/lib/pubid/nist/builder.rb +5 -1
  167. data/lib/pubid/nist/caster.rb +13 -3
  168. data/lib/pubid/nist/identifiers/base.rb +37 -2
  169. data/lib/pubid/nist/identifiers/circular.rb +2 -2
  170. data/lib/pubid/nist/identifiers/circular_supplement.rb +2 -2
  171. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +2 -2
  172. data/lib/pubid/nist/identifiers/crpl_report.rb +2 -2
  173. data/lib/pubid/nist/identifiers/handbook.rb +2 -2
  174. data/lib/pubid/nist/identifiers/internal_report.rb +2 -2
  175. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  176. data/lib/pubid/nist/identifiers/monograph.rb +2 -2
  177. data/lib/pubid/nist/identifiers/report.rb +2 -2
  178. data/lib/pubid/nist/parser.rb +19 -775
  179. data/lib/pubid/nist/router.rb +4 -1
  180. data/lib/pubid/nist/supplement_identifier.rb +2 -2
  181. data/lib/pubid/oasis/parser.rb +2 -16
  182. data/lib/pubid/ogc/parser.rb +2 -26
  183. data/lib/pubid/oiml/identifiers/certification_system.rb +7 -0
  184. data/lib/pubid/oiml/identifiers/dual_published.rb +2 -2
  185. data/lib/pubid/oiml/parser.rb +1 -303
  186. data/lib/pubid/oiml.rb +1 -2
  187. data/lib/pubid/omg/identifier.rb +6 -1
  188. data/lib/pubid/omg/parser.rb +2 -76
  189. data/lib/pubid/parser.rb +5 -4
  190. data/lib/pubid/parsers/mr_string.rb +1 -1
  191. data/lib/pubid/plateau/parser.rb +1 -66
  192. data/lib/pubid/plateau.rb +1 -2
  193. data/lib/pubid/renderers/directives_renderer.rb +1 -1
  194. data/lib/pubid/renderers/iwa_renderer.rb +1 -1
  195. data/lib/pubid/renderers/publisher_stripper.rb +82 -0
  196. data/lib/pubid/renderers/supplement_renderer.rb +1 -1
  197. data/lib/pubid/renderers.rb +1 -0
  198. data/lib/pubid/sae/parser.rb +1 -48
  199. data/lib/pubid/tgpp/identifier.rb +1 -1
  200. data/lib/pubid/tgpp/parser.rb +2 -59
  201. data/lib/pubid/un/identifier.rb +1 -1
  202. data/lib/pubid/un/parser.rb +1 -18
  203. data/lib/pubid/version.rb +1 -1
  204. data/lib/pubid/w3c/parser.rb +2 -27
  205. data/lib/pubid/xsf/identifier.rb +10 -6
  206. data/lib/pubid/xsf/parser.rb +3 -31
  207. data/lib/pubid/xsf/renderer.rb +2 -4
  208. data/lib/pubid.rb +8 -9
  209. metadata +9 -23
  210. data/lib/pubid/ieee/aiee/parser.rb +0 -130
  211. data/lib/pubid/ieee/ire/parser.rb +0 -109
  212. data/lib/pubid/ieee/nesc/parser.rb +0 -155
  213. data/lib/pubid/iho/parser.rb +0 -68
  214. data/lib/pubid/parser/common_parse_methods.rb +0 -13
  215. data/lib/pubid/parser/common_parse_rules.rb +0 -56
  216. data/lib/pubid/parser/grammar.rb +0 -96
@@ -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 tree of a guide-first spelling
98
- # carries an empty :type_with_stage plus the type under
99
- # :type_with_stage_fr, so deferring the rename selected the
100
- # default International Standard class for every guide-first
101
- # (and Cyrillic "Руководства ИСО …") reference.
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
- .tr("#{Parser::DASH_CHARS.join}/", "-")
154
- .gsub(" ", "-")
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[:supplement]) }
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,14 +519,10 @@ 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
311
- if value.is_a?(Parslet::Slice) || value.is_a?(String) ||
525
+ if value.is_a?(String) ||
312
526
  value.is_a?(Integer)
313
527
  value.to_s
314
528
  else
@@ -135,8 +135,10 @@ module Pubid
135
135
  map "languages", to: :languages
136
136
  # publisher emitted only when the primary isn't the type default;
137
137
  # copublishers (the other bodies) as an array, omitted when empty.
138
- map "publisher", with: { to: :publisher_to_kv, from: :publisher_from_kv }
139
- map "copublishers", with: { to: :copublishers_to_kv, from: :copublishers_from_kv }
138
+ map "publisher",
139
+ with: { to: :publisher_to_kv, from: :publisher_from_kv }
140
+ map "copublishers",
141
+ with: { to: :copublishers_to_kv, from: :copublishers_from_kv }
140
142
  map "locality", to: :locality
141
143
  # `type` and generic `stage` are fully derived from `typed_stage`
142
144
  # (builder sets them via to_type/to_stage), so we serialize only the
@@ -169,8 +171,10 @@ module Pubid
169
171
  # instead of falling back. The IEC twin (iec/identifier.rb) was
170
172
  # migrated at the time; ISO was missed.
171
173
  ts = (model.class.const_defined?(:TYPED_STAGES) &&
172
- model.class::TYPED_STAGES.find { |t| t.code.to_s == value.to_s }) ||
173
- Pubid::Iso.locate_stage_by_code(value)
174
+ model.class::TYPED_STAGES.find do |t|
175
+ t.code.to_s == value.to_s
176
+ end) ||
177
+ Pubid::Iso.locate_stage_by_code(value)
174
178
  return unless ts
175
179
 
176
180
  # The renderer prefers `original_abbr` (the parsed surface form); without
@@ -196,7 +200,7 @@ module Pubid
196
200
  return if v.nil? || v.to_s.empty?
197
201
 
198
202
  doc.add_child(Lutaml::KeyValue::DataModel::Element.new("stage_iteration",
199
- v.to_s))
203
+ v.to_s))
200
204
  end
201
205
 
202
206
  def stage_iteration_from_kv(model, value)
@@ -219,7 +223,11 @@ module Pubid
219
223
  # --- date serialized flat as year/month/day ---
220
224
  def year_to_kv(model, doc) = emit_date_part(doc, "year", model.date&.year)
221
225
  def year_from_kv(model, value) = date_for(model).year = value.to_s
222
- def month_to_kv(model, doc) = emit_date_part(doc, "month", model.date&.month)
226
+
227
+ def month_to_kv(model, doc)
228
+ emit_date_part(doc, "month", model.date&.month)
229
+ end
230
+
223
231
  def month_from_kv(model, value) = date_for(model).month = value.to_s
224
232
  def day_to_kv(model, doc) = emit_date_part(doc, "day", model.date&.day)
225
233
  def day_from_kv(model, value) = date_for(model).day = value.to_s
@@ -252,7 +260,8 @@ module Pubid
252
260
  pub = model.publisher&.publisher
253
261
  return if pub.nil? || pub == model.class.default_publisher&.publisher
254
262
 
255
- doc.add_child(Lutaml::KeyValue::DataModel::Element.new("publisher", pub))
263
+ doc.add_child(Lutaml::KeyValue::DataModel::Element.new("publisher",
264
+ pub))
256
265
  end
257
266
 
258
267
  def publisher_from_kv(model, value)
@@ -264,7 +273,8 @@ module Pubid
264
273
  return unless cp&.any?
265
274
 
266
275
  doc.add_child(
267
- Lutaml::KeyValue::DataModel::Element.new("copublishers", cp.map(&:to_s)),
276
+ Lutaml::KeyValue::DataModel::Element.new("copublishers",
277
+ cp.map(&:to_s)),
268
278
  )
269
279
  end
270
280
 
@@ -304,10 +314,10 @@ module Pubid
304
314
  when :mr_string
305
315
  Pubid::Parsers::MrString.parse(string)
306
316
  else
307
- # R1 parser swap: the baked PG artifact is the identifier
308
- # parser of record; the Builder consumes the same attribute
309
- # hash it always has.
310
- parsed = Pubid::Parg::Backend.parse(:iso, string)
317
+ # The baked PG artifact parses through the grammar's declared
318
+ # bindings: the Builder consumes the typed bound map (every
319
+ # capture bound by name, supplement layers position-scoped).
320
+ parsed = Pubid::Parg::Model.bound(:iso, string)
311
321
  Pubid::Iso::Builder.new.build(parsed)
312
322
  end
313
323
  end
@@ -35,9 +35,12 @@ module Pubid
35
35
  end
36
36
 
37
37
  def to_s(**opts)
38
- context = build_rendering_context(nil, format: :human, **opts)
39
- Pubid::Renderers::DirectivesRenderer.new(self).render(context:,
40
- **opts.slice(:with_edition))
38
+ context = build_rendering_context(
39
+ nil, format: :human, **opts.except(:with_publisher)
40
+ )
41
+ rendered = Pubid::Renderers::DirectivesRenderer.new(self)
42
+ .render(context:, **opts.slice(:with_edition))
43
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
41
44
  end
42
45
 
43
46
  # Directives use a different URN scheme: urn:iso:doc instead of urn:iso:std
@@ -91,9 +91,12 @@ module Pubid
91
91
  def to_s(**opts)
92
92
  return super if opts[:format] && opts[:format] != :human
93
93
 
94
- context = build_rendering_context(nil, format: :human, **opts)
95
- Pubid::Renderers::GuideRenderer.new(self).render(context:,
96
- **opts.slice(:with_edition))
94
+ context = build_rendering_context(
95
+ nil, format: :human, **opts.except(:with_publisher)
96
+ )
97
+ rendered = Pubid::Renderers::GuideRenderer.new(self)
98
+ .render(context:, **opts.slice(:with_edition))
99
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
97
100
  end
98
101
  end
99
102
  end
@@ -85,9 +85,12 @@ module Pubid
85
85
  end
86
86
 
87
87
  def to_s(**opts)
88
- context = build_rendering_context(nil, format: :human, **opts)
89
- Pubid::Renderers::IwaRenderer.new(self).render(context:,
90
- **opts.slice(:with_edition))
88
+ context = build_rendering_context(
89
+ nil, format: :human, **opts.except(:with_publisher)
90
+ )
91
+ rendered = Pubid::Renderers::IwaRenderer.new(self)
92
+ .render(context:, **opts.slice(:with_edition))
93
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
91
94
  end
92
95
  end
93
96
  end
@@ -7,7 +7,7 @@ module Pubid
7
7
  # update_codes regex rewrites, and the DAD/FDAM pattern that the PEG
8
8
  # parser cannot recognize because it treats "/" as a copublisher separator.
9
9
  class Normalizer
10
- DAD_PATTERN = /^(.+?)\/(F?DAD)\s+(\d+)(?::(\d{4}))?$/.freeze
10
+ DAD_PATTERN = /^(.+?)\/(F?DAD)\s+(\d+)(?::(\d{4}))?$/
11
11
 
12
12
  class << self
13
13
  # Apply all normalizations to an identifier string and return the
@@ -29,8 +29,9 @@ module Pubid
29
29
  def parse_with_builder(string)
30
30
  # R2 ingestion hook: normalized strings reach the model through
31
31
  # the same parser of record as Identifier.parse (the baked PG
32
- # artifact), so every Tier-3 normalization feeds one grammar.
33
- parsed = Pubid::Parg::Backend.parse(:iso, string)
32
+ # artifact's bindings), so every Tier-3 normalization feeds one
33
+ # grammar.
34
+ parsed = Pubid::Parg::Model.bound(:iso, string)
34
35
  Pubid::Iso.builder.build(parsed)
35
36
  end
36
37
 
@@ -71,7 +72,7 @@ module Pubid
71
72
  supplement_number = match[3] # "1"
72
73
  supplement_year = match[4] # "1987" or nil
73
74
 
74
- base_parsed = Pubid::Iso.parser.parse(base_str)
75
+ base_parsed = Pubid::Parg::Model.bound(:iso, base_str)
75
76
  base = Pubid::Iso.builder.build(base_parsed)
76
77
 
77
78
  addendum = Identifiers::Addendum.new