// XML2RFC v2 rendering backend
package mmark
import (
"bytes"
"strconv"
"strings"
)
// References code in Xml2rfcv3.go
// XML renderer configuration options.
const (
XML2_STANDALONE = 1 << iota // create standalone document
)
// Xml2 is a type that implements the Renderer interface for XML2RFV3 output.
//
// Do not create this directly, instead use the Xml2Renderer function.
type xml2 struct {
flags int // XML2_* options
sectionLevel int // current section level
docLevel int // frontmatter/mainmatter or backmatter
part bool // parts cannot nest, if true a part has been opened
specialSection int // are we in a special section
paraInList bool // subsequent paras in lists are faked with vspace
// store the IAL we see for this block element
ial *inlineAttr
// titleBlock in TOML
titleBlock *title
// (@good) example list group counter
group map[string]int
}
// Xml2Renderer creates and configures a Xml2 object, which
// satisfies the Renderer interface.
//
// flags is a set of XML2_* options ORed together
func Xml2Renderer(flags int) Renderer {
return &xml2{flags: flags, group: make(map[string]int)}
}
func (options *xml2) Flags() int { return options.flags }
func (options *xml2) State() int { return 0 }
func (options *xml2) SetAttr(i *inlineAttr) {
options.ial = i
}
func (options *xml2) Attr() *inlineAttr {
if options.ial == nil {
return newInlineAttr()
}
return options.ial
}
func (options *xml2) AttrString(i *inlineAttr) string {
if i == nil {
return ""
}
s := ""
if i.id != "" {
s = " anchor=\"" + i.id + "\""
}
keys := i.SortClasses()
if len(keys) > 0 {
s += " class=\"" + strings.Join(keys, " ") + "\""
}
keys = i.SortAttributes()
attr := make([]string, len(keys))
for j, k := range keys {
v := i.attr[k]
attr[j] = k + "=\"" + v + "\""
}
if len(keys) > 0 {
s += " " + strings.Join(attr, " ")
}
return s
}
// render code chunks using verbatim, or listings if we have a language
func (options *xml2) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure, callout bool) {
ial := options.Attr()
ial.GetOrDefaultAttr("align", "center")
ialArtwork := newInlineAttr()
prefix := ial.Value("prefix")
ial.DropAttr("prefix") // it's a fake attribute, so drop it
if lang == "" {
lang = ial.Value("type")
}
if lang != "" {
ialArtwork.SetAttr("type", lang)
}
ial.DropAttr("type")
// subfigure stuff. TODO(miek): check
if len(caption) > 0 {
ial.GetOrDefaultAttr("title", string(sanitizeXML(caption)))
}
s := options.AttrString(ial)
out.WriteString("\n<figure" + s + "><artwork" + ial.Key("align") + options.AttrString(ialArtwork) + ">\n")
text = blockCodePrefix(prefix, text)
if callout {
attrEscapeInCode(options, out, text)
} else {
writeEntity(out, text)
}
out.WriteString("</artwork></figure>\n")
}
func (options *xml2) CalloutCode(out *bytes.Buffer, index, id string) {
// Should link to id
attrEscape(out, []byte("<"))
out.WriteString(index)
attrEscape(out, []byte(">"))
return
}
func (options *xml2) CalloutText(out *bytes.Buffer, index string, id []string) {
out.WriteByte('(')
for i, k := range id {
out.WriteString(k)
if i < len(id)-1 {
out.WriteString(", ")
}
}
out.WriteByte(')')
}
func (options *xml2) TitleBlockTOML(out *bytes.Buffer, block *title) {
if options.flags&XML2_STANDALONE == 0 {
return
}
options.titleBlock = block
out.WriteString("<rfc")
out.WriteString(" ipr=\"" + options.titleBlock.Ipr + "\"")
out.WriteString(" category=\"" + options.titleBlock.Category + "\"")
out.WriteString(" docName=\"" + options.titleBlock.DocName + "\"")
if len(options.titleBlock.Updates) > 0 {
updates := make([]string, len(options.titleBlock.Updates))
for i := range updates {
updates[i] = strconv.Itoa(options.titleBlock.Updates[i])
}
out.WriteString(" updates=\"" + strings.Join(updates, ", ") + "\"")
}
if len(options.titleBlock.Obsoletes) > 0 {
obsoletes := make([]string, len(options.titleBlock.Obsoletes))
for i := range obsoletes {
obsoletes[i] = strconv.Itoa(options.titleBlock.Obsoletes[i])
}
out.WriteString(" obsoletes=\"" + strings.Join(obsoletes, ", ") + "\"")
}
out.WriteString(">\n")
// Default processing instructions
for _, p := range PIs {
out.WriteString(titleBlockTOMLPI(options.titleBlock.PI, p, 2))
}
out.WriteString("<front>\n")
options.docLevel = _DOC_FRONT_MATTER
out.WriteString("<title abbrev=\"" + options.titleBlock.Abbrev + "\">")
out.WriteString(options.titleBlock.Title + "</title>\n\n")
for _, a := range options.titleBlock.Author {
titleBlockTOMLAuthor(out, a)
}
titleBlockTOMLDate(out, options.titleBlock.Date)
out.WriteString("<area>" + options.titleBlock.Area + "</area>\n")
out.WriteString("<workgroup>" + options.titleBlock.Workgroup + "</workgroup>\n")
titleBlockTOMLKeyword(out, options.titleBlock.Keyword)
out.WriteString("\n")
}
func (options *xml2) BlockQuote(out *bytes.Buffer, text []byte, attribution []byte) {
// Fake a list paragraph
// TODO(miek): IAL, clear them for now
options.Attr()
out.WriteString("<t><list style=\"empty\">\n")
out.Write(text)
// check for "person -- URI" syntax use those if found
// need to strip tags because these are attributes
if len(attribution) != 0 {
parts := bytes.Split(attribution, []byte("-- "))
if len(parts) > 0 {
cite := bytes.TrimSpace(parts[0])
var quotedFrom []byte
if len(parts) == 2 {
quotedFrom = bytes.TrimSpace(parts[1])
}
out.WriteString("<t>-- ")
out.Write(cite)
if len(parts) == 2 {
if len(cite) > 0 {
out.WriteString(", ")
}
// len(quotedFrom) == 0 check as well?
out.Write(quotedFrom)
}
out.WriteString("</t>\n")
}
}
out.WriteString("</list></t>\n")
}
func (options *xml2) Aside(out *bytes.Buffer, text []byte) {
options.BlockQuote(out, text, nil)
}
func (options *xml2) CommentHtml(out *bytes.Buffer, text []byte) {
i := bytes.Index(text, []byte("-->"))
if i > 0 {
text = text[:i]
}
// strip, <!--
text = text[4:]
var source []byte
l := len(text)
if l > 20 {
l = 20
}
for i := 0; i < l; i++ {
if text[i] == '-' && text[i+1] == '-' {
source = text[:i]
text = text[i+2:]
break
}
}
// don't output a cref if it is not name -- remark
if len(source) != 0 {
source = bytes.TrimSpace(source)
text = bytes.TrimSpace(text)
out.WriteString("<t><cref source=\"")
out.Write(source)
out.WriteString("\">")
out.Write(text)
out.WriteString("</cref></t>\n")
}
return
}
func (options *xml2) BlockHtml(out *bytes.Buffer, text []byte) {
printf(nil, "syntax not supported: BlockHtml")
}
func (options *xml2) Part(out *bytes.Buffer, text func() bool, id string) {
printf(nil, "syntax not supported: Part")
}
func (options *xml2) Note(out *bytes.Buffer, text func() bool, id string) {
switch options.specialSection {
case _ABSTRACT:
out.WriteString("</abstract>\n\n")
case _NOTE:
out.WriteString("</note>\n\n")
}
level := 1
if level <= options.sectionLevel {
// close previous ones
for i := options.sectionLevel - level + 1; i > 0; i-- {
out.WriteString("</section>\n")
}
}
ial := options.Attr()
out.WriteString("\n<note" + options.AttrString(ial))
out.WriteString(" title=\"")
text()
out.WriteString("\">\n")
options.sectionLevel = 0
options.specialSection = _NOTE
return
}
func (options *xml2) SpecialHeader(out *bytes.Buffer, what []byte, text func() bool, id string) {
if string(what) == "preface" {
printf(nil, "handling preface like abstract")
what = []byte("abstract")
}
switch options.specialSection {
case _ABSTRACT:
out.WriteString("</abstract>\n\n")
case _NOTE:
out.WriteString("</note>\n\n")
}
level := 1
if level <= options.sectionLevel {
// close previous ones
for i := options.sectionLevel - level + 1; i > 0; i-- {
out.WriteString("</section>\n")
}
}
ial := options.Attr()
out.WriteString("\n<abstract" + options.AttrString(ial) + ">\n")
options.sectionLevel = 0
options.specialSection = _ABSTRACT
return
}
func (options *xml2) Header(out *bytes.Buffer, text func() bool, level int, id string) {
switch options.specialSection {
case _ABSTRACT:
out.WriteString("</abstract>\n\n")
case _NOTE:
out.WriteString("</note>\n\n")
}
if level > options.sectionLevel+1 {
printf(nil, "section jump from H%d to H%d, id: \"%s\"", options.sectionLevel, level, id)
}
if level <= options.sectionLevel {
// close previous ones
for i := options.sectionLevel - level + 1; i > 0; i-- {
out.WriteString("</section>\n")
}
}
ial := options.Attr()
ial.GetOrDefaultId(id)
ial.KeepAttr([]string{"title", "toc"})
ial.KeepClass(nil)
// new section
out.WriteString("\n<section" + options.AttrString(ial))
out.WriteString(" title=\"")
text()
out.WriteString("\">\n")
options.sectionLevel = level
options.specialSection = 0
return
}
func (options *xml2) HRule(out *bytes.Buffer) {
printf(nil, "syntax not supported: HRule")
}
func (options *xml2) List(out *bytes.Buffer, text func() bool, flags, start int, group []byte) {
marker := out.Len()
// inside lists we must drop the paragraph
if flags&_LIST_INSIDE_LIST == 0 {
out.WriteString("<t>\n")
}
ial := options.Attr()
ial.KeepAttr([]string{"style", "counter"})
// start > 1 is not supported
// for group, fake a numbered format (if not already given and put a
// group -> current number in options
switch {
case flags&_LIST_TYPE_ORDERED != 0:
switch {
case flags&_LIST_TYPE_ORDERED_ALPHA_LOWER != 0:
ial.GetOrDefaultAttr("style", "format %c")
case flags&_LIST_TYPE_ORDERED_ALPHA_UPPER != 0:
ial.GetOrDefaultAttr("style", "format %C")
case flags&_LIST_TYPE_ORDERED_ROMAN_LOWER != 0:
ial.GetOrDefaultAttr("style", "format %i")
case flags&_LIST_TYPE_ORDERED_ROMAN_UPPER != 0:
ial.GetOrDefaultAttr("style", "format %I")
case flags&_LIST_TYPE_ORDERED_GROUP != 0:
if group != nil {
// don't think we need ++ this.
options.group[string(group)]++
ial.GetOrDefaultAttr("counter", string(group))
ial.GetOrDefaultAttr("style", "format (%d)")
}
default:
ial.GetOrDefaultAttr("style", "numbers")
}
case flags&_LIST_TYPE_DEFINITION != 0:
ial.GetOrDefaultAttr("style", "hanging")
default:
ial.GetOrDefaultAttr("style", "symbols")
}
out.WriteString("<list" + options.AttrString(ial) + ">\n")
if !text() {
out.Truncate(marker)
return
}
switch {
case flags&_LIST_TYPE_ORDERED != 0:
out.WriteString("</list>\n")
case flags&_LIST_TYPE_DEFINITION != 0:
out.WriteString("</t>\n</list>\n")
default:
out.WriteString("</list>\n")
}
if flags&_LIST_INSIDE_LIST == 0 {
out.WriteString("</t>\n")
}
}
func (options *xml2) ListItem(out *bytes.Buffer, text []byte, flags int) {
if flags&_LIST_TYPE_DEFINITION != 0 && flags&_LIST_TYPE_TERM == 0 {
out.Write(text)
return
}
if flags&_LIST_TYPE_TERM != 0 {
if flags&_LIST_ITEM_BEGINNING_OF_LIST == 0 {
out.WriteString("</t>\n")
}
// close previous one?/
out.WriteString("<t hangText=\"")
n := out.Len()
writeSanitizeXML(out, text)
if n == out.Len() {
printf(nil, "no text remained after sanitizing XML for definition term: '"+string(text)+"'")
}
out.WriteString("\">\n")
out.WriteString("<vspace />\n") // Align HTML and XML2 output, but inserting a new line (vspace here)
return
}
out.WriteString("<t>")
out.Write(text)
out.WriteString("</t>\n")
options.paraInList = false
}
func (options *xml2) Example(out *bytes.Buffer, index int) {
out.WriteByte('(')
out.WriteString(strconv.Itoa(index))
out.WriteByte(')')
}
func (options *xml2) Paragraph(out *bytes.Buffer, text func() bool, flags int) {
marker := out.Len()
if flags&_LIST_TYPE_DEFINITION == 0 && flags&_LIST_INSIDE_LIST == 0 {
out.WriteString("<t>")
} else {
if options.paraInList && flags&_LIST_ITEM_BEGINNING_OF_LIST != 0 {
out.WriteString("<vspace blankLines=\"1\" />\n")
}
}
if !text() {
out.Truncate(marker)
return
}
if marker+3 == out.Len() { // empty paragraph, suppress
out.Truncate(marker)
return
}
out.WriteByte('\n')
if flags&_LIST_TYPE_DEFINITION == 0 && flags&_LIST_INSIDE_LIST == 0 {
out.WriteString("</t>\n")
} else {
options.paraInList = true
}
}
func (options *xml2) Math(out *bytes.Buffer, text []byte, display bool) {
// Just output whatever is the text.
writeEntity(out, text)
}
func (options *xml2) Table(out *bytes.Buffer, header []byte, body []byte, footer []byte, columnData []int, caption []byte) {
ial := options.Attr()
if caption != nil {
ial.GetOrDefaultAttr("title", string(sanitizeXML(caption)))
}
s := options.AttrString(ial)
out.WriteString("<texttable" + s + ">\n")
out.Write(header)
out.Write(body)
out.Write(footer)
out.WriteString("</texttable>\n")
}
func (options *xml2) TableRow(out *bytes.Buffer, text []byte) {
out.Write(text)
out.WriteString("\n")
}
func (options *xml2) TableHeaderCell(out *bytes.Buffer, text []byte, align, colspan int) {
if colspan > 1 {
printf(nil, "syntax not supported: TableHeaderCell: colspan=%d", colspan)
}
a := ""
switch align {
case _TABLE_ALIGNMENT_LEFT:
a = " align=\"left\""
case _TABLE_ALIGNMENT_RIGHT:
a = " align=\"right\""
default:
a = " align=\"center\""
}
out.WriteString("<ttcol" + a + ">")
writeSanitizeXML(out, text)
out.WriteString("</ttcol>\n")
}
func (options *xml2) TableCell(out *bytes.Buffer, text []byte, align, colspan int) {
if colspan > 1 {
printf(nil, "syntax not supported: TableCell: colspan=%d", colspan)
}
out.WriteString("<c>")
out.Write(text)
out.WriteString("</c>")
}
func (options *xml2) Footnotes(out *bytes.Buffer, text func() bool) {
printf(nil, "syntax not supported: Footnotes")
}
func (options *xml2) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
printf(nil, "syntax not supported: FootnoteItem")
}
func (options *xml2) Index(out *bytes.Buffer, primary, secondary []byte, prim bool) {
p := ""
if prim {
p = " primary=\"true\""
}
out.WriteString("<iref item=\"" + string(primary) + "\"" + p)
out.WriteString(" subitem=\"" + string(secondary) + "\"" + "/>")
}
func (options *xml2) Citation(out *bytes.Buffer, link, title []byte) {
if len(title) == 0 {
out.WriteString("<xref target=\"" + string(link) + "\"/>")
return
}
out.WriteString("<xref target=\"" + string(link) + "\"/>")
}
func (options *xml2) References(out *bytes.Buffer, citations map[string]*citation) {
if options.flags&XML2_STANDALONE == 0 {
return
}
// close any option section tags
for i := options.sectionLevel; i > 0; i-- {
out.WriteString("</section>\n")
options.sectionLevel--
}
switch options.docLevel {
case _DOC_FRONT_MATTER:
out.WriteString("</front>\n")
out.WriteString("<back>\n")
case _DOC_MAIN_MATTER:
out.WriteString("</middle>\n")
out.WriteString("<back>\n")
case _DOC_BACK_MATTER:
// nothing to do
}
options.docLevel = _DOC_BACK_MATTER
keys := []string{}
refi, refn, keys := countCitationsAndSort(citations)
// output <xi:include href="<references file>.xml"/>, we use file it its not empty, otherwise
// we construct one for RFCNNNN and I-D.something something.
if refi+refn > 0 {
if refn > 0 {
out.WriteString("<references title=\"Normative References\">\n")
for _, k := range keys {
c := citations[k]
if c.typ == 'n' {
if c.xml != nil {
out.Write(c.xml)
out.WriteByte('\n')
continue
}
f := referenceFile(c)
out.WriteString("<?rfc include=\"" + f + "\"?>\n")
}
}
out.WriteString("</references>\n")
}
if refi > 0 {
out.WriteString("<references title=\"Informative References\">\n")
for _, k := range keys {
c := citations[k]
if c.typ == 'i' {
// if we have raw xml, output that
if c.xml != nil {
out.Write(c.xml)
out.WriteByte('\n')
continue
}
f := referenceFile(c)
out.WriteString("<?rfc include=\"" + f + "\"?>\n")
}
}
out.WriteString("</references>\n")
}
}
}
func (options *xml2) AutoLink(out *bytes.Buffer, link []byte, kind int) {
out.WriteString("<eref target=\"")
if kind == _LINK_TYPE_EMAIL {
out.WriteString("mailto:")
}
out.Write(link)
out.WriteString("\"/>")
}
func (options *xml2) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString("<spanx style=\"verb\">")
writeEntity(out, text)
out.WriteString("</spanx>")
}
func (options *xml2) DoubleEmphasis(out *bytes.Buffer, text []byte) {
// Check for 2119 Keywords, strip emphasis from them.
s := string(text)
if _, ok := words2119[s]; ok {
out.Write(text)
return
}
out.WriteString("<spanx style=\"strong\">")
out.Write(text)
out.WriteString("</spanx>")
}
func (options *xml2) Emphasis(out *bytes.Buffer, text []byte) {
out.WriteString("<spanx style=\"emph\">")
out.Write(text)
out.WriteString("</spanx>")
}
func (options *xml2) Subscript(out *bytes.Buffer, text []byte) {
// There is no subscript
out.WriteByte('~')
out.Write(text)
out.WriteByte('~')
}
func (options *xml2) Superscript(out *bytes.Buffer, text []byte) {
// There is no superscript
out.WriteByte('^')
out.Write(text)
out.WriteByte('^')
}
func (options *xml2) Figure(out *bytes.Buffer, text []byte, caption []byte) {
// the caption is discarded here.
out.Write(text)
}
func (options *xml2) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte, subfigure bool) {
// convert to url or image wrapped in figure
ial := options.Attr()
ial.GetOrDefaultAttr("align", "center")
ial.DropAttr("type") // type may be set, but is not valid in xml 2 syntax
s := options.AttrString(ial)
if len(title) != 0 {
out.WriteString("<figure" + s + " title=\"")
title1 := sanitizeXML(title)
out.Write(title1)
out.WriteString("\">\n")
// empty artwork
out.WriteString("<artwork" + ial.Key("align") + ">" + string(link) + "</artwork>\n")
out.WriteString("<postamble>")
}
out.WriteString("<eref target=\"")
out.Write(link)
out.WriteString("\">")
out.Write(alt)
out.WriteString("</eref>")
if len(title) != 0 {
out.WriteString("</postamble>\n")
out.WriteString("</figure>\n")
}
}
func (options *xml2) LineBreak(out *bytes.Buffer) {
out.WriteString("\n<vspace/>\n")
}
func (options *xml2) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
if link[0] == '#' {
out.WriteString("<xref target=\"")
out.Write(link[1:])
out.WriteString("\"/>")
return
}
out.WriteString("<eref target=\"")
out.Write(link)
out.WriteString("\">")
out.Write(content)
out.WriteString("</eref>")
}
func (options *xml2) Abbreviation(out *bytes.Buffer, abbr, title []byte) {
out.Write(abbr)
}
func (options *xml2) RawHtmlTag(out *bytes.Buffer, tag []byte) {
// We recognize a few tags: <br/>
switch {
case bytes.Compare(tag, []byte("<br/>")) == 0:
out.WriteString("<vspace/>\n")
return
}
printf(nil, "syntax not supported: RawHtmlTag: %s", string(tag))
}
func (options *xml2) TripleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString("<spanx style=\"strong\"><spanx style=\"emph\">")
out.Write(text)
out.WriteString("</spanx></spanx>")
}
func (options *xml2) StrikeThrough(out *bytes.Buffer, text []byte) {
out.Write(text)
}
func (options *xml2) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
printf(nil, "syntax not supported: FootnoteRef")
}
func (options *xml2) Entity(out *bytes.Buffer, entity []byte) {
out.Write(entity)
}
func (options *xml2) NormalText(out *bytes.Buffer, text []byte) {
attrEscape(out, text)
}
// header and footer
func (options *xml2) DocumentHeader(out *bytes.Buffer, first bool) {
if !first || options.flags&XML2_STANDALONE == 0 {
return
}
out.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
out.WriteString("<!DOCTYPE rfc SYSTEM 'rfc2629.dtd' []>\n")
}
func (options *xml2) DocumentFooter(out *bytes.Buffer, first bool) {
if !first {
return
}
switch options.specialSection {
case _ABSTRACT:
out.WriteString("</abstract>\n\n")
case _NOTE:
out.WriteString("</note>\n\n")
}
// close any option section tags
for i := options.sectionLevel; i > 0; i-- {
out.WriteString("</section>\n")
options.sectionLevel--
}
if options.flags&XML2_STANDALONE == 0 {
return
}
switch options.docLevel {
case _DOC_FRONT_MATTER:
out.WriteString("\n</front>\n")
case _DOC_MAIN_MATTER:
out.WriteString("\n</middle>\n")
case _DOC_BACK_MATTER:
out.WriteString("\n</back>\n")
}
// Only write closing rfc if it is opened in the tite block
if options.titleBlock != nil {
out.WriteString("</rfc>\n")
}
}
func (options *xml2) DocumentMatter(out *bytes.Buffer, matter int) {
if options.flags&XML2_STANDALONE == 0 {
return
}
switch options.specialSection {
case _ABSTRACT:
out.WriteString("</abstract>\n\n")
case _NOTE:
out.WriteString("</note>\n\n")
}
// we default to frontmatter already openened in the documentHeader
for i := options.sectionLevel; i > 0; i-- {
out.WriteString("</section>\n")
options.sectionLevel--
}
switch matter {
case _DOC_FRONT_MATTER:
if options.docLevel == 0 {
out.WriteString("<front>\n")
}
case _DOC_MAIN_MATTER:
if options.docLevel == _DOC_FRONT_MATTER {
out.WriteString("\n</front>\n")
}
out.WriteString("\n<middle>\n")
case _DOC_BACK_MATTER:
if options.docLevel == _DOC_MAIN_MATTER {
out.WriteString("\n</middle>\n")
}
out.WriteString("<back>\n")
}
options.docLevel = matter
options.specialSection = 0
}