metanorma-iso 2.0.4 → 2.0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/isodoc/iso/html/header.html +42 -33
  3. data/lib/isodoc/iso/html/isodoc.css +9 -7
  4. data/lib/isodoc/iso/html/isodoc.scss +7 -5
  5. data/lib/isodoc/iso/html/style-human.css +0 -1
  6. data/lib/isodoc/iso/html/style-human.scss +0 -1
  7. data/lib/isodoc/iso/html/style-iso.css +0 -1
  8. data/lib/isodoc/iso/html/style-iso.scss +0 -1
  9. data/lib/isodoc/iso/html/word_iso_titlepage.html +12 -11
  10. data/lib/isodoc/iso/html/wordstyle.css +46 -19
  11. data/lib/isodoc/iso/html/wordstyle.scss +52 -25
  12. data/lib/isodoc/iso/i18n-en.yaml +7 -2
  13. data/lib/isodoc/iso/i18n-fr.yaml +5 -2
  14. data/lib/isodoc/iso/i18n-ru.yaml +5 -2
  15. data/lib/isodoc/iso/i18n-zh-Hans.yaml +6 -2
  16. data/lib/isodoc/iso/init.rb +1 -2
  17. data/lib/isodoc/iso/iso.amendment.xsl +704 -328
  18. data/lib/isodoc/iso/iso.international-standard.xsl +704 -328
  19. data/lib/isodoc/iso/presentation_xml_convert.rb +58 -21
  20. data/lib/metanorma/iso/boilerplate-fr.xml +5 -6
  21. data/lib/metanorma/iso/boilerplate-ru.xml +4 -6
  22. data/lib/metanorma/iso/boilerplate.xml +5 -6
  23. data/lib/metanorma/iso/cleanup.rb +43 -10
  24. data/lib/metanorma/iso/front_id.rb +1 -0
  25. data/lib/metanorma/iso/processor.rb +14 -7
  26. data/lib/metanorma/iso/validate.rb +29 -1
  27. data/lib/metanorma/iso/validate_image.rb +3 -3
  28. data/lib/metanorma/iso/validate_list.rb +122 -0
  29. data/lib/metanorma/iso/validate_section.rb +39 -32
  30. data/lib/metanorma/iso/validate_style.rb +32 -2
  31. data/lib/metanorma/iso/validate_title.rb +13 -1
  32. data/lib/metanorma/iso/version.rb +1 -1
  33. data/metanorma-iso.gemspec +1 -1
  34. data/spec/isodoc/inline_spec.rb +127 -10
  35. data/spec/isodoc/postproc_spec.rb +2 -4
  36. data/spec/isodoc/terms_spec.rb +4 -4
  37. data/spec/metanorma/cleanup_spec.rb +11 -11
  38. data/spec/metanorma/refs_spec.rb +273 -61
  39. data/spec/metanorma/section_spec.rb +18 -26
  40. data/spec/metanorma/validate_spec.rb +448 -17
  41. data/spec/spec_helper.rb +6 -4
  42. data/spec/vcr_cassettes/docrels.yml +393 -0
  43. data/spec/vcr_cassettes/withdrawn_iso.yml +301 -0
  44. metadata +8 -5
@@ -47,37 +47,74 @@ module IsoDoc
47
47
 
48
48
  def eref_delim(delim, type)
49
49
  if delim == ";" then ";"
50
- else type == "list" ? "" : delim
50
+ else type == "list" ? " " : delim
51
51
  end
52
52
  end
53
53
 
54
- def eref_localities1_zh(target, type, from, upto, node, delim)
55
- subsection = from&.text&.match(/\./)
56
- ret = eref_delim(delim, type)
57
- ret += " 第#{from.text}" if from
58
- ret += "–#{upto.text}" if upto
59
- loc = (@i18n.locality[type] || type.sub(/^locality:/, "").capitalize)
60
- ret += " #{loc}" unless (subsection && type == "clause") ||
61
- type == "list" || target.match(/^IEV$|^IEC 60050-/) ||
62
- node["droploc"] == "true"
54
+ def can_conflate_eref_rendering?(refs)
55
+ super or return false
56
+
57
+ first = subclause?(nil, refs.first.at(ns("./locality/@type"))&.text,
58
+ refs.first.at(ns("./locality/referenceFrom"))&.text)
59
+ refs.all? do |r|
60
+ subclause?(nil, r.at(ns("./locality/@type"))&.text,
61
+ r.at(ns("./locality/referenceFrom"))&.text) == first
62
+ end
63
+ end
64
+
65
+ def locality_delimiter(loc)
66
+ loc&.next_element&.attribute("type")&.text == "list" and return " "
67
+ super
68
+ end
69
+
70
+ def eref_localities_conflated(refs, target, node)
71
+ droploc = node["droploc"]
72
+ node["droploc"] = true
73
+ ret = resolve_eref_connectives(eref_locality_stacks(refs, target,
74
+ node))
75
+ node["droploc"] = droploc
76
+ eref_localities1(target,
77
+ prefix_clause(target, refs.first.at(ns("./locality"))),
78
+ l10n(ret[1..-1].join), nil, node, @lang)
79
+ end
80
+
81
+ def prefix_clause(target, loc)
82
+ loc["type"] == "clause" or return loc["type"]
83
+
84
+ if subclause?(target, loc["type"], loc&.at(ns("./referenceFrom"))&.text)
85
+ ""
86
+ else
87
+ "clause"
88
+ end
89
+ end
90
+
91
+ def subclause?(target, type, from)
92
+ (from&.match?(/\./) && type == "clause") ||
93
+ type == "list" || target&.match(/^IEV$|^IEC 60050-/)
94
+ end
95
+
96
+ def eref_localities1_zh(target, type, from, upto, node)
97
+ ret = " 第#{from}" if from
98
+ ret += "–#{upto}" if upto
99
+ if node["droploc"] != "true" && !subclause?(target, type, from)
100
+ ret += eref_locality_populate(type, node)
101
+ end
63
102
  ret += ")" if type == "list"
64
103
  ret
65
104
  end
66
105
 
67
- def eref_localities1(target, type, from, upto, delim, node, lang = "en")
68
- return "" if type == "anchor"
106
+ def eref_localities1(target, type, from, upto, node, lang = "en")
107
+ return nil if type == "anchor"
69
108
 
70
- subsection = from&.text&.match(/\./)
71
109
  type = type.downcase
72
110
  lang == "zh" and
73
- return l10n(eref_localities1_zh(target, type, from, upto, node,
74
- delim))
75
- ret = eref_delim(delim, type)
76
- ret += eref_locality_populate(type, node) unless (subsection &&
77
- type == "clause") || type == "list" ||
78
- target.match(/^IEV$|^IEC 60050-/)
79
- ret += " #{from.text}" if from
80
- ret += "–#{upto.text}" if upto
111
+ return l10n(eref_localities1_zh(target, type, from, upto, node))
112
+ ret = if node["droploc"] != "true" && !subclause?(target, type, from)
113
+ eref_locality_populate(type, node)
114
+ else ""
115
+ end
116
+ ret += " #{from}" if from
117
+ ret += "–#{upto}" if upto
81
118
  ret += ")" if type == "list"
82
119
  l10n(ret)
83
120
  end
@@ -1,11 +1,11 @@
1
1
  <boilerplate>
2
2
  <copyright-statement>
3
3
  <clause>
4
- <title>DOCUMENT PROTÉGÉ PAR COPYRIGHT</title>
5
4
  <p id="boilerplate-year">&#xa9; {{ agency }} {{ docyear }}</p>
6
5
 
7
6
  <p id="boilerplate-message">
8
- Droits de reproduction réservés. Sauf indication contraire, aucune partie de cette publication ne
7
+ Droits de reproduction réservés. Sauf indication contraire, ou requise dans le cadre de sa mise en œuvre,
8
+ aucune partie de cette publication ne
9
9
  peut être reproduite ni utilisée sous quelque forme que ce soit et par aucun procédé, électronique
10
10
  ou mécanique, y compris la photocopie, l’affichage sur l’internet ou sur un Intranet, sans
11
11
  autorisation écrite préalable. Les demandes d’autorisation peuvent être adressées à l’ISO à
@@ -14,10 +14,9 @@ l’adresse ci-après ou au comité membre de l’ISO dans le pays du demandeur.
14
14
 
15
15
  <p id="boilerplate-address" align="left">
16
16
  ISO copyright office<br/>
17
- Ch. de Blandonnet 8 &#x2022; CP 401<br/>
18
- CH-1214 Vernier, Geneva, Switzerland<br/>
19
- Tel.&#xa0;&#xa0;+ 41 22 749 01 11<br/>
20
- Fax&#xa0;&#xa0;+ 41 22 749 09 47<br/>
17
+ CP 401 &#x2022; Ch. de Blandonnet 8<br/>
18
+ CH-1214 Vernier, Geneva<br/>
19
+ Tél.&#xa0;&#xa0;+ 41 22 749 01 11<br/>
21
20
  Email: copyright@iso.org<br/>
22
21
  Website: www.iso.org
23
22
  </p>
@@ -1,21 +1,19 @@
1
1
  <boilerplate>
2
2
  <copyright-statement>
3
3
  <clause>
4
- <title>ДОКУМЕНТ, ОХРАНЯЕМЫЙ АВТОРСКИМ ПРАВОМ</title>
5
4
  <p id="boilerplate-year">
6
5
  &#xa9; {{ agency }} {{ docyear }}
7
6
  </p>
8
7
 
9
8
  <p id="boilerplate-message">
10
- Все права защищены. Если иначе не определено, никакая часть этой публикации не может быть воспроизведена или использована иначе в любой форме или каким-либо образом, электронным или механическим, включая фотокопирование, или публикацию в Интернете или интранете, без предварительного письменного разрешения. Разрешение может быть запрошено ISO по адресу, указанному ниже, или у органа — члена ISO страны запрашивающего.
9
+ Все права защищены. Если иначе не определено или не требуется в контексте его реализации, никакая часть этой публикации не может быть воспроизведена или использована иначе в любой форме или каким-либо образом, электронным или механическим, включая фотокопирование, или публикацию в Интернете или интранете, без предварительного письменного разрешения. Разрешение может быть запрошено ISO по адресу, указанному ниже, или у органа — члена ISO страны запрашивающего.
11
10
  </p>
12
11
 
13
12
  <p id="boilerplate-address" align="left">
14
13
  Бюро ISO по охране авторских прав<br/>
15
- Ch. de Blandonnet 8 &#x2022; CP 401<br/>
16
- CH-1214 Vernier, Geneva, Switzerland<br/>
17
- Tel.&#xa0;&#xa0;+ 41 22 749 01 11<br/>
18
- Fax&#xa0;&#xa0;+ 41 22 749 09 47<br/>
14
+ CP 401 &#x2022; Ch. de Blandonnet 8<br/>
15
+ CH-1214 Vernier, Geneva<br/>
16
+ Тел.&#xa0;&#xa0;+ 41 22 749 01 11<br/>
19
17
  Электронная почта: copyright@iso.org<br/>
20
18
  Сайт: www.iso.org
21
19
  </p>
@@ -1,14 +1,14 @@
1
1
  <boilerplate>
2
2
  <copyright-statement>
3
3
  <clause>
4
- <title>COPYRIGHT PROTECTED DOCUMENT</title>
5
4
  <p id="boilerplate-year">
6
5
  &#xa9; {{ agency }} {{ docyear }}
7
6
  </p>
8
7
 
9
8
  <p id="boilerplate-message">
10
9
  All rights
11
- reserved. Unless otherwise specified, no part of this publication may be
10
+ reserved. Unless otherwise specified, or required in the context of its implementation,
11
+ no part of this publication may be
12
12
  reproduced or utilized otherwise in any form or by any means, electronic or
13
13
  mechanical, including photocopying, or posting on the internet or an intranet,
14
14
  without prior written permission. Permission can be requested from either ISO
@@ -17,10 +17,9 @@ at the address below or ISO's member body in the country of the requester.
17
17
 
18
18
  <p id="boilerplate-address" align="left">
19
19
  ISO copyright office<br/>
20
- Ch. de Blandonnet 8 &#x2022; CP 401<br/>
21
- CH-1214 Vernier, Geneva, Switzerland<br/>
22
- Tel.&#xa0;&#xa0;+ 41 22 749 01 11<br/>
23
- Fax&#xa0;&#xa0;+ 41 22 749 09 47<br/>
20
+ CP 401 &#x2022; Ch. de Blandonnet 8<br/>
21
+ CH-1214 Vernier, Geneva<br/>
22
+ Phone:&#xa0;&#xa0;+ 41 22 749 01 11<br/>
24
23
  Email: copyright@iso.org<br/>
25
24
  Website: www.iso.org
26
25
  </p>
@@ -37,13 +37,11 @@ module Metanorma
37
37
  end
38
38
 
39
39
  def get_id_prefix(xmldoc)
40
- prefix = []
41
40
  xmldoc.xpath("//bibdata/contributor[role/@type = 'publisher']"\
42
- "/organization").each do |x|
41
+ "/organization").each_with_object([]) do |x, prefix|
43
42
  x1 = x.at("abbreviation")&.text || x.at("name")&.text
44
43
  (x1 == "ISO" and prefix.unshift("ISO")) or prefix << x1
45
44
  end
46
- prefix
47
45
  end
48
46
 
49
47
  # ISO as a prefix goes first
@@ -140,8 +138,7 @@ module Metanorma
140
138
 
141
139
  def unpub_footnotes(xmldoc)
142
140
  xmldoc.xpath("//bibitem/note[@type = 'Unpublished-Status']").each do |n|
143
- id = n.parent["id"]
144
- e = xmldoc.at("//eref[@bibitemid = '#{id}']") or next
141
+ e = xmldoc.at("//eref[@bibitemid = '#{n.parent['id']}']") or next
145
142
  fn = n.children.to_xml
146
143
  n&.elements&.first&.name == "p" or fn = "<p>#{fn}</p>"
147
144
  e.next = "<fn>#{fn}</fn>"
@@ -151,21 +148,52 @@ module Metanorma
151
148
  def bibitem_cleanup(xmldoc)
152
149
  super
153
150
  unpublished_note(xmldoc)
151
+ withdrawn_note(xmldoc)
154
152
  end
155
153
 
156
154
  def unpublished_note(xmldoc)
157
- xmldoc.xpath("//bibitem[not(note[@type = 'Unpublished-Status'])]")
158
- .each do |b|
155
+ xmldoc.xpath("//bibitem[not(./ancestor::bibitem)]"\
156
+ "[not(note[@type = 'Unpublished-Status'])]").each do |b|
159
157
  next if pub_class(b) > 2
160
158
  next unless (s = b.at("./status/stage")) && (s.text.to_i < 60)
161
159
 
162
160
  id = b.at("docidentifier").text
163
- b.at("./language | ./script | ./abstract | ./status")
164
- .previous = %(<note type="Unpublished-Status">
165
- <p>#{@i18n.under_preparation.sub(/%/, id)}</p></note>)
161
+ insert_unpub_note(b, @i18n.under_preparation.sub(/%/, id))
166
162
  end
167
163
  end
168
164
 
165
+ def withdrawn_note(xmldoc)
166
+ xmldoc.xpath("//bibitem[not(note[@type = 'Unpublished-Status'])]")
167
+ .each do |b|
168
+ next unless withdrawn_ref?(b)
169
+
170
+ if id = replacement_standard(b)
171
+ insert_unpub_note(b, @i18n.cancelled_and_replaced.sub(/%/, id))
172
+ else
173
+ insert_unpub_note(b, @i18n.withdrawn)
174
+ end
175
+ end
176
+ end
177
+
178
+ def withdrawn_ref?(biblio)
179
+ return false if pub_class(biblio) > 2
180
+
181
+ (s = biblio.at("./status/stage")) && (s.text.to_i == 95) &&
182
+ (t = biblio.at("./status/substage")) && (t.text.to_i == 99)
183
+ end
184
+
185
+ def replacement_standard(biblio)
186
+ r = biblio.at("./relation[@type = 'updates']/bibitem") or return nil
187
+ id = r.at("./formattedref | ./docidentifier[@primary = 'true'] | "\
188
+ "./docidentifier | ./formattedref") or return nil
189
+ id.text
190
+ end
191
+
192
+ def insert_unpub_note(biblio, msg)
193
+ biblio.at("./language | ./script | ./abstract | ./status")
194
+ .previous = %(<note type="Unpublished-Status"><p>#{msg}</p></note>)
195
+ end
196
+
169
197
  def termdef_boilerplate_insert(xmldoc, isodoc, once = false)
170
198
  once = true
171
199
  super
@@ -175,6 +203,11 @@ module Metanorma
175
203
  @vocab and src.empty? and return
176
204
  super
177
205
  end
206
+
207
+ def section_names_terms_cleanup(xml)
208
+ @vocab and return
209
+ super
210
+ end
178
211
  end
179
212
  end
180
213
  end
@@ -39,6 +39,7 @@ module Metanorma
39
39
  ret = STAGE_ABBRS[stage.to_sym]
40
40
  ret = "PRF" if stage == "60" && substage == "00"
41
41
  ret = "AWI" if stage == "10" && substage == "99"
42
+ ret = "AWI" if stage == "20" && substage == "00"
42
43
  if %w(amendment technical-corrigendum technical-report
43
44
  technical-specification).include?(doctype)
44
45
  ret = "D" if stage == "40" && doctype == "amendment"
@@ -45,19 +45,26 @@ module Metanorma
45
45
  def output(isodoc_node, inname, outname, format, options={})
46
46
  case format
47
47
  when :html
48
- IsoDoc::Iso::HtmlConvert.new(options).convert(inname, isodoc_node, nil, outname)
48
+ IsoDoc::Iso::HtmlConvert.new(options)
49
+ .convert(inname, isodoc_node, nil, outname)
49
50
  when :html_alt
50
- IsoDoc::Iso::HtmlConvert.new(options.merge(alt: true)).convert(inname, isodoc_node, nil, outname)
51
+ IsoDoc::Iso::HtmlConvert.new(options.merge(alt: true))
52
+ .convert(inname, isodoc_node, nil, outname)
51
53
  when :doc
52
- IsoDoc::Iso::WordConvert.new(options).convert(inname, isodoc_node, nil, outname)
54
+ IsoDoc::Iso::WordConvert.new(options)
55
+ .convert(inname, isodoc_node, nil, outname)
53
56
  when :pdf
54
- IsoDoc::Iso::PdfConvert.new(options).convert(inname, isodoc_node, nil, outname)
57
+ IsoDoc::Iso::PdfConvert.new(options)
58
+ .convert(inname, isodoc_node, nil, outname)
55
59
  when :sts
56
- IsoDoc::Iso::StsConvert.new(options).convert(inname, isodoc_node, nil, outname)
60
+ IsoDoc::Iso::StsConvert.new(options)
61
+ .convert(inname, isodoc_node, nil, outname)
57
62
  when :isosts
58
- IsoDoc::Iso::IsoStsConvert.new(options).convert(inname, isodoc_node, nil, outname)
63
+ IsoDoc::Iso::IsoStsConvert.new(options)
64
+ .convert(inname, isodoc_node, nil, outname)
59
65
  when :presentation
60
- IsoDoc::Iso::PresentationXMLConvert.new(options).convert(inname, isodoc_node, nil, outname)
66
+ IsoDoc::Iso::PresentationXMLConvert.new(options)
67
+ .convert(inname, isodoc_node, nil, outname)
61
68
  else
62
69
  super
63
70
  end
@@ -4,6 +4,7 @@ require_relative "./validate_requirements"
4
4
  require_relative "./validate_section"
5
5
  require_relative "./validate_title"
6
6
  require_relative "./validate_image"
7
+ require_relative "./validate_list"
7
8
  require "nokogiri"
8
9
  require "jing"
9
10
  require "iev"
@@ -35,7 +36,7 @@ module Metanorma
35
36
  /\b(see| refer to)\s*\Z/mi.match(preceding)
36
37
 
37
38
  (target = root.at("//*[@id = '#{t['target']}']")) || next
38
- if target&.at("./ancestor-or-self::*[@obligation = 'normative']")
39
+ if target.at("./ancestor-or-self::*[@obligation = 'normative']")
39
40
  @log.add("Style", t,
40
41
  "'see #{t['target']}' is pointing to a normative section")
41
42
  end
@@ -75,6 +76,30 @@ module Metanorma
75
76
  regex.match(text) && @log.add("Style", elem, "#{term}: #{msg}")
76
77
  end
77
78
 
79
+ # https://www.iso.org/ISO-house-style.html#iso-hs-s-text-r-r-ref_clause3
80
+ def term_xrefs_validate(xmldoc)
81
+ termids = xmldoc
82
+ .xpath("//sections/terms | //sections/clause[.//terms] | "\
83
+ "//annex[.//terms]").each_with_object({}) do |t, m|
84
+ t.xpath(".//*/@id").each { |a| m[a.text] = true }
85
+ t.name == "terms" and m[t["id"]] = true
86
+ end
87
+ xmldoc.xpath(".//xref").each do |x|
88
+ term_xrefs_validate1(x, termids)
89
+ end
90
+ end
91
+
92
+ def term_xrefs_validate1(xref, termids)
93
+ (termids[xref["target"]] && !termids[xref.parent["id"]]) and
94
+ @log.add("Style", xref,
95
+ "only terms clauses can cross-reference terms clause "\
96
+ "(#{xref['target']})")
97
+ (!termids[xref["target"]] && termids[xref.parent["id"]]) and
98
+ @log.add("Style", xref,
99
+ "non-terms clauses cannot cross-reference terms clause "\
100
+ "(#{xref['target']})")
101
+ end
102
+
78
103
  # ISO/IEC DIR 2, 16.5.6
79
104
  def termdef_style(xmldoc)
80
105
  xmldoc.xpath("//term").each do |t|
@@ -139,11 +164,14 @@ module Metanorma
139
164
  onlychild_clause_validate(doc.root)
140
165
  termdef_style(doc.root)
141
166
  see_xrefs_validate(doc.root)
167
+ term_xrefs_validate(doc.root)
142
168
  see_erefs_validate(doc.root)
143
169
  locality_erefs_validate(doc.root)
144
170
  bibdata_validate(doc.root)
145
171
  bibitem_validate(doc.root)
146
172
  figure_validate(doc.root)
173
+ listcount_validate(doc)
174
+ list_punctuation(doc)
147
175
  end
148
176
 
149
177
  def bibitem_validate(xmldoc)
@@ -78,9 +78,9 @@ module Metanorma
78
78
  xmldoc.xpath("//image").each do |i|
79
79
  next if i["src"].start_with?("data:")
80
80
 
81
- if /^ISO_\d+_/.match?(File.basename(i["src"]))
82
- elsif /^(SL)?#{prefix}fig/.match?(File.basename(i["src"]))
83
- image_name_validate1(i, prefix)
81
+ case File.basename(i["src"])
82
+ when /^ISO_\d+_/
83
+ when /^(SL)?#{prefix}fig/ then image_name_validate1(i, prefix)
84
84
  else
85
85
  @log.add("Style", i,
86
86
  "image name #{i['src']} does not match DRG requirements: expect #{prefix}fig")
@@ -0,0 +1,122 @@
1
+ module Metanorma
2
+ module ISO
3
+ class Converter < Standoc::Converter
4
+ # https://www.iso.org/ISO-house-style.html#iso-hs-s-text-r-p-lists
5
+ def listcount_validate(doc)
6
+ return if @novalid
7
+
8
+ ol_count_validate(doc)
9
+ li_depth_validate(doc)
10
+ end
11
+
12
+ def ol_count_validate(doc)
13
+ doc.xpath("//clause | //annex").each do |c|
14
+ next if c.xpath(".//ol").empty?
15
+
16
+ ols = c.xpath(".//ol") -
17
+ c.xpath(".//ul//ol | .//ol//ol | .//clause//ol")
18
+ ols.size > 1 and
19
+ style_warning(c, "More than 1 ordered list in a numbered clause",
20
+ nil)
21
+ end
22
+ end
23
+
24
+ def li_depth_validate(doc)
25
+ doc.xpath("//li//li//li//li").each do |l|
26
+ l.at(".//li") and
27
+ style_warning(l, "List more than four levels deep", nil)
28
+ end
29
+ end
30
+
31
+ # https://www.iso.org/ISO-house-style.html#iso-hs-s-text-r-p-lists
32
+ def list_punctuation(doc)
33
+ return if @novalid
34
+
35
+ ((doc.xpath("//ol") - doc.xpath("//ul//ol | //ol//ol")) +
36
+ (doc.xpath("//ul") - doc.xpath("//ul//ul | //ol//ul"))).each do |list|
37
+ next if skip_list_punctuation(list)
38
+
39
+ prec = list.at("./preceding::text()[normalize-space(.) != ''][1]")
40
+ list_punctuation1(list, prec&.text)
41
+ end
42
+ end
43
+
44
+ def skip_list_punctuation(list)
45
+ return true if list.at("./ancestor::table")
46
+ return true if list.at("./following-sibling::term") # terms boilerplate
47
+
48
+ list.xpath(".//li").each do |entry|
49
+ l = entry.dup
50
+ l.xpath(".//ol | .//ul").each(&:remove)
51
+ l.text.split.size > 2 and return false
52
+ end
53
+ true
54
+ end
55
+
56
+ def list_punctuation1(list, prectext)
57
+ prectext ||= ""
58
+ entries = list.xpath(".//li")
59
+ case prectext.strip.chars.last
60
+ when ":", "" then list_after_colon_punctuation(list, entries)
61
+ when "." then entries.each { |li| list_full_sentence(li) }
62
+ else style_warning(list, "All lists must be preceded by "\
63
+ "colon or full stop", prectext)
64
+ end
65
+ end
66
+
67
+ # if first list entry starts lowercase, treat as sentence broken up
68
+ def list_after_colon_punctuation(list, entries)
69
+ lower = starts_lowercase?(list.at(".//li").text)
70
+ entries.each_with_index do |li, i|
71
+ if lower
72
+ list_semicolon_phrase(li, i == entries.size - 1)
73
+ else
74
+ list_full_sentence(li)
75
+ end
76
+ end
77
+ end
78
+
79
+ def list_semicolon_phrase(elem, last)
80
+ text = elem.text.strip
81
+ starts_lowercase?(text) or
82
+ style_warning(elem, "List entry of broken up sentence must start "\
83
+ "with lowercase letter", text)
84
+ list_semicolon_phrase_punct(elem, text, last)
85
+ end
86
+
87
+ def list_semicolon_phrase_punct(elem, text, last)
88
+ punct = text.strip.sub(/^.*?(\S)$/m, "\\1")
89
+ if last
90
+ punct == "." or
91
+ style_warning(elem, "Final list entry of broken up "\
92
+ "sentence must end with full stop", text)
93
+ else
94
+ punct == ";" or
95
+ style_warning(elem, "List entry of broken up sentence must "\
96
+ "end with semicolon", text)
97
+ end
98
+ end
99
+
100
+ def list_full_sentence(elem)
101
+ text = elem.text.strip
102
+ starts_uppercase?(text) or
103
+ style_warning(elem, "List entry of separate sentences must start "\
104
+ "with uppercase letter", text)
105
+ punct = text.strip.sub(/^.*?(\S)$/m, "\\1")
106
+ punct == "." or
107
+ style_warning(elem, "List entry of separate sentences must "\
108
+ "end with full stop", text)
109
+ end
110
+
111
+ # allow that all-caps word (acronym) is agnostic as to lowercase
112
+ def starts_lowercase?(text)
113
+ text.match?(/^[^[[:upper:]][[:lower:]]]*[[:lower:]]/) ||
114
+ text.match?(/^[^[[:upper:]][[:lower:]]]*[[:upper:]][[:upper:]]+[^[[:alpha:]]]/)
115
+ end
116
+
117
+ def starts_uppercase?(text)
118
+ text.match?(/^[^[[:upper:]][[:lower:]]]*[[:upper:]]/)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -14,6 +14,7 @@ module Metanorma
14
14
  end
15
15
  section_style(doc.root)
16
16
  subclause_validate(doc.root)
17
+ @vocab and vocab_terms_titles_validate(doc.root)
17
18
  super
18
19
  end
19
20
 
@@ -33,7 +34,6 @@ module Metanorma
33
34
 
34
35
  ONE_SYMBOLS_WARNING = "Only one Symbols and Abbreviated "\
35
36
  "Terms section in the standard".freeze
36
-
37
37
  NON_DL_SYMBOLS_WARNING = "Symbols and Abbreviated Terms can "\
38
38
  "only contain a definition list".freeze
39
39
 
@@ -42,11 +42,12 @@ module Metanorma
42
42
  f.empty? && return
43
43
  (f.size == 1 || @vocab) or
44
44
  @log.add("Style", f.first, ONE_SYMBOLS_WARNING)
45
- f.first.elements.each do |e|
46
- unless %w(title dl).include? e.name
47
- @log.add("Style", f.first, NON_DL_SYMBOLS_WARNING)
48
- return
49
- end
45
+ f.first.elements.reject { |e| %w(title dl).include? e.name }.empty? or
46
+ @log.add("Style", f.first, NON_DL_SYMBOLS_WARNING)
47
+ @vocab and f.each do |f1|
48
+ f1.at("./ancestor::annex") or
49
+ @log.add("Style", f1, "In vocabulary documents, Symbols and "\
50
+ "Abbreviated Terms are only permitted in annexes")
50
51
  end
51
52
  end
52
53
 
@@ -72,26 +73,17 @@ module Metanorma
72
73
 
73
74
  # spec of permissible section sequence
74
75
  # we skip normative references, it goes to end of list
75
- SEQ =
76
- [
77
- {
78
- msg: "Initial section must be (content) Foreword",
79
- val: ["./self::foreword"],
80
- },
81
- {
82
- msg: "Prefatory material must be followed by (clause) Scope",
83
- val: ["./self::introduction", "./self::clause[@type = 'scope']"],
84
- },
85
- {
86
- msg: "Prefatory material must be followed by (clause) Scope",
87
- val: ["./self::clause[@type = 'scope']"],
88
- },
89
- {
90
- msg: "Normative References must be followed by "\
91
- "Terms and Definitions",
92
- val: ["./self::terms | .//terms"],
93
- },
94
- ].freeze
76
+ SEQ = [
77
+ { msg: "Initial section must be (content) Foreword",
78
+ val: ["./self::foreword"] },
79
+ { msg: "Prefatory material must be followed by (clause) Scope",
80
+ val: ["./self::introduction", "./self::clause[@type = 'scope']"] },
81
+ { msg: "Prefatory material must be followed by (clause) Scope",
82
+ val: ["./self::clause[@type = 'scope']"] },
83
+ { msg: "Normative References must be followed by "\
84
+ "Terms and Definitions",
85
+ val: ["./self::terms | .//terms"] },
86
+ ].freeze
95
87
 
96
88
  SECTIONS_XPATH =
97
89
  "//foreword | //introduction | //sections/terms | .//annex | "\
@@ -198,11 +190,6 @@ module Metanorma
198
190
  end
199
191
  end
200
192
 
201
- ASSETS_TO_STYLE =
202
- "//termsource | //formula | //termnote | "\
203
- "//p[not(ancestor::boilerplate)] | //li[not(p)] | //dt | "\
204
- "//dd[not(p)] | //td[not(p)] | //th[not(p)]".freeze
205
-
206
193
  NORM_BIBITEMS =
207
194
  "//references[@normative = 'true']/bibitem".freeze
208
195
 
@@ -217,7 +204,9 @@ module Metanorma
217
204
 
218
205
  def asset_style(root)
219
206
  root.xpath("//example | //termexample").each { |e| example_style(e) }
220
- root.xpath("//definition/verbal-definition").each { |e| definition_style(e) }
207
+ root.xpath("//definition/verbal-definition").each do |e|
208
+ definition_style(e)
209
+ end
221
210
  root.xpath("//note").each { |e| note_style(e) }
222
211
  root.xpath("//fn").each { |e| footnote_style(e) }
223
212
  root.xpath(ASSETS_TO_STYLE).each { |e| style(e, extract_text(e)) }
@@ -243,6 +232,24 @@ module Metanorma
243
232
  @log.add("Style", nil, "#{location}: subclause is only child")
244
233
  end
245
234
  end
235
+
236
+ # https://www.iso.org/ISO-house-style.html#iso-hs-s-formatting-r-vocabulary
237
+ def vocab_terms_titles_validate(root)
238
+ terms = root.xpath("//sections/terms | //sections/clause[.//terms]")
239
+ if terms.size == 1
240
+ ((t = terms.first.at("./title")) && (t&.text == @i18n.termsdef)) or
241
+ @log.add("Style", terms.first,
242
+ "Single terms clause in vocabulary document "\
243
+ "should have normal Terms and definitions heading")
244
+ elsif terms.size > 1
245
+ terms.each do |x|
246
+ ((t = x.at("./title")) && /^#{@i18n.termsrelated}/.match?(t&.text)) or
247
+ @log.add("Style", x,
248
+ "Multiple terms clauses in vocabulary document "\
249
+ "should have 'Terms related to' heading")
250
+ end
251
+ end
252
+ end
246
253
  end
247
254
  end
248
255
  end