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
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Parg
5
+ # Typed model binding for one artifact entry point. The grammar declares
6
+ # which captures bind to which attributes (its `bindings` block); this
7
+ # module parses through those bindings — either straight into the
8
+ # flavor's model class (`parse`) or into the builder-ready hash its
9
+ # Builder consumes (`bound`) — so consumers never see the wire tree.
10
+ module Model
11
+ ALL_PARTS_SUFFIX = "(all parts)"
12
+ ALL_PARTS_PATTERN = /\s*\(all parts\)\s*\z/
13
+
14
+ @registered = {}
15
+ @mutex = Mutex.new
16
+
17
+ class << self
18
+ # Parse +input+ through the artifact's declared bindings into the
19
+ # builder-ready attribute hash, without materializing a model. For
20
+ # flavors whose Builder still owns type dispatch and component
21
+ # construction: the bindings replace tree normalization, not the
22
+ # builder. The "(all parts)" suffix keeps its contract — stripped
23
+ # before parsing, marked on the hash for the builder's AllPartsWrap.
24
+ def bound(flavor, input, entry: "identifier")
25
+ ensure_bindings!(flavor, entry)
26
+ all_parts = input.end_with?(ALL_PARTS_SUFFIX)
27
+ source = all_parts ? input.sub(ALL_PARTS_PATTERN, "") : input
28
+ hash = Artifact.for(flavor).parse_and_bind(entry, source)
29
+ all_parts ? Backend.mark_all_parts(hash) : hash
30
+ rescue Parsanol::ParseFailed => e
31
+ parse_error(e, input, flavor)
32
+ end
33
+
34
+ # Parse +input+ into +model_class+ through the artifact's declared
35
+ # bindings. The "(all parts)" suffix is stripped before parsing and
36
+ # re-applied to the model, exactly as the tree backend does.
37
+ def parse(flavor, model_class, input, entry: "identifier")
38
+ register(flavor, model_class, entry)
39
+ model, all_parts = materialize(flavor, model_class, input)
40
+ all_parts ? model.to_all_parts : model
41
+ rescue Parsanol::ParseFailed => e
42
+ parse_error(e, input, flavor)
43
+ end
44
+
45
+ private
46
+
47
+ def parse_error(error, input, flavor)
48
+ raise Pubid::Errors::ParseError.new(
49
+ error.message,
50
+ nil,
51
+ input: input,
52
+ flavor: Backend.registered_name(flavor),
53
+ )
54
+ end
55
+
56
+ def materialize(flavor, model_class, input)
57
+ all_parts = input.end_with?(ALL_PARTS_SUFFIX)
58
+ source = all_parts ? input.sub(ALL_PARTS_PATTERN, "") : input
59
+ [model_class.public_send(:"from_#{format_for(flavor)}", source),
60
+ all_parts]
61
+ end
62
+
63
+ def register(flavor, model_class, entry)
64
+ format = format_for(flavor)
65
+ @mutex.synchronize do
66
+ return if @registered[format] == model_class
67
+
68
+ ensure_bindings!(flavor, entry)
69
+ bind!(flavor, model_class, format, entry)
70
+ @registered[format] = model_class
71
+ end
72
+ end
73
+
74
+ def bind!(flavor, model_class, format, entry)
75
+ Parsanol::PARG::Lutaml.register(
76
+ model_class,
77
+ format_name: format,
78
+ artifact: Artifact.path_for(flavor),
79
+ entry: entry,
80
+ tables_dir: Artifact::TABLES_DIR,
81
+ )
82
+ end
83
+
84
+ def ensure_bindings!(flavor, entry)
85
+ return unless Artifact.for(flavor).bindings(entry).empty?
86
+
87
+ raise Pubid::Errors::ParseError,
88
+ "PG artifact for #{flavor} declares no bindings " \
89
+ "for entry #{entry}"
90
+ end
91
+
92
+ def format_for(flavor)
93
+ :"pg_#{flavor}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/pubid/parg.rb CHANGED
@@ -4,5 +4,6 @@ module Pubid
4
4
  module Parg
5
5
  autoload :Artifact, "pubid/parg/artifact"
6
6
  autoload :Backend, "pubid/parg/backend"
7
+ autoload :Model, "pubid/parg/model"
7
8
  end
8
9
  end
data/lib/pubid/parser.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pubid"
4
3
  module Pubid
5
4
  module Parser
6
- autoload :Grammar, "pubid/parser/grammar"
7
- autoload :CommonParseMethods, "pubid/parser/common_parse_methods"
8
- autoload :CommonParseRules, "pubid/parser/common_parse_rules"
5
+ # A trailing "(all parts)" marks the reference as the whole document.
6
+ # NIST's parse path strips it before preprocessing and carries the
7
+ # marker into the tree (:all_parts) for the builder to wrap; the
8
+ # shared Builder::Base reads the same marker for every other flavor.
9
+ ALL_PARTS_SUFFIX = "(all parts)".freeze
9
10
  end
10
11
  end
@@ -116,7 +116,7 @@ module Pubid
116
116
  # failure — the class the cross-flavor contract requires, and the one
117
117
  # `Pubid.parse` treats as "try the next flavor".
118
118
  if identifier_string == mr_string && flavor_module == Pubid::Iso
119
- raise Parslet::ParseFailed,
119
+ raise Pubid::Errors::ParseError,
120
120
  "#{mr_string.inspect} is not an MR string"
121
121
  end
122
122
 
@@ -2,72 +2,7 @@
2
2
 
3
3
  module Pubid
4
4
  module Plateau
5
- class Parser < ::Pubid::Parser::Grammar
6
- rule(:space) { str(" ") }
7
- rule(:dash) { str("-") }
8
- rule(:hash) { str("#") }
9
-
10
- rule(:digit) { match["0-9"] }
11
- rule(:digits) { digit.repeat(1) }
12
-
13
- # PLATEAU
14
- rule(:publisher) { str("PLATEAU") }
15
-
16
- # Handbook or Technical Report
17
- rule(:doc_type) do
18
- (str("Handbook") | str("Technical Report")).as(:type)
19
- end
20
-
21
- # Number: #00, #01, #46, etc.
22
- rule(:number) { hash >> digits.as(:number) }
23
-
24
- # Annex: -1, -2, etc. Legacy Latin references use "_" as the sub-number
25
- # separator (e.g. "#46_1"); both render as the canonical dash.
26
- rule(:annex) { (dash | str("_")) >> digits.as(:annex) }
27
-
28
- # Edition (Handbook only). Canonical form is 第X.Y版; legacy Latin
29
- # references write the bare X.Y (e.g. "PLATEAU Handbook #00 1.0"). Both
30
- # capture the same numeric :edition slice, so they build the same object
31
- # and render to the canonical 第X.Y版.
32
- rule(:edition_part) do
33
- str("第") >>
34
- (digits >> str(".") >> digits).as(:edition) >>
35
- str("版")
36
- end
37
-
38
- rule(:edition_latin) { (digits >> str(".") >> digits).as(:edition) }
39
-
40
- rule(:edition) { space >> (edition_part | edition_latin) }
41
-
42
- # Annex supplement: "Annex A", "Annex B", etc.
43
- rule(:annex_letter) { match["A-Z"].as(:annex_letter) }
44
- rule(:annex_supplement) do
45
- space >> str("Annex") >> space >> annex_letter
46
- end
47
-
48
- # Full identifier patterns
49
- rule(:handbook) do
50
- publisher >> space >> doc_type >> space >>
51
- number >> annex.maybe >> edition.maybe
52
- end
53
-
54
- # Technical Report has no edition. A legacy Latin TR reference carrying a
55
- # trailing edition (e.g. "#00 1.0") is matched by the `handbook` rule
56
- # (doc_type matches either keyword, tried first); the builder dispatches
57
- # on :type and drops the captured edition for Technical Report.
58
- rule(:technical_report) do
59
- publisher >> space >> doc_type >> space >>
60
- number >> annex.maybe
61
- end
62
-
63
- # Annex identifier (supplement)
64
- rule(:annex_identifier) do
65
- (handbook | technical_report).as(:base) >> annex_supplement
66
- end
67
-
68
- rule(:identifier) { annex_identifier | handbook | technical_report }
69
-
70
- rule(:root) { identifier }
5
+ class Parser
71
6
  end
72
7
  end
73
8
  end
data/lib/pubid/plateau.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pubid"
4
- require "parslet"
5
4
 
6
5
  module Pubid
7
6
  module Plateau
@@ -31,7 +30,7 @@ module Pubid
31
30
 
32
31
  # Apply legacy update_codes normalization first
33
32
  normalized = Core::UpdateCodes.apply(input, :plateau)
34
- parsed = Pubid::Parg::Backend.parse(:plateau, normalized)
33
+ parsed = Pubid::Parg::Model.bound(:plateau, normalized)
35
34
  Builder.build(parsed)
36
35
  end
37
36
 
@@ -3,7 +3,7 @@
3
3
  module Pubid
4
4
  module Renderers
5
5
  class DirectivesRenderer < HumanReadable
6
- def render(context:, with_edition: false)
6
+ def render(context:, with_edition: false, **_opts)
7
7
  parts = []
8
8
  parts << render_publisher_portion(context)
9
9
  num_port = render_number_portion(context)
@@ -3,7 +3,7 @@
3
3
  module Pubid
4
4
  module Renderers
5
5
  class IwaRenderer < HumanReadable
6
- def render(context:, with_edition: false)
6
+ def render(context:, with_edition: false, **_opts)
7
7
  parts = []
8
8
  if @id.typed_stage
9
9
  parts << annotate(@id.typed_stage.render(context:),
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pubid
4
+ module Renderers
5
+ # Removes the leading publisher token from an already-rendered identifier,
6
+ # for `to_s(with_publisher: false)`.
7
+ #
8
+ # relaton#49 asks for an internal form of every identifier: the form an SDO
9
+ # uses when it cites its own documents, with the publisher prefix dropped
10
+ # (`NIST SP 800-113` -> `SP 800-113`). Teaching 40-odd flavor renderers to
11
+ # omit the publisher would mean 40 chances to drift, and a flavor added
12
+ # tomorrow would get nothing. This class works from the OUTSIDE instead,
13
+ # the way {Annotator} does: the identifier names the prefixes its rendering
14
+ # can start with (`Pubid::Identifier#publisher_prefixes`), and the prefix
15
+ # is cut off together with the one separator that follows it.
16
+ #
17
+ # Deliberate limits:
18
+ #
19
+ # * A prefix must be followed by a separator (space, `-`, `/` or `.`), so
20
+ # `ISO` never cuts into `ISOFIX`.
21
+ # * When no prefix matches, or cutting it would leave nothing, the input
22
+ # comes back unchanged: the option is ignored rather than producing a
23
+ # broken identifier.
24
+ class PublisherStripper
25
+ SEPARATOR = %r{\A(?:\s+|[-/.]\s*)}
26
+
27
+ def initialize(identifier)
28
+ @id = identifier
29
+ end
30
+
31
+ # A co-published group is not always joined by `/`: CIE prints
32
+ # `CIE ISO 10916:2024`. The cut therefore repeats while a publisher
33
+ # token still leads, so the group drops whole whatever its joiner.
34
+ # The cap only guards against a cycle; no corpus id has more than three.
35
+ MAX_CUTS = 4
36
+
37
+ # @param rendered [String] the default human rendering of the identifier
38
+ # @return [String] the rendering without its leading publisher token
39
+ def strip(rendered)
40
+ return rendered unless rendered.is_a?(String)
41
+
42
+ candidates = prefixes
43
+ MAX_CUTS.times do
44
+ stripped = cut_once(rendered, candidates)
45
+ break if stripped.equal?(rendered)
46
+
47
+ rendered = stripped
48
+ end
49
+ rendered
50
+ end
51
+
52
+ private
53
+
54
+ def cut_once(rendered, candidates)
55
+ candidates.each do |prefix|
56
+ next unless rendered.start_with?(prefix)
57
+
58
+ rest = rendered[prefix.length..]
59
+ next unless rest.match?(SEPARATOR)
60
+
61
+ stripped = rest.sub(SEPARATOR, "")
62
+ return stripped unless stripped.empty?
63
+ end
64
+
65
+ rendered
66
+ end
67
+
68
+ # Longest first, so `ISO/IEC` wins over `ISO`. Each prefix also comes in
69
+ # its annotated spelling: ISO annotates inside its own renderer, so its
70
+ # string already reads `<span class="publisher">ISO</span> …` when it
71
+ # reaches this class.
72
+ def prefixes
73
+ Array(@id.publisher_prefixes)
74
+ .map { |p| p.to_s.strip }
75
+ .reject(&:empty?)
76
+ .flat_map { |p| [%(<span class="publisher">#{p}</span>), p] }
77
+ .uniq
78
+ .sort_by { |p| -p.length }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -3,7 +3,7 @@
3
3
  module Pubid
4
4
  module Renderers
5
5
  class SupplementRenderer < HumanReadable
6
- def render(context:, with_edition: false)
6
+ def render(context:, with_edition: false, **_opts)
7
7
  parts = []
8
8
 
9
9
  base_str = @id.base.to_s(
@@ -3,6 +3,7 @@
3
3
  module Pubid
4
4
  module Renderers
5
5
  autoload :Annotator, "pubid/renderers/annotator"
6
+ autoload :PublisherStripper, "pubid/renderers/publisher_stripper"
6
7
  autoload :Base, "pubid/renderers/base"
7
8
  autoload :DirectivesRenderer, "pubid/renderers/directives_renderer"
8
9
  autoload :GuideRenderer, "pubid/renderers/guide_renderer"
@@ -16,7 +16,7 @@ module Pubid
16
16
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
17
17
  end
18
18
 
19
- parsed = Pubid::Parg::Backend.parse(:sae, input)
19
+ parsed = Pubid::Parg::Model.bound(:sae, input)
20
20
  Builder.build(parsed)
21
21
  end
22
22
 
@@ -1,55 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Sae
7
- class Parser < ::Pubid::Parser::Grammar
8
- rule(:space) { str(" ") }
9
- rule(:digit) { match["0-9"] }
10
- rule(:digits) { digit.repeat(1) }
11
- rule(:letter) { match["A-Z"] }
12
- rule(:colon) { str(":") }
13
-
14
- # Publisher
15
- rule(:publisher) { str("SAE").as(:publisher) }
16
-
17
- # Document types with optional number suffix (e.g., J300, J308A)
18
- rule(:doc_type) do
19
- (
20
- str("AMS") | # Aerospace Material Specification
21
- str("AIR") | # Aerospace Information Report
22
- str("ARP") | # Aerospace Recommended Practice
23
- str("AS") | # Aerospace Standard
24
- str("J") | # SAE Standard (e.g., J300)
25
- str("MA") # Material Advisory
26
- ).as(:type)
27
- end
28
-
29
- # Number with optional letter prefix or suffix
30
- rule(:number) do
31
- ((letter.repeat(1,
32
- 1) >> digits) | (digits >> letter.repeat(0,
33
- 1))).as(:number)
34
- end
35
-
36
- # Year
37
- rule(:year) { digit.repeat(4, 4).as(:year) }
38
-
39
- # Date
40
- rule(:date) { colon >> year }
41
-
42
- # Main identifier
43
- rule(:identifier) do
44
- publisher >> space >> doc_type >> space.maybe >>
45
- number >> date.maybe
46
- end
47
-
48
- root(:identifier)
49
-
50
- def self.parse(input)
51
- new.parse(input)
52
- end
5
+ class Parser
53
6
  end
54
7
  end
55
8
  end
@@ -113,7 +113,7 @@ module Pubid
113
113
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
114
114
  end
115
115
 
116
- parsed = Pubid::Parg::Backend.parse(:tgpp, identifier)
116
+ parsed = Pubid::Parg::Model.bound(:tgpp, identifier)
117
117
  Builder.build(parsed)
118
118
  end
119
119
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Tgpp
7
- # Parslet grammar for 3GPP identifiers.
5
+ # Grammar for 3GPP identifiers.
8
6
  #
9
7
  # Form: `[3GPP ]<TR|TS> <NN.NNN>[suffix][-part…][:<release>][/<version>]`
10
8
  # TS 23.207:REL-4/2.0.0
@@ -17,62 +15,7 @@ module Pubid
17
15
  # document alone ("3GPP TS 23.207"), and relaton parses that bare form to
18
16
  # search the index. Only the type and the dotted number core are required,
19
17
  # so "TS" and "TS foo" are still rejected.
20
- class Parser < ::Pubid::Parser::Grammar
21
- rule(:digit) { match["0-9"] }
22
- rule(:digits) { digit.repeat(1) }
23
- rule(:space) { str(" ") }
24
-
25
- # Optional leading publisher token.
26
- rule(:publisher_prefix) { str("3GPP") >> space }
27
-
28
- # Document type.
29
- rule(:type) { (str("TR") | str("TS")).as(:type) }
30
-
31
- # Dotted number core, kept as a string (preserves leading zeros).
32
- rule(:number_core) { (digits >> str(".") >> digits).as(:number) }
33
-
34
- # Letter suffix directly after the number ("U"/"dcs"/"ext"…). Generic so
35
- # future controlled-vocabulary values keep round-tripping.
36
- rule(:suffix) { match["A-Za-z"].repeat(1).as(:suffix) }
37
-
38
- # Hyphen parts.
39
- rule(:part) { str("-") >> digits.as(:part) }
40
- rule(:parts) { part.repeat(0).as(:parts) }
41
-
42
- # Release token, stored verbatim: everything up to the version separator.
43
- # Excludes ":" (as well as "/") so the ":"-delimited URN scheme can always
44
- # encode it — every real release ("REL-4", "Ph1", "UMTS", "Release 2000")
45
- # is colon-free.
46
- #
47
- # A trailing segment shaped exactly like a version is a mistyped "/"
48
- # separator ("TS 23.207:2.0.0"), not a release: no release in the 88,464
49
- # published index rows contains a dot at all. Reject it rather than
50
- # silently misfiling the version as the release — which only became
51
- # reachable once the version segment was made optional.
52
- rule(:release) do
53
- (version_core >> any.absent?).absent? >>
54
- match["^/:"].repeat(1).as(:release)
55
- end
56
-
57
- # Three-part version, kept as a string.
58
- rule(:version_core) do
59
- digits >> str(".") >> digits >> str(".") >> digits
60
- end
61
- rule(:version) { version_core.as(:version) }
62
-
63
- rule(:identifier) do
64
- publisher_prefix.maybe >>
65
- type >> space >>
66
- number_core >> suffix.maybe >> parts >>
67
- (str(":") >> release).maybe >>
68
- (str("/") >> version).maybe
69
- end
70
-
71
- rule(:root) { identifier }
72
-
73
- def self.parse(input)
74
- new.parse(input)
75
- end
18
+ class Parser
76
19
  end
77
20
  end
78
21
  end
@@ -32,7 +32,7 @@ module Pubid
32
32
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
33
33
  end
34
34
 
35
- parsed = Pubid::Parg::Backend.parse(:un, identifier, merge_top_sequence: false)
35
+ parsed = Pubid::Parg::Model.bound(:un, identifier)
36
36
  Builder.build(parsed)
37
37
  end
38
38
  end
@@ -1,25 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Un
7
- class Parser < ::Pubid::Parser::Grammar
8
- rule(:slash) { str("/") }
9
- rule(:un_prefix) { (str("UN") >> str(" ")).maybe }
10
- rule(:token) { match("[A-Z0-9.]").repeat(1) }
11
-
12
- rule(:identifier) do
13
- un_prefix >>
14
- token.as(:token) >>
15
- (slash >> token.as(:token)).repeat(1)
16
- end
17
-
18
- rule(:root) { identifier }
19
-
20
- def self.parse(input)
21
- new.parse(input)
22
- end
5
+ class Parser
23
6
  end
24
7
  end
25
8
  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"
4
+ VERSION = "2.0.2"
5
5
  end
@@ -122,7 +122,7 @@ module Pubid
122
122
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
123
123
  end
124
124
 
125
- parsed = Pubid::Parg::Backend.parse(:w3c, identifier)
125
+ parsed = Pubid::Parg::Model.bound(:w3c, identifier)
126
126
  Builder.build(parsed)
127
127
  end
128
128
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module W3c
7
- # Parslet grammar for printed W3C identifiers, e.g.
5
+ # Grammar for printed W3C identifiers, e.g.
8
6
  # "W3C WD-charmod-19991129" (type + code + date)
9
7
  # "W3C NOTE-xml-names" (type + code)
10
8
  # "W3C 2dcontext" (bare code)
@@ -14,30 +12,7 @@ module Pubid
14
12
  # greedily consume the trailing date and make a `.maybe` date rule
15
13
  # unreliable. This is safe: no real W3C code ends in a same-width (4/6/8)
16
14
  # digit run, so the Builder's fixed-width trailing-date test never collides.
17
- class Parser < ::Pubid::Parser::Grammar
18
- rule(:prefix) { str("W3C") >> str(" ") }
19
-
20
- # Known W3C maturity tokens. Order matters for shared prefixes: CRD before
21
- # CR, PER before PR, DNOTE before NOTE.
22
- rule(:type_token) do
23
- str("DNOTE") | str("NOTE") | str("WD") | str("CRD") | str("CR") |
24
- str("REC") | str("PER") | str("PR") | str("SPSD") | str("OBSL")
25
- end
26
-
27
- # A leading token is only a type when immediately followed by "-".
28
- rule(:type_part) { type_token.as(:type) >> str("-") }
29
-
30
- # The remainder: code plus an optional trailing date. Captured whole and
31
- # split in the Builder.
32
- rule(:rest) { any.repeat(1).as(:rest) }
33
-
34
- rule(:identifier) { prefix >> type_part.maybe >> rest }
35
-
36
- rule(:root) { identifier }
37
-
38
- def self.parse(input)
39
- new.parse(input)
40
- end
15
+ class Parser
41
16
  end
42
17
  end
43
18
  end
@@ -30,15 +30,19 @@ module Pubid
30
30
  # publisher name in an identifier — the leading token is "XEP".
31
31
  PUBLISHER = "XSF"
32
32
 
33
- attr_reader :with_publisher
34
-
35
- # Basic string representation. Delegates to renderer; `with_publisher:
36
- # false` yields the bare number.
37
- def to_s(with_publisher: true, **opts)
38
- @with_publisher = with_publisher
33
+ # Basic string representation. Delegates to renderer.
34
+ #
35
+ # `with_publisher:` is accepted and has no effect: `XEP` names the
36
+ # series, and XSF itself cites `XEP 0001`, so the internal form is the
37
+ # external one (see {#publisher_prefixes}).
38
+ def to_s(**opts)
39
39
  render(format: :human, **opts)
40
40
  end
41
41
 
42
+ def publisher_prefixes
43
+ []
44
+ end
45
+
42
46
  # from_hash is the shared polymorphic dispatch on Pubid::Identifier;
43
47
  # XSF_TYPE_MAP remains as the key_value polymorphic_map.
44
48
 
@@ -58,7 +62,7 @@ module Pubid
58
62
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
59
63
  end
60
64
 
61
- parsed = Pubid::Parg::Backend.parse(:xsf, identifier)
65
+ parsed = Pubid::Parg::Model.bound(:xsf, identifier)
62
66
  Builder.build(parsed)
63
67
  end
64
68
  end
@@ -1,38 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Xsf
7
- # Parslet grammar for XSF identifiers. Two shapes: "XEP NNNN", and the two
8
- # named documents in SPECIAL_NUMBERS.
9
- class Parser < ::Pubid::Parser::Grammar
10
- # The XMPP extensions repository publishes two documents whose name is
11
- # not a number: the editor README and the `xep-xxxx` template. Both reach
12
- # pubid as real primary docids ("XEP README", "XEP xxxx"), so the number
13
- # rule admits these two literals beside the digits. Exact case, and no
14
- # other word: the flavor still rejects "XEP foo".
15
- SPECIAL_NUMBERS = %w[README xxxx].freeze
16
-
17
- rule(:digit) { match["0-9"] }
18
- rule(:digits) { digit.repeat(1) }
19
-
20
- rule(:special_number) do
21
- SPECIAL_NUMBERS.map { |name| str(name) }.reduce(:|)
22
- end
23
-
24
- # "XEP 0001" — publisher token, a single space, then the number kept as
25
- # a string of digits (leading zeros preserved by the builder), or the
26
- # name of one of the two named documents.
27
- rule(:identifier) do
28
- str("XEP") >> str(" ") >> (digits | special_number).as(:number)
29
- end
30
-
31
- rule(:root) { identifier }
32
-
33
- def self.parse(input)
34
- new.parse(input)
35
- end
5
+ # Grammar for XSF identifiers. Two shapes: "XEP NNNN", and the two
6
+ # named documents (README and the four-x placeholder).
7
+ class Parser
36
8
  end
37
9
  end
38
10
  end