pubid 2.0.0.pre.alpha.49 → 2.0.0.pre.alpha.50

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.
@@ -32,6 +32,15 @@ module Pubid
32
32
  return Identifiers::TcDocument
33
33
  end
34
34
 
35
+ # Bare TC reference (pubid-iso#285): "184/SC 4/WG 12 N10291" — the
36
+ # ISO/TC route without the ORIGINATOR prefix. tc_type is absent by
37
+ # construction; sc_type is the route's required segment and cannot
38
+ # appear on a standard identifier.
39
+ if parsed_hash[:sc_type] && parsed_hash[:tc_number]
40
+ parsed_hash[:tc_type] = "TC"
41
+ return Identifiers::TcDocument
42
+ end
43
+
35
44
  # An explicit document type (e.g. from a URN's `:tr:`/`:ts:` token) pins
36
45
  # the class regardless of the stage. The stage and type are separate URN
37
46
  # fields, but the stage is later folded into `:type_with_stage` (e.g.
@@ -29,7 +29,8 @@ module Pubid
29
29
  # TC documents must come first since they start with "ISO/"
30
30
  # and we need to distinguish them from regular ISO/IEC identifiers
31
31
  rule(:identifier) do
32
- nsb_iso_identifier |
32
+ bare_tc_document |
33
+ nsb_iso_identifier |
33
34
  tc_document |
34
35
  directives_identifiers |
35
36
  jcgm_identifier |
@@ -62,6 +63,18 @@ module Pubid
62
63
  (str(":") >> year_digits.as(:year)).maybe
63
64
  end
64
65
 
66
+ # Bare TC document reference (pubid-iso#285): "184/SC 4/WG 12 N10291"
67
+ # — the ISO/TC route without the ORIGINATOR prefix; the builder
68
+ # canonicalizes the publisher to ISO. The SC segment is required so
69
+ # bare numbers cannot leak into the route.
70
+ rule(:bare_tc_document) do
71
+ digits.as(:tc_number) >>
72
+ str("/") >> sc_type >> space >> digits.as(:sc_number) >>
73
+ (str("/") >> wg_type >> space >> digits.as(:wg_number)).maybe >>
74
+ space >> str("N") >> space? >> digits.as(:number) >>
75
+ (str(":") >> year_digits.as(:year)).maybe
76
+ end
77
+
65
78
  rule(:tc_subcommittee_part) do
66
79
  # Full path: TC/SC 4/WG 3
67
80
  (str("/") >> sc_type >> space >> digits.as(:sc_number) >> str("/") >>
@@ -104,6 +104,16 @@ module Pubid
104
104
  identifier.part = letter_suffix_part
105
105
  end
106
106
 
107
+ # Part type "n" with a letter value — "NCSTAR 1nA" (pubid#555):
108
+ # the corpus rows carry part {type: "n", value: <letter>}, which
109
+ # renders as "n<letter>".
110
+ if parsed_hash[:n_letter_part]
111
+ identifier.part = Components::Part.new(
112
+ type: "n",
113
+ value: parsed_hash[:n_letter_part].to_s.sub(/\An/, "").upcase,
114
+ )
115
+ end
116
+
107
117
  # NEW: If we extracted an Edition from edition_dash_year, assign it now
108
118
  if edition_from_dash_year
109
119
  identifier.edition = edition_from_dash_year
@@ -729,6 +729,12 @@ module Pubid
729
729
  str_value = value.to_s.strip
730
730
  return nil if str_value.empty?
731
731
 
732
+ # The index suffix parses from both spellings ("index", "indx");
733
+ # the component's value is the long form either way, so
734
+ # parse(render(x)) == x for the CRPL-F-B corpus rows that print
735
+ # "indx" (pubid#557).
736
+ str_value = "index" if type == :index
737
+
732
738
  # For volume, create Volume component from string value
733
739
  # This handles patterns like "v1" that come from parser as simple strings
734
740
  if type == :volume
@@ -139,10 +139,18 @@ module Pubid
139
139
  # Single letter: any letter not followed by excluded keywords
140
140
  rule(:number_suffix) do
141
141
  (str("U") >> lower_letter) |
142
+ # n+UPPERCASE annex suffix (e.g., 1nA, 85nB — the BH/HR/NCSTAR/SP
143
+ # annex designations, pubid#555). Before the general letter branch,
144
+ # which would consume the lone "n" and strand the capital letter.
145
+ (str("n") >> upper_letter) |
142
146
  # An "r" directly followed by a digit is the revision marker (pubid#170),
143
147
  # not a suffix — leave it for the second_number/edition rules.
144
148
  # The guard sits before the letter so it tests the suffix start itself.
145
- ((str("r") >> digit).absent? >> match("[a-zA-Z]") >> (
149
+ ((str("r") >> digit).absent? >> match("[a-zA-Z]") >>
150
+ # An uppercase letter following the suffix letter is the part
151
+ # type "n" + letter form ("1nA", pubid#555) — the suffix must
152
+ # stop so the parts rule can take the whole token.
153
+ match("[A-Z]").absent? >> (
146
154
  # Match suffixes
147
155
  str("ec") |
148
156
  str("ndex") |
@@ -438,9 +446,13 @@ module Pubid
438
446
  # Dash with letter suffix (e.g., 1-1A, 1-3B for NCSTAR, 73-197Ur for IR)
439
447
  # Must come before GCR pattern which expects another dash
440
448
  # Supports U+lowercase letter suffix (e.g., Ur, Ua, Ub-Uj for Unclassified variants)
449
+ # Supports n+UPPERCASE annex suffix (e.g., 1-1nA, 1-1nB — the BH/HR/NCSTAR
450
+ # annex designations, pubid#555): the n prefix lowercases to the same
451
+ # letter_suffix face as the plain letter
441
452
  # For other uppercase letters, only match single letter (A, B, C) to avoid consuming revision r
442
453
  (dash >> digits.as(:letter_base) >> (
443
454
  (str("U") >> lower_letter.as(:letter_suffix_extra)) |
455
+ (str("n") >> upper_letter.as(:letter_suffix_extra)) |
444
456
  upper_letter
445
457
  ).as(:letter_suffix)).as(:letter_number) |
446
458
  # Dash-SEPARATED letter suffix on a numbered part (e.g., 21-4-B,
@@ -669,6 +681,10 @@ module Pubid
669
681
  # Put more specific patterns first
670
682
  # CRITICAL: new_stage BEFORE language_code to avoid "ipd" being treated as translation
671
683
  new_stage |
684
+ # Part type "n" with a letter value — "NCSTAR 1nA" (pubid#555):
685
+ # the letter part suffixes the corpus renders as n<A> (Part
686
+ # type "n"); must precede the generic index/letter rules.
687
+ (str("n") >> upper_letter).as(:n_letter_part) |
672
688
  section | index | insert | appendix | pd_suffix |
673
689
  edition | date | legacy_edition | revision |
674
690
  version | # MOVED BEFORE volume - try dotted versions (v1.1) before simple volumes (v1)
@@ -16,6 +16,16 @@ module Pubid
16
16
  # Return a cast hash, or nil to skip assignment.
17
17
  # Default: split letter suffix into a Part component.
18
18
  def self.cast_letter_number(value, _parsed_hash)
19
+ # The n+UPPERCASE annex capture (pubid#555, e.g., 800-85nB) arrives
20
+ # as a nested letter_suffix_extra: it casts to the n-notation Part
21
+ # (Part.new(type: "n", value: "B") renders "nB").
22
+ if value[:letter_suffix].is_a?(Hash)
23
+ letter = value[:letter_suffix][:letter_suffix_extra].to_s
24
+ return nil if letter.empty?
25
+
26
+ return { part: Components::Part.new(type: "n", value: letter.upcase) }
27
+ end
28
+
19
29
  full = combine_letter_suffix(value)
20
30
  return nil if full.nil? || full.empty?
21
31
 
@@ -47,8 +57,12 @@ module Pubid
47
57
  def self.finalize_identifier(_identifier, _parsed_hash); end
48
58
 
49
59
  # Combine `:letter_suffix` and `:letter_suffix_extra` (e.g., "U" + "r").
60
+ # The n+UPPERCASE annex capture nests the extra inside the suffix hash
61
+ # (pubid#555): unwrap it before stringifying.
50
62
  def self.combine_letter_suffix(value)
51
- suffix = value[:letter_suffix].to_s.strip
63
+ suffix = value[:letter_suffix]
64
+ suffix = suffix[:letter_suffix_extra] if suffix.is_a?(Hash)
65
+ suffix = suffix.to_s.strip
52
66
  extra = value[:letter_suffix_extra].to_s.strip
53
67
  extra.empty? ? suffix : "#{suffix}#{extra}"
54
68
  end
data/lib/pubid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pubid
4
- VERSION = "2.0.0.pre.alpha.49"
4
+ VERSION = "2.0.0.pre.alpha.50"
5
5
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yard"
4
+
5
+ namespace :docs do
6
+ # Generates docs/api-reference.md from the YARD registry over lib/ —
7
+ # the generated API reference the website assembles (pubid#577). CI
8
+ # regenerates and fails on drift, so the committed file cannot rot.
9
+ desc "Generate docs/api-reference.md from YARD"
10
+ task :api_reference do
11
+ require "pubid"
12
+ Pubid.eager_load_flavors!
13
+ YARD::Registry.clear
14
+ YARD::Parser::SourceParser.parse(Dir.glob("lib/**/*.rb").sort, [])
15
+
16
+ entry = lambda do |obj|
17
+ sig = obj.signature.to_s.strip.sub(/\Adef\s+/, "").gsub(/\s+/, " ")
18
+ doc = obj.docstring.strip.lines.first.to_s.strip
19
+ out = +"### `#{sig}`\n"
20
+ out << "\n#{doc}\n" unless doc.empty?
21
+ out
22
+ end
23
+
24
+ module_section = lambda do |path, names|
25
+ mod = YARD::Registry.at(path)
26
+ return "" unless mod
27
+
28
+ out = +"## `#{path}`\n\n"
29
+ names.sort.each do |name|
30
+ obj = mod.child("::#{name}") || mod.child(name.to_s)
31
+ out << entry.call(obj) if obj
32
+ end
33
+ out
34
+ end
35
+
36
+ out = +<<~HEADER
37
+ # API reference
38
+
39
+ <!-- Generated by `rake docs:api_reference` (YARD over lib/) — do not
40
+ edit by hand; CI fails on drift. The website assembles this file
41
+ into the library reference. -->
42
+
43
+ HEADER
44
+
45
+ out << module_section.call(
46
+ "Pubid",
47
+ %w[parse from_hash locate_stage locate_stage_flavors prefixes
48
+ prefix_flavors parse_by_prefix],
49
+ )
50
+ errors = YARD::Registry.at("Pubid::Errors")
51
+ if errors
52
+ out << "## `Pubid::Errors`\n\n"
53
+ errors.children
54
+ .select { |c| c.is_a?(YARD::CodeObjects::ClassObject) }
55
+ .sort_by(&:name).each do |klass|
56
+ sig = "Pubid::Errors::#{klass.name}"
57
+ sup = klass.superclass.to_s.split("::").last
58
+ doc = klass.docstring.strip.lines.first.to_s.strip
59
+ out << "### `#{sig} < #{sup}`\n"
60
+ out << "\n#{doc}\n" unless doc.empty?
61
+ end
62
+ end
63
+
64
+ Pubid::Registry.flavor_names.sort.each do |flavor|
65
+ mod = Pubid::Registry.get(flavor)
66
+ names = %w[parse parse_urn locate_stage locate_stage_by_harmonized_code
67
+ locate_type prefixes all_typed_stages all_project_stages]
68
+ .select { |m| mod.respond_to?(m) }
69
+ out << module_section.call(mod.name, names)
70
+ end
71
+
72
+ id_klass = YARD::Registry.at("Pubid::Identifier")
73
+ if id_klass
74
+ out << "## `Pubid::Identifier` (instance methods)\n\n"
75
+ id_klass.meths(scope: :instance, inherited: false)
76
+ .select { |m| m.visibility == :public }
77
+ .sort_by(&:name).each do |obj|
78
+ out << entry.call(obj)
79
+ end
80
+ end
81
+
82
+ File.write("docs/api-reference.md", out)
83
+ puts "docs/api-reference.md: #{out.lines.count} lines, " \
84
+ "#{Pubid::Registry.flavor_names.size} flavors"
85
+ end
86
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubid
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.alpha.49
4
+ version: 2.0.0.pre.alpha.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1048,6 +1048,7 @@ files:
1048
1048
  - lib/pubid/xsf/renderer.rb
1049
1049
  - lib/pubid/xsf/urn_generator.rb
1050
1050
  - lib/pubid/xsf/urn_parser.rb
1051
+ - lib/tasks/api_reference.rake
1051
1052
  - lib/tasks/benchmark.rake
1052
1053
  - lib/tasks/conformance.rake
1053
1054
  - lib/tasks/docs.rake