pubid 2.0.0.pre.alpha.6 → 2.0.0.pre.alpha.7

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +40 -0
  3. data/lib/pubid/amca.rb +6 -0
  4. data/lib/pubid/ansi.rb +7 -0
  5. data/lib/pubid/api.rb +5 -0
  6. data/lib/pubid/ashrae.rb +6 -0
  7. data/lib/pubid/asme.rb +5 -0
  8. data/lib/pubid/astm.rb +5 -0
  9. data/lib/pubid/bsi.rb +15 -0
  10. data/lib/pubid/ccsds.rb +5 -0
  11. data/lib/pubid/cen_cenelec.rb +7 -0
  12. data/lib/pubid/cie.rb +5 -0
  13. data/lib/pubid/csa.rb +5 -0
  14. data/lib/pubid/etsi.rb +7 -0
  15. data/lib/pubid/iala/builder.rb +62 -6
  16. data/lib/pubid/iala/identifier.rb +11 -7
  17. data/lib/pubid/iala/identifiers/advice.rb +15 -0
  18. data/lib/pubid/iala/identifiers/annex.rb +55 -0
  19. data/lib/pubid/iala/identifiers/general_assembly.rb +16 -0
  20. data/lib/pubid/iala/identifiers/letter.rb +16 -0
  21. data/lib/pubid/iala/identifiers.rb +12 -8
  22. data/lib/pubid/iala/parser.rb +39 -8
  23. data/lib/pubid/iala/renderer.rb +14 -0
  24. data/lib/pubid/iala/urn_generator.rb +17 -0
  25. data/lib/pubid/iala.rb +5 -0
  26. data/lib/pubid/idf.rb +5 -0
  27. data/lib/pubid/iec.rb +8 -0
  28. data/lib/pubid/ieee.rb +8 -0
  29. data/lib/pubid/iho.rb +5 -0
  30. data/lib/pubid/iso.rb +6 -0
  31. data/lib/pubid/itu.rb +5 -0
  32. data/lib/pubid/jcgm.rb +5 -0
  33. data/lib/pubid/jis.rb +6 -0
  34. data/lib/pubid/nist.rb +13 -0
  35. data/lib/pubid/oiml/builder.rb +33 -0
  36. data/lib/pubid/oiml/identifier.rb +1 -0
  37. data/lib/pubid/oiml/identifiers/bulletin.rb +101 -0
  38. data/lib/pubid/oiml/identifiers.rb +1 -0
  39. data/lib/pubid/oiml/parser.rb +43 -1
  40. data/lib/pubid/oiml/renderer.rb +51 -0
  41. data/lib/pubid/oiml/urn_generator.rb +22 -0
  42. data/lib/pubid/oiml.rb +5 -0
  43. data/lib/pubid/plateau.rb +5 -0
  44. data/lib/pubid/prefixes_support.rb +51 -0
  45. data/lib/pubid/sae.rb +6 -0
  46. data/lib/pubid/version.rb +1 -1
  47. data/lib/pubid.rb +68 -0
  48. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b1fa5f38da07bc9a507c0c507ba9ddfc8f6541ab9f4b6793b09abe302ab8849
4
- data.tar.gz: 63b7eba9c9942c177e95b9c78a09462679a11acde4fd6a2c2e486848fb43ca93
3
+ metadata.gz: 28589e7efce00bb93f0cabd35bb4169210a69396e1da6e1869c51fec7dd80450
4
+ data.tar.gz: 181f33478261d666dc3bf93488326f0c88d999da0d958822bc49b5e9b113b5bd
5
5
  SHA512:
6
- metadata.gz: 915034be3882a43991ce3c0f934c4097f216f1fdac35dcb59ed6a0decb639dd4a19882de4f28940d4f29e6e9bb351de3383b07bd95cc0dc2a53bb4ad875f072d
7
- data.tar.gz: 1cb7e935accc6e66bed8b599d25605b26cd415547c1ddec9ba2ecf1b7183883102c99078b830b2de9da436bc17613db042b5fc05e0d7d5515786fae71592aeb8
6
+ metadata.gz: 23924b813af8f851b79fcb39ff90133df69f9219559d4bbc15f6bd46540c63ef4bcd5ba5eccf1ba013a774b7c8e4b0bfdb11d9fb308db0ac1a2c1df8f005a0b6
7
+ data.tar.gz: 38d8b970836cfb889c4b1bef346daac97b340656e652ab4d1cd687e7ff7436c73c976404204be54ac2e18ab37cd6982d634e4b5d9a08255ae400d5418b2023e3
data/README.adoc CHANGED
@@ -292,6 +292,46 @@ base.root.equal?(base) # => true
292
292
  * ✅ **Works with supplements** - Traverses supplement chains correctly
293
293
 
294
294
 
295
+ == Flavor Prefix Routing
296
+
297
+ Every flavor exposes a static `prefixes` method returning the *leading
298
+ identifier prefix tokens* it recognizes — the tokens a printed identifier can
299
+ start with such that the string belongs to that SDO. This is the routing key a
300
+ consumer (such as relaton) uses to dispatch a reference string to the right
301
+ backend, without parsing.
302
+
303
+ [source,ruby]
304
+ ----
305
+ require 'pubid'
306
+
307
+ Pubid::Iso.prefixes
308
+ # => ["ISO", "ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"]
309
+
310
+ Pubid::Bsi.prefixes
311
+ # => ["BS", "BSI", "DD", "PD", "PAS", "NA", "TS", "HB", ... , "BS EN ISO"]
312
+
313
+ # Look up a single flavor by name
314
+ Pubid.prefixes(:iec)
315
+ # => ["IEC", "CISPR", "IECEE", "IECEx", "IECQ", "ISO/IEC", "IEC/ISO", "ISO/IEC/IEEE"]
316
+
317
+ # Reverse index: prefix token => owning flavor(s)
318
+ Pubid.prefix_flavors["ISO/IEC"] # => [:iec, :iso]
319
+ Pubid.prefix_flavors["ISO/IEC/IEEE"] # => [:iec, :ieee, :iso]
320
+ Pubid.prefix_flavors["DD"] # => [:bsi]
321
+ ----
322
+
323
+ **Semantics** (see `Pubid::PrefixesSupport` for the full contract):
324
+
325
+ * **Static** — no identifier string is parsed; callable at startup.
326
+ * **Uniform** — every registered flavor returns a plain `Array<String>`.
327
+ * **Symmetric joints** — a co-published prefix is listed by every co-publisher
328
+ (`"ISO/IEC"` is in both `Pubid::Iso.prefixes` and `Pubid::Iec.prefixes`).
329
+ * **Inclusion** — publisher prefixes (`ISO`, `NIST`, `BS`…) and leading
330
+ series/type tokens that can *begin* a reference (BSI `DD`/`PD`, NIST `FIPS`).
331
+ * **Exclusion** — pure sub-series that never lead a reference and
332
+ dangerously-ambiguous single letters (e.g. BSI aerospace `A`/`B`/`C`).
333
+
334
+
295
335
  == Advanced Rendering Styles
296
336
 
297
337
  ISO and IEC identifiers support multiple abbreviation forms for supplements to maintain round-trip fidelity with different official formats.
data/lib/pubid/amca.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Amca
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # AMCA publisher token; the ANSI/AMCA joint form comes from
8
+ # Pubid::JOINT_PREFIXES.
9
+ PREFIXES = ["AMCA"].freeze
10
+
5
11
  autoload :Identifier, "#{__dir__}/amca/identifier"
6
12
  autoload :Identifiers, "#{__dir__}/amca/identifiers"
7
13
  autoload :Builder, "#{__dir__}/amca/builder"
data/lib/pubid/ansi.rb CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Pubid
4
4
  module Ansi
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # ANSI publisher token; ANSI/ASHRAE and ANSI/AMCA joint forms come from
8
+ # Pubid::JOINT_PREFIXES. Parser::ORGANIZATIONS are co-publishers, not
9
+ # leading ANSI tokens, so they are excluded.
10
+ PREFIXES = ["ANSI"].freeze
11
+
5
12
  autoload :Builder, "#{__dir__}/ansi/builder"
6
13
  autoload :Identifier, "#{__dir__}/ansi/identifier"
7
14
  autoload :Identifiers, "#{__dir__}/ansi/identifiers"
data/lib/pubid/api.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Api
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole API publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["API"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/api/builder"
6
11
  autoload :Identifier, "#{__dir__}/api/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/api/identifiers"
data/lib/pubid/ashrae.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Pubid
4
4
  module Ashrae
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # ASHRAE publisher token; the ANSI/ASHRAE joint form comes from
8
+ # Pubid::JOINT_PREFIXES.
9
+ PREFIXES = ["ASHRAE"].freeze
10
+
5
11
  autoload :Builder, "#{__dir__}/ashrae/builder"
6
12
  autoload :Identifier, "#{__dir__}/ashrae/identifier"
7
13
  autoload :Identifiers, "#{__dir__}/ashrae/identifiers"
data/lib/pubid/asme.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Asme
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole ASME publisher token (see the parser's `asme_publisher` rule).
8
+ PREFIXES = ["ASME"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/asme/builder"
6
11
  autoload :Components, "#{__dir__}/asme/components/code"
7
12
  autoload :Identifier, "#{__dir__}/asme/identifier"
data/lib/pubid/astm.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Astm
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole ASTM publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["ASTM"].freeze
9
+
5
10
  autoload :Components, "#{__dir__}/astm/components"
6
11
  autoload :Identifier, "#{__dir__}/astm/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/astm/identifiers"
data/lib/pubid/bsi.rb CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  module Pubid
4
4
  module Bsi
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Leading tokens a printed BSI reference can start with, curated from the
8
+ # parser grammar (lib/pubid/bsi/parser.rb): the publisher (`bs` => BS/BSI),
9
+ # the native series/type rules that begin references (dd, pd, pas, na, ts,
10
+ # handbook => HB, pp, bip, ep_type), and the "BS <adopted-org>" combos the
11
+ # grammar builds from `bs` + `adopted_org_prefix`. Deliberately EXCLUDES the
12
+ # aerospace single/multi-letter prefixes (see
13
+ # identifiers/aerospace_standard.rb SINGLE_LETTER_PREFIXES / the parser's
14
+ # `single_letter_prefix` rule — A/B/C/S/X ...) because they are dangerously
15
+ # ambiguous and never lead a routable reference on their own; and the bare
16
+ # adopted forms (ISO/IEC/EN alone), which route to their own SDOs.
17
+ PREFIXES = (%w[BS BSI DD PD PAS NA TS HB PP BIP EP] +
18
+ ["BS EN", "BS ISO", "BS IEC", "BS EN ISO", "BS EN IEC"]).freeze
19
+
5
20
  module Components
6
21
  autoload :Code, "#{__dir__}/bsi/components/code"
7
22
  autoload :Date, "#{__dir__}/bsi/components/date"
data/lib/pubid/ccsds.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Ccsds
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CCSDS publisher token (see the parser's `ccsds_prefix` rule).
8
+ PREFIXES = ["CCSDS"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/ccsds/builder"
6
11
  autoload :Identifier, "#{__dir__}/ccsds/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/ccsds/identifiers"
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Pubid
4
4
  module CenCenelec
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # CEN/CENELEC publisher tokens, mirroring the parser's publisher rules
8
+ # (lib/pubid/cen_cenelec/parser.rb). Adopted-org prefixes (ISO/IEC/CISPR)
9
+ # are excluded — they route to their own SDOs, not to CEN.
10
+ PREFIXES = %w[EN CEN CLC CWA HD ES CR ENV].freeze
11
+
5
12
  autoload :Builder, "#{__dir__}/cen_cenelec/builder"
6
13
  autoload :Identifier, "#{__dir__}/cen_cenelec/identifier"
7
14
  autoload :Identifiers, "#{__dir__}/cen_cenelec/identifiers"
data/lib/pubid/cie.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Cie
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CIE publisher token (see the parser grammar).
8
+ PREFIXES = ["CIE"].freeze
9
+
5
10
  module Components
6
11
  autoload :Code, "#{__dir__}/cie/components/code"
7
12
  autoload :Language, "#{__dir__}/cie/components/language"
data/lib/pubid/csa.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Csa
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole CSA publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["CSA"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/csa/builder"
6
11
  autoload :Identifier, "#{__dir__}/csa/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/csa/identifiers"
data/lib/pubid/etsi.rb CHANGED
@@ -4,6 +4,13 @@ require "lutaml/model"
4
4
 
5
5
  module Pubid
6
6
  module Etsi
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sole ETSI publisher token (see the parser's `etsi_prefix` rule). The
10
+ # document-type tokens (EN, TS, TR, ...) are sub-types that follow the ETSI
11
+ # prefix, not standalone routing tokens, so they are excluded.
12
+ PREFIXES = ["ETSI"].freeze
13
+
7
14
  autoload :Builder, "#{__dir__}/etsi/builder"
8
15
  autoload :Components, "#{__dir__}/etsi/components"
9
16
  autoload :Identifier, "#{__dir__}/etsi/identifier"
@@ -6,10 +6,13 @@ module Pubid
6
6
  class Builder
7
7
  def build(parsed)
8
8
  hash = parsed
9
+
10
+ return build_annex(hash) if hash[:annex_marker]
11
+
9
12
  type_letter = stringify(hash[:type_letter])&.upcase
10
13
 
11
14
  attrs = {
12
- number: build_number(hash),
15
+ number: build_number(hash, type_letter),
13
16
  edition: stringify(hash.dig(:edition, :edition_value)),
14
17
  language: stringify(hash.dig(:language_group, :language))&.upcase,
15
18
  }.compact
@@ -23,13 +26,66 @@ module Pubid
23
26
 
24
27
  private
25
28
 
26
- # Compose "1070" or "0126-1" from doc_number + optional subpart.
27
- def build_number(hash)
29
+ # Build the Annex wrapper from the parse tree. The base sub-tree
30
+ # (captured under :base) is recursively built via the same logic as
31
+ # a flat identifier, then wrapped.
32
+ def build_annex(hash)
33
+ base_hash = hash[:base]
34
+ base_type = stringify(base_hash[:type_letter])&.upcase
35
+ base_number = build_number(base_hash, base_type)
36
+ base = Iala.identifier_klass_for_type_letter(base_type).new(number: base_number)
37
+
38
+ attrs = {
39
+ base_identifier: base,
40
+ annex_form: stringify(hash[:annex_marker]),
41
+ letter: stringify(hash[:annex_letter]),
42
+ edition: stringify(hash.dig(:edition, :edition_value)),
43
+ language: stringify(hash.dig(:language_group, :language))&.upcase,
44
+ }.compact
45
+
46
+ Identifiers::Annex.new(**attrs)
47
+ end
48
+
49
+ # Compose the full number string from the leading doc_number plus any
50
+ # dotted continuation (GA01.13 → "01.13", L2.1.11 → "2.1.11") plus
51
+ # the optional dash-separated subpart (C0103-1 → "0103-1", L2.7.1-2
52
+ # → "2.7.1-2"). Padding is type-aware per IALA's canonical form:
53
+ # typed S/R/G/M/C zero-pad to 4 digits; GeneralAssembly (GA) zero-
54
+ # pads each segment to 2; Advices (A) and Letters (L) preserve input
55
+ # (their canonical form is already what the IALA site prints).
56
+ def build_number(hash, type_letter = nil)
28
57
  base = stringify(hash[:doc_number])
29
- return base unless hash[:subpart]
58
+ return base unless base
59
+
60
+ base = canonical_base(base, type_letter)
61
+
62
+ dots = hash[:doc_number_dots]
63
+ dots_str = dots.is_a?(Array) ? "" : canonical_dots(stringify(dots).to_s, type_letter)
64
+
65
+ subpart_str = hash[:subpart] ? "-#{subpart_to_s(hash[:subpart])}" : ""
66
+ subpart_str = "" if subpart_str == "-"
67
+ "#{base}#{dots_str}#{subpart_str}"
68
+ end
69
+
70
+ # Per-type canonical width for the leading numeric run.
71
+ def canonical_base(base, type_letter)
72
+ case type_letter
73
+ when "S", "R", "G", "M", "C", "X", "P" then base.rjust(4, "0")
74
+ when "GA" then base.rjust(2, "0")
75
+ else base
76
+ end
77
+ end
78
+
79
+ # Per-type canonical width for each dotted continuation segment.
80
+ # `str` looks like ".13" or ".1.11" (leading dot, then digits).
81
+ def canonical_dots(str, type_letter)
82
+ return "" if str.empty?
30
83
 
31
- subpart_str = subpart_to_s(hash[:subpart])
32
- subpart_str.empty? ? base : "#{base}-#{subpart_str}"
84
+ if type_letter == "GA"
85
+ str.split(".", -1).map { |seg| seg.empty? ? seg : seg.rjust(2, "0") }.join(".")
86
+ else
87
+ str
88
+ end
33
89
  end
34
90
 
35
91
  # The subpart capture may be an array of {subpart_number: "1"},
@@ -33,13 +33,17 @@ module Pubid
33
33
  attribute :language, :string # "E", "F", "S", "C", "A", "R", nil
34
34
 
35
35
  IALA_TYPE_MAP = {
36
- "pubid:iala:standard" => "Pubid::Iala::Identifiers::Standard",
37
- "pubid:iala:recommendation" => "Pubid::Iala::Identifiers::Recommendation",
38
- "pubid:iala:guideline" => "Pubid::Iala::Identifiers::Guideline",
39
- "pubid:iala:manual" => "Pubid::Iala::Identifiers::Manual",
40
- "pubid:iala:model-course" => "Pubid::Iala::Identifiers::ModelCourse",
41
- "pubid:iala:report" => "Pubid::Iala::Identifiers::Report",
42
- "pubid:iala:resolution" => "Pubid::Iala::Identifiers::Resolution",
36
+ "pubid:iala:standard" => "Pubid::Iala::Identifiers::Standard",
37
+ "pubid:iala:recommendation" => "Pubid::Iala::Identifiers::Recommendation",
38
+ "pubid:iala:guideline" => "Pubid::Iala::Identifiers::Guideline",
39
+ "pubid:iala:manual" => "Pubid::Iala::Identifiers::Manual",
40
+ "pubid:iala:model-course" => "Pubid::Iala::Identifiers::ModelCourse",
41
+ "pubid:iala:advice" => "Pubid::Iala::Identifiers::Advice",
42
+ "pubid:iala:general-assembly" => "Pubid::Iala::Identifiers::GeneralAssembly",
43
+ "pubid:iala:letter" => "Pubid::Iala::Identifiers::Letter",
44
+ "pubid:iala:annex" => "Pubid::Iala::Identifiers::Annex",
45
+ "pubid:iala:report" => "Pubid::Iala::Identifiers::Report",
46
+ "pubid:iala:resolution" => "Pubid::Iala::Identifiers::Resolution",
43
47
  }.freeze
44
48
 
45
49
  key_value do
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Advices (A prefix). Numbering is dashed: A12-01, A13-04.
7
+ # Carries no edition in the corpus.
8
+ class Advice < Base
9
+ def self.type
10
+ { key: :advice, title: "Advice", short: "A" }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Annex to a base publication. Two surface forms appear in the
7
+ # corpus, both wrapping a Guideline:
8
+ #
9
+ # "G1045 Annex Ed 1" — bare Annex, no letter
10
+ # "G1128 ANNEX A Ed 1.6" — uppercase + letter (A/B/C/D)
11
+ #
12
+ # The case of the marker is preserved (Annex vs ANNEX) for round-trip
13
+ # fidelity. Edition and language apply to the Annex itself, not the base.
14
+ class Annex < Base
15
+ # The publication this annex supplements. Always present; carries the
16
+ # type letter and number (e.g. G1045).
17
+ attribute :base_identifier, ::Pubid::Iala::Identifier, polymorphic: true
18
+ # "Annex" or "ANNEX" — preserved verbatim for exact round-trip.
19
+ attribute :annex_form, :string
20
+ # "A", "B", "C", "D", or nil for the bare form.
21
+ attribute :letter, :string
22
+
23
+ key_value do
24
+ map "base_identifier",
25
+ with: { to: :base_identifier_to_kv,
26
+ from: :base_identifier_from_kv }
27
+ map "annex_form", to: :annex_form
28
+ map "letter", to: :letter
29
+ end
30
+
31
+ def self.type
32
+ { key: :annex, title: "Annex", short: nil }
33
+ end
34
+
35
+ private
36
+
37
+ def base_identifier_to_kv(model, doc)
38
+ base = model.base_identifier
39
+ return unless base
40
+
41
+ doc.add_child(
42
+ Lutaml::KeyValue::DataModel::Element.new("base_identifier",
43
+ base.to_hash),
44
+ )
45
+ end
46
+
47
+ def base_identifier_from_kv(model, value)
48
+ return unless value
49
+
50
+ model.base_identifier = ::Pubid::Iala::Identifier.from_hash(value)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA General Assembly resolutions (GA prefix). Numbering is dotted
7
+ # with a series and an index: GA01.01, GA01.13. No edition.
8
+ class GeneralAssembly < Base
9
+ def self.type
10
+ { key: :"general-assembly", title: "General Assembly Resolution",
11
+ short: "GA" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Iala
5
+ module Identifiers
6
+ # IALA Letters (L prefix). Numbering is dotted with a series, sub-series,
7
+ # and item: L2.1.11, L2.7.1-2 (the trailing -2 is a sub-part on top of
8
+ # the dotted number).
9
+ class Letter < Base
10
+ def self.type
11
+ { key: :letter, title: "Letter", short: "L" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,14 +3,18 @@
3
3
  module Pubid
4
4
  module Iala
5
5
  module Identifiers
6
- autoload :Base, "#{__dir__}/identifiers/base"
7
- autoload :Standard, "#{__dir__}/identifiers/standard"
8
- autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
9
- autoload :Guideline, "#{__dir__}/identifiers/guideline"
10
- autoload :Manual, "#{__dir__}/identifiers/manual"
11
- autoload :ModelCourse, "#{__dir__}/identifiers/model_course"
12
- autoload :Report, "#{__dir__}/identifiers/report"
13
- autoload :Resolution, "#{__dir__}/identifiers/resolution"
6
+ autoload :Base, "#{__dir__}/identifiers/base"
7
+ autoload :Advice, "#{__dir__}/identifiers/advice"
8
+ autoload :Annex, "#{__dir__}/identifiers/annex"
9
+ autoload :Standard, "#{__dir__}/identifiers/standard"
10
+ autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
11
+ autoload :Guideline, "#{__dir__}/identifiers/guideline"
12
+ autoload :Manual, "#{__dir__}/identifiers/manual"
13
+ autoload :ModelCourse, "#{__dir__}/identifiers/model_course"
14
+ autoload :GeneralAssembly, "#{__dir__}/identifiers/general_assembly"
15
+ autoload :Letter, "#{__dir__}/identifiers/letter"
16
+ autoload :Report, "#{__dir__}/identifiers/report"
17
+ autoload :Resolution, "#{__dir__}/identifiers/resolution"
14
18
  end
15
19
  end
16
20
  end
@@ -28,16 +28,32 @@ module Pubid
28
28
  rule(:lparen) { str("(") }
29
29
  rule(:rparen) { str(")") }
30
30
 
31
- # Type letterone of IALA's seven document classes. Captured to
32
- # route to the right Identifiers::* subclass.
31
+ # Type prefix — IALA document classes. GA must be tried before G so it
32
+ # isn't left with a dangling "A". Captured to route to the right
33
+ # Identifiers::* subclass.
33
34
  rule(:type_letter) do
34
- (str("S") | str("R") | str("G") | str("M") |
35
- str("C") | str("X") | str("P")).as(:type_letter)
35
+ (str("GA") | str("S") | str("R") | str("G") | str("M") |
36
+ str("C") | str("A") | str("L") | str("X") | str("P")).as(:type_letter)
36
37
  end
37
38
 
38
- # 4-digit document number, e.g. "1070", "0126".
39
+ # Document number — one or more decimal digits. The dataset historically
40
+ # zero-pads to 4 (S1070, R0126, M0001) but the canonical form going
41
+ # forward is unpadded (M1, GA1.1). Accept any length so both shapes
42
+ # round-trip; the number attribute preserves whatever was parsed.
39
43
  rule(:doc_number) do
40
- match("[0-9]").repeat(4, 4).as(:doc_number)
44
+ match("[0-9]").repeat(1).as(:doc_number)
45
+ end
46
+
47
+ # Dotted continuation that some prefixes use as part of the number:
48
+ # GA01.01 (series.index), L2.1.11 (series.sub.item). Captured into the
49
+ # same :doc_number atom so the number attribute stays a single string
50
+ # (e.g. "01.01", "2.1.11") and the existing builder needs no changes.
51
+ rule(:dotted_number) do
52
+ dot >> match("[0-9]").repeat(1)
53
+ end
54
+
55
+ rule(:number_with_dots) do
56
+ doc_number >> dotted_number.repeat.as(:doc_number_dots)
41
57
  end
42
58
 
43
59
  # Numeric sub-part suffix(es): "-1", "-9-10", "-11". The catalogue
@@ -48,7 +64,7 @@ module Pubid
48
64
  end
49
65
 
50
66
  rule(:code) do
51
- type_letter >> doc_number >> subpart.maybe
67
+ type_letter >> number_with_dots >> subpart.maybe
52
68
  end
53
69
 
54
70
  # Edition forms observed on covers and listing pages:
@@ -83,7 +99,22 @@ module Pubid
83
99
  end
84
100
 
85
101
  rule(:identifier) do
86
- publisher >> code >> edition.maybe >> language.maybe
102
+ annex_identifier | (publisher >> code >> edition.maybe >> language.maybe)
103
+ end
104
+
105
+ # Annex to a base publication. Wraps the base code and adds an
106
+ # "Annex" / "ANNEX" marker (case preserved verbatim), an optional
107
+ # single-letter suffix (A/B/C/D), then the usual edition + language.
108
+ # Tried before the flat `identifier` so the annex marker isn't left
109
+ # dangling after a successful base-code match.
110
+ #
111
+ # The letter suffix uses a negative lookahead for a following lowercase
112
+ # letter so the "E" in "Ed 1" isn't mis-captured as annex letter "E".
113
+ rule(:annex_identifier) do
114
+ (publisher >> type_letter >> number_with_dots >> subpart.maybe).as(:base) >>
115
+ space >> (str("Annex") | str("ANNEX")).as(:annex_marker) >>
116
+ (space >> match("[A-Z]").as(:annex_letter) >> match("[a-z]").absent?).maybe >>
117
+ edition.maybe >> language.maybe
87
118
  end
88
119
 
89
120
  # Parse a string and return the raw parslet tree.
@@ -12,11 +12,25 @@ module Pubid
12
12
  def render(context: nil, **_opts)
13
13
  id = @id
14
14
 
15
+ return render_annex(id) if id.is_a?(Identifiers::Annex)
16
+
15
17
  rendered = "#{id.publisher} #{id.type_letter}#{id.number}"
16
18
  rendered << " Ed #{id.edition}" if id.edition
17
19
  rendered << " (#{id.language})" if id.language
18
20
  rendered
19
21
  end
22
+
23
+ # Render an Annex wrapper. The base identifier is rendered first, then
24
+ # the marker form is preserved verbatim ("Annex" vs "ANNEX"), then the
25
+ # optional letter, edition, and language.
26
+ def render_annex(id)
27
+ base_str = id.base_identifier.to_s
28
+ rendered = "#{base_str} #{id.annex_form}"
29
+ rendered << " #{id.letter}" if id.letter
30
+ rendered << " Ed #{id.edition}" if id.edition
31
+ rendered << " (#{id.language})" if id.language
32
+ rendered
33
+ end
20
34
  end
21
35
  end
22
36
  end
@@ -16,6 +16,8 @@ module Pubid
16
16
  end
17
17
 
18
18
  def generate
19
+ return generate_annex if identifier.is_a?(Identifiers::Annex)
20
+
19
21
  parts = ["urn:mrn:iala:pub", code_segment]
20
22
  parts << "ed#{identifier.edition}" if identifier.edition
21
23
  parts << identifier.language.downcase if identifier.language
@@ -24,6 +26,21 @@ module Pubid
24
26
 
25
27
  private
26
28
 
29
+ # URN for an Annex: embeds the base's code, then an "annex" segment
30
+ # (with optional letter suffix), then the annex's own edition/lang.
31
+ # G1045 Annex Ed 1 → urn:mrn:iala:pub:g1045:annex:ed1
32
+ # G1128 ANNEX A Ed 1.6 (E) → urn:mrn:iala:pub:g1128:annex-a:ed1.6:e
33
+ def generate_annex
34
+ base = identifier.base_identifier
35
+ parts = ["urn:mrn:iala:pub",
36
+ "#{base.type_letter.downcase}#{base.number}"]
37
+ annex_seg = identifier.letter ? "annex-#{identifier.letter.downcase}" : "annex"
38
+ parts << annex_seg
39
+ parts << "ed#{identifier.edition}" if identifier.edition
40
+ parts << identifier.language.downcase if identifier.language
41
+ parts.join(":")
42
+ end
43
+
27
44
  def code_segment
28
45
  "#{identifier.type_letter.downcase}#{identifier.number}"
29
46
  end
data/lib/pubid/iala.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Iala
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole IALA publisher token (see the parser's `publisher` rule).
8
+ PREFIXES = ["IALA"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/iala/builder"
6
11
  autoload :Identifier, "#{__dir__}/iala/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/iala/identifiers"
data/lib/pubid/idf.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Pubid
4
4
  module Idf
5
+ extend Pubid::PrefixesSupport
6
+
7
+ # Sole IDF publisher token (see the parser's `prefix_sole_publisher` rule).
8
+ PREFIXES = ["IDF"].freeze
9
+
5
10
  autoload :Builder, "#{__dir__}/idf/builder"
6
11
  autoload :Identifier, "#{__dir__}/idf/identifier"
7
12
  autoload :Identifiers, "#{__dir__}/idf/identifiers"
data/lib/pubid/iec.rb CHANGED
@@ -3,6 +3,14 @@
3
3
  module Pubid
4
4
  module Iec
5
5
  autoload :Components, "#{__dir__}/iec/components"
6
+
7
+ extend Pubid::PrefixesSupport
8
+
9
+ # Sourced from the parser's publisher list (Components::Publisher::PUBLISHERS)
10
+ # minus the joint "ISO/IEC", which is contributed symmetrically via
11
+ # Pubid::JOINT_PREFIXES (along with IEC/ISO and ISO/IEC/IEEE).
12
+ PREFIXES = (Components::Publisher::PUBLISHERS.keys - ["ISO/IEC"]).freeze
13
+
6
14
  autoload :Identifier, "#{__dir__}/iec/identifier"
7
15
  autoload :Identifiers, "#{__dir__}/iec/identifiers"
8
16
  autoload :SingleIdentifier, "#{__dir__}/iec/single_identifier"