class Kramdown::Converter::Rfc2629
Converts a Kramdown::Document to HTML.
def self.bcp_std_ref(t, n)
def self.bcp_std_ref(t, n) warn "*** #{t} anchors not supported in v2 format" unless $options.v3 [name = "reference.#{t}.#{"%04d" % n.to_i}.xml", "#{XML_RESOURCE_ORG_PREFIX}/bibxml-rfcsubseries/#{name}"] # FOR NOW end
def self.hex_to_lin(h)
def self.hex_to_lin(h) h.to_i(16)**2.22 # approximating sRGB gamma end
def self.process_markdown(v)
def self.process_markdown(v) process_markdown1(v)[3..-6] # skip <t>...</t>\n end
def self.process_markdown1(v) # Uuh. Heavy coupling.
def self.process_markdown1(v) # Uuh. Heavy coupling. doc = ::Kramdown::Document.new(v, $global_markdown_options) $stderr.puts doc.warnings.to_yaml unless doc.warnings.empty? doc.to_rfc2629 end
def self.process_markdown_to_rexml(v)
def self.process_markdown_to_rexml(v) s = process_markdown1(v) REXML::Document.new(s) end
def capture_croak(t, err)
def capture_croak(t, err) if err != '' err.lines do |l| warn "*** [#{t}:] #{l.chomp}" end end end
def clean_pcdata(parts) # hack, will become unnecessary with XML2RFCv3
def clean_pcdata(parts) # hack, will become unnecessary with XML2RFCv3 clean = '' irefs = '' # warn "clean_parts: #{parts.inspect}" parts.each do |p| md = p.match(%r{([^<]*)(.*)}) clean << md[1] irefs << md[2] # breaks for spanx... don't emphasize in headings! end [clean, irefs] end
def clean_pcdatav3(parts) # hack, will become unnecessary with v3 tables
def clean_pcdatav3(parts) # hack, will become unnecessary with v3 tables clean = '' parts.each do |p| next if p.empty? d = REXML::Document.new("<foo>#{p}</foo>") t = REXML::XPath.each(d.root, "//text()").to_a.join if t != p warn "** simplified markup #{p.inspect} into #{t.inspect} in table heading" end clean << t end clean end
def convert(el)
def convert(el) opts = el.options[:options] # warn "** tree opts #{opts.inspect}" if nested_ol_types = @options[:nested_ol_types] opts[:nested_ol_types] ||= nested_ol_types # warn "** tree opts out #{opts.inspect}" end indent = -INDENTATION if el.children[-1].type == :raw raw = convert1(el.children.pop, indent, opts) end "#{convert1(el, indent, opts)}#{end_sections(1, indent, el.options[:location])}#{raw}" end
def convert1(el, indent, opts = {})
def convert1(el, indent, opts = {}) nopts = el.rfc2629_fix(opts) send("convert_#{el.type}", el, indent, nopts) end
def convert_a(el, indent, opts)
def convert_a(el, indent, opts) gi = el.attr.delete('gi') res = inner(el, indent, opts) target = el.attr['target'] if target[0..1] == "{{" # XXX ignoring all attributes and content s = ::Kramdown::Converter::Rfc2629::process_markdown(target) # if res != '' && s[-2..-1] == '/>' # if s =~ /\A<([-A-Za-z0-9_.]+) / # gi ||= $1 # end # s[-2..-1] = ">#{res}</#{gi}>" # end return s end if target[0] == "#" # handle [](#foo) as xref as in RFC 7328 el.attr['target'] = target = target[1..-1] if target.downcase == res.downcase res = '' # get rid of raw anchors leaking through end gi ||= "xref" else gi ||= "eref" end "<#{gi}#{el_html_attributes(el)}>#{res}</#{gi}>" end
def convert_abbreviation(el, indent, opts) # XXX: This is wrong
def convert_abbreviation(el, indent, opts) # XXX: This is wrong if opts[:noabbrev] return el.value end value = el.value ix = value.gsub(/[\s\p{Z}]+/, " ") title = @root.options[:abbrev_defs][ix] if title.nil? warn "*** abbrev mismatch: value = #{value.inspect} ix = #{ix.inspect}" else title = nil if title.empty? end if title == "<bcp14>" && $options.v3 return "<bcp14>#{value}</bcp14>" end hacked_value = value nobr = false if title && title =~ /\A<nobr>(\z|\s)/ nobr = true _nobr, title = title.split(' ', 2) hacked_value = nobr_hack(value) if title.nil? || title.empty? return hacked_value # we have "exhausted" this abbrev -- suppress normal meaning end end if title && title[0] == "#" target, title = title.split(' ', 2) if target == "#" target = value else target = target[1..-1] end else target = nil end if item = title pairs = title.split(Parser::RFC2629Kramdown::IREF_START).each_slice(2).to_a replacement = pairs.map {|x,| s = x.strip; s unless s.empty?}.compact.join(" ") irefs = pairs.map {|_,x| x && [x]}.compact warn "@@@ ABBREV MISMATCH #{irefs}" if title.scan(Parser::RFC2629Kramdown::IREF_START) != irefs if irefs.empty? subitem = value else iref = irefs.map{|a,| iref_attr(a)}.join('') end unless replacement.empty? replacement = nobr_hack(replacement) if nobr # XXX this can break XML replacement = ::Kramdown::Converter::Rfc2629::process_markdown(replacement) hacked_value = replacement end else item = value end iref ||= "<iref#{html_attributes(item: item, subitem: subitem)}/>" if target "#{iref}<xref#{html_attributes(target: target, format: "none")}>#{hacked_value}</xref>" else "#{iref}#{hacked_value}" end end
def convert_blank(el, indent, opts)
def convert_blank(el, indent, opts) "\n" end
def convert_blockquote(el, indent, opts)
def convert_blockquote(el, indent, opts) text = inner(el, indent, opts) if $options.v3 gi = el.attr.delete('gi') if gi && gi != 'ul' "#{' '*indent}<#{gi}#{el_html_attributes(el)}>\n#{text}#{' '*indent}</#{gi}>\n" else "#{' '*indent}<ul#{el_html_attributes_with(el, {"empty" => 'true'})}><li>\n#{text}#{' '*indent}</li></ul>\n" end else text = "<t></t>" unless text =~ /</ # empty block quote "#{' '*indent}<t><list style='empty'#{el_html_attributes(el)}>\n#{text}#{' '*indent}</list></t>\n" end end
def convert_br(el, indent, opts)
def convert_br(el, indent, opts) if $options.v3 "<br />" else "<vspace />" end end
def convert_codeblock(el, indent, opts)
def convert_codeblock(el, indent, opts) # el.attr['anchor'] ||= saner_generate_id(el.value) -- no longer in 1.0.6 result = el.value gi = el.attr.delete('gi') blockclass = el.attr.delete('class') if blockclass == 'language-tbreak' result = result.lines.map {|line| [line.chomp, 0]} spaceind = 0 result.each_with_index {|pair, index| if pair[0] == '' result[spaceind][1] += 1 pair[0] = nil unless index == spaceind else spaceind = index end } # $stderr.puts(result.inspect) result = result.map {|line, space| "<![CDATA[#{line.gsub(/^\s+/) {|s| "\u00A0" * s.size}}]]><vspace blankLines=\"#{space}\"/>" if line }.compact.join("\n") "#{' '*indent}<t>#{result}</t>\n" else artwork_attr = {} t = nil if blockclass classes = blockclass.split(' ') classes.each do |cl| if md = cl.match(/\Alanguage-(.*)/) t = artwork_attr["type"] = md[1] # XXX overwrite else $stderr.puts "*** Unimplemented codeblock class: #{cl}" end end end # compensate for XML2RFC idiosyncrasy by insisting on a blank line unless el.attr.delete('tight') result[0,0] = "\n" unless result[0,1] == "\n" end el.attr.each do |k, v| if md = k.match(/\A(?:artwork|sourcecode)-(.*)/) el.attr.delete(k) artwork_attr[md[1]] = v end end case t when "aasvg", "ditaa", "goat", "math", "math-asciitex", "mermaid", "mscgen", "plantuml", "plantuml-utxt", "protocol", "protocol-aasvg", "protocol-goat", "railroad", "railroad-utf8" if gi warn "*** Can't set GI #{gi} for composite SVG artset" end result, result1 = memoize(:svg_tool_process, t, artwork_attr.delete("svg-options"), artwork_attr.delete("txt-options"), result) retart = mk_artwork(artwork_attr, "ascii-art", "<![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]>") if result1 # nest TXT in artset with SVG retsvg = mk_artwork(artwork_attr, "svg", result1.sub(/.*?<svg/m, "<svg")) retart = "<artset>#{retsvg}#{retart}</artset>" end "#{' '*indent}<figure#{el_html_attributes(el)}>#{retart}</figure>\n" else gi ||= ( if !$options.v3 || !t || ARTWORK_TYPES.include?(t) || artwork_attr["align"] "artwork" else "sourcecode" end ) loc_str = if anchor = el.attr['anchor'] "##{anchor}" elsif lineno = el.options[:location] "#{correct_location(lineno)}" else "UNKNOWN" end preprocs = el.attr.delete("pre") checks = el.attr.delete("check") postprocs = el.attr.delete("post") case t when "cbor" warn "** There is no sourcecode-type “cbor”." warn "** Do you mean “cbor-diag” (diagnostic notation)" warn "** or “cbor-pretty” (annotated hex-dump)?" when "json" checks ||= "json" when /\A(.*)-from-yaml\z/ t = $1 preprocs ||= "yaml2json" end preprocs = (preprocs || '').split("-") checks = (checks || '').split("-") postprocs = (postprocs || '').split("-") result = sourcecode_checkproc(preprocs, checks, postprocs, loc_str, result) "#{' '*indent}<figure#{el_html_attributes(el)}><#{gi}#{html_attributes(artwork_attr)}><![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]></#{gi}></figure>\n" end end end
def convert_codespan(el, indent, opts)
def convert_codespan(el, indent, opts) attrstring = el_html_attributes_with(el, {"style" => 'verb'}) "<spanx#{attrstring}>#{escape_html(el.value)}</spanx>" end
def convert_comment(el, indent, opts)
def convert_comment(el, indent, opts) 't actually output all those comments into the XML: if el.options[:category] == :block "#{' '*indent}<!-- #{el.value} -->\n" else "<!-- #{el.value} -->" end end
def convert_contact(el, indent, opts)
def convert_contact(el, indent, opts) "<contact#{el_html_attributes(el)}/>" end
def convert_dd(el, indent, opts)
def convert_dd(el, indent, opts) if $options.v3 out = '' if !opts[:haddt] out ="#{' '*indent}<dt/>\n" # you can't make this one up end opts[:haddt] = false out << "#{' '*indent}<dd#{el_html_attributes(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</dd>\n" else output = ' '*indent if @in_dt == 1 @in_dt = 0 else output << "<t#{el_html_attributes(el)}>" end res = inner(el, indent+INDENTATION, opts.merge(unpacked: true)) if el.children.empty? || el.children.first.options[:category] != :block output << res << (res =~ /\n\Z/ ? ' '*indent : '') else FIXME: The latter case is needed for more complex cases output << "\n" << res << ' '*indent end output << "</t>\n" end end
def convert_dl(el, indent, opts)
def convert_dl(el, indent, opts) if $options.v3 if hangindent = el.attr.delete('hangIndent') el.attr['indent'] ||= hangindent # new attribute name wins end vspace = el.attr.delete('vspace') if vspace && !el.attr['newline'] el.attr['newline'] = 'true' end "#{' '*indent}<dl#{el_html_attributes(el)}>\n#{inner(el, indent, opts.dup)}#{' '*indent}</dl>\n" else convert_ul(el, indent, opts) end end
def convert_dt(el, indent, opts) # SERIOUSLY BAD HACK:
def convert_dt(el, indent, opts) # SERIOUSLY BAD HACK: if $options.v3 out = '' if opts[:haddt] out ="#{' '*indent}<dd><t/></dd>\n" # you can't make this one up end opts[:haddt] = true out << "#{' '*indent}<dt#{el_html_attributes(el)}>#{inner(el, indent, opts)}</dt>\n" else close = "#{' '*indent}</t>\n" * @in_dt @in_dt = 1 vspace = opts[:vspace] vspaceel = "<vspace blankLines='#{vspace}'/>" if vspace ht = escape_html(inner(el, indent, opts), :attribute) # XXX this may leave gunk "#{close}#{' '*indent}<t#{el_html_attributes(el)} hangText=\"#{ht}\">#{vspaceel}\n" end end
def convert_em(el, indent, opts)
def convert_em(el, indent, opts) if $options.v3 gi = el.type "<#{gi}#{el_html_attributes(el)}>#{inner(el, indent, opts)}</#{gi}>" else attrstring = el_html_attributes_with(el, {"style" => EMPH[el.type]}) span, irefs = clean_pcdata(inner_a(el, indent, opts)) "<spanx#{attrstring}>#{span}</spanx>#{irefs}" end end
def convert_entity(el, indent, opts)
def convert_entity(el, indent, opts) entity_to_str(el.value) end
def convert_footnote(el, indent, opts) # XXX: footnotes into crefs???
def convert_footnote(el, indent, opts) # XXX: footnotes into crefs??? # this would be more like xml2rfc v3: # "\n#{' '*indent}<cref>\n#{inner(el.value, indent, opts).rstrip}\n#{' '*indent}</cref>" content = inner(el.value, indent, opts).strip content = content.sub(/\A<t>(.*)<\/t>\z/m) {$1} name = ::Kramdown::Parser::RFC2629Kramdown.idref_cleanup(el.options[:name]) o_name = name.dup while @footnote_names_in_use[name] do if name =~ /_\d+\z/ name.succ! else name << "_1" end end @footnote_names_in_use[name] = true attrstring = el_html_attributes_with(el, {"anchor" => name}) if $options.v3 if o_name[-1] == "-" # Ignore HTML attributes. Hmm. content else # do not indent span-level so we can stick to previous word. Good? "<cref#{attrstring}>#{content}</cref>" end else content = escape_html(content, :text) # text only... "\n#{' '*indent}<cref#{attrstring}>#{content}</cref>" end end
def convert_header(el, indent, opts)
def convert_header(el, indent, opts) # todo: handle appendix tags el = el.deep_clone options = @doc ? @doc.options : @options # XXX: 0.11 vs. 0.12 if options[:auto_ids] && !el.attr['anchor'] el.attr['anchor'] = saner_generate_id(el.options[:raw_text]) end if $options.v3 if sl = el.attr.delete('slugifiedName') # could do general name- play attrstring = html_attributes({'slugifiedName' => sl}) end # noabbrev: true -- Workaround for https://github.com/ietf-tools/xml2rfc/issues/683 nm = inner(el, indent, opts.merge(noabbrev: true)) if ttl = el.attr['title'] warn "*** Section has two titles: >>#{ttl}<< and >>#{nm}<<" warn "*** Do you maybe have a loose IAL?" end irefs = "<name#{attrstring}>#{nm}</name>" # else clean, irefs = clean_pcdata(inner_a(el, indent, opts)) el.attr['title'] = clean end "#{end_sections(el.options[:level], indent, el.options[:location])}#{' '*indent}<section#{@sec_level += 1; el_html_attributes(el)}>#{irefs}\n" end
def convert_hr(el, indent, opts) # misuse for page break
def convert_hr(el, indent, opts) # misuse for page break "#{' '*indent}<t><vspace blankLines='999' /></t>\n" end
def convert_html_element(el, indent, opts)
def convert_html_element(el, indent, opts) res = inner(el, indent, opts) if el.options[:category] == :span "<#{el.value}#{el_html_attributes(el)}" << (!res.empty? ? ">#{res}</#{el.value}>" : " />") else output = '' output << ' '*indent if !el.options[:parent_is_raw] output << "<#{el.value}#{el_html_attributes(el)}" if !res.empty? && el.options[:parse_type] != :block output << ">#{res}</#{el.value}>" elsif !res.empty? output << ">\n#{res}" << ' '*indent << "</#{el.value}>" elsif HTML_TAGS_WITH_BODY.include?(el.value) output << "></#{el.value}>" else output << " />" end output << "\n" if el.options[:outer_element] || !el.options[:parent_is_raw] output end end
def convert_img(el, indent, opts) # misuse the tag!
def convert_img(el, indent, opts) # misuse the tag! if a = el.attr alt = a.delete('alt').strip alt = '' if alt == '!' # work around re-wrap uglyness if src = a.delete('src') a['target'] = src end end if alt == ":include:" # Really bad misuse of tag... ann = el.attr.delete('ann') anchor = el.attr.delete('anchor') || ( # not yet warn "*** missing anchor for '#{src}'" src ) anchor = ::Kramdown::Parser::RFC2629Kramdown.idref_cleanup(anchor) anchor.gsub!('/', '_') # should take out all illegals to_insert = "" src.scan(/(W3C|3GPP|[A-Z-]+)[.]?([A-Za-z_0-9.\(\)\/\+-]+)/) do |t, n| never_altproc = n.sub!(/^[.]/, "") fn = "reference.#{t}.#{n}.xml" sub, ttl, _can_anchor, altproc, always_altproc = XML_RESOURCE_ORG_MAP[t] ttl ||= KRAMDOWN_REFCACHETTL # everything but RFCs might change a lot puts "*** Huh: #{fn}" unless sub if altproc && !never_altproc && (!KRAMDOWN_USE_TOOLS_SERVER || always_altproc) fn, url = altproc.call(fn, n) else url = "#{XML_RESOURCE_ORG_PREFIX}/#{sub}/#{fn}" fn = "alt-#{fn}" if never_altproc || KRAMDOWN_USE_TOOLS_SERVER end # if can_anchor # create anchor server-side for stand_alone: false # url << "?anchor=#{anchor}" # fn[/.xml$/] = "--anchor=#{anchor}.xml" # end to_insert = get_and_cache_resource(url, fn.gsub('/', '_'), ttl) to_insert.scrub! rescue nil # only do this for Ruby >= 2.1 begin d = REXML::Document.new(to_insert) d.xml_decl.nowrite d.delete d.doctype d.context[:attribute_quote] = :quote # Set double-quote as the attribute value delimiter d.root.attributes["anchor"] = anchor if t == "RFC" or t == "I-D" if KRAMDOWN_NO_TARGETS || !KRAMDOWN_KEEP_TARGETS d.root.attributes["target"] = nil REXML::XPath.each(d.root, "/reference/format") { |x| d.root.delete_element(x) } else REXML::XPath.each(d.root, "/reference/format") { |x| x.attributes["target"].sub!(%r{https?://www.ietf.org/internet-drafts/}, %{https://www.ietf.org/archive/id/}) if t == "I-D" } end elsif t == "IANA" d.root.attributes["target"].sub!(%r{\Ahttp://www.iana.org/assignments/}, 'https://www.iana.org/assignments/') end if ann el = ::Kramdown::Converter::Rfc2629::process_markdown_to_rexml(ann).root el.name = "annotation" d.root.add_element(el) end to_insert = d.to_s rescue Exception => e warn "** Can't manipulate reference XML: #{e}" broken = true to_insert = nil end # this may be a bit controversial: Don't break the build if reference is broken if KRAMDOWN_OFFLINE || broken unless to_insert to_insert = "<reference anchor='#{anchor}'> <front> <title>*** BROKEN REFERENCE ***</title> <author> <organization/> </author> <date/> </front> </reference>" warn "*** KRAMDOWN_OFFLINE: Inserting broken reference for #{fn}" end else exit 66 unless to_insert # EX_NOINPUT end end to_insert else "<xref#{el_html_attributes(el)}>#{alt}</xref>" end end
def convert_iref(el, indent, opts)
def convert_iref(el, indent, opts) iref_attr(el.attr['target']) end
def convert_li(el, indent, opts)
def convert_li(el, indent, opts) res_a = inner_a(el, indent, opts) if el.children.empty? || el.children.first.options[:category] == :span res = res_a.join('') else # merge multiple <t> elements res = res_a.select { |x| x.strip != '' }.map { |x| x.sub(/\A\s*<t>(.*)<\/t>\s*\Z/m) { $1} }.join("#{' '*indent}<vspace blankLines='1'/>\n").gsub(%r{(</list>)\s*<vspace blankLines='1'/>}) { $1 }.gsub(%r{<vspace blankLines='1'/>\s*(<list)}) { $1 } end "#{' '*indent}<t#{el_html_attributes(el)}>#{res}#{(res =~ /\n\Z/ ? ' '*indent : '')}</t>\n" end
def convert_math(el, indent, opts) # XXX: This is wrong
def convert_math(el, indent, opts) # XXX: This is wrong el = el.deep_clone if el.options[:category] == :block el.attr['artwork-type'] ||= '' el.attr['artwork-type'] += (el.attr['artwork-type'].empty? ? '' : ' ') + 'math' artwork_attr = {} el.attr.each do |k, v| if md = k.match(/\Aartwork-(.*)/) el.attr.delete(k) artwork_attr[md[1]] = v end end result, err, _s = Open3.capture3("tex2mail -noindent -ragged -by_par -linelength=69", stdin_data: el.value); # warn "*** tex2mail not in path?" unless s.success? -- doesn't have useful status capture_croak("tex2mail", err) "#{' '*indent}<figure#{el_html_attributes(el)}><artwork#{html_attributes(artwork_attr)}><![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]></artwork></figure>\n" else type = 'spanx' if $options.v3 type = 'contact' result = munge_latex(el.value) attrstring = el_html_attributes_with(el, {"fullname" => result.chomp, "asciiFullname" => ''}) else warn "*** no support for inline math in XML2RFCv2" type = 'spanx' attrstring = el_html_attributes_with(el, {"style" => 'verb'}) content = escape_html(el.value, :text) end "<#{type}#{attrstring}>#{content}</#{type}>" end end
def convert_ol(el, indent, opts)
def convert_ol(el, indent, opts) if @options[:ol_start_at_first_marker] and (first_list_marker = el.options[:first_list_marker]) el.attr['start'] ||= first_list_marker[/\d+/] end nested_types = opts[:nested_ol_types] || ["1"] # warn "** ol opts #{opts.inspect} types #{nested_types.inspect}" if nested_attr = el.attr.delete('nestedOlTypes') nested_types = ::Kramdown::Options.parse(:nested_ol_types, nested_attr) end if nested_types = nested_types.dup # warn "** nested_types #{nested_types.inspect}" nested_here = nested_types.shift opts = opts.merge(nested_ol_types: nested_types << nested_here) el.attr['type'] ||= nested_here # warn "** actual ol type #{el.attr['type'].inspect}" end convert_ul(el, indent, opts) end
def convert_p(el, indent, opts)
def convert_p(el, indent, opts) if (el.children.size == 1 && el.children[0].type == :img) || opts[:unpacked] inner(el, indent, opts) # Part of the bad reference hack else "#{' '*indent}<t#{el_html_attributes(el)}>#{inner(el, indent, opts)}</t>\n" end end
def convert_raw(el, indent, opts)
def convert_raw(el, indent, opts) end_sections(1, indent, el.options[:location]) + el.value + (el.options[:category] == :block ? "\n" : '') end
def convert_root(el, indent, opts)
def convert_root(el, indent, opts) result = inner(el, indent, opts) end
def convert_smart_quote(el, indent, opts)
def convert_smart_quote(el, indent, opts) entity_to_str(smart_quote_entity(el)) end
def convert_table(el, indent, opts) # This only works for tables with headers
def convert_table(el, indent, opts) # This only works for tables with headers alignment = el.options[:alignment].map { |al| ALIGNMENTS[al]} cols = (el.attr.delete("cols") || "").split(' ') "#{' '*indent}<texttable#{el_html_attributes(el)}>\n#{inner(el, indent, opts.merge(table_alignment: alignment, table_cols: cols))}#{' '*indent}</texttable>\n" end
def convert_td(el, indent, opts)
def convert_td(el, indent, opts) if alignment = opts[:table_alignment] alignment = alignment.shift if cols = opts[:table_cols].shift md = cols.match(/(\d*(|em|[%*]))([lrc])/) if md[1].to_i != 0 widthval = md[1] widthval << "em" if md[2].empty? widthopt = "width='#{widthval}' " end alignment = COLS_ALIGN[md[3]] || :left end end if alignment xmlres = inner_a(el, indent, opts) if $options.v3 res = clean_pcdatav3(xmlres) else res, irefs = clean_pcdata(xmlres) warn "*** lost markup #{irefs} in table heading" unless irefs.empty? end "#{' '*indent}<ttcol #{widthopt}align='#{alignment}'#{el_html_attributes(el)}>#{res.empty? ? " " : res}</ttcol>\n" # XXX need clean_pcdata else res = inner(el, indent, opts) "#{' '*indent}<c#{el_html_attributes(el)}>#{res.empty? ? " " : res}</c>\n" end end
def convert_text(el, indent, opts)
def convert_text(el, indent, opts) escape_html(el.value, :text) end
def convert_thead(el, indent, opts)
def convert_thead(el, indent, opts) inner(el, indent, opts) end
def convert_typographic_sym(el, indent, opts)
def convert_typographic_sym(el, indent, opts) if (result = @options[:typographic_symbols][el.value]) escape_html(result, :text) else TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join('') end end
def convert_ul(el, indent, opts)
def convert_ul(el, indent, opts) opts = opts.merge(vspace: el.attr.delete('vspace')) attrstring = el_html_attributes_with(el, {"style" => STYLES[el.type]}) if opts[:unpacked] "#{' '*indent}<list#{attrstring}>\n#{inner(el, indent, opts)}#{' '*indent}</list>\n" else "#{' '*indent}<t><list#{attrstring}>\n#{inner(el, indent, opts)}#{' '*indent}</list></t>\n" end end
def convert_xml_comment(el, indent, opts)
def convert_xml_comment(el, indent, opts) if el.value =~ /\A<\?line (([-+]?)[0-9]+)\?>\z/ lineno = $1.to_i case $2 when '' # absolute @location_delta = lineno - el.options[:location] when '+', '-' # correction (pre-scanning!) @location_correction += lineno end end if el.options[:category] == :block && !el.options[:parent_is_raw] ' '*indent + el.value + "\n" else el.value end end
def convert_xref(el, indent, opts)
def convert_xref(el, indent, opts) gi = el.attr.delete('gi') text = el.attr.delete('text') target = el.attr['target'] if target[0] == "&" "#{target};" else if target =~ %r{\A\w+:(?://|.*@)} gi ||= "eref" else gi ||= "xref" end if text tail = ">#{Rfc2629::process_markdown(text)}</#{gi}>" else tail = "/>" end "<#{gi}#{el_html_attributes(el)}#{tail}" end end
def correct_location(location)
def correct_location(location) location + @location_delta + @location_correction end
def el_html_attributes(el)
def el_html_attributes(el) html_attributes(el.attr) end
def el_html_attributes_with(el, defattr)
def el_html_attributes_with(el, defattr) html_attributes(defattr.merge(el.attr)) end
def end_sections(to_level, indent, location)
def end_sections(to_level, indent, location) if indent < 0 indent = 0 end if @sec_level >= to_level delta = (@sec_level - to_level) @sec_level = to_level "#{' '*indent}</section>\n" * delta else $stderr.puts "** #{correct_location(location)}: Bad section nesting: start heading level at 1 and increment by 1" end end
def get_and_cache_resource(url, cachefile, tvalid = 7200, tn = Time.now)
def get_and_cache_resource(url, cachefile, tvalid = 7200, tn = Time.now) fn = "#{REFCACHEDIR}/#{cachefile}" Dir.mkdir(REFCACHEDIR) unless Dir.exist?(REFCACHEDIR) f = File.stat(fn) rescue nil unless KRAMDOWN_REFCACHE_REFETCH if !KRAMDOWN_OFFLINE && (!f || tn - f.mtime >= tvalid) if f message = "renewing (stale by #{"%.1f" % ((tn-f.mtime)/86400)} days)" fetch_timeout = 10 # seconds, give up quickly if just renewing else message = "fetching" fetch_timeout = 60 # seconds; long timeout needed for Travis end $stderr.puts "#{fn}: #{message} from #{url}" if Array === url begin case url[0] when :DOI ref = get_doi(url[1]) File.write(fn, ref) end rescue Exception => e warn "*** Error fetching #{url[0]} #{url[1].inspect}: #{e}" end elsif ENV["HAVE_WGET"] `cd #{REFCACHEDIR}; wget -t 3 -T #{fetch_timeout} -Nnv "#{url}"` # ignore errors if offline (hack) begin File.utime nil, nil, fn rescue Errno::ENOENT warn "Can't fetch #{url} -- is wget in path?" end else require 'open-uri' require 'socket' require 'openssl' require 'timeout' begin Timeout::timeout(fetch_timeout) do if $http begin # belt and suspenders get_and_write_resource_persistently(url, fn) rescue Exception => e warn "*** Can't get with persistent HTTP: #{e}" get_and_write_resource(url, fn) end else get_and_write_resource(url, fn) end end rescue OpenURI::HTTPError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, SocketError, Timeout::Error => e warn "*** #{e} while fetching #{url}" end end end begin File.read(fn) # this blows up if no cache available after fetch attempt rescue Errno::ENOENT => e warn "*** #{e} for #{fn}" end end
def get_and_write_resource(url, fn)
def get_and_write_resource(url, fn) options = {} if ENV["KRAMDOWN_DONT_VERIFY_HTTPS"] options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE end # workaround for OpenSSL on Windows... # URI.open(url, **options) do |uf| # not portable to older versions OpenURI.open_uri(url, **options) do |uf| s = uf.read if uf.status[0] != "200" warn "*** Status code #{status} while fetching #{url}" else File.write(fn, s) end end end
def get_and_write_resource_persistently(url, fn)
def get_and_write_resource_persistently(url, fn) t1 = Time.now response = $http.request(URI(url)) if response.code != "200" raise "Status code #{response.code} while fetching #{url}" else File.write(fn, response.body) end t2 = Time.now warn "(#{"%.3f" % (t2 - t1)} s)" if KRAMDOWN_PERSISTENT_VERBOSE end
def get_doi(refname)
def get_doi(refname) lit = doi_fetch_and_convert(refname, fuzzy: true) anchor = "DOI_#{refname.gsub("/", "_")}" KramdownRFC::ref_to_xml(anchor, lit) end
def initialize(*doc)
def initialize(*doc) super @sec_level = 1 @location_delta = 100000 # until reset @location_correction = 0 # pre-scanning corrections @in_dt = 0 @footnote_names_in_use = {} end
def inner(el, indent, opts)
def inner(el, indent, opts) inner_a(el, indent, opts).join('') end
def inner_a(el, indent, opts)
def inner_a(el, indent, opts) indent += INDENTATION el.children.map do |inner_el| nopts = inner_el.rfc2629_fix(opts) send("convert_#{inner_el.type}", inner_el, indent, nopts) end end
def iref_attr(s)
def iref_attr(s) md = s.match(IREF_RE) attr = { item: md[2] || md[3], subitem: md[4] || md[5], primary: md[1] && 'true', } "<iref#{html_attributes(attr)}/>" end
def memoize(meth, *args)
def memoize(meth, *args) require 'digest' Dir.mkdir(REFCACHEDIR) unless Dir.exist?(REFCACHEDIR) kdrfc_version = Gem.loaded_specs["kramdown-rfc2629"].version.to_s.gsub('.', '_') rescue "UNKNOWN" fn = "#{REFCACHEDIR}/kdrfc-#{kdrfc_version}-#{meth}-#{Digest::SHA256.hexdigest(Marshal.dump(args))[0...40]}.cache" begin out = Marshal.load(File.binread(fn)) rescue StandardError => e # warn e.inspect out = method(meth).call(*args) File.binwrite(fn, Marshal.dump(out)) end out end
def mk_artwork(artwork_attr, typ, content)
def mk_artwork(artwork_attr, typ, content) "<artwork #{html_attributes(artwork_attr.merge("type" => typ))}>#{content}</artwork>" end
def munge_latex(s)
def munge_latex(s) MATH_REPLACEMENTS.each do |o, n| s.gsub!(o, n) end MATH_COMBININGMARKS.each do |m, n| re = /\\#{m[1..-1]}\{(\X)\}/ s.gsub!(re) { "#$1#{n}" } end s end
def nobr_hack(s) # replace this by actual <nobr> once that exists
def nobr_hack(s) # replace this by actual <nobr> once that exists # https://github.com/ietf-tools/xml2rfc/blob/main/xml2rfc/utils.py#L42 s.gsub(/([-\s\/])(?!\s)/) { case rep = $1 when /\A\s\z/ "\u00A0" # nbsp when "-" "\u2011" # nbhy -- XXX this might mangle dashes else "#{rep}\u2060" end } end
def saner_generate_id(value)
def saner_generate_id(value) generate_id(value).gsub(/-+/, '-') end
def shell_prepare(opt)
def shell_prepare(opt) " " << opt.shellsplit.shelljoin end
def sourcecode_checkproc(preprocs, checks, postprocs, loc_str, result)
def sourcecode_checkproc(preprocs, checks, postprocs, loc_str, result) preprocs.each do |proc| result = sourcecode_proc(proc, loc_str, result) end if preprocs check_input = result checks.each do |check| case check when "skipheader" check_input = handle_artwork_sourcecode(check_input).sub(/.*?\n\n/m, '') when "json" # check for 8792; undo if needed: begin JSON.load(handle_artwork_sourcecode(check_input)) rescue => e err1 = "*** #{loc_str}: JSON isn't: #{JSON.dump(e.message[0..40])}\n" begin JSON.load("{" << check_input << "}") rescue => e warn err1 << "*** not even with braces added around: #{JSON.dump(e.message[0..40])}" end end else warn "*** #{loc_str}: unknown check '#{check}'" end end if checks postprocs.each do |proc| result = sourcecode_proc(proc, loc_str, result) end if postprocs result end
def sourcecode_proc(proc, loc_str, result)
def sourcecode_proc(proc, loc_str, result) case proc when "dedent" result = remove_indentation(result) when /\Afold(\d*)(left(\d*))?(dry)?\z/ fold = [$1.to_i, # col 0 for '' ($3.to_i if $2), # left 0 for '', nil if no "left" $4] # dry result = fix_unterminated_line(fold8792_1(trim_empty_lines_around(result), *fold)) # XXX when "yaml2json" begin y = YAML.safe_load(result, aliases: true, filename: loc_str) result = JSON.pretty_generate(y) rescue => e warn "*** #{loc_str}: YAML isn't: #{e.message}\n" end else warn "*** #{loc_str}: unknown proc '#{proc}'" end result end
def svg_clean(s) # expensive, risky
def svg_clean(s) # expensive, risky d = REXML::Document.new(s) REXML::XPath.each(d.root, "//*[@shape-rendering]") { |x| x.attributes["shape-rendering"] = nil } #; warn x.inspect } REXML::XPath.each(d.root, "//*[@text-rendering]") { |x| x.attributes["text-rendering"] = nil } #; warn x.inspect } REXML::XPath.each(d.root, "//*[@stroke]") { |x| x.attributes["stroke"] = svg_munch_color(x.attributes["stroke"], false) } REXML::XPath.each(d.root, "//*[@fill]") { |x| x.attributes["fill"] = svg_munch_color(x.attributes["fill"], true) } REXML::XPath.each(d.root, "//*[@id]") { |x| x.attributes["id"] = svg_munch_id(x.attributes["id"]) } REXML::XPath.each(d.root, "//rect") { |x| x.attributes["style"] = "fill:none;stroke:black;stroke-width:1" unless x.attributes["style"] } # Fix for mermaid: REXML::XPath.each(d.root, "//polygon") { |x| x.attributes["rx"] = nil; x.attributes["ry"] = nil } d.to_s rescue => detail warn "*** Can't clean SVG: #{detail}" d end
def svg_clean_kgt(s)
def svg_clean_kgt(s) d = REXML::Document.new(s) REXML::XPath.each(d.root, "/xmlns:svg", SVG_NAMESPACES) do |x| if (w = x.attributes["width"]) && (h = x.attributes["height"]) x.attributes["viewBox"] = "0 0 %d %d" % [w, h] end if x.attributes["viewBox"] x.attributes["width"] = nil x.attributes["height"] = nil end end REXML::XPath.each(d.root, "//rect|//line|//path") do |x| x.attributes["fill"] = "none" x.attributes["stroke"] = "black" x.attributes["stroke-width"] = "1.5" end d.to_s rescue => detail warn "*** Can't clean SVG: #{detail}" d end
def svg_munch_color(c, fill)
def svg_munch_color(c, fill) c = SVG_COLORS[c] case c when /\A#(..)(..)(..)\z/ if hex_to_lin($1)*0.2126 + hex_to_lin($2)*0.7152 + hex_to_lin($3)*0.0722 >= B_W_THRESHOLD 'white' else 'black' end when 'none' 'none' if fill # delete for stroke else c end end
def svg_munch_id(id)
def svg_munch_id(id) id.gsub(/[^-._A-Za-z0-9]/) {|x| "_%02X" % x.ord} end
def svg_tool_process(t, svg_opt, txt_opt, result)
def svg_tool_process(t, svg_opt, txt_opt, result) require 'tempfile' file = Tempfile.new("kramdown-rfc") file.write(result) file.close dont_clean = false dont_check = false svg_opt = shell_prepare(svg_opt) if svg_opt txt_opt = shell_prepare(txt_opt) if txt_opt case t when "protocol", "protocol-goat", "protocol-aasvg" cmdparm = result.lines.map(&:strip).select {|x| x != ''}.join(',') result, err, _s = Open3.capture3("protocol #{Shellwords.escape(cmdparm)}#{txt_opt}", stdin_data: '') if t == "protocol-goat" file.unlink file = Tempfile.new("kramdown-rfc") file.write(result) file.close result1, err, _s = Open3.capture3("goat#{svg_opt} #{file.path}", stdin_data: result); dont_clean = true elsif t == "protocol-aasvg" result1, err, _s = Open3.capture3("#{DEFAULT_AASVG}#{svg_opt}", stdin_data: result); dont_clean = true dont_check = true else result1 = nil end when "goat" result1, err, _s = Open3.capture3("goat#{svg_opt} #{file.path}", stdin_data: result); dont_clean = true when "aasvg" result1, err, _s = Open3.capture3("#{DEFAULT_AASVG}#{svg_opt}", stdin_data: result); dont_clean = true dont_check = true when "ditaa" # XXX: This needs some form of option-setting result1, err, _s = Open3.capture3("ditaa #{file.path} --svg -o -#{svg_opt}", stdin_data: result); when "mscgen" result1, err, _s = Open3.capture3("mscgen -T svg -i #{file.path} -o -#{svg_opt}", stdin_data: result); when "mermaid" result1, err, _s = Open3.capture3("mmdc -i #{file.path}#{svg_opt}", stdin_data: result); # -b transparent outpath = file.path + ".svg" result1 = File.read(outpath) rescue '' # don't die before providing error message File.unlink(outpath) rescue nil # ditto when "plantuml", "plantuml-utxt" plantuml = "@startuml\n#{result}\n@enduml" result1, err, _s = Open3.capture3("plantuml -pipe -tsvg#{svg_opt}", stdin_data: plantuml); result, err1, _s = Open3.capture3("plantuml -pipe -tutxt#{txt_opt}", stdin_data: plantuml) if t == "plantuml-utxt" err << err1.to_s when "railroad", "railroad-utf8" result1, err1, _s = Open3.capture3("kgt -l abnf -e svg#{svg_opt}", stdin_data: result); result1 = svg_clean_kgt(result1); dont_clean = true result, err, _s = Open3.capture3("kgt -l abnf -e rr#{t == "railroad" ? "text" : "utf8"}#{txt_opt}", stdin_data: result); err << err1.to_s when "math", "math-asciitex" result1, err, _s = Open3.capture3("tex2svg --font STIX --speech=false#{svg_opt} #{Shellwords.escape(' ' << result)}"); begin raise Errno::ENOENT if t == "math-asciitex" result, err1, s = Open3.capture3("utftex -m #{txt_opt}", stdin_data: result) if s.exitstatus != 0 warn "** utftex: #{err1.inspect}" raise Errno::ENOENT end rescue Errno::ENOENT warn "** utftex not working, falling back to asciitex" unless t == "math-asciitex" result, err1, _s = Open3.capture3("asciitex -f #{file.path}#{txt_opt}") end err << err1 end capture_croak(t, err) # warn ["text:", result.inspect] # warn ["svg:", result1.inspect] file.unlink if result1 result1 = svg_clean(result1) unless dont_clean unless dont_check file = Tempfile.new("kramdown-rfc") file.write(result1) file.close result1, err, _s = Open3.capture3("svgcheck -qa #{file.path}"); file.unlink # warn ["svgcheck:", result1.inspect] capture_croak("svgcheck", err) end if result1 == '' warn "*** could not create svg for #{result.inspect[0...20]}..." exit 65 # EX_DATAERR end end [result, result1] # text, svg end