pubid 2.0.1 → 2.0.3

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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/data/ieee/update_codes.yaml +2 -2
  3. data/data/parg/adobe.json +1 -1
  4. data/data/parg/aiee.json +1 -1
  5. data/data/parg/aiee.parg +99 -9
  6. data/data/parg/amca.json +1 -1
  7. data/data/parg/ansi.json +1 -1
  8. data/data/parg/api.json +1 -1
  9. data/data/parg/ashrae.json +1 -1
  10. data/data/parg/asme.json +1 -1
  11. data/data/parg/astm.json +1 -1
  12. data/data/parg/bipm.json +1 -1
  13. data/data/parg/bsi.json +1 -1
  14. data/data/parg/ccsds.json +1 -1
  15. data/data/parg/cen_cenelec.json +1 -1
  16. data/data/parg/cie.json +1 -1
  17. data/data/parg/csa.json +1 -1
  18. data/data/parg/ecma.json +1 -1
  19. data/data/parg/etsi.json +1 -1
  20. data/data/parg/evs.json +1 -1
  21. data/data/parg/gost.json +1 -1
  22. data/data/parg/iala.json +1 -1
  23. data/data/parg/iana.json +1 -1
  24. data/data/parg/idf.json +1 -1
  25. data/data/parg/iec.json +1 -1
  26. data/data/parg/ieee.json +1 -1
  27. data/data/parg/iho.json +1 -0
  28. data/data/parg/ire.json +1 -1
  29. data/data/parg/ire.parg +51 -12
  30. data/data/parg/isbn.json +1 -1
  31. data/data/parg/iso.json +1 -1
  32. data/data/parg/iso.parg +751 -0
  33. data/data/parg/itu.json +1 -1
  34. data/data/parg/jcgm.json +1 -1
  35. data/data/parg/jis.json +1 -1
  36. data/data/parg/nesc.json +1 -1
  37. data/data/parg/nesc.parg +97 -12
  38. data/data/parg/nist.json +1 -1
  39. data/data/parg/oiml.json +1 -1
  40. data/data/parg/omg.json +1 -1
  41. data/data/parg/parg.json +1 -1
  42. data/data/parg/plateau.json +1 -1
  43. data/data/parg/tgpp.json +1 -1
  44. data/data/parg/un.json +1 -1
  45. data/lib/pubid/adobe/identifier.rb +1 -1
  46. data/lib/pubid/adobe/parser.rb +2 -113
  47. data/lib/pubid/amca/identifiers/base.rb +1 -1
  48. data/lib/pubid/amca/parser.rb +1 -145
  49. data/lib/pubid/ansi/identifier.rb +1 -1
  50. data/lib/pubid/ansi/parser.rb +1 -82
  51. data/lib/pubid/api/builder.rb +32 -1
  52. data/lib/pubid/api/identifier.rb +1 -1
  53. data/lib/pubid/api/parser.rb +1 -131
  54. data/lib/pubid/ashrae/identifiers/base.rb +1 -1
  55. data/lib/pubid/ashrae/parser.rb +3 -710
  56. data/lib/pubid/asme/builder.rb +6 -4
  57. data/lib/pubid/asme/identifier.rb +1 -2
  58. data/lib/pubid/asme/parser.rb +1 -328
  59. data/lib/pubid/astm/builder.rb +12 -3
  60. data/lib/pubid/astm/identifier.rb +1 -2
  61. data/lib/pubid/astm/parser.rb +1 -227
  62. data/lib/pubid/bipm/identifier.rb +8 -1
  63. data/lib/pubid/bipm/parser.rb +2 -224
  64. data/lib/pubid/bsi/builder.rb +2 -4
  65. data/lib/pubid/bsi/parser.rb +1 -666
  66. data/lib/pubid/bsi/single_identifier.rb +38 -1
  67. data/lib/pubid/calconnect/parser.rb +2 -40
  68. data/lib/pubid/ccsds/identifier.rb +1 -1
  69. data/lib/pubid/ccsds/parser.rb +1 -66
  70. data/lib/pubid/cen_cenelec/identifier.rb +16 -1
  71. data/lib/pubid/cen_cenelec/parser.rb +3 -136
  72. data/lib/pubid/cie/identifier.rb +1 -1
  73. data/lib/pubid/cie/parser.rb +3 -378
  74. data/lib/pubid/conformance.rb +1 -1
  75. data/lib/pubid/csa/builder.rb +41 -18
  76. data/lib/pubid/csa/identifier.rb +12 -4
  77. data/lib/pubid/csa/identifiers/amendment.rb +28 -0
  78. data/lib/pubid/csa/identifiers.rb +1 -0
  79. data/lib/pubid/csa/parser.rb +16 -397
  80. data/lib/pubid/doi/parser.rb +2 -37
  81. data/lib/pubid/easc/builder.rb +1 -1
  82. data/lib/pubid/easc/parser.rb +2 -70
  83. data/lib/pubid/ecma/CLAUDE.md +8 -1
  84. data/lib/pubid/ecma/identifier.rb +13 -8
  85. data/lib/pubid/ecma/parser.rb +2 -54
  86. data/lib/pubid/ecma/renderer.rb +3 -7
  87. data/lib/pubid/errors.rb +18 -13
  88. data/lib/pubid/etsi/builder.rb +8 -5
  89. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  90. data/lib/pubid/etsi/parser.rb +1 -132
  91. data/lib/pubid/evs/identifier.rb +7 -0
  92. data/lib/pubid/evs/parser.rb +1 -8
  93. data/lib/pubid/evs.rb +1 -1
  94. data/lib/pubid/gb/identifier.rb +6 -0
  95. data/lib/pubid/gb/parser.rb +2 -51
  96. data/lib/pubid/gost/identifier.rb +9 -1
  97. data/lib/pubid/gost/parser.rb +4 -81
  98. data/lib/pubid/iala/builder.rb +48 -39
  99. data/lib/pubid/iala/identifier.rb +1 -1
  100. data/lib/pubid/iala/parser.rb +2 -121
  101. data/lib/pubid/iana/identifier.rb +1 -1
  102. data/lib/pubid/iana/parser.rb +2 -24
  103. data/lib/pubid/identifier.rb +78 -1
  104. data/lib/pubid/idf/builder.rb +42 -0
  105. data/lib/pubid/idf/identifier.rb +1 -1
  106. data/lib/pubid/idf/parser.rb +1 -137
  107. data/lib/pubid/idf.rb +1 -2
  108. data/lib/pubid/iec/builder.rb +158 -50
  109. data/lib/pubid/iec/identifier.rb +1 -1
  110. data/lib/pubid/iec/parser.rb +3 -430
  111. data/lib/pubid/ieee/CLAUDE.md +2 -0
  112. data/lib/pubid/ieee/aiee/identifier.rb +2 -1
  113. data/lib/pubid/ieee/aiee.rb +0 -1
  114. data/lib/pubid/ieee/builder.rb +177 -30
  115. data/lib/pubid/ieee/components/code.rb +46 -7
  116. data/lib/pubid/ieee/identifiers/base.rb +5 -2
  117. data/lib/pubid/ieee/identifiers/joint_development.rb +7 -3
  118. data/lib/pubid/ieee/ire/identifier.rb +2 -1
  119. data/lib/pubid/ieee/ire.rb +0 -1
  120. data/lib/pubid/ieee/nesc/builder.rb +1 -1
  121. data/lib/pubid/ieee/nesc.rb +0 -1
  122. data/lib/pubid/ieee/parser.rb +1 -1257
  123. data/lib/pubid/ieee/project_renderer.rb +5 -1
  124. data/lib/pubid/ieee/renderer.rb +4 -2
  125. data/lib/pubid/ieee/urn_generator.rb +4 -1
  126. data/lib/pubid/ietf/identifiers/base.rb +17 -1
  127. data/lib/pubid/ietf/parser.rb +2 -47
  128. data/lib/pubid/ietf/renderer.rb +17 -2
  129. data/lib/pubid/iho/CLAUDE.md +98 -0
  130. data/lib/pubid/iho/builder.rb +4 -1
  131. data/lib/pubid/iho/identifiers/base.rb +28 -2
  132. data/lib/pubid/iho/renderer.rb +8 -0
  133. data/lib/pubid/iho/urn_generator.rb +14 -1
  134. data/lib/pubid/iho/urn_parser.rb +45 -32
  135. data/lib/pubid/iho.rb +0 -1
  136. data/lib/pubid/isbn/identifier.rb +1 -1
  137. data/lib/pubid/isbn/parser.rb +2 -31
  138. data/lib/pubid/iso/CLAUDE.md +66 -0
  139. data/lib/pubid/iso/builder.rb +229 -15
  140. data/lib/pubid/iso/identifier.rb +22 -12
  141. data/lib/pubid/iso/identifiers/directives.rb +6 -3
  142. data/lib/pubid/iso/identifiers/guide.rb +6 -3
  143. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +6 -3
  144. data/lib/pubid/iso/normalizer.rb +5 -4
  145. data/lib/pubid/iso/parser.rb +1 -539
  146. data/lib/pubid/iso/supplement_identifier.rb +6 -3
  147. data/lib/pubid/iso/urn_parser.rb +13 -8
  148. data/lib/pubid/iso/utilities.rb +4 -5
  149. data/lib/pubid/iso.rb +1 -19
  150. data/lib/pubid/itu/builder.rb +36 -20
  151. data/lib/pubid/itu/identifiers/base.rb +15 -2
  152. data/lib/pubid/itu/parser.rb +1 -775
  153. data/lib/pubid/jcgm/builder.rb +35 -0
  154. data/lib/pubid/jcgm/parser.rb +1 -151
  155. data/lib/pubid/jcgm.rb +1 -2
  156. data/lib/pubid/jis/CLAUDE.md +36 -0
  157. data/lib/pubid/jis/builder.rb +30 -41
  158. data/lib/pubid/jis/identifier.rb +12 -2
  159. data/lib/pubid/jis/identifiers/welding_engineering_standard.rb +35 -0
  160. data/lib/pubid/jis/identifiers.rb +2 -0
  161. data/lib/pubid/jis/parser.rb +1 -128
  162. data/lib/pubid/jis/renderer.rb +1 -1
  163. data/lib/pubid/jis/supplement_identifier.rb +5 -5
  164. data/lib/pubid/jis/urn_parser.rb +2 -1
  165. data/lib/pubid/jis.rb +3 -2
  166. data/lib/pubid/nist/builder.rb +5 -1
  167. data/lib/pubid/nist/caster.rb +13 -3
  168. data/lib/pubid/nist/identifiers/base.rb +37 -2
  169. data/lib/pubid/nist/identifiers/circular.rb +2 -2
  170. data/lib/pubid/nist/identifiers/circular_supplement.rb +2 -2
  171. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +2 -2
  172. data/lib/pubid/nist/identifiers/crpl_report.rb +2 -2
  173. data/lib/pubid/nist/identifiers/handbook.rb +2 -2
  174. data/lib/pubid/nist/identifiers/internal_report.rb +2 -2
  175. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  176. data/lib/pubid/nist/identifiers/monograph.rb +2 -2
  177. data/lib/pubid/nist/identifiers/report.rb +2 -2
  178. data/lib/pubid/nist/parser.rb +19 -775
  179. data/lib/pubid/nist/router.rb +4 -1
  180. data/lib/pubid/nist/supplement_identifier.rb +2 -2
  181. data/lib/pubid/oasis/parser.rb +2 -16
  182. data/lib/pubid/ogc/parser.rb +2 -26
  183. data/lib/pubid/oiml/identifiers/certification_system.rb +7 -0
  184. data/lib/pubid/oiml/identifiers/dual_published.rb +2 -2
  185. data/lib/pubid/oiml/parser.rb +1 -303
  186. data/lib/pubid/oiml.rb +1 -2
  187. data/lib/pubid/omg/identifier.rb +6 -1
  188. data/lib/pubid/omg/parser.rb +2 -76
  189. data/lib/pubid/parser.rb +5 -4
  190. data/lib/pubid/parsers/mr_string.rb +1 -1
  191. data/lib/pubid/plateau/parser.rb +1 -66
  192. data/lib/pubid/plateau.rb +1 -2
  193. data/lib/pubid/renderers/directives_renderer.rb +1 -1
  194. data/lib/pubid/renderers/iwa_renderer.rb +1 -1
  195. data/lib/pubid/renderers/publisher_stripper.rb +82 -0
  196. data/lib/pubid/renderers/supplement_renderer.rb +1 -1
  197. data/lib/pubid/renderers.rb +1 -0
  198. data/lib/pubid/sae/parser.rb +1 -48
  199. data/lib/pubid/tgpp/identifier.rb +1 -1
  200. data/lib/pubid/tgpp/parser.rb +2 -59
  201. data/lib/pubid/un/identifier.rb +1 -1
  202. data/lib/pubid/un/parser.rb +1 -18
  203. data/lib/pubid/version.rb +1 -1
  204. data/lib/pubid/w3c/parser.rb +2 -27
  205. data/lib/pubid/xsf/identifier.rb +10 -6
  206. data/lib/pubid/xsf/parser.rb +3 -31
  207. data/lib/pubid/xsf/renderer.rb +2 -4
  208. data/lib/pubid.rb +8 -9
  209. metadata +9 -23
  210. data/lib/pubid/ieee/aiee/parser.rb +0 -130
  211. data/lib/pubid/ieee/ire/parser.rb +0 -109
  212. data/lib/pubid/ieee/nesc/parser.rb +0 -155
  213. data/lib/pubid/iho/parser.rb +0 -68
  214. data/lib/pubid/parser/common_parse_methods.rb +0 -13
  215. data/lib/pubid/parser/common_parse_rules.rb +0 -56
  216. data/lib/pubid/parser/grammar.rb +0 -96
@@ -2,20 +2,20 @@
2
2
 
3
3
  module Pubid
4
4
  module Iala
5
- # Builds an IALA identifier object from the Parslet parse tree.
5
+ # Builds an IALA identifier object from the artifact's bound map —
6
+ # the flat attribute hash the grammar's bindings block declares
7
+ # (typed at the engine, one key per capture).
6
8
  class Builder
7
9
  def build(parsed)
8
- hash = parsed
10
+ return build_annex(parsed) if parsed[:annex_marker]
9
11
 
10
- return build_annex(hash) if hash[:annex_marker]
11
-
12
- type_letter = stringify(hash[:type_letter])&.upcase
12
+ type_letter = stringify(parsed[:type_letter])&.upcase
13
13
  klass = Iala.identifier_klass_for_type_letter(type_letter)
14
14
 
15
15
  attrs = {
16
- number: build_number(hash, klass),
17
- edition: stringify(hash.dig(:edition, :edition_value)),
18
- language: stringify(hash.dig(:language_group, :language))&.upcase,
16
+ number: build_number(parsed, klass),
17
+ edition: stringify(parsed[:edition_value]),
18
+ language: stringify(parsed[:language])&.upcase,
19
19
  }.compact
20
20
 
21
21
  klass.new(**attrs)
@@ -27,31 +27,34 @@ module Pubid
27
27
 
28
28
  private
29
29
 
30
- # Build the Annex wrapper from the parse tree. The base sub-tree
31
- # (captured under :base) is recursively built via the same logic as
32
- # a flat identifier, then wrapped.
30
+ # Build the Annex wrapper from the bound map. The base composite in
31
+ # the grammar stays unbound: its captures (type_letter, doc_number,
32
+ # …) are the only occurrence of each name on the annex path, so the
33
+ # harvest hoists them to the top level and this branch reads the
34
+ # same flat keys as the plain identifier.
33
35
  def build_annex(hash)
34
- base_hash = hash[:base]
35
- base_type = stringify(base_hash[:type_letter])&.upcase
36
+ base_type = stringify(hash[:type_letter])&.upcase
36
37
  base_klass = Iala.identifier_klass_for_type_letter(base_type)
37
- base = base_klass.new(number: build_number(base_hash, base_klass))
38
+ base = base_klass.new(number: build_number(hash, base_klass))
38
39
 
39
40
  attrs = {
40
41
  base: base,
41
- annex_form: stringify(hash[:annex_marker]),
42
- letter: stringify(hash[:annex_letter]),
43
- edition: stringify(hash.dig(:edition, :edition_value)),
44
- language: stringify(hash.dig(:language_group, :language))&.upcase,
42
+ annex_form: stringify(hash[:annex_marker]),
43
+ letter: stringify(hash[:annex_letter]),
44
+ edition: stringify(hash[:edition_value]),
45
+ language: stringify(hash[:language])&.upcase,
45
46
  }.compact
46
47
 
47
48
  Identifiers::Annex.new(**attrs)
48
49
  end
49
50
 
50
- # Compose the full number string from the leading doc_number plus any
51
- # dotted continuation (GA01.13 → "01.13", L2.1.11 → "2.1.11") plus
52
- # the optional dash-separated subpart (C0103-1 → "0103-1", L2.7.1-2
53
- # → "2.7.1-2"). Padding is class-driven: the resolved identifier
54
- # class declares its canonical IALA cover-page widths via
51
+ # Compose the full number string from the bound map's three number
52
+ # columns: the leading doc_number, the dotted continuation segments
53
+ # (doc_number_dots, each `{dotted_segment: "01"}`) and the dash
54
+ # subpart runs (subpart, each `{subpart_number: "1"}`). GA01.13 →
55
+ # "01.13", L2.1.11 → "2.1.11", C0103-1 → "0103-1", L2.7.1-2 →
56
+ # "2.7.1-2". Padding is class-driven: the resolved identifier class
57
+ # declares its canonical IALA cover-page widths via
55
58
  # `canonical_number_width` / `canonical_dotted_segment_width`, so a
56
59
  # new type declares its padding on its own class instead of editing
57
60
  # this builder's case statement.
@@ -61,34 +64,40 @@ module Pubid
61
64
 
62
65
  base = canonical_base(base, klass)
63
66
 
64
- dots = hash[:doc_number_dots]
65
- dots_str = dots.is_a?(Array) ? "" : canonical_dots(stringify(dots).to_s, klass)
67
+ dots = canonical_dots(
68
+ number_segments(hash[:doc_number_dots], :dotted_segment), klass
69
+ )
70
+ dots_str = dots.empty? ? "" : ".#{dots}"
71
+
72
+ subparts = number_segments(hash[:subpart], :subpart_number)
73
+ subpart_str = subparts.empty? ? "" : "-#{subparts.join('-')}"
66
74
 
67
- subpart_str = hash[:subpart] ? "-#{subpart_to_s(hash[:subpart])}" : ""
68
- subpart_str = "" if subpart_str == "-"
69
75
  "#{base}#{dots_str}#{subpart_str}"
70
76
  end
71
77
 
78
+ # The string values of a bound repetition column: doc_number_dots
79
+ # carries `{dotted_segment: …}` elements, subpart carries
80
+ # `{subpart_number: …}` elements.
81
+ def number_segments(column, key)
82
+ Array(column).filter_map do |element|
83
+ stringify(element[key]) if element.is_a?(Hash)
84
+ end
85
+ end
86
+
72
87
  def canonical_base(base, klass)
73
88
  width = klass&.canonical_number_width
74
89
  width ? base.rjust(width, "0") : base
75
90
  end
76
91
 
77
- def canonical_dots(str, klass)
78
- return "" if str.empty?
92
+ # Pad each dotted continuation segment to the class's canonical
93
+ # width; nil width preserves the segments verbatim (the L series).
94
+ def canonical_dots(segments, klass)
95
+ return "" if segments.empty?
79
96
 
80
97
  width = klass&.canonical_dotted_segment_width
81
- return str unless width
82
-
83
- str.split(".", -1).map { |seg| seg.empty? ? seg : seg.rjust(width, "0") }.join(".")
84
- end
85
-
86
- # The subpart capture may be an array of {subpart_number: "1"},
87
- # {subpart_number: "9"}, … (repeated matches). Join with "-".
88
- def subpart_to_s(subpart)
89
- return stringify(subpart).to_s unless subpart.is_a?(Array)
98
+ return segments.join(".") unless width
90
99
 
91
- subpart.filter_map { |h| h.is_a?(Hash) ? stringify(h[:subpart_number]) : nil }.join("-")
100
+ segments.map { |segment| segment.rjust(width, "0") }.join(".")
92
101
  end
93
102
 
94
103
  def stringify(value)
@@ -33,7 +33,7 @@ module Pubid
33
33
  if FormatDetector.detect(identifier) == :urn
34
34
  UrnParser.parse(identifier)
35
35
  else
36
- parsed = Pubid::Parg::Backend.parse(:iala, identifier)
36
+ parsed = Pubid::Parg::Model.bound(:iala, identifier)
37
37
  Builder.build(parsed)
38
38
  end
39
39
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Iala
7
- # Parslet grammar for IALA identifiers.
5
+ # Grammar for IALA identifiers.
8
6
  #
9
7
  # Accepts every shape that appears on the IALA website or cover page:
10
8
  #
@@ -15,124 +13,7 @@ module Pubid
15
13
  # R1016:ed2.0(F)
16
14
  # C0103-1
17
15
  # C0103-1 Ed 3.0
18
- class Parser < ::Pubid::Parser::Grammar
19
- include ::Pubid::Parser::CommonParseRules
20
-
21
- root :identifier
22
-
23
- rule(:space) { str(" ") }
24
- rule(:space?) { space.maybe }
25
- rule(:dash) { str("-") }
26
- rule(:colon) { str(":") }
27
- rule(:dot) { str(".") }
28
- rule(:lparen) { str("(") }
29
- rule(:rparen) { str(")") }
30
-
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.
34
- rule(:type_letter) do
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)
37
- end
38
-
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.
43
- rule(:doc_number) do
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)
57
- end
58
-
59
- # Numeric sub-part suffix(es): "-1", "-9-10", "-11". The catalogue
60
- # uses ranges like R0124-9-10 & 11 — that compound is captured
61
- # verbatim and resolved by the caller, not here.
62
- rule(:subpart) do
63
- (dash >> digits.as(:subpart_number)).repeat(1).as(:subpart)
64
- end
65
-
66
- rule(:code) do
67
- type_letter >> number_with_dots >> subpart.maybe
68
- end
69
-
70
- # Edition forms observed on covers and listing pages:
71
- # " Ed 2.0" — cover-page human form
72
- # ":ed2.0" — listing-page compact form
73
- rule(:edition_human) do
74
- (space >> str("Ed") >> space >> edition_value).as(:edition)
75
- end
76
-
77
- rule(:edition_compact) do
78
- (colon >> str("ed") >> edition_value).as(:edition)
79
- end
80
-
81
- rule(:edition_value) do
82
- (digits >> (dot >> digits).repeat).as(:edition_value)
83
- end
84
-
85
- rule(:edition) do
86
- edition_human | edition_compact
87
- end
88
-
89
- # Language is a single uppercase letter inside parens: (E), (F), …
90
- rule(:language) do
91
- (space? >> lparen >>
92
- (str("E") | str("F") | str("S") | str("C") | str("A") | str("R")).as(:language) >>
93
- rparen).as(:language_group)
94
- end
95
-
96
- # Optional "IALA " prefix.
97
- rule(:publisher) do
98
- (str("IALA") >> space).maybe
99
- end
100
-
101
- rule(:identifier) do
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
118
- end
119
-
120
- # Parse a string and return the raw parslet tree.
121
- # @param string [String]
122
- # @return [Hash]
123
- def self.parse(string)
124
- unless string.is_a?(String)
125
- raise ::Pubid::Errors::InvalidInputError,
126
- ::Pubid::INPUT_NOT_A_STRING_MESSAGE
127
- end
128
-
129
- if string.length > ::Pubid::MAX_INPUT_LENGTH
130
- raise ::Pubid::Errors::InvalidInputError,
131
- ::Pubid::INPUT_TOO_LONG_MESSAGE
132
- end
133
-
134
- new.parse(string.strip)
135
- end
16
+ class Parser
136
17
  end
137
18
  end
138
19
  end
@@ -132,7 +132,7 @@ module Pubid
132
132
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
133
133
  end
134
134
 
135
- parsed = Pubid::Parg::Backend.parse(:iana, identifier)
135
+ parsed = Pubid::Parg::Model.bound(:iana, identifier)
136
136
  Builder.build(parsed)
137
137
  end
138
138
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Iana
7
- # Parslet grammar for IANA registry identifiers.
5
+ # Grammar for IANA registry identifiers.
8
6
  #
9
7
  # Accepts the authoritative printed form "IANA <slug>[/<sub-slug>]" as well
10
8
  # as a bare index-key slug (no "IANA " prefix). The optional prefix is
@@ -12,27 +10,7 @@ module Pubid
12
10
  #
13
11
  # Slugs use only [a-zA-Z0-9._-] and contain at most one "/", so a plain
14
12
  # character-class repeat parses them; there is no ReDoS surface.
15
- class Parser < ::Pubid::Parser::Grammar
16
- # A single registry slug: letters, digits, dot, underscore, hyphen.
17
- rule(:slug) { match['a-zA-Z0-9._\-'].repeat(1) }
18
-
19
- # Optional publisher prefix. The trailing space is required, so a
20
- # space-free slug never triggers it; PEG `.maybe` backtracks the whole
21
- # sequence if the space is absent (e.g. a slug that happens to start
22
- # "IANA...").
23
- rule(:iana_prefix) { (str("IANA") >> str(" ")).maybe }
24
-
25
- rule(:identifier) do
26
- iana_prefix >>
27
- slug.as(:registry) >>
28
- (str("/") >> slug.as(:sub_registry)).maybe
29
- end
30
-
31
- rule(:root) { identifier }
32
-
33
- def self.parse(input)
34
- new.parse(input)
35
- end
13
+ class Parser
36
14
  end
37
15
  end
38
16
  end
@@ -817,14 +817,86 @@ module Pubid
817
817
  # consumed by the renderer, not the rendering context — and some flavors
818
818
  # override build_rendering_context with a strict signature, so strip them
819
819
  # here rather than widening that signature.
820
- ctx_opts = opts.except(:trademark, :with_volume)
820
+ # `:with_publisher` is a render-time flag too. It reaches the renderer
821
+ # only for the human format and only when given, so a renderer that
822
+ # reads it (BIPM, NIST) sees it and the default call is unchanged.
823
+ with_publisher = opts[:with_publisher]
824
+ ctx_opts = opts.except(:trademark, :with_volume, :with_publisher)
821
825
  context = build_rendering_context(renderer, format:, **ctx_opts)
822
826
  render_opts = opts.slice(:with_edition, :trademark, :with_volume)
827
+ if format == :human && !with_publisher.nil?
828
+ render_opts[:with_publisher] = with_publisher
829
+ end
823
830
  result = renderer.new(self).render(context:, **render_opts)
831
+ result = apply_with_publisher(result, format, with_publisher)
824
832
 
825
833
  annotate_rendered(result, format, ctx_opts[:annotated], context)
826
834
  end
827
835
 
836
+ # The publisher tokens this identifier's human rendering can start with,
837
+ # for `to_s(with_publisher: false)` ({Renderers::PublisherStripper}).
838
+ #
839
+ # The default is the rendered publisher, and the publisher joined by `/`
840
+ # to its copublishers in either order, so a co-published `ISO/IEC 27001`
841
+ # or `ANSI/AMCA 210-16` drops the whole group. Copublishers come from the
842
+ # shared `copublishers` collection or from a flavor's own `copublisher`
843
+ # (a String or an Array of them: AMCA, ASHRAE, IEEE). A wrapper with no
844
+ # publisher of its own prints the document it wraps, so it asks `base`.
845
+ #
846
+ # A flavor overrides this where the generic answer is wrong: where the
847
+ # leading token is a series that belongs to the designation (`BS 8611`,
848
+ # `GB/T 20223`, `ECMA-262`), the answer is `[]`; where the publisher is
849
+ # welded to another token (`ITU-T`), the answer is the welded group.
850
+ #
851
+ # @return [Array<String>]
852
+ def publisher_prefixes
853
+ pubs = publisher_texts(:publisher)
854
+ copubs = publisher_texts(:copublishers) + publisher_texts(:copublisher)
855
+ # A supplement can name its own publisher while the co-published group
856
+ # lives on the document it supplements, so `base` is always asked too.
857
+ # The stripper tries the longest prefix first, so order does not matter.
858
+ publisher_groups(pubs, copubs) + base_publisher_prefixes
859
+ end
860
+
861
+ # Each publisher alone, and the `/`-joined group in either order.
862
+ def publisher_groups(pubs, copubs)
863
+ return pubs + copubs if pubs.empty? || copubs.empty?
864
+
865
+ [[*pubs, *copubs].join("/"), [*copubs, *pubs].join("/"), *pubs, *copubs]
866
+ end
867
+ private :publisher_groups
868
+
869
+ # The printed form of a publisher-like attribute: a component, a String,
870
+ # or an Array of either (IEEE `DualPublished` returns an Array).
871
+ def publisher_texts(name)
872
+ return [] unless respond_to?(name)
873
+
874
+ value = public_send(name)
875
+ values = value.is_a?(Array) ? value : [value]
876
+ values.compact.map { |v| v.to_s.strip }.reject(&:empty?)
877
+ end
878
+ private :publisher_texts
879
+
880
+ def base_publisher_prefixes
881
+ nested = respond_to?(:base) ? base : nil
882
+ return [] unless nested.is_a?(::Pubid::Identifier) && !nested.equal?(self)
883
+
884
+ nested.publisher_prefixes
885
+ end
886
+ private :base_publisher_prefixes
887
+
888
+ # `with_publisher: false` on the human format drops the leading publisher
889
+ # token. Every other value, and every other format, leaves the string as
890
+ # the renderer produced it — `true` is the default rendering for every
891
+ # flavor whose default carries the prefix, and a flavor whose default
892
+ # omits it (3GPP, OGC, IETF) handles `true` itself.
893
+ def apply_with_publisher(result, format, with_publisher)
894
+ return result unless with_publisher == false && format == :human
895
+
896
+ Renderers::PublisherStripper.new(self).strip(result)
897
+ end
898
+ private :apply_with_publisher
899
+
828
900
  # Apply semantic spans when the renderer did not.
829
901
  #
830
902
  # `context.annotated` reaches all 39 flavor renderers and all 39 ignore it,
@@ -867,7 +939,12 @@ module Pubid
867
939
  # @param rendered [String] the plain rendering this method produced
868
940
  # @param opts [Hash] the render options it was called with
869
941
  # @return [String] annotated when `annotated:` was asked for, else as-is
942
+ #
943
+ # `with_publisher: false` is applied here too, before the annotation, so a
944
+ # hand-composed `to_s` drops its publisher token the same way {#render}
945
+ # does.
870
946
  def annotate_plain_render(rendered, **opts)
947
+ rendered = apply_with_publisher(rendered, :human, opts[:with_publisher])
871
948
  annotate_rendered(rendered, :human, opts[:annotated], nil)
872
949
  end
873
950
  protected :annotate_plain_render
@@ -13,6 +13,12 @@ module Pubid
13
13
  }.freeze
14
14
 
15
15
  def build(parsed_hash)
16
+ parsed_hash = split_supplement_base(parsed_hash)
17
+ # The Parslet tree carried a zero-width type_with_stage (an empty
18
+ # capture when no type is spelled) whose cast drove the default
19
+ # stage; the artifact tree omits the key entirely — restore the
20
+ # default input.
21
+ parsed_hash[:type_with_stage] ||= ""
16
22
  all_parts = parsed_hash.delete(:all_parts)
17
23
  typed_stage = Pubid::Idf.locate_stage(parsed_hash[:type_with_stage])
18
24
  identifier = Pubid::Idf.locate_type(typed_stage.type_code).new
@@ -32,8 +38,44 @@ module Pubid
32
38
  all_parts ? identifier.to_all_parts : identifier
33
39
  end
34
40
 
41
+ # The bound grammar flattens the supplement's base wrapper (the
42
+ # bindings cannot nest): the supplement layer keeps the canonical
43
+ # number_with_part/date/type_with_stage captures and the base
44
+ # portion captures under base_* names. Rebuild the nested shape
45
+ # the builder has always consumed — the base_* keys rename onto
46
+ # their canonical counterparts and the shared publisher capture
47
+ # belongs to the base (the tree backend captured it inside the
48
+ # base subtree, so the supplement itself carries no publisher).
49
+ BASE_KEY_MAP = {
50
+ base_number_with_part: :number_with_part,
51
+ base_date: :date,
52
+ base_type_with_stage: :type_with_stage,
53
+ base_stage_iteration: :stage_iteration,
54
+ }.freeze
55
+
35
56
  private
36
57
 
58
+ def split_supplement_base(parsed_hash)
59
+ unless BASE_KEY_MAP.keys.any? { |k| parsed_hash.key?(k) }
60
+ return parsed_hash
61
+ end
62
+
63
+ base_hash = {}
64
+ BASE_KEY_MAP.each do |bound_key, tree_key|
65
+ next unless parsed_hash.key?(bound_key)
66
+
67
+ base_hash[tree_key] = parsed_hash.delete(bound_key)
68
+ end
69
+ if parsed_hash[:publisher]
70
+ base_hash[:publisher] = parsed_hash.delete(:publisher)
71
+ end
72
+ # Keep the base as a raw hash: the shared assign_attributes loop
73
+ # routes :base through cast, which recursively builds it — the
74
+ # same path the tree backend's nested subtree took.
75
+ parsed_hash[:base] = base_hash
76
+ parsed_hash
77
+ end
78
+
37
79
  def default_identifier_class
38
80
  Identifiers::InternationalStandard
39
81
  end
@@ -83,7 +83,7 @@ module Pubid
83
83
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
84
84
  end
85
85
 
86
- parsed = Pubid::Idf::Parser.new.parse(string)
86
+ parsed = Pubid::Parg::Model.bound(:idf, string)
87
87
  Pubid::Idf::Builder.new.build(parsed)
88
88
  end
89
89
  end
@@ -1,144 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Idf
7
- class Parser < ::Pubid::Parser::Grammar
8
- include ::Pubid::Parser::CommonParseRules
9
- include ::Pubid::Parser::CommonParseMethods
10
-
11
- # We need to sort by length to match longest first because that's how Parslet works
12
- TYPED_STAGES = Pubid::Idf.all_typed_stages
13
- .map(&:abbr).flatten.sort_by(&:length).reverse
14
- TYPED_STAGES_SUPPLEMENTS = Pubid::Idf.all_typed_stages
15
- .select { |ts| %w[amd cor].include?(ts.type_code.to_s) }
16
- .map(&:abbr).flatten.sort_by(&:length).reverse
17
-
18
- root :identifier
19
-
20
- rule(:identifier) do
21
- supplement_identifier |
22
- identifier_publisher
23
- end
24
-
25
- # IDF 125A:1988
26
- rule(:prefix_sole_publisher) do
27
- str("IDF").as(:publisher)
28
- end
29
-
30
- rule(:number_with_part) do
31
- (number >> part_and_subpart.maybe).as(:number_with_part)
32
- end
33
-
34
- # IDF identifiers can contain alphanumeric characters
35
- # IDF 125A:1988
36
- rule(:number) do
37
- match('\w').repeat(1, 5)
38
- end
39
-
40
- rule(:date) do
41
- (year_digits >>
42
- (dash >> month_digits).maybe >>
43
- (dash >> day_digits).maybe
44
- ).as(:date)
45
- end
46
-
47
- rule(:part_and_subpart) do
48
- # IDF 80-1:2001
49
- # the "space?" needed to handle "ISO/TS 10303- 1751:2014"
50
- dash >> space? >>
51
- # matches a part
52
- match('\w').repeat >>
53
- # matches a subpart, e.g. "A01" or "1-2" (yes we treat {5}-{1-1} as part and subpart)
54
- (dash >> match('\w').repeat).repeat.maybe
55
- end
56
-
57
- rule(:all_parts) do
58
- str("(all parts)").as(:all_parts)
59
- end
60
-
61
- rule(:type_with_stage) do
62
- # IDF/RM 82:2004
63
- # IDF/RM 254:2022
64
- array_to_str(TYPED_STAGES).as(:type_with_stage) >>
65
- (match('\d').as(:stage_iteration) >> space).maybe
66
- end
67
-
68
- rule(:language) do
69
- # IDF/RM 82:2004(en,fr)
70
- str("(") >>
71
- (
72
- # parse 2-char language codes: ru,en,fr,de,ar,es
73
- (match["a-z"].repeat(1) >> str(",").maybe) |
74
- # parse single language codes: R/E/F
75
- (match["EFARDS"] >> str("/").maybe)
76
- ).repeat.as(:languages) >>
77
- str(")")
78
- end
79
-
80
- rule(:stage_iteration) do
81
- # "ISO 11243.1"
82
- str(".") >> match('\d').as(:stage_iteration)
83
- end
84
-
85
- rule(:second_part) do
86
- number_with_part >>
87
- stage_iteration.maybe >>
88
- space? >> (
89
- # :2005-02
90
- str(":") >> date
91
- ).maybe
92
- end
93
-
94
- rule(:third_part) do
95
- # End of the identifier, we can have language, or all parts
96
- space? >> language.maybe >> all_parts.maybe
97
- end
98
-
99
- # prefix sole publishers with type and stage
100
- # IDF/RM 82:2004
101
- rule(:identifier_publisher) do
102
- identifier_publisher_no_third >> third_part
103
- end
104
-
105
- rule(:identifier_publisher_no_third) do
106
- prefix_sole_publisher >> space? >> str("/").maybe >>
107
- type_with_stage.maybe >>
108
- space.maybe >>
109
- second_part
110
- end
111
-
112
- rule(:supplement_type_with_stage) do
113
- # "AMD" or "COR"
114
- array_to_str(TYPED_STAGES_SUPPLEMENTS).as(:type_with_stage)
115
- end
116
-
117
- # IDF 148-1:2008 / COR 1:2009
118
- # IDF 140-1:2007 / AMD1:2012 (no space between AMD and 1)
119
- rule(:supplement_identifier_no_third) do
120
- identifier_publisher_no_third.as(:base) >>
121
- space? >> str("/") >> space? >>
122
- supplement_type_with_stage >>
123
- space.maybe >> second_part
124
- end
125
-
126
- rule(:supplement_identifier) do
127
- supplement_identifier_no_third >> third_part
128
- end
129
-
130
- # # ISO/IEC 13818-1:2015/Amd 3:2016/Cor 1:2017
131
- # # ISO/IEC 19794-7:2014/Amd 1:2015/CD Cor 1
132
- # rule(:supplement_supplement_identifier) do
133
- # supplement_identifier_no_third.as(:base) >>
134
- # str("/") >> supplement_type_with_stage >>
135
- # # digits.as(:stage_iteration).maybe >>
136
- # space? >> second_part >> third_part
137
- # end
138
-
139
- rule(:dash) do
140
- str("-") | str("‑") | str("‐")
141
- end
5
+ class Parser
142
6
  end
143
7
  end
144
8
  end
data/lib/pubid/idf.rb CHANGED
@@ -28,10 +28,9 @@ module Pubid
28
28
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
29
29
  end
30
30
 
31
- parser = Parser.new
32
31
  builder = Builder.new
33
32
 
34
- parsed = parser.parse(identifier)
33
+ parsed = Pubid::Parg::Model.bound(:idf, identifier)
35
34
  builder.build(parsed)
36
35
  end
37
36