obp-access 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 728e6533423209e6fc2a17945ca7afc15f7b36c8bfe39451e1ad8f66d6554bb5
4
- data.tar.gz: 92a04641a9ebc2774e60518e560871287c9855998848f2d3d2ffddcee95f0221
3
+ metadata.gz: b11820d71fdd40bfeeddd92447062fc728a2642a3bdd4b572fcb3162bd86230a
4
+ data.tar.gz: 4c061e02a1ead75b4db4828d7646d69052a44ed0369bb905c9a0aafb8f064a99
5
5
  SHA512:
6
- metadata.gz: e4f9adc699152992207abfc9986d5e06e8db3a110f37eb2ed1e6cfd4f3dcd57d6f4ee63614ed44b26a40bcf70d355a3ebd2cf51041ba961040a5063a9ee88802
7
- data.tar.gz: 883124569b65db99872fa2f12de85070ee72dea94fa113b6af87d94a8ecc2bb2c61f7eaf0a261eacdb69d817e032c9820810fb5973301043476f0f15768e5989
6
+ metadata.gz: 819be7f79f169115d5e0c486f09e8b061ac68fb85040e764832f0280e63094893c9d2da2428395f32179fff03625906f841ed4dea4020c7aa484873170603d49
7
+ data.tar.gz: ae43acfe12065482d2bfb872e59b0a221416664c43415a49078b206957e5190744b3ebafd60c7f78e5bb15821f04693f876f7de5127238cdabab29af95c125d4
data/CLAUDE.md CHANGED
@@ -9,10 +9,11 @@ Ruby gem (`obp-access`) that fetches informative content (introduction, scope, t
9
9
  ## Commands
10
10
 
11
11
  - **Install dependencies:** `bundle install`
12
- - **Run tests:** `bundle exec rspec`
12
+ - **Run tests:** `bundle exec rspec` (note: retriever/access-fetch specs hit the live OBP API)
13
13
  - **Lint:** `bundle exec rubocop`
14
14
  - **Run a single test:** `bundle exec rspec spec/obp/access/grammar_parser_spec.rb`
15
15
  - **Console:** `bin/console` (loads the gem for interactive use)
16
+ - **CLI:** `bundle exec exe/obp-access` (fetch a document or all languages from OBP)
16
17
 
17
18
  ## Critical rules
18
19
 
@@ -25,13 +26,15 @@ Ruby gem (`obp-access`) that fetches informative content (introduction, scope, t
25
26
 
26
27
  Data flows through these classes in `lib/obp/access/`:
27
28
 
28
- 1. **`Access`** (entry point) — orchestrates the pipeline. `Obp::Access.fetch(urn)` returns an instance per language. `Access.fetch_all(urn, languages:)` returns separate instances per language.
29
+ 1. **`Access`** (entry point) — orchestrates the pipeline. `Obp::Access.fetch(urn)` returns an instance per language. `Access.fetch_all(urn, languages:)` returns separate instances per language. `Access.from_html(urn:, html:, caption:, titles:)` converts an already-downloaded OBP HTML fragment with no network access (the offline path used by fixture-based tests).
29
30
 
30
- 2. **`Parser`** — fetches content from the ISO OBP API (`https://www.iso.org/obp/ui`) via HTTP POST with a URN payload. Parses the JSON response to extract HTML content, titles, and images.
31
+ 2. **`Parser`** — fetches content from the ISO OBP API (`https://www.iso.org/obp/ui`) via HTTP POST with a URN payload. Parses the JSON response to extract HTML content, titles, and images. **`RawHtmlParser`** subclasses it and duck-types the API for pre-captured HTML.
31
32
 
32
- 3. **`Converter`** — wraps the HTML source, normalizes whitespace, parses it with Nokogiri, and passes DOM nodes to the Renderer.
33
+ 3. **`Urn`** — parses URN identity metadata (`originator`, `doc_number` incl. part numbers like `80000:-12` → "80000-12", `doc_type` IS/TS/TR/PAS/Guide/IWA, `edition`, `version`). `Elements::Root` builds `<std-ident>` from these never parse URN segments positionally elsewhere.
33
34
 
34
- 4. **`Renderer`** — recursively walks DOM nodes and dispatches them to element classes registered in `ElementRegistry`. Each element class matches against CSS classes and builds NISO STS XML.
35
+ 4. **`Converter`** — wraps the HTML source, normalizes whitespace, parses it with Nokogiri, and passes DOM nodes to the Renderer.
36
+
37
+ 5. **`Renderer`** — recursively walks DOM nodes and dispatches them to element classes registered in `ElementRegistry`. Each element class matches against CSS classes and builds NISO STS XML.
35
38
 
36
39
  ### Element system
37
40
 
@@ -43,11 +46,15 @@ Data flows through these classes in `lib/obp/access/`:
43
46
 
44
47
  ### Supporting classes
45
48
 
46
- - **`GrammarParser`** — extracts part-of-speech and gender from bold term markup.
49
+ - **`GrammarParser`** — extracts part-of-speech and gender from bold term markup (POS markers `adj.`/`verb`, gender markers `m`/`f`/`n`, 〈bracketed〉 subject fields). Gender/POS only occur in non-English term entries (fr/de/ru); examples live in `spec/obp/access/grammar_parser_spec.rb`.
47
50
  - **`DomainExtractor`** — extracts subject-field domains from definition text.
48
51
  - **`InlineRenderer`** — renders inline HTML elements (links, xrefs, italic, bold, entailed terms) to STS XML.
49
52
  - **`Imager`** — downloads images from OBP. Uses `Parallel` for concurrent downloads.
50
53
 
54
+ ### Testing conventions
55
+
56
+ - Captured OBP HTML fixtures live in `obp-output/` at the repo root (gitignored — download them via the CLI or a browser). Conversion specs read them through `Access.from_html` and **skip** when the fixture is absent, so the offline suite never touches the network.
57
+
51
58
  ## Key dependencies
52
59
 
53
60
  - **sts** — NISO STS gem for generating STS objects from XML
data/README.adoc CHANGED
@@ -54,6 +54,27 @@ obp = Obp::Access.fetch("iso:std:iso:5598:ed-3:v1:en", languages: :all)
54
54
  obp.to_xml(pretty: true) # => NISO STS XML with all available language langSets
55
55
  ----
56
56
 
57
+ ==== Already-downloaded HTML
58
+
59
+ When the OBP page was fetched out of band (e.g. through a browser or
60
+ waffle-punch), build an Access instance from the raw HTML fragment and
61
+ convert it without any further network calls:
62
+
63
+ [source,ruby]
64
+ ----
65
+ obp = Obp::Access.from_html(
66
+ urn: "iso:std:iso:80000-12:ed-2:v2:en",
67
+ html: File.read("iso-80000-12-en.html"),
68
+ caption: "ISO 80000-12:2019",
69
+ titles: { "en" => "Quantities and units — Part 12: Solid state physics" },
70
+ )
71
+ obp.to_xml(pretty: true) # => NISO STS XML
72
+ obp.to_terms # => YAML string of table-carried terms
73
+ ----
74
+
75
+ `caption` is used to build the STS metadata (std-ref, copyright year); it
76
+ should match the document reference. `titles` is optional.
77
+
57
78
  ==== CLI
58
79
 
59
80
  Fetch a single document (English only):
@@ -47,7 +47,7 @@ module Obp
47
47
  end
48
48
 
49
49
  def id
50
- @id ||= node.attr("id").split("_").last
50
+ @id ||= node.attr("id")&.split("_")&.last
51
51
  end
52
52
 
53
53
  def to_xml
@@ -25,18 +25,25 @@ module Obp
25
25
  end
26
26
 
27
27
  def render_fn(xml, note)
28
- xml.fn(id: "fn_#{fn_id(note)}") do
29
- xml.label fn_id(note)
28
+ id = fn_id(note)
29
+ xml.fn(id: "fn_#{id}") do
30
+ xml.label id
30
31
  xml.p fn_text(note)
31
32
  end
32
33
  end
33
34
 
34
35
  def fn_id(note)
35
- note.attr("id").split("_").last
36
+ note.attr("id")&.split("_")&.last || note.object_id.to_s
36
37
  end
37
38
 
38
39
  def fn_text(note)
39
- note.at_css("div").text.strip.sub(/\A\d+\s*/, "")
40
+ div = note.at_css("div")
41
+ if div
42
+ div.text.strip.sub(/\A\d+\s*/,
43
+ "")
44
+ else
45
+ note.text.strip.sub(/\A\d+\s*/, "")
46
+ end
40
47
  end
41
48
  end
42
49
  end
@@ -42,7 +42,7 @@ module Obp
42
42
  def std_meta_content(xml)
43
43
  render_titles(xml)
44
44
  xml.public_send(:"proj-id", ref_undated)
45
- xml.public_send(:"release-version", doc_type)
45
+ xml.public_send(:"release-version", urn.doc_type)
46
46
  render_std_ident(xml)
47
47
  xml.public_send(:"content-language", metas["language"])
48
48
  xml.public_send(:"std-ref", ref_dated, type: "dated")
@@ -55,10 +55,10 @@ module Obp
55
55
  def render_std_ident(xml)
56
56
  xml.public_send(:"std-ident") do
57
57
  xml.originator holder
58
- xml.public_send(:"doc-type", doc_type)
59
- xml.public_send(:"doc-number", urn_parts[3])
60
- xml.edition urn_parts[4].delete_prefix("ed-")
61
- xml.version urn_parts[5].delete_prefix("v")
58
+ xml.public_send(:"doc-type", urn.doc_type)
59
+ xml.public_send(:"doc-number", urn.doc_number)
60
+ xml.edition urn.edition
61
+ xml.version urn.version
62
62
  end
63
63
  end
64
64
 
@@ -85,35 +85,27 @@ module Obp
85
85
  end
86
86
 
87
87
  def holder
88
- metas["caption"].split.first
88
+ caption&.split(/[[:space:]]/)&.first || urn.originator.upcase
89
89
  end
90
90
 
91
91
  def ref
92
- metas["caption"]
92
+ caption || urn.to_s
93
93
  end
94
94
 
95
95
  def ref_dated
96
- metas["caption"].gsub(/\(.*?\)/, "")
96
+ caption&.gsub(/\([^()]*\)/, "") || ref
97
97
  end
98
98
 
99
99
  def ref_undated
100
- @ref_undated ||= metas["caption"].split(":").first
100
+ @ref_undated ||= caption&.split(":")&.first || urn.doc_number
101
101
  end
102
102
 
103
103
  def copyright_year
104
- metas["caption"][/:(\d{4})/, 1]
104
+ caption&.[](/:(\d{4})/, 1)
105
105
  end
106
106
 
107
- def doc_type
108
- case urn_parts[3]
109
- when "ts" then "TS"
110
- when "tr" then "TR"
111
- else "IS"
112
- end
113
- end
114
-
115
- def urn_parts
116
- @urn_parts ||= urn.raw.split(":")
107
+ def caption
108
+ metas["caption"]
117
109
  end
118
110
  end
119
111
  end
@@ -36,7 +36,10 @@ module Obp
36
36
  # following rows when the builder re-parses the fragment. OBP's
37
37
  # "sts-unknown-element" placeholders are stripped first.
38
38
  def table_markup
39
- table = node.at_css("table").dup
39
+ table = node.at_css("table")
40
+ return "" unless table
41
+
42
+ table = table.dup
40
43
  table.css(".sts-unknown-element").each(&:remove)
41
44
  table.children.map(&:to_xml).join
42
45
  end
@@ -48,7 +48,10 @@ module Obp
48
48
  "v-loc" => "#{API_URL}##{@urn}",
49
49
  )
50
50
 
51
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
51
+ Net::HTTP.start(
52
+ uri.hostname, uri.port, use_ssl: true,
53
+ open_timeout: 30, read_timeout: 60
54
+ ) do |http|
52
55
  http.request(request)
53
56
  end
54
57
  end
@@ -55,7 +55,7 @@ module Obp
55
55
  end
56
56
 
57
57
  def tab_data
58
- @tab_data ||= state.filter_map { |attr| attr["tabs"] }.first.last
58
+ @tab_data ||= state.filter_map { |attr| attr["tabs"] }.first&.last || {}
59
59
  end
60
60
 
61
61
  def titles
@@ -0,0 +1,41 @@
1
+ module Obp
2
+ class Access
3
+ # Parser that converts an already-downloaded OBP HTML fragment (e.g.
4
+ # captured via a browser / waffle-punch) without touching the network.
5
+ # Duck-types with Parser for the Access pipeline.
6
+ class RawHtmlParser < Parser
7
+ def initialize(urn:, directory:, html:, caption: nil, titles: nil)
8
+ super(urn:, directory:)
9
+ @raw_html = html
10
+ @caption = caption
11
+ @raw_titles = titles
12
+ end
13
+
14
+ def html
15
+ @raw_html
16
+ end
17
+
18
+ def available_languages
19
+ [urn.language]
20
+ end
21
+
22
+ private
23
+
24
+ def state
25
+ raise "RawHtmlParser does not fetch OBP state"
26
+ end
27
+
28
+ def tab_data
29
+ { "caption" => @caption, "description" => title }
30
+ end
31
+
32
+ def title
33
+ @caption
34
+ end
35
+
36
+ def titles
37
+ @raw_titles || { urn.language => title }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,6 +1,8 @@
1
1
  module Obp
2
2
  class Access
3
3
  class Urn
4
+ DOC_TYPE_SEGMENTS = %w[ts tr pas guide iwa].freeze
5
+
4
6
  attr_reader :raw, :language, :base
5
7
 
6
8
  def initialize(raw)
@@ -26,6 +28,59 @@ module Obp
26
28
  def hash
27
29
  raw.hash
28
30
  end
31
+
32
+ def parts
33
+ @parts ||= raw.split(":")
34
+ end
35
+
36
+ # Originator segment: "iso", "iec", "itu", ...
37
+ def originator
38
+ parts[2]
39
+ end
40
+
41
+ # Document type segment if present ("ts", "tr", ...), else nil
42
+ def doc_type_segment
43
+ segments = identifier_segments
44
+ segments.find { |s| DOC_TYPE_SEGMENTS.include?(s) }
45
+ end
46
+
47
+ # Document number, including part number when present.
48
+ # "iso:std:iso:80000:-12:ed-2:v1:en" → "80000-12"
49
+ def doc_number
50
+ identifier_segments
51
+ .reject { |s| DOC_TYPE_SEGMENTS.include?(s) }
52
+ .join
53
+ end
54
+
55
+ def edition
56
+ segment = parts.find { |p| p.start_with?("ed-") }
57
+ segment&.delete_prefix("ed-")
58
+ end
59
+
60
+ def version
61
+ segment = parts.find { |p| p.match?(/\Av\d+\z/) }
62
+ segment&.delete_prefix("v")
63
+ end
64
+
65
+ def doc_type
66
+ case doc_type_segment
67
+ when "ts" then "TS"
68
+ when "tr" then "TR"
69
+ when "pas" then "PAS"
70
+ when "guide" then "Guide"
71
+ when "iwa" then "IWA"
72
+ else "IS"
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # Segments between the originator and the edition segment.
79
+ def identifier_segments
80
+ start_index = 3
81
+ end_index = parts.index { |p| p.start_with?("ed-") } || parts.size
82
+ parts[start_index...end_index]
83
+ end
29
84
  end
30
85
  end
31
86
  end
@@ -1,5 +1,5 @@
1
1
  module Obp
2
2
  class Access
3
- VERSION = "0.1.5".freeze
3
+ VERSION = "0.1.6".freeze
4
4
  end
5
5
  end
data/lib/obp/access.rb CHANGED
@@ -16,6 +16,7 @@ require_relative "access/retriever"
16
16
  require_relative "access/fetcher"
17
17
  require_relative "access/element_registry"
18
18
  require_relative "access/parser"
19
+ require_relative "access/raw_html_parser"
19
20
  require_relative "access/converter"
20
21
  require_relative "access/imager"
21
22
  require_relative "access/renderer"
@@ -76,6 +77,15 @@ module Obp
76
77
  resolved.map { |lang| new(Urn.new("#{urn.base}:#{lang}")) }
77
78
  end
78
79
 
80
+ # Build an Access from an already-downloaded OBP HTML fragment (e.g.
81
+ # captured via a browser or waffle-punch). No network requests are made.
82
+ def self.from_html(urn:, html:, caption: nil, titles: nil)
83
+ raise ArgumentError, "URN is required" unless urn
84
+ raise ArgumentError, "HTML is required" unless html
85
+
86
+ new(Urn.new(urn), html: html, caption: caption, titles: titles)
87
+ end
88
+
79
89
  def self.resolve_languages(primary, requested, available)
80
90
  case requested
81
91
  when :all then [primary] | available
@@ -84,8 +94,11 @@ module Obp
84
94
  end
85
95
  end
86
96
 
87
- def initialize(urn)
97
+ def initialize(urn, html: nil, caption: nil, titles: nil)
88
98
  @urn = urn
99
+ @raw_html = html
100
+ @caption = caption
101
+ @raw_titles = titles
89
102
  end
90
103
 
91
104
  def to_xml(pretty: false)
@@ -119,10 +132,20 @@ module Obp
119
132
  parser.available_languages
120
133
  end
121
134
 
135
+ # Raw sts-standard HTML fragment as served by the OBP.
136
+ def html
137
+ parser.html
138
+ end
139
+
122
140
  private
123
141
 
124
142
  def parser
125
- @parser ||= Parser.new(urn:, directory: tmpdir)
143
+ @parser ||= if @raw_html
144
+ RawHtmlParser.new(urn:, directory: tmpdir, html: @raw_html,
145
+ caption: @caption, titles: @raw_titles)
146
+ else
147
+ Parser.new(urn:, directory: tmpdir)
148
+ end
126
149
  end
127
150
 
128
151
  # Shares Parser#html (memoized) with the STS conversion path, so
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/obp/access/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "obp-access"
7
+ spec.version = Obp::Access::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = ""
12
+ spec.description = ""
13
+ spec.homepage = ""
14
+ spec.required_ruby_version = ">= 3.1"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(__dir__) do
19
+ `git ls-files -z`.split("\x0").reject do |f|
20
+ (File.expand_path(f) == __FILE__) ||
21
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor
22
+ Gemfile])
23
+ end
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+ spec.add_dependency "nokogiri"
29
+ spec.add_dependency "parallel"
30
+ spec.add_dependency "sts"
31
+ spec.add_dependency "thor"
32
+ spec.metadata["rubygems_mfa_required"] = "true"
33
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: obp-access
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-18 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -125,12 +125,14 @@ files:
125
125
  - lib/obp/access/imager.rb
126
126
  - lib/obp/access/inline_renderer.rb
127
127
  - lib/obp/access/parser.rb
128
+ - lib/obp/access/raw_html_parser.rb
128
129
  - lib/obp/access/renderer.rb
129
130
  - lib/obp/access/retriever.rb
130
131
  - lib/obp/access/table_mapper.rb
131
132
  - lib/obp/access/table_term_extractor.rb
132
133
  - lib/obp/access/urn.rb
133
134
  - lib/obp/access/version.rb
135
+ - obp-access.gemspec
134
136
  homepage: ''
135
137
  licenses: []
136
138
  metadata: