pubid 2.0.0.pre.alpha.1 → 2.0.0.pre.alpha.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.
- checksums.yaml +4 -4
- data/data/nist/update_codes.yaml +2 -0
- data/lib/pubid/amca/identifier.rb +39 -0
- data/lib/pubid/ansi/identifier.rb +42 -0
- data/lib/pubid/api/identifier.rb +47 -0
- data/lib/pubid/ashrae/identifier.rb +39 -0
- data/lib/pubid/asme/identifier.rb +46 -0
- data/lib/pubid/astm/identifier.rb +77 -0
- data/lib/pubid/bsi/identifier.rb +60 -0
- data/lib/pubid/ccsds/identifier.rb +68 -0
- data/lib/pubid/ccsds/identifiers/base.rb +11 -0
- data/lib/pubid/ccsds/single_identifier.rb +4 -1
- data/lib/pubid/cen_cenelec/identifier.rb +37 -0
- data/lib/pubid/cie/identifier.rb +53 -0
- data/lib/pubid/components/factory.rb +50 -0
- data/lib/pubid/components/typed_stage.rb +4 -0
- data/lib/pubid/components.rb +1 -0
- data/lib/pubid/csa/identifier.rb +56 -0
- data/lib/pubid/etsi/identifier.rb +43 -0
- data/lib/pubid/identifier.rb +8 -1
- data/lib/pubid/idf/identifier.rb +60 -0
- data/lib/pubid/iec/builder.rb +2 -1
- data/lib/pubid/iec/components/code.rb +2 -1
- data/lib/pubid/iec/components/publisher.rb +2 -1
- data/lib/pubid/iec/identifier.rb +235 -0
- data/lib/pubid/iec/identifiers/base.rb +4 -0
- data/lib/pubid/iec/identifiers/consolidated_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/fragment_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/sheet_identifier.rb +0 -4
- data/lib/pubid/iec/identifiers/vap_identifier.rb +0 -4
- data/lib/pubid/iec/parser.rb +7 -2
- data/lib/pubid/iec/urn_generator.rb +57 -171
- data/lib/pubid/iec/urn_parser.rb +53 -252
- data/lib/pubid/ieee/identifier.rb +41 -0
- data/lib/pubid/iho/identifier.rb +42 -0
- data/lib/pubid/iho/identifiers/base.rb +1 -1
- data/lib/pubid/iho/identifiers/bibliographic.rb +0 -4
- data/lib/pubid/iho/identifiers/circular_letter.rb +0 -4
- data/lib/pubid/iho/identifiers/miscellaneous.rb +0 -4
- data/lib/pubid/iho/identifiers/publication.rb +0 -4
- data/lib/pubid/iho/identifiers/standard.rb +0 -4
- data/lib/pubid/iho/urn_generator.rb +1 -1
- data/lib/pubid/iso/builder.rb +5 -1
- data/lib/pubid/iso/identifier.rb +261 -0
- data/lib/pubid/iso/parser.rb +4 -2
- data/lib/pubid/iso/scheme.rb +6 -0
- data/lib/pubid/iso/single_identifier.rb +6 -3
- data/lib/pubid/iso/urn_generator.rb +17 -3
- data/lib/pubid/iso/urn_parser.rb +16 -2
- data/lib/pubid/itu/identifier.rb +87 -22
- data/lib/pubid/jcgm/identifier.rb +43 -0
- data/lib/pubid/jis/identifier.rb +43 -0
- data/lib/pubid/nist/builder.rb +174 -5
- data/lib/pubid/nist/components/edition.rb +16 -0
- data/lib/pubid/nist/components/supplement.rb +88 -21
- data/lib/pubid/nist/identifier.rb +62 -0
- data/lib/pubid/nist/identifiers/base.rb +103 -24
- data/lib/pubid/nist/identifiers/circular_supplement.rb +1 -1
- data/lib/pubid/nist/identifiers/crpl_report.rb +1 -4
- data/lib/pubid/nist/identifiers/federal_information_processing_standards.rb +10 -0
- data/lib/pubid/nist/identifiers/report.rb +1 -2
- data/lib/pubid/nist/parser.rb +36 -3
- data/lib/pubid/nist/supplement_identifier.rb +8 -24
- data/lib/pubid/nist/urn_generator.rb +14 -8
- data/lib/pubid/nist.rb +1 -0
- data/lib/pubid/oiml/identifier.rb +50 -0
- data/lib/pubid/plateau/identifier.rb +57 -0
- data/lib/pubid/plateau.rb +1 -0
- data/lib/pubid/renderers/base.rb +34 -0
- data/lib/pubid/renderers/directives_renderer.rb +13 -14
- data/lib/pubid/renderers/guide_renderer.rb +5 -1
- data/lib/pubid/renderers/human_readable.rb +20 -8
- data/lib/pubid/renderers/iwa_renderer.rb +5 -1
- data/lib/pubid/renderers/supplement_renderer.rb +4 -1
- data/lib/pubid/rendering/context.rb +5 -2
- data/lib/pubid/sae/identifier.rb +23 -0
- data/lib/pubid/scheme.rb +12 -0
- data/lib/pubid/version.rb +1 -1
- metadata +5 -2
|
@@ -50,10 +50,13 @@ module Pubid
|
|
|
50
50
|
attribute :update_year, :string
|
|
51
51
|
attribute :addendum, :string
|
|
52
52
|
attribute :addendum_number, :string
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
# Single source of truth for the supplement: a structured component with
|
|
54
|
+
# isolated parts (number / year / month / date-range / revision), so a
|
|
55
|
+
# supplement's year is queryable independently of its number. Presence
|
|
56
|
+
# (non-nil) means "is a supplement"; an all-empty component is a bare
|
|
57
|
+
# "sup" marker. Replaces the former flat :supplement string plus the
|
|
58
|
+
# separate date-range/has_revision fields.
|
|
59
|
+
attribute :supplement, Components::Supplement
|
|
57
60
|
attribute :errata, :string
|
|
58
61
|
attribute :index, :string
|
|
59
62
|
attribute :insert, :string
|
|
@@ -84,6 +87,90 @@ module Pubid
|
|
|
84
87
|
# See lib/pubid/nist/builder.rb lines 368-472 for compound number logic
|
|
85
88
|
end
|
|
86
89
|
|
|
90
|
+
# Attributes that are build artifacts or rendering aliases, not part
|
|
91
|
+
# of an identifier's logical identity. They diverge between equally-
|
|
92
|
+
# valid spellings of the same id (e.g. long "Rev. 1" vs short "r1"):
|
|
93
|
+
# - edition_component: redundant alias of :edition
|
|
94
|
+
# - first_number/second_number: decomposed parts of the canonical
|
|
95
|
+
# :number, retained from the parse for building
|
|
96
|
+
# - parsed_format: records the input format for round-trip rendering
|
|
97
|
+
EQUALITY_IGNORED_ATTRS = %i[
|
|
98
|
+
edition_component first_number second_number parsed_format
|
|
99
|
+
].freeze
|
|
100
|
+
|
|
101
|
+
# Logical identity comparison: equal when every attribute except the
|
|
102
|
+
# build artifacts/aliases above matches. (Edition#== already ignores
|
|
103
|
+
# its rendering-only original_prefix.)
|
|
104
|
+
def ==(other)
|
|
105
|
+
return false unless other.instance_of?(self.class)
|
|
106
|
+
|
|
107
|
+
self.class.attributes.each_key.all? do |name|
|
|
108
|
+
EQUALITY_IGNORED_ATTRS.include?(name) || send(name) == other.send(name)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
alias eql? ==
|
|
113
|
+
|
|
114
|
+
def hash
|
|
115
|
+
vals = self.class.attributes.each_key.reject do |name|
|
|
116
|
+
EQUALITY_IGNORED_ATTRS.include?(name)
|
|
117
|
+
end.map { |name| send(name) }
|
|
118
|
+
[self.class, *vals].hash
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Wildcard / partial-identifier match. Treats +self+ as a QUERY pattern
|
|
122
|
+
# and +candidate+ as a concrete document: every ID part SET on the query
|
|
123
|
+
# must equal the candidate's, while parts left unset (nil/empty) are
|
|
124
|
+
# wildcards that match any value. So a query carrying no edition and no
|
|
125
|
+
# supplement matches that document across ALL editions, years, and
|
|
126
|
+
# supplements — the basis for "select docs by ID parts".
|
|
127
|
+
#
|
|
128
|
+
# Asymmetric (unlike ==): "NBS CIRC 25".matches?("NBS CIRC 25sup1924")
|
|
129
|
+
# is true, but not the reverse. The candidate must be the same class or
|
|
130
|
+
# a subclass so series-level identity still holds.
|
|
131
|
+
def matches?(candidate)
|
|
132
|
+
return false unless candidate.is_a?(self.class)
|
|
133
|
+
|
|
134
|
+
self.class.attributes.each_key.all? do |name|
|
|
135
|
+
next true if EQUALITY_IGNORED_ATTRS.include?(name)
|
|
136
|
+
|
|
137
|
+
query_val = public_send(name)
|
|
138
|
+
next true if query_val.nil? ||
|
|
139
|
+
(query_val.respond_to?(:empty?) && query_val.empty?)
|
|
140
|
+
|
|
141
|
+
query_val == candidate.public_send(name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Return a copy with the named attributes nil'd. Overrides
|
|
146
|
+
# Pubid::Identifier#exclude because NIST's initialize is keyword-only
|
|
147
|
+
# (initialize(**attributes)) while the inherited exclude rebuilds via
|
|
148
|
+
# the positional self.class.new(attrs) form — passing a positional
|
|
149
|
+
# hash to a keyword-only initializer raises ArgumentError. Rebuild
|
|
150
|
+
# with the keyword splat instead.
|
|
151
|
+
def exclude(*args)
|
|
152
|
+
excluded_args = args.dup
|
|
153
|
+
excluded_args << :date if excluded_args.delete(:year)
|
|
154
|
+
|
|
155
|
+
attrs = self.class.attributes.each_with_object({}) do |(name, _), h|
|
|
156
|
+
h[name] = excluded_args.include?(name) ? nil : send(name)
|
|
157
|
+
end
|
|
158
|
+
self.class.new(**attrs)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Short-form supplement fragment ("sup", "sup1924", "supJan1924",
|
|
162
|
+
# "suprev", " supJun1925-Jun1926"), rendered from the structured
|
|
163
|
+
# component. A present-but-empty component is the bare "sup" marker; a
|
|
164
|
+
# number-less date range gets the leading space the number would have
|
|
165
|
+
# supplied. Shared by base and the per-series to_short_style overrides.
|
|
166
|
+
def supplement_short
|
|
167
|
+
return "" unless supplement
|
|
168
|
+
|
|
169
|
+
prefix = (supplement.range? && !number) ? " " : ""
|
|
170
|
+
rendered = supplement.to_s(:short)
|
|
171
|
+
prefix + (rendered.empty? ? "sup" : rendered)
|
|
172
|
+
end
|
|
173
|
+
|
|
87
174
|
# Compute revision from edition component for backward compatibility
|
|
88
175
|
# @return [String, nil] revision string (e.g., "r5") or nil
|
|
89
176
|
def revision
|
|
@@ -333,30 +420,22 @@ module Pubid
|
|
|
333
420
|
end
|
|
334
421
|
end
|
|
335
422
|
|
|
336
|
-
# V2: Use version_component if available, else use version string
|
|
423
|
+
# V2: Use version_component if available, else use version string.
|
|
424
|
+
# Attach directly (no leading space) to match edition rendering
|
|
425
|
+
# (e.g. "800-53r5"), so version reads "800-45ver2" not
|
|
426
|
+
# "800-45 ver2".
|
|
337
427
|
if version_component
|
|
338
|
-
result +=
|
|
428
|
+
result += version_component.to_s(:short)
|
|
339
429
|
elsif version
|
|
340
|
-
result += "
|
|
430
|
+
result += "ver#{version}"
|
|
341
431
|
end
|
|
342
432
|
|
|
343
|
-
# Add supplement
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# Smart dash logic:
|
|
350
|
-
# - If supplement starts with letter (month like "Jan1924"), NO dash
|
|
351
|
-
# - If supplement is digits only (year like "1924"), WITH dash
|
|
352
|
-
result += if supplement.match?(/^[A-Z]/)
|
|
353
|
-
"supp#{supplement}"
|
|
354
|
-
else
|
|
355
|
-
"supp-#{supplement}"
|
|
356
|
-
end
|
|
357
|
-
elsif supplement
|
|
358
|
-
result += "supp"
|
|
359
|
-
end
|
|
433
|
+
# Add supplement. NIST/NBS canonical short form is single-p "sup"
|
|
434
|
+
# with the suffix attached directly, no dash (relaton-data-nist
|
|
435
|
+
# uses "sup2", "sup1940", "supA"); date-range keeps its inner dash.
|
|
436
|
+
# Rendered from the structured component; a present-but-empty
|
|
437
|
+
# component is the bare "sup" marker.
|
|
438
|
+
result += supplement_short
|
|
360
439
|
|
|
361
440
|
# Add other attributes
|
|
362
441
|
result += errata.to_s if errata
|
|
@@ -38,7 +38,7 @@ module Pubid
|
|
|
38
38
|
def to_s(format = :short)
|
|
39
39
|
# Handle date range supplements (no base identifier)
|
|
40
40
|
if supplement_date_range_start && supplement_date_range_end
|
|
41
|
-
return "NBS CIRC
|
|
41
|
+
return "NBS CIRC sup#{supplement_date_range_start}-#{supplement_date_range_end}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Use parent's rendering for base + supplement
|
|
@@ -121,10 +121,7 @@ module Pubid
|
|
|
121
121
|
result += part.to_s if part
|
|
122
122
|
|
|
123
123
|
# Add supplement with "sup" prefix for CRPL identifiers
|
|
124
|
-
|
|
125
|
-
# Check if supplement already has "sup" prefix (for backward compatibility)
|
|
126
|
-
result += (supplement.start_with?("sup") ? supplement : "sup#{supplement}")
|
|
127
|
-
end
|
|
124
|
+
result += supplement_short
|
|
128
125
|
|
|
129
126
|
result += range_notation if range_notation
|
|
130
127
|
result
|
|
@@ -86,6 +86,16 @@ module Pubid
|
|
|
86
86
|
if edition
|
|
87
87
|
result += "#{edition.type}#{edition.id}"
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
# Mirror Base#to_short_style update rendering — FIPS overrides
|
|
91
|
+
# to_short_style entirely, so update (e.g. /Upd2) would otherwise
|
|
92
|
+
# be dropped.
|
|
93
|
+
if update_component
|
|
94
|
+
result += update_component.to_s(:short)
|
|
95
|
+
elsif update
|
|
96
|
+
result += "-upd#{update}"
|
|
97
|
+
end
|
|
98
|
+
|
|
89
99
|
result
|
|
90
100
|
end
|
|
91
101
|
end
|
|
@@ -52,8 +52,7 @@ module Pubid
|
|
|
52
52
|
def to_short_style
|
|
53
53
|
result = "#{default_publisher} #{series_code}"
|
|
54
54
|
result += " #{number}" if number
|
|
55
|
-
result +=
|
|
56
|
-
result += "sup" if supplement == ""
|
|
55
|
+
result += supplement_short
|
|
57
56
|
result
|
|
58
57
|
end
|
|
59
58
|
|
data/lib/pubid/nist/parser.rb
CHANGED
|
@@ -204,9 +204,12 @@ module Pubid
|
|
|
204
204
|
# FIXED: Pattern must start with "v" or digit to avoid matching "rev 2013" as "v" + " 2013"
|
|
205
205
|
# CRITICAL: Added word boundary \b to prevent matching "v" within "rev"
|
|
206
206
|
# CRITICAL FIX: Use \b to ensure match starts at word boundary
|
|
207
|
-
cleaned = cleaned.gsub(/(\b(?:v|\d)[v\d]*[-A-Z]*)\s+(\d+)\s+(\d+)/, '\1.\2.\3') # Three parts
|
|
208
|
-
# CRITICAL FIX: Use \b to ensure match starts at word boundary
|
|
209
|
-
|
|
207
|
+
cleaned = cleaned.gsub(/(\b(?:v|\d)[v\d]*[-A-Z]*)\s+(\d+)(?!(?i:pd|wd|prd)\b)\s+(\d+)(?!(?i:pd|wd|prd)\b)/, '\1.\2.\3') # Three parts
|
|
208
|
+
# CRITICAL FIX: Use \b to ensure match starts at word boundary.
|
|
209
|
+
# Negative lookahead: don't swallow the digit of a numeric draft
|
|
210
|
+
# stage ("189 2pd" must stay split, not become "189.2pd"); letter
|
|
211
|
+
# stages ("ipd") already don't match the trailing \d+.
|
|
212
|
+
cleaned = cleaned.gsub(/(\b(?:v|\d)[v\d]*)\s+(\d+)(?!(?i:pd|wd|prd)\b)/, '\1.\2') # Two parts
|
|
210
213
|
|
|
211
214
|
# Fix update patterns: ensure space before -upd or /upd (not just at end)
|
|
212
215
|
# Enhanced to handle optional digits after upd: -upd, -upd1, /upd, /upd1
|
|
@@ -240,6 +243,16 @@ module Pubid
|
|
|
240
243
|
# Normalize "sup" to "supp" for LCIRC patterns to match circ_supplement_identifier rule
|
|
241
244
|
cleaned = cleaned.gsub(/(\d+)sup(\d+\/\d{4})/, '\1supp\2') # 118sup12/1926 → 118supp12/1926
|
|
242
245
|
|
|
246
|
+
# Unify dashed/undashed year supplements: "supp-YYYY" → "suppYYYY".
|
|
247
|
+
# A bare dash before a 4-digit year is not semantic — "25supp-1924" and
|
|
248
|
+
# "25supp1924" denote the same publication (the genuine edition marker is
|
|
249
|
+
# explicit "e", e.g. "25suppe1924"). Collapsing the dash here gives both
|
|
250
|
+
# spellings ONE parse tree (the normal first_number path), so they build
|
|
251
|
+
# to an identical Circular with supplement=<year>, with equal ==/URN.
|
|
252
|
+
# Guard: 4 digits NOT followed by another digit or a slash, so the
|
|
253
|
+
# dash-slash form "supp-12/1926" (supplement_dash_slash_year) is untouched.
|
|
254
|
+
cleaned = cleaned.gsub(/(\d)(supp?)-(\d{4})(?![\d\/])/, '\1\2\3') # 25supp-1924 → 25supp1924
|
|
255
|
+
|
|
243
256
|
# REMOVED: Revision letter patterns that add space before revision with letter
|
|
244
257
|
# These conflicted with the fix at lines 131-142 which keeps "22r1a" together
|
|
245
258
|
# for second_number pattern matching. The comprehensive fix now handles:
|
|
@@ -361,6 +374,21 @@ module Pubid
|
|
|
361
374
|
# Fix verbose "Revision" format: " Revision (r)" → " r"
|
|
362
375
|
cleaned = cleaned.gsub(/\s+Revision\s+\(r\)/, " r")
|
|
363
376
|
|
|
377
|
+
# Fix verbose "Part N" → short "ptN": "800-57 Part 2 Rev. 1" →
|
|
378
|
+
# "800-57pt2 Rev. 1". The grammar already accepts short "ptN" (and
|
|
379
|
+
# "ptN Rev. M"); only the verbose spelling was unsupported. Attaches
|
|
380
|
+
# to the preceding number so the existing part rule applies.
|
|
381
|
+
cleaned = cleaned.gsub(/\s+Part\s+(\d+)/, 'pt\1')
|
|
382
|
+
|
|
383
|
+
# Normalize verbose addendum " Add"/" add" (with or without period)
|
|
384
|
+
# to the canonical " Add." the grammar accepts, and uppercase a
|
|
385
|
+
# doc-number letter that immediately precedes it ("800-38a Add" →
|
|
386
|
+
# "800-38A Add.") — NIST doc-number letters are canonically uppercase
|
|
387
|
+
# and the letter_number grammar rule only splits the uppercase form.
|
|
388
|
+
# Scoped to the addendum context so bare markers like "800-90r"
|
|
389
|
+
# (revision) are left untouched.
|
|
390
|
+
cleaned = cleaned.gsub(/(\d[a-z]?)\s+Add\b\.?/i) { "#{Regexp.last_match(1).upcase} Add." }
|
|
391
|
+
|
|
364
392
|
# Fix verbose "rev YYYY" format: "126 rev 2013" → "126r2013"
|
|
365
393
|
# Removes space between number and "rev", and converts to "r" prefix
|
|
366
394
|
# Handles patterns like "NIST SP 260-126 rev 2013" → "NIST SP 260-126r2013"
|
|
@@ -634,6 +662,11 @@ module Pubid
|
|
|
634
662
|
# NEW: Exclude "draft" keyword
|
|
635
663
|
str("draft").absent? >>
|
|
636
664
|
(
|
|
665
|
+
# Trailing bare supplement marker on a compound second number
|
|
666
|
+
# (e.g. "800-53sup") so it isn't split into "53s" + "up". Builder
|
|
667
|
+
# strips the marker and sets supplement="" (canonical "sup").
|
|
668
|
+
(digits >> (str("supp") | str("sup")) >>
|
|
669
|
+
(digit.absent? >> letter.absent?)) |
|
|
637
670
|
# NEW: Revision pattern with U+letter suffix (e.g., "22r1Ua", "38Ua")
|
|
638
671
|
# MUST come BEFORE general letter suffix to avoid matching just "U" from "Ua"
|
|
639
672
|
(digits >> str("r") >> digits >> str("U") >> lower_letter) |
|
|
@@ -28,7 +28,7 @@ module Pubid
|
|
|
28
28
|
def to_s(format = :short)
|
|
29
29
|
# Handle date range supplements (no base identifier)
|
|
30
30
|
if supplement_date_range_start && supplement_date_range_end
|
|
31
|
-
return "NBS CIRC
|
|
31
|
+
return "NBS CIRC sup#{supplement_date_range_start}-#{supplement_date_range_end}"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
return super unless base_identifier
|
|
@@ -37,34 +37,18 @@ module Pubid
|
|
|
37
37
|
|
|
38
38
|
# NEW: Handle update attribute (e.g., "Upd12-1926" for supplement patterns)
|
|
39
39
|
if update
|
|
40
|
-
#
|
|
41
|
-
#
|
|
40
|
+
# Implicit supplements (e.g. "145r11/1925") have no explicit marker;
|
|
41
|
+
# everything else uses the canonical single-p "sup" marker
|
|
42
|
+
# (relaton-data-nist uses "sup" across all series).
|
|
42
43
|
is_implicit = self.class.attributes.key?(:implicit_supplement) && implicit_supplement == true
|
|
43
|
-
|
|
44
|
-
if is_implicit
|
|
45
|
-
# Implicit supplement: "{base}/{update}" (e.g., "145/Upd1-192511")
|
|
46
|
-
else
|
|
47
|
-
# Explicit supplement: "{base}sup/{update}" (e.g., "118sup/Upd12-1926")
|
|
48
|
-
is_circ_supplement = ["LCIRC", "CIRC"].include?(series.to_s)
|
|
49
|
-
result += is_circ_supplement ? "sup" : "supp"
|
|
50
|
-
end
|
|
44
|
+
result += "sup" unless is_implicit
|
|
51
45
|
result += "/#{update}"
|
|
52
46
|
return result
|
|
53
47
|
end
|
|
54
48
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
is_circ_supplement = ["LCIRC", "CIRC"].include?(series.to_s)
|
|
59
|
-
# When update is present, use "sup" (e.g., "118sup/Upd1-192612")
|
|
60
|
-
# When update is not present, use "supp" (e.g., "378Gsupp" - normalized from "sup")
|
|
61
|
-
result += if is_circ_supplement && !update
|
|
62
|
-
"supp"
|
|
63
|
-
elsif is_circ_supplement
|
|
64
|
-
"sup"
|
|
65
|
-
else
|
|
66
|
-
"supp"
|
|
67
|
-
end
|
|
49
|
+
# Canonical supplement marker is single-p "sup" across all NIST/NBS
|
|
50
|
+
# series (relaton-data-nist: SP/CIRC/HB/RPT/LC/IR/MONO/BMS all use "sup").
|
|
51
|
+
result += "sup"
|
|
68
52
|
|
|
69
53
|
# Add edition information if present (just ID, not type prefix)
|
|
70
54
|
if edition&.id
|
|
@@ -61,18 +61,24 @@ module Pubid
|
|
|
61
61
|
identifier_parts << identifier.stage.to_s
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Supplement is now a structured component. Preserve the existing URN
|
|
65
|
+
# branching exactly: range → "supp{start}-{end}", revision → "supprev",
|
|
66
|
+
# a valued supplement → "suppX"/"supp-X" (dash unless it starts with an
|
|
67
|
+
# uppercase letter), and — quirk retained — a nil (absent) supplement
|
|
68
|
+
# still emits "supp", while a present-but-empty one emits nothing.
|
|
64
69
|
supp = identifier.supplement
|
|
65
|
-
if
|
|
66
|
-
identifier_parts << "supp#{
|
|
67
|
-
elsif
|
|
70
|
+
if supp&.range?
|
|
71
|
+
identifier_parts << "supp#{supp.month}#{supp.year}-#{supp.month_end}#{supp.year_end}"
|
|
72
|
+
elsif supp&.has_revision
|
|
68
73
|
identifier_parts << "supprev"
|
|
69
|
-
elsif supp && !supp.empty?
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
elsif supp && !supp.value_string.empty?
|
|
75
|
+
value = supp.value_string
|
|
76
|
+
identifier_parts << if value.match?(/^[A-Z]/)
|
|
77
|
+
"supp#{value}"
|
|
72
78
|
else
|
|
73
|
-
"supp-#{
|
|
79
|
+
"supp-#{value}"
|
|
74
80
|
end
|
|
75
|
-
elsif
|
|
81
|
+
elsif supp.nil?
|
|
76
82
|
identifier_parts << "supp"
|
|
77
83
|
end
|
|
78
84
|
|
data/lib/pubid/nist.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Pubid
|
|
|
5
5
|
autoload :Builder, "#{__dir__}/nist/builder"
|
|
6
6
|
autoload :Components, "#{__dir__}/nist/components"
|
|
7
7
|
autoload :Configuration, "#{__dir__}/nist/configuration"
|
|
8
|
+
autoload :Identifier, "#{__dir__}/nist/identifier"
|
|
8
9
|
autoload :Identifiers, "#{__dir__}/nist/identifiers"
|
|
9
10
|
autoload :Parser, "#{__dir__}/nist/parser"
|
|
10
11
|
autoload :Scheme, "#{__dir__}/nist/scheme"
|
|
@@ -6,6 +6,56 @@ module Pubid
|
|
|
6
6
|
def to_urn
|
|
7
7
|
UrnGenerator.new(self).generate
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
# Factory mirroring pubid 1.x's `Pubid::Oiml::Identifier.create` API.
|
|
11
|
+
# Default subclass is {Identifiers::Recommendation}.
|
|
12
|
+
TYPE_KEY_TO_KLASS = {
|
|
13
|
+
recommendation: "Recommendation",
|
|
14
|
+
document: "Document",
|
|
15
|
+
guide: "Guide",
|
|
16
|
+
vocabulary: "Vocabulary",
|
|
17
|
+
basic_publication: "BasicPublication",
|
|
18
|
+
expert_report: "ExpertReport",
|
|
19
|
+
seminar_report: "SeminarReport",
|
|
20
|
+
annex: "Annex",
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def self.create(type: nil, **opts)
|
|
24
|
+
klass = resolve_create_class(type)
|
|
25
|
+
klass.new(**coerce_create_attrs(opts))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.resolve_create_class(type)
|
|
29
|
+
return Identifiers::Recommendation if type.nil?
|
|
30
|
+
|
|
31
|
+
klass_name = TYPE_KEY_TO_KLASS[type.to_sym]
|
|
32
|
+
raise ArgumentError, "Unknown OIML type: #{type.inspect}" unless klass_name
|
|
33
|
+
|
|
34
|
+
Identifiers.const_get(klass_name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.coerce_create_attrs(opts)
|
|
38
|
+
attrs = { publisher: (opts[:publisher] || "OIML").to_s }
|
|
39
|
+
|
|
40
|
+
if opts[:code].is_a?(Pubid::Oiml::Components::Code)
|
|
41
|
+
attrs[:code] = opts[:code]
|
|
42
|
+
elsif opts[:code] || opts[:number]
|
|
43
|
+
attrs[:code] = Pubid::Oiml::Components::Code.new(
|
|
44
|
+
number: (opts[:number] || opts[:code])&.to_s,
|
|
45
|
+
part: opts[:part]&.to_s,
|
|
46
|
+
subpart: opts[:subpart]&.to_s,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if (v = opts[:year])
|
|
51
|
+
attrs[:date] = Pubid::Components::Date.new(year: v.to_s)
|
|
52
|
+
end
|
|
53
|
+
%i[stage iteration language edition].each do |k|
|
|
54
|
+
attrs[k] = opts[k].to_s unless opts[k].nil?
|
|
55
|
+
end
|
|
56
|
+
attrs
|
|
57
|
+
end
|
|
58
|
+
private_class_method :resolve_create_class, :coerce_create_attrs
|
|
9
59
|
end
|
|
10
60
|
end
|
|
11
61
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Plateau
|
|
5
|
+
# Plateau factory entry point. `.parse` lives on `Pubid::Plateau`
|
|
6
|
+
# itself for historical reasons; this module hosts `.create` for API
|
|
7
|
+
# consistency with the other pubid flavors.
|
|
8
|
+
module Identifier
|
|
9
|
+
# Delegate to the flavor module so callers can use
|
|
10
|
+
# `Pubid::Plateau::Identifier.parse` consistently with other flavors.
|
|
11
|
+
def self.parse(identifier)
|
|
12
|
+
Pubid::Plateau.parse(identifier)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Factory that builds a PLATEAU identifier from a hash of primitives.
|
|
16
|
+
#
|
|
17
|
+
# Dispatch on `:type`:
|
|
18
|
+
# * `:handbook` (default) → Identifiers::Handbook
|
|
19
|
+
# * `:technical_report` / `:tr` → Identifiers::TechnicalReport
|
|
20
|
+
# * `:annex` → Identifiers::Annex
|
|
21
|
+
#
|
|
22
|
+
# Attributes are plain integers/strings — no Component wrapping.
|
|
23
|
+
# `:publisher` is silently ignored (PLATEAU is hardcoded).
|
|
24
|
+
def self.create(type: nil, **opts)
|
|
25
|
+
klass = resolve_create_class(type)
|
|
26
|
+
klass.new(**coerce_create_attrs(opts, klass: klass))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.resolve_create_class(type)
|
|
30
|
+
case type&.to_sym
|
|
31
|
+
when nil, :handbook
|
|
32
|
+
Identifiers::Handbook
|
|
33
|
+
when :technical_report, :tr
|
|
34
|
+
Identifiers::TechnicalReport
|
|
35
|
+
when :annex
|
|
36
|
+
Identifiers::Annex
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, "Unknown PLATEAU type: #{type.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.coerce_create_attrs(opts, klass:)
|
|
43
|
+
attrs = {}
|
|
44
|
+
attrs[:number] = opts[:number].to_i if opts[:number]
|
|
45
|
+
attrs[:annex] = opts[:annex].to_i if opts[:annex]
|
|
46
|
+
# :edition exists only on Handbook (and possibly TechnicalReport);
|
|
47
|
+
# silently drop on Annex.
|
|
48
|
+
if opts[:edition] && klass.attributes.key?(:edition)
|
|
49
|
+
attrs[:edition] = opts[:edition].to_s
|
|
50
|
+
end
|
|
51
|
+
# TODO(create-shim): :publisher silently ignored (PLATEAU hardcoded).
|
|
52
|
+
attrs
|
|
53
|
+
end
|
|
54
|
+
private_class_method :resolve_create_class, :coerce_create_attrs
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/pubid/plateau.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "parslet"
|
|
|
5
5
|
module Pubid
|
|
6
6
|
module Plateau
|
|
7
7
|
autoload :Builder, "#{__dir__}/plateau/builder"
|
|
8
|
+
autoload :Identifier, "#{__dir__}/plateau/identifier"
|
|
8
9
|
autoload :Identifiers, "#{__dir__}/plateau/identifiers"
|
|
9
10
|
autoload :Parser, "#{__dir__}/plateau/parser"
|
|
10
11
|
autoload :Scheme, "#{__dir__}/plateau/scheme"
|
data/lib/pubid/renderers/base.rb
CHANGED
|
@@ -14,6 +14,40 @@ module Pubid
|
|
|
14
14
|
def self.render(identifier)
|
|
15
15
|
new(identifier).render
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# Partitions a value into (leading separators, core, trailing separators)
|
|
19
|
+
# so "- : / space , ." stay OUTSIDE the annotation span (v1 parity).
|
|
20
|
+
SEMANTIC_SPLIT = %r{\A([-:/ ,.]*)(.*?)([-:/ ,.]*)\z}m
|
|
21
|
+
|
|
22
|
+
# type_code (a string) → semantic CSS class for the typed-stage token.
|
|
23
|
+
# Anything not listed (and not the default "is") renders as "doctype".
|
|
24
|
+
TYPED_STAGE_CSS = {
|
|
25
|
+
"amd" => "amendment",
|
|
26
|
+
"cor" => "corrigendum",
|
|
27
|
+
"add" => "addendum",
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Wrap a rendered value in a <span class="css_class"> when annotation is
|
|
33
|
+
# enabled, keeping leading/trailing separator chars outside the span.
|
|
34
|
+
def annotate(value, css_class, annotated:)
|
|
35
|
+
str = value.to_s
|
|
36
|
+
return value unless annotated && css_class && !str.empty?
|
|
37
|
+
|
|
38
|
+
lead, core, trail = str.match(SEMANTIC_SPLIT).captures
|
|
39
|
+
return value if core.empty?
|
|
40
|
+
|
|
41
|
+
%(#{lead}<span class="#{css_class}">#{core}</span>#{trail})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Choose between "stage" and a type/supplement class for a typed stage.
|
|
45
|
+
def typed_stage_css(typed_stage)
|
|
46
|
+
code = typed_stage&.type_code.to_s
|
|
47
|
+
return "stage" if code.empty? || code == "is"
|
|
48
|
+
|
|
49
|
+
TYPED_STAGE_CSS[code] || "doctype"
|
|
50
|
+
end
|
|
17
51
|
end
|
|
18
52
|
end
|
|
19
53
|
end
|
|
@@ -27,18 +27,13 @@ module Pubid
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def render_publisher_portion(context)
|
|
30
|
-
|
|
30
|
+
ann = context.annotated
|
|
31
|
+
pub_str = annotate(@id.publisher.render(context:), "publisher",
|
|
32
|
+
annotated: ann) if @id.publisher
|
|
31
33
|
abbr = @id.typed_stage ? @id.typed_stage.abbreviation(format_long: false) : ""
|
|
34
|
+
abbr = annotate(abbr, typed_stage_css(@id.typed_stage), annotated: ann) unless abbr.empty?
|
|
32
35
|
subgroup_str = @id.subgroup.render(context:) if @id.subgroup
|
|
33
36
|
|
|
34
|
-
unless @id.publisher&.copublished?
|
|
35
|
-
return [
|
|
36
|
-
pub_str,
|
|
37
|
-
(subgroup_str ? " #{subgroup_str}" : ""),
|
|
38
|
-
(abbr.empty? ? "" : " #{abbr}"),
|
|
39
|
-
].join
|
|
40
|
-
end
|
|
41
|
-
|
|
42
37
|
[
|
|
43
38
|
pub_str,
|
|
44
39
|
(subgroup_str ? " #{subgroup_str}" : ""),
|
|
@@ -47,13 +42,17 @@ module Pubid
|
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
def render_number_portion(context)
|
|
45
|
+
ann = context.annotated
|
|
50
46
|
parts = []
|
|
51
|
-
parts << @id.number.render(context:)
|
|
52
|
-
|
|
53
|
-
parts << "
|
|
54
|
-
parts << "
|
|
47
|
+
parts << annotate(@id.number.render(context:), "docnumber",
|
|
48
|
+
annotated: ann) if @id.number
|
|
49
|
+
parts << " #{annotate(@id.part.render(context:), 'part', annotated: ann)}" if @id.part
|
|
50
|
+
parts << "-#{annotate(@id.subpart.render(context:), 'part', annotated: ann)}" if @id.subpart
|
|
51
|
+
if @id.stage_iteration
|
|
52
|
+
parts << ".#{annotate(@id.stage_iteration.render(context:), 'iteration', annotated: ann)}"
|
|
53
|
+
end
|
|
55
54
|
date_str = @id.date.render(context:) if @id.date && context.with_date
|
|
56
|
-
parts << ":#{date_str}" if date_str
|
|
55
|
+
parts << ":#{annotate(date_str, 'year', annotated: ann)}" if date_str
|
|
57
56
|
result = parts.join.strip
|
|
58
57
|
result.empty? ? nil : result
|
|
59
58
|
end
|
|
@@ -6,10 +6,14 @@ module Pubid
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def render_publisher_and_stage(context)
|
|
9
|
-
|
|
9
|
+
ann = context.annotated
|
|
10
|
+
pub_str = annotate(@id.publisher.render(context:), "publisher",
|
|
11
|
+
annotated: ann) if @id.publisher
|
|
10
12
|
stage_str = @id.typed_stage.render(context:) if @id.typed_stage
|
|
11
13
|
|
|
12
14
|
if stage_str && !stage_str.empty?
|
|
15
|
+
stage_str = annotate(stage_str, typed_stage_css(@id.typed_stage),
|
|
16
|
+
annotated: ann)
|
|
13
17
|
"#{pub_str} #{stage_str}"
|
|
14
18
|
else
|
|
15
19
|
pub_str
|
|
@@ -17,12 +17,16 @@ module Pubid
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def render_publisher_and_stage(context)
|
|
20
|
-
|
|
20
|
+
ann = context.annotated
|
|
21
|
+
pub_str = annotate(@id.publisher.render(context:), "publisher",
|
|
22
|
+
annotated: ann) if @id.publisher
|
|
21
23
|
stage_str = @id.typed_stage.render(context:) if @id.typed_stage
|
|
22
24
|
|
|
23
25
|
if stage_str && !stage_str.empty?
|
|
24
26
|
has_copub = @id.publisher&.copublished?
|
|
25
27
|
sep = has_copub ? " " : "/"
|
|
28
|
+
stage_str = annotate(stage_str, typed_stage_css(@id.typed_stage),
|
|
29
|
+
annotated: ann)
|
|
26
30
|
"#{pub_str}#{sep}#{stage_str}"
|
|
27
31
|
else
|
|
28
32
|
pub_str
|
|
@@ -30,18 +34,25 @@ module Pubid
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def render_number_portion(context)
|
|
37
|
+
ann = context.annotated
|
|
33
38
|
parts = []
|
|
34
|
-
parts << @id.number.render(context:)
|
|
35
|
-
|
|
36
|
-
parts << "-#{@id.
|
|
37
|
-
parts << "
|
|
39
|
+
parts << annotate(@id.number.render(context:), "docnumber",
|
|
40
|
+
annotated: ann) if @id.number
|
|
41
|
+
parts << "-#{annotate(@id.part.render(context:), 'part', annotated: ann)}" if @id.part
|
|
42
|
+
parts << "-#{annotate(@id.subpart.render(context:), 'part', annotated: ann)}" if @id.subpart
|
|
43
|
+
if @id.stage_iteration
|
|
44
|
+
parts << ".#{annotate(@id.stage_iteration.render(context:), 'iteration', annotated: ann)}"
|
|
45
|
+
end
|
|
38
46
|
date_str = @id.date.render(context:) if @id.date && context.with_date
|
|
39
|
-
parts << ":#{date_str}" if date_str
|
|
47
|
+
parts << ":#{annotate(date_str, 'year', annotated: ann)}" if date_str
|
|
40
48
|
parts.join
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
def render_edition_portion(context)
|
|
44
|
-
|
|
52
|
+
return unless @id.edition&.number
|
|
53
|
+
|
|
54
|
+
annotate(@id.edition.render(context:), "edition",
|
|
55
|
+
annotated: context.annotated)
|
|
45
56
|
end
|
|
46
57
|
|
|
47
58
|
def render_language_portion(context, with_edition: false)
|
|
@@ -49,7 +60,8 @@ module Pubid
|
|
|
49
60
|
|
|
50
61
|
use_single = with_edition ? false : context.with_language_code == :single
|
|
51
62
|
rendered = @id.languages.map do |l|
|
|
52
|
-
l.render(context:, lang_single: use_single)
|
|
63
|
+
annotate(l.render(context:, lang_single: use_single), "language",
|
|
64
|
+
annotated: context.annotated)
|
|
53
65
|
end
|
|
54
66
|
"(#{rendered.join(use_single ? '/' : ',')})"
|
|
55
67
|
end
|