pubid 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. data/data/parg/adobe.json +1 -1
  3. data/data/parg/aiee.json +1 -1
  4. data/data/parg/amca.json +1 -1
  5. data/data/parg/ansi.json +1 -1
  6. data/data/parg/api.json +1 -1
  7. data/data/parg/ashrae.json +1 -1
  8. data/data/parg/asme.json +1 -1
  9. data/data/parg/astm.json +1 -1
  10. data/data/parg/bipm.json +1 -1
  11. data/data/parg/bsi.json +1 -1
  12. data/data/parg/calconnect.json +1 -1
  13. data/data/parg/ccsds.json +1 -1
  14. data/data/parg/cen_cenelec.json +1 -1
  15. data/data/parg/cie.json +1 -1
  16. data/data/parg/csa.json +1 -1
  17. data/data/parg/doi.json +1 -1
  18. data/data/parg/easc.json +1 -1
  19. data/data/parg/ecma.json +1 -1
  20. data/data/parg/etsi.json +1 -1
  21. data/data/parg/evs.json +1 -1
  22. data/data/parg/gb.json +1 -1
  23. data/data/parg/gost.json +1 -1
  24. data/data/parg/iala.json +1 -1
  25. data/data/parg/iana.json +1 -1
  26. data/data/parg/idf.json +1 -1
  27. data/data/parg/iec.json +1 -1
  28. data/data/parg/ieee.json +1 -29571
  29. data/data/parg/ietf.json +1 -1
  30. data/data/parg/iho.json +1 -0
  31. data/data/parg/ire.json +1 -1
  32. data/data/parg/isbn.json +1 -1
  33. data/data/parg/iso.json +1 -8510
  34. data/data/parg/itu.json +1 -1
  35. data/data/parg/jcgm.json +1 -1
  36. data/data/parg/jis.json +1 -1
  37. data/data/parg/nesc.json +1 -1
  38. data/data/parg/nist.json +1 -9588
  39. data/data/parg/oasis.json +1 -1
  40. data/data/parg/ogc.json +1 -1
  41. data/data/parg/oiml.json +1 -1
  42. data/data/parg/omg.json +1 -1
  43. data/data/parg/parg.json +1 -1
  44. data/data/parg/plateau.json +1 -1
  45. data/data/parg/sae.json +1 -1
  46. data/data/parg/tables/typed_stages.yaml +1 -0
  47. data/data/parg/tgpp.json +1 -1
  48. data/data/parg/un.json +1 -1
  49. data/data/parg/w3c.json +1 -1
  50. data/data/parg/xsf.json +1 -1
  51. data/lib/pubid/adobe/identifier.rb +1 -1
  52. data/lib/pubid/adobe/parser.rb +2 -113
  53. data/lib/pubid/amca/identifiers/base.rb +1 -1
  54. data/lib/pubid/amca/parser.rb +1 -145
  55. data/lib/pubid/ansi/identifier.rb +1 -1
  56. data/lib/pubid/ansi/parser.rb +1 -82
  57. data/lib/pubid/api/builder.rb +32 -1
  58. data/lib/pubid/api/identifier.rb +1 -1
  59. data/lib/pubid/api/parser.rb +1 -131
  60. data/lib/pubid/ashrae/identifiers/base.rb +1 -1
  61. data/lib/pubid/ashrae/parser.rb +3 -710
  62. data/lib/pubid/asme/builder.rb +6 -4
  63. data/lib/pubid/asme/identifier.rb +1 -2
  64. data/lib/pubid/asme/parser.rb +1 -328
  65. data/lib/pubid/astm/builder.rb +12 -3
  66. data/lib/pubid/astm/identifier.rb +1 -2
  67. data/lib/pubid/astm/parser.rb +1 -227
  68. data/lib/pubid/bipm/identifier.rb +8 -1
  69. data/lib/pubid/bipm/parser.rb +2 -224
  70. data/lib/pubid/bsi/builder.rb +2 -4
  71. data/lib/pubid/bsi/parser.rb +1 -666
  72. data/lib/pubid/bsi/single_identifier.rb +38 -1
  73. data/lib/pubid/calconnect/identifier.rb +1 -1
  74. data/lib/pubid/calconnect/parser.rb +2 -40
  75. data/lib/pubid/ccsds/identifier.rb +1 -1
  76. data/lib/pubid/ccsds/parser.rb +1 -66
  77. data/lib/pubid/cen_cenelec/identifier.rb +16 -1
  78. data/lib/pubid/cen_cenelec/parser.rb +3 -136
  79. data/lib/pubid/cie/identifier.rb +1 -1
  80. data/lib/pubid/cie/parser.rb +3 -378
  81. data/lib/pubid/conformance.rb +1 -1
  82. data/lib/pubid/csa/builder.rb +41 -18
  83. data/lib/pubid/csa/identifier.rb +12 -4
  84. data/lib/pubid/csa/identifiers/amendment.rb +28 -0
  85. data/lib/pubid/csa/identifiers.rb +1 -0
  86. data/lib/pubid/csa/parser.rb +16 -397
  87. data/lib/pubid/doi/identifier.rb +1 -1
  88. data/lib/pubid/doi/parser.rb +2 -37
  89. data/lib/pubid/easc/builder.rb +1 -1
  90. data/lib/pubid/easc/identifier.rb +1 -1
  91. data/lib/pubid/easc/parser.rb +2 -70
  92. data/lib/pubid/ecma/CLAUDE.md +8 -1
  93. data/lib/pubid/ecma/identifier.rb +13 -8
  94. data/lib/pubid/ecma/parser.rb +2 -54
  95. data/lib/pubid/ecma/renderer.rb +3 -7
  96. data/lib/pubid/errors.rb +18 -13
  97. data/lib/pubid/etsi/builder.rb +8 -5
  98. data/lib/pubid/etsi/identifiers/base.rb +1 -1
  99. data/lib/pubid/etsi/parser.rb +1 -132
  100. data/lib/pubid/evs/identifier.rb +7 -0
  101. data/lib/pubid/evs/parser.rb +1 -8
  102. data/lib/pubid/evs.rb +1 -1
  103. data/lib/pubid/gb/identifier.rb +7 -1
  104. data/lib/pubid/gb/parser.rb +2 -51
  105. data/lib/pubid/gost/identifier.rb +9 -1
  106. data/lib/pubid/gost/parser.rb +4 -81
  107. data/lib/pubid/iala/parser.rb +2 -121
  108. data/lib/pubid/iana/identifier.rb +1 -1
  109. data/lib/pubid/iana/parser.rb +2 -24
  110. data/lib/pubid/identifier.rb +78 -1
  111. data/lib/pubid/idf/builder.rb +42 -0
  112. data/lib/pubid/idf/identifier.rb +1 -1
  113. data/lib/pubid/idf/parser.rb +1 -137
  114. data/lib/pubid/idf.rb +1 -2
  115. data/lib/pubid/iec/builder.rb +158 -50
  116. data/lib/pubid/iec/identifier.rb +1 -1
  117. data/lib/pubid/iec/parser.rb +3 -430
  118. data/lib/pubid/ieee/aiee/identifier.rb +2 -1
  119. data/lib/pubid/ieee/aiee.rb +0 -1
  120. data/lib/pubid/ieee/builder.rb +2 -2
  121. data/lib/pubid/ieee/identifiers/base.rb +1 -1
  122. data/lib/pubid/ieee/ire/identifier.rb +2 -1
  123. data/lib/pubid/ieee/ire.rb +0 -1
  124. data/lib/pubid/ieee/nesc.rb +0 -1
  125. data/lib/pubid/ieee/parser.rb +1 -1257
  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 +89 -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 +11 -1
  134. data/lib/pubid/iho/urn_parser.rb +21 -11
  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/builder.rb +1 -1
  139. data/lib/pubid/iso/identifiers/directives.rb +6 -3
  140. data/lib/pubid/iso/identifiers/guide.rb +6 -3
  141. data/lib/pubid/iso/identifiers/international_standard.rb +3 -1
  142. data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +6 -3
  143. data/lib/pubid/iso/normalizer.rb +1 -1
  144. data/lib/pubid/iso/parser.rb +1 -539
  145. data/lib/pubid/iso/supplement_identifier.rb +6 -3
  146. data/lib/pubid/iso/utilities.rb +4 -5
  147. data/lib/pubid/iso.rb +1 -19
  148. data/lib/pubid/itu/builder.rb +36 -20
  149. data/lib/pubid/itu/identifiers/base.rb +15 -2
  150. data/lib/pubid/itu/parser.rb +1 -775
  151. data/lib/pubid/jcgm/builder.rb +35 -0
  152. data/lib/pubid/jcgm/parser.rb +1 -151
  153. data/lib/pubid/jcgm.rb +1 -2
  154. data/lib/pubid/jis/builder.rb +18 -29
  155. data/lib/pubid/jis/identifier.rb +1 -1
  156. data/lib/pubid/jis/parser.rb +1 -128
  157. data/lib/pubid/nist/builder.rb +5 -1
  158. data/lib/pubid/nist/caster.rb +13 -3
  159. data/lib/pubid/nist/identifiers/base.rb +37 -2
  160. data/lib/pubid/nist/identifiers/circular.rb +2 -2
  161. data/lib/pubid/nist/identifiers/circular_supplement.rb +2 -2
  162. data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +2 -2
  163. data/lib/pubid/nist/identifiers/crpl_report.rb +2 -2
  164. data/lib/pubid/nist/identifiers/handbook.rb +2 -2
  165. data/lib/pubid/nist/identifiers/internal_report.rb +2 -2
  166. data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
  167. data/lib/pubid/nist/identifiers/monograph.rb +2 -2
  168. data/lib/pubid/nist/identifiers/report.rb +2 -2
  169. data/lib/pubid/nist/parser.rb +19 -775
  170. data/lib/pubid/nist/router.rb +4 -1
  171. data/lib/pubid/nist/supplement_identifier.rb +2 -2
  172. data/lib/pubid/oasis/identifier.rb +1 -1
  173. data/lib/pubid/oasis/parser.rb +2 -16
  174. data/lib/pubid/ogc/identifier.rb +1 -1
  175. data/lib/pubid/ogc/parser.rb +2 -26
  176. data/lib/pubid/oiml/identifiers/certification_system.rb +7 -0
  177. data/lib/pubid/oiml/identifiers/dual_published.rb +2 -2
  178. data/lib/pubid/oiml/parser.rb +1 -303
  179. data/lib/pubid/oiml.rb +1 -2
  180. data/lib/pubid/omg/identifier.rb +6 -1
  181. data/lib/pubid/omg/parser.rb +2 -76
  182. data/lib/pubid/parg/artifact.rb +22 -3
  183. data/lib/pubid/parg/model.rb +98 -0
  184. data/lib/pubid/parg.rb +1 -0
  185. data/lib/pubid/parser.rb +5 -4
  186. data/lib/pubid/parsers/mr_string.rb +1 -1
  187. data/lib/pubid/plateau/parser.rb +1 -66
  188. data/lib/pubid/plateau.rb +1 -2
  189. data/lib/pubid/renderers/directives_renderer.rb +1 -1
  190. data/lib/pubid/renderers/iwa_renderer.rb +1 -1
  191. data/lib/pubid/renderers/publisher_stripper.rb +82 -0
  192. data/lib/pubid/renderers/supplement_renderer.rb +1 -1
  193. data/lib/pubid/renderers.rb +1 -0
  194. data/lib/pubid/sae/identifiers/base.rb +1 -1
  195. data/lib/pubid/sae/parser.rb +1 -48
  196. data/lib/pubid/tgpp/identifier.rb +1 -1
  197. data/lib/pubid/tgpp/parser.rb +2 -59
  198. data/lib/pubid/un/identifier.rb +1 -1
  199. data/lib/pubid/un/parser.rb +1 -18
  200. data/lib/pubid/version.rb +1 -1
  201. data/lib/pubid/w3c/identifier.rb +1 -1
  202. data/lib/pubid/w3c/parser.rb +2 -27
  203. data/lib/pubid/xsf/identifier.rb +11 -7
  204. data/lib/pubid/xsf/parser.rb +3 -31
  205. data/lib/pubid/xsf/renderer.rb +2 -4
  206. data/lib/pubid.rb +8 -9
  207. metadata +15 -25
  208. data/lib/pubid/ieee/aiee/parser.rb +0 -130
  209. data/lib/pubid/ieee/ire/parser.rb +0 -109
  210. data/lib/pubid/ieee/nesc/parser.rb +0 -155
  211. data/lib/pubid/iho/parser.rb +0 -68
  212. data/lib/pubid/parser/common_parse_methods.rb +0 -13
  213. data/lib/pubid/parser/common_parse_rules.rb +0 -56
  214. data/lib/pubid/parser/grammar.rb +0 -96
@@ -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