stepmod-utils 0.6.10.pre.alpha.1 → 0.6.10

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: f8b65cef0a5a325ab2e28a21e5dcd38be81535e4cac5662b2dd4bc4244ce1d40
4
- data.tar.gz: 8259f12eb5f6f2e4df159be483c36335e7b715aad8793943ea61a60523e812f8
3
+ metadata.gz: 9af636493e609c32af652ba4c7ba9c27bcf544b2b050f166d046d320cc951c3e
4
+ data.tar.gz: 721483a250021b902ccb6f9c13848ac9b2eb9679ca419c3a4c041badffc10533
5
5
  SHA512:
6
- metadata.gz: a8b84ea4e01a4c46c105c3be24bac8aedccb38ba41bb1bc0a60f43cd12f53a64895714c70824cc7cec4d82261135e38eef82871bd7e2c0df3c0124730c1b53e7
7
- data.tar.gz: f1fabfe7cf7da21378c85563996da2d262a950c951936a530db0d650b32b8188a36f929429f986a90a30c7f43c2267e180bd3cead1450aead8765b921aa8916e
6
+ metadata.gz: 572cb2422c959eb5dee160ecac7e35d32c3533ab8368aa60a9051a2a834c6a41297809539154caa63e08884b0cca17e7c4bb19c7ae28be5843d938e12ad02910
7
+ data.tar.gz: 87ca83a2d9413b9de293bc2857d749ad3ef437b61cc7aba90e2f1ec0760d2053746684ff1aa6393f5afb3c2c2b33000aa387187b523b81b671016e819b625050
data/.rubocop.yml CHANGED
@@ -10,4 +10,4 @@ Style/MultilineMethodCallIndentation:
10
10
  EnforcedStyle: aligned
11
11
 
12
12
  AllCops:
13
- TargetRubyVersion: 3.1
13
+ TargetRubyVersion: 3.3
data/README.adoc CHANGED
@@ -30,6 +30,10 @@ Or include it in your gemspec.
30
30
  This command uses Expressir to validate all EXPRESS schemas listed in the
31
31
  `{stepmod}/repository_index.xml` file.
32
32
 
33
+ Validation parses each file separately so that a broken schema is reported
34
+ without preventing validation of the remaining files. It does not build the
35
+ shared repository used for concept extraction.
36
+
33
37
  There is a `SHOULD_REWRITE` constant that can be switched in the code which
34
38
  allows overwriting of the EXPRESS schemas into a pretty-printed format.
35
39
 
@@ -68,6 +72,10 @@ SCHEMA: data/resources/geometric_model_schema/geometric_model_schema.exp
68
72
  This command takes the STEPmod directory and parses its XML to synthesize
69
73
  with the target EXPRESS schemas to migrate into Annotated EXPRESS schemas.
70
74
 
75
+ Annotation parses each input file to determine its schema name before combining
76
+ it with the XML descriptions. It does not resolve references across schemas;
77
+ repository-wide parsing happens when extracting concepts from the annotated files.
78
+
71
79
  Given the following schemas:
72
80
 
73
81
  * Simple EXPRESS schema: `{stepmod-root-dir}/data/resources/action_schema/action_schema.exp`
@@ -119,6 +127,15 @@ $ bundle exec stepmod-annotate-all --stepmod-dir iso-10303-stepmod-wg12 --output
119
127
 
120
128
  === ISO 10303-2 concepts extraction
121
129
 
130
+ Concept extraction passes the complete selected set of annotated EXPRESS files to
131
+ `Expressir::Express::Parser.from_files` once, producing a shared repository for
132
+ cross-schema references as requested in
133
+ https://github.com/metanorma/stepmod-utils/issues/136[issue #136]. The set contains
134
+ existing ARM and MIM files for published, non-withdrawn modules and existing
135
+ schemas for published, non-withdrawn resources listed in `repository_index.xml`.
136
+ ISO 13584 schemas are excluded. BOMs and application protocols do not contribute
137
+ Clause 3 terms and are excluded from concept extraction.
138
+
122
139
  [source,sh]
123
140
  ----
124
141
  $ bundle exec stepmod-extract-concepts \
@@ -256,6 +273,56 @@ End!
256
273
  [stepmod-utils] ******************************
257
274
  ----
258
275
 
276
+ === Converting the STEPmod bibliography to a Relaton collection
277
+
278
+ STEPmod keeps the shared references for `normref.inc` and `bibitem.inc` in
279
+ `data/basic/normrefs.xml` and `data/basic/bibliography.xml`. This command
280
+ converts both lists into one Relaton collection, so that Metanorma can cite any
281
+ entry by its id.
282
+
283
+ [source,sh]
284
+ ----
285
+ # from a STEPmod checkout
286
+ $ stepmod-convert-bibliography -s {stepmod-directory} -o stepmod-bibliography.xml
287
+
288
+ # or from the two files directly (either one can be left out)
289
+ $ stepmod-convert-bibliography --normrefs normrefs.xml \
290
+ --bibliography bibliography.xml -o stepmod-bibliography.xml
291
+ ----
292
+
293
+ Without `-o`, the collection is written to standard output. `-t` sets the
294
+ collection title.
295
+
296
+ The output is a `<relaton-collection>` with one `<relation type="partOf">` per
297
+ entry, each holding a Relaton `<bibitem>`:
298
+
299
+ * The item id is the STEPmod id with the prefix `normref_` or `bibitem_` (for
300
+ example `bibitem_ref8824-1`), the anchor form that the migrated ISO 10303
301
+ documents use. The raw STEPmod id is kept as a `docidentifier` of type
302
+ `STEPmod-id`.
303
+ * The primary `docidentifier` is `orgname` + `stdnumber` for a normref (for
304
+ example `ISO 31-0`), the `stdnumber` for a bibliography entry, and
305
+ `report-type` + `number` for a technical report.
306
+ * `bibitem` and `normref` become `standard`, `bibitem.techreport` becomes
307
+ `techreport`, and `bibitem.book` becomes `book`.
308
+ * `<term>` children of a normref are not bibliographic data and are skipped.
309
+ * A duplicate id in one list is skipped with a warning. An unknown named
310
+ entity is kept as text with a warning.
311
+
312
+ From Ruby:
313
+
314
+ [source,ruby]
315
+ ----
316
+ require "stepmod/utils/bibliography_converter"
317
+
318
+ converter = Stepmod::Utils::BibliographyConverter.new(
319
+ normrefs: "data/basic/normrefs.xml",
320
+ bibliography: "data/basic/bibliography.xml",
321
+ )
322
+ converter.items # => Array<Relaton::Bib::ItemData>
323
+ converter.to_xml # => relaton-collection XML
324
+ ----
325
+
259
326
  == Conversion of STEPmod descriptions.xml into adoc
260
327
 
261
328
  [source,sh]
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # resolve bin path, ignoring symlinks
4
+ require "pathname"
5
+ bin_file = Pathname.new(__FILE__).realpath
6
+
7
+ # add self to libpath
8
+ $:.unshift File.expand_path("../../lib", bin_file)
9
+
10
+ require "bundler/setup"
11
+ require "optparse"
12
+ require "stepmod/utils/bibliography_converter"
13
+
14
+ def log(message)
15
+ warn "[stepmod-utils] #{message}"
16
+ end
17
+
18
+ options = {}
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{$0} [options]\n\n" \
21
+ "Convert STEPmod normrefs.xml and bibliography.xml into " \
22
+ "one Relaton collection."
23
+
24
+ opts.on("-s", "--stepmod DIR",
25
+ "STEPmod root; reads data/basic/normrefs.xml and " \
26
+ "data/basic/bibliography.xml") do |dir|
27
+ options[:stepmod] = dir
28
+ end
29
+
30
+ opts.on("--normrefs PATH", "Path to normrefs.xml") do |path|
31
+ options[:normrefs] = path
32
+ end
33
+
34
+ opts.on("--bibliography PATH", "Path to bibliography.xml") do |path|
35
+ options[:bibliography] = path
36
+ end
37
+
38
+ opts.on("-o", "--output PATH",
39
+ "Output collection XML (default: standard output)") do |path|
40
+ options[:output] = path
41
+ end
42
+
43
+ opts.on("-t", "--title TITLE", "Title of the collection") do |title|
44
+ options[:title] = title
45
+ end
46
+
47
+ opts.on_tail("-h", "--help", "Show this message") do
48
+ puts opts
49
+ exit
50
+ end
51
+ end
52
+ begin
53
+ parser.parse!
54
+ rescue OptionParser::ParseError => e
55
+ warn e.message
56
+ warn parser
57
+ exit 1
58
+ end
59
+
60
+ unless ARGV.empty?
61
+ warn "Unexpected argument: #{ARGV.join(' ')}"
62
+ warn parser
63
+ exit 1
64
+ end
65
+
66
+ if options[:stepmod]
67
+ basic = File.join(options[:stepmod], "data", "basic")
68
+ options[:normrefs] ||= File.join(basic, "normrefs.xml")
69
+ options[:bibliography] ||= File.join(basic, "bibliography.xml")
70
+ end
71
+
72
+ inputs = options.values_at(:normrefs, :bibliography).compact
73
+ if inputs.empty?
74
+ warn "Give --stepmod DIR, or --normrefs PATH and/or --bibliography PATH."
75
+ warn parser
76
+ exit 1
77
+ end
78
+
79
+ missing = inputs.reject { |path| File.file?(path) }
80
+ unless missing.empty?
81
+ warn "File not found: #{missing.join(', ')}"
82
+ exit 1
83
+ end
84
+
85
+ converter = Stepmod::Utils::BibliographyConverter.new(
86
+ normrefs: options[:normrefs],
87
+ bibliography: options[:bibliography],
88
+ title: options[:title],
89
+ )
90
+ xml = converter.to_xml
91
+
92
+ if options[:output]
93
+ File.write(options[:output], xml, encoding: "UTF-8")
94
+ log "#{converter.items.size} items written to #{options[:output]}"
95
+ else
96
+ $stdout.write(xml)
97
+ end
@@ -0,0 +1,294 @@
1
+ require "date"
2
+ require "nokogiri"
3
+ require "relaton/bib"
4
+
5
+ module Stepmod
6
+ module Utils
7
+ # Converts the STEPmod shared reference lists, data/basic/normrefs.xml
8
+ # and data/basic/bibliography.xml, into one Relaton collection, so that
9
+ # Metanorma can cite any entry by its id.
10
+ #
11
+ # Each entry becomes a Relaton::Bib::ItemData. Its id is the STEPmod id
12
+ # with the list prefix the migrated documents use ("normref_",
13
+ # "bibitem_"), and the raw STEPmod id is kept as a docidentifier of type
14
+ # "STEPmod-id".
15
+ class BibliographyConverter
16
+ COLLECTION_NS = "https://open.ribose.com/relaton-xml".freeze
17
+ DEFAULT_TITLE = "STEPmod bibliography".freeze
18
+ AUTHOR = "ISO/TC 184/SC 4".freeze
19
+
20
+ XML_ENTITIES = %w[amp lt gt quot apos].freeze
21
+ DASHES = /\A[\s—–-]+|[\s—–-]+\z/
22
+ ITEM_TYPES = {
23
+ "normref" => "standard",
24
+ "bibitem" => "standard",
25
+ "bibitem.techreport" => "techreport",
26
+ "bibitem.book" => "book",
27
+ }.freeze
28
+
29
+ Bib = Relaton::Bib
30
+
31
+ attr_reader :title
32
+
33
+ # @param normrefs [String, nil] path to normrefs.xml
34
+ # @param bibliography [String, nil] path to bibliography.xml
35
+ # @param title [String, nil] title of the collection
36
+ def initialize(normrefs: nil, bibliography: nil, title: nil)
37
+ @normrefs = normrefs
38
+ @bibliography = bibliography
39
+ @title = title || DEFAULT_TITLE
40
+ end
41
+
42
+ # @return [Array<Relaton::Bib::ItemData>]
43
+ def items
44
+ @items ||= read_list(@normrefs, "normref", "//normref") +
45
+ read_list(@bibliography, "bibitem",
46
+ "//bibitem | //bibitem.techreport | //bibitem.book")
47
+ end
48
+
49
+ # @return [String] a relaton-collection XML document
50
+ def to_xml
51
+ doc = Nokogiri::XML::Document.new
52
+ doc.encoding = "UTF-8"
53
+ root = doc.add_child(doc.create_element("relaton-collection",
54
+ xmlns: COLLECTION_NS))
55
+ root.add_child(doc.create_element("title", title))
56
+ root.add_child(author_node(doc))
57
+ items.each do |item|
58
+ relation = root.add_child(doc.create_element("relation",
59
+ type: "partOf"))
60
+ relation.add_child(Nokogiri::XML(item.to_xml, &:noblanks).root)
61
+ end
62
+ doc.to_xml
63
+ end
64
+
65
+ private
66
+
67
+ def author_node(doc)
68
+ contributor = doc.create_element("contributor")
69
+ contributor.add_child(doc.create_element("role", type: "author"))
70
+ org = contributor.add_child(doc.create_element("organization"))
71
+ org.add_child(doc.create_element("name", AUTHOR))
72
+ contributor
73
+ end
74
+
75
+ def read_list(path, prefix, xpath)
76
+ return [] unless path
77
+
78
+ seen = {}
79
+ parse(path).xpath(xpath).filter_map do |node|
80
+ raw_id = node["id"]
81
+ id = "#{prefix}_#{sanitize_id(raw_id)}"
82
+ if seen[id]
83
+ log(path, "duplicate id #{raw_id.inspect} skipped")
84
+ next
85
+ end
86
+
87
+ seen[id] = true
88
+ build_item(node, id, raw_id)
89
+ end
90
+ end
91
+
92
+ def log(path, message)
93
+ warn "[stepmod-utils] #{path}: #{message}"
94
+ end
95
+
96
+ # The DTD that declares the HTML entities is not available, so resolve
97
+ # them before the parse.
98
+ # An unknown entity would be dropped silently by the parser, so it is
99
+ # reported and kept as text.
100
+ def parse(path)
101
+ xml = File.read(path, encoding: "UTF-8")
102
+ xml = xml.gsub(/&([a-zA-Z][a-zA-Z0-9]*);/) do |match|
103
+ name = Regexp.last_match(1)
104
+ next match if XML_ENTITIES.include?(name)
105
+
106
+ resolve_entity(name) || unknown_entity(path, match)
107
+ end
108
+ Nokogiri::XML(xml, &:nonet)
109
+ end
110
+
111
+ def resolve_entity(name)
112
+ codepoint = entity_lookup[name]
113
+ [codepoint].pack("U") if codepoint
114
+ end
115
+
116
+ def unknown_entity(path, match)
117
+ log(path, "unknown entity #{match} kept as text")
118
+ "&amp;#{match[1..]}"
119
+ end
120
+
121
+ def entity_lookup
122
+ @entity_lookup ||= Nokogiri::HTML4::EntityLookup.new
123
+ end
124
+
125
+ def sanitize_id(raw_id)
126
+ raw_id.to_s.gsub(/[^\w.-]+/, "_")
127
+ end
128
+
129
+ def build_item(node, id, raw_id)
130
+ ref = node.at("./stdref") || node
131
+ Bib::ItemData.new(
132
+ id: id,
133
+ type: ITEM_TYPES[node.name],
134
+ docidentifier: docidentifiers(node, ref, raw_id),
135
+ title: titles(node, ref),
136
+ date: dates(ref),
137
+ contributor: contributors(node, ref),
138
+ place: places(node),
139
+ source: sources(node),
140
+ note: notes(node),
141
+ )
142
+ end
143
+
144
+ def docidentifiers(node, ref, raw_id)
145
+ primary = primary_identifier(node, ref)
146
+ stepmod_id = Bib::Docidentifier.new(content: raw_id,
147
+ type: "STEPmod-id")
148
+ primary ? [primary, stepmod_id] : [stepmod_id]
149
+ end
150
+
151
+ # A normref stdnumber is the bare number ("31-0"). A bibliography
152
+ # stdnumber is already the full identifier ("ISO 286-1", "RFC 3986").
153
+ def primary_identifier(node, ref)
154
+ orgname = text(ref, "orgname")
155
+ number = text(ref, "stdnumber")
156
+ if number
157
+ content = node.name == "normref" ? prefixed(orgname, number) : number
158
+ type = docid_type(orgname)
159
+ elsif (number = text(node, "number"))
160
+ content = prefixed(text(node, "report-type"), number)
161
+ type = "STEPmod"
162
+ end
163
+ return unless content
164
+
165
+ Bib::Docidentifier.new(content: content, type: type, primary: true)
166
+ end
167
+
168
+ def prefixed(prefix, value)
169
+ return value if prefix.nil? || value.start_with?(prefix)
170
+
171
+ "#{prefix} #{value}"
172
+ end
173
+
174
+ def docid_type(orgname)
175
+ case orgname
176
+ when /\AISO/ then "ISO"
177
+ when /\AIEC/ then "IEC"
178
+ else "STEPmod"
179
+ end
180
+ end
181
+
182
+ # A normref subtitle continues the title ("— Part 1: ..."). A
183
+ # bibliography subtitle is free text, so it becomes a note instead.
184
+ def titles(node, ref)
185
+ main = strip_dashes(text(ref, "stdtitle"))
186
+ return [] unless main
187
+
188
+ subtitle = node.name == "normref" && strip_dashes(text(ref, "subtitle"))
189
+ return [title_of(main, "main")] unless subtitle
190
+
191
+ result = [title_of(join_title(main, subtitle), "main"),
192
+ title_of(main, "title-main")]
193
+ part = subtitle.start_with?("Part")
194
+ result << title_of(subtitle, "title-part") if part
195
+ result
196
+ end
197
+
198
+ # Some subtitles continue the sentence (", corrected and reprinted
199
+ # in 1995", "(OMG-UML VER 1.3)"); the others are title components.
200
+ def join_title(main, subtitle)
201
+ case subtitle
202
+ when /\A[,;.]/ then "#{main}#{subtitle}"
203
+ when /\A\(/ then "#{main} #{subtitle}"
204
+ else "#{main} — #{subtitle}"
205
+ end
206
+ end
207
+
208
+ def title_of(content, type)
209
+ Bib::Title.new(content: content, type: type, language: "en",
210
+ script: "Latn")
211
+ end
212
+
213
+ def strip_dashes(value)
214
+ stripped = value&.gsub(DASHES, "")
215
+ stripped unless stripped.nil? || stripped.empty?
216
+ end
217
+
218
+ def notes(node)
219
+ return [] if node.name == "normref"
220
+
221
+ subtitle = text(node, "subtitle")
222
+ subtitle ? [Bib::Note.new(content: subtitle)] : []
223
+ end
224
+
225
+ # STEPmod dates are "YYYY", "YYYY-MM-DD", or free text such as
226
+ # "January 1992". Free text is kept as the date text. A value without
227
+ # a year ("—", "-", "CHECK DATE") means "undated" and is dropped, and
228
+ # so is a future date ("To be published in 2018").
229
+ def dates(ref)
230
+ value = text(ref, "pubdate")
231
+ return [] unless value
232
+
233
+ if value.match?(/\A\d{4}(-\d{2}){0,2}\z/)
234
+ return [Bib::Date.new(type: "published", at: value)]
235
+ end
236
+
237
+ year = value[/\b(\d{4})\b/, 1]
238
+ return [] if year.nil? || value.match?(/to be published/i)
239
+
240
+ [Bib::Date.new(type: "published", at: year, text: value)]
241
+ end
242
+
243
+ def contributors(node, ref)
244
+ authors = texts(node, "primary-responsibility").map do |name|
245
+ Bib::Contributor.new(role: [role("author")], person: person(name))
246
+ end
247
+ publishers = %w[orgname institution publisher].flat_map do |tag|
248
+ texts(tag == "orgname" ? ref : node, tag)
249
+ end
250
+ authors + publishers.uniq.map do |name|
251
+ Bib::Contributor.new(role: [role("publisher")],
252
+ organization: organization(name))
253
+ end
254
+ end
255
+
256
+ def role(type)
257
+ Bib::Contributor::Role.new(type: type)
258
+ end
259
+
260
+ def person(name)
261
+ Bib::Person.new(
262
+ name: Bib::FullName.new(
263
+ completename: Bib::LocalizedString.new(content: name),
264
+ ),
265
+ )
266
+ end
267
+
268
+ def organization(name)
269
+ Bib::Organization.new(
270
+ name: [Bib::TypedLocalizedString.new(content: name)],
271
+ )
272
+ end
273
+
274
+ def places(node)
275
+ texts(node, "place").map { |p| Bib::Place.new(formatted_place: p) }
276
+ end
277
+
278
+ def sources(node)
279
+ texts(node, "ulink").map { |u| Bib::Uri.new(type: "src", content: u) }
280
+ end
281
+
282
+ def text(node, tag)
283
+ texts(node, tag).first
284
+ end
285
+
286
+ def texts(node, tag)
287
+ node.xpath("./#{tag}").filter_map do |el|
288
+ value = el.text.gsub(/\s+/, " ").strip
289
+ value unless value.empty?
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
@@ -12,7 +12,8 @@ module Stepmod
12
12
  # Find non-asterisk content that is enclosed by two or
13
13
  # more asterisks. Ensure that only one whitespace occurs
14
14
  # in the border area.
15
- # Same for underscores and brackets.
15
+ # Same for underscores and brackets. A missing border is added, except
16
+ # next to a word character: unconstrained markup may sit inside a word.
16
17
  def clean_tag_borders(string)
17
18
  patterns = {
18
19
  /\s?\*{2,}.*?\*{2,}\s?/ => "**",
@@ -23,8 +24,7 @@ module Stepmod
23
24
  result = string.dup
24
25
  patterns.each do |pattern, value|
25
26
  result = result.gsub(pattern) do |match|
26
- preserve_border_whitespaces(match,
27
- default_border: Coradoc::Input::Html.config.tag_border) do
27
+ border_tags(match, Regexp.last_match) do
28
28
  match.strip.sub(" #{value}", value).sub("#{value} ", value)
29
29
  end
30
30
  end
@@ -37,6 +37,26 @@ module Stepmod
37
37
  end
38
38
  result
39
39
  end
40
+
41
+ private
42
+
43
+ # Like preserve_border_whitespaces, but a border is only invented where
44
+ # the neighbouring character is not part of a word.
45
+ def border_tags(match, matched)
46
+ return match if /\A\s*\Z/.match?(match)
47
+
48
+ invent = !/[\[(\])]/.match?(match)
49
+ start = match[/\A\s*/]
50
+ finish = match[/\s*\Z/]
51
+ start = border(matched.pre_match[-1]) if start.empty? && invent
52
+ finish = border(matched.post_match[0]) if finish.empty? && invent
53
+
54
+ start + yield + finish
55
+ end
56
+
57
+ def border(neighbour)
58
+ neighbour&.match?(/\w/) ? "" : Coradoc::Input::Html.config.tag_border
59
+ end
40
60
  end
41
61
  end
42
62
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "figure"
4
+
3
5
  module Stepmod
4
6
  module Utils
5
7
  module Converters
@@ -18,6 +20,7 @@ module Stepmod
18
20
  "[[#{id}]]"
19
21
  elsif href.to_s.start_with?("#")
20
22
  href = href.sub(/^#/, "").gsub(/\s/, "").gsub(/__+/, "_")
23
+ href = figure_anchor(href, state) || href
21
24
  if name.empty? || number_with_optional_prefix?(name)
22
25
  "<<#{href}>>"
23
26
  else
@@ -36,6 +39,17 @@ module Stepmod
36
39
 
37
40
  private
38
41
 
42
+ # A reference to a figure must use the id the figure is given by
43
+ # Figure.pattern, or the link does not resolve.
44
+ def figure_anchor(href, state)
45
+ return unless state[:schema_and_entity] || state[:schema_name]
46
+
47
+ figure_id = href[/\Afigure[_-](.+)\z/, 1]
48
+ figure_id = href if figure_id.nil? &&
49
+ Array(state[:figure_ids]).include?(href)
50
+ Figure.pattern(state, figure_id) if figure_id
51
+ end
52
+
39
53
  def number_with_optional_prefix?(name)
40
54
  /.*?\s*\(?\d+\)?/.match(name)
41
55
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "strong"
4
+
3
5
  module Stepmod
4
6
  module Utils
5
7
  module Converters
6
8
  class Em < Stepmod::Utils::Converters::Base
9
+ BOLD_TAGS = %w[b strong].freeze
10
+
7
11
  def convert(node, state = {})
8
12
  italic_converted(node, state)
9
13
  end
@@ -15,6 +19,11 @@ module Stepmod
15
19
  private
16
20
 
17
21
  def italic_converted(node, state)
22
+ bold = sole_bold_child(node)
23
+ if bold && !state[:already_italic]
24
+ return bold_italic(node, bold, state)
25
+ end
26
+
18
27
  cloned_node = node.clone
19
28
  equations = extract_equations(cloned_node)
20
29
  content = treat_children(cloned_node,
@@ -34,6 +43,26 @@ module Stepmod
34
43
  [content, equation_content].compact.join("")
35
44
  end
36
45
 
46
+ # <i><b>x</b></i> is written bold outside, italic inside: `*_x_*`.
47
+ def bold_italic(node, bold, state)
48
+ content = treat_children(bold, state.merge(already_italic: true))
49
+ return content if content.strip.empty?
50
+
51
+ lead = content[/^\s*/]
52
+ trail = content[/\s*$/]
53
+ Strong.new.mark_up(node, "#{lead}_#{content.strip}_#{trail}", state)
54
+ end
55
+
56
+ def sole_bold_child(node)
57
+ return unless node.element_children.one?
58
+ return if node.children.any? { |c| c.text? && !c.text.strip.empty? }
59
+
60
+ bold = node.element_children.first
61
+ return unless BOLD_TAGS.include?(bold.name)
62
+
63
+ bold unless bold.element_children.any? { |c| c.name == "eqn" }
64
+ end
65
+
37
66
  def extract_equations(node)
38
67
  equations = []
39
68
 
@@ -7,11 +7,12 @@ module Stepmod
7
7
  module Converters
8
8
  class Figure < Coradoc::Input::Html::Converters::Figure
9
9
  def self.pattern(state, id)
10
- if state[:schema_and_entity].nil?
10
+ schema_and_entity = state[:schema_and_entity] || state[:schema_name]
11
+ if schema_and_entity.nil?
11
12
  raise StandardError.new("[figure]: no state given, #{id}")
12
13
  end
13
14
 
14
- schema = state[:schema_and_entity].split(".").first
15
+ schema = schema_and_entity.split(".").first
15
16
  "figure-#{schema}-#{id}"
16
17
  end
17
18
 
@@ -10,8 +10,36 @@ module Stepmod
10
10
  bold_converted(node, state)
11
11
  end
12
12
 
13
+ # Wraps already converted +content+ in AsciiDoc bold markup, as
14
+ # +node+ (a <b>, or an element standing in for one) would.
15
+ def mark_up(node, content, state)
16
+ return content if content.strip.empty? || state[:already_strong] ||
17
+ state[:equation]
18
+
19
+ strong_tag = unconstrained?(node, content, state) ? "**" : "*"
20
+ leading = content[0, content.length - content.lstrip.length]
21
+ trailing = content[content.rstrip.length..]
22
+ handle_express_escape_seq(
23
+ node,
24
+ "#{leading}#{strong_tag}#{content.strip}#{strong_tag}#{trailing}",
25
+ )
26
+ end
27
+
13
28
  private
14
29
 
30
+ # Constrained bold (`*x*`) is not recognised inside a word, so
31
+ # unconstrained markup is needed whenever a word character touches it.
32
+ def unconstrained?(node, content, state)
33
+ return true if state[:non_flanking_whitesapce]
34
+
35
+ (content.match?(/\A\S/) && word_sibling?(node.previous, /\w\z/)) ||
36
+ (content.match?(/\S\z/) && word_sibling?(node.next, /\A\w/))
37
+ end
38
+
39
+ def word_sibling?(sibling, pattern)
40
+ sibling&.text? && sibling.text.match?(pattern)
41
+ end
42
+
15
43
  def bold_converted(node, state)
16
44
  cloned_node = node.clone
17
45
  equations = extract_equations(cloned_node)
@@ -23,14 +51,8 @@ module Stepmod
23
51
 
24
52
  content = if state[:equation] && state[:convert_bold_and_italics]
25
53
  "bb(#{content.strip})"
26
- elsif content.strip.empty? || state[:already_strong] || state[:equation]
27
- content
28
54
  else
29
- strong_tag = state[:non_flanking_whitesapce] ? "**" : "*"
30
- handle_express_escape_seq(
31
- node,
32
- "#{content[/^\s*/]}#{strong_tag}#{content.strip}#{strong_tag}#{content[/\s*$/]}",
33
- )
55
+ mark_up(node, content, state)
34
56
  end
35
57
 
36
58
  [content, equation_content].compact.join("")
@@ -1,3 +1,5 @@
1
+ require "pubid"
2
+
1
3
  module Stepmod
2
4
  module Utils
3
5
  class ExpressBibdata
@@ -22,7 +24,7 @@ module Stepmod
22
24
  @pubid = Pubid::Iso::Identifier.parse(published_info)
23
25
 
24
26
  @part = pubid.part&.to_i
25
- @version = pubid.edition&.to_i
27
+ @version = pubid.edition&.number&.to_i
26
28
  @pub_year = pubid.year&.to_i
27
29
  elsif !schema.version.nil?
28
30
  @part = schema.version.items.find { |i| i.name == "part" }.value&.to_i
@@ -87,7 +87,7 @@ module Stepmod
87
87
  # TODO there are schemas with only arm, mim, mim_lf:
88
88
  # schemas/modules/limited_length_or_area_indicator_assignment/mim_lf.exp
89
89
  part = @identifier.part
90
- edition = @identifier.edition
90
+ edition = @identifier.edition&.number
91
91
  schema_or_object = (schema_type == :module) ? "schema" : "object"
92
92
 
93
93
  "\nSCHEMA #{@schema_name} '{ " \
@@ -246,6 +246,11 @@ processed_images_cache)
246
246
  examples = description.xpath("example")
247
247
  figures = description.xpath("figure")
248
248
  tables = description.xpath("table")
249
+ state = {
250
+ schema_and_entity: description["linkend"],
251
+ descriptions_file: descriptions_file,
252
+ figure_ids: figures.map { |f| f["id"] || f["number"] },
253
+ }
249
254
 
250
255
  converted_description = <<~DESCRIPTION
251
256
 
@@ -253,6 +258,7 @@ processed_images_cache)
253
258
  wrapper,
254
259
  no_notes_examples: true,
255
260
  descriptions_file: descriptions_file,
261
+ figure_ids: state[:figure_ids],
256
262
  )}
257
263
  DESCRIPTION
258
264
 
@@ -263,7 +269,7 @@ processed_images_cache)
263
269
  converted_figures = figures.map do |figure|
264
270
  Stepmod::Utils::Converters::ExpressFigure
265
271
  .new
266
- .convert(figure, schema_and_entity: description["linkend"], descriptions_file: descriptions_file)
272
+ .convert(figure, state)
267
273
  end.join
268
274
 
269
275
  converted_tables = tables.map do |table|
@@ -275,13 +281,13 @@ processed_images_cache)
275
281
  converted_notes = notes.map do |note|
276
282
  Stepmod::Utils::Converters::ExpressNote
277
283
  .new
278
- .convert(note, schema_and_entity: description["linkend"], descriptions_file: descriptions_file)
284
+ .convert(note, state)
279
285
  end.join
280
286
 
281
287
  converted_examples = examples.map do |example|
282
288
  Stepmod::Utils::Converters::ExpressExample
283
289
  .new
284
- .convert(example, schema_and_entity: description["linkend"], descriptions_file: descriptions_file)
290
+ .convert(example, state)
285
291
  end.join
286
292
 
287
293
  [
@@ -402,16 +408,12 @@ processed_images_cache)
402
408
  year = "" if year == "tbd"
403
409
  edition = bib.attributes["version"].value
404
410
 
405
- pubid = Pubid::Iso::Identifier.create(
406
- publisher: "ISO",
407
- number: 10303,
408
- )
409
-
410
- pubid.part = part if part && !part.empty?
411
- pubid.year = year.split("-").first if year && !year.empty?
412
- pubid.edition = edition if edition && !edition.empty?
411
+ attrs = { number: 10303 }
412
+ attrs[:part] = part if part && !part.empty?
413
+ attrs[:date] = { year: year.split("-").first } if year && !year.empty?
414
+ attrs[:edition] = { number: edition } if edition && !edition.empty?
413
415
 
414
- pubid
416
+ Pubid::Iso::Identifier.new(**attrs)
415
417
  end
416
418
 
417
419
  def resource_bib_data(bib, schema_and_entity)
@@ -541,12 +541,12 @@ document:)
541
541
  raise Error.new("[stepmod-utils] encountered unsupported schema_type")
542
542
  end
543
543
 
544
- if entity.subtype_of.empty?
544
+ if entity.subtype_of.to_a.empty?
545
545
  "#{entity_type} " \
546
546
  "that represents the " \
547
547
  "#{entity_name_to_text(entity.id)} {{entity}}"
548
548
  else
549
- entity_subtypes = entity.subtype_of.map do |e|
549
+ entity_subtypes = entity.subtype_of.to_a.map do |e|
550
550
  "{{#{e.id}}}"
551
551
  end
552
552
 
@@ -1,5 +1,5 @@
1
1
  module Stepmod
2
2
  module Utils
3
- VERSION = "0.6.10.pre.alpha.1".freeze
3
+ VERSION = "0.6.10".freeze
4
4
  end
5
5
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
  spec.files = `git ls-files`.split("\n")
20
20
  spec.test_files = `git ls-files -- {spec}/*`.split("\n")
21
- spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -42,7 +42,8 @@ Gem::Specification.new do |spec|
42
42
  spec.add_dependency "ptools"
43
43
  # The pubid monogem ships Pubid::Iso itself; the separate pubid-iso
44
44
  # gem is superseded and its lib/pubid/iso.rb shadows the monogem's.
45
- spec.add_dependency "pubid", "~> 2.0.0.pre.alpha"
45
+ spec.add_dependency "pubid", "~> 2.0"
46
+ spec.add_dependency "relaton", "~> 3.0"
46
47
  spec.add_dependency "shale"
47
48
  spec.add_dependency "thor", ">= 0.20"
48
49
  spec.add_dependency "unitsml"
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stepmod-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.10.pre.alpha.1
4
+ version: 0.6.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-11-06 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: concurrent-ruby
@@ -141,14 +142,28 @@ dependencies:
141
142
  requirements:
142
143
  - - "~>"
143
144
  - !ruby/object:Gem::Version
144
- version: 2.0.0.pre.alpha
145
+ version: '2.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: relaton
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.0'
145
160
  type: :runtime
146
161
  prerelease: false
147
162
  version_requirements: !ruby/object:Gem::Requirement
148
163
  requirements:
149
164
  - - "~>"
150
165
  - !ruby/object:Gem::Version
151
- version: 2.0.0.pre.alpha
166
+ version: '3.0'
152
167
  - !ruby/object:Gem::Dependency
153
168
  name: shale
154
169
  requirement: !ruby/object:Gem::Requirement
@@ -198,6 +213,7 @@ email:
198
213
  - open.source@ribose.com
199
214
  executables:
200
215
  - stepmod-annotate-all
216
+ - stepmod-convert-bibliography
201
217
  - stepmod-convert-express-description
202
218
  - stepmod-convert-express-resource
203
219
  - stepmod-extract-changes
@@ -221,6 +237,7 @@ files:
221
237
  - bin/console
222
238
  - bin/setup
223
239
  - exe/stepmod-annotate-all
240
+ - exe/stepmod-convert-bibliography
224
241
  - exe/stepmod-convert-express-description
225
242
  - exe/stepmod-convert-express-resource
226
243
  - exe/stepmod-extract-changes
@@ -229,6 +246,7 @@ files:
229
246
  - exe/stepmod-validate-express
230
247
  - lib/stepmod/utils.rb
231
248
  - lib/stepmod/utils/bibdata.rb
249
+ - lib/stepmod/utils/bibliography_converter.rb
232
250
  - lib/stepmod/utils/change.rb
233
251
  - lib/stepmod/utils/change_collection.rb
234
252
  - lib/stepmod/utils/change_edition.rb
@@ -346,6 +364,7 @@ homepage: https://github.com/metanorma/stepmod-utils
346
364
  licenses:
347
365
  - BSD-2-Clause
348
366
  metadata: {}
367
+ post_install_message:
349
368
  rdoc_options: []
350
369
  require_paths:
351
370
  - lib
@@ -353,14 +372,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
353
372
  requirements:
354
373
  - - ">="
355
374
  - !ruby/object:Gem::Version
356
- version: 3.2.0
375
+ version: 3.3.0
357
376
  required_rubygems_version: !ruby/object:Gem::Requirement
358
377
  requirements:
359
378
  - - ">="
360
379
  - !ruby/object:Gem::Version
361
380
  version: '0'
362
381
  requirements: []
363
- rubygems_version: 3.6.9
382
+ rubygems_version: 3.5.22
383
+ signing_key:
364
384
  specification_version: 4
365
385
  summary: Stepmod-utils is a toolkit that works on STEPmod data.
366
386
  test_files: []