pubid 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/ieee/update_codes.yaml +2 -2
- data/data/parg/adobe.json +1 -1
- data/data/parg/aiee.json +1 -1
- data/data/parg/aiee.parg +99 -9
- data/data/parg/amca.json +1 -1
- data/data/parg/ansi.json +1 -1
- data/data/parg/api.json +1 -1
- data/data/parg/ashrae.json +1 -1
- data/data/parg/asme.json +1 -1
- data/data/parg/astm.json +1 -1
- data/data/parg/bipm.json +1 -1
- data/data/parg/bsi.json +1 -1
- data/data/parg/ccsds.json +1 -1
- data/data/parg/cen_cenelec.json +1 -1
- data/data/parg/cie.json +1 -1
- data/data/parg/csa.json +1 -1
- data/data/parg/ecma.json +1 -1
- data/data/parg/etsi.json +1 -1
- data/data/parg/evs.json +1 -1
- data/data/parg/gost.json +1 -1
- data/data/parg/iala.json +1 -1
- data/data/parg/iana.json +1 -1
- data/data/parg/idf.json +1 -1
- data/data/parg/iec.json +1 -1
- data/data/parg/ieee.json +1 -1
- data/data/parg/iho.json +1 -0
- data/data/parg/ire.json +1 -1
- data/data/parg/ire.parg +51 -12
- data/data/parg/isbn.json +1 -1
- data/data/parg/iso.json +1 -1
- data/data/parg/iso.parg +751 -0
- data/data/parg/itu.json +1 -1
- data/data/parg/jcgm.json +1 -1
- data/data/parg/jis.json +1 -1
- data/data/parg/nesc.json +1 -1
- data/data/parg/nesc.parg +97 -12
- data/data/parg/nist.json +1 -1
- data/data/parg/oiml.json +1 -1
- data/data/parg/omg.json +1 -1
- data/data/parg/parg.json +1 -1
- data/data/parg/plateau.json +1 -1
- data/data/parg/tgpp.json +1 -1
- data/data/parg/un.json +1 -1
- data/lib/pubid/adobe/identifier.rb +1 -1
- data/lib/pubid/adobe/parser.rb +2 -113
- data/lib/pubid/amca/identifiers/base.rb +1 -1
- data/lib/pubid/amca/parser.rb +1 -145
- data/lib/pubid/ansi/identifier.rb +1 -1
- data/lib/pubid/ansi/parser.rb +1 -82
- data/lib/pubid/api/builder.rb +32 -1
- data/lib/pubid/api/identifier.rb +1 -1
- data/lib/pubid/api/parser.rb +1 -131
- data/lib/pubid/ashrae/identifiers/base.rb +1 -1
- data/lib/pubid/ashrae/parser.rb +3 -710
- data/lib/pubid/asme/builder.rb +6 -4
- data/lib/pubid/asme/identifier.rb +1 -2
- data/lib/pubid/asme/parser.rb +1 -328
- data/lib/pubid/astm/builder.rb +12 -3
- data/lib/pubid/astm/identifier.rb +1 -2
- data/lib/pubid/astm/parser.rb +1 -227
- data/lib/pubid/bipm/identifier.rb +8 -1
- data/lib/pubid/bipm/parser.rb +2 -224
- data/lib/pubid/bsi/builder.rb +2 -4
- data/lib/pubid/bsi/parser.rb +1 -666
- data/lib/pubid/bsi/single_identifier.rb +38 -1
- data/lib/pubid/calconnect/parser.rb +2 -40
- data/lib/pubid/ccsds/identifier.rb +1 -1
- data/lib/pubid/ccsds/parser.rb +1 -66
- data/lib/pubid/cen_cenelec/identifier.rb +16 -1
- data/lib/pubid/cen_cenelec/parser.rb +3 -136
- data/lib/pubid/cie/identifier.rb +1 -1
- data/lib/pubid/cie/parser.rb +3 -378
- data/lib/pubid/conformance.rb +1 -1
- data/lib/pubid/csa/builder.rb +41 -18
- data/lib/pubid/csa/identifier.rb +12 -4
- data/lib/pubid/csa/identifiers/amendment.rb +28 -0
- data/lib/pubid/csa/identifiers.rb +1 -0
- data/lib/pubid/csa/parser.rb +16 -397
- data/lib/pubid/doi/parser.rb +2 -37
- data/lib/pubid/easc/builder.rb +1 -1
- data/lib/pubid/easc/parser.rb +2 -70
- data/lib/pubid/ecma/CLAUDE.md +8 -1
- data/lib/pubid/ecma/identifier.rb +13 -8
- data/lib/pubid/ecma/parser.rb +2 -54
- data/lib/pubid/ecma/renderer.rb +3 -7
- data/lib/pubid/errors.rb +18 -13
- data/lib/pubid/etsi/builder.rb +8 -5
- data/lib/pubid/etsi/identifiers/base.rb +1 -1
- data/lib/pubid/etsi/parser.rb +1 -132
- data/lib/pubid/evs/identifier.rb +7 -0
- data/lib/pubid/evs/parser.rb +1 -8
- data/lib/pubid/evs.rb +1 -1
- data/lib/pubid/gb/identifier.rb +6 -0
- data/lib/pubid/gb/parser.rb +2 -51
- data/lib/pubid/gost/identifier.rb +9 -1
- data/lib/pubid/gost/parser.rb +4 -81
- data/lib/pubid/iala/builder.rb +48 -39
- data/lib/pubid/iala/identifier.rb +1 -1
- data/lib/pubid/iala/parser.rb +2 -121
- data/lib/pubid/iana/identifier.rb +1 -1
- data/lib/pubid/iana/parser.rb +2 -24
- data/lib/pubid/identifier.rb +78 -1
- data/lib/pubid/idf/builder.rb +42 -0
- data/lib/pubid/idf/identifier.rb +1 -1
- data/lib/pubid/idf/parser.rb +1 -137
- data/lib/pubid/idf.rb +1 -2
- data/lib/pubid/iec/builder.rb +158 -50
- data/lib/pubid/iec/identifier.rb +1 -1
- data/lib/pubid/iec/parser.rb +3 -430
- data/lib/pubid/ieee/CLAUDE.md +2 -0
- data/lib/pubid/ieee/aiee/identifier.rb +2 -1
- data/lib/pubid/ieee/aiee.rb +0 -1
- data/lib/pubid/ieee/builder.rb +177 -30
- data/lib/pubid/ieee/components/code.rb +46 -7
- data/lib/pubid/ieee/identifiers/base.rb +5 -2
- data/lib/pubid/ieee/identifiers/joint_development.rb +7 -3
- data/lib/pubid/ieee/ire/identifier.rb +2 -1
- data/lib/pubid/ieee/ire.rb +0 -1
- data/lib/pubid/ieee/nesc/builder.rb +1 -1
- data/lib/pubid/ieee/nesc.rb +0 -1
- data/lib/pubid/ieee/parser.rb +1 -1257
- data/lib/pubid/ieee/project_renderer.rb +5 -1
- data/lib/pubid/ieee/renderer.rb +4 -2
- data/lib/pubid/ieee/urn_generator.rb +4 -1
- data/lib/pubid/ietf/identifiers/base.rb +17 -1
- data/lib/pubid/ietf/parser.rb +2 -47
- data/lib/pubid/ietf/renderer.rb +17 -2
- data/lib/pubid/iho/CLAUDE.md +98 -0
- data/lib/pubid/iho/builder.rb +4 -1
- data/lib/pubid/iho/identifiers/base.rb +28 -2
- data/lib/pubid/iho/renderer.rb +8 -0
- data/lib/pubid/iho/urn_generator.rb +14 -1
- data/lib/pubid/iho/urn_parser.rb +45 -32
- data/lib/pubid/iho.rb +0 -1
- data/lib/pubid/isbn/identifier.rb +1 -1
- data/lib/pubid/isbn/parser.rb +2 -31
- data/lib/pubid/iso/CLAUDE.md +66 -0
- data/lib/pubid/iso/builder.rb +229 -15
- data/lib/pubid/iso/identifier.rb +22 -12
- data/lib/pubid/iso/identifiers/directives.rb +6 -3
- data/lib/pubid/iso/identifiers/guide.rb +6 -3
- data/lib/pubid/iso/identifiers/international_workshop_agreement.rb +6 -3
- data/lib/pubid/iso/normalizer.rb +5 -4
- data/lib/pubid/iso/parser.rb +1 -539
- data/lib/pubid/iso/supplement_identifier.rb +6 -3
- data/lib/pubid/iso/urn_parser.rb +13 -8
- data/lib/pubid/iso/utilities.rb +4 -5
- data/lib/pubid/iso.rb +1 -19
- data/lib/pubid/itu/builder.rb +36 -20
- data/lib/pubid/itu/identifiers/base.rb +15 -2
- data/lib/pubid/itu/parser.rb +1 -775
- data/lib/pubid/jcgm/builder.rb +35 -0
- data/lib/pubid/jcgm/parser.rb +1 -151
- data/lib/pubid/jcgm.rb +1 -2
- data/lib/pubid/jis/CLAUDE.md +36 -0
- data/lib/pubid/jis/builder.rb +30 -41
- data/lib/pubid/jis/identifier.rb +12 -2
- data/lib/pubid/jis/identifiers/welding_engineering_standard.rb +35 -0
- data/lib/pubid/jis/identifiers.rb +2 -0
- data/lib/pubid/jis/parser.rb +1 -128
- data/lib/pubid/jis/renderer.rb +1 -1
- data/lib/pubid/jis/supplement_identifier.rb +5 -5
- data/lib/pubid/jis/urn_parser.rb +2 -1
- data/lib/pubid/jis.rb +3 -2
- data/lib/pubid/nist/builder.rb +5 -1
- data/lib/pubid/nist/caster.rb +13 -3
- data/lib/pubid/nist/identifiers/base.rb +37 -2
- data/lib/pubid/nist/identifiers/circular.rb +2 -2
- data/lib/pubid/nist/identifiers/circular_supplement.rb +2 -2
- data/lib/pubid/nist/identifiers/commercial_standards_monthly.rb +2 -2
- data/lib/pubid/nist/identifiers/crpl_report.rb +2 -2
- data/lib/pubid/nist/identifiers/handbook.rb +2 -2
- data/lib/pubid/nist/identifiers/internal_report.rb +2 -2
- data/lib/pubid/nist/identifiers/miscellaneous_publication.rb +2 -2
- data/lib/pubid/nist/identifiers/monograph.rb +2 -2
- data/lib/pubid/nist/identifiers/report.rb +2 -2
- data/lib/pubid/nist/parser.rb +19 -775
- data/lib/pubid/nist/router.rb +4 -1
- data/lib/pubid/nist/supplement_identifier.rb +2 -2
- data/lib/pubid/oasis/parser.rb +2 -16
- data/lib/pubid/ogc/parser.rb +2 -26
- data/lib/pubid/oiml/identifiers/certification_system.rb +7 -0
- data/lib/pubid/oiml/identifiers/dual_published.rb +2 -2
- data/lib/pubid/oiml/parser.rb +1 -303
- data/lib/pubid/oiml.rb +1 -2
- data/lib/pubid/omg/identifier.rb +6 -1
- data/lib/pubid/omg/parser.rb +2 -76
- data/lib/pubid/parser.rb +5 -4
- data/lib/pubid/parsers/mr_string.rb +1 -1
- data/lib/pubid/plateau/parser.rb +1 -66
- data/lib/pubid/plateau.rb +1 -2
- data/lib/pubid/renderers/directives_renderer.rb +1 -1
- data/lib/pubid/renderers/iwa_renderer.rb +1 -1
- data/lib/pubid/renderers/publisher_stripper.rb +82 -0
- data/lib/pubid/renderers/supplement_renderer.rb +1 -1
- data/lib/pubid/renderers.rb +1 -0
- data/lib/pubid/sae/parser.rb +1 -48
- data/lib/pubid/tgpp/identifier.rb +1 -1
- data/lib/pubid/tgpp/parser.rb +2 -59
- data/lib/pubid/un/identifier.rb +1 -1
- data/lib/pubid/un/parser.rb +1 -18
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid/w3c/parser.rb +2 -27
- data/lib/pubid/xsf/identifier.rb +10 -6
- data/lib/pubid/xsf/parser.rb +3 -31
- data/lib/pubid/xsf/renderer.rb +2 -4
- data/lib/pubid.rb +8 -9
- metadata +9 -23
- data/lib/pubid/ieee/aiee/parser.rb +0 -130
- data/lib/pubid/ieee/ire/parser.rb +0 -109
- data/lib/pubid/ieee/nesc/parser.rb +0 -155
- data/lib/pubid/iho/parser.rb +0 -68
- data/lib/pubid/parser/common_parse_methods.rb +0 -13
- data/lib/pubid/parser/common_parse_rules.rb +0 -56
- data/lib/pubid/parser/grammar.rb +0 -96
|
@@ -14,8 +14,12 @@ module Pubid
|
|
|
14
14
|
|
|
15
15
|
def render(context: nil, **_opts)
|
|
16
16
|
id = @id
|
|
17
|
+
# Canonical code: a project draft's code is reassembled from
|
|
18
|
+
# parse-tree captures, and the assembled separators are synthetic
|
|
19
|
+
# — the printed face is the uniform spelling (the same document
|
|
20
|
+
# spelled with all dashes or all dots renders identically).
|
|
17
21
|
body = if id.respond_to?(:code_obj) && id.code_obj
|
|
18
|
-
id.code_obj.
|
|
22
|
+
id.code_obj.canonical_to_s
|
|
19
23
|
else
|
|
20
24
|
id.number.to_s
|
|
21
25
|
end
|
data/lib/pubid/ieee/renderer.rb
CHANGED
|
@@ -472,8 +472,10 @@ module Pubid
|
|
|
472
472
|
end
|
|
473
473
|
|
|
474
474
|
# Code (number) with draft version for PSI — the mark goes after the
|
|
475
|
-
# code, before the draft and the date.
|
|
476
|
-
|
|
475
|
+
# code, before the draft and the date. Canonical code: the PSI
|
|
476
|
+
# code is reassembled from parse-tree captures (synthetic
|
|
477
|
+
# separators — see Components::Code#canonical_to_s).
|
|
478
|
+
code_part = id.code.canonical_to_s
|
|
477
479
|
code_part += mark(id.code_obj&.number, id.code_obj&.prefix,
|
|
478
480
|
publishers: publishers_of(id))
|
|
479
481
|
if id.draft_obj
|
|
@@ -190,7 +190,10 @@ module Pubid
|
|
|
190
190
|
|
|
191
191
|
return nil unless identifier.code_obj
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
# The URN is spelling-stable: codes printed with mixed part
|
|
194
|
+
# separators ("7.4-3-2") share one identity with their uniform
|
|
195
|
+
# spelling, so the segment renders canonically, not as printed.
|
|
196
|
+
identifier.code_obj.canonical_to_s
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
# A multi-designation wrapper (AdoptedStandard, DualPublished,
|
|
@@ -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::Model.bound(: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
|
data/lib/pubid/ietf/parser.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
data/lib/pubid/ietf/renderer.rb
CHANGED
|
@@ -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
|
-
|
|
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,98 @@
|
|
|
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:** only the dotted `version` is an untagged
|
|
66
|
+
segment. A year is tagged `y.N` (`urn:iho:p:5:y.2024`); a volume is a
|
|
67
|
+
`vol.N` segment with an optional `issue.N` segment after it
|
|
68
|
+
(`urn:iho:p:1:vol.30:issue.2`). The URN parser walks the tagged
|
|
69
|
+
segments as a run and hands the rebuilt text back through the flavor
|
|
70
|
+
parse, so a URN round-trip exercises the same grammar as a string.
|
|
71
|
+
**The number is not escaped and can hold a colon** (`IHO S-158:100`
|
|
72
|
+
gives `urn:iho:s:158:100`), so the parser joins every untagged segment
|
|
73
|
+
that is not an a.b.c version back into the number. That is why the
|
|
74
|
+
year must carry a tag: with the bare year that pubid 2.0.2 emitted,
|
|
75
|
+
`IHO S-158:2024` and `IHO S-158 2024` both gave `urn:iho:s:158:2024`,
|
|
76
|
+
and no colon number round-tripped at all. A 2.0.2 year URN such as
|
|
77
|
+
`urn:iho:p:5:2024` now decodes as the number `5:2024`; it was emitted
|
|
78
|
+
only by 2.0.2. A tagged segment with an unknown tag raises
|
|
79
|
+
`Pubid::Errors::UrnParseError`.
|
|
80
|
+
|
|
81
|
+
## Fixtures run through the classify pipeline
|
|
82
|
+
|
|
83
|
+
`spec/fixtures/iho/identifiers/full/identifiers.txt` is the input
|
|
84
|
+
corpus; `rake "validation:classify[iho]"` regenerates `pass/` and
|
|
85
|
+
`fail/` from it (the classifier deletes pass/fail first — never edit
|
|
86
|
+
them by hand, edit `full/` and reclassify). Before the relaton#407
|
|
87
|
+
branch the pass files were hand-curated with **no** `full/` tree, the
|
|
88
|
+
OASIS hazard: a classify run would have wiped 131 entries and rebuilt
|
|
89
|
+
nothing. The classifier's per-flavor class detection has an `iho`
|
|
90
|
+
case, so fail entries land in a named class file.
|
|
91
|
+
|
|
92
|
+
## relaton-side follow-ups (not this repo)
|
|
93
|
+
|
|
94
|
+
relaton/relaton#407 items 3-4 stay open on the relaton side:
|
|
95
|
+
`Relaton::Iho::Bibliography#pubid_match?`/`version_key` need the new
|
|
96
|
+
component, relaton-cli's `MachineIndex::REFINABLE_KEYS` needs
|
|
97
|
+
`year`/`volume` beside `version`/`edition`, and the pubid pin bumps
|
|
98
|
+
in step with the relaton-data-iho re-key.
|
data/lib/pubid/iho/builder.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Pubid
|
|
4
4
|
module Iho
|
|
5
|
-
# Build IHO identifier objects from
|
|
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 =
|
|
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
|
data/lib/pubid/iho/renderer.rb
CHANGED
|
@@ -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,16 @@ 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}][:vol.{volume}[:issue.{issue}]][:{version}][:y.{year}]
|
|
8
8
|
# Example: urn:iho:s:44:5.0.0 for "IHO S-44 5.0.0".
|
|
9
|
+
# Only the dotted version is a bare segment. A year is tagged
|
|
10
|
+
# ("urn:iho:p:5:y.2024" for "IHO P-5 2024"), and so is a volume with
|
|
11
|
+
# its optional issue ("urn:iho:p:1:vol.30:issue.2" for
|
|
12
|
+
# "IHO P-1 Vol. 30 (2)").
|
|
13
|
+
#
|
|
14
|
+
# The number is not escaped, so "IHO S-158:100" gives
|
|
15
|
+
# "urn:iho:s:158:100". A bare year would then make "IHO S-158:2024"
|
|
16
|
+
# and "IHO S-158 2024" share one URN; the y. tag keeps them apart.
|
|
9
17
|
class UrnGenerator
|
|
10
18
|
attr_reader :identifier
|
|
11
19
|
|
|
@@ -21,7 +29,12 @@ module Pubid
|
|
|
21
29
|
parts << "part.#{identifier.part}" if identifier.part
|
|
22
30
|
parts << "annex.#{identifier.annex}" if identifier.annex
|
|
23
31
|
parts << "suppl.#{identifier.supplement}" if identifier.supplement
|
|
32
|
+
# The grammar's tail picks exactly one discriminator; a volume
|
|
33
|
+
# may carry an issue beside it.
|
|
34
|
+
parts << "vol.#{identifier.volume}" if identifier.volume
|
|
35
|
+
parts << "issue.#{identifier.issue}" if identifier.issue
|
|
24
36
|
parts << identifier.version.to_s if identifier.version
|
|
37
|
+
parts << "y.#{identifier.year}" if identifier.year
|
|
25
38
|
parts.join(":")
|
|
26
39
|
end
|
|
27
40
|
end
|
data/lib/pubid/iho/urn_parser.rb
CHANGED
|
@@ -5,53 +5,66 @@ module Pubid
|
|
|
5
5
|
# Parses IHO URNs back into identifiers.
|
|
6
6
|
#
|
|
7
7
|
# UrnGenerator emits:
|
|
8
|
-
# urn:iho:{type-lower}:{number}[:{
|
|
8
|
+
# urn:iho:{type-lower}:{number}[:{tag}.{value}]*[:{version}][:y.{year}]
|
|
9
9
|
#
|
|
10
10
|
# type-lower is the lowercase series letter (s, p, m, b, c).
|
|
11
|
-
#
|
|
12
|
-
# version is the dotted
|
|
11
|
+
# tag is a lowercase label: part, ap, annex, suppl, vol, issue or y.
|
|
12
|
+
# version is the dotted a.b.c edition, the only untagged tail.
|
|
13
|
+
#
|
|
14
|
+
# The number is not escaped, so it can span segments
|
|
15
|
+
# ("IHO S-158:100" gives "urn:iho:s:158:100"). Every untagged
|
|
16
|
+
# segment that is not a version therefore belongs to the number.
|
|
13
17
|
#
|
|
14
18
|
# Examples:
|
|
15
19
|
# - urn:iho:s:44:5.0.0 → IHO S-44 5.0.0
|
|
16
20
|
# - urn:iho:s:100:part.4a:1.0.0 → IHO S-100 Part 4a 1.0.0
|
|
17
21
|
# - urn:iho:s:65:ap.A:1.0.0 → IHO S-65 Ap. A 1.0.0
|
|
22
|
+
# - urn:iho:s:158:100:1.0.0 → IHO S-158:100 1.0.0
|
|
23
|
+
# - urn:iho:p:5:y.2024 → IHO P-5 2024
|
|
24
|
+
# - urn:iho:p:1:vol.30 → IHO P-1 Vol. 30
|
|
25
|
+
# - urn:iho:p:1:vol.30:issue.2 → IHO P-1 Vol. 30 (2)
|
|
18
26
|
class UrnParser < Pubid::UrnParser::Base
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
27
|
+
# Tag => the text that the human-readable form prints for it.
|
|
28
|
+
TAG_TEXT = {
|
|
29
|
+
"part" => ->(v) { " Part #{v}" },
|
|
30
|
+
"ap" => ->(v) { " Ap. #{v}" },
|
|
31
|
+
"annex" => ->(v) { " Annex #{v}" },
|
|
32
|
+
"suppl" => ->(v) { " Suppl #{v}" },
|
|
33
|
+
"vol" => ->(v) { " Vol. #{v}" },
|
|
34
|
+
"issue" => ->(v) { " (#{v})" },
|
|
35
|
+
"y" => ->(v) { " #{v}" },
|
|
24
36
|
}.freeze
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
body = strip_namespace(urn)
|
|
28
|
-
parts = split_parts(body)
|
|
29
|
-
|
|
30
|
-
type_token = parts.fetch(0).upcase
|
|
31
|
-
number = parts.fetch(1)
|
|
32
|
-
idx = 2
|
|
33
|
-
part_label = nil
|
|
34
|
-
if parts[idx] && part_kind?(parts[idx])
|
|
35
|
-
kind, value = parts[idx].split(".", 2)
|
|
36
|
-
label = PART_LABELS.fetch(kind.downcase) { kind.capitalize }
|
|
37
|
-
part_label = "#{label} #{value}"
|
|
38
|
-
idx += 1
|
|
39
|
-
end
|
|
40
|
-
version = parts[idx]
|
|
38
|
+
VERSION_SEGMENT = /\A\d+\.\d+\.\d+\z/
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
def parse_urn(urn)
|
|
41
|
+
type_token, first, *rest = split_parts(strip_namespace(urn))
|
|
42
|
+
number = [first, *rest.take_while { |s| untagged_number?(s) }]
|
|
43
|
+
tail = rest.drop(number.size - 1).map { |s| segment_text(s, urn) }
|
|
44
|
+
flavor_parse("IHO #{type_token.upcase}-#{number.join(':')}#{tail.join}")
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
private
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
def untagged_number?(segment)
|
|
50
|
+
!tagged?(segment) && !segment.match?(VERSION_SEGMENT)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tagged?(segment)
|
|
54
|
+
segment.match?(/\A[a-z]+\./i)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def segment_text(segment, urn)
|
|
58
|
+
return " #{segment}" if segment.match?(VERSION_SEGMENT)
|
|
59
|
+
|
|
60
|
+
tag, value = segment.split(".", 2)
|
|
61
|
+
render = TAG_TEXT[tag.downcase] if tagged?(segment)
|
|
62
|
+
unless render
|
|
63
|
+
raise Pubid::Errors::UrnParseError,
|
|
64
|
+
"Unexpected segment #{segment.inspect} in #{urn.inspect}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
render.call(value)
|
|
55
68
|
end
|
|
56
69
|
end
|
|
57
70
|
end
|
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::
|
|
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
|
data/lib/pubid/isbn/parser.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
data/lib/pubid/iso/CLAUDE.md
CHANGED
|
@@ -45,3 +45,69 @@
|
|
|
45
45
|
needs a lookbehind, because `tc_number.value` and `edition.number.value` are
|
|
46
46
|
different attributes — the first pass stripped `edition.number.value` too,
|
|
47
47
|
and two corrigendum examples caught it.
|
|
48
|
+
|
|
49
|
+
- **Typed model bindings (Phase D): the builder consumes the grammar's bound
|
|
50
|
+
map, not a parse tree.** `Identifier.parse` and `Normalizer` parse through
|
|
51
|
+
`Pubid::Parg::Model.bound(:iso, …)`; `UrnParser#build_identifier` constructs
|
|
52
|
+
the same bound-map dialect. The grammar is edition 2 with every
|
|
53
|
+
builder-consumed capture bound (`grammars/iso.parg`, `bindings identifier`).
|
|
54
|
+
The conversion preserved byte fidelity: `to_s` and `to_hash` are identical
|
|
55
|
+
for all 7,859 fixture inputs replayed against a `main` baseline, and the
|
|
56
|
+
wasm engine agrees with Ruby on every tree and bound map (the
|
|
57
|
+
`corpus-parity` gate). **What to know before editing either side:**
|
|
58
|
+
|
|
59
|
+
- **Supplement layers capture into position-scoped prefixes** — `supp_*`
|
|
60
|
+
(first supplement), `supp2_*`/`supp3_*` (chain layers), `isor_supp_*`
|
|
61
|
+
(ISO/R supplement), `dsupp_*` (directive supplement), `bs1_/bs2_/bs3_`
|
|
62
|
+
(bundle items), `idf_*/idf_supp_*` (the joint's IDF side, whose rules are
|
|
63
|
+
inlined from the imported idf grammar so the names never collide with the
|
|
64
|
+
ISO ones). Bindings.collect_captures collects captures globally by name
|
|
65
|
+
and delivers the FIRST occurrence, so shared names across layers would
|
|
66
|
+
collapse a layer's number/date onto the base's. `Builder#build` peels the
|
|
67
|
+
layers outermost-first, re-keying each prefix onto the builder-input
|
|
68
|
+
names and nesting the rest as the layer's `:base`.
|
|
69
|
+
- **The base document's own edition captures as `base_edition`** wherever a
|
|
70
|
+
supplement layer's third_part can carry its own edition
|
|
71
|
+
(`supplement_base_no_third`, the joint base). The builder re-keys it onto
|
|
72
|
+
`:edition` only at the innermost base — moving it inside a peel step
|
|
73
|
+
mis-assigns the edition to the wrapper on multi-layer chains (the
|
|
74
|
+
`ISO/IEC Guide 98-3 ED1/Suppl 1:2008/Cor 1:2009` spec caught that).
|
|
75
|
+
- **No array bindings.** The wasm engine's array-group harvest pads
|
|
76
|
+
non-firing reps with empty hashes (parsanol-ruby#399), so Ruby and wasm
|
|
77
|
+
bound maps disagree on any input whose parse backtracks past the array
|
|
78
|
+
branch. Two consequences: copublishers capture as ONE scalar span
|
|
79
|
+
(`"/IEC/IEEE"`, at-least-one item, absent when none) that the builder
|
|
80
|
+
splits, and bundle items capture into `bs1_/bs2_/bs3_` prefixes the
|
|
81
|
+
builder assembles into `supplements`. Verified to hold unchanged at the
|
|
82
|
+
1.3.121 pin (the capture-flatten release): the re-baked artifact's
|
|
83
|
+
grammar/entries/bindings/tests/tables are byte-identical to the 1.3.114
|
|
84
|
+
bake. A newer engine's `(raw)` wrapper interiors would replace both
|
|
85
|
+
shapes — re-visit when such a wrapper ships.
|
|
86
|
+
- **The artifact exposes `["identifier", "idf.identifier"]`** — the
|
|
87
|
+
`use idf` import survives the conversion so the joint composition's
|
|
88
|
+
entry surface stays available (the fleet gate asserts the entry list).
|
|
89
|
+
The joint's IDF side itself binds as `idf_*` scalars; the pilot's
|
|
90
|
+
`publisher -> publisher_name (string) preprocess: publisher_names`
|
|
91
|
+
binding is also kept (the T1 materialization asserts the full
|
|
92
|
+
organization name), and the Ruby builder's `cast` returns nil for
|
|
93
|
+
`:publisher_name` — the Ruby identifier has no such attribute. The
|
|
94
|
+
Ruby artifact load resolves `use idf` against `data/parg/idf.parg`,
|
|
95
|
+
which sits beside the vendored artifact.
|
|
96
|
+
- **Captures must inline their bodies to raw terms.** A capture wrapping a
|
|
97
|
+
bare rule ref exposes the ref's child nodes and cannot bind as a string
|
|
98
|
+
(`cannot bind a Array capture`), and date/language bodies are inlined at
|
|
99
|
+
all capture sites for that reason. Multi-line rules must be brace bodies
|
|
100
|
+
(line-mode rejects newlines inside groups).
|
|
101
|
+
- **The URN parser emits the same dialect**: its supplement loop writes
|
|
102
|
+
`supp_`/`supp2_`/`supp3_`-prefixed flat keys (the prefixes the builder
|
|
103
|
+
peels), not `{base:, **supp}` nesting; the builder peels `supp_year` and
|
|
104
|
+
`supp_supplement_version` the same way by prefix. Its raw-stage fallback
|
|
105
|
+
(`:stage`), `:document_type` and `:supplement_version` keys are
|
|
106
|
+
builder-input names, not grammar captures, and pass through `cast`
|
|
107
|
+
unchanged.
|
|
108
|
+
- **Grammar probes**: `ruby -e 'gem "parsanol", "1.3.121"; …'` against the
|
|
109
|
+
artifact (`parse_and_bind`) shows the bound map; `Model.bound(:iso, …)`
|
|
110
|
+
shows the builder's actual input. The artifact and its vendored twin
|
|
111
|
+
(`data/parg/iso.parg`) must move together — the twin is the embedded
|
|
112
|
+
source's provenance copy.
|
|
113
|
+
|