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,382 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Csa
7
- class Parser < ::Pubid::Parser::Grammar
8
- # Basic building blocks
9
- rule(:space) { str(" ") }
10
- rule(:dash) { str("-") }
11
- rule(:slash) { str("/") }
12
- rule(:colon) { str(":") }
13
- rule(:comma) { str(",") }
14
- rule(:dot) { str(".") }
15
- rule(:ampersand) { str("&") }
16
- rule(:plus) { str(" + ") }
17
- rule(:digit) { match("[0-9]") }
18
- rule(:digits) { digit.repeat(1) }
19
- rule(:letter) { match("[A-Z]") }
20
- rule(:letters) { letter.repeat(1) }
21
-
22
- # Publisher
23
- rule(:publisher) do
24
- str("CSA") >> space >> (str("").as(:has_publisher) | str("0").as(:has_publisher))
25
- end
26
-
27
- # Code pattern: letter + dotted numbers (e.g., B149.1, C22.2, A123.17)
28
- # Can also have dash-number-letter suffix (e.g., for NO. numbers like 60950-1A)
29
- # Can have letter suffixes like HB, CIICHB, SP at the end
30
- # Can have multiple dash-number sequences (e.g., C13256-1-01)
31
- # NEW: Also support pure numbers with HB suffix (e.g., 15189HB)
32
- # NEW: Simple letter+number codes (e.g., Z240, A220) for Series identifiers
33
- rule(:code_pattern) do
34
- # Pattern 1: Pure dotted numbers - e.g., 12.4, 2.15, 3.11
35
- (match("[0-9]").repeat(1) >> dot >> match("[0-9]").repeat(1) >>
36
- # Allow single-dash suffix for part numbers (e.g., "12.4-1")
37
- # But NOT 2-digit years which are handled separately
38
- (dash >> match("[0-9]").repeat(1)).repeat(0, 1) >>
39
- letter.repeat(2, 6).maybe).as(:code) |
40
- # Pattern 2: Pure numbers with HB suffix - e.g., 15189HB (NEW)
41
- (match("[0-9]").repeat(1) >> letter.repeat(2, 6)).as(:code) |
42
- # Pattern 3: Letter + numbers with dots then dashes - e.g., C22.2-144, B149.1
43
- (letter >> match("[0-9]").repeat(1) >>
44
- (dot >> match("[0-9]").repeat(1)).repeat >> # Dot sections first
45
- # Dash sections after - allow part numbers but not 2-digit years
46
- # Single digit dash = part number (e.g., "-1", "-2")
47
- # 3+ digits or letter suffix = part number (e.g., "-144", "-1A")
48
- # But DON'T consume 2-digit years which are matched separately
49
- (dash >> (
50
- (match("[0-9]") >> match("[0-9]").absent?) | # exactly 1 digit (no digit follows)
51
- match("[0-9]").repeat(3) | # 3+ digits = part number (e.g., "-144")
52
- (match("[0-9]").repeat(1) >> letter.repeat(1)) # digit + letter = part number (e.g., "-1A")
53
- )).repeat >>
54
- letter.repeat(2, 6).maybe).as(:code) | # Allow HB, CIICHB, SP, etc.
55
- # Pattern 4: Simple letter + number codes - e.g., Z240, A220, B140 (for Series)
56
- (letter >> match("[0-9]").repeat(1) >> dot >> match("[0-9]").repeat(1).maybe).as(:code) |
57
- # Pattern 5: Letter + multi-digit number without dots/dashes - e.g., Z240, A220
58
- (letter >> match("[0-9]").repeat(2)).as(:code)
59
- end
60
-
61
- # NO. notation - "Number" designation within a series (e.g., C22.2 NO. 1)
62
- rule(:no_notation) do
63
- space >> (str("NO.") | str("No.")).as(:no_notation) >> space
64
- end
65
-
66
- # Number after NO. - can be complex (e.g., 60601-1-9, 144.1, 1010.2.031, 1, 60601-1)
67
- # Note: Single-dash patterns allowed only when first part has 3+ digits
68
- # to avoid consuming year patterns like "1-04" where "-04" is the year
69
- rule(:no_number) do
70
- (
71
- # Multi-part with dashes (e.g., 60601-1-9, 1-9-2)
72
- # Requires at least 2 dashes (3+ numeric parts) to distinguish from year patterns
73
- (match("[0-9]").repeat(1) >>
74
- (dash >> match("[0-9]").repeat(1)).repeat(2, 10)) |
75
- # Single-dash pattern (e.g., 60601-1, 60079-11) - allowed if first part has 3+ digits
76
- # This prevents consuming year patterns like "1-04"
77
- # CRITICAL: 2-digit suffix allowed only when first part has 5+ digits
78
- # (IEC-style numbers like 60601, 60079). 3-4 digit first parts with 2-digit
79
- # suffixes are year patterns (e.g., "109-17" = number 109, year 2017).
80
- (match("[0-9]").repeat(5) >> dash >> match("[0-9]").repeat(1)) | # 5+ digits: any suffix
81
- (match("[0-9]").repeat(3, 4) >> dash >> (
82
- (match("[0-9]") >> match("[0-9]").absent?) | # exactly 1 digit (e.g., -1)
83
- match("[0-9]").repeat(3) # 3+ digits (e.g., -144)
84
- )) |
85
- # Multi-part dotted number (e.g., 144.1, 1010.2.031, 1010.2.031.5)
86
- # Can have 1-3 dots (2-4 numeric parts)
87
- (match("[0-9]").repeat(1) >>
88
- (dot >> match("[0-9]").repeat(1)).repeat(1, 3)) |
89
- # Simple number (e.g., 1, 286)
90
- match("[0-9]").repeat(1)
91
- ).as(:no_number)
92
- end
93
-
94
- # Year format with optional F or M prefix
95
- rule(:year_prefix) { (str("F") | str("M")).as(:year_prefix).maybe }
96
- rule(:year_2digit) { digit.repeat(2, 2) }
97
- rule(:year_4digit) { digit.repeat(4, 4) }
98
-
99
- # Year with colon (modern format) - mark as colon_year
100
- # Support both with prefix (:M04) and without prefix (:04)
101
- rule(:colon_year) do
102
- str("").as(:colon_format) >> # Mark that colon format was used
103
- colon >> (
104
- # With M/F prefix (e.g., :M04, :F04)
105
- (year_prefix >> (year_4digit | year_2digit).as(:year)) |
106
- # Without prefix (e.g., :04)
107
- (year_4digit | year_2digit).as(:year)
108
- )
109
- end
110
-
111
- # Year with dash (older format) - mark as dash_year
112
- # Support both with prefix (-M04) and without prefix (-04)
113
- rule(:dash_year) do
114
- str("").as(:dash_format) >> # Mark that dash format was used
115
- dash >> (
116
- # With M/F prefix (e.g., -M04, -F04)
117
- (year_prefix >> (year_4digit | year_2digit).as(:year)) |
118
- # Without prefix (e.g., -04)
119
- (year_4digit | year_2digit).as(:year)
120
- )
121
- end
122
-
123
- # Reaffirmation notation - supports both 2-digit and 4-digit years
124
- # Tracks original format via :reaffirmation_4digit flag
125
- rule(:reaffirmation) do
126
- space >> str("(R") >>
127
- (year_4digit.as(:reaffirmation_4digit) | year_2digit.as(:reaffirmation_2digit)) >>
128
- str(")")
129
- end
130
-
131
- # Amendment notation (e.g., /A1:15, /A2:22, /Amd 1)
132
- rule(:amendment_slash) do
133
- slash >>
134
- (str("Amd") >> space).maybe >>
135
- str("A").maybe >>
136
- digits.as(:amendment_number) >>
137
- (colon >> year_2digit.as(:amendment_year)).maybe
138
- end
139
-
140
- # Package keywords
141
- rule(:package_keyword) do
142
- str("Code") | str("Handbook") | str("Training Package") | str("Package")
143
- end
144
-
145
- # Package portion: "Code, Handbook & Training Package"
146
- rule(:package_portion) do
147
- space >>
148
- package_keyword >>
149
- (
150
- (comma >> space >> package_keyword) |
151
- (space >> ampersand >> space >> package_keyword)
152
- ).repeat
153
- end
154
-
155
- # Series prefix: 2-3 letters before SERIES keyword (e.g., MH, RV)
156
- rule(:series_prefix) do
157
- letter.repeat(2, 3).as(:series_prefix)
158
- end
159
-
160
- # SERIES keyword - just the word, don't consume the delimiter
161
- rule(:series_keyword) do
162
- str("SERIES")
163
- end
164
-
165
- # Series identifier - SERIES as PRIMARY type (not modifier)
166
- # Pattern: CSA code [PREFIX] SERIES:year
167
- # Examples: CSA Z240 MH SERIES:16, CSA Z240 RV SERIES:23, CSA Z245.20 SERIES:22
168
- rule(:series_identifier) do
169
- publisher >>
170
- # Code comes first (can be letter+number or dotted numbers)
171
- (
172
- code_pattern |
173
- # Letter + multi-digit number for Series (e.g., Z240, A220, B139)
174
- (letter >> match("[0-9]").repeat(2)).as(:code) |
175
- # Dotted numeric code (e.g., 245.20)
176
- (match("[0-9]").repeat(1) >> dot >> match("[0-9]").repeat(1)).as(:code)
177
- ) >>
178
- space >>
179
- # Optional series prefix (MH, RV, etc.) before SERIES keyword
180
- (series_prefix >> space).maybe >>
181
- series_keyword.as(:series_type) >>
182
- (colon_year | dash_year).maybe >>
183
- reaffirmation.maybe
184
- end
185
-
186
- # CEC (Canadian Electrical Code) identifier
187
- # Pattern: CSA C22.{2,3,4,6} NO. {number}:{year}
188
- # Examples: CSA C22.2 NO. 286:23, CSA C22.3 NO. 7:20
189
- # The "NO." notation is a semantic component and must be preserved
190
- rule(:cec_identifier) do
191
- publisher >>
192
- (
193
- str("C22.2") | str("C22.3") | str("C22.4") | str("C22.6")
194
- ).as(:cec_part) >>
195
- no_notation >>
196
- no_number >>
197
- (colon_year | dash_year).maybe >>
198
- reaffirmation.maybe
199
- end
200
-
201
- # ISO/IEC/CISPR/CEI adopted standards pattern
202
- # Examples:
203
- # CSA ISO/IEC 9594-2:21
204
- # CSA ISO/IEC TR 12785-3:15
205
- # CSA CISPR 16-1-1:18
206
- # CAN/CSA-ISO 10819:16
207
- # CAN/CSA-IEC 62443-2-4:17
208
- # CAN/CSA-CEI/IEC 61000-4-28-01
209
- rule(:adoption_identifier) do
210
- publisher.as(:adoption_pub) >>
211
- (
212
- # Option 1: ISO/IEC with TR/TS
213
- (str("ISO/IEC").as(:adoption_org) >> space >>
214
- (str("TR") | str("TS")).as(:iso_type) >> space >>
215
- match("[0-9-]").repeat(1).as(:adoption_number)) |
216
- # Option 1a: ISO/IEC without TR/TS (must come after TR/TS option in priority)
217
- (str("ISO/IEC").as(:adoption_org_with_type) >> space >>
218
- match("[0-9-]").repeat(1).as(:adoption_number)) |
219
- # Option 2: ISO alone
220
- (str("ISO").as(:adoption_org) >> space >>
221
- match("[0-9-]").repeat(1).as(:adoption_number)) |
222
- # Option 3: IEC alone
223
- (str("IEC").as(:adoption_org) >> space >>
224
- match("[0-9-]").repeat(1).as(:adoption_number)) |
225
- # Option 4: CEI/IEC (French name for IEC)
226
- (str("CEI/IEC").as(:adoption_org) >> space >>
227
- match("[0-9-]").repeat(1).as(:adoption_number)) |
228
- # Option 5: CISPR
229
- (str("CISPR").as(:adoption_org) >> space >>
230
- match("[0-9-]").repeat(1).as(:adoption_number))
231
- ) >>
232
- (colon | dash).as(:adoption_year_sep) >>
233
- (year_4digit | year_2digit).as(:adoption_year) >>
234
- (
235
- # Amendment with year: /A1:22 or /A1-22
236
- slash >> str("A") >> digit.as(:adoption_amendment) >>
237
- (colon | dash) >> (year_4digit | year_2digit).as(:adoption_amendment_year)
238
- ).maybe >>
239
- reaffirmation.maybe
240
- end
241
-
242
- # Legacy: ISO/IEC TR/TS only pattern (for backward compatibility)
243
- rule(:iso_iec_adoption) do
244
- adoption_identifier
245
- end
246
-
247
- # Basic CSA identifier
248
- rule(:csa_code) do
249
- publisher >>
250
- code_pattern >>
251
- (
252
- # Option 1: NO. notation (e.g., C22.2 NO. 1:20)
253
- (no_notation >> no_number >> (colon_year | dash_year)) |
254
- # Option 2: series with prefix (space + prefix + space + keyword + year)
255
- (space >> series_prefix >> space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
256
- # Option 3: series without prefix (space + keyword + year)
257
- (space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
258
- # Option 4: just year (no series, no NO.)
259
- (colon_year | dash_year)
260
- ).maybe >> # trailing year optional (partial reference)
261
- amendment_slash.maybe # Add amendment support here
262
- end
263
-
264
- # Base CSA code without amendment (for combined identifiers)
265
- rule(:base_csa_code) do
266
- publisher >>
267
- code_pattern >>
268
- (
269
- # Option 1: NO. notation
270
- (no_notation >> no_number >> (colon_year | dash_year)) |
271
- # Option 2: series with prefix
272
- (space >> series_prefix >> space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
273
- # Option 3: series without prefix
274
- (space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
275
- # Option 4: just year
276
- (colon_year | dash_year)
277
- ).maybe # trailing year optional (partial reference)
278
- end
279
-
280
- # Continuation code (optional CSA publisher prefix, for combined identifiers)
281
- rule(:continuation_code) do
282
- publisher.as(:has_publisher).maybe >>
283
- code_pattern >>
284
- (
285
- # Option 1: NO. notation
286
- (no_notation >> no_number >> (colon_year | dash_year)) |
287
- # Option 2: series with prefix
288
- (space >> series_prefix >> space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
289
- # Option 3: series without prefix
290
- (space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
291
- # Option 4: just year
292
- (colon_year | dash_year)
293
- ).maybe >> # trailing year optional (partial reference)
294
- amendment_slash.maybe
295
- end
296
-
297
- # Bundled portion - code that follows + (base is implied, no CSA prefix)
298
- # Example: "A2:22" in "CSA C22.2 NO. 60601-1:14 + A2:22"
299
- rule(:bundled_portion) do
300
- code_pattern >>
301
- (
302
- # Option 1: NO. notation
303
- (no_notation >> no_number >> (colon_year | dash_year).maybe) |
304
- # Option 2: just year or no year
305
- (colon_year | dash_year).maybe
306
- )
307
- end
308
-
309
- # Bundled identifier with + notation (consolidated documents)
310
- rule(:bundled_identifier) do
311
- csa_code.as(:base) >>
312
- plus >>
313
- bundled_portion.as(:bundled_first) >>
314
- (plus >> bundled_portion).repeat.as(:bundled_rest) >>
315
- reaffirmation.maybe
316
- end
317
-
318
- # Combined CSA standards with slash
319
- rule(:combined_slash) do
320
- base_csa_code.as(:first) >>
321
- slash >>
322
- continuation_code.as(:second) >>
323
- (slash >> continuation_code.as(:third)).maybe >>
324
- reaffirmation.maybe >>
325
- package_portion.maybe
326
- end
327
-
328
- # Combined with comma (CSA B149.1:25, CSA B149.2:25)
329
- rule(:combined_comma) do
330
- csa_code.as(:first) >>
331
- comma >> space >>
332
- csa_code.as(:second) >>
333
- reaffirmation.maybe >>
334
- (
335
- (space >> ampersand >> package_portion) |
336
- package_portion
337
- ).as(:package_portion).maybe >>
338
- str("").as(:comma_separator) # Mark as comma-based
339
- end
340
-
341
- # Single identifier with optional package
342
- rule(:single_identifier) do
343
- csa_code >> reaffirmation.maybe >> package_portion.as(:package_portion).maybe
344
- end
345
-
346
- # Code-only identifier (no CSA prefix) - try this last
347
- rule(:code_only_identifier) do
348
- code_pattern >>
349
- (
350
- # Option 1: series with prefix
351
- (space >> series_prefix >> space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
352
- # Option 2: series without prefix
353
- (space >> series_keyword.as(:series) >> (colon_year | dash_year)) |
354
- # Option 3: just year
355
- (colon_year | dash_year)
356
- ).maybe >> # trailing year optional (partial reference)
357
- (space >> str("PACKAGE")).as(:package_portion).maybe
358
- end
359
-
360
- # Main identifier
361
- rule(:identifier) do
362
- iso_iec_adoption | series_identifier | cec_identifier | bundled_identifier | combined_slash | combined_comma | single_identifier | code_only_identifier
363
- end
364
-
365
- root(:identifier)
366
-
367
- # Preprocessing to normalize input
368
- def parse(input)
369
- prefix = self.class.publisher_prefix_for(input)
370
- result = super(self.class.normalize_input(input))
371
- if prefix && result.is_a?(Hash)
372
- self.class.inject_publisher_prefix(result, prefix)
373
- end
374
- result
375
- end
376
-
377
- # Pre-parse ingestion normalizations (R2): every parse path —
378
- # parslet and PG artifact alike — feeds the grammar the same
379
- # normalized string.
5
+ class Parser
6
+ # Pre-parse ingestion normalizations (R2): every parse path feeds
7
+ # the grammar the same normalized string.
380
8
  def self.normalize_input(input)
381
9
  # Skip comment lines
382
10
  if input.strip.start_with?("#")
@@ -435,30 +63,21 @@ module Pubid
435
63
  private
436
64
 
437
65
  def self.inject_publisher_prefix(hash, prefix)
438
- # For combined identifiers
439
- if hash[:first]
440
- hash[:first][:publisher_prefix] = prefix
441
- end
442
- # For combined_comma (has comma_separator), both first and second are full csa_code
443
- # For combined_slash (no comma_separator), second is continuation that may have has_publisher
444
- if hash[:second] && (hash[:comma_separator] || hash[:second][:has_publisher])
445
- hash[:second][:publisher_prefix] = prefix
446
- end
447
- if hash[:third] && hash[:third][:has_publisher]
448
- hash[:third][:publisher_prefix] =
449
- prefix
450
- end
451
-
452
- # For bundled identifiers
453
- if hash[:base]
454
- inject_publisher_prefix(hash[:base], prefix)
455
- end
456
-
457
- # For single identifiers (no :first, no :base, just raw attributes)
458
- if !hash[:first] && !hash[:base] && !hash[:bundled_first]
459
- hash[:publisher_prefix] = prefix
66
+ # Combined and bundled identifiers carry their parts under the
67
+ # csa_code_part[] group; the first part always takes the prefix,
68
+ # later ones only when they spell their own publisher token (a
69
+ # slash continuation like "CSA X/60601-1-6" does not).
70
+ if (parts = hash[:csa_code_part])
71
+ parts.each_with_index do |part, index|
72
+ next if index.positive? && !part[:part_has_publisher] &&
73
+ !hash[:comma_separator]
74
+
75
+ part[:publisher_prefix] = prefix
76
+ end
77
+ return hash
460
78
  end
461
79
 
80
+ hash[:publisher_prefix] = prefix
462
81
  hash
463
82
  end
464
83
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Doi
7
- # Parslet grammar for Digital Object Identifiers per ISO 26324.
5
+ # Grammar for Digital Object Identifiers per ISO 26324.
8
6
  #
9
7
  # Accepts:
10
8
  # [doi:|DOI:]10.{REGISTRAR}/{SUFFIX}
@@ -13,40 +11,7 @@ module Pubid
13
11
  # SUFFIX is one-or-more chars from [A-Za-z0-9._\-/()] (printable minus
14
12
  # whitespace). The grammar is intentionally permissive on suffix chars
15
13
  # because real DOIs cover the full printable range.
16
- class Parser < ::Pubid::Parser::Grammar
17
- rule(:dot) { str(".") }
18
- rule(:slash) { str("/") }
19
-
20
- # Optional URL resolver prefix.
21
- rule(:url_prefix) { (str("https://") | str("http://")).maybe >> str("doi.org").maybe }
22
-
23
- # Optional DOI scheme prefix.
24
- rule(:scheme_prefix) do
25
- (str("doi:") | str("DOI:") | str("Doi:")).maybe
26
- end
27
-
28
- # Prefix: "10." then 2+ registrar digits.
29
- rule(:prefix) { str("10") >> dot >> match("[0-9]").repeat(2).as(:prefix) }
30
-
31
- # Suffix: at least one printable non-whitespace char (DOI Handbook §2.2).
32
- rule(:suffix) { match("[A-Za-z0-9._\\-/()]").repeat(1).as(:suffix) }
33
-
34
- rule(:identifier) do
35
- url_prefix.maybe >>
36
- scheme_prefix.maybe >>
37
- slash.maybe >>
38
- prefix >> slash >> suffix
39
- end
40
-
41
- rule(:root) { identifier }
42
-
43
- def self.parse(input)
44
- # Normalize a URL resolver form to bare form before parsing. The
45
- # parser's `url_prefix.maybe >> scheme_prefix.maybe >> slash.maybe`
46
- # chain also handles this in-grammar, but pre-stripping yields a
47
- # cleaner error message for malformed URLs.
48
- new.parse(input)
49
- end
14
+ class Parser
50
15
  end
51
16
  end
52
17
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pubid
4
4
  module Easc
5
- # Builds an EASC identifier object from the Parslet parse tree.
5
+ # Builds an EASC identifier object from the parg artifact parse tree.
6
6
  class Builder
7
7
  def build(parsed)
8
8
  series_cyr = stringify(parsed[:series])&.upcase
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Easc
7
- # Parslet grammar for EASC identifiers.
5
+ # Grammar for EASC identifiers.
8
6
  #
9
7
  # Accepts the Cyrillic surface forms observed in mgscatalog.by:
10
8
  #
@@ -16,73 +14,7 @@ module Pubid
16
14
  # Also accepts Latin transliterations (PMG, RMG, V) for
17
15
  # round-tripping with English-language tooling. The renderer
18
16
  # emits Cyrillic as the canonical form.
19
- class Parser < ::Pubid::Parser::Grammar
20
- include ::Pubid::Parser::CommonParseRules
21
-
22
- root :identifier
23
-
24
- rule(:space) { str(" ") }
25
- rule(:space?) { space.maybe }
26
- rule(:dot) { str(".") }
27
-
28
- # Year separator: ASCII hyphen OR Unicode em-dash / en-dash,
29
- # optionally surrounded by whitespace.
30
- rule(:year_sep) do
31
- space?.maybe >> (str("-") | str("—") | str("–")) >> space?.maybe
32
- end
33
-
34
- # Series prefix. Captured to route to the right Identifiers::*
35
- # subclass (Pmg vs Rmg). Both Cyrillic and Latin forms.
36
- rule(:series_word) do
37
- (str("ПМГ") | str("РМГ") | str("PMG") | str("RMG")).as(:series) >> space
38
- end
39
-
40
- # Defense variant marker — Cyrillic "В" or Latin "V".
41
- rule(:variant_word) do
42
- (str("В") | str("V")).as(:variant) >> space
43
- end
44
-
45
- rule(:prefix) do
46
- series_word >> variant_word.maybe
47
- end
48
-
49
- rule(:digits) do
50
- match("[0-9]").repeat(1)
51
- end
52
-
53
- rule(:number) do
54
- (digits >> (dot >> digits).repeat).as(:number)
55
- end
56
-
57
- rule(:year_2digit) do
58
- match("[0-9]").repeat(2, 2)
59
- end
60
-
61
- rule(:year_4digit) do
62
- match("[0-9]").repeat(4, 4)
63
- end
64
-
65
- rule(:year) do
66
- (year_4digit | year_2digit).as(:year)
67
- end
68
-
69
- rule(:identifier) do
70
- prefix >> number >> (year_sep >> year).maybe
71
- end
72
-
73
- def self.parse(string)
74
- unless string.is_a?(String)
75
- raise ::Pubid::Errors::InvalidInputError,
76
- ::Pubid::INPUT_NOT_A_STRING_MESSAGE
77
- end
78
-
79
- if string.length > ::Pubid::MAX_INPUT_LENGTH
80
- raise ::Pubid::Errors::InvalidInputError,
81
- ::Pubid::INPUT_TOO_LONG_MESSAGE
82
- end
83
-
84
- new.parse(string.strip)
85
- end
17
+ class Parser
86
18
  end
87
19
  end
88
20
  end
@@ -6,7 +6,7 @@ These notes were part of the root `CLAUDE.md`. Read them before you change `lib/
6
6
 
7
7
  - **ECMA edition and volume — the one flavor whose `to_s` deliberately renders MORE than the document number**: `Relaton::Index::Type#add_or_update` keys on a **bare** `id.to_s` and cannot pass render options, so the default rendering **is** the index key. ECMA's renderer used to say outright that "the edition is never rendered", and 740 of the 804 published `relaton-data-ecma` rows carry one — so all 804 rows collapsed onto **421** keys and **383 were dropped on every crawl, silently** (`ECMA-74` 22 rows to 1, `ECMA-262` 18 to 1). `edition` and `volume` are therefore rendered **by default**, with `to_s(with_edition: false, with_volume: false)` as the opt-out — the inverse of pubid's usual opt-in, and per-flavor divergence in exactly the way 3GPP (`with_publisher: false`) and W3C (`true`) already diverge. **`volume` is a new `attribute :volume, :string`** (relaton's `:vol:`), needed by exactly four rows: ECMA-269 ed3 vol1-4 share one docidentifier **and one title**, so the volume is the only thing separating those four index rows — it is an index discriminator, not something the document prints about itself. Both are plain `:string`s, so `#exclude`/`#matches?` already treat them as wildcards with no override (`ECMA-269` matches every edition under `ignore: %i[edition volume]`); ECMA's `partial_ref_spec.rb` entry moved from `omits: []` to `%i[edition volume]`.
8
8
  **Grammar (`lib/pubid/ecma/parser.rb`)**: ` ed<N>` (dotted — `ed5.1` is real, ECMA-402) and ` vol<N>` attach as two independent `.maybe`s **after** the `tr | mem | standard` alternation, so a technical report and a memento carry them exactly as a standard does. The `standard` branch also gained a **space separator** (`ECMA 6`), which is a **NORMALIZING parse** — it renders back as `ECMA-6`, so it lives in `spec/pubid/ecma/edition_volume_spec.rb` and must **never** enter the byte-exact `pass` fixtures (the BIPM `CIPM/2005-06(REV)` precedent). **Two PEG facts are load-bearing and easy to lose.** (1) `standard` cannot shadow ` TR/`/` MEM/` because after the space it demands a **digit** — verified by inverting the alternation order, which still parses both correctly. Keeping `tr | mem` first is belt-and-braces; the **disjoint first character** is the real invariant. (2) Appending a sequence moved the alternation **out of tail position**, so Parslet no longer threads `consume_all` into it and **will not backtrack into an alternative that already succeeded** (`rule(:alt) { str("ab") | str("a") }; rule(:root) { alt >> str("bc") }` fails on `"abc"`). A future branch matching a proper prefix of another branch's input would now fail the whole parse where before it was rescued. `maybe` is `repeat(0,1)` and does not backtrack either, so a marker without its number (` ed`, ` ed5.`, ` vol`) fails atomically and then fails `consume_all` — which is what rejects `ECMA-6 ed` and pins the canonical suffix order (`ECMA-6 vol1 ed3` is rejected).
9
- **Render-option threading — `:with_volume` rides the `:trademark` route, deliberately.** `Pubid::Identifier#render` `except`s a flag from the rendering context and `slice`s it to the renderer; `:with_edition` was already in both, and `:trademark` is the precedent for an **IEEE-only** flag carried the same way. So `:with_volume` was added to those two lines and **`build_rendering_context`'s signature was left alone** — widening it would touch `Pubid::Iso::SingleIdentifier`'s strict override. The payoff is that **ECMA's `to_s` needed no change at all** and the renderer owns the defaults: no second transient ivar beside the pre-existing `@with_publisher`, so rendering stays stateless and reentrant. Residual blast radius, accepted: passing `with_volume:` to a *non*-ECMA identifier now reaches that flavor's renderer instead of dying in the context builder — `Renderers::HumanReadable` absorbs unknown opts by design, while ISO's three strict renderers (`DirectivesRenderer`/`IwaRenderer`/`SupplementRenderer`) raise, which they did before too.
9
+ **Render-option threading — `:with_volume` rides the `:trademark` route, deliberately.** `Pubid::Identifier#render` `except`s a flag from the rendering context and `slice`s it to the renderer; `:with_edition` was already in both, and `:trademark` is the precedent for an **IEEE-only** flag carried the same way. So `:with_volume` was added to those two lines and **`build_rendering_context`'s signature was left alone** — widening it would touch `Pubid::Iso::SingleIdentifier`'s strict override. The payoff is that **ECMA's `to_s` needed no change at all** and the renderer owns the defaults: no transient ivar, so rendering stays stateless and reentrant (the old `@with_publisher` ivar is gone too — see below). Residual blast radius, accepted: passing `with_volume:` to a *non*-ECMA identifier now reaches that flavor's renderer instead of dying in the context builder — `Renderers::HumanReadable` absorbs unknown opts by design, and ISO's three formerly strict renderers (`DirectivesRenderer`/`IwaRenderer`/`SupplementRenderer`) now take `**_opts` as well, because the shared `with_publisher:` flag reaches them.
10
10
  **All three identity surfaces, and the two defects found by measuring them.** Per the project rule, edition and volume reach `to_s`, `to_urn` **and** `to_mr_string`. **URN**: labelled segments after the existing `part-<n>` (`urn:ecma:269:ed-3:vol-2`), purely additive — `UrnParser` probes for a labelled prefix (a `shift_labelled` helper using `delete_prefix`, not `sub`), so a URN minted before the segments existed still reads back. The **dot of a decimal edition is KEPT** (`urn:ecma:402:ed-5.1`): `.` is `unreserved` in RFC 8141 and the URN has an inverse, so it must be lossless. **MR**: ECMA defined **no** `mr_*` hooks, so (a) the base `mr_edition` (`edition&.number`) **raised `NoMethodError` on all 740 edition-carrying rows** — the BIPM/ASTM/ASHRAE crash again — and (b) `mr_publisher` was nil (the publisher is the `PUBLISHER` constant, not the inherited attribute) and `mr_type` was nil (the Builder picks a class, it never sets a `typed_stage`), so **`ECMA-101` and `ECMA TR/101` both slugged to `"101"`** and one output filename overwrote the other. Four hooks fix it, sanitising **by charset** (`gsub(/[^a-z0-9]+/, "-")`, the BIPM `mr_slug` precedent) rather than by an escape list. **The MR is deliberately lossy where the URN is not** (`ed5-1` vs `ed-5.1`): `Renderers::MrString` joins **segments** with `.`, so a dot inside one breaks the documented structure, and nothing parses an ECMA MR back (`Parsers::MrString::FLAVOR_MAP` has no ECMA entry). Say so in the comment rather than leaving the asymmetry looking accidental.
11
11
  **New shared `mr_volume` slot.** `Renderers::MrString#render_flat` had no volume segment, so it gained one plus a **literal-nil** `mr_volume` hook on `Pubid::Identifier` — literally nil, **not** `volume&.to_s`, because `Pubid::Nist::Identifiers::Base` declares its own `volume` attribute with different MR semantics and a generic reader would silently move NIST's slug. Inert by construction (no flavor defined `mr_volume`, there is no `method_missing` in `lib/pubid`, and NIST overrides `to_mr_string` wholesale), and verified rather than assumed: a `main` baseline replay of `to_mr_string` over all **98,217** identifiers in `spec/fixtures/*/identifiers/pass/*.txt` is **byte-identical**. `render_flat` was rewritten around a frozen `SEGMENTS` list of hook names while it was open, which also cleared a pre-existing ABC offence.
12
12
  **The corpus is the gate.** `spec/fixtures/ecma/identifiers/pass/index_corpus.txt` is the complete published index (804 rows) rendered as `"<:id:>[ ed<:ed:>][ vol<:vol:>]"`, generated from `relaton-data-ecma/index-v1.yaml`; `spec/pubid/ecma/corpus_spec.rb` asserts over all of it that every row parses and renders byte-exactly, that `to_s`/`to_urn`/`to_mr_string` each give **804 distinct** keys, that every slug is filename-safe, that the URN and the hash both round-trip, that `from_hash(to_hash) == id` (stronger than hash equality — `#matches?` is built on `==`), and that `root.number` is non-empty. **ECMA has no `identifiers/full/` corpus**, so `rake "validation:classify[ecma]"` cannot run and cannot clobber these files — its fixtures are hand-maintained, unlike every generated flavor. Note ECMA's `fixtures_spec.rb` glob is **correct** (two `..`, lowercase dir) and is not one of the ten dead ones. **Two existing specs asserted the inverted contract** and had to flip (`serialization_spec.rb`'s "omits edition from the printed string"), so a green suite before the code change was the failure signal, not the success one. **relaton note**: this unblocks `relaton__relaton__ecma-consume-index-v2`; `relaton-data-ecma`'s `index-v1` is string-keyed and no ECMA pubid index is published yet, so nothing needs migrating — but the crawl must run after this lands, or the rows key on the collapsed form again. (hand-off: ecma-edition-and-volume.)
@@ -26,3 +26,10 @@ keeps `#matches?(other, ignore:)`.
26
26
  before the declaration existed. `edition` and `volume` stay wildcards —
27
27
  an omitted edition really does mean "any edition", which is why
28
28
  `partial_ref_spec.rb` lists them under `omits`.
29
+
30
+ - **`with_publisher: false` keeps the `ECMA` token.** `ECMA-262` is how ECMA
31
+ cites its own standard; the old `false` form gave `-262` and `TR/101`, which
32
+ are no designation. `#publisher_prefixes` returns `[]`, so the shared
33
+ stripper (root `CLAUDE.md`, the `with_publisher:` bullet) leaves the string
34
+ alone, and the `@with_publisher` ivar and the renderer branch that read it
35
+ were deleted. XSF made the same change for `XEP 0001`.
@@ -63,21 +63,26 @@ module Pubid
63
63
  # would otherwise fail serialization type validation.
64
64
  PUBLISHER = "ECMA"
65
65
 
66
- # Whether the last `to_s` should include the "ECMA" publisher token.
67
- attr_reader :with_publisher
68
-
69
- # Basic string representation. Delegates to renderer. `with_publisher:
70
- # false` drops the ECMA token (e.g. "-411", "TR/84").
66
+ # Basic string representation. Delegates to renderer.
71
67
  #
72
68
  # The edition and volume suffixes are rendered by DEFAULT and are opted
73
69
  # out of with `to_s(with_edition: false, with_volume: false)`. Both flags
74
70
  # ride in `opts` to the renderer, which owns their defaults — see
75
71
  # Pubid::Ecma::Renderer for why the default is "with".
76
- def to_s(with_publisher: true, **opts)
77
- @with_publisher = with_publisher
72
+ #
73
+ # `with_publisher:` is accepted and has no effect: `ECMA` is part of the
74
+ # designation (`ECMA-262`), not a publisher prefix, so the internal form
75
+ # is the external one (see {#publisher_prefixes}).
76
+ def to_s(**opts)
78
77
  render(format: :human, **opts)
79
78
  end
80
79
 
80
+ # `ECMA-262` is how ECMA itself cites the standard; dropping the token
81
+ # leaves `-262` or `262`, which is no designation at all.
82
+ def publisher_prefixes
83
+ []
84
+ end
85
+
81
86
  # Type token that precedes the number ("TR"/"MEM"), or nil for standards.
82
87
  # Overridden by each concrete identifier class.
83
88
  def type_prefix
@@ -129,7 +134,7 @@ module Pubid
129
134
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
130
135
  end
131
136
 
132
- parsed = Pubid::Parg::Backend.parse(:ecma, identifier)
137
+ parsed = Pubid::Parg::Model.bound(:ecma, identifier)
133
138
  Builder.build(parsed)
134
139
  end
135
140