pubid 2.0.0.pre.alpha.5 → 2.0.0.pre.alpha.7
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/README.adoc +40 -0
- data/lib/pubid/amca.rb +6 -0
- data/lib/pubid/ansi.rb +7 -0
- data/lib/pubid/api.rb +5 -0
- data/lib/pubid/ashrae.rb +6 -0
- data/lib/pubid/asme.rb +5 -0
- data/lib/pubid/astm.rb +5 -0
- data/lib/pubid/bsi/builder.rb +41 -48
- data/lib/pubid/bsi/identifiers/amendment.rb +28 -0
- data/lib/pubid/bsi/identifiers/consolidated_identifier.rb +11 -1
- data/lib/pubid/bsi/identifiers/corrigendum.rb +24 -0
- data/lib/pubid/bsi/identifiers/expert_commentary.rb +5 -0
- data/lib/pubid/bsi/identifiers/flex.rb +4 -0
- data/lib/pubid/bsi/parser.rb +19 -5
- data/lib/pubid/bsi/renderer.rb +19 -17
- data/lib/pubid/bsi.rb +15 -0
- data/lib/pubid/ccsds.rb +5 -0
- data/lib/pubid/cen_cenelec.rb +7 -0
- data/lib/pubid/cie.rb +5 -0
- data/lib/pubid/csa.rb +5 -0
- data/lib/pubid/etsi.rb +7 -0
- data/lib/pubid/iala/builder.rb +107 -0
- data/lib/pubid/iala/identifier.rb +67 -0
- data/lib/pubid/iala/identifiers/advice.rb +15 -0
- data/lib/pubid/iala/identifiers/annex.rb +55 -0
- data/lib/pubid/iala/identifiers/base.rb +14 -0
- data/lib/pubid/iala/identifiers/general_assembly.rb +16 -0
- data/lib/pubid/iala/identifiers/guideline.rb +15 -0
- data/lib/pubid/iala/identifiers/letter.rb +16 -0
- data/lib/pubid/iala/identifiers/manual.rb +16 -0
- data/lib/pubid/iala/identifiers/model_course.rb +15 -0
- data/lib/pubid/iala/identifiers/recommendation.rb +15 -0
- data/lib/pubid/iala/identifiers/report.rb +16 -0
- data/lib/pubid/iala/identifiers/resolution.rb +16 -0
- data/lib/pubid/iala/identifiers/standard.rb +15 -0
- data/lib/pubid/iala/identifiers.rb +20 -0
- data/lib/pubid/iala/parser.rb +130 -0
- data/lib/pubid/iala/renderer.rb +36 -0
- data/lib/pubid/iala/urn_generator.rb +49 -0
- data/lib/pubid/iala/urn_parser.rb +50 -0
- data/lib/pubid/iala.rb +71 -0
- data/lib/pubid/identifier.rb +54 -1
- data/lib/pubid/idf.rb +5 -0
- data/lib/pubid/iec.rb +8 -0
- data/lib/pubid/ieee.rb +8 -0
- data/lib/pubid/iho.rb +5 -0
- data/lib/pubid/iso.rb +6 -0
- data/lib/pubid/itu.rb +5 -0
- data/lib/pubid/jcgm.rb +5 -0
- data/lib/pubid/jis.rb +6 -0
- data/lib/pubid/nist.rb +13 -0
- data/lib/pubid/oiml/builder.rb +33 -0
- data/lib/pubid/oiml/identifier.rb +1 -0
- data/lib/pubid/oiml/identifiers/bulletin.rb +101 -0
- data/lib/pubid/oiml/identifiers.rb +1 -0
- data/lib/pubid/oiml/parser.rb +43 -1
- data/lib/pubid/oiml/renderer.rb +51 -0
- data/lib/pubid/oiml/urn_generator.rb +22 -0
- data/lib/pubid/oiml.rb +5 -0
- data/lib/pubid/plateau.rb +5 -0
- data/lib/pubid/prefixes_support.rb +51 -0
- data/lib/pubid/sae.rb +6 -0
- data/lib/pubid/version.rb +1 -1
- data/lib/pubid.rb +69 -0
- metadata +24 -2
data/lib/pubid/etsi.rb
CHANGED
|
@@ -4,6 +4,13 @@ require "lutaml/model"
|
|
|
4
4
|
|
|
5
5
|
module Pubid
|
|
6
6
|
module Etsi
|
|
7
|
+
extend Pubid::PrefixesSupport
|
|
8
|
+
|
|
9
|
+
# Sole ETSI publisher token (see the parser's `etsi_prefix` rule). The
|
|
10
|
+
# document-type tokens (EN, TS, TR, ...) are sub-types that follow the ETSI
|
|
11
|
+
# prefix, not standalone routing tokens, so they are excluded.
|
|
12
|
+
PREFIXES = ["ETSI"].freeze
|
|
13
|
+
|
|
7
14
|
autoload :Builder, "#{__dir__}/etsi/builder"
|
|
8
15
|
autoload :Components, "#{__dir__}/etsi/components"
|
|
9
16
|
autoload :Identifier, "#{__dir__}/etsi/identifier"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
# Builds an IALA identifier object from the Parslet parse tree.
|
|
6
|
+
class Builder
|
|
7
|
+
def build(parsed)
|
|
8
|
+
hash = parsed
|
|
9
|
+
|
|
10
|
+
return build_annex(hash) if hash[:annex_marker]
|
|
11
|
+
|
|
12
|
+
type_letter = stringify(hash[:type_letter])&.upcase
|
|
13
|
+
|
|
14
|
+
attrs = {
|
|
15
|
+
number: build_number(hash, type_letter),
|
|
16
|
+
edition: stringify(hash.dig(:edition, :edition_value)),
|
|
17
|
+
language: stringify(hash.dig(:language_group, :language))&.upcase,
|
|
18
|
+
}.compact
|
|
19
|
+
|
|
20
|
+
Iala.identifier_klass_for_type_letter(type_letter).new(**attrs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.build(parsed)
|
|
24
|
+
new.build(parsed)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Build the Annex wrapper from the parse tree. The base sub-tree
|
|
30
|
+
# (captured under :base) is recursively built via the same logic as
|
|
31
|
+
# a flat identifier, then wrapped.
|
|
32
|
+
def build_annex(hash)
|
|
33
|
+
base_hash = hash[:base]
|
|
34
|
+
base_type = stringify(base_hash[:type_letter])&.upcase
|
|
35
|
+
base_number = build_number(base_hash, base_type)
|
|
36
|
+
base = Iala.identifier_klass_for_type_letter(base_type).new(number: base_number)
|
|
37
|
+
|
|
38
|
+
attrs = {
|
|
39
|
+
base_identifier: base,
|
|
40
|
+
annex_form: stringify(hash[:annex_marker]),
|
|
41
|
+
letter: stringify(hash[:annex_letter]),
|
|
42
|
+
edition: stringify(hash.dig(:edition, :edition_value)),
|
|
43
|
+
language: stringify(hash.dig(:language_group, :language))&.upcase,
|
|
44
|
+
}.compact
|
|
45
|
+
|
|
46
|
+
Identifiers::Annex.new(**attrs)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Compose the full number string from the leading doc_number plus any
|
|
50
|
+
# dotted continuation (GA01.13 → "01.13", L2.1.11 → "2.1.11") plus
|
|
51
|
+
# the optional dash-separated subpart (C0103-1 → "0103-1", L2.7.1-2
|
|
52
|
+
# → "2.7.1-2"). Padding is type-aware per IALA's canonical form:
|
|
53
|
+
# typed S/R/G/M/C zero-pad to 4 digits; GeneralAssembly (GA) zero-
|
|
54
|
+
# pads each segment to 2; Advices (A) and Letters (L) preserve input
|
|
55
|
+
# (their canonical form is already what the IALA site prints).
|
|
56
|
+
def build_number(hash, type_letter = nil)
|
|
57
|
+
base = stringify(hash[:doc_number])
|
|
58
|
+
return base unless base
|
|
59
|
+
|
|
60
|
+
base = canonical_base(base, type_letter)
|
|
61
|
+
|
|
62
|
+
dots = hash[:doc_number_dots]
|
|
63
|
+
dots_str = dots.is_a?(Array) ? "" : canonical_dots(stringify(dots).to_s, type_letter)
|
|
64
|
+
|
|
65
|
+
subpart_str = hash[:subpart] ? "-#{subpart_to_s(hash[:subpart])}" : ""
|
|
66
|
+
subpart_str = "" if subpart_str == "-"
|
|
67
|
+
"#{base}#{dots_str}#{subpart_str}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Per-type canonical width for the leading numeric run.
|
|
71
|
+
def canonical_base(base, type_letter)
|
|
72
|
+
case type_letter
|
|
73
|
+
when "S", "R", "G", "M", "C", "X", "P" then base.rjust(4, "0")
|
|
74
|
+
when "GA" then base.rjust(2, "0")
|
|
75
|
+
else base
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Per-type canonical width for each dotted continuation segment.
|
|
80
|
+
# `str` looks like ".13" or ".1.11" (leading dot, then digits).
|
|
81
|
+
def canonical_dots(str, type_letter)
|
|
82
|
+
return "" if str.empty?
|
|
83
|
+
|
|
84
|
+
if type_letter == "GA"
|
|
85
|
+
str.split(".", -1).map { |seg| seg.empty? ? seg : seg.rjust(2, "0") }.join(".")
|
|
86
|
+
else
|
|
87
|
+
str
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The subpart capture may be an array of {subpart_number: "1"},
|
|
92
|
+
# {subpart_number: "9"}, … (repeated matches). Join with "-".
|
|
93
|
+
def subpart_to_s(subpart)
|
|
94
|
+
return stringify(subpart).to_s unless subpart.is_a?(Array)
|
|
95
|
+
|
|
96
|
+
subpart.filter_map { |h| h.is_a?(Hash) ? stringify(h[:subpart_number]) : nil }.join("-")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stringify(value)
|
|
100
|
+
return nil if value.nil?
|
|
101
|
+
|
|
102
|
+
str = value.to_s
|
|
103
|
+
str.empty? ? nil : str
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
# Base class for all IALA identifiers. Canonical name
|
|
6
|
+
# Pubid::Iala::Identifier; every concrete IALA identifier
|
|
7
|
+
# (Identifiers::*) descends from it, and Identifiers::Base — aliased
|
|
8
|
+
# at the foot of identifiers/base.rb — points back to it.
|
|
9
|
+
#
|
|
10
|
+
# IALA identifiers have the form:
|
|
11
|
+
# [IALA ]{S|R|G|M|C|X|P}<4-digit number>[-<subpart>][ Ed <edition>][ (<LangLetter>)]
|
|
12
|
+
#
|
|
13
|
+
# Examples:
|
|
14
|
+
# S1070
|
|
15
|
+
# IALA S1070 Ed 2.0
|
|
16
|
+
# R1016:ed2.0(F)
|
|
17
|
+
# C0103-1 Ed 3.0
|
|
18
|
+
class Identifier < ::Pubid::Identifier
|
|
19
|
+
# Parse an IALA identifier string into an identifier object.
|
|
20
|
+
# @param identifier [String]
|
|
21
|
+
# @return [Pubid::Iala::Identifier]
|
|
22
|
+
# @raise [Parslet::ParseFailed] If parsing fails
|
|
23
|
+
def self.parse(identifier)
|
|
24
|
+
parsed = Parser.parse(identifier)
|
|
25
|
+
Builder.build(parsed)
|
|
26
|
+
rescue Parslet::ParseFailed => e
|
|
27
|
+
raise "Failed to parse IALA identifier '#{identifier}': #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attribute :publisher, :string, default: "IALA"
|
|
31
|
+
attribute :number, :string # "1070", "0126", "0103-1"
|
|
32
|
+
attribute :edition, :string # "2.0", "1.3", nil
|
|
33
|
+
attribute :language, :string # "E", "F", "S", "C", "A", "R", nil
|
|
34
|
+
|
|
35
|
+
IALA_TYPE_MAP = {
|
|
36
|
+
"pubid:iala:standard" => "Pubid::Iala::Identifiers::Standard",
|
|
37
|
+
"pubid:iala:recommendation" => "Pubid::Iala::Identifiers::Recommendation",
|
|
38
|
+
"pubid:iala:guideline" => "Pubid::Iala::Identifiers::Guideline",
|
|
39
|
+
"pubid:iala:manual" => "Pubid::Iala::Identifiers::Manual",
|
|
40
|
+
"pubid:iala:model-course" => "Pubid::Iala::Identifiers::ModelCourse",
|
|
41
|
+
"pubid:iala:advice" => "Pubid::Iala::Identifiers::Advice",
|
|
42
|
+
"pubid:iala:general-assembly" => "Pubid::Iala::Identifiers::GeneralAssembly",
|
|
43
|
+
"pubid:iala:letter" => "Pubid::Iala::Identifiers::Letter",
|
|
44
|
+
"pubid:iala:annex" => "Pubid::Iala::Identifiers::Annex",
|
|
45
|
+
"pubid:iala:report" => "Pubid::Iala::Identifiers::Report",
|
|
46
|
+
"pubid:iala:resolution" => "Pubid::Iala::Identifiers::Resolution",
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
key_value do
|
|
50
|
+
map "_type", to: :_type, polymorphic_map: IALA_TYPE_MAP
|
|
51
|
+
map "number", to: :number
|
|
52
|
+
map "edition", to: :edition
|
|
53
|
+
map "language", to: :language
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_urn
|
|
57
|
+
UrnGenerator.new(self).generate
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Single-letter type code (S, R, G, …) — convenience accessor used
|
|
61
|
+
# by Renderer and UrnGenerator.
|
|
62
|
+
def type_letter
|
|
63
|
+
self.class.type[:short]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Advices (A prefix). Numbering is dashed: A12-01, A13-04.
|
|
7
|
+
# Carries no edition in the corpus.
|
|
8
|
+
class Advice < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :advice, title: "Advice", short: "A" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Annex to a base publication. Two surface forms appear in the
|
|
7
|
+
# corpus, both wrapping a Guideline:
|
|
8
|
+
#
|
|
9
|
+
# "G1045 Annex Ed 1" — bare Annex, no letter
|
|
10
|
+
# "G1128 ANNEX A Ed 1.6" — uppercase + letter (A/B/C/D)
|
|
11
|
+
#
|
|
12
|
+
# The case of the marker is preserved (Annex vs ANNEX) for round-trip
|
|
13
|
+
# fidelity. Edition and language apply to the Annex itself, not the base.
|
|
14
|
+
class Annex < Base
|
|
15
|
+
# The publication this annex supplements. Always present; carries the
|
|
16
|
+
# type letter and number (e.g. G1045).
|
|
17
|
+
attribute :base_identifier, ::Pubid::Iala::Identifier, polymorphic: true
|
|
18
|
+
# "Annex" or "ANNEX" — preserved verbatim for exact round-trip.
|
|
19
|
+
attribute :annex_form, :string
|
|
20
|
+
# "A", "B", "C", "D", or nil for the bare form.
|
|
21
|
+
attribute :letter, :string
|
|
22
|
+
|
|
23
|
+
key_value do
|
|
24
|
+
map "base_identifier",
|
|
25
|
+
with: { to: :base_identifier_to_kv,
|
|
26
|
+
from: :base_identifier_from_kv }
|
|
27
|
+
map "annex_form", to: :annex_form
|
|
28
|
+
map "letter", to: :letter
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.type
|
|
32
|
+
{ key: :annex, title: "Annex", short: nil }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def base_identifier_to_kv(model, doc)
|
|
38
|
+
base = model.base_identifier
|
|
39
|
+
return unless base
|
|
40
|
+
|
|
41
|
+
doc.add_child(
|
|
42
|
+
Lutaml::KeyValue::DataModel::Element.new("base_identifier",
|
|
43
|
+
base.to_hash),
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def base_identifier_from_kv(model, value)
|
|
48
|
+
return unless value
|
|
49
|
+
|
|
50
|
+
model.base_identifier = ::Pubid::Iala::Identifier.from_hash(value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Backward-compatible alias: IALA's base class used to be
|
|
4
|
+
# Pubid::Iala::Identifiers::Base. It is now Pubid::Iala::Identifier
|
|
5
|
+
# (defined in identifier.rb), which is loaded by the parent autoload.
|
|
6
|
+
require_relative "../identifier" unless defined?(Pubid::Iala::Identifier)
|
|
7
|
+
|
|
8
|
+
module Pubid
|
|
9
|
+
module Iala
|
|
10
|
+
module Identifiers
|
|
11
|
+
Base = Pubid::Iala::Identifier
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA General Assembly resolutions (GA prefix). Numbering is dotted
|
|
7
|
+
# with a series and an index: GA01.01, GA01.13. No edition.
|
|
8
|
+
class GeneralAssembly < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :"general-assembly", title: "General Assembly Resolution",
|
|
11
|
+
short: "GA" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Guidelines (G series).
|
|
7
|
+
# Examples: G1015 Ed 2.2, G1078 Ed 2.1.
|
|
8
|
+
class Guideline < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :guideline, title: "Guideline", short: "G" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Letters (L prefix). Numbering is dotted with a series, sub-series,
|
|
7
|
+
# and item: L2.1.11, L2.7.1-2 (the trailing -2 is a sub-part on top of
|
|
8
|
+
# the dotted number).
|
|
9
|
+
class Letter < Base
|
|
10
|
+
def self.type
|
|
11
|
+
{ key: :letter, title: "Letter", short: "L" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Manuals (M prefix, planned).
|
|
7
|
+
# Today manuals carry no code (NAVGUIDE, VTS Manual, …); M is
|
|
8
|
+
# reserved for future numbered manuals.
|
|
9
|
+
class Manual < Base
|
|
10
|
+
def self.type
|
|
11
|
+
{ key: :manual, title: "Manual", short: "M" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Model Courses (C series).
|
|
7
|
+
# Examples: C0103-1 Ed 3.0, C2001-1 Ed 1.0.
|
|
8
|
+
class ModelCourse < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :"model-course", title: "Model Course", short: "C" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Recommendations (R series).
|
|
7
|
+
# Examples: R0103 Ed 3.1, R0126 Ed 2.0, R1024 Ed 1.0.
|
|
8
|
+
class Recommendation < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :recommendation, title: "Recommendation", short: "R" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Reports & Proceedings (X prefix, planned).
|
|
7
|
+
# Today reports carry no code; X is reserved for future numbered
|
|
8
|
+
# reports.
|
|
9
|
+
class Report < Base
|
|
10
|
+
def self.type
|
|
11
|
+
{ key: :report, title: "Report", short: "X" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Resolutions and other Council publications (P prefix,
|
|
7
|
+
# planned). Today resolutions carry no code; P is reserved for
|
|
8
|
+
# future numbered resolutions.
|
|
9
|
+
class Resolution < Base
|
|
10
|
+
def self.type
|
|
11
|
+
{ key: :resolution, title: "Resolution", short: "P" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
# IALA Standards (S series).
|
|
7
|
+
# Examples: S1010, S1020 Ed 2.0, S1070 Ed 2.0.
|
|
8
|
+
class Standard < Base
|
|
9
|
+
def self.type
|
|
10
|
+
{ key: :standard, title: "Standard", short: "S" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
module Identifiers
|
|
6
|
+
autoload :Base, "#{__dir__}/identifiers/base"
|
|
7
|
+
autoload :Advice, "#{__dir__}/identifiers/advice"
|
|
8
|
+
autoload :Annex, "#{__dir__}/identifiers/annex"
|
|
9
|
+
autoload :Standard, "#{__dir__}/identifiers/standard"
|
|
10
|
+
autoload :Recommendation, "#{__dir__}/identifiers/recommendation"
|
|
11
|
+
autoload :Guideline, "#{__dir__}/identifiers/guideline"
|
|
12
|
+
autoload :Manual, "#{__dir__}/identifiers/manual"
|
|
13
|
+
autoload :ModelCourse, "#{__dir__}/identifiers/model_course"
|
|
14
|
+
autoload :GeneralAssembly, "#{__dir__}/identifiers/general_assembly"
|
|
15
|
+
autoload :Letter, "#{__dir__}/identifiers/letter"
|
|
16
|
+
autoload :Report, "#{__dir__}/identifiers/report"
|
|
17
|
+
autoload :Resolution, "#{__dir__}/identifiers/resolution"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Pubid
|
|
6
|
+
module Iala
|
|
7
|
+
# Parslet grammar for IALA identifiers.
|
|
8
|
+
#
|
|
9
|
+
# Accepts every shape that appears on the IALA website or cover page:
|
|
10
|
+
#
|
|
11
|
+
# S1070
|
|
12
|
+
# S1070 Ed 2.0
|
|
13
|
+
# IALA S1070 Ed 2.0
|
|
14
|
+
# S1070 Ed 2.0 (F)
|
|
15
|
+
# R1016:ed2.0(F)
|
|
16
|
+
# C0103-1
|
|
17
|
+
# C0103-1 Ed 3.0
|
|
18
|
+
class Parser < Parslet::Parser
|
|
19
|
+
include ::Pubid::Parser::CommonParseRules
|
|
20
|
+
|
|
21
|
+
root :identifier
|
|
22
|
+
|
|
23
|
+
rule(:space) { str(" ") }
|
|
24
|
+
rule(:space?) { space.maybe }
|
|
25
|
+
rule(:dash) { str("-") }
|
|
26
|
+
rule(:colon) { str(":") }
|
|
27
|
+
rule(:dot) { str(".") }
|
|
28
|
+
rule(:lparen) { str("(") }
|
|
29
|
+
rule(:rparen) { str(")") }
|
|
30
|
+
|
|
31
|
+
# Type prefix — IALA document classes. GA must be tried before G so it
|
|
32
|
+
# isn't left with a dangling "A". Captured to route to the right
|
|
33
|
+
# Identifiers::* subclass.
|
|
34
|
+
rule(:type_letter) do
|
|
35
|
+
(str("GA") | str("S") | str("R") | str("G") | str("M") |
|
|
36
|
+
str("C") | str("A") | str("L") | str("X") | str("P")).as(:type_letter)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Document number — one or more decimal digits. The dataset historically
|
|
40
|
+
# zero-pads to 4 (S1070, R0126, M0001) but the canonical form going
|
|
41
|
+
# forward is unpadded (M1, GA1.1). Accept any length so both shapes
|
|
42
|
+
# round-trip; the number attribute preserves whatever was parsed.
|
|
43
|
+
rule(:doc_number) do
|
|
44
|
+
match("[0-9]").repeat(1).as(:doc_number)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Dotted continuation that some prefixes use as part of the number:
|
|
48
|
+
# GA01.01 (series.index), L2.1.11 (series.sub.item). Captured into the
|
|
49
|
+
# same :doc_number atom so the number attribute stays a single string
|
|
50
|
+
# (e.g. "01.01", "2.1.11") and the existing builder needs no changes.
|
|
51
|
+
rule(:dotted_number) do
|
|
52
|
+
dot >> match("[0-9]").repeat(1)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
rule(:number_with_dots) do
|
|
56
|
+
doc_number >> dotted_number.repeat.as(:doc_number_dots)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Numeric sub-part suffix(es): "-1", "-9-10", "-11". The catalogue
|
|
60
|
+
# uses ranges like R0124-9-10 & 11 — that compound is captured
|
|
61
|
+
# verbatim and resolved by the caller, not here.
|
|
62
|
+
rule(:subpart) do
|
|
63
|
+
(dash >> digits.as(:subpart_number)).repeat(1).as(:subpart)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
rule(:code) do
|
|
67
|
+
type_letter >> number_with_dots >> subpart.maybe
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Edition forms observed on covers and listing pages:
|
|
71
|
+
# " Ed 2.0" — cover-page human form
|
|
72
|
+
# ":ed2.0" — listing-page compact form
|
|
73
|
+
rule(:edition_human) do
|
|
74
|
+
(space >> str("Ed") >> space >> edition_value).as(:edition)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
rule(:edition_compact) do
|
|
78
|
+
(colon >> str("ed") >> edition_value).as(:edition)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
rule(:edition_value) do
|
|
82
|
+
(digits >> (dot >> digits).repeat).as(:edition_value)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
rule(:edition) do
|
|
86
|
+
edition_human | edition_compact
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Language is a single uppercase letter inside parens: (E), (F), …
|
|
90
|
+
rule(:language) do
|
|
91
|
+
(space? >> lparen >>
|
|
92
|
+
(str("E") | str("F") | str("S") | str("C") | str("A") | str("R")).as(:language) >>
|
|
93
|
+
rparen).as(:language_group)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Optional "IALA " prefix.
|
|
97
|
+
rule(:publisher) do
|
|
98
|
+
(str("IALA") >> space).maybe
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
rule(:identifier) do
|
|
102
|
+
annex_identifier | (publisher >> code >> edition.maybe >> language.maybe)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Annex to a base publication. Wraps the base code and adds an
|
|
106
|
+
# "Annex" / "ANNEX" marker (case preserved verbatim), an optional
|
|
107
|
+
# single-letter suffix (A/B/C/D), then the usual edition + language.
|
|
108
|
+
# Tried before the flat `identifier` so the annex marker isn't left
|
|
109
|
+
# dangling after a successful base-code match.
|
|
110
|
+
#
|
|
111
|
+
# The letter suffix uses a negative lookahead for a following lowercase
|
|
112
|
+
# letter so the "E" in "Ed 1" isn't mis-captured as annex letter "E".
|
|
113
|
+
rule(:annex_identifier) do
|
|
114
|
+
(publisher >> type_letter >> number_with_dots >> subpart.maybe).as(:base) >>
|
|
115
|
+
space >> (str("Annex") | str("ANNEX")).as(:annex_marker) >>
|
|
116
|
+
(space >> match("[A-Z]").as(:annex_letter) >> match("[a-z]").absent?).maybe >>
|
|
117
|
+
edition.maybe >> language.maybe
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Parse a string and return the raw parslet tree.
|
|
121
|
+
# @param string [String]
|
|
122
|
+
# @return [Hash]
|
|
123
|
+
def self.parse(string)
|
|
124
|
+
raise ArgumentError, "IALA identifier string exceeds maximum length" if string.length > ::Pubid::MAX_INPUT_LENGTH
|
|
125
|
+
|
|
126
|
+
new.parse(string.strip)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
# Human-readable renderer for IALA identifiers.
|
|
6
|
+
#
|
|
7
|
+
# Produces strings like:
|
|
8
|
+
# "IALA S1070"
|
|
9
|
+
# "IALA S1070 Ed 2.0"
|
|
10
|
+
# "IALA R1016 Ed 2.0 (F)"
|
|
11
|
+
class Renderer < ::Pubid::Renderers::Base
|
|
12
|
+
def render(context: nil, **_opts)
|
|
13
|
+
id = @id
|
|
14
|
+
|
|
15
|
+
return render_annex(id) if id.is_a?(Identifiers::Annex)
|
|
16
|
+
|
|
17
|
+
rendered = "#{id.publisher} #{id.type_letter}#{id.number}"
|
|
18
|
+
rendered << " Ed #{id.edition}" if id.edition
|
|
19
|
+
rendered << " (#{id.language})" if id.language
|
|
20
|
+
rendered
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Render an Annex wrapper. The base identifier is rendered first, then
|
|
24
|
+
# the marker form is preserved verbatim ("Annex" vs "ANNEX"), then the
|
|
25
|
+
# optional letter, edition, and language.
|
|
26
|
+
def render_annex(id)
|
|
27
|
+
base_str = id.base_identifier.to_s
|
|
28
|
+
rendered = "#{base_str} #{id.annex_form}"
|
|
29
|
+
rendered << " #{id.letter}" if id.letter
|
|
30
|
+
rendered << " Ed #{id.edition}" if id.edition
|
|
31
|
+
rendered << " (#{id.language})" if id.language
|
|
32
|
+
rendered
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pubid
|
|
4
|
+
module Iala
|
|
5
|
+
# Generates URN strings for IALA identifiers using the Maritime Resource
|
|
6
|
+
# Name (MRN) namespace that IALA prints on its cover pages.
|
|
7
|
+
#
|
|
8
|
+
# Format: urn:mrn:iala:pub:<type-lower><number>[:ed<edition>][:<lang-lower>]
|
|
9
|
+
# Example: urn:mrn:iala:pub:s1070:ed2.0
|
|
10
|
+
# urn:mrn:iala:pub:r1016:ed2.0:f
|
|
11
|
+
class UrnGenerator
|
|
12
|
+
attr_reader :identifier
|
|
13
|
+
|
|
14
|
+
def initialize(identifier)
|
|
15
|
+
@identifier = identifier
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate
|
|
19
|
+
return generate_annex if identifier.is_a?(Identifiers::Annex)
|
|
20
|
+
|
|
21
|
+
parts = ["urn:mrn:iala:pub", code_segment]
|
|
22
|
+
parts << "ed#{identifier.edition}" if identifier.edition
|
|
23
|
+
parts << identifier.language.downcase if identifier.language
|
|
24
|
+
parts.join(":")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# URN for an Annex: embeds the base's code, then an "annex" segment
|
|
30
|
+
# (with optional letter suffix), then the annex's own edition/lang.
|
|
31
|
+
# G1045 Annex Ed 1 → urn:mrn:iala:pub:g1045:annex:ed1
|
|
32
|
+
# G1128 ANNEX A Ed 1.6 (E) → urn:mrn:iala:pub:g1128:annex-a:ed1.6:e
|
|
33
|
+
def generate_annex
|
|
34
|
+
base = identifier.base_identifier
|
|
35
|
+
parts = ["urn:mrn:iala:pub",
|
|
36
|
+
"#{base.type_letter.downcase}#{base.number}"]
|
|
37
|
+
annex_seg = identifier.letter ? "annex-#{identifier.letter.downcase}" : "annex"
|
|
38
|
+
parts << annex_seg
|
|
39
|
+
parts << "ed#{identifier.edition}" if identifier.edition
|
|
40
|
+
parts << identifier.language.downcase if identifier.language
|
|
41
|
+
parts.join(":")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def code_segment
|
|
45
|
+
"#{identifier.type_letter.downcase}#{identifier.number}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|