obp-access 0.1.4 → 0.1.5
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/.rubocop.yml +22 -9
- data/.rubocop_todo.yml +108 -1
- data/README.adoc +111 -0
- data/lib/obp/access/element_registry.rb +3 -1
- data/lib/obp/access/elements/base.rb +7 -2
- data/lib/obp/access/elements/disp_formula.rb +53 -0
- data/lib/obp/access/elements/footnotes.rb +47 -0
- data/lib/obp/access/elements/non_normative_example.rb +28 -0
- data/lib/obp/access/elements/protected_content_note.rb +25 -0
- data/lib/obp/access/elements/table_wrap.rb +14 -3
- data/lib/obp/access/inline_renderer.rb +4 -1
- data/lib/obp/access/renderer.rb +23 -11
- data/lib/obp/access/table_mapper.rb +117 -0
- data/lib/obp/access/table_term_extractor.rb +248 -0
- data/lib/obp/access/version.rb +1 -1
- data/lib/obp/access.rb +24 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 728e6533423209e6fc2a17945ca7afc15f7b36c8bfe39451e1ad8f66d6554bb5
|
|
4
|
+
data.tar.gz: 92a04641a9ebc2774e60518e560871287c9855998848f2d3d2ffddcee95f0221
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4f9adc699152992207abfc9986d5e06e8db3a110f37eb2ed1e6cfd4f3dcd57d6f4ee63614ed44b26a40bcf70d355a3ebd2cf51041ba961040a5063a9ee88802
|
|
7
|
+
data.tar.gz: 883124569b65db99872fa2f12de85070ee72dea94fa113b6af87d94a8ecc2bb2c61f7eaf0a261eacdb69d817e032c9820810fb5973301043476f0f15768e5989
|
data/.rubocop.yml
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
# Auto-generated by Cimas: Do not edit it manually!
|
|
2
2
|
# See https://github.com/metanorma/cimas
|
|
3
3
|
inherit_from:
|
|
4
|
+
- https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
|
|
5
|
+
# .rubocop_todo.yml MUST be the last entry. inherit_from is last-wins:
|
|
6
|
+
# if listed before oss-guides, the shared config's stricter Metrics/
|
|
7
|
+
# (MethodLength, BlockLength, etc.) rules override the todo's per-file
|
|
8
|
+
# grandfathering, and the todo becomes inert on those cops. Empirically
|
|
9
|
+
# verified on suma 2026-07-06: with todo listed first, 34 Metrics/* offenses
|
|
10
|
+
# remained; moved last, cleared. Every gem in the metanorma-org fleet
|
|
11
|
+
# already ships a `.rubocop_todo.yml` on its live tree (audited 2026-07-06,
|
|
12
|
+
# 54/54 have it), so this reference is safe to emit unconditionally.
|
|
4
13
|
- .rubocop_todo.yml
|
|
5
|
-
|
|
14
|
+
|
|
15
|
+
# Rubocop plugins enabled centrally so every metanorma-org gem picks them up
|
|
16
|
+
# on cimas sync — best practice belongs at the shared-template layer, not
|
|
17
|
+
# per-repo. Per ronaldtse feedback on metanorma/ci#332.
|
|
18
|
+
plugins:
|
|
19
|
+
- rubocop-rspec
|
|
20
|
+
- rubocop-performance
|
|
21
|
+
- rubocop-rake
|
|
6
22
|
|
|
7
23
|
# local repo-specific modifications
|
|
8
24
|
# ...
|
|
9
25
|
|
|
10
26
|
AllCops:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Layout/LineLength:
|
|
18
|
-
Max: 120
|
|
27
|
+
# 3.3 matches the org-wide minimum being pushed via #274 (Raise minimum
|
|
28
|
+
# Ruby version to 3.3 due to EOL of 3.2 on 2026-03-31). Was 3.4 before
|
|
29
|
+
# this commit — 3.4 was above the org's stated minimum and would have
|
|
30
|
+
# applied Rubocop rules that fail-close on gems still targeting 3.3.
|
|
31
|
+
TargetRubyVersion: 3.3
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-
|
|
3
|
+
# on 2026-07-18 01:14:30 UTC using RuboCop version 1.88.2.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,9 +11,116 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'obp-access.gemspec'
|
|
13
13
|
|
|
14
|
+
# Offense count: 139
|
|
15
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
16
|
+
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
17
|
+
# URISchemes: http, https
|
|
18
|
+
Layout/LineLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- 'fetch_en_html.rb'
|
|
21
|
+
- 'fetch_html.rb'
|
|
22
|
+
- 'lib/obp/access/catalog.rb'
|
|
23
|
+
- 'lib/obp/access/cli.rb'
|
|
24
|
+
- 'lib/obp/access/domain_extractor.rb'
|
|
25
|
+
- 'lib/obp/access/elements/base.rb'
|
|
26
|
+
- 'lib/obp/access/elements/bibliography.rb'
|
|
27
|
+
- 'lib/obp/access/elements/figure_group.rb'
|
|
28
|
+
- 'lib/obp/access/elements/index.rb'
|
|
29
|
+
- 'lib/obp/access/elements/root.rb'
|
|
30
|
+
- 'lib/obp/access/elements/terminology/definition.rb'
|
|
31
|
+
- 'lib/obp/access/elements/terminology/example.rb'
|
|
32
|
+
- 'lib/obp/access/elements/terminology/tig.rb'
|
|
33
|
+
- 'lib/obp/access/elements/terminology/tig_deprecated.rb'
|
|
34
|
+
- 'lib/obp/access/grammar_parser.rb'
|
|
35
|
+
- 'lib/obp/access/inline_renderer.rb'
|
|
36
|
+
- 'lib/obp/access/parser.rb'
|
|
37
|
+
- 'lib/obp/access/retriever.rb'
|
|
38
|
+
- 'obp-access.gemspec'
|
|
39
|
+
- 'spec/obp/access/access_spec.rb'
|
|
40
|
+
- 'spec/obp/access/cli_spec.rb'
|
|
41
|
+
- 'spec/obp/access/deliverable_spec.rb'
|
|
42
|
+
- 'spec/obp/access/elements/bib_ref_spec.rb'
|
|
43
|
+
- 'spec/obp/access/elements/disp_formula_spec.rb'
|
|
44
|
+
- 'spec/obp/access/elements/figure_group_spec.rb'
|
|
45
|
+
- 'spec/obp/access/elements/figure_spec.rb'
|
|
46
|
+
- 'spec/obp/access/elements/footnotes_spec.rb'
|
|
47
|
+
- 'spec/obp/access/elements/index_spec.rb'
|
|
48
|
+
- 'spec/obp/access/elements/non_normative_example_spec.rb'
|
|
49
|
+
- 'spec/obp/access/elements/protected_content_note_spec.rb'
|
|
50
|
+
- 'spec/obp/access/elements/section_title_spec.rb'
|
|
51
|
+
- 'spec/obp/access/elements/table_wrap_spec.rb'
|
|
52
|
+
- 'spec/obp/access/elements/terminology/definition_spec.rb'
|
|
53
|
+
- 'spec/obp/access/elements/terminology/tig_spec.rb'
|
|
54
|
+
- 'spec/obp/access/grammar_parser_spec.rb'
|
|
55
|
+
- 'spec/obp/access/inline_renderer_spec.rb'
|
|
56
|
+
- 'spec/obp/access/retriever_spec.rb'
|
|
57
|
+
|
|
58
|
+
# Offense count: 1
|
|
59
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
60
|
+
Lint/SafeNavigationWithEmpty:
|
|
61
|
+
Exclude:
|
|
62
|
+
- 'lib/obp/access/elements/disp_formula.rb'
|
|
63
|
+
|
|
14
64
|
# Offense count: 3
|
|
15
65
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
16
66
|
Metrics/AbcSize:
|
|
17
67
|
Exclude:
|
|
18
68
|
- 'lib/obp/access/elements/bibliography/bib_ref.rb'
|
|
19
69
|
- 'lib/obp/access/elements/root.rb'
|
|
70
|
+
|
|
71
|
+
# Offense count: 11
|
|
72
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
73
|
+
Metrics/MethodLength:
|
|
74
|
+
Max: 16
|
|
75
|
+
|
|
76
|
+
# Offense count: 1
|
|
77
|
+
# Configuration parameters: IgnoredMetadata.
|
|
78
|
+
RSpec/DescribeClass:
|
|
79
|
+
Exclude:
|
|
80
|
+
- '**/spec/features/**/*'
|
|
81
|
+
- '**/spec/requests/**/*'
|
|
82
|
+
- '**/spec/routing/**/*'
|
|
83
|
+
- '**/spec/system/**/*'
|
|
84
|
+
- '**/spec/views/**/*'
|
|
85
|
+
- 'spec/obp/access/elements/terminology/tig_spec.rb'
|
|
86
|
+
|
|
87
|
+
# Offense count: 44
|
|
88
|
+
# Configuration parameters: CountAsOne.
|
|
89
|
+
RSpec/ExampleLength:
|
|
90
|
+
Max: 26
|
|
91
|
+
|
|
92
|
+
# Offense count: 1
|
|
93
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
94
|
+
RSpec/IteratedExpectation:
|
|
95
|
+
Exclude:
|
|
96
|
+
- 'spec/obp/access/catalog_spec.rb'
|
|
97
|
+
|
|
98
|
+
# Offense count: 1
|
|
99
|
+
RSpec/MultipleDescribes:
|
|
100
|
+
Exclude:
|
|
101
|
+
- 'spec/obp/access/elements/terminology/tig_spec.rb'
|
|
102
|
+
|
|
103
|
+
# Offense count: 65
|
|
104
|
+
RSpec/MultipleExpectations:
|
|
105
|
+
Max: 11
|
|
106
|
+
|
|
107
|
+
# Offense count: 14
|
|
108
|
+
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
109
|
+
# SupportedInflectors: default, active_support
|
|
110
|
+
RSpec/SpecFilePathFormat:
|
|
111
|
+
Exclude:
|
|
112
|
+
- '**/spec/routing/**/*'
|
|
113
|
+
- 'spec/obp/access/access_spec.rb'
|
|
114
|
+
- 'spec/obp/access/elements/bib_ref_spec.rb'
|
|
115
|
+
- 'spec/obp/access/elements/disp_formula_spec.rb'
|
|
116
|
+
- 'spec/obp/access/elements/figure_group_spec.rb'
|
|
117
|
+
- 'spec/obp/access/elements/figure_spec.rb'
|
|
118
|
+
- 'spec/obp/access/elements/footnotes_spec.rb'
|
|
119
|
+
- 'spec/obp/access/elements/index_spec.rb'
|
|
120
|
+
- 'spec/obp/access/elements/non_normative_example_spec.rb'
|
|
121
|
+
- 'spec/obp/access/elements/protected_content_note_spec.rb'
|
|
122
|
+
- 'spec/obp/access/elements/root_spec.rb'
|
|
123
|
+
- 'spec/obp/access/elements/section_title_spec.rb'
|
|
124
|
+
- 'spec/obp/access/elements/section_type_spec.rb'
|
|
125
|
+
- 'spec/obp/access/elements/table_wrap_spec.rb'
|
|
126
|
+
- 'spec/obp/access/elements/terminology/definition_spec.rb'
|
data/README.adoc
CHANGED
|
@@ -86,6 +86,117 @@ The output is NISO STS XML with TBX-Basic terminology markup:
|
|
|
86
86
|
* Domains are extracted as `<tbx:subjectField>`
|
|
87
87
|
* Deprecated terms use `<tbx:normativeAuthorization value="deprecatedTerm"/>`
|
|
88
88
|
|
|
89
|
+
=== Tables and terms as YAML
|
|
90
|
+
|
|
91
|
+
Besides STS XML, the same preview HTML can be read as YAML table data.
|
|
92
|
+
ISO standards that carry terminology in tables rather than TBX term
|
|
93
|
+
sections (the ISO 80000 quantity tables, "list of equivalent terms"
|
|
94
|
+
tables) become machine-readable through two views:
|
|
95
|
+
|
|
96
|
+
[source,ruby]
|
|
97
|
+
----
|
|
98
|
+
obp = Obp::Access.fetch("iso:std:iso:80000-12:ed-2:v2:en")
|
|
99
|
+
obp.to_tables # => YAML string: every table in the document
|
|
100
|
+
obp.to_terms # => YAML string: term entries from term-carrying tables
|
|
101
|
+
----
|
|
102
|
+
|
|
103
|
+
Both reuse the already-fetched preview HTML, so no extra OBP request is
|
|
104
|
+
made. A document without tables yields an empty YAML array (`--- []`).
|
|
105
|
+
|
|
106
|
+
==== Generic tables (`to_tables`)
|
|
107
|
+
|
|
108
|
+
`Obp::Access::TableMapper` maps every table-carrying node —
|
|
109
|
+
`div.sts-table-wrap` and `div.sts-array`, in document order — to plain
|
|
110
|
+
hashes, arrays and strings:
|
|
111
|
+
|
|
112
|
+
[source,yaml]
|
|
113
|
+
----
|
|
114
|
+
- id: iso_std_iso_80000-12_ed-2_v2_en_tab_1 # wrapper id, if present
|
|
115
|
+
section: '3' # containing clause ("index"/"bibl"/... stay raw)
|
|
116
|
+
label: Table 1 # .sts-caption-label, if present
|
|
117
|
+
caption: Quantities and units used in condensed matter physics
|
|
118
|
+
header:
|
|
119
|
+
- - text: Item No.
|
|
120
|
+
- text: Quantity
|
|
121
|
+
colspan: 3 # colspan/rowspan only when greater than 1
|
|
122
|
+
- text: Unit
|
|
123
|
+
- text: Remarks
|
|
124
|
+
rows:
|
|
125
|
+
- - text: 12-1.1
|
|
126
|
+
- text: lattice vector
|
|
127
|
+
- text: R
|
|
128
|
+
- text: translation vector that maps the crystal lattice on itself
|
|
129
|
+
- text: m
|
|
130
|
+
- text: The non-SI unit ångström (Å) is widely used ...
|
|
131
|
+
----
|
|
132
|
+
|
|
133
|
+
Cell text is plain stripped text: OBP `sts-unknown-element` placeholder
|
|
134
|
+
spans are removed, and `<br>` / `div.sts-p` boundaries become spaces so
|
|
135
|
+
multi-paragraph cells do not glue words together.
|
|
136
|
+
|
|
137
|
+
==== Term entries (`to_terms`)
|
|
138
|
+
|
|
139
|
+
`Obp::Access::TableTermExtractor` consumes the generic table model (not
|
|
140
|
+
raw HTML) and extracts entries from term-carrying tables. Recognition
|
|
141
|
+
rules, first match wins per table:
|
|
142
|
+
|
|
143
|
+
. *Quantity/item tables* (ISO 80000 family): at least 4 columns, the
|
|
144
|
+
table sits in a numbered clause, and a majority of body rows start
|
|
145
|
+
with an item number (`12-1.1`, `4-1`). Column roles are resolved from
|
|
146
|
+
the joined header text — this handles the two-row colspan headers in
|
|
147
|
+
both English (Item No. / Quantity / Unit / Remarks) and French
|
|
148
|
+
(N° / Grandeur / Unité / Remarques) — falling back to the ISO 80000
|
|
149
|
+
column order item, designation, symbol, definition, unit, remarks.
|
|
150
|
+
. *Equivalent-terms tables*: a header row naming languages in at least
|
|
151
|
+
2 columns. One entry per row, with `designations` keyed by downcased
|
|
152
|
+
language name; rows with a single designation (letter dividers) are
|
|
153
|
+
skipped.
|
|
154
|
+
. Anything else is not a term table and yields no entries.
|
|
155
|
+
|
|
156
|
+
[source,yaml]
|
|
157
|
+
----
|
|
158
|
+
- id: 12-1.1
|
|
159
|
+
designation: lattice vector
|
|
160
|
+
definition: translation vector that maps the crystal lattice on itself
|
|
161
|
+
symbol: R
|
|
162
|
+
unit: m
|
|
163
|
+
remarks: The non-SI unit ångström (Å) is widely used ...
|
|
164
|
+
source:
|
|
165
|
+
table: iso_std_iso_80000-12_ed-2_v2_en_tab_1
|
|
166
|
+
section: '3'
|
|
167
|
+
- id: '2'
|
|
168
|
+
designations:
|
|
169
|
+
english: absorption, atmospheric
|
|
170
|
+
francais: absorption atmosphérique
|
|
171
|
+
deutsch: atmosphärische Absorption
|
|
172
|
+
source:
|
|
173
|
+
section: '1.1'
|
|
174
|
+
----
|
|
175
|
+
|
|
176
|
+
Only non-empty fields are emitted (`id` and `designation` are always
|
|
177
|
+
present); duplicate item numbers within one table keep the first row.
|
|
178
|
+
For equivalent-terms tables the entry `id` is the row's first cell when
|
|
179
|
+
it looks like an item number, else the 1-based row index.
|
|
180
|
+
|
|
181
|
+
==== Known limitations
|
|
182
|
+
|
|
183
|
+
* Column mapping is header-text and position based; exotic header
|
|
184
|
+
layouts (rowspans in `thead`, role columns in unusual order) are not
|
|
185
|
+
resolved.
|
|
186
|
+
* Quantity tables are recognized only in numbered clauses and only when
|
|
187
|
+
a strict majority of rows start with an item number; continuation
|
|
188
|
+
rows (blank item number) are skipped rather than merged into the
|
|
189
|
+
previous entry.
|
|
190
|
+
* Item numbers must match `\d+-\d+(\.\d+)?` (U+2011 non-breaking hyphen
|
|
191
|
+
is normalized to `-`); rows printed differently in the source, such
|
|
192
|
+
as the `3.10.2` typo in ISO 80000-3, are skipped.
|
|
193
|
+
* Cell text is flattened: subscripts/superscripts, inline formulas and
|
|
194
|
+
images lose their markup (`a1` for a₁), and French gender markers stay
|
|
195
|
+
glued to designations (`vecteur du réseau, m`).
|
|
196
|
+
* Equivalent-terms designations are keyed by the downcased header text
|
|
197
|
+
as printed (`francais` in ISO 5843-6); mojibake headers (e.g.
|
|
198
|
+
`PyCCKMM` for Russian) are not recognized as languages.
|
|
199
|
+
|
|
89
200
|
== Credits
|
|
90
201
|
|
|
91
202
|
This gem is developed, maintained and funded by
|
|
@@ -11,8 +11,10 @@ module Obp
|
|
|
11
11
|
@elements ||= []
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Flattened unique class names across all registered elements, so
|
|
15
|
+
# callers can test membership against individual node classes.
|
|
14
16
|
def css_classes
|
|
15
|
-
@css_classes ||= elements.filter_map(&:classes).uniq
|
|
17
|
+
@css_classes ||= elements.filter_map(&:classes).flatten.uniq
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -17,12 +17,17 @@ module Obp
|
|
|
17
17
|
nil
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Subset match: OBP varies class order and adds marker classes
|
|
21
|
+
# (e.g. "commentable"), so a node matches when it carries at
|
|
22
|
+
# least the registered classes, regardless of order or extras.
|
|
20
23
|
def match_node?
|
|
21
|
-
|
|
24
|
+
(self.class.classes - node.classes).empty?
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def render(target:)
|
|
25
|
-
|
|
28
|
+
# Top-level nodes without their own insertion target (e.g. a
|
|
29
|
+
# floating sts-p between sections) default to the body.
|
|
30
|
+
effective_target = insertion_target || target || "body"
|
|
26
31
|
effective_target = "#{effective_target}#{path_suffix}" if path_suffix
|
|
27
32
|
document.at(effective_target).public_send(insert_method, to_xml)
|
|
28
33
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Obp
|
|
2
|
+
class Access
|
|
3
|
+
class Renderer
|
|
4
|
+
class Elements
|
|
5
|
+
class DispFormula < Base
|
|
6
|
+
def self.classes
|
|
7
|
+
%w[sts-disp-formula-panel]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def content
|
|
13
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
14
|
+
xml.public_send(:"disp-formula") do
|
|
15
|
+
xml.label label if label
|
|
16
|
+
if math
|
|
17
|
+
xml << math.to_xml
|
|
18
|
+
elsif graphic_path
|
|
19
|
+
xml.graphic("xlink:href": graphic_path)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def label
|
|
26
|
+
text = node.at_css(".sts-disp-formula-label")&.text&.strip
|
|
27
|
+
text unless text&.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def math
|
|
31
|
+
@math ||= node.at_css("math")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# OBP serves formula images from its own graphics store, which
|
|
35
|
+
# Imager does not download; fall back to the original src.
|
|
36
|
+
def graphic_path
|
|
37
|
+
@graphic_path ||= begin
|
|
38
|
+
img = node.at_css("img")
|
|
39
|
+
img && (stored_image_path(img) || img.attr("src"))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stored_image_path(img)
|
|
44
|
+
images = metas["images"]
|
|
45
|
+
images[img.attr("src")] if images.is_a?(Hash)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Obp::Access::ElementRegistry.register(Obp::Access::Renderer::Elements::DispFormula)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Obp
|
|
2
|
+
class Access
|
|
3
|
+
class Renderer
|
|
4
|
+
class Elements
|
|
5
|
+
# OBP emits page footnotes as a trailing "sts-footnotes" container
|
|
6
|
+
# holding one "sts-fn" div per note, each text prefixed with the
|
|
7
|
+
# note number.
|
|
8
|
+
class Footnotes < Base
|
|
9
|
+
def self.classes
|
|
10
|
+
%w[sts-footnotes]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def insertion_target
|
|
16
|
+
"back"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def content
|
|
20
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
21
|
+
xml.public_send(:"fn-group") do
|
|
22
|
+
node.css("div.sts-fn").each { |note| render_fn(xml, note) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render_fn(xml, note)
|
|
28
|
+
xml.fn(id: "fn_#{fn_id(note)}") do
|
|
29
|
+
xml.label fn_id(note)
|
|
30
|
+
xml.p fn_text(note)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fn_id(note)
|
|
35
|
+
note.attr("id").split("_").last
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fn_text(note)
|
|
39
|
+
note.at_css("div").text.strip.sub(/\A\d+\s*/, "")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Obp::Access::ElementRegistry.register(Obp::Access::Renderer::Elements::Footnotes)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Obp
|
|
2
|
+
class Access
|
|
3
|
+
class Renderer
|
|
4
|
+
class Elements
|
|
5
|
+
# Renders the example label; the content div.sts-p children are
|
|
6
|
+
# rendered by Paragraph when the renderer recurses into them.
|
|
7
|
+
class NonNormativeExample < Base
|
|
8
|
+
def self.classes
|
|
9
|
+
%w[sts-non-normative-example]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def content
|
|
15
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
16
|
+
xml.public_send(:"non-normative-example") do
|
|
17
|
+
label_node = node.at_css(".sts-non-normative-example-label")
|
|
18
|
+
xml.label label_node.text if label_node
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Obp::Access::ElementRegistry.register(Obp::Access::Renderer::Elements::NonNormativeExample)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Obp
|
|
2
|
+
class Access
|
|
3
|
+
class Renderer
|
|
4
|
+
class Elements
|
|
5
|
+
# OBP replaces paywalled sections with this placeholder ("Only
|
|
6
|
+
# informative sections of standards are publicly available...").
|
|
7
|
+
# Skip it explicitly: render nothing and halt recursion, so the
|
|
8
|
+
# placeholder text never leaks into the output.
|
|
9
|
+
class ProtectedContentNote < Base
|
|
10
|
+
def self.classes
|
|
11
|
+
%w[sts-protected-content-note]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Accepts (and ignores) the render target; the nil return tells
|
|
15
|
+
# the renderer to halt recursion into this subtree.
|
|
16
|
+
def render(*)
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Obp::Access::ElementRegistry.register(Obp::Access::Renderer::Elements::ProtectedContentNote)
|
|
@@ -4,7 +4,7 @@ module Obp
|
|
|
4
4
|
class Elements
|
|
5
5
|
class TableWrap < Base
|
|
6
6
|
def self.classes
|
|
7
|
-
%w[sts-table-wrap
|
|
7
|
+
%w[sts-table-wrap]
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def match_node?
|
|
@@ -26,17 +26,28 @@ module Obp
|
|
|
26
26
|
xml.title caption_text
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
|
-
xml.table { xml <<
|
|
29
|
+
xml.table { xml << table_markup }
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Serialize the children as XML so void elements (e.g. <col>)
|
|
35
|
+
# self-close; inner_html emits unclosed HTML voids that swallow
|
|
36
|
+
# following rows when the builder re-parses the fragment. OBP's
|
|
37
|
+
# "sts-unknown-element" placeholders are stripped first.
|
|
38
|
+
def table_markup
|
|
39
|
+
table = node.at_css("table").dup
|
|
40
|
+
table.css(".sts-unknown-element").each(&:remove)
|
|
41
|
+
table.children.map(&:to_xml).join
|
|
42
|
+
end
|
|
43
|
+
|
|
34
44
|
def caption_label
|
|
35
45
|
@caption_label ||= node.at_css(".sts-caption-label")&.content
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def caption_text
|
|
39
|
-
@caption_text ||= node.at_css(".sts-caption")&.content
|
|
49
|
+
@caption_text ||= node.at_css(".sts-caption-title")&.content ||
|
|
50
|
+
node.at_css(".sts-caption")&.content
|
|
40
51
|
end
|
|
41
52
|
end
|
|
42
53
|
end
|
|
@@ -6,6 +6,9 @@ module Obp
|
|
|
6
6
|
%w[sts-xref] => :xref,
|
|
7
7
|
%w[sts-std-ref] => :std_ref,
|
|
8
8
|
%w[sts-label] => :label,
|
|
9
|
+
# OBP's marker for content it could not map; drop its placeholder
|
|
10
|
+
# text ("[no rendering defined for element: ...]").
|
|
11
|
+
%w[sts-unknown-element] => :skip,
|
|
9
12
|
}.freeze
|
|
10
13
|
|
|
11
14
|
def render_inline(xml, node)
|
|
@@ -19,7 +22,7 @@ module Obp
|
|
|
19
22
|
def render_node_by_type(xml, node, type)
|
|
20
23
|
if CONTAINER_TYPES.key?(type)
|
|
21
24
|
render_container(xml, node, CONTAINER_TYPES[type])
|
|
22
|
-
elsif type
|
|
25
|
+
elsif %i[label skip].include?(type)
|
|
23
26
|
nil
|
|
24
27
|
elsif type == :element
|
|
25
28
|
render_children(xml, node)
|
data/lib/obp/access/renderer.rb
CHANGED
|
@@ -20,23 +20,35 @@ module Obp
|
|
|
20
20
|
def render(node:, target: nil)
|
|
21
21
|
return unless css_classes_match?(node)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
element = matching_element(node)
|
|
24
|
+
# A nil xml means the element rendered nothing (e.g. an explicit
|
|
25
|
+
# skip element); recursion into its children halts with it.
|
|
26
|
+
xml = element&.render(target:)
|
|
27
|
+
render_children(node, xml) if xml
|
|
28
|
+
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
def render_children(node, xml)
|
|
31
|
+
section_path = xml.first&.path
|
|
32
|
+
return unless section_path
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
node.children.each do |child|
|
|
35
|
+
render(node: child, target: section_path)
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# First match wins: the matching element with the most registered
|
|
40
|
+
# classes is the most specific (e.g. Terminology beats Section for
|
|
41
|
+
# "sts-section sts-tbx-sec"); ties resolve in registration order
|
|
42
|
+
# because max_by keeps the first maximum.
|
|
43
|
+
def matching_element(node)
|
|
44
|
+
ElementRegistry.elements.filter_map do |element_class|
|
|
45
|
+
element = element_class.new(document:, metas:, node:)
|
|
46
|
+
element if element.match_node?
|
|
47
|
+
end.max_by { |element| element.class.classes.size }
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def css_classes_match?(node)
|
|
39
|
-
ElementRegistry.css_classes.
|
|
51
|
+
ElementRegistry.css_classes.intersect?(node.classes)
|
|
40
52
|
end
|
|
41
53
|
end
|
|
42
54
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Obp
|
|
7
|
+
class Access
|
|
8
|
+
# Maps the table-carrying nodes of an OBP preview HTML fragment —
|
|
9
|
+
# div.sts-table-wrap and div.sts-array, in document order — to a
|
|
10
|
+
# generic table model built from plain hashes, arrays and strings,
|
|
11
|
+
# ready for YAML serialization. This is an additive read of the same
|
|
12
|
+
# HTML source the Converter renders to STS XML; TableTermExtractor
|
|
13
|
+
# consumes this model.
|
|
14
|
+
class TableMapper
|
|
15
|
+
# Both wrapper kinds carry a single <table>.
|
|
16
|
+
WRAP_SELECTOR = "div.sts-table-wrap, div.sts-array"
|
|
17
|
+
|
|
18
|
+
# Section ids look like "toc_iso_std_iso_80000-12_ed-2_v2_en_sec_3";
|
|
19
|
+
# the clause key is whatever follows "_sec_" ("3", "1.1", "index").
|
|
20
|
+
SECTION_ID_PATTERN = /_sec_(.+)\z/
|
|
21
|
+
|
|
22
|
+
attr_reader :source
|
|
23
|
+
|
|
24
|
+
def initialize(source:)
|
|
25
|
+
@source = source
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tables
|
|
29
|
+
@tables ||= wraps.map { |node| map_table(node) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_yaml
|
|
33
|
+
tables.to_yaml
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def document
|
|
39
|
+
@document ||= Nokogiri::HTML(source.gsub(/[[:space:]]/, " "))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def wraps
|
|
43
|
+
@wraps ||= begin
|
|
44
|
+
nodes = document.css("body > div.sts-standard").css(WRAP_SELECTOR)
|
|
45
|
+
nodes.each { |node| node.css(".sts-unknown-element").each(&:remove) }
|
|
46
|
+
nodes
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def map_table(node)
|
|
51
|
+
{
|
|
52
|
+
"id" => node["id"],
|
|
53
|
+
"section" => section_of(node),
|
|
54
|
+
"label" => text_of(node.at_css(".sts-caption-label")),
|
|
55
|
+
"caption" => caption_of(node),
|
|
56
|
+
"header" => map_rows(node.css("thead tr")),
|
|
57
|
+
"rows" => map_rows(body_rows(node)),
|
|
58
|
+
}.compact
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def section_of(node)
|
|
62
|
+
section = node.ancestors("div").find do |ancestor|
|
|
63
|
+
ancestor.classes.include?("sts-section")
|
|
64
|
+
end
|
|
65
|
+
section&.[]("id")&.[](SECTION_ID_PATTERN, 1)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def caption_of(node)
|
|
69
|
+
text_of(node.at_css(".sts-caption-title")) ||
|
|
70
|
+
text_of(node.at_css(".sts-caption"))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Body rows come from <tbody> when the source splits head/body;
|
|
74
|
+
# otherwise every <tr> outside a <thead> is a body row.
|
|
75
|
+
def body_rows(node)
|
|
76
|
+
rows = node.css("tbody tr")
|
|
77
|
+
return rows unless rows.empty?
|
|
78
|
+
|
|
79
|
+
node.css("table tr").reject { |tr| tr.ancestors("thead").any? }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def map_rows(rows)
|
|
83
|
+
rows.map do |tr|
|
|
84
|
+
tr.element_children.map { |cell| map_cell(cell) }
|
|
85
|
+
end.reject(&:empty?)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def map_cell(cell)
|
|
89
|
+
mapped = { "text" => cell_text(cell) }
|
|
90
|
+
%w[colspan rowspan].each do |attribute|
|
|
91
|
+
span = cell[attribute].to_i
|
|
92
|
+
mapped[attribute] = span if span > 1
|
|
93
|
+
end
|
|
94
|
+
mapped
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Plain stripped cell text. OBP's unknown-element placeholders are
|
|
98
|
+
# removed at the wrapper level; <br> and block-level div.sts-p
|
|
99
|
+
# boundaries become spaces so multi-paragraph cells do not glue
|
|
100
|
+
# words together.
|
|
101
|
+
def cell_text(cell)
|
|
102
|
+
copy = cell.dup
|
|
103
|
+
copy.css("br, div.sts-p").each do |node|
|
|
104
|
+
node.add_previous_sibling(" ")
|
|
105
|
+
end
|
|
106
|
+
copy.content.gsub(/[[:space:]]+/, " ").strip
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def text_of(node)
|
|
110
|
+
return unless node
|
|
111
|
+
|
|
112
|
+
text = node.content.gsub(/[[:space:]]+/, " ").strip
|
|
113
|
+
text unless text.empty?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Obp
|
|
6
|
+
class Access
|
|
7
|
+
# Extracts term entries from TableMapper's generic table model.
|
|
8
|
+
#
|
|
9
|
+
# Recognition rules, first match wins per table:
|
|
10
|
+
#
|
|
11
|
+
# 1. Quantity/item tables (the ISO 80000 family): at least 4 columns,
|
|
12
|
+
# placed in a numbered clause (the terms clause), and a majority of
|
|
13
|
+
# body rows whose first cell is an item number (e.g. "12-1.1").
|
|
14
|
+
# Column roles come from header text when possible ("Quantity
|
|
15
|
+
# Symbol", "Grandeur Symbole", ...), falling back to the ISO 80000
|
|
16
|
+
# column order [item, designation, symbol, definition, unit,
|
|
17
|
+
# remarks].
|
|
18
|
+
# 2. Equivalent-terms tables: a header row naming languages in at
|
|
19
|
+
# least 2 columns; one entry per row with designations keyed by
|
|
20
|
+
# the downcased language name.
|
|
21
|
+
#
|
|
22
|
+
# Anything else is not a term table and yields no entries.
|
|
23
|
+
class TableTermExtractor
|
|
24
|
+
# "12-1.1" or "4-1"; OBP sometimes prints U+2011 NON-BREAKING
|
|
25
|
+
# HYPHEN, normalized away by normalize_item.
|
|
26
|
+
ITEM_PATTERN = /\A\d+-\d+(?:\.\d+)?\z/
|
|
27
|
+
|
|
28
|
+
MIN_QUANTITY_COLUMNS = 4
|
|
29
|
+
MIN_LANGUAGE_COLUMNS = 2
|
|
30
|
+
|
|
31
|
+
# Terms clauses are numbered ("3", "3.2"); foreword, intro, bibl
|
|
32
|
+
# and index sections are not.
|
|
33
|
+
NUMERIC_SECTION = /\A\d+(\.\d+)*\z/
|
|
34
|
+
|
|
35
|
+
# Checked in order against each column's joined header text; the
|
|
36
|
+
# first unassigned column matching wins, so "Quantity Symbol"
|
|
37
|
+
# becomes "symbol" and not "designation".
|
|
38
|
+
COLUMN_ROLE_PATTERNS = [
|
|
39
|
+
["symbol", /symbol|symbole/i],
|
|
40
|
+
["definition", /definition|définition/i],
|
|
41
|
+
["unit", /unit|unité/i],
|
|
42
|
+
["remarks", /remark|remarque/i],
|
|
43
|
+
["item", /item\s+no|n[°º]|numéro|no\./i],
|
|
44
|
+
["designation", /quantity|grandeur|term|terme|name|nom/i],
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# Fallback when header-text matching cannot place item+designation.
|
|
48
|
+
POSITIONAL_ROLES = %w[
|
|
49
|
+
item designation symbol definition unit remarks
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
# Language names recognized in equivalent-terms table headers.
|
|
53
|
+
LANGUAGES = %w[
|
|
54
|
+
english french français francais anglais russian russe русский
|
|
55
|
+
german deutsch allemand spanish español espagnol italian italiano
|
|
56
|
+
italien chinese chinois japanese japonais arabic arabe portuguese
|
|
57
|
+
portugais korean coréen
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
attr_reader :tables
|
|
61
|
+
|
|
62
|
+
def initialize(tables:)
|
|
63
|
+
@tables = tables
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def entries
|
|
67
|
+
@entries ||= tables.flat_map { |table| table_entries(table) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_yaml
|
|
71
|
+
entries.to_yaml
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def table_entries(table)
|
|
77
|
+
if quantity_table?(table)
|
|
78
|
+
quantity_entries(table)
|
|
79
|
+
elsif equivalent_terms_table?(table)
|
|
80
|
+
equivalent_entries(table)
|
|
81
|
+
else
|
|
82
|
+
[]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def quantity_table?(table)
|
|
87
|
+
return false unless NUMERIC_SECTION.match?(table["section"].to_s)
|
|
88
|
+
return false unless column_count(table) >= MIN_QUANTITY_COLUMNS
|
|
89
|
+
|
|
90
|
+
rows = table["rows"]
|
|
91
|
+
return false if rows.empty?
|
|
92
|
+
|
|
93
|
+
rows.count { |row| item_number?(cell_text(row, 0)) } * 2 > rows.size
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def quantity_entries(table)
|
|
97
|
+
roles = column_roles(table)
|
|
98
|
+
seen = {}
|
|
99
|
+
table["rows"].filter_map do |row|
|
|
100
|
+
id = normalize_item(cell_text(row, roles["item"]))
|
|
101
|
+
next unless ITEM_PATTERN.match?(id)
|
|
102
|
+
next if seen[id]
|
|
103
|
+
|
|
104
|
+
seen[id] = true
|
|
105
|
+
quantity_entry(table, row, roles, id)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def quantity_entry(table, row, roles, id)
|
|
110
|
+
entry = { "id" => id,
|
|
111
|
+
"designation" => cell_text(row, roles["designation"]).to_s }
|
|
112
|
+
%w[definition symbol unit remarks].each do |role|
|
|
113
|
+
text = cell_text(row, roles[role])
|
|
114
|
+
entry[role] = text unless text.nil? || text.empty?
|
|
115
|
+
end
|
|
116
|
+
entry["source"] = source_of(table)
|
|
117
|
+
entry
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def equivalent_terms_table?(table)
|
|
121
|
+
!language_header_row(table).nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def equivalent_entries(table)
|
|
125
|
+
languages = language_columns(language_header_row(table))
|
|
126
|
+
seen = {}
|
|
127
|
+
table["rows"].each_with_index.filter_map do |row, index|
|
|
128
|
+
entry = equivalent_entry(table, row, languages, index)
|
|
129
|
+
next if entry.nil? || seen[entry["id"]]
|
|
130
|
+
|
|
131
|
+
seen[entry["id"]] = true
|
|
132
|
+
entry
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def equivalent_entry(table, row, languages, index)
|
|
137
|
+
designations = languages.each_with_object({}) do |(column, lang), hash|
|
|
138
|
+
text = cell_text(row, column)
|
|
139
|
+
hash[lang] = text unless text.nil? || text.empty?
|
|
140
|
+
end
|
|
141
|
+
# Rows with a single designation are letter dividers ("A", "B",
|
|
142
|
+
# ...), not equivalences.
|
|
143
|
+
return if designations.size < 2
|
|
144
|
+
|
|
145
|
+
{ "id" => entry_id(row, index),
|
|
146
|
+
"designations" => designations,
|
|
147
|
+
"source" => source_of(table) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def entry_id(row, index)
|
|
151
|
+
first = normalize_item(cell_text(row, 0))
|
|
152
|
+
ITEM_PATTERN.match?(first) ? first : (index + 1).to_s
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def language_header_row(table)
|
|
156
|
+
table["header"].find do |row|
|
|
157
|
+
row.count { |cell| language?(cell["text"]) } >= MIN_LANGUAGE_COLUMNS
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def language_columns(header_row)
|
|
162
|
+
columns = {}
|
|
163
|
+
position = 0
|
|
164
|
+
header_row.each do |cell|
|
|
165
|
+
columns[position] = cell["text"].downcase if language?(cell["text"])
|
|
166
|
+
position += cell["colspan"] || 1
|
|
167
|
+
end
|
|
168
|
+
columns
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def language?(text)
|
|
172
|
+
LANGUAGES.include?(text.to_s.downcase)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# First unassigned column matching each role pattern wins; matched
|
|
176
|
+
# columns are blanked so later roles cannot claim them again.
|
|
177
|
+
def column_roles(table)
|
|
178
|
+
headers = column_header_texts(table)
|
|
179
|
+
roles = {}
|
|
180
|
+
COLUMN_ROLE_PATTERNS.each do |role, pattern|
|
|
181
|
+
column = headers.index { |text| text.match?(pattern) }
|
|
182
|
+
next unless column
|
|
183
|
+
|
|
184
|
+
headers[column] = ""
|
|
185
|
+
roles[role] = column
|
|
186
|
+
end
|
|
187
|
+
return roles if roles["item"] && roles["designation"]
|
|
188
|
+
|
|
189
|
+
positional_roles(table)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def positional_roles(table)
|
|
193
|
+
POSITIONAL_ROLES.first(column_count(table)).each_with_index.to_h
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Each column's header text joined across header rows, colspans
|
|
197
|
+
# repeated ("Quantity" + "Symbol" => "Quantity Symbol").
|
|
198
|
+
def column_header_texts(table)
|
|
199
|
+
grid = table["header"].map { |row| expand_row(row) }
|
|
200
|
+
(0...grid_width(grid)).map do |column|
|
|
201
|
+
grid.filter_map { |row| row[column] }.reject(&:empty?).join(" ")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def grid_width(grid)
|
|
206
|
+
grid.map(&:size).max || 0
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def expand_row(row)
|
|
210
|
+
row.flat_map { |cell| [cell["text"]] * (cell["colspan"] || 1) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def column_count(table)
|
|
214
|
+
(table["header"] + table["rows"])
|
|
215
|
+
.map { |row| row.sum { |cell| cell["colspan"] || 1 } }.max || 0
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def cell_text(row, column)
|
|
219
|
+
return if column.nil?
|
|
220
|
+
|
|
221
|
+
cell_at(row, column)&.fetch("text")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def cell_at(row, column)
|
|
225
|
+
position = 0
|
|
226
|
+
row.each do |cell|
|
|
227
|
+
span = cell["colspan"] || 1
|
|
228
|
+
return cell if column >= position && column < position + span
|
|
229
|
+
|
|
230
|
+
position += span
|
|
231
|
+
end
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def item_number?(text)
|
|
236
|
+
ITEM_PATTERN.match?(normalize_item(text))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def normalize_item(text)
|
|
240
|
+
text.to_s.tr("\u2011", "-")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def source_of(table)
|
|
244
|
+
{ "table" => table["id"], "section" => table["section"] }.compact
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
data/lib/obp/access/version.rb
CHANGED
data/lib/obp/access.rb
CHANGED
|
@@ -19,6 +19,8 @@ require_relative "access/parser"
|
|
|
19
19
|
require_relative "access/converter"
|
|
20
20
|
require_relative "access/imager"
|
|
21
21
|
require_relative "access/renderer"
|
|
22
|
+
require_relative "access/table_mapper"
|
|
23
|
+
require_relative "access/table_term_extractor"
|
|
22
24
|
require_relative "access/cli"
|
|
23
25
|
|
|
24
26
|
require_relative "access/elements/base"
|
|
@@ -36,6 +38,10 @@ require_relative "access/elements/table_wrap"
|
|
|
36
38
|
require_relative "access/elements/title"
|
|
37
39
|
require_relative "access/elements/paragraph"
|
|
38
40
|
require_relative "access/elements/non_normative_note"
|
|
41
|
+
require_relative "access/elements/non_normative_example"
|
|
42
|
+
require_relative "access/elements/disp_formula"
|
|
43
|
+
require_relative "access/elements/footnotes"
|
|
44
|
+
require_relative "access/elements/protected_content_note"
|
|
39
45
|
require_relative "access/elements/copyright"
|
|
40
46
|
require_relative "access/elements/bibliography"
|
|
41
47
|
require_relative "access/elements/terminology"
|
|
@@ -91,6 +97,18 @@ module Obp
|
|
|
91
97
|
Sts::NisoSts::Standard.from_xml(to_xml)
|
|
92
98
|
end
|
|
93
99
|
|
|
100
|
+
# YAML serialization of every table in the OBP preview HTML
|
|
101
|
+
# (TableMapper's generic table model).
|
|
102
|
+
def to_tables
|
|
103
|
+
table_mapper.to_yaml
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# YAML serialization of term entries extracted from the document's
|
|
107
|
+
# term-carrying tables (TableTermExtractor over the table model).
|
|
108
|
+
def to_terms
|
|
109
|
+
TableTermExtractor.new(tables: table_mapper.tables).to_yaml
|
|
110
|
+
end
|
|
111
|
+
|
|
94
112
|
def to_xml_file
|
|
95
113
|
path = File.join(tmpdir, "#{urn.safe}.xml")
|
|
96
114
|
File.write(path, to_xml(pretty: true))
|
|
@@ -107,6 +125,12 @@ module Obp
|
|
|
107
125
|
@parser ||= Parser.new(urn:, directory: tmpdir)
|
|
108
126
|
end
|
|
109
127
|
|
|
128
|
+
# Shares Parser#html (memoized) with the STS conversion path, so
|
|
129
|
+
# reading tables does not re-POST to the OBP.
|
|
130
|
+
def table_mapper
|
|
131
|
+
@table_mapper ||= TableMapper.new(source: parser.html)
|
|
132
|
+
end
|
|
133
|
+
|
|
110
134
|
def pretty_print(xml)
|
|
111
135
|
Nokogiri::XML(xml, &:noblanks).to_xml
|
|
112
136
|
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.
|
|
4
|
+
version: 0.1.5
|
|
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-
|
|
11
|
+
date: 2026-07-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -93,13 +93,17 @@ files:
|
|
|
93
93
|
- lib/obp/access/elements/bibliography.rb
|
|
94
94
|
- lib/obp/access/elements/bibliography/bib_ref.rb
|
|
95
95
|
- lib/obp/access/elements/copyright.rb
|
|
96
|
+
- lib/obp/access/elements/disp_formula.rb
|
|
96
97
|
- lib/obp/access/elements/figure.rb
|
|
97
98
|
- lib/obp/access/elements/figure_group.rb
|
|
99
|
+
- lib/obp/access/elements/footnotes.rb
|
|
98
100
|
- lib/obp/access/elements/index.rb
|
|
99
101
|
- lib/obp/access/elements/introduction.rb
|
|
100
102
|
- lib/obp/access/elements/list.rb
|
|
103
|
+
- lib/obp/access/elements/non_normative_example.rb
|
|
101
104
|
- lib/obp/access/elements/non_normative_note.rb
|
|
102
105
|
- lib/obp/access/elements/paragraph.rb
|
|
106
|
+
- lib/obp/access/elements/protected_content_note.rb
|
|
103
107
|
- lib/obp/access/elements/root.rb
|
|
104
108
|
- lib/obp/access/elements/section.rb
|
|
105
109
|
- lib/obp/access/elements/section_title.rb
|
|
@@ -123,6 +127,8 @@ files:
|
|
|
123
127
|
- lib/obp/access/parser.rb
|
|
124
128
|
- lib/obp/access/renderer.rb
|
|
125
129
|
- lib/obp/access/retriever.rb
|
|
130
|
+
- lib/obp/access/table_mapper.rb
|
|
131
|
+
- lib/obp/access/table_term_extractor.rb
|
|
126
132
|
- lib/obp/access/urn.rb
|
|
127
133
|
- lib/obp/access/version.rb
|
|
128
134
|
homepage: ''
|