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,25 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Nist
7
5
  # Parser class for NIST identifiers
8
6
  # Single Responsibility: Parsing NIST identifier syntax
9
- class Parser < ::Pubid::Parser::Grammar
7
+ class Parser
8
+ # The bound hash passes engine nodes through for the (raw) wrapper
9
+ # bindings: Parsanol::Slice spans, single-element repetition arrays,
10
+ # and named-node hashes. The Parslet tree gave the caster plain
11
+ # strings and hashes of strings; this restores that shape.
12
+ def self.normalize_bound(value)
13
+ case value
14
+ when Parsanol::Slice then value.to_s
15
+ when Hash then value.transform_values { |v| normalize_bound(v) }
16
+ when Array
17
+ inner = value.map { |v| normalize_bound(v) }
18
+ inner.length == 1 ? inner.first : inner
19
+ else value
20
+ end
21
+ end
10
22
 
11
23
  # Class-level parse method with preprocessing.
12
24
  # Delegates all string normalization to Nist::Preprocessor, then
13
- # feeds the cleaned string to the Parslet grammar and stamps the
25
+ # feeds the cleaned string to the parg backend and stamps the
14
26
  # detected format onto the parse tree.
15
27
  def self.class_parse_with_preprocessing(input)
16
28
  # The shared Grammar strips a trailing "(all parts)", but the NIST
17
29
  # preprocessor runs first and would swallow it — strip before it and
18
30
  # carry the marker to the tree like Grammar#parse does.
19
- all_parts = input.is_a?(String) && input.match?(::Pubid::Parser::Grammar::ALL_PARTS_SUFFIX)
20
- input = input.sub(::Pubid::Parser::Grammar::ALL_PARTS_SUFFIX, "") if all_parts
31
+ all_parts = input.is_a?(String) && input.match?(::Pubid::Parser::ALL_PARTS_SUFFIX)
32
+ input = input.sub(::Pubid::Parser::ALL_PARTS_SUFFIX, "") if all_parts
21
33
  result = Preprocessor.new(input).call
22
- parsed = new.parse(result.cleaned)
34
+ parsed = normalize_bound(Pubid::Parg::Model.bound(:nist, result.cleaned))
23
35
  if all_parts
24
36
  parsed = case parsed
25
37
  when Hash then parsed.merge(all_parts: true)
@@ -41,774 +53,6 @@ module Pubid
41
53
  parsed
42
54
  end
43
55
  end
44
-
45
- # Basic building blocks
46
- rule(:space) { str(" ") }
47
- rule(:dot) { str(".") }
48
- rule(:dash) { str("-") }
49
- rule(:slash) { str("/") }
50
- rule(:digit) { match("[0-9]") }
51
- rule(:digits) { digit.repeat(1) }
52
- rule(:letter) { match("[A-Za-z]") }
53
- rule(:upper_letter) { match("[A-Z]") }
54
- rule(:lower_letter) { match("[a-z]") }
55
-
56
- # Hash prefix for machine-readable formats
57
- rule(:hash_prefix) { str("#") }
58
-
59
- # Month abbreviations
60
- rule(:month_abbrev) do
61
- str("January") | str("February") | str("March") | str("April") |
62
- str("May") | str("June") | str("July") | str("August") |
63
- str("September") | str("October") | str("November") | str("December") |
64
- str("Jan") | str("Feb") | str("Mar") | str("Apr") |
65
- str("Jun") | str("Jul") | str("Aug") | str("Sep") | str("Oct") | str("Nov") | str("Dec")
66
- end
67
-
68
- # Language codes for translations - 2-4 letter codes
69
- # Supports: " spa", "(spa)", ".spa" (MR format)
70
- rule(:language_code) do
71
- ((space | dot).maybe >> (str("es") | str("pt") | str("chi") | str("viet") | str("port") | str("esp") |
72
- match("[a-z]").repeat(2, 4))).as(:translation)
73
- end
74
-
75
- # Stage ID: i (initial), f (final), 1-9 (numbered iterations)
76
- rule(:stage_id) do
77
- str("i") | str("I") | str("f") | str("F") |
78
- str("1") | str("2") | str("3") | str("4") | str("5") |
79
- str("6") | str("7") | str("8") | str("9")
80
- end
81
-
82
- # Stage type: pd (public draft), wd (work-in-progress), prd (preliminary)
83
- rule(:stage_type) do
84
- str("pd") | str("PD") | str("wd") | str("WD") | str("prd") | str("PRD")
85
- end
86
-
87
- # Old style stage: (IPD), (FPD), (2PD) - parenthetical at document start
88
- rule(:old_stage) do
89
- str("(") >> (stage_id.as(:stage_id) >> stage_type.as(:stage_type)).as(:stage) >> str(")")
90
- end
91
-
92
- # New style stage: " ipd", ".ipd" - inline at document end
93
- rule(:new_stage) do
94
- (space | dot) >> (stage_id.as(:stage_id) >> stage_type.as(:stage_type)).as(:stage)
95
- end
96
-
97
- # Publisher
98
- rule(:publisher) do
99
- (str("NBS") | str("NIST")).as(:publisher)
100
- end
101
-
102
- # Compound series (include publisher in series name) - must be checked FIRST
103
- rule(:compound_series) do
104
- (
105
- # Longest patterns first to avoid partial matches
106
- str("NBS BRPD-CRPL-D") | str("NBS CRPL-F-A") | str("NBS CRPL-F-B") |
107
- str("NBS CS-E") | str("CSRC Building Block") | str("CSRC Use Case") | str("CSRC Book") |
108
- str("ITL Bulletin") | str("NSRDS-NBS") |
109
- # NBS and NIST specific patterns that conflict with simple series
110
- # CRITICAL: Put longer patterns before shorter to avoid partial matches!
111
- str("NIST LCIRC") | str("NBS LCIRC") | str("NIST.LCIRC") | str("NBS.LCIRC") | str("NBS RPT") |
112
- str("NIST PS") | str("NIST DCI") | str("NIST Other") |
113
- str("NISTPUB") |
114
- str("NBS CSM") | str("NBS CIRC") | str("NBS.CRPL") | str("NBS CRPL") | str("NBS CS") |
115
- str("NBS CIS") | str("NBS HR") | str("NBS IRPL") | str("NBS IP") | str("NBS PS") |
116
- str("NBS BH")
117
- ).as(:series)
118
- end
119
-
120
- # Simple series (no publisher prefix)
121
- rule(:simple_series) do
122
- (
123
- str("AMS") | str("VTS") | # NEW - Added for NIST AMS and VTS series
124
- str("BSS") | str("BMS") | str("BH") |
125
- str("FIPS") | str("GCR") | str("HB") | str("MONO") |
126
- str("MP") | str("NCSTAR") | str("NSRDS") | str("IR") |
127
- str("SP") | str("TN") | str("CSWP") |
128
- str("AI") | str("CIRC") | str("CS") | str("CSM") |
129
- str("CRPL") | str("LCIRC") | str("OWMWP") | str("PC") | str("RPT") |
130
- str("SIBS") | str("TIBM") | str("TTB") | str("EAB") |
131
- str("JPCRD") | str("JRES") |
132
- # NEW - CHIPS Act, NWIRP, and Research Brief series
133
- str("CHIPS") | str("NWIRP") | str("RB")
134
- ).as(:series)
135
- end
136
-
137
- # Suffix letter(s) after number - supports single letters and specific two-letter suffixes
138
- # Two-letter suffixes: Ur (Unclassified Revised), Ua (Unclassified Amended), Ub-Uj (series variants)
139
- # Single letter: any letter not followed by excluded keywords
140
- rule(:number_suffix) do
141
- (str("U") >> lower_letter) |
142
- # An "r" directly followed by a digit is the revision marker (pubid#170),
143
- # not a suffix — leave it for the second_number/edition rules.
144
- # The guard sits before the letter so it tests the suffix start itself.
145
- ((str("r") >> digit).absent? >> match("[a-zA-Z]") >>
146
- # An uppercase letter following the suffix letter is the part
147
- # type "n" + letter form ("1nA", pubid#555) — the suffix must
148
- # stop so the parts rule can take the whole token.
149
- match("[A-Z]").absent? >> (
150
- # Match suffixes
151
- str("ec") |
152
- str("ndex") |
153
- str("ndx") |
154
- str("nsert") |
155
- str("rrata") |
156
- str("raft") | # NEW: Exclude "draft" from number suffix matching
157
- str("pp") |
158
- str("s") |
159
- str("t") |
160
- str("hi") |
161
- str("iet") |
162
- str("ort") |
163
- str("r") | # NEW: Exclude "r" revision marker (e.g., r5, r1963)
164
- str("p") # NEW: Exclude "p" part marker (e.g., 28p11969 - part with year pattern)
165
- ).absent? >>
166
- digits.maybe)
167
- end
168
-
169
- rule(:digits_with_suffix) do
170
- digits >>
171
- # Suffix only if not followed by digit (e.g., don't match 'e' in '140e2')
172
- (number_suffix >> digit.absent?).maybe
173
- end
174
-
175
- # Report number - first part - support edition prefixes like "e104" and supplement suffixes like "144supp"
176
- # Supplements should be handled as separate parts
177
- rule(:first_number) do
178
- (
179
- # OWMWP date format: MM-DD-YYYY (e.g., 06-13-2018)
180
- # Must be FIRST to match before other dash patterns
181
- (match("[0-9]").repeat(2, 2).as(:owmwp_month) >> dash >>
182
- match("[0-9]").repeat(2, 2).as(:owmwp_day) >> dash >>
183
- match("[0-9]").repeat(4, 4).as(:owmwp_year)).as(:owmwp_date_number) |
184
- # Special text patterns - MOST SPECIFIC FIRST (NEW for RPT patterns)
185
- str("ADHOC") | (str("div") >> digits) |
186
- # Month ranges for RPT: Apr-Jun1948 (NEW)
187
- (month_abbrev >> dash >> month_abbrev >> digits) |
188
- # Number with volume suffix (e.g., "539v10" for CIRC, "1011v1" for general patterns)
189
- # CRITICAL: Must be before CS series pattern to avoid consuming "GB" as letter suffix
190
- (digits.as(:number) >> str("v") >> digits.as(:volume_suffix)).as(:number_with_volume) |
191
- # Roman numeral patterns: 1011-I-2.0, 1011-II-1.0 (ENHANCED to accept optional dots)
192
- (digits >> dash >> (str("III") | str("II") | str("IV") | str("I") | str("V") | str("VI") | str("VII") | str("VIII") | str("IX") | str("X")) >> dash >> digits >> (dot >> digits).maybe) |
193
- # GB series pattern: 1190GB-1, 1190GB-4A
194
- (digits >> str("GB") >> dash >> digits >> upper_letter.maybe) |
195
- # CS series pattern with letter in middle: 102E-42, 123A-50
196
- (digits >> upper_letter >> dash >> digits) |
197
- # Volume-number format for CSM series: v6n1, v7n12
198
- # CHANGED: Capture volume and issue_number separately for proper semantics
199
- (str("v") >> digits.as(:volume_number) >> str("n") >> digits.as(:issue_number)) |
200
- # Regular number with supplement and revision suffix: "154supprev"
201
- (digits >> str("supprev")) |
202
- # Regular number with edition and revision year-only: "13e2rev1908"
203
- (digits >> str("e") >> digits >> str("rev") >> digits) |
204
- # NEW: Number with revision year (rv pattern for LetterCircular): "1013rv1953"
205
- (digits.as(:number) >> str("rv") >> digits.as(:revision_year)).as(:number_with_rev_year) |
206
- # Regular number with edition, revision, and month-date: "13e2revJune1908"
207
- (digits >> str("e") >> digits >> str("rev") >> month_abbrev >> digits) |
208
- # Regular number with eN suffix and optional supplement (e.g., "101e2supp") - most specific
209
- (digits >> str("e") >> digits >> str("supp") >> digits.maybe) |
210
- # Edition prefix with revision and date: e2revJune1908
211
- (str("e") >> digits >> str("rev") >> month_abbrev >> digits) |
212
- # Edition prefix followed by digits and optional supplement with digits
213
- (str("e") >> digits >> str("supp") >> digits.maybe) |
214
- # Regular number with eN suffix (e.g., "101e2")
215
- (digits >> str("e") >> digits) |
216
- # NEW: Bare edition (just "e2" without number prefix)
217
- (str("e") >> digits >> (dash >> digits).absent?) |
218
- # Letter prefix with digits (e.g., "c4" for CRPL)
219
- (lower_letter >> digits) |
220
- # Regular number with supplement suffix with month/year (e.g., "24suppJan1924")
221
- (digits >> str("supp") >> month_abbrev >> digits) |
222
- # Regular number with supplement suffix (e.g., "144supp") - with optional digits
223
- (digits >> str("supp") >> digits.maybe) |
224
- # Regular number with supplement suffix followed by month/year for date range
225
- (digits >> str("sup") >> month_abbrev >> digits) |
226
- # Regular number with "sup" suffix (e.g., "9350sup") - NEW for RPT patterns
227
- (digits >> str("sup")) |
228
- # Language code suffix without separator (e.g., "1088sp")
229
- # Must come BEFORE general suffix pattern to capture specific language codes
230
- # Must come AFTER other patterns (like sup, supp, etc.) to avoid consuming them
231
- # Note: Preprocessing doesn't convert attached suffixes, so we handle both cases
232
- (digits.as(:number) >> (str("sp") | str("pt") | str("es") | str("SP") | str("PT") | str("ES")).as(:language_code) >> (upper_letter.absent? >> digit.absent? >> letter.absent? >> dash.absent? >> dot.absent?)) |
233
- # Part+edition suffix for MR format: "28pt1e1969" (part notation + edition year)
234
- # Handles patterns like "NBS.HB.28pt1e1969" where part and edition are attached
235
- # Must come BEFORE language code pattern to take priority
236
- (digits.as(:number) >> str("pt") >> digits.as(:part_number) >> str("e") >> digits.as(:edition_year)) |
237
- # Parenthetical language code (e.g., "378(sp)")
238
- # Must come AFTER other patterns to avoid consuming letter suffixes
239
- # Note: Preprocessing converts content inside parentheses to uppercase
240
- # Use specific patterns to avoid consuming other parenthetical content
241
- (digits.as(:number) >> str("(") >> (str("SP") | str("PT") | str("ES")).as(:language_code) >> str(")")) |
242
- # Number with letter suffix followed by revision (e.g., "8278Ar1", "256Ar1930")
243
- # CRITICAL: Must come BEFORE digits_with_suffix because number_suffix's
244
- # str("r").absent? guard rejects any letter followed by 'r' (would
245
- # otherwise drop the letter and parse "256" + "Ar1930" as garbage).
246
- (digits.as(:number) >>
247
- upper_letter.as(:letter_suffix) >>
248
- str("r") >>
249
- digits.as(:revision_id)).as(:number_with_letter_revision) |
250
- # Regular number with optional suffix (original) - includes letters like "A"
251
- digits_with_suffix
252
- ).as(:first_number)
253
- end
254
-
255
- # Second number (after dash) - allow pt suffix, letter suffixes, and CRPL patterns
256
- rule(:second_number) do
257
- # Explicitly exclude month abbreviations at start (so -Feb1985 goes to edition, not second_number)
258
- month_abbrev.absent? >>
259
- # NEW: Exclude "draft" keyword
260
- str("draft").absent? >>
261
- (
262
- # Trailing bare supplement marker on a compound second number
263
- # (e.g. "800-53sup") so it isn't split into "53s" + "up". Builder
264
- # strips the marker and sets supplement="" (canonical "sup").
265
- (digits >> (str("supp") | str("sup")) >>
266
- (digit.absent? >> letter.absent?)) |
267
- # NEW: Revision pattern with U+letter suffix (e.g., "22r1Ua", "38Ua")
268
- # MUST come BEFORE general letter suffix to avoid matching just "U" from "Ua"
269
- (digits >> str("r") >> digits >> str("U") >> lower_letter) |
270
- # NEW: Revision pattern with letter suffix (e.g., "22r1a", "22r1A" for SP patterns)
271
- # This allows second_number to match the entire "22r1A" as a single unit
272
- # MUST come BEFORE plain r+digits to avoid greedy match of just "22r1"
273
- (digits >> str("r") >> digits >> match("[a-zA-Z]")) |
274
- # NEW: Revision pattern with year (e.g., "126r2013")
275
- # This handles SP revision format where revision is attached to second_number
276
- # Dotted minor accepted atomically (pubid#170): "53r4.1" binds id "4.1"
277
- (digits.as(:number_only) >> str("r") >>
278
- (digits >> (str(".") >> digits).maybe).as(:edition_id)) |
279
- # CRPL range with underscore (e.g., "2_3-1A")
280
- (digits >> str("_") >> digits >> dash >> digits >> upper_letter.maybe) |
281
- # Letter followed by dash and digits (e.g., "m-5")
282
- (lower_letter >> dash >> digits) |
283
- # Number with pt suffix (e.g., "57pt1")
284
- # EXCLUDE pt#-# patterns (e.g., "pt3-1") which are part components for CRPL
285
- # Use negative lookahead to prevent matching when followed by dash
286
- (digits >> str("pt") >> digits >> dash.absent?) |
287
- # Number with uppercase letter suffix (e.g., "56A", "123B") - for patterns like "56Ar2"
288
- (digits >> upper_letter) |
289
- # NEW: Revision pattern where r is directly followed by a letter (e.g., "27ra" -> rA)
290
- # For patterns like NIST SP 800-27ra where revision 'ra' is attached directly to number
291
- (digits.as(:number_only) >> str("r") >> match("[a-zA-Z]").as(:letter)).as(:revision_letter) |
292
- # NEW: Revision pattern where r is directly followed by a letter without leading digits (e.g., "rA")
293
- # For patterns like NIST SP 800-27ra where revision 'ra' is attached directly to number
294
- (str("r") >> match("[a-zA-Z]")).as(:revision_letter_suffix) |
295
- # NEW: Simple revision pattern r followed by digits (e.g., "r1", "r2") for trailing revision
296
- # Dotted minor accepted atomically (pubid#170): "r4.1" binds id "4.1"
297
- (str("r") >>
298
- (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:revision_simple) |
299
- # Special patterns like "NCNR", "PERMIS", "BFRL"
300
- str("NCNR") | str("PERMIS") | str("BFRL") |
301
- # Just capital letters (e.g., "A", "B", "C") - standalone;
302
- # a trailing lowercase means a capitalized word ("Suppl"),
303
- # not a second number.
304
- upper_letter.repeat(1, 3) >> lower_letter.absent? |
305
- # Regular number with optional suffix - but NOT if part of FIPS date (digit-dash-month-digit-slash)
306
- (digits_with_suffix >> (dash >> month_abbrev >> digits >> slash).absent?) |
307
- # Single lowercase letter (e.g., "a", "b") - but NOT "r" followed by digits (edition marker)
308
- # This is for patterns like "126a" but not "126r2"
309
- (lower_letter >> digit.absent?)
310
- ).as(:second_number)
311
- end
312
-
313
- # Edition component per NIST spec: <edition-type><edition-id>
314
- # Type: "e" (edition), "r" (revision), "rev" (revision verbose), "-" (historical)
315
- # ID: number (1-9) or year (yyyy)
316
- # Examples: e2, e2021, r5, rev2013, rev 2013, -3
317
- # Enhanced: Support space-separated format from preprocessing (r1 separated from number)
318
- rule(:edition) do
319
- # Edition with "e" prefix: e2, e3, e2021 (1-4 digits for ID)
320
- # Dotted minor accepted atomically (pubid#170): "e2006.1" binds id "2006.1"
321
- (space.maybe >> str("e") >>
322
- (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_e) |
323
- # Revision with "r" prefix and SPACE, with letter: r 5A (preserve format)
324
- # Dotted minor accepted atomically (pubid#170)
325
- (space >> str("r") >> (digits >> (str(".") >> digits).maybe).as(:edition_id) >>
326
- match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_with_space_letter) |
327
- # Revision with "r" prefix and SPACE: r 5 (preserve format)
328
- (space >> str("r") >>
329
- (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_r_with_space) |
330
- # Revision with "r" prefix NO space, with letter: r5A (compact format)
331
- (str("r") >> (digits >> (str(".") >> digits).maybe).as(:edition_id) >>
332
- match("[a-zA-Z]").as(:edition_letter)).as(:edition_r_no_space_letter) |
333
- # Revision with "r" prefix NO space: r5 (compact format)
334
- (str("r") >>
335
- (digits >> (str(".") >> digits).maybe).as(:edition_id)).as(:edition_r_no_space) |
336
- # Revision with "rev" prefix (verbose): rev2013, rev 2013
337
- (space.maybe >> str("rev") >> space.maybe >> digits.as(:edition_id)).as(:edition_rev) |
338
- # Historical with "-" prefix: -2, -3 (ONLY if followed by non-digit or end)
339
- # This avoids consuming date patterns like "-1908"
340
- # Historical precedent uses small numbers (1-9), dates use 4-digit years
341
- (dash >> match("[1-9]").as(:edition_id) >> digit.absent?).as(:edition_historical) |
342
- # Edition dash-year pattern: -1979, -1990 (dash + 4-digit year)
343
- # This matches year-only editions like "NBS HB 130-1979"
344
- (dash >> match("[0-9]").repeat(4,
345
- 4).as(:dash_year)).as(:edition_dash_year)
346
- end
347
-
348
- # Date component per NIST spec: -{YYYY} or -{YYYYMM} or -{YYYYMMDD}
349
- # Separate from Edition - both can coexist
350
- # Examples: -1908, -190806, -19770930
351
- rule(:date) do
352
- (
353
- # Date with month and day: -19770930 (YYYYMMDD)
354
- (dash >> match("[0-9]").repeat(4, 4).as(:date_year) >>
355
- match("[0-9]").repeat(2, 2).as(:date_month) >>
356
- match("[0-9]").repeat(2, 2).as(:date_day)) |
357
- # Date with month: -190806 (YYYYMM)
358
- (dash >> match("[0-9]").repeat(4, 4).as(:date_year) >>
359
- match("[0-9]").repeat(2, 2).as(:date_month)) |
360
- # Date with year only: -1908 (YYYY)
361
- (dash >> match("[0-9]").repeat(4, 4).as(:date_year)) |
362
- # Legacy month format: -June1908, -Jan1925 (normalize to YYYYMM)
363
- (dash >> month_abbrev.as(:date_month) >> digits.as(:date_year))
364
- ).as(:date)
365
- end
366
-
367
- # ISO date token (YYYY-MM-DD) for date-style identifiers
368
- rule(:iso_date) do
369
- (match("[0-9]").repeat(4, 4).as(:date_year) >> dash >>
370
- match("[0-9]").repeat(2, 2).as(:date_month) >> dash >>
371
- match("[0-9]").repeat(2, 2).as(:date_day))
372
- end
373
-
374
- # Date-style identifier with no series (e.g. "NIST 2022-04-15 001",
375
- # "NIST.2022-04-15.001" — DOI 10.6028/NIST.2022-04-15.001)
376
- rule(:dated_identifier) do
377
- hash_prefix.maybe >>
378
- publisher >> (space | dot) >>
379
- iso_date.as(:dated_date) >> (space | dot) >>
380
- match("[0-9]").repeat(1).as(:dated_seq)
381
- end
382
-
383
- # "NIST Research Library (YYYY)" — one-off NIST publication that doesn't
384
- # fit the standard series+number shape (no report number, year in parens).
385
- # See pubid/pubid#151.
386
- rule(:research_library_identifier) do
387
- publisher >> space >>
388
- str("Research Library").as(:series) >>
389
- (space >> str("(") >>
390
- match("[0-9]").repeat(4, 4).as(:year) >> str(")")).maybe
391
- end
392
-
393
- # LEGACY EDITION PATTERNS (for backward compatibility during migration)
394
- # These will be gradually replaced as we migrate to proper Edition/Date components
395
- rule(:legacy_edition) do
396
- # Complex revision patterns: r1a, r2b
397
- ((str("r") | str(" R")) >> match("[0-9]").repeat(1,
398
- 2).as(:edition) >> lower_letter.as(:edition_letter)) |
399
- # Edition with revision and year: rev2013, rev2020, rev 2013 (with space)
400
- (str("rev") >> space.maybe >> digits.as(:edition_year)) |
401
- # Edition with revision and date: e2revJune1908 (will migrate to e2 + date)
402
- ((str("e") | str(" E")) >> match("[0-9]").repeat(1, 3).as(:edition) >>
403
- str("rev") >> match("[A-Za-z]").repeat(3,
404
- 9).as(:edition_month) >> digits.as(:edition_year)) |
405
- # Edition with year and month: e201801 (ambiguous - could be e2018 or year 2018 month 01)
406
- (str("e") >> match("[0-9]").repeat(4,
407
- 4).as(:edition_year) >> match("[0-9]").repeat(
408
- 2, 2
409
- ).as(:edition_month).maybe) |
410
- # Revision-based edition: revJune1908, revJan1925 (normalize to date)
411
- (str("rev") >> match("[A-Za-z]").repeat(3,
412
- 9).as(:edition_month) >> digits.as(:edition_year))
413
- end
414
-
415
- # CRPL range pattern (e.g., 1-2_3-1, 1-2_3-1A with suffix) - matches after first dash
416
- rule(:crpl_range) do
417
- (digits >> str("_") >> digits >> dash >> digits >> upper_letter.maybe).as(:crpl_range)
418
- end
419
-
420
- # Full report number - support dot-separated parts AND CRPL ranges
421
- # ENHANCED: Support multiple dashes for GCR patterns (Session 220)
422
- # FIXED: Put GCR pattern first to prioritize matching full dash-separated patterns
423
- # FIXED: Add edition.maybe to support revision patterns like 800-53r5 in short format
424
- # FIXED: Month abbreviation as edition (e.g., 107-Mar1985, 11-Jan1925)
425
- # FIXED: FIPS date format with day and slash (e.g., 11-1-Sep30/1977)
426
- rule(:report_number) do
427
- first_number >>
428
- (
429
- # Underscore edition-year (space-form mirror of mr_identifier): "1648_2009"
430
- (str("_") >> digits.as(:edition_year)) |
431
- # Month abbreviation as edition (e.g., 107-Mar1985, 11-Jan1925)
432
- # MUST BE FIRST to catch -MonthYear patterns before they're
433
- # incorrectly parsed as other alternatives
434
- (dash >> month_abbrev.as(:edition_month) >> digits.as(:edition_year)) |
435
- # FIPS date format: -1-Sep30/1977 (part-month-day/year with slash)
436
- # Must come before GCR pattern to avoid being matched as multi-dash
437
- (dash >> digits.as(:fips_part) >> dash >> month_abbrev.as(:edition_month) >>
438
- digits.as(:edition_day) >> slash >> digits.as(:edition_year)) |
439
- # Dash with decimal suffix (e.g., 80-2073.3, 123-45.67)
440
- # Must come before GCR pattern which expects another dash after second_number
441
- (dash >> digits.as(:decimal_base) >> dot >> digits.as(:decimal_suffix)).as(:decimal_number) |
442
- # Dash with letter suffix (e.g., 1-1A, 1-3B for NCSTAR, 73-197Ur for IR)
443
- # Must come before GCR pattern which expects another dash
444
- # Supports U+lowercase letter suffix (e.g., Ur, Ua, Ub-Uj for Unclassified variants)
445
- # Supports n+UPPERCASE annex suffix (e.g., 1-1nA, 1-1nB — the BH/HR/NCSTAR
446
- # annex designations, pubid#555): the n prefix lowercases to the same
447
- # letter_suffix face as the plain letter
448
- # For other uppercase letters, only match single letter (A, B, C) to avoid consuming revision r
449
- (dash >> digits.as(:letter_base) >> (
450
- (str("U") >> lower_letter.as(:letter_suffix_extra)) |
451
- (str("n") >> upper_letter.as(:letter_suffix_extra)) |
452
- upper_letter
453
- ).as(:letter_suffix)).as(:letter_number) |
454
- # Dash-SEPARATED letter suffix on a numbered part (e.g., 21-4-B,
455
- # 173-1-B for FIPS, and the reparseable form of letter_number's own
456
- # rendered output 200-30-B). The month_abbrev guard keeps FIPS date
457
- # forms like 11-1-Sep1977 with the earlier patterns; the trailing
458
- # guard restricts to a single clean letter so GCR -200-30B is unaffected.
459
- (dash >> digits.as(:letter_base) >> dash >> month_abbrev.absent? >>
460
- upper_letter.as(:letter_suffix) >> (letter | digit).absent?).as(:letter_number) |
461
- # Edition dash-year pattern (e.g., -1979 for handbooks like "NBS HB 130-1979")
462
- # Matches any 4-digit sequence - the builder decides if it's a year or second_number
463
- (dash >> match("[0-9]").repeat(4,
464
- 4).as(:dash_year) >> (space | dot | part | crpl_range | second_number | dash).absent?).as(:edition_dash_year) |
465
- # Second number followed by edition dash-year (e.g., -1-1990 for "105-1-1990")
466
- # Handles compound numbers with edition year at the end
467
- # MUST be BEFORE GCR pattern because both start with dash + second_number + dash
468
- (dash >> second_number >> dash >> match("[0-9]").repeat(4,
469
- 4).as(:dash_year) >> (space | dot | part | crpl_range | revision | draft).absent?).as(:second_number_edition_year) |
470
- # FIPS month+year pattern after part (e.g., -1-Sep1977 for "11-1-Sep1977")
471
- (dash >> second_number >> dash >> month_abbrev.as(:edition_month) >> digits.as(:edition_year) >> (space | dot | part | crpl_range | edition | revision | draft).absent?).as(:fips_month_year_after_part) |
472
- # GCR multi-dash pattern (e.g., 85-3273-37, 19-200-30B)
473
- (dash >> second_number >> dash >> (digits >> upper_letter.maybe).as(:part_number)) |
474
- # Dot-separated part (e.g., 984.4 = number 984, part 4)
475
- (dot >> second_number) |
476
- # Dash-separated with optional revision (e.g., 800-53r5, 1019r1963)
477
- (dash >> (crpl_range | second_number) >> edition.maybe) |
478
- (dash >> edition)
479
- # TODO: Language code suffix without separator (e.g., "1088sp")
480
- # Must come AFTER other patterns to avoid consuming them
481
- # | (str("sp") | str("pt") | str("es") | match("[a-z]").repeat(2, 4)).as(:language_code) >> (space | dot | part | crpl_range | second_number).absent?) |
482
- # Parenthetical language code (e.g., "378(sp)")
483
- # | (str("(") >> match("[a-z]").repeat(2, 4).as(:language_code) >> str(")") >> (space | dot | part | crpl_range | second_number).absent?)
484
- ).maybe
485
- end
486
-
487
- # Volume
488
- rule(:volume) do
489
- # Long-form "Vol." comes FIRST so PEG ordered choice doesn't commit to
490
- # the bare "v" alternative after consuming the leading space via
491
- # space.maybe (which would then starve the " Vol. " form). Both
492
- # "Vol. 4" (period + space) and "Vol.4" (period only) are accepted.
493
- (space.maybe >>
494
- (str("Vol. ") | str("Vol.") | str("v"))) >>
495
- (digits >>
496
- # Support letter ranges (lowercase normalized in preprocessing)
497
- (str("a-l") | str("m-z") | str("A-L") | str("M-Z")).maybe >>
498
- # Support single uppercase letters (e.g., v3B, v1A)
499
- upper_letter.repeat(0, 2)).as(:volume)
500
- end
501
-
502
- # Part - enhanced to support patterns like p1adde1 AND pt3r1 (part with revision)
503
- rule(:part) do
504
- (str(" Part. ") | str(" Part.") | str(" Part ") |
505
- (space.maybe >> (str("pt") | str("p") | str("P")))) >>
506
- (digits >>
507
- # NEW: Revision after part number: pt3r1, p1r1 (space.maybe for preprocessing)
508
- (space.maybe >> str("r") >> digits).maybe >>
509
- # Existing: Addendum with optional edition: add, adde1
510
- (str("add") >> (str("e") >> digits).maybe).maybe >>
511
- (dash >> digits).maybe).as(:part)
512
- end
513
-
514
- # Revision
515
- rule(:revision) do
516
- # NEW: Revision with month and year: rJun1992, r Jun1992 - LONGEST MATCH FIRST
517
- # Enhanced to support leading space (Session 219)
518
- (space.maybe >> (str("r") | str("rev")) >> space.maybe >> month_abbrev.as(:revision_month) >> digits.as(:revision_year)) |
519
- # Revision with slash and year: r6/1925, r11/1924 (NEW for LCIRC patterns)
520
- (space.maybe >> (str("r") | str("rev")) >> digits.as(:revision) >>
521
- slash >> digits.as(:revision_year)) |
522
- # Revision with 4-digit year directly: r1995, r 1995 (allow space before year)
523
- ((str(" r") | str("r")) >> space.maybe >> match("[0-9]").repeat(4,
524
- 4).as(:revision_year)) |
525
- # Revision with year: rev2013, rev 2013 (allow space before year)
526
- (str("rev") >> space.maybe >> digits.as(:revision_year)) |
527
- # Revision with digits AND/OR letters: r1a, r1A, ra, r1
528
- # Enhanced to accept letter-only revisions and space before r
529
- # ENHANCED: Accept BOTH lowercase and uppercase letters in suffix
530
- # ENHANCED: Capture original format prefix for format preservation (e.g., " Rev. 5")
531
- ((str(" rev ") | str("rev") | str(" r") | str("r") | str(" Rev. ") | str(" Rev.") | str(" Revision (r)")).as(:revision_prefix) >>
532
- space.maybe >>
533
- ((digits >> match("[a-zA-Z]").maybe) | match("[a-zA-Z]").repeat(1)).as(:revision_id)).as(:revision) |
534
- # NEW: Standalone 'r' - MUST BE LAST to avoid consuming from other patterns
535
- # Matches " r" at end of input (after preprocessing: "800-56a r", "800-27 r")
536
- (str(" r") >> any.absent?).as(:revision_standalone)
537
- end
538
-
539
- # Version - V1 SP PARSER COMPATIBLE
540
- # Supports: ver1.0.2, ver2, " Ver. 2.0", " Version 1.0", v1.0.2, -v1.0, .ver1.0 (MR format)
541
- rule(:version) do
542
- # Verbose "ver" form - with or without dots (space.maybe before AND after "ver")
543
- # ENHANCED: Accept dot prefix for MR format (e.g., "500-281.ver1.0")
544
- ((space | dot).maybe >> str("ver") >> space.maybe >> (digits >> (dot >> digits).repeat).as(:version)) |
545
- # Verbose forms with space: " Ver. ", " Version " - require dots
546
- ((str(" Ver. ") | str(" Version ")) >>
547
- (digits >> dot >> digits >> (dot >> digits).maybe).as(:version)) |
548
- # Short form "v" with mandatory dots (v1.0, v1.0.2) - allow optional dash or space before
549
- ((dash | space).maybe >> str("v") >> (digits >> dot >> digits >> (dot >> digits).maybe).as(:version))
550
- end
551
-
552
- # Update - V1 COMPATIBLE
553
- # Format: /Upd{N}-{YYYY}{MM} where MM is optional
554
- # Examples: /Upd1-2015, /Upd3-202102, -upd, /upd (after preprocessing)
555
- # Update number is optional (e.g., "500-300-upd" has no number)
556
- # Captures prefix to preserve original format (-upd vs /Upd)
557
- rule(:update) do
558
- prefix = (
559
- str("/Upd") |
560
- (space.maybe >> (str("/upd") | str("-upd")))
561
- ).as(:update_prefix)
562
-
563
- prefix >>
564
- (
565
- digits.as(:update_number).maybe >>
566
- (dash >>
567
- (
568
- # Real updates: 4-digit year + optional 2-digit month
569
- # (e.g. /Upd1-2015, /Upd3-202102).
570
- (match("[0-9]").repeat(4, 4).as(:update_year) >>
571
- match("[0-9]").repeat(2, 2).as(:update_month)) |
572
- # Fallback: capture the whole digit run as the year. Handles the
573
- # unpadded CIRC/LCIRC supplement form pubid emits, where year and
574
- # revision are fused without a 2-digit month boundary
575
- # (e.g. /Upd1-19256 = 1925 + revision 6). Keeps these strings
576
- # re-parseable so generated ids round-trip.
577
- match("[0-9]").repeat(4).as(:update_year)
578
- )
579
- ).maybe
580
- ).as(:update)
581
- end
582
-
583
- # Addendum
584
- rule(:addendum) do
585
- ((str("-add") | str(".add") | str(" Add.")) >>
586
- (space | dash).maybe >> (digits | str("")).as(:addendum_number)).as(:addendum)
587
- end
588
-
589
- # The capitalized supplement words a letter slot must yield to.
590
- rule(:supplement_word) do
591
- str("Suppl") | str("Supp") | str("Sup")
592
- end
593
-
594
- # Supplement - enhanced to support date patterns, year patterns, and combined with revision
595
- # Examples: suppJan1924, supp3/1926, supp1925, supJun1925-Jun1927 (date ranges), supprev
596
- rule(:supplement) do
597
- space.maybe >>
598
- dot.maybe >>
599
- # The capitalized catalogue spellings ("NIST.SP.955.Suppl") ride
600
- # the same slot; longest-first so "Suppl" is not "Sup"+"p".
601
- (str("Suppl") | str("Supp") | str("Sup") |
602
- str("supp") | str("sup")) >>
603
- (
604
- # Supplement followed by revision: supprev
605
- str("rev").as(:supplement_with_rev) |
606
- # Date range pattern: Jan1924-Jan1926
607
- (month_abbrev.as(:supp_month_start) >> digits.as(:supp_year_start) >>
608
- dash >> month_abbrev.as(:supp_month_end) >> digits.as(:supp_year_end)).as(:supplement_date_range) |
609
- # Month and year: Jan1924
610
- (month_abbrev.as(:supp_month) >> digits.as(:supp_year)).as(:supplement_date) |
611
- # Number with slash and year: 3/1926
612
- (digits.as(:supp_number) >> slash >> digits.as(:supp_year)).as(:supplement_slash_year) |
613
- # Just year: 1925
614
- digits.as(:supp_year) |
615
- # General suffix (other patterns)
616
- match("[A-Za-z0-9]").repeat(1).as(:supplement_suffix)
617
- ).maybe
618
- end
619
-
620
- # Errata
621
- rule(:errata) do
622
- (dash.maybe >> (str("errata") | str("err"))).as(:errata)
623
- end
624
-
625
- # Index
626
- rule(:index) do
627
- (str("index") | str("indx")).as(:index)
628
- end
629
-
630
- # Insert
631
- rule(:insert) do
632
- (str("insert") | str("ins")).as(:insert)
633
- end
634
-
635
- # Appendix
636
- rule(:appendix) do
637
- str("app").as(:appendix)
638
- end
639
-
640
- # Section - make digits optional for patterns like just "sec"
641
- rule(:section) do
642
- str("sec") >> digits.as(:section).maybe
643
- end
644
-
645
- # Translation (3-letter language code) - V1 COMPATIBLE
646
- # Supports: (spa), " spa", ".spa" (MR format)
647
- rule(:translation) do
648
- # Parenthetical format: (spa), (por), (ind)
649
- (str("(") >> match('\w').repeat(3, 3).as(:translation) >> str(")")) |
650
- # Space-prefix format: " spa"
651
- (space >> match('\w').repeat(3, 3).as(:translation)) |
652
- # Dot-prefix format: ".spa" (machine-readable), optional leading space: " .spa"
653
- (space.maybe >> dot >> match('\w').repeat(3, 3).as(:translation))
654
- end
655
-
656
- # Public draft suffix - for patterns like 2pd, 3pd
657
- rule(:pd_suffix) do
658
- (space >> digits >> str("pd")).as(:public_draft)
659
- end
660
-
661
- # Draft stage - enhanced to support suffix pattern and number after draft
662
- # ENHANCED: Accept optional space before dash to match after report_number
663
- rule(:draft) do
664
- ((space >> str("(Draft)")) |
665
- (space.maybe >> dash >> str("draft") >> ((space >> digits) | digits).maybe) | # Match " -draft 2" OR "-draft2"
666
- pd_suffix).as(:draft)
667
- end
668
-
669
- # Special date format with slash for FIPS (part of number, not edition)
670
- rule(:fips_date) do
671
- dash >> digits.as(:fips_part) >> dash >> month_abbrev.as(:fips_month) >>
672
- digits.as(:fips_day) >> slash >> digits.as(:fips_year)
673
- end
674
-
675
- # All possible parts (order matters!)
676
- rule(:parts) do
677
- # Put more specific patterns first
678
- # CRITICAL: new_stage BEFORE language_code to avoid "ipd" being treated as translation
679
- new_stage |
680
- # Part type "n" with a letter value — "NCSTAR 1nA" (pubid#555):
681
- # the letter part suffixes the corpus renders as n<A> (Part
682
- # type "n"); must precede the generic index/letter rules.
683
- (str("n") >> upper_letter).as(:n_letter_part) |
684
- section | index | insert | appendix | pd_suffix |
685
- edition | date | legacy_edition | revision |
686
- version | # MOVED BEFORE volume - try dotted versions (v1.1) before simple volumes (v1)
687
- volume | part | update | addendum |
688
- supplement | errata | language_code
689
- end
690
-
691
- # CIRC Supplement identifier - split into base + supplement
692
- # Examples:
693
- # - "NBS CIRC 101e2supp" → base="NBS CIRC 101e2", supplement
694
- # - "NBS CIRC 25supp-1924" → base="NBS CIRC 25", supplement_year="1924"
695
- # - "NBS CIRC 24suppJan1924" → base="NBS CIRC 24", supplement_edition="Jan1924"
696
- # - "NBS CIRC suppJun1925-Jun1926" → date range supplement (no base)
697
- # - "NBS LCIRC 378Gsup" → base="NBS LCIRC 378G", supplement (no metadata)
698
- # - "NBS.LCIRC.378sup1/1927" → dot-separated MR format (after preprocessing)
699
- # Dot-separated machine-readable format: NIST.SP.800-116 or #NIST.2024-01-15.123
700
- # Enhanced to support parts after number like NIST.SP.1011-I-2.0
701
- # Enhanced to support revision+update patterns like NIST.IR.8115r1-upd
702
- rule(:mr_identifier) do
703
- hash_prefix.maybe >>
704
- publisher >> dot >>
705
- # The catalogue also lists bare series identities with no
706
- # report number ("NBS.CIRC").
707
- simple_series >> dot.maybe >> report_number.maybe >>
708
- # Edition with underscore separator (MR format: 1648_2009)
709
- (str("_") >> digits.as(:edition_year)).maybe >>
710
- # Support letter suffix before update (e.g., 8286C-upd1) - Session 219;
711
- # a capitalized supplement word ("Suppl") is not a letter suffix.
712
- supplement_word.absent?.maybe >> upper_letter.maybe >>
713
- # Support revision component (r1, r5, etc.) before update
714
- edition.maybe >>
715
- update.maybe >>
716
- # Additional dot-separated parts (parts, version, volume, etc.)
717
- # MUST come before translation to avoid conflicting with language codes
718
- (dot >> (digits | (supplement_word.absent? >> upper_letter))).repeat(0, 3) >>
719
- # Language codes at end (.spa, .por, .ind)
720
- parts.repeat >> draft.maybe
721
- end
722
-
723
- # Main identifier structure
724
- # Try compound series first (longest match), then publisher + simple series
725
- rule(:identifier) do
726
- circ_supplement_identifier |
727
- dated_identifier |
728
- mr_identifier |
729
- research_library_identifier |
730
- (
731
- # Compound series (includes publisher in series name)
732
- compound_series >> (space | dot) >>
733
- old_stage.maybe >> # Old style stage after series
734
- report_number.maybe >> fips_date.maybe >> parts.repeat >> draft.maybe >> translation.maybe >> new_stage.maybe
735
- ) |
736
- (
737
- # Publisher + simple series - require space/dot between publisher and series
738
- publisher >> (space | dot) >>
739
- simple_series >>
740
- old_stage.maybe >> # Old style stage after series
741
- (space | dot) >>
742
- report_number.maybe >> fips_date.maybe >> parts.repeat >> draft.maybe >> translation.maybe >> new_stage.maybe
743
- ) |
744
- (
745
- # Simple series only (no publisher)
746
- simple_series >>
747
- old_stage.maybe >> # Old style stage after series
748
- (space | dot) >>
749
- report_number.maybe >> fips_date.maybe >> parts.repeat >> draft.maybe >> translation.maybe >> new_stage.maybe
750
- )
751
- end
752
-
753
- # CIRC Supplement identifier - split into base + supplement
754
- # Must be complete rule with all patterns
755
- rule(:circ_supplement_identifier) do
756
- (
757
- (str("NBS CIRC") | str("NBS LCIRC") | str("NBS.CIRC") | str("NBS.LCIRC")).as(:series) >>
758
- (space | dot)
759
- ).as(:circ_series) >>
760
- (
761
- # Date range supplement (no base number)
762
- (str("supp") >> month_abbrev.as(:supp_month_start) >> digits.as(:supp_year_start) >>
763
- dash >> month_abbrev.as(:supp_month_end) >> digits.as(:supp_year_end)).as(:supplement_date_range) |
764
- # Year-only date range supplement to the whole series, no months
765
- # (e.g. "NBS CIRC sup1925-1926" — pubid/pubid#152). Requires two
766
- # 4-digit years so it can't swallow "sup3/1926" or a base number.
767
- ((str("supp") | str("sup")) >> match("[0-9]").repeat(4, 4).as(:supp_year_start) >>
768
- dash >> match("[0-9]").repeat(4, 4).as(:supp_year_end)).as(:supplement_date_range) |
769
- # Bare supplement marker to the whole series, no base number
770
- # ("NBS.CIRC.sup" — testsuite#5 C4): the marker alone is the
771
- # supplement.
772
- ((str("supp") | str("sup")) >>
773
- (
774
- (month_abbrev >> digits).as(:supplement_month_year) |
775
- (digits.as(:supp_number) >> slash >> digits.as(:supp_year)).as(:supplement_slash_year) |
776
- str("").as(:supplement_empty)
777
- ).maybe) |
778
- # With base identifier + supplement
779
- (
780
- # Capture base portion (everything before "supp" or "sup" or slash+year)
781
- (
782
- # Number with edition: "101e2"
783
- (digits.as(:base_number) >> str("e") >> digits.as(:edition_number)) |
784
- # Number with revision (for supplement patterns): "145r11"
785
- (digits.as(:base_number) >> lower_letter.as(:revision_letter) >> digits.as(:revision_number)) |
786
- # Number with letter suffix: "378G"
787
- (digits.as(:base_number) >> upper_letter.as(:letter_suffix)) |
788
- # Just number: "25", "24"
789
- digits.as(:simple_number)
790
- ).as(:base_portion) >>
791
- # Supplement marker - support both "supp" and "sup", OR implicit supplement via slash+year
792
- (
793
- # Explicit supplement marker
794
- ((str("supp") | str("sup")) >>
795
- # Optional supplement metadata
796
- (
797
- (month_abbrev >> digits).as(:supplement_month_year) |
798
- # Dash + number + slash + year (e.g., supp-12/1926)
799
- (dash >> digits.as(:supp_number) >> slash >> digits.as(:supp_year)).as(:supplement_dash_slash_year) |
800
- (dash >> digits.as(:supplement_year)) |
801
- (digits.as(:supp_number) >> slash >> digits.as(:supp_year)).as(:supplement_slash_year) |
802
- str("").as(:supplement_empty)
803
- ).maybe) |
804
- # Implicit supplement via slash+year (e.g., "145r11/1925")
805
- (slash >> digits.as(:implicit_supplement_year)).as(:implicit_supplement)
806
- )
807
- )
808
- )
809
- end
810
-
811
- root(:identifier)
812
56
  end
813
57
  end
814
58
  end