pubid 2.0.1 → 2.0.2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/data/parg/adobe.json +1 -1
  3. data/data/parg/amca.json +1 -1
  4. data/data/parg/ansi.json +1 -1
  5. data/data/parg/api.json +1 -1
  6. data/data/parg/ashrae.json +1 -1
  7. data/data/parg/asme.json +1 -1
  8. data/data/parg/astm.json +1 -1
  9. data/data/parg/bipm.json +1 -1
  10. data/data/parg/bsi.json +1 -1
  11. data/data/parg/ccsds.json +1 -1
  12. data/data/parg/cen_cenelec.json +1 -1
  13. data/data/parg/cie.json +1 -1
  14. data/data/parg/csa.json +1 -1
  15. data/data/parg/ecma.json +1 -1
  16. data/data/parg/etsi.json +1 -1
  17. data/data/parg/evs.json +1 -1
  18. data/data/parg/gost.json +1 -1
  19. data/data/parg/iana.json +1 -1
  20. data/data/parg/idf.json +1 -1
  21. data/data/parg/iec.json +1 -1
  22. data/data/parg/iho.json +1 -0
  23. data/data/parg/isbn.json +1 -1
  24. data/data/parg/itu.json +1 -1
  25. data/data/parg/jcgm.json +1 -1
  26. data/data/parg/jis.json +1 -1
  27. data/data/parg/nist.json +1 -1
  28. data/data/parg/oiml.json +1 -1
  29. data/data/parg/omg.json +1 -1
  30. data/data/parg/plateau.json +1 -1
  31. data/data/parg/tgpp.json +1 -1
  32. data/data/parg/un.json +1 -1
  33. data/lib/pubid/adobe/identifier.rb +1 -1
  34. data/lib/pubid/adobe/parser.rb +2 -113
  35. data/lib/pubid/amca/identifiers/base.rb +1 -1
  36. data/lib/pubid/amca/parser.rb +1 -145
  37. data/lib/pubid/ansi/identifier.rb +1 -1
  38. data/lib/pubid/ansi/parser.rb +1 -82
  39. data/lib/pubid/api/builder.rb +32 -1
  40. data/lib/pubid/api/identifier.rb +1 -1
  41. data/lib/pubid/api/parser.rb +1 -131
  42. data/lib/pubid/ashrae/identifiers/base.rb +1 -1
  43. data/lib/pubid/ashrae/parser.rb +3 -710
  44. data/lib/pubid/asme/builder.rb +6 -4
  45. data/lib/pubid/asme/identifier.rb +1 -2
  46. data/lib/pubid/asme/parser.rb +1 -328
  47. data/lib/pubid/astm/builder.rb +12 -3
  48. data/lib/pubid/astm/identifier.rb +1 -2
  49. data/lib/pubid/astm/parser.rb +1 -227
  50. data/lib/pubid/bipm/identifier.rb +8 -1
  51. data/lib/pubid/bipm/parser.rb +2 -224
  52. data/lib/pubid/bsi/builder.rb +2 -4
  53. data/lib/pubid/bsi/parser.rb +1 -666
  54. data/lib/pubid/bsi/single_identifier.rb +38 -1
  55. data/lib/pubid/calconnect/parser.rb +2 -40
  56. data/lib/pubid/ccsds/identifier.rb +1 -1
  57. data/lib/pubid/ccsds/parser.rb +1 -66
  58. data/lib/pubid/cen_cenelec/identifier.rb +16 -1
  59. data/lib/pubid/cen_cenelec/parser.rb +3 -136
  60. data/lib/pubid/cie/identifier.rb +1 -1
  61. data/lib/pubid/cie/parser.rb +3 -378
  62. data/lib/pubid/conformance.rb +1 -1
  63. data/lib/pubid/csa/builder.rb +41 -18
  64. data/lib/pubid/csa/identifier.rb +12 -4
  65. data/lib/pubid/csa/identifiers/amendment.rb +28 -0
  66. data/lib/pubid/csa/identifiers.rb +1 -0
  67. data/lib/pubid/csa/parser.rb +16 -397
  68. data/lib/pubid/doi/parser.rb +2 -37
  69. data/lib/pubid/easc/builder.rb +1 -1
  70. data/lib/pubid/easc/parser.rb +2 -70
  71. data/lib/pubid/ecma/CLAUDE.md +8 -1
  72. data/lib/pubid/ecma/identifier.rb +13 -8
  73. data/lib/pubid/ecma/parser.rb +2 -54
  74. data/lib/pubid/ecma/renderer.rb +3 -7
  75. data/lib/pubid/errors.rb +18 -13
  76. data/lib/pubid/etsi/builder.rb +8 -5
  77. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  78. data/lib/pubid/etsi/parser.rb +1 -132
  79. data/lib/pubid/evs/identifier.rb +7 -0
  80. data/lib/pubid/evs/parser.rb +1 -8
  81. data/lib/pubid/evs.rb +1 -1
  82. data/lib/pubid/gb/identifier.rb +6 -0
  83. data/lib/pubid/gb/parser.rb +2 -51
  84. data/lib/pubid/gost/identifier.rb +9 -1
  85. data/lib/pubid/gost/parser.rb +4 -81
  86. data/lib/pubid/iala/parser.rb +2 -121
  87. data/lib/pubid/iana/identifier.rb +1 -1
  88. data/lib/pubid/iana/parser.rb +2 -24
  89. data/lib/pubid/identifier.rb +78 -1
  90. data/lib/pubid/idf/builder.rb +42 -0
  91. data/lib/pubid/idf/identifier.rb +1 -1
  92. data/lib/pubid/idf/parser.rb +1 -137
  93. data/lib/pubid/idf.rb +1 -2
  94. data/lib/pubid/iec/builder.rb +158 -50
  95. data/lib/pubid/iec/identifier.rb +1 -1
  96. data/lib/pubid/iec/parser.rb +3 -430
  97. data/lib/pubid/ieee/aiee/identifier.rb +2 -1
  98. data/lib/pubid/ieee/aiee.rb +0 -1
  99. data/lib/pubid/ieee/builder.rb +2 -2
  100. data/lib/pubid/ieee/identifiers/base.rb +1 -1
  101. data/lib/pubid/ieee/ire/identifier.rb +2 -1
  102. data/lib/pubid/ieee/ire.rb +0 -1
  103. data/lib/pubid/ieee/nesc.rb +0 -1
  104. data/lib/pubid/ieee/parser.rb +1 -1257
  105. data/lib/pubid/ietf/identifiers/base.rb +17 -1
  106. data/lib/pubid/ietf/parser.rb +2 -47
  107. data/lib/pubid/ietf/renderer.rb +17 -2
  108. data/lib/pubid/iho/CLAUDE.md +89 -0
  109. data/lib/pubid/iho/builder.rb +4 -1
  110. data/lib/pubid/iho/identifiers/base.rb +28 -2
  111. data/lib/pubid/iho/renderer.rb +8 -0
  112. data/lib/pubid/iho/urn_generator.rb +11 -1
  113. data/lib/pubid/iho/urn_parser.rb +21 -11
  114. data/lib/pubid/iho.rb +0 -1
  115. data/lib/pubid/isbn/identifier.rb +1 -1
  116. data/lib/pubid/isbn/parser.rb +2 -31
  117. data/lib/pubid/iso/builder.rb +1 -1
  118. data/lib/pubid/iso/identifiers/directives.rb +6 -3
  119. data/lib/pubid/iso/identifiers/guide.rb +6 -3
  120. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +6 -3
  121. data/lib/pubid/iso/normalizer.rb +1 -1
  122. data/lib/pubid/iso/parser.rb +1 -539
  123. data/lib/pubid/iso/supplement_identifier.rb +6 -3
  124. data/lib/pubid/iso/utilities.rb +4 -5
  125. data/lib/pubid/iso.rb +1 -19
  126. data/lib/pubid/itu/builder.rb +36 -20
  127. data/lib/pubid/itu/identifiers/base.rb +15 -2
  128. data/lib/pubid/itu/parser.rb +1 -775
  129. data/lib/pubid/jcgm/builder.rb +35 -0
  130. data/lib/pubid/jcgm/parser.rb +1 -151
  131. data/lib/pubid/jcgm.rb +1 -2
  132. data/lib/pubid/jis/builder.rb +18 -29
  133. data/lib/pubid/jis/identifier.rb +1 -1
  134. data/lib/pubid/jis/parser.rb +1 -128
  135. data/lib/pubid/nist/builder.rb +5 -1
  136. data/lib/pubid/nist/caster.rb +13 -3
  137. data/lib/pubid/nist/identifiers/base.rb +37 -2
  138. data/lib/pubid/nist/identifiers/circular.rb +2 -2
  139. data/lib/pubid/nist/identifiers/circular_supplement.rb +2 -2
  140. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +2 -2
  141. data/lib/pubid/nist/identifiers/crpl_report.rb +2 -2
  142. data/lib/pubid/nist/identifiers/handbook.rb +2 -2
  143. data/lib/pubid/nist/identifiers/internal_report.rb +2 -2
  144. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  145. data/lib/pubid/nist/identifiers/monograph.rb +2 -2
  146. data/lib/pubid/nist/identifiers/report.rb +2 -2
  147. data/lib/pubid/nist/parser.rb +19 -775
  148. data/lib/pubid/nist/router.rb +4 -1
  149. data/lib/pubid/nist/supplement_identifier.rb +2 -2
  150. data/lib/pubid/oasis/parser.rb +2 -16
  151. data/lib/pubid/ogc/parser.rb +2 -26
  152. data/lib/pubid/oiml/identifiers/certification_system.rb +7 -0
  153. data/lib/pubid/oiml/identifiers/dual_published.rb +2 -2
  154. data/lib/pubid/oiml/parser.rb +1 -303
  155. data/lib/pubid/oiml.rb +1 -2
  156. data/lib/pubid/omg/identifier.rb +6 -1
  157. data/lib/pubid/omg/parser.rb +2 -76
  158. data/lib/pubid/parser.rb +5 -4
  159. data/lib/pubid/parsers/mr_string.rb +1 -1
  160. data/lib/pubid/plateau/parser.rb +1 -66
  161. data/lib/pubid/plateau.rb +1 -2
  162. data/lib/pubid/renderers/directives_renderer.rb +1 -1
  163. data/lib/pubid/renderers/iwa_renderer.rb +1 -1
  164. data/lib/pubid/renderers/publisher_stripper.rb +82 -0
  165. data/lib/pubid/renderers/supplement_renderer.rb +1 -1
  166. data/lib/pubid/renderers.rb +1 -0
  167. data/lib/pubid/sae/parser.rb +1 -48
  168. data/lib/pubid/tgpp/identifier.rb +1 -1
  169. data/lib/pubid/tgpp/parser.rb +2 -59
  170. data/lib/pubid/un/identifier.rb +1 -1
  171. data/lib/pubid/un/parser.rb +1 -18
  172. data/lib/pubid/version.rb +1 -1
  173. data/lib/pubid/w3c/parser.rb +2 -27
  174. data/lib/pubid/xsf/identifier.rb +10 -6
  175. data/lib/pubid/xsf/parser.rb +3 -31
  176. data/lib/pubid/xsf/renderer.rb +2 -4
  177. data/lib/pubid.rb +8 -9
  178. metadata +6 -23
  179. data/lib/pubid/ieee/aiee/parser.rb +0 -130
  180. data/lib/pubid/ieee/ire/parser.rb +0 -109
  181. data/lib/pubid/ieee/nesc/parser.rb +0 -155
  182. data/lib/pubid/iho/parser.rb +0 -68
  183. data/lib/pubid/parser/common_parse_methods.rb +0 -13
  184. data/lib/pubid/parser/common_parse_rules.rb +0 -56
  185. data/lib/pubid/parser/grammar.rb +0 -96
@@ -1,383 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Cie
7
5
  # Parser for CIE identifiers
8
6
  # Handles dual-style system (legacy vs current)
9
- class Parser < ::Pubid::Parser::Grammar
10
- # Basic building blocks
11
- rule(:space) { str(" ") }
12
- rule(:space?) { space.maybe }
13
- rule(:dash) { str("-") }
14
- rule(:colon) { str(":") }
15
- rule(:dot) { str(".") }
16
- rule(:slash) { str("/") }
17
- rule(:comma) { str(",") }
18
- rule(:digit) { match("[0-9]") }
19
- rule(:digits) { digit.repeat(1) }
20
- rule(:upper) { match("[A-Z]") }
21
- rule(:lower) { match("[a-z]") }
22
-
23
- # Year (4 digits: 1900-2099)
24
- rule(:year_digits) do
25
- (str("19") | str("20")) >> digit.repeat(2, 2)
26
- end
27
-
28
- # Date with separator detection (CRITICAL for style)
29
- # Legacy: dash "-" (pre-2001)
30
- # Current: colon ":" (2001+)
31
- rule(:legacy_date) do
32
- (dash >> year_digits.as(:year)).as(:legacy_date)
33
- end
34
-
35
- rule(:current_date) do
36
- (colon >> year_digits.as(:year)).as(:current_date)
37
- end
38
-
39
- rule(:date) do
40
- current_date | legacy_date
41
- end
42
-
43
- # Legacy format: NUMBER-YEAR (e.g., 001-1980)
44
- # This is BEFORE 2001, using dash as year separator (NOT part)
45
- # Can optionally have trailing language like "(RU-2020)"
46
- rule(:legacy_code_with_year) do
47
- str("CIE") >> space >> digits.as(:number) >> dash >> year_digits.as(:year) >>
48
- (language_paren_year | str("")).as(:trailing_lang)
49
- end
50
-
51
- # Code patterns
52
- # Pattern 1: "006.1" - iteration (leading zero + dot)
53
- rule(:code_with_iteration) do
54
- digits.as(:number) >> dot >> digits.as(:iteration)
55
- end
56
-
57
- # Pattern 1b: "19/2.1" - part with slash THEN iteration with dot (complex)
58
- rule(:code_with_part_and_iteration_slash) do
59
- digits.as(:number) >> slash >> digits.as(:part) >> dot >> digits.as(:iteration)
60
- end
61
-
62
- # Pattern 1c: "19-2.1" - part with dash THEN iteration with dot (complex)
63
- rule(:code_with_part_and_iteration_dash) do
64
- digits.as(:number) >> dash >> digits.as(:part) >> dot >> digits.as(:iteration) >>
65
- dash.absent? # Don't consume date dash
66
- end
67
-
68
- # Pattern 2: "170-1" - part with dash (current style)
69
- # NOTE: Part must be 1-3 digits, and NOT followed by a 4-digit year
70
- rule(:code_with_part_dash) do
71
- digits.as(:number) >> dash >> match("[0-9]").repeat(1, 3).as(:part) >>
72
- str("").as(:dash_sep) >> # Marker for dash separator
73
- # Negative lookahead: don't consume if followed by 4-digit year
74
- (str("19") | str("20")).absent? >>
75
- (dot | dash).absent? # Don't consume dot or date dash
76
- end
77
-
78
- # Pattern 3: "170/1" - part with slash (legacy style)
79
- rule(:code_with_part_slash) do
80
- digits.as(:number) >> slash >> digits.as(:part) >>
81
- str("").as(:slash_sep) >> # Marker for slash separator
82
- dot.absent? # Don't consume iteration dot
83
- end
84
-
85
- # Pattern 4: Simple number
86
- rule(:code_simple) do
87
- digits.as(:number)
88
- end
89
-
90
- # Code rule (longest match first - complex patterns before simple)
91
- rule(:code) do
92
- code_with_part_and_iteration_slash |
93
- code_with_part_and_iteration_dash |
94
- code_with_iteration |
95
- code_with_part_slash |
96
- code_with_part_dash |
97
- code_simple
98
- end
99
-
100
- # Language patterns
101
- # Format 1: /E, /F, /G (slash-prefix, legacy)
102
- rule(:language_slash) do
103
- (slash >> upper.as(:lang_code)).as(:language_slash)
104
- end
105
-
106
- # Format 2: (DE), (ES), (CN), (en), (E), (F), (G)
107
- rule(:language_paren) do
108
- (str("(") >> match("[A-Za-z]").repeat(1,
109
- 3).as(:lang_code) >> str(")")).as(:language_paren)
110
- end
111
-
112
- # Format 3: (RU-2021) - with translation year
113
- # NOTE: The space before parenthesis is OPTIONAL (e.g., "2003 (RU-2021)" or "2003(RU-2021)")
114
- rule(:language_paren_year) do
115
- (space.maybe >> str("(") >> upper.repeat(2).as(:lang_code) >>
116
- dash >> year_digits.as(:trans_year) >> str(")")).as(:language_paren_year)
117
- end
118
-
119
- rule(:language) do
120
- language_paren_year | language_paren | language_slash
121
- end
122
-
123
- # S prefix (optional "Standard" marker)
124
- rule(:s_prefix) do
125
- str("S") >> space
126
- end
127
-
128
- # Conference x-prefix
129
- rule(:x_prefix) do
130
- str("x")
131
- end
132
-
133
- # Stage codes (DIS, DS, FDIS, etc.)
134
- rule(:stage_code) do
135
- (str("FDIS") | str("DIS") | str("DS")) >> space
136
- end
137
-
138
- # Document type (TR for Technical Report, TS for Technical Specification)
139
- rule(:doc_type) do
140
- (str("TR") | str("TS")) # Don't consume trailing space
141
- end
142
-
143
- # Proceedings paper code: 1+ uppercase letters (OP, PO, PP, WP, IP, P, ...)
144
- # Deliberately generic (open set); greedy letters then digits.
145
- rule(:paper_code) do
146
- match("[A-Z]").repeat(1).as(:paper_code)
147
- end
148
-
149
- # Techstreet opaque conference variant: /<lowercase-alnum slug>
150
- # Charset kept lowercase-alnum so it can't collide with the uppercase
151
- # /E, /Cor, /IEC tails or the self.parse language gsub.
152
- rule(:variant_slug) do
153
- slash >> match("[0-9a-z]").repeat(1).as(:variant)
154
- end
155
-
156
- # Conference proceedings paper (x-prefixed): CIE x043-OP01
157
- rule(:conf_proceedings) do
158
- str("CIE") >> space >> str("x") >> digits.as(:conf_number) >>
159
- dash >> paper_code >> digits.as(:paper_number)
160
- end
161
-
162
- # Standalone proceedings paper: CIE OP02 1-5
163
- rule(:standalone_proceedings) do
164
- str("CIE") >> space >> paper_code >> digits.as(:paper_number) >>
165
- space >> (digits >> dash >> digits).as(:page_range)
166
- end
167
-
168
- # D-series: CIE D001-2006 / CIE D002:2004
169
- rule(:d_series_identifier) do
170
- str("CIE") >> space >> str("D").as(:d_prefix) >> digits.as(:number) >> date
171
- end
172
-
173
- # Standard with bare slash-year (no language letter): CIE S 007/1998
174
- # Uses a narrow number(+iteration) pattern rather than the full code rule
175
- # so code_with_part_slash doesn't greedily consume the /year.
176
- rule(:standard_with_slash_year) do
177
- str("CIE") >> space >>
178
- stage_code.maybe.as(:stage) >>
179
- s_prefix.maybe.as(:s_prefix) >>
180
- (digits.as(:number) >> (dot >> digits.as(:iteration)).maybe) >>
181
- slash >> year_digits.as(:slash_year)
182
- end
183
-
184
- # DIS stage with supplement pattern (e.g., DIS 025-SP1/E:2019)
185
- rule(:dis_with_supplement) do
186
- str("CIE") >> space >>
187
- stage_code.as(:stage) >>
188
- s_prefix.maybe.as(:s_prefix) >>
189
- digits.as(:base_number) >>
190
- dash >> str("SP") >> digits.as(:supplement_number) >>
191
- (dot >> digits.as(:supplement_part)).maybe >>
192
- slash >> upper.as(:lang_code) >> colon.as(:lang_colon) >> year_digits.as(:year)
193
- end
194
-
195
- # Standard identifier (most common) - language can come AFTER date
196
- rule(:standard_identifier) do
197
- str("CIE") >> space >>
198
- stage_code.maybe.as(:stage) >>
199
- s_prefix.maybe.as(:s_prefix) >>
200
- code >>
201
- # Language can come BEFORE date, AFTER date, or both.
202
- # The whole date group is optional so a bare "CIE 015" (no year)
203
- # parses with year left nil (partial reference for relaton matching).
204
- (
205
- (language >> date).as(:lang_before) |
206
- (date >> language.maybe).as(:date_then_lang) |
207
- date
208
- ).maybe
209
- end
210
-
211
- # Standard identifier without ISO reference but with language-year (CIE S 014-4/E:2007)
212
- rule(:standard_with_language_year) do
213
- str("CIE") >> space >>
214
- stage_code.maybe.as(:stage) >>
215
- s_prefix.maybe.as(:s_prefix) >>
216
- code >>
217
- slash >> upper.as(:lang_code) >> colon.as(:lang_colon) >> year_digits.as(:year)
218
- end
219
-
220
- # Conference identifier (x-prefix)
221
- rule(:conference_identifier) do
222
- str("CIE") >> space >>
223
- x_prefix.as(:conference) >>
224
- digits.as(:conf_number) >>
225
- date >>
226
- # Optional amendment
227
- (space >> str("Amendment") >> space >> digits.as(:amd_number)).maybe >>
228
- # Optional techstreet opaque /slug variant
229
- variant_slug.maybe
230
- end
231
-
232
- # Joint published with ISO
233
- rule(:joint_with_iso) do
234
- str("CIE") >> space >>
235
- str("ISO").as(:copublisher) >> # Match and mark once
236
- (slash >> str("CIE")).absent? >> # Make sure it's not "ISO/CIE"
237
- space >>
238
- (doc_type >> space).maybe.as(:doc_type) >> # Consume space here
239
- stage_code.maybe.as(:stage) >>
240
- digits.as(:number) >>
241
- (dash >> digits.as(:part)).maybe >>
242
- ((colon >> year_digits.as(:year)) | (dash >> year_digits.as(:year))).maybe >>
243
- language.maybe
244
- end
245
-
246
- # Joint with ISO/CIE (special case)
247
- rule(:joint_with_iso_cie) do
248
- str("CIE") >> space >>
249
- str("ISO/CIE").as(:copublisher) >> # Match and mark once
250
- space >>
251
- (doc_type >> space).maybe.as(:doc_type) >> # Consume space here
252
- digits.as(:number) >>
253
- (colon >> year_digits.as(:year)).maybe >>
254
- language.maybe
255
- end
256
-
257
- # Joint published with IEC
258
- rule(:joint_with_iec) do
259
- str("CIE") >> space >>
260
- str("IEC").as(:copublisher) >> # Match and mark once
261
- space >>
262
- digits.as(:number) >>
263
- dot >> digits.as(:part) >> # IEC always uses dot
264
- dash >> year_digits.as(:year)
265
- end
266
-
267
- # Identical with ISO (parenthetical reference)
268
- # Patterns:
269
- # "CIE S 006.1/1998 (ISO 16508:1999)" - iteration with dot
270
- # "CIE S 014-4/E2007" - part with dash + language without colon (needs colon insertion)
271
- # "CIE S 008/E:2001 (ISO 8995-1:2002(E))" - language with colon year
272
- rule(:identical_with_iso) do
273
- str("CIE") >> space >>
274
- s_prefix.maybe.as(:s_prefix) >>
275
- digits.as(:number) >>
276
- # Can have iteration (.1) OR part (-4)
277
- (
278
- (dot >> digits.as(:iteration)) |
279
- (dash >> digits.as(:part))
280
- ).maybe >>
281
- # Slash handling - three mutually exclusive patterns:
282
- (slash >>
283
- (
284
- # /E:2001 - language code WITH colon year
285
- (upper.as(:lang_code) >> colon.as(:lang_colon) >> year_digits.as(:year)) |
286
- # /E2007 - language code WITHOUT colon + year (needs colon insertion)
287
- (upper.as(:lang_code) >> year_digits.as(:year)) |
288
- # /1998 - just legacy slash-year (no language)
289
- year_digits.as(:slash_year)
290
- )
291
- ).maybe >>
292
- # ISO reference - handle nested parentheses for language codes
293
- space >> str("(ISO") >> space >>
294
- # Match ISO identifier: digits, colons, dashes, slashes, dots
295
- # Plus optional language code in parentheses like (E), (F)
296
- (
297
- match("[0-9A-Z:/.\\-]").repeat(1) >> # Main ISO identifier
298
- (str("(") >> match("[A-Z]").repeat(1) >> str(")")).maybe # Optional (E), (F), etc.
299
- ).as(:iso_reference) >>
300
- str(")")
301
- end
302
-
303
- # Dual published with IEC (slash separator between full identifiers)
304
- # Pattern: "CIE S 009:2002/IEC 62471:2006"
305
- rule(:dual_with_iec) do
306
- str("CIE") >> space >>
307
- s_prefix.maybe.as(:s_prefix) >>
308
- digits.as(:number) >>
309
- colon >> year_digits.as(:year) >>
310
- slash >> str("IEC") >> space >>
311
- match("[^)\\n]").repeat(1).as(:iec_identifier)
312
- end
313
-
314
- # Supplement identifier (-SPN notation)
315
- rule(:supplement_identifier) do
316
- str("CIE") >> space >>
317
- digits.as(:base_number) >>
318
- dash >> str("SP") >> digits.as(:supplement_number) >>
319
- (dot >> digits.as(:supplement_part)).maybe >>
320
- colon >> year_digits.as(:year)
321
- end
322
-
323
- # Corrigendum identifier (/CorN notation)
324
- rule(:corrigendum_identifier) do
325
- # Parse base identifier first (could be supplement or standard)
326
- str("CIE") >> space >>
327
- digits.as(:base_number) >>
328
- # Could have supplement notation before corrigendum
329
- (dash >> str("SP") >> digits.as(:base_supplement) >>
330
- (dot >> digits.as(:base_supplement_part)).maybe).maybe >>
331
- colon >> year_digits.as(:base_year) >>
332
- # Corrigendum portion
333
- slash >> str("Cor") >> digits.as(:cor_number) >>
334
- colon >> year_digits.as(:cor_year)
335
- end
336
-
337
- # Bundle identifier (comma-separated list)
338
- rule(:bundle_identifier) do
339
- str("CIE") >> space >>
340
- digits.as(:first_number) >>
341
- dash >> str("SP") >> digits >> dot >> digits >>
342
- colon >> year_digits >>
343
- (comma >> digits.as(:next_number) >>
344
- dash >> str("SP") >> digits >> dot >> digits >>
345
- colon >> year_digits).repeat(1).as(:bundle_items)
346
- end
347
-
348
- # Tutorial Bundle
349
- rule(:tutorial_bundle) do
350
- str("CIE Tutorials Bundle") >> space >> digits.as(:bundle_number)
351
- end
352
-
353
- # Main identifier rule (longest/most specific first)
354
- root(:identifier)
355
-
356
- rule(:identifier) do
357
- tutorial_bundle |
358
- bundle_identifier |
359
- joint_with_iso_cie |
360
- joint_with_iso |
361
- joint_with_iec |
362
- identical_with_iso |
363
- dis_with_supplement |
364
- dual_with_iec |
365
- corrigendum_identifier |
366
- supplement_identifier |
367
- standard_with_language_year |
368
- standard_with_slash_year |
369
- d_series_identifier |
370
- conf_proceedings |
371
- standalone_proceedings |
372
- legacy_code_with_year | # Before standard to catch 001-1980 pattern
373
- conference_identifier |
374
- standard_identifier
375
- end
376
-
377
- # Class method for parsing with preprocessing
378
- # Pre-parse ingestion normalizations (R2): every parse path —
379
- # parslet and PG artifact alike — feeds the grammar the same
380
- # normalized string.
7
+ class Parser
8
+ # Pre-parse ingestion normalizations (R2): every parse path feeds
9
+ # the grammar the same normalized string.
381
10
  def self.normalize_input(string)
382
11
  # Minimal preprocessing for data quality
383
12
  cleaned = string.strip
@@ -394,10 +23,6 @@ module Pubid
394
23
 
395
24
  cleaned
396
25
  end
397
-
398
- def self.parse(string)
399
- new.parse(normalize_input(string))
400
- end
401
26
  end
402
27
  end
403
28
  end
@@ -20,7 +20,7 @@ module Pubid
20
20
  # Neutral error vocabulary. Each implementation maps its native
21
21
  # exceptions onto these codes; the corpus never records Ruby classes.
22
22
  ERROR_CODES = {
23
- "Parslet::ParseFailed" => "parse_failed",
23
+ "Pubid::Errors::ParseError" => "parse_failed",
24
24
  "ArgumentError" => "invalid_argument",
25
25
  }.freeze
26
26
 
@@ -33,12 +33,12 @@ module Pubid
33
33
  end
34
34
 
35
35
  # Handle bundled identifiers (with + notation)
36
- if parsed_hash.key?(:bundled_first)
36
+ if parsed_hash[:plus_separator]
37
37
  return build_bundled(parsed_hash)
38
38
  end
39
39
 
40
40
  # Handle combined identifiers (with / notation)
41
- if parsed_hash[:first] && parsed_hash[:second]
41
+ if parsed_hash[:csa_code_part] && !parsed_hash[:plus_separator]
42
42
  return build_combined(parsed_hash)
43
43
  end
44
44
 
@@ -101,7 +101,7 @@ module Pubid
101
101
  else
102
102
  begin
103
103
  Pubid::Iso.parse(base_str)
104
- rescue Parslet::ParseFailed
104
+ rescue Pubid::Errors::ParseError
105
105
  Pubid::Iec.parse(base_str)
106
106
  end
107
107
  end
@@ -135,7 +135,7 @@ module Pubid
135
135
  else
136
136
  begin
137
137
  Pubid::Iso.parse(amendment_str)
138
- rescue Parslet::ParseFailed
138
+ rescue Pubid::Errors::ParseError
139
139
  Pubid::Iec.parse(amendment_str)
140
140
  end
141
141
  end
@@ -155,7 +155,7 @@ module Pubid
155
155
  else
156
156
  begin
157
157
  Pubid::Iso.parse(wrapped_id_str)
158
- rescue Parslet::ParseFailed
158
+ rescue Pubid::Errors::ParseError
159
159
  Pubid::Iec.parse(wrapped_id_str)
160
160
  end
161
161
  end
@@ -417,17 +417,11 @@ module Pubid
417
417
  bundled = Identifiers::Bundled.new
418
418
 
419
419
  # Build base identifier
420
- bundled.base = build_single(parsed_hash[:base])
420
+ parts = parsed_hash[:csa_code_part]
421
+ bundled.base = build_single(strip_part_prefix(parts.first))
421
422
 
422
- # Build bundled portions (amendments/supplements)
423
- # Combine bundled_first with bundled_rest array
424
- bundled_portions = [parsed_hash[:bundled_first]]
425
- if parsed_hash[:bundled_rest] && !parsed_hash[:bundled_rest].empty?
426
- bundled_portions += parsed_hash[:bundled_rest]
427
- end
428
-
429
- bundled.bundled_with = bundled_portions.map do |portion|
430
- build_single(portion)
423
+ bundled.bundled_with = parts[1..].map do |portion|
424
+ build_single(strip_part_prefix(portion))
431
425
  end
432
426
 
433
427
  # Reaffirmation - supports both 2-digit and 4-digit years
@@ -450,13 +444,33 @@ module Pubid
450
444
  bundled
451
445
  end
452
446
 
447
+
448
+ # Strips the part_-prefix the combined/bundled array group carries,
449
+ # leaving the plain field names build_single reads.
450
+ def strip_part_prefix(part)
451
+ part.each_with_object({}) do |(key, value), acc|
452
+ acc[key.to_s.delete_prefix("part_").to_sym] = value
453
+ end
454
+ end
455
+
453
456
  def build_combined(parsed_hash)
454
457
  combined = Identifiers::Combined.new
455
458
 
459
+ parts = parsed_hash[:csa_code_part].map(&:dup)
460
+ # SERIES and series_prefix bind as top-level scalars: the capture
461
+ # only fires in the parts that carry the keyword, and array
462
+ # bindings pair occurrences by index, so a sparse capture would
463
+ # land on the wrong part. The keyword qualifies the designation it
464
+ # follows — the last part in every combined form.
465
+ if parsed_hash[:part_series]
466
+ parts.last[:part_series] = parsed_hash[:part_series]
467
+ parts.last[:part_series_prefix] = parsed_hash[:part_series_prefix] if
468
+ parsed_hash[:part_series_prefix]
469
+ end
470
+
456
471
  # Build each designation, in printed order, into one collection.
457
- combined.identifiers = %i[first second third]
458
- .filter_map { |key| parsed_hash[key] }
459
- .map { |part| build_single(part) }
472
+ combined.identifiers = parts
473
+ .map { |part| build_single(strip_part_prefix(part)) }
460
474
 
461
475
  # Detect separator from parser marker
462
476
  combined.separator = if parsed_hash[:comma_separator]
@@ -627,6 +641,15 @@ module Pubid
627
641
  identifier.package = data[:package_portion].to_s
628
642
  end
629
643
 
644
+ # Amendment riding on the identifier ("/A1:15" after the base)
645
+ if data[:amendment_number]
646
+ return Identifiers::Amendment.new(
647
+ base: identifier,
648
+ amendment_number: data[:amendment_number].to_s,
649
+ amendment_year: data[:amendment_year]&.to_s,
650
+ )
651
+ end
652
+
630
653
  identifier
631
654
  end
632
655
 
@@ -61,7 +61,7 @@ module Pubid
61
61
  normalized = normalized.gsub(/\s+/, " ").strip
62
62
 
63
63
  # Parse normally (will create Bundled or Combined identifier)
64
- tree = Pubid::Parg::Backend.parse(:csa, Parser.normalize_input(normalized))
64
+ tree = Pubid::Parg::Model.bound(:csa, Parser.normalize_input(normalized))
65
65
  prefix = Parser.publisher_prefix_for(normalized)
66
66
  Parser.inject_publisher_prefix(tree, prefix) if prefix && tree.is_a?(Hash)
67
67
  result = build!(tree, input)
@@ -421,7 +421,7 @@ module Pubid
421
421
  # Normalize CAN3- to CSA (historical prefix)
422
422
  normalized = normalized.gsub("CAN3-", "CSA ")
423
423
 
424
- tree = Pubid::Parg::Backend.parse(:csa, Parser.normalize_input(normalized))
424
+ tree = Pubid::Parg::Model.bound(:csa, Parser.normalize_input(normalized))
425
425
  prefix = Parser.publisher_prefix_for(normalized)
426
426
  Parser.inject_publisher_prefix(tree, prefix) if prefix && tree.is_a?(Hash)
427
427
  result = build!(tree, input)
@@ -433,7 +433,7 @@ module Pubid
433
433
  result.year_format = "dash" if has_dash_year && result.year_format.nil?
434
434
 
435
435
  result
436
- rescue Parslet::ParseFailed => e
436
+ rescue Pubid::Errors::ParseError => e
437
437
  raise e
438
438
  end
439
439
 
@@ -459,7 +459,7 @@ module Pubid
459
459
  # it is the one caller that genuinely wants a nil rather than a raise.
460
460
  def self.try_parse(input)
461
461
  parse(input)
462
- rescue Parslet::ParseFailed, ArgumentError
462
+ rescue Pubid::Errors::ParseError, ArgumentError
463
463
  nil
464
464
  end
465
465
 
@@ -523,6 +523,14 @@ module Pubid
523
523
  nil
524
524
  end
525
525
 
526
+ # The leading publisher prefixes a CSA rendering can carry
527
+ # (`publisher_prefix` records which one the input used). The national
528
+ # forms `CAN/CSA-` and `CAN3-` are dropped whole, so
529
+ # `with_publisher: false` gives `C22.2 NO. 60601-1:14`, not `CSA-C22.2…`.
530
+ def publisher_prefixes
531
+ ["CAN/CSA", "CAN3", "CSA"]
532
+ end
533
+
526
534
  # CSA stores the publication year in its own plain `year` string
527
535
  # attribute (not a `Components::Date`), so the base `#exclude`'s
528
536
  # `:year`->`:date` remap nils the unused inherited `date` and leaves
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Csa
5
+ module Identifiers
6
+ # An amendment riding on a CSA identifier: "/A1:15" after the base
7
+ # designation. The grammar binds amendment_number and
8
+ # amendment_year; the builder wraps the base identifier in this
9
+ # class when they are present.
10
+ class Amendment < Identifier
11
+ attribute :base, ::Pubid::Identifier
12
+ attribute :amendment_number, :string
13
+ attribute :amendment_year, :string
14
+
15
+ def initialize(base:, amendment_number:, amendment_year: nil, **opts)
16
+ super(base: base, amendment_number: amendment_number,
17
+ amendment_year: amendment_year, **opts)
18
+ end
19
+
20
+ def to_s(**opts)
21
+ suffix = "/A#{amendment_number}"
22
+ suffix += ":#{amendment_year}" if amendment_year
23
+ "#{base.to_s(**opts)}#{suffix}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,6 +4,7 @@ module Pubid
4
4
  module Csa
5
5
  module Identifiers
6
6
  autoload :Base, "#{__dir__}/identifiers/base"
7
+ autoload :Amendment, "#{__dir__}/identifiers/amendment"
7
8
  autoload :Bundled, "#{__dir__}/identifiers/bundled"
8
9
  autoload :CanadianAdopted, "#{__dir__}/identifiers/canadian_adopted"
9
10
  autoload :Cec, "#{__dir__}/identifiers/cec"