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
@@ -64,9 +64,25 @@ module Pubid
64
64
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
65
65
  end
66
66
 
67
- parsed = Pubid::Parg::Backend.parse(:ietf, identifier)
67
+ parsed = Pubid::Parg::Model.bound(:ietf, strip_prefix(identifier))
68
68
  Builder.build(parsed)
69
69
  end
70
+
71
+ # The external spellings `IETF RFC 8341`, `IETF I-D draft-…`,
72
+ # `IETF Internet-Draft draft-…`, `Internet-Draft draft-…` and
73
+ # `I-D draft-…` name the same document as the bare form. The publisher
74
+ # and the draft label are not part of the identity, so they are dropped
75
+ # before the baked grammar runs, and `to_s(with_publisher:)` renders
76
+ # them again. The lookahead keeps the strip off anything that is not
77
+ # followed by an IETF designation.
78
+ EXTERNAL_PREFIX = /
79
+ \A(?:IETF\s+)?(?:(?:I-D|Internet-Draft)\s+(?=draft-))?
80
+ (?=(?:RFC|BCP|STD|FYI)\s|draft-)
81
+ /x
82
+
83
+ def self.strip_prefix(identifier)
84
+ identifier.sub(EXTERNAL_PREFIX, "")
85
+ end
70
86
  end
71
87
  end
72
88
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Ietf
7
- # Parslet grammar for the three IETF families. The top-level alternation is
5
+ # Grammar for the three IETF families. The top-level alternation is
8
6
  # keyed by leading token, and each alternative captures the raw fields the
9
7
  # Builder needs:
10
8
  # * RFC -> { number: }
@@ -12,50 +10,7 @@ module Pubid
12
10
  # * draft -> { draft_rest: } (everything after "draft-"; the version
13
11
  # split is done in the Builder to keep the digit-tail
14
12
  # heuristic in one place)
15
- class Parser < ::Pubid::Parser::Grammar
16
- rule(:space) { str(" ") }
17
- rule(:digit) { match["0-9"] }
18
- rule(:digits) { digit.repeat(1) }
19
-
20
- # "RFC 2119", and the zero-padded, space-less spelling the RFC editor's
21
- # rfc-index.xml emits ("RFC0001"). See #subseries for why.
22
- rule(:rfc) { str("RFC") >> space.maybe >> digits.as(:number) }
23
-
24
- # "BCP 3" / "STD 66" / "FYI 1".
25
- #
26
- # The space is optional and leading zeros are allowed because
27
- # rfc-index.xml writes the sub-series membership of an RFC in the padded,
28
- # space-less form -- <is-also><doc-id>STD0066</doc-id></is-also> -- and
29
- # relaton builds the STD/BCP/FYI <-> RFC cross-references from exactly
30
- # those elements without normalizing them itself. The Builder strips the
31
- # pad, so the printed form stays the canonical "STD 66".
32
- rule(:subseries) do
33
- (str("BCP") | str("STD") | str("FYI")).as(:series) >>
34
- space.maybe >> digits.as(:number)
35
- end
36
-
37
- # Internet-Draft slug characters: lowercase, digits and the "-", "+", "_"
38
- # separators, plus two historical shapes the published corpus carries --
39
- # a dot inside a topic token ("draft-ietf-pilc-2.5g3g-12") and uppercase
40
- # letters in protocol/organisation names ("draft-chapin-clnp-ISO8473-00").
41
- # 50 of the 166,740 relaton-data-ids identifiers need those two
42
- # characters; nothing else outside [a-z0-9-] occurs in the corpus.
43
- #
44
- # Widening the tail is unambiguous: the top-level alternation is keyed on
45
- # the leading token (RFC / BCP|STD|FYI / draft-), so a wider draft tail
46
- # cannot make another branch match.
47
- #
48
- # The whole remainder after "draft-" is captured; the Builder anchors the
49
- # optional trailing "-NN" version.
50
- rule(:draft_rest) { match['a-zA-Z0-9+_.\-'].repeat(1).as(:draft_rest) }
51
- rule(:draft) { str("draft-") >> draft_rest }
52
-
53
- rule(:identifier) { rfc | subseries | draft }
54
- rule(:root) { identifier }
55
-
56
- def self.parse(input)
57
- new.parse(input)
58
- end
13
+ class Parser
59
14
  end
60
15
  end
61
16
  end
@@ -8,10 +8,25 @@ module Pubid
8
8
  # "RFC 2119"
9
9
  # "BCP 3" / "STD 66" / "FYI 1"
10
10
  # "draft-giuliano-treedn-02" / "draft-giuliano-treedn"
11
+ #
12
+ # The default form carries no publisher, which is how IETF documents are
13
+ # cited. `with_publisher:` names the two forms relaton#49 asks for:
14
+ # true -> "IETF RFC 2119" / "IETF Internet-Draft draft-giuliano-treedn"
15
+ # false -> "RFC 2119" / "Internet-Draft draft-giuliano-treedn"
16
+ # A draft slug says nothing about its type, so both explicit forms label
17
+ # it; the default keeps the bare slug.
11
18
  class Renderer < ::Pubid::Renderers::Base
12
- def render(context: nil, **_opts)
19
+ INTERNET_DRAFT_LABEL = "Internet-Draft"
20
+
21
+ def render(context: nil, with_publisher: nil, **_opts)
13
22
  require_number!(@id)
14
- render_identifier(@id)
23
+ result = render_identifier(@id)
24
+ return result if with_publisher.nil?
25
+
26
+ if @id.is_a?(Identifiers::InternetDraft)
27
+ result = "#{INTERNET_DRAFT_LABEL} #{result}"
28
+ end
29
+ with_publisher ? "#{Identifier::PUBLISHER} #{result}" : result
15
30
  end
16
31
 
17
32
  private
@@ -0,0 +1,89 @@
1
+ # IHO flavor notes
2
+
3
+ IHO (International Hydrographic Organization) identifiers: the
4
+ S/P/M/B/C letter series, their parg-driven parse, and the series
5
+ components introduced for relaton/relaton#407.
6
+
7
+ Read this before you change `lib/pubid/iho/` or `spec/pubid/iho/`. The
8
+ root `CLAUDE.md` keeps the cross-flavor contract that every flavor
9
+ obeys.
10
+
11
+ ## Parsing runs on the parg artifact
12
+
13
+ There is no `parser.rb` and no Parslet anywhere in this flavor: parse
14
+ goes `Pubid::Parg::Backend.parse(:iho, ...)` over the baked artifact
15
+ `data/parg/iho.json`. The artifact is baked from the flavor grammar
16
+ source `grammars/iho.parg` in the pubid-grammar repository (commit
17
+ 840b2b1d "Retire the last live Parslet parsers" ports the original
18
+ 13-rule Parslet grammar byte-identically).
19
+
20
+ **Drift warning:** the artifact on this branch carries the
21
+ relaton/relaton#407 tail rules (year / volume / issue), while the
22
+ checked-in `grammars/iho.parg` in pubid-grammar still has only the
23
+ version tail. Until the updated grammar source lands in pubid-grammar,
24
+ the artifact is ahead of its source and not reproducible from it —
25
+ the same artifact-ahead-of-source state the root notes describe under
26
+ grammar-artifact drift. Do not re-bake from the stale source, and do
27
+ not hand-edit the artifact; land the grammar first.
28
+
29
+ ## Series components (relaton/relaton#407)
30
+
31
+ The B-7, P-1, P-5, P-6 series are continuously updated publications
32
+ whose trailing component is not an a.b.c edition: P-5/P-6 (and B-7)
33
+ are identified by **year**, P-1 (the International Hydrographic
34
+ Review) by **volume** with an optional **issue**. The grammar tail
35
+ picks exactly one discriminator, so only one of `version` / `year` /
36
+ `volume` is ever present, with `issue` only beside a volume:
37
+
38
+ - `IHO P-5 2024` — year is a plain `:string` scalar, 19xx/20xx only
39
+ (`IHO P-5 1850` and the two-segment `IHO B-12 3.0` reject).
40
+ - `IHO P-1 Vol. 30 (2)` — volume/issue scalars; `Issue 2` is an input
41
+ alias whose canonical print is `(2)`.
42
+ - `IHO S-44 5.0.0` — the dotted `version` stays for the edition-bearing
43
+ series and legacy `1.0.0` placeholder rows.
44
+
45
+ All three are ordinary lutaml attributes mapped through the flavor's
46
+ `key_value` block — no converters, no hand-rolled serialization.
47
+
48
+ **Identity semantics:** the components are identity-bearing in `==` /
49
+ `matches?`; a bare reference (`IHO P-5`) leaves all four nil and
50
+ wildcards the series (`spec/pubid/partial_ref_spec.rb` locks the
51
+ omitted-key set). The volume/issue pair is a scalar **cluster**:
52
+ `exclude(:volume)` nils the issue with it, so a reference that omits
53
+ the issue still matches a hit that carries one — the same
54
+ additive-alias shape the base applies to `:year -> :date`. A lone
55
+ scalar `:year` needs no override; the base nils a declared `:year`
56
+ directly.
57
+
58
+ **`all_parts_edition_keys` needs no override:** the default
59
+ `%i[date year edition version]` already covers `:year` and `version`,
60
+ and volume/issue only ride part-less ids, which cannot join
61
+ `AllParts` (`validate_member!` demands a `part`). The audit case in
62
+ `spec/pubid/all_parts_edition_keys_audit_spec.rb` covers the
63
+ version-bearing S-series collapse.
64
+
65
+ **URN conventions:** a year is a bare trailing segment
66
+ (`urn:iho:p:5:2024`); a volume is a `vol.N` part-kind segment with an
67
+ optional `issue.N` after it (`urn:iho:p:1:vol.30.issue.2`). The URN
68
+ parser walks part-kind tokens as a run and hands the rebuilt text
69
+ back through the flavor parse, so a URN round-trip exercises the same
70
+ grammar as a string.
71
+
72
+ ## Fixtures run through the classify pipeline
73
+
74
+ `spec/fixtures/iho/identifiers/full/identifiers.txt` is the input
75
+ corpus; `rake "validation:classify[iho]"` regenerates `pass/` and
76
+ `fail/` from it (the classifier deletes pass/fail first — never edit
77
+ them by hand, edit `full/` and reclassify). Before the relaton#407
78
+ branch the pass files were hand-curated with **no** `full/` tree, the
79
+ OASIS hazard: a classify run would have wiped 131 entries and rebuilt
80
+ nothing. The classifier's per-flavor class detection has an `iho`
81
+ case, so fail entries land in a named class file.
82
+
83
+ ## relaton-side follow-ups (not this repo)
84
+
85
+ relaton/relaton#407 items 3-4 stay open on the relaton side:
86
+ `Relaton::Iho::Bibliography#pubid_match?`/`version_key` need the new
87
+ component, relaton-cli's `MachineIndex::REFINABLE_KEYS` needs
88
+ `year`/`volume` beside `version`/`edition`, and the pubid pin bumps
89
+ in step with the relaton-data-iho re-key.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pubid
4
4
  module Iho
5
- # Build IHO identifier objects from parslet parse output.
5
+ # Build IHO identifier objects from the parg artifact parse tree.
6
6
  class Builder
7
7
  def build(parsed)
8
8
  hash = parsed.is_a?(Array) ? parsed.reduce({}, :merge) : parsed
@@ -15,6 +15,9 @@ module Pubid
15
15
  annex: stringify(hash[:annex]),
16
16
  supplement: stringify(hash[:supplement]),
17
17
  version: stringify(hash[:version]),
18
+ year: stringify(hash[:year]),
19
+ volume: stringify(hash[:volume]),
20
+ issue: stringify(hash[:issue]),
18
21
  }.compact
19
22
 
20
23
  Pubid::Iho.identifier_klass_for_type_letter(type_letter).new(**attrs)
@@ -6,7 +6,7 @@ module Pubid
6
6
  # every concrete IHO identifier (Identifiers::*) descends from it.
7
7
  #
8
8
  # IHO identifiers have the form:
9
- # IHO {S|P|M|B|C}-{number}[ Ap. {appendix}][ Part {part}][ Annex {annex}][ Suppl {supplement}][ {version}]
9
+ # IHO {S|P|M|B|C}-{number}[ Ap. {appendix}][ Part {part}][ Annex {annex}][ Suppl {supplement}][ {version|year|Vol. N [issue]}]
10
10
  #
11
11
  # The leading IHO publisher prefix is optional on input but always
12
12
  # emitted on output.
@@ -25,7 +25,7 @@ module Pubid
25
25
  raise Pubid::Errors::InvalidInputError, Pubid::INPUT_TOO_LONG_MESSAGE
26
26
  end
27
27
 
28
- parsed = Parser.parse(identifier)
28
+ parsed = Pubid::Parg::Backend.parse(:iho, identifier.strip)
29
29
  Builder.build(parsed)
30
30
  end
31
31
 
@@ -40,6 +40,17 @@ module Pubid
40
40
  attribute :annex, :string
41
41
  attribute :supplement, :string
42
42
  attribute :version, :string
43
+ # Series identity components (relaton#407): the year-identified
44
+ # series (B-7, P-5, P-6) and the volume/issue-identified ones
45
+ # (P-1 and the IHR) carry no a.b.c edition — their trailing
46
+ # component is a year or a "Vol. N [issue]" pair instead. Plain
47
+ # scalars, like every other IHO attribute; the grammar's tail
48
+ # picks exactly one discriminator, so only one of
49
+ # version/year/volume is ever present, with issue only
50
+ # alongside volume.
51
+ attribute :year, :string
52
+ attribute :volume, :string
53
+ attribute :issue, :string
43
54
 
44
55
  # Polymorphic type map for lutaml::Model key_value serialization: maps
45
56
  # each subclass's polymorphic_name to its class name so `from_hash` can
@@ -65,10 +76,25 @@ module Pubid
65
76
  map "annex", to: :annex
66
77
  map "supplement", to: :supplement
67
78
  map "version", to: :version
79
+ map "year", to: :year
80
+ map "volume", to: :volume
81
+ map "issue", to: :issue
68
82
  end
69
83
 
70
84
  # from_hash is the shared polymorphic dispatch on Pubid::Identifier.
71
85
  # IHO_TYPE_MAP remains as the key_value polymorphic_map.
86
+
87
+ # The volume/issue pair is a scalar cluster: a reference that omits
88
+ # the issue must still match a hit that carries one ("P-1 Vol. 30"
89
+ # vs "P-1 Vol. 30 (2)"), so excluding the volume nils the issue
90
+ # with it — the same additive-alias shape the base applies to
91
+ # :year -> :date. A lone scalar :year needs no override here: the
92
+ # base nils a declared :year directly (the additive :year -> :date
93
+ # alias nils whichever name the flavor declares).
94
+ def exclude(*args)
95
+ args << :issue if args.include?(:volume)
96
+ super
97
+ end
72
98
  end
73
99
 
74
100
  end
@@ -9,6 +9,8 @@ module Pubid
9
9
  # "IHO P-1/21 1.0.0"
10
10
  # "IHO S-100 Part 4a 1.0.0"
11
11
  # "IHO S-65 Ap. A 1.0.0"
12
+ # "IHO P-5 2024" (year-identified series)
13
+ # "IHO P-1 Vol. 30 (2)" (volume/issue-identified series)
12
14
  #
13
15
  # The renderer is registered as the `:human` format in the IHO format
14
16
  # registry and invoked via `render(format: :human)`.
@@ -22,7 +24,13 @@ module Pubid
22
24
  rendered << " Part #{id.part}" if id.part
23
25
  rendered << " Annex #{id.annex}" if id.annex
24
26
  rendered << " Suppl #{id.supplement}" if id.supplement
27
+ # The grammar's tail picks exactly one discriminator: a dotted
28
+ # version, a year, or a volume (with an optional issue). The
29
+ # issue prints parenthesized; "Issue N" is an input alias only.
25
30
  rendered << " #{id.version}" if id.version
31
+ rendered << " #{id.year}" if id.year
32
+ rendered << " Vol. #{id.volume}" if id.volume
33
+ rendered << " (#{id.issue})" if id.issue
26
34
  rendered
27
35
  end
28
36
  end
@@ -4,8 +4,13 @@ module Pubid
4
4
  module Iho
5
5
  # Generates URN strings for IHO identifiers.
6
6
  #
7
- # Format: urn:iho:{type-letter-lowercase}:{number}[:ap.{appendix}][:part.{part}][:annex.{annex}][:suppl.{supplement}][:{version}]
7
+ # Format: urn:iho:{type-letter-lowercase}:{number}[:ap.{appendix}][:part.{part}][:annex.{annex}][:suppl.{supplement}][:{version|year|vol.N[.issue.N]}]
8
8
  # Example: urn:iho:s:44:5.0.0 for "IHO S-44 5.0.0".
9
+ # The trailing series component follows the flavor's own conventions:
10
+ # a version or a year is a bare segment ("urn:iho:p:5:2024" for
11
+ # "IHO P-5 2024"), a volume is a vol.N part-kind segment with an
12
+ # optional issue.N after it ("urn:iho:p:1:vol.30.issue.2" for
13
+ # "IHO P-1 Vol. 30 (2)").
9
14
  class UrnGenerator
10
15
  attr_reader :identifier
11
16
 
@@ -21,7 +26,12 @@ module Pubid
21
26
  parts << "part.#{identifier.part}" if identifier.part
22
27
  parts << "annex.#{identifier.annex}" if identifier.annex
23
28
  parts << "suppl.#{identifier.supplement}" if identifier.supplement
29
+ # The grammar's tail picks exactly one discriminator; a volume
30
+ # may carry an issue beside it.
31
+ parts << "vol.#{identifier.volume}" if identifier.volume
32
+ parts << "issue.#{identifier.issue}" if identifier.issue
24
33
  parts << identifier.version.to_s if identifier.version
34
+ parts << identifier.year.to_s if identifier.year
25
35
  parts.join(":")
26
36
  end
27
37
  end
@@ -5,22 +5,28 @@ module Pubid
5
5
  # Parses IHO URNs back into identifiers.
6
6
  #
7
7
  # UrnGenerator emits:
8
- # urn:iho:{type-lower}:{number}[:{part-kind}.{part}]:{version}
8
+ # urn:iho:{type-lower}:{number}[:{part-kind}.{part}][:{version|year|vol.N[.issue.N]}]
9
9
  #
10
10
  # type-lower is the lowercase series letter (s, p, m, b, c).
11
- # part-kind is the lowercase part label (part, ap, annex, suppl).
12
- # version is the dotted version string.
11
+ # part-kind is the lowercase part label (part, ap, annex, suppl,
12
+ # vol, issue).
13
+ # The trailing segment is the dotted version, a bare year, or the
14
+ # volume/issue pair.
13
15
  #
14
16
  # Examples:
15
17
  # - urn:iho:s:44:5.0.0 → IHO S-44 5.0.0
16
18
  # - urn:iho:s:100:part.4a:1.0.0 → IHO S-100 Part 4a 1.0.0
17
19
  # - urn:iho:s:65:ap.A:1.0.0 → IHO S-65 Ap. A 1.0.0
20
+ # - urn:iho:p:5:2024 → IHO P-5 2024
21
+ # - urn:iho:p:1:vol.30 → IHO P-1 Vol. 30
22
+ # - urn:iho:p:1:vol.30.issue.2 → IHO P-1 Vol. 30 (2)
18
23
  class UrnParser < Pubid::UrnParser::Base
19
24
  PART_LABELS = {
20
25
  "part" => "Part",
21
26
  "ap" => "Ap.",
22
27
  "annex" => "Annex",
23
28
  "suppl" => "Suppl",
29
+ "vol" => "Vol.",
24
30
  }.freeze
25
31
 
26
32
  def parse_urn(urn)
@@ -30,18 +36,22 @@ module Pubid
30
36
  type_token = parts.fetch(0).upcase
31
37
  number = parts.fetch(1)
32
38
  idx = 2
33
- part_label = nil
34
- if parts[idx] && part_kind?(parts[idx])
39
+ text = "IHO #{type_token}-#{number}"
40
+ # Part-kind tokens come in any run (a volume may carry an issue
41
+ # beside it); "issue.N" prints as the parenthesized issue.
42
+ while parts[idx] && part_kind?(parts[idx])
35
43
  kind, value = parts[idx].split(".", 2)
36
- label = PART_LABELS.fetch(kind.downcase) { kind.capitalize }
37
- part_label = "#{label} #{value}"
44
+ if kind.downcase == "issue"
45
+ text += " (#{value})"
46
+ else
47
+ label = PART_LABELS.fetch(kind.downcase) { kind.capitalize }
48
+ text += " #{label} #{value}"
49
+ end
38
50
  idx += 1
39
51
  end
40
- version = parts[idx]
52
+ tail = parts[idx]
41
53
 
42
- text = "IHO #{type_token}-#{number}"
43
- text += " #{part_label}" if part_label
44
- text += " #{version}" if version
54
+ text += " #{tail}" if tail
45
55
  flavor_parse(text)
46
56
  end
47
57
 
data/lib/pubid/iho.rb CHANGED
@@ -11,7 +11,6 @@ module Pubid
11
11
  autoload :Builder, "#{__dir__}/iho/builder"
12
12
  autoload :Identifier, "#{__dir__}/iho/identifiers/base"
13
13
  autoload :Identifiers, "#{__dir__}/iho/identifiers"
14
- autoload :Parser, "#{__dir__}/iho/parser"
15
14
  autoload :Renderer, "#{__dir__}/iho/renderer"
16
15
  autoload :UrnGenerator, "#{__dir__}/iho/urn_generator"
17
16
  autoload :UrnParser, "#{__dir__}/iho/urn_parser"
@@ -64,7 +64,7 @@ module Pubid
64
64
 
65
65
  # @raise [Pubid::Errors::ParseError] if the string is not a valid ISBN
66
66
  def self.build_identifier(identifier)
67
- parsed = Pubid::Parg::Backend.parse(:isbn, identifier)
67
+ parsed = Pubid::Parg::Model.bound(:isbn, identifier)
68
68
  Builder.build(parsed)
69
69
  rescue ArgumentError => e
70
70
  # The Builder validates length and check digit. Surface that as a parse
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
4
-
5
3
  module Pubid
6
4
  module Isbn
7
- # Parslet grammar for ISBN identifiers per ISO 2108.
5
+ # Grammar for ISBN identifiers per ISO 2108.
8
6
  #
9
7
  # Accepts:
10
8
  # [ISBN[ |:]] {10 or 13 digits with optional hyphens}
@@ -12,34 +10,7 @@ module Pubid
12
10
  # ISBN-10 final digit may be "X" (check digit value 10). Hyphens are
13
11
  # optional and may appear in any group position; they are preserved
14
12
  # for round-trip rendering.
15
- class Parser < ::Pubid::Parser::Grammar
16
- rule(:space) { str(" ") }
17
- rule(:colon) { str(":") }
18
- rule(:hyphen) { str("-") }
19
- rule(:digit) { match("[0-9]") }
20
- rule(:x_digit) { digit | str("X") }
21
-
22
- rule(:isbn_prefix) do
23
- (str("ISBN") >> (space | (space.maybe >> colon >> space.maybe))).maybe
24
- end
25
-
26
- rule(:digit_group) { digit.repeat(1) }
27
- rule(:x_group) { x_digit.repeat(1) }
28
-
29
- # Loose "ISBN-ish" token: digits or final X, separated by hyphens.
30
- rule(:isbn_body) do
31
- (digit_group >> (hyphen >> digit_group).repeat >>
32
- (hyphen >> x_digit).maybe).as(:body) |
33
- (digit.repeat(9, 12) >> x_digit.maybe).as(:body)
34
- end
35
-
36
- rule(:identifier) { isbn_prefix >> isbn_body }
37
-
38
- rule(:root) { identifier }
39
-
40
- def self.parse(input)
41
- new.parse(input)
42
- end
13
+ class Parser
43
14
  end
44
15
  end
45
16
  end
@@ -308,7 +308,7 @@ module Pubid
308
308
  when :number
309
309
  # For TC documents, number is the document number (N number)
310
310
  # For regular identifiers, this is handled in :number_with_part
311
- if value.is_a?(Parslet::Slice) || value.is_a?(String) ||
311
+ if value.is_a?(String) ||
312
312
  value.is_a?(Integer)
313
313
  value.to_s
314
314
  else
@@ -35,9 +35,12 @@ module Pubid
35
35
  end
36
36
 
37
37
  def to_s(**opts)
38
- context = build_rendering_context(nil, format: :human, **opts)
39
- Pubid::Renderers::DirectivesRenderer.new(self).render(context:,
40
- **opts.slice(:with_edition))
38
+ context = build_rendering_context(
39
+ nil, format: :human, **opts.except(:with_publisher)
40
+ )
41
+ rendered = Pubid::Renderers::DirectivesRenderer.new(self)
42
+ .render(context:, **opts.slice(:with_edition))
43
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
41
44
  end
42
45
 
43
46
  # Directives use a different URN scheme: urn:iso:doc instead of urn:iso:std
@@ -91,9 +91,12 @@ module Pubid
91
91
  def to_s(**opts)
92
92
  return super if opts[:format] && opts[:format] != :human
93
93
 
94
- context = build_rendering_context(nil, format: :human, **opts)
95
- Pubid::Renderers::GuideRenderer.new(self).render(context:,
96
- **opts.slice(:with_edition))
94
+ context = build_rendering_context(
95
+ nil, format: :human, **opts.except(:with_publisher)
96
+ )
97
+ rendered = Pubid::Renderers::GuideRenderer.new(self)
98
+ .render(context:, **opts.slice(:with_edition))
99
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
97
100
  end
98
101
  end
99
102
  end
@@ -54,7 +54,9 @@ module Pubid
54
54
  code: :wds,
55
55
  stage_code: :wds,
56
56
  type_code: :is,
57
- abbr: ["WDS"],
57
+ # ISO launched the "WD Study" WG consultation (stage 20.20/20.60,
58
+ # relaton-iso#125); its spelled-out form is an accepted input spelling.
59
+ abbr: ["WDS", "WD Study"],
58
60
  name: "Working Draft Study",
59
61
  harmonized_stages: %w[20.20 20.60],
60
62
  ),
@@ -85,9 +85,12 @@ module Pubid
85
85
  end
86
86
 
87
87
  def to_s(**opts)
88
- context = build_rendering_context(nil, format: :human, **opts)
89
- Pubid::Renderers::IwaRenderer.new(self).render(context:,
90
- **opts.slice(:with_edition))
88
+ context = build_rendering_context(
89
+ nil, format: :human, **opts.except(:with_publisher)
90
+ )
91
+ rendered = Pubid::Renderers::IwaRenderer.new(self)
92
+ .render(context:, **opts.slice(:with_edition))
93
+ apply_with_publisher(rendered, :human, opts[:with_publisher])
91
94
  end
92
95
  end
93
96
  end
@@ -71,7 +71,7 @@ module Pubid
71
71
  supplement_number = match[3] # "1"
72
72
  supplement_year = match[4] # "1987" or nil
73
73
 
74
- base_parsed = Pubid::Iso.parser.parse(base_str)
74
+ base_parsed = Pubid::Parg::Backend.parse(:iso, base_str)
75
75
  base = Pubid::Iso.builder.build(base_parsed)
76
76
 
77
77
  addendum = Identifiers::Addendum.new