class Asciidoctor::ISO::Converter

def add_amd_parts(dn, node)

def add_amd_parts(dn, node)
  a = node.attr("amendment-number")
  c = node.attr("corrigendum-number")
  case node.attr("doctype")
  when "amendment"
    "#{dn}/Amd #{node.attr('amendment-number')}"
  when "technical-corrigendum"
    "#{dn}/Cor.#{node.attr('corrigendum-number')}"
  end
end

def add_id_parts(dn, part, subpart)

def add_id_parts(dn, part, subpart)
  dn += "-#{part}" if part
  dn += "-#{subpart}" if subpart
  dn
end

def appendix_parse(attrs, xml, node)

def appendix_parse(attrs, xml, node)
  attrs["inline-header".to_sym] = node.option? "inline-header"
  set_obligation(attrs, node)
  xml.appendix **attr_code(attrs) do |xml_section|
    xml_section.title { |name| name << node.title }
    xml_section << node.content
  end
end

def asset_style(root)

def asset_style(root)
  root.xpath("//example | //termexample").each { |e| example_style(e) }
  root.xpath("//definition").each { |e| definition_style(e) }
  root.xpath("//note").each { |e| note_style(e) }
  root.xpath("//fn").each { |e| footnote_style(e) }
  root.xpath(ASSETS_TO_STYLE).each { |e| style(e, extract_text(e)) }
  norm_bibitem_style(root)
  super
end

def bibdata_validate(doc)

def bibdata_validate(doc)
  doctype_validate(doc)
  script_validate(doc)
  stage_validate(doc)
  substage_validate(doc)
  iteration_validate(doc)
end

def bibitem_validate(xmldoc)

def bibitem_validate(xmldoc)
  xmldoc.xpath("//bibitem[date/on = '–']").each do |b|
    b.at("./note[@type = 'ISO DATE']") or
      @log.add("Style", b,
               "Reference #{b&.at("./@id")&.text} does not have an "\
               "associated footnote indicating unpublished status")
  end
end

def cited_term_style(xmldoc)

ISO/IEC DIR 2, 16.5.10
def cited_term_style(xmldoc)
  xmldoc.xpath("//term//xref").each do |x|
    next unless xmldoc.at("//term[@id = '#{x['target']}']")
    x&.previous&.text == " (" and x&.previous&.previous&.name == "em" or
      style_warning(x, "term citation not preceded with italicised term",
                    x.parent.text)
  end
end

def clause_parse(attrs, xml, node)

def clause_parse(attrs, xml, node)
  node.option? "appendix" and return appendix_parse(attrs, xml, node)
  super
end

def content_validate(doc)

def content_validate(doc)
  super
  title_validate(doc.root)
  isosubgroup_validate(doc.root)
  onlychild_clause_validate(doc.root)
  termdef_style(doc.root)
  iev_validate(doc.root)
  see_xrefs_validate(doc.root)
  see_erefs_validate(doc.root)
  locality_erefs_validate(doc.root)
  bibdata_validate(doc.root)
  bibitem_validate(doc.root)
end

def definition_style(node)

ISO/IEC DIR 2, 16.5.6
def definition_style(node)
  return if @novalid
  r = requirement_check(extract_text(node))
  style_warning(node, "Definition may contain requirement", r) if r
end

def doc_converter(node)

def doc_converter(node)
  IsoDoc::Iso::WordConvert.new(doc_extract_attributes(node))
end

def docidentifier_cleanup(xmldoc)

ISO as a prefix goes first
def docidentifier_cleanup(xmldoc)
  prefix = get_id_prefix(xmldoc)
  id = xmldoc.at("//bibdata/docidentifier[@type = 'ISO']") or return
  id.content = id_prefix(prefix, id)
  id = xmldoc.at("//bibdata/ext/structuredidentifier/project-number") and
    id.content = id_prefix(prefix, id)
  id = xmldoc.at("//bibdata/docidentifier[@type = 'iso-with-lang']") and
    id.content = id_prefix(prefix, id)
  id = xmldoc.at("//bibdata/docidentifier[@type = 'iso-reference']") and
    id.content = id_prefix(prefix, id)
end

def doctype_validate(xmldoc)

def doctype_validate(xmldoc)
  doctype = xmldoc&.at("//bibdata/ext/doctype")&.text
  %w(international-standard technical-specification technical-report 
  publicly-available-specification international-workshop-agreement 
  guide amendment technical-corrigendum).include? doctype or
  @log.add("Document Attributes", nil,
           "#{doctype} is not a recognised document type")
end

def example_style(node)

ISO/IEC DIR 2, 25.5
ISO/IEC DIR 2, 16.5.7
def example_style(node)
  return if @novalid
  style_no_guidance(node, extract_text(node), "Example")
  style(node, extract_text(node))
end

def external_constraint(text)

def external_constraint(text)
  text.split(/\.\s+/).each do |t|
    return t if /\b(must)\b/xi.match t
  end
  nil
end

def extract_text(node)

def extract_text(node)
  return "" if node.nil?
  node1 = Nokogiri::XML.fragment(node.to_s)
  node1.xpath("//link | //locality | //localityStack").each(&:remove)
  ret = ""
  node1.traverse { |x| ret += x.text if x.text? }
  ret
end

def footnote_style(node)

ISO/IEC DIR 2, 26.5
def footnote_style(node)
  return if @novalid
  style_no_guidance(node, extract_text(node), "Footnote")
  style(node, extract_text(node))
end

def foreword_style(node)

ISO/IEC DIR 2, 12.2
def foreword_style(node)
  return if @novalid
  style_no_guidance(node, extract_text(node), "Foreword")
end

def foreword_validate(root)

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

def format_ref(ref, type, isopub)

def format_ref(ref, type, isopub)
  ref = ref.sub(/ \(All Parts\)/i, "")
  super
end

def get_id_prefix(xmldoc)

def get_id_prefix(xmldoc)
  prefix = []
  xmldoc.xpath("//bibdata/contributor[role/@type = 'publisher']"\
               "/organization").each do |x|
    x1 = x.at("abbreviation")&.text || x.at("name")&.text
    x1 == "ISO" and prefix.unshift("ISO") or prefix << x1
  end
  prefix
end

def get_stage(node)

def get_stage(node)
  stage = node.attr("status") || node.attr("docstage") || "60"
end

def get_substage(node)

def get_substage(node)
  stage = get_stage(node)
  node.attr("docsubstage") || ( stage == "60" ? "60" : "00" )
end

def get_typeabbr(node)

def get_typeabbr(node)
  case node.attr("doctype")
  when "technical-report" then "TR "
  when "technical-specification" then "TS "
  else
    nil
  end
end

def html_converter(node)

def html_converter(node)
  IsoDoc::Iso::HtmlConvert.new(html_extract_attributes(node))
end

def html_converter_alt(node)

def html_converter_alt(node)
  IsoDoc::Iso::HtmlConvert.new(html_extract_attributes(node).
                               merge(alt: true))
end

def id_add_year(dn, node)

def id_add_year(dn, node)
  year = node.attr("copyright-year")
  @amd and year ||= node.attr("updated-date")&.sub(/-.*$/, "")
  dn += ":#{year}" if year
  dn
end

def id_langsuffix(dn, node)

def id_langsuffix(dn, node)
  lang = node.attr("language") || "en"
  suffix = case lang
           when "en" then "(E)"
           when "fr" then "(F)"
           else
             "(X)"
           end
  "#{dn}#{suffix}"
end

def id_prefix(prefix, id)

def id_prefix(prefix, id)
  return id.text if @amd # we're just inheriting the prefixes from parent doc
  prefix.join("/") + ( id.text.match(%{^/}) ? "" :  " " ) + id.text
end

def id_stage_abbr(stage, substage, node)

def id_stage_abbr(stage, substage, node)
  ret = IsoDoc::Iso::Metadata.new("en", "Latn", @i18n).
    status_abbrev(stage_abbr(stage, substage, node.attr("doctype")),
                  substage, node.attr("iteration"),
                  node.attr("draft"), node.attr("doctype"))
  if %w(amendment technical-corrigendum amendment
    technical-corrigendum).include?(node.attr("doctype"))
    ret = ret + " " unless %w(40 50).include?(stage)
  end
  ret
end

def id_stage_prefix(dn, node, force_year)

def id_stage_prefix(dn, node, force_year)
  stage = get_stage(node)
  typeabbr = get_typeabbr(node)
  if stage && (stage.to_i < 60)
    dn = unpub_stage_prefix(dn, stage, typeabbr, node)
  elsif typeabbr && !@amd then dn = "/#{typeabbr}#{dn}"
  end
  (force_year || !(stage && (stage.to_i < 60))) and
    dn = id_add_year(dn, node)
  dn
end

def init(node)

def init(node)
  super
  @amd = %w(amendment technical-corrigendum).include? node.attr("doctype")
end

def introduction_style(node)

ISO/IEC DIR 2, 13.2
def introduction_style(node)
  return if @novalid
  r = requirement_check(extract_text(node))
  style_warning(node, "Introduction may contain requirement", r) if r
end

def iso_id(node, xml)

def iso_id(node, xml)
  return unless !@amd && node.attr("docnumber") ||
    @amd && node.attr("updates")
  dn = iso_id1(node)
  dn1 = id_stage_prefix(dn, node, false)
  dn2 = id_stage_prefix(dn, node, true)
  xml.docidentifier dn1, **attr_code(type: "ISO")
  xml.docidentifier id_langsuffix(dn1, node),
    **attr_code(type: "iso-with-lang")
  xml.docidentifier id_langsuffix(dn2, node),
    **attr_code(type: "iso-reference")
end

def iso_id1(node)

def iso_id1(node)
  if @amd
    dn = node.attr("updates")
    return add_amd_parts(dn, node)
  else
    part, subpart = node&.attr("partnumber")&.split(/-/)
    return add_id_parts(node.attr("docnumber"), part, subpart)
  end
end

def isosubgroup_validate(root)

def isosubgroup_validate(root)
  root.xpath("//technical-committee/@type").each do |t|
    unless %w{TC PC JTC JPC}.include? t.text
      @log.add("Document Attributes", nil,
               "invalid technical committee type #{t}")
    end
  end
  root.xpath("//subcommittee/@type").each do |t|
    unless %w{SC JSC}.include? t.text
      @log.add("Document Attributes", nil,
               "invalid subcommittee type #{t}")
    end
  end
end

def iteration_validate(xmldoc)

def iteration_validate(xmldoc)
  iteration = xmldoc&.at("//bibdata/status/iteration")&.text or return
  /^\d+/.match(iteration) or
    @log.add("Document Attributes", nil,
             "#{iteration} is not a recognised iteration")
end

def locality_erefs_validate(root)

ISO/IEC DIR 2, 10.4
def locality_erefs_validate(root)
  root.xpath("//eref[descendant::locality]").each do |t|
    if /^(ISO|IEC)/.match t["citeas"]
      unless /:[ ]?(\d+{4}|–)$/.match t["citeas"]
        @log.add("Style", t,
                 "undated reference #{t['citeas']} should not contain "\
                 "specific elements")
      end
    end
  end
end

def metadata_author(node, xml)

def metadata_author(node, xml)
  publishers = node.attr("publisher") || "ISO"
  csv_split(publishers).each do |p|
    xml.contributor do |c|
      c.role **{ type: "author" }
      c.organization { |a| organization(a, p) }
    end
  end
end

def metadata_committee(node, xml)

def metadata_committee(node, xml)
  xml.editorialgroup do |a|
    committee_component("technical-committee", node, a)
    committee_component("subcommittee", node, a)
    committee_component("workgroup", node, a)
    node.attr("secretariat") && a.secretariat(node.attr("secretariat"))
  end
end

def metadata_copyright(node, xml)

def metadata_copyright(node, xml)
  publishers = node.attr("copyright-holder") || node.attr("publisher") || "ISO"
  csv_split(publishers).each do |p|
    xml.copyright do |c|
      c.from (node.attr("copyright-year") || Date.today.year)
      c.owner do |owner|
        owner.organization { |o| organization(o, p) }
      end
    end
  end
end

def metadata_ext(node, xml)

def metadata_ext(node, xml)
  super
  structured_id(node, xml)
  xml.stagename stage_name(get_stage(node), get_substage(node),
                           node.attr("doctype"), node.attr("iteration"))
  @amd && a = node.attr("updates-document-type") and
    xml.updates_document_type a
end

def metadata_id(node, xml)

def metadata_id(node, xml)
  iso_id(node, xml)
  node&.attr("tc-docnumber")&.split(/,\s*/)&.each do |n|
    xml.docidentifier(n, **attr_code(type: "iso-tc"))
  end
  xml.docnumber node&.attr("docnumber")
end

def metadata_publisher(node, xml)

def metadata_publisher(node, xml)
  publishers = node.attr("publisher") || "ISO"
  csv_split(publishers).each do |p|
    xml.contributor do |c|
      c.role **{ type: "publisher" }
      c.organization { |a| organization(a, p) }
    end
  end
end

def metadata_status(node, xml)

def metadata_status(node, xml)
  stage = get_stage(node)
  substage = get_substage(node)
  xml.status do |s|
    s.stage stage, **attr_code(abbreviation: stage_abbr(stage, substage, node.attr("doctype")))
    s.substage substage
    node.attr("iteration") && (s.iteration node.attr("iteration"))
  end
end

def norm_bibitem_style(root)

ISO/IEC DIR 2, 10.2
def norm_bibitem_style(root)
  root.xpath(NORM_BIBITEMS).each do |b|
    if b.at(Standoc::Converter::ISO_PUBLISHER_XPATH).nil?
      @log.add("Style", b, "#{NORM_ISO_WARN}: #{b.text}")
    end
  end
end

def normref_validate(root)

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

def note_style(node)

ISO/IEC DIR 2, 24.5
def note_style(node)
  return if @novalid
  style_no_guidance(node, extract_text(node), "Note")
  style(node, extract_text(node))
end

def onlychild_clause_validate(root)

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

def organization(org, orgname)

def organization(org, orgname)
  if ["ISO",
      "International Organization for Standardization"].include? orgname
    org.name "International Organization for Standardization"
    org.abbreviation "ISO"
  elsif ["IEC",
         "International Electrotechnical Commission"].include? orgname
    org.name "International Electrotechnical Commission"
    org.abbreviation "IEC"
  else
    org.name orgname
  end
end

def other_footnote_renumber(xmldoc)

def other_footnote_renumber(xmldoc)
  seen = {}
  i = 0
  xmldoc.xpath(PRE_NORMREF_FOOTNOTES).each do |fn|
    i, seen = other_footnote_renumber1(fn, i, seen)
  end
  xmldoc.xpath(NORMREF_FOOTNOTES).each do |fn|
    i, seen = other_footnote_renumber1(fn, i, seen)
  end
  xmldoc.xpath(POST_NORMREF_FOOTNOTES).each do |fn|
    i, seen = other_footnote_renumber1(fn, i, seen)
  end
end

def outputs(node, ret)

def outputs(node, ret)
    File.open(@filename + ".xml", "w:UTF-8") { |f| f.write(ret) }
    presentation_xml_converter(node).convert(@filename + ".xml")
    html_converter_alt(node).convert(@filename + ".presentation.xml", 
                                     nil, false, "#{@filename}_alt.html")
    html_converter(node).convert(@filename + ".presentation.xml", 
                                 nil, false, "#{@filename}.html")
    doc_converter(node).convert(@filename + ".presentation.xml", 
                                nil, false, "#{@filename}.doc")
    pdf_converter(node)&.convert(@filename + ".presentation.xml", 
                                 nil, false, "#{@filename}.pdf")
    #sts_converter(node)&.convert(@filename + ".xml")
end

def patent_notice_parse(xml, node)

def patent_notice_parse(xml, node)
  # xml.patent_notice do |xml_section|
  #  xml_section << node.content
  # end
  xml << node.content
end

def pdf_converter(node)

def pdf_converter(node)
  return nil if node.attr("no-pdf")
  IsoDoc::Iso::PdfConvert.new(doc_extract_attributes(node))
end

def permission_check(text)

def permission_check(text)
  text.split(/\.\s+/).each do |t|
    return t if PERMISSION_RE.match t
  end
  nil
end

def possibility(text)

def possibility(text)
  text.split(/\.\s+/).each { |t| return t if POSSIBILITY_RE.match t }
  nil
end

def presentation_xml_converter(node)

def presentation_xml_converter(node)
  IsoDoc::Iso::PresentationXMLConvert.new(html_extract_attributes(node))
end

def pub_class(bib)

def pub_class(bib)
  return 1 if bib.at("#{PUBLISHER}[abbreviation = 'ISO']")
  return 1 if bib.at("#{PUBLISHER}[name = 'International Organization "\
                     "for Standardization']")
  return 2 if bib.at("#{PUBLISHER}[abbreviation = 'IEC']")
  return 2 if bib.at("#{PUBLISHER}[name = 'International "\
                     "Electrotechnical Commission']")
  return 3 if bib.at("./docidentifier[@type][not(#{OTHERIDS})]")
  4
end

def recommendation_check(text)

def recommendation_check(text)
  text.split(/\.\s+/).each do |t|
    return t if RECOMMENDATION_RE.match t
  end
  nil
end

def requirement_check(text)

def requirement_check(text)
  text.split(/\.\s+/).each do |t|
    return t if REQUIREMENT_RE.match t
  end
  nil
end

def scope_parse(attrs, xml, node)

def scope_parse(attrs, xml, node)
  attrs = attrs.merge(type: "scope") unless @amd 
  clause_parse(attrs, xml, node)
end

def scope_style(node)

ISO/IEC DIR 2, 14.2
def scope_style(node)
  return if @novalid
  style_no_guidance(node, extract_text(node), "Scope")
end

def script_validate(xmldoc)

def script_validate(xmldoc)
  script = xmldoc&.at("//bibdata/script")&.text
  script == "Latn" or
    @log.add("Document Attributes", nil,
             "#{script} is not a recognised script")
end

def section_attributes(node)

def section_attributes(node)
  super.merge(
    change: @amd ? node.attr("change") : nil,
    locality: @amd ? node.attr("locality") : nil,
  )
end

def section_style(root)

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)
end

def section_validate(doc)

def section_validate(doc)
  foreword_validate(doc.root)
  normref_validate(doc.root)
  symbols_validate(doc.root)
  sections_presence_validate(doc.root)
  sections_sequence_validate(doc.root)
  section_style(doc.root)
  subclause_validate(doc.root)
  super
end

def sections_cleanup(x)

def sections_cleanup(x)
  super
  return unless @amd
  x.xpath("//*[@inline-header]").each do |h|
    h.delete('inline-header')
  end
end

def sections_presence_validate(root)

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

def sections_sequence_validate(root)

def sections_sequence_validate(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]) 
  if n&.at("./self::introduction")
    names = seqcheck(names, SEQ[2][:msg], SEQ[2][:val]) 
  end
  names = seqcheck(names, SEQ[3][:msg], SEQ[3][:val]) 
  n = names.shift
  if n&.at("./self::definitions")
    n = names.shift
  end
  if n.nil? || n.name != "clause"
    @log.add("Style", nil, "Document must contain at least one clause")
  end
  n&.at("./self::clause") ||
    @log.add("Style", nil, "Document must contain clause after "\
         "Terms and Definitions")
  n&.at("./self::clause[@type = 'scope']") &&
    @log.add("Style", nil, "Scope must occur before Terms and Definitions")
  n = names.shift 
  while n&.name == "clause"
    n&.at("./self::clause[@type = 'scope']")
      @log.add("Style", nil, "Scope must occur before Terms and Definitions")
    n = names.shift 
  end
  unless %w(annex references).include? n&.name
    @log.add("Style", nil, "Only annexes and references can follow clauses")
  end
  while n&.name == "annex"
    n = names.shift
    if n.nil?
      @log.add("Style", nil, "Document must include (references) "\
           "Normative References")
    end
  end
  n&.at("./self::references[@normative = 'true']") ||
    @log.add("Style", nil, "Document must include (references) "\
         "Normative References")
  n = names&.shift
  n&.at("./self::references[@normative = 'false']") ||
    @log.add("Style", nil, "Final section must be (references) Bibliography")
  names.empty? ||
    @log.add("Style", nil, "There are sections after the final Bibliography")
end

def sectiontype(node, level = true)

def sectiontype(node, level = true)
  return nil if @amd
  super
end

def see_erefs_validate(root)

ISO/IEC DIR 2, 15.5.3
def see_erefs_validate(root)
  root.xpath("//eref").each do |t|
    preceding = t.at("./preceding-sibling::text()[last()]")
    next unless !preceding.nil? &&
      /\b(see|refer to)\s*$/mi.match(preceding)
    unless target = root.at("//*[@id = '#{t['bibitemid']}']")
      @log.add("Bibliography", t,
               "'#{t} is not pointing to a real reference")
      next
    end
    if target.at("./ancestor::references[@normative = 'true']")
      @log.add("Style", t,
               "'see #{t}' is pointing to a normative reference")
    end
  end
end

def see_xrefs_validate(root)

ISO/IEC DIR 2, 15.5.3
def see_xrefs_validate(root)
  root.xpath("//xref").each do |t|
    # does not deal with preceding text marked up
    preceding = t.at("./preceding-sibling::text()[last()]")
    next unless !preceding.nil? &&
      /\b(see| refer to)\s*$/mi.match(preceding)
    (target = root.at("//*[@id = '#{t['target']}']")) || next
    if target&.at("./ancestor-or-self::*[@obligation = 'normative']")
      @log.add("Style", t,
               "'see #{t['target']}' is pointing to a normative section")
    end
  end
end

def seqcheck(names, msg, accepted)

def seqcheck(names, msg, accepted)
  n = names.shift 
  return [] if n.nil?
  test = accepted.map { |a| n.at(a) }
  if test.all? { |a| a.nil? }
    @log.add("Style", nil, msg)
  end
  names
end

def sort_biblio(bib)

def sort_biblio(bib)
  bib.sort do |a, b|
    sort_biblio_key(a) <=> sort_biblio_key(b)
  end
end

def sort_biblio_key(bib)

then title
then doc id (not DOI &c)
then doc part number if present, numeric sort
else alphanumeric metanorma id (abbreviation)
then docnumber if present, numeric sort
then standard class (docid class other than DOI &c)
sort by: doc class (ISO, IEC, other standard (not DOI &c), other
TODO sort by authors
def sort_biblio_key(bib)
  pubclass = pub_class(bib)
  num = bib&.at("./docnumber")&.text
  id = bib&.at("./docidentifier[not(#{OTHERIDS})]")
  metaid = bib&.at("./docidentifier[@type = 'metanorma']")&.text
  abbrid = metaid unless /^\[\d+\]$/.match(metaid)
  /\d-(?<partid>\d+)/ =~ id&.text
  type = id['type'] if id
  title = bib&.at("./title[@type = 'main']")&.text ||
    bib&.at("./title")&.text || bib&.at("./formattedref")&.text
  "#{pubclass} :: #{type} :: "\
    "#{num.nil? ? abbrid : sprintf("%09d", num.to_i)} :: "\
    "#{partid} :: #{id&.text} :: #{title}"
end

def stage_abbr(stage, substage, doctype)

def stage_abbr(stage, substage, doctype)
  return nil if stage.to_i > 60
  ret = STAGE_ABBRS[stage.to_sym]
  ret = "PRF" if stage == "60" && substage == "00"
  if %w(amendment technical-corrigendum technical-report 
    technical-specification).include?(doctype)
    ret = "NP" if stage == "10"
    ret = "AWI" if stage == "10" && substage == "99"
    ret = "D" if stage == "40" and doctype == "amendment"
    ret = "FD" if stage == "50" and
      %w(amendment technical-corrigendum).include?(doctype)
    ret = "D" if stage == "50" and
      %w(technical-report technical-specification).include?(doctype)
  end
  ret
end

def stage_name(stage, substage, doctype, iteration = nil)

def stage_name(stage, substage, doctype, iteration = nil)
  return "Proof" if stage == "60" && substage == "00"
  ret = STAGE_NAMES[stage.to_sym]
  if iteration && %w(20 30).include?(stage)
    prefix  = iteration.to_i.localize(@lang.to_sym).
      to_rbnf_s("SpelloutRules", "spellout-ordinal")
    ret = "#{prefix.capitalize} #{ret.downcase}"
  end
  ret
end

def stage_validate(xmldoc)

def stage_validate(xmldoc)
  stage = xmldoc&.at("//bibdata/status/stage")&.text
  %w(00 10 20 30 40 50 60 90 95).include? stage or
    @log.add("Document Attributes", nil,
             "#{stage} is not a recognised stage")
end

def structured_id(node, xml)

def structured_id(node, xml)
  return unless node.attr("docnumber")
  part, subpart = node&.attr("partnumber")&.split(/-/)
  xml.structuredidentifier do |i|
    i.project_number node.attr("docnumber"),
      **attr_code(part: part, subpart: subpart,
                  amendment: node.attr("amendment-number"),
                  corrigendum: node.attr("corrigendum-number"),
                  origyr: node.attr("created-date"))
  end
end

def sts_converter(node)

def sts_converter(node)
  return nil if node.attr("no-pdf")
  IsoDoc::Iso::StsConvert.new(html_extract_attributes(node))
end

def style(n, t)

def style(n, t)
  return if @novalid
  style_number(n, t)
  style_percent(n, t)
  style_abbrev(n, t)
  style_units(n, t)
end

def style_abbrev(n, t)

ISO/IEC DIR 2, 9.3
ISO/IEC DIR 2, 8.4
def style_abbrev(n, t)
  style_regex(/(^|\s)(?!e\.g\.|i\.e\.)
              (?<num>[a-z]{1,2}\.([a-z]{1,2}|\.))\b/ix,
                "no dots in abbreviations", n, t)
  style_regex(/\b(?<num>ppm)\b/i,
              "language-specific abbreviation", n, t)
end

def style_no_guidance(node, text, docpart)

def style_no_guidance(node, text, docpart)
  r = requirement_check(text)
  style_warning(node, "#{docpart} may contain requirement", r) if r
  r = permission_check(text)
  style_warning(node, "#{docpart} may contain permission", r) if r
  r = recommendation_check(text)
  style_warning(node, "#{docpart} may contain recommendation", r) if r
end

def style_non_std_units(n, t)

ISO/IEC DIR 2, 9.3
def style_non_std_units(n, t)
  NONSTD_UNITS.each do |k, v|
    style_regex(/\b(?<num>[0-9][0-9,]*\s+#{k})\b/,
                "non-standard unit (should be #{v})", n, t)
  end
end

def style_number(n, t)

ISO/IEC DIR 2, Table B.1
ISO/IEC DIR 2, 9.1
def style_number(n, t)
  style_two_regex_not_prev(
    n, t, /^(?<num>-?[0-9]{4,}[,0-9]*)$/,
    %r{\b(ISO|IEC|IEEE/|(in|January|February|March|April|May|June|August|September|October|November|December)\b)$},
    "number not broken up in threes")
  style_regex(/\b(?<num>[0-9]+\.[0-9]+)/i,
              "possible decimal point", n, t)
  style_regex(/\b(?<num>billion[s]?)\b/i,
              "ambiguous number", n, t)
end

def style_percent(n, t)

ISO/IEC DIR 2, 9.2.1
def style_percent(n, t)
  style_regex(/\b(?<num>[0-9.,]+%)/,
              "no space before percent sign", n, t)
  style_regex(/\b(?<num>[0-9.,]+ \u00b1 [0-9,.]+ %)/,
              "unbracketed tolerance before percent sign", n, t)
end

def style_regex(re, warning, n, text)

def style_regex(re, warning, n, text)
  (m = re.match(text)) && style_warning(n, warning, m[:num])
end

def style_two_regex_not_prev(n, text, re, re_prev, warning)

and a negative match on its preceding token
style check with a regex on a token
def style_two_regex_not_prev(n, text, re, re_prev, warning)
  return if text.nil?
  arr = Tokenizer::WhitespaceTokenizer.new.tokenize(text)
  arr.each_index do |i|
    m = re.match arr[i]
    m_prev = i.zero? ? nil : re_prev.match(arr[i - 1])
    if !m.nil? && m_prev.nil?
      style_warning(n, warning, m[:num])
    end
  end
end

def style_units(n, t)

ISO/IEC DIR 2, 9.3
def style_units(n, t)
  style_regex(/\b(?<num>[0-9][0-9,]*\s+[\u00b0\u2032\u2033])/,
              "space between number and degrees/minutes/seconds", n, t)
  style_regex(/\b(?<num>[0-9][0-9,]*#{SI_UNIT})\b/,
              "no space between number and SI unit", n, t)
  style_non_std_units(n, t)
end

def style_warning(node, msg, text = nil)

def style_warning(node, msg, text = nil)
  return if @novalid
  w = msg
  w += ": #{text}" if text
  @log.add("Style", node, w)
end

def subclause_validate(root)

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

def substage_validate(xmldoc)

def substage_validate(xmldoc)
  substage = xmldoc&.at("//bibdata/status/substage")&.text or return
  %w(00 20 60 90 92 93 98 99).include? substage or
    @log.add("Document Attributes", nil,
             "#{substage} is not a recognised substage")
end

def symbols_validate(root)

def symbols_validate(root)
  f = root.xpath("//definitions")
  f.empty? && return
  (f.size == 1) || @log.add("Style", f.first, ONE_SYMBOLS_WARNING)
  f.first.elements.each do |e|
    unless e.name == "dl"
      @log.add("Style", f.first, NON_DL_SYMBOLS_WARNING)
      return
    end
  end
end

def termdef_cleanup(xmldoc)

def termdef_cleanup(xmldoc)
  Asciidoctor::ISO::TermLookupCleanup.new(xmldoc, @log).call
  super
end

def termdef_style(xmldoc)

ISO/IEC DIR 2, 16.5.6
def termdef_style(xmldoc)
  xmldoc.xpath("//term").each do |t|
    para = t.at("./definition") || return
    term = t.at("./preferred").text
    termdef_warn(para.text, /^(the|a)\b/i, t, term,
                 "term definition starts with article")
    termdef_warn(para.text, /\.$/i, t, term,
                 "term definition ends with period")
  end
  cited_term_style(xmldoc)
end

def termdef_warn(text, re, t, term, msg)

def termdef_warn(text, re, t, term, msg)
  re.match(text) && @log.add("Style", t, "#{term}: #{msg}")
end

def title(node, xml)

def title(node, xml)
  ["en", "fr"].each do |lang|
    at = { language: lang, format: "text/plain" }
    title_full(node, xml, lang, at)
    title_intro(node, xml, lang, at)
    title_main(node, xml, lang, at)
    title_part(node, xml, lang, at)
    title_amd(node, xml, lang, at) if @amd
  end
end

def title_all_siblings(xpath, label)

ISO/IEC DIR 2, 22.2
def title_all_siblings(xpath, label)
  notitle = false
  withtitle = false
  xpath.each do |s|
    title_all_siblings(s.xpath("./clause | ./terms | ./references"),
                       s&.at("./title")&.text || s["id"])
    subtitle = s.at("./title")
    notitle = notitle || (!subtitle || subtitle.text.empty?)
    withtitle = withtitle || (subtitle && !subtitle.text.empty?)
  end
  notitle && withtitle &&
    @log.add("Style", nil, "#{label}: all subclauses must have a title, or none")
end

def title_amd(node, t, lang, at)

def title_amd(node, t, lang, at)
  return unless node.attr("title-amendment-#{lang}")
  t.title(**attr_code(at.merge(type: "title-amd"))) do |t1|
    t1 << Asciidoctor::Standoc::Utils::asciidoc_sub(node.attr("title-amendment-#{lang}"))
  end
end

def title_first_level_validate(root)

ISO/IEC DIR 2, 22.2
def title_first_level_validate(root)
  root.xpath(SECTIONS_XPATH).each do |s|
    title = s&.at("./title")&.text || s.name
    s.xpath("./clause | ./terms | ./references").each do |ss|
      subtitle = ss.at("./title")
      !subtitle.nil? && !subtitle&.text&.empty? ||
        @log.add("Style", ss, "#{title}: each first-level subclause must have a title")
    end
  end
end

def title_full(node, t, lang, at)

def title_full(node, t, lang, at)
  title = node.attr("title-main-#{lang}")
  intro = node.attr("title-intro-#{lang}")
  part = node.attr("title-part-#{lang}")
  amd = node.attr("title-amendment-#{lang}")
  title = "#{intro} -- #{title}" if intro
  title = "#{title} -- #{part}" if part
  title = "#{title} -- #{amd}" if amd && @amd
  t.title **attr_code(at.merge(type: "main")) do |t1|
    t1 << Asciidoctor::Standoc::Utils::asciidoc_sub(title)
  end
end

def title_intro(node, t, lang, at)

def title_intro(node, t, lang, at)
  return unless node.attr("title-intro-#{lang}")
  t.title(**attr_code(at.merge(type: "title-intro"))) do |t1|
    t1 << Asciidoctor::Standoc::Utils::asciidoc_sub(node.attr("title-intro-#{lang}"))
  end
end

def title_intro_validate(root)

def title_intro_validate(root)
  title_intro_en = root.at("//title[@type='title-intro' and @language='en']")
  title_intro_fr = root.at("//title[@type='title-intro' and @language='fr']")
  if title_intro_en.nil? && !title_intro_fr.nil?
    @log.add("Style", title_intro_fr, "No English Title Intro!")
  end
  if !title_intro_en.nil? && title_intro_fr.nil?
    @log.add("Style", title_intro_en, "No French Title Intro!")
  end
end

def title_main(node, t, lang, at)

def title_main(node, t, lang, at)
  t.title **attr_code(at.merge(type: "title-main")) do |t1|
    t1 << Asciidoctor::Standoc::Utils::asciidoc_sub(node.attr("title-main-#{lang}"))
  end
end

def title_main_validate(root)

def title_main_validate(root)
  title_main_en = root.at("//title[@type='title-main' and @language='en']")
  title_main_fr = root.at("//title[@type='title-main' and @language='fr']")
  if title_main_en.nil? && !title_main_fr.nil?
    @log.add("Style", title_main_fr, "No English Title!")
  end
  if !title_main_en.nil? && title_main_fr.nil?
    @log.add("Style", title_main_en, "No French Title!")
  end
end

def title_names_type_validate(root)

ISO/IEC DIR 2, 11.5.2
def title_names_type_validate(root)
  doctypes = /International\sStandard | Technical\sSpecification |
  Publicly\sAvailable\sSpecification | Technical\sReport | Guide /xi
  title_main_en = root.at("//title[@type='title-main' and @language='en']")
  if !title_main_en.nil? && doctypes.match(title_main_en.text)
    @log.add("Style", title_main_en, "Main Title may name document type")
  end
  title_intro_en = root.at("//title[@type='title-intro' and @language='en']")
  if !title_intro_en.nil? && doctypes.match(title_intro_en.text)
    @log.add("Style", title_intro_en, "Title Intro may name document type")
  end
end

def title_part(node, t, lang, at)

def title_part(node, t, lang, at)
  return unless node.attr("title-part-#{lang}")
  t.title(**attr_code(at.merge(type: "title-part"))) do |t1|
    t1 << Asciidoctor::Standoc::Utils::asciidoc_sub(node.attr("title-part-#{lang}"))
  end
end

def title_part_validate(root)

def title_part_validate(root)
  title_part_en = root.at("//title[@type='title-part' and @language='en']")
  title_part_fr = root.at("//title[@type='title-part' and @language='fr']")
  (title_part_en.nil? && !title_part_fr.nil?) &&
    @log.add("Style", title_part_fr, "No English Title Part!")
  (!title_part_en.nil? && title_part_fr.nil?) &&
    @log.add("Style", title_part_en, "No French Title Part!")
end

def title_subpart_validate(root)

ISO/IEC DIR 2, 11.4
def title_subpart_validate(root)
  docid = root.at("//bibdata/docidentifier[@type = 'ISO']")
  subpart = /-\d+-\d+/.match docid
  iec = root.at("//bibdata/contributor[role/@type = 'publisher']/"\
                "organization[abbreviation = 'IEC' or "\
                "name = 'International Electrotechnical Commission']")
  @log.add("Style", docid, "Subpart defined on non-IEC document!") if subpart && !iec
end

def title_validate(root)

def title_validate(root)
  title_intro_validate(root)
  title_main_validate(root)
  title_part_validate(root)
  title_subpart_validate(root)
  title_names_type_validate(root)
  title_first_level_validate(root)
  title_all_siblings(root.xpath(SECTIONS_XPATH), "(top level)")
end

def unpub_stage_prefix(dn, stage, typeabbr, node)

def unpub_stage_prefix(dn, stage, typeabbr, node)
  abbr = id_stage_abbr(stage, get_substage(node), node)
  %w(40 50).include?(stage) && i = node.attr("iteration") and
    itersuffix = ".#{i}"
  return dn if abbr.nil? || abbr.empty? # prefixes added in cleanup
  return "/#{abbr}#{typeabbr} #{dn}#{itersuffix}" unless @amd
  a = dn.split(%r{/})
  a[-1] = "#{abbr}#{a[-1]}#{itersuffix}"
  a.join("/")
end

def validate(doc)

def validate(doc)
  content_validate(doc)
  doctype = doc&.at("//bibdata/ext/doctype")&.text
  schema = case doctype
           when "amendment", "technical-corrigendum" # @amd
             "isostandard-amd.rng"
           else
             "isostandard.rng"
           end
  schema_validate(formattedstr_strip(doc.dup),
                  File.join(File.dirname(__FILE__), schema))
end