require "nokogiri" module Metanorma module ISO class Converter < Standoc::Converter def section_validate(doc) doctype = doc.at("//bibdata/ext/doctype")&.text unless %w(amendment technical-corrigendum).include? doctype foreword_validate(doc.root) normref_validate(doc.root) symbols_validate(doc.root) sections_presence_validate(doc.root) sections_sequence_validate(doc.root) end section_style(doc.root) subclause_validate(doc.root) @vocab and vocab_terms_titles_validate(doc.root) super end # ISO/IEC DIR 2, 12.4 def foreword_validate(root) f = root.at("//foreword") || return s = f.at("./clause") @log.add("Style", f, "foreword contains subclauses") unless s.nil? end # ISO/IEC DIR 2, 15.4 def normref_validate(root) f = root.at("//references[@normative = 'true']") || return f.at("./references | ./clause") && @log.add("Style", f, "normative references contains subclauses") end ONE_SYMBOLS_WARNING = "Only one Symbols and Abbreviated " \ "Terms section in the standard".freeze NON_DL_SYMBOLS_WARNING = "Symbols and Abbreviated Terms can " \ "only contain a definition list".freeze def symbols_validate(root) f = root.xpath("//definitions") f.empty? && return (f.size == 1 || @vocab) or @log.add("Style", f.first, ONE_SYMBOLS_WARNING) f.first.elements.reject { |e| %w(title dl).include? e.name }.empty? or @log.add("Style", f.first, NON_DL_SYMBOLS_WARNING) @vocab and f.each do |f1| f1.at("./ancestor::annex") or @log.add("Style", f1, "In vocabulary documents, Symbols and " \ "Abbreviated Terms are only permitted in annexes") end end def seqcheck(names, msg, accepted) n = names.shift return [] if n.nil? test = accepted.map { |a| n.at(a) } if test.all?(&:nil?) @log.add("Style", nil, msg) end names end def sections_presence_validate(root) root.at("//sections/clause[@type = 'scope']") or @log.add("Style", nil, "Scope clause missing") root.at("//references[@normative = 'true']") or @log.add("Style", nil, "Normative references missing") root.at("//terms") or @log.add("Style", nil, "Terms & definitions missing") end # spec of permissible section sequence # we skip normative references, it goes to end of list SEQ = [ { msg: "Initial section must be (content) Foreword", val: ["./self::foreword"] }, { msg: "Prefatory material must be followed by (clause) Scope", val: ["./self::introduction", "./self::clause[@type = 'scope']"] }, { msg: "Prefatory material must be followed by (clause) Scope", val: ["./self::clause[@type = 'scope']"] }, { msg: "Normative References must be followed by " \ "Terms and Definitions", val: ["./self::terms | .//terms"] }, ].freeze SECTIONS_XPATH = "//foreword | //introduction | //sections/terms | .//annex | " \ "//sections/definitions | //sections/clause | " \ "//references[not(parent::clause)] | " \ "//clause[descendant::references][not(parent::clause)]".freeze def sections_sequence_validate(root) names, n = sections_sequence_validate_start(root) if @vocab names, n = sections_sequence_validate_body_vocab(names, n) else names, n = sections_sequence_validate_body(names, n) end sections_sequence_validate_end(names, n) end def sections_sequence_validate_start(root) names = root.xpath(SECTIONS_XPATH) names = seqcheck(names, SEQ[0][:msg], SEQ[0][:val]) n = names[0] names = seqcheck(names, SEQ[1][:msg], SEQ[1][:val]) n&.at("./self::introduction") and names = seqcheck(names, SEQ[2][:msg], SEQ[2][:val]) names = seqcheck(names, SEQ[3][:msg], SEQ[3][:val]) n = names.shift n = names.shift if n&.at("./self::definitions") [names, n] end def sections_sequence_validate_body(names, elem) if elem.nil? || elem.name != "clause" @log.add("Style", elem, "Document must contain at least one clause") end elem&.at("./self::clause") || @log.add("Style", elem, "Document must contain clause after " \ "Terms and Definitions") elem&.at("./self::clause[@type = 'scope']") && @log.add("Style", elem, "Scope must occur before Terms and Definitions") elem = names.shift while elem&.name == "clause" elem&.at("./self::clause[@type = 'scope']") @log.add("Style", elem, "Scope must occur before Terms and Definitions") elem = names.shift end %w(annex references).include? elem&.name or @log.add("Style", elem, "Only annexes and references can follow clauses") [names, elem] end def sections_sequence_validate_body_vocab(names, elem) while elem && %w(clause terms).include?(elem.name) elem = names.shift end %w(annex references).include? elem&.name or @log.add("Style", elem, "Only annexes and references can follow terms and clauses") [names, elem] end def sections_sequence_validate_end(names, elem) while elem&.name == "annex" elem = names.shift if elem.nil? @log.add("Style", nil, "Document must include (references) " \ "Normative References") end end elem&.at("./self::references[@normative = 'true']") || @log.add("Style", nil, "Document must include (references) " \ "Normative References") elem = names&.shift elem&.at("./self::references[@normative = 'false']") || @log.add("Style", elem, "Final section must be (references) Bibliography") names.empty? || @log.add("Style", elem, "There are sections after the final Bibliography") end NORM_ISO_WARN = "non-ISO/IEC reference not expected as normative".freeze SCOPE_WARN = "Scope contains subclauses: should be succinct".freeze def section_style(root) foreword_style(root.at("//foreword")) introduction_style(root.at("//introduction")) scope_style(root.at("//clause[@type = 'scope']")) scope = root.at("//clause[@type = 'scope']/clause") # ISO/IEC DIR 2, 14.4 scope.nil? || style_warning(scope, SCOPE_WARN, nil) tech_report_style(root) end def tech_report_style(root) root.at("//bibdata/ext/doctype")&.text == "technical-report" or return root.xpath("//sections/clause[not(@type = 'scope')] | //annex") .each do |s| r = requirement_check(extract_text(s)) and style_warning(s, "Technical Report clause may contain requirement", r) end end NORM_BIBITEMS = "//references[@normative = 'true']/bibitem".freeze ISO_PUBLISHER_XPATH = <<~XPATH.freeze ./contributor[role/@type = 'publisher']/organization[abbreviation = 'ISO' or abbreviation = 'IEC' or name = 'International Organization for Standardization' or name = 'International Electrotechnical Commission'] XPATH # ISO/IEC DIR 2, 10.2 def norm_bibitem_style(root) root.xpath(NORM_BIBITEMS).each do |b| if b.at(ISO_PUBLISHER_XPATH).nil? @log.add("Style", b, "#{NORM_ISO_WARN}: #{b.text}") end end end def subclause_validate(root) root.xpath("//clause/clause/clause/clause/clause/clause/clause/clause") .each do |c| style_warning(c, "Exceeds the maximum clause depth of 7", nil) end end # ISO/IEC DIR 2, 22.3.2 def onlychild_clause_validate(root) root.xpath(Standoc::Utils::SUBCLAUSE_XPATH).each do |c| next unless c.xpath("../clause").size == 1 title = c.at("./title") location = c["id"] || "#{c.text[0..60]}..." location += ":#{title.text}" if c["id"] && !title.nil? @log.add("Style", nil, "#{location}: subclause is only child") end end # https://www.iso.org/ISO-house-style.html#iso-hs-s-formatting-r-vocabulary def vocab_terms_titles_validate(root) terms = root.xpath("//sections/terms | //sections/clause[.//terms]") if terms.size == 1 ((t = terms.first.at("./title")) && (t&.text == @i18n.termsdef)) or @log.add("Style", terms.first, "Single terms clause in vocabulary document " \ "should have normal Terms and definitions heading") elsif terms.size > 1 terms.each do |x| ((t = x.at("./title")) && /^#{@i18n.termsrelated}/.match?(t&.text)) or @log.add("Style", x, "Multiple terms clauses in vocabulary document " \ "should have 'Terms related to' heading") end end end end end end