320 lines
8.2 KiB
Go
320 lines
8.2 KiB
Go
package mmark
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// xml2rfc.go contains common code and variables that is shared
|
|
// between xml2rfcv[23].go.
|
|
|
|
var (
|
|
// These have been known to change, these are the current ones (2015-08-27).
|
|
CitationsBase = "https://xml2rfc.tools.ietf.org/public/rfc/"
|
|
|
|
// CitationsRFC is the URL where the citations for RFCs are.
|
|
CitationsRFC = CitationsBase + "bibxml/"
|
|
|
|
// CitationsANSI is the URL where the citations for ANSI documents are.
|
|
CitationsANSI = CitationsBase + "bibxml2/"
|
|
|
|
// CitationsID is the URL where the citations for I-Ds are.
|
|
CitationsID = CitationsBase + "bibxml3/"
|
|
|
|
// CitationsW3C is the URL where the citations for W3C documents are.
|
|
CitationsW3C = CitationsBase + "bibxml4/"
|
|
)
|
|
|
|
const (
|
|
referenceRFC = "reference.RFC."
|
|
referenceID = "reference.I-D.draft-"
|
|
referenceIDLatest = "reference.I-D."
|
|
referenceW3C = "reference."
|
|
referenceANSI = "reference."
|
|
ext = ".xml"
|
|
)
|
|
|
|
// referenceFile creates a .xml filename for the citation c.
|
|
// For I-D references like '[@?I-D.ietf-dane-openpgpkey#02]' it will
|
|
// create http://<CitationsID>/reference.I-D.draft-ietf-dane-openpgpkey-02.xml
|
|
// without an sequence number it becomes:
|
|
// http://<CitationsID>/reference.I-D.ietf-dane-openpgpkey.xml
|
|
func referenceFile(c *citation) string {
|
|
if len(c.link) < 4 {
|
|
return ""
|
|
}
|
|
switch {
|
|
case bytes.HasPrefix(c.link, []byte("RFC")):
|
|
return CitationsRFC + referenceRFC + string(c.link[3:]) + ext
|
|
case bytes.HasPrefix(c.link, []byte("I-D")):
|
|
seq := ""
|
|
if c.seq != -1 {
|
|
seq = "-" + fmt.Sprintf("%02d", c.seq)
|
|
return CitationsID + referenceID + string(c.link[4:]) + seq + ext
|
|
}
|
|
return CitationsID + referenceIDLatest + string(c.link[4:]) + ext
|
|
case bytes.HasPrefix(c.link, []byte("W3C")):
|
|
return CitationsW3C + referenceW3C + string(c.link) + ext
|
|
case bytes.HasPrefix(c.link, []byte("ANSI")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("CCITT")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("FIPS")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("IEEE")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("ISO")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("ITU")):
|
|
fallthrough
|
|
case bytes.HasPrefix(c.link, []byte("PKCS")):
|
|
return CitationsANSI + referenceANSI + string(c.link) + ext
|
|
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// countCitationsAndSort returns the number of informative and normative
|
|
// references and a string slice with the sorted keys.
|
|
func countCitationsAndSort(citations map[string]*citation) (int, int, []string) {
|
|
keys := make([]string, 0, len(citations))
|
|
refi, refn := 0, 0
|
|
for k, c := range citations {
|
|
if c.typ == 'i' {
|
|
refi++
|
|
}
|
|
if c.typ == 'n' {
|
|
refn++
|
|
}
|
|
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return refi, refn, keys
|
|
}
|
|
|
|
var entityConvert = map[byte][]byte{
|
|
'<': []byte("<"),
|
|
'>': []byte(">"),
|
|
'&': []byte("&"),
|
|
// '\'': []byte("'"),
|
|
// '"': []byte("""),
|
|
}
|
|
|
|
func writeEntity(out *bytes.Buffer, text []byte) {
|
|
for i := 0; i < len(text); i++ {
|
|
if s, ok := entityConvert[text[i]]; ok {
|
|
out.Write(s)
|
|
continue
|
|
}
|
|
out.WriteByte(text[i])
|
|
}
|
|
}
|
|
|
|
// sanitizeXML strips XML from a string.
|
|
func sanitizeXML(s []byte) []byte {
|
|
inTag := false
|
|
j := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if s[i] == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
s[j] = s[i]
|
|
j++
|
|
}
|
|
}
|
|
return s[:j]
|
|
}
|
|
|
|
// writeSanitizeXML strips XML from a string and writes
|
|
// to out.
|
|
func writeSanitizeXML(out *bytes.Buffer, s []byte) {
|
|
inTag := false
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if s[i] == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
out.WriteByte(s[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// titleBlockTOMLAuthor outputs the author from the TOML title block.
|
|
func titleBlockTOMLAuthor(out *bytes.Buffer, a author) {
|
|
out.WriteString("<author")
|
|
|
|
if a.Role != "" {
|
|
out.WriteString(" role=\"")
|
|
writeEntity(out, []byte(a.Role))
|
|
out.WriteString("\"")
|
|
}
|
|
|
|
out.WriteString(" initials=\"")
|
|
writeEntity(out, []byte(a.Initials))
|
|
out.WriteString("\"")
|
|
|
|
out.WriteString(" surname=\"")
|
|
writeEntity(out, []byte(a.Surname))
|
|
out.WriteString("\"")
|
|
|
|
out.WriteString(" fullname=\"")
|
|
writeEntity(out, []byte(a.Fullname))
|
|
out.WriteString("\">\n")
|
|
|
|
abbrev := ""
|
|
if a.OrganizationAbbrev != "" {
|
|
abbrev = " abbrev=\"" + a.OrganizationAbbrev + "\""
|
|
}
|
|
out.WriteString("<organization")
|
|
writeEntity(out, []byte(abbrev))
|
|
out.WriteString(">")
|
|
writeEntity(out, []byte(a.Organization))
|
|
out.WriteString("</organization>\n")
|
|
|
|
out.WriteString("<address>\n")
|
|
out.WriteString("<postal>\n")
|
|
|
|
out.WriteString("<street>")
|
|
writeEntity(out, []byte(a.Address.Postal.Street))
|
|
out.WriteString("</street>\n")
|
|
for _, street := range a.Address.Postal.Streets {
|
|
out.WriteString("<street>")
|
|
writeEntity(out, []byte(street))
|
|
out.WriteString("</street>\n")
|
|
}
|
|
|
|
out.WriteString("<city>")
|
|
writeEntity(out, []byte(a.Address.Postal.City))
|
|
out.WriteString("</city>\n")
|
|
for _, city := range a.Address.Postal.Cities {
|
|
out.WriteString("<city>")
|
|
writeEntity(out, []byte(city))
|
|
out.WriteString("</city>\n")
|
|
}
|
|
|
|
out.WriteString("<code>")
|
|
writeEntity(out, []byte(a.Address.Postal.Code))
|
|
out.WriteString("</code>\n")
|
|
for _, code := range a.Address.Postal.Codes {
|
|
out.WriteString("<code>")
|
|
writeEntity(out, []byte(code))
|
|
out.WriteString("</code>\n")
|
|
}
|
|
|
|
out.WriteString("<country>")
|
|
writeEntity(out, []byte(a.Address.Postal.Country))
|
|
out.WriteString("</country>\n")
|
|
for _, country := range a.Address.Postal.Countries {
|
|
out.WriteString("<country>")
|
|
writeEntity(out, []byte(country))
|
|
out.WriteString("</country>\n")
|
|
}
|
|
|
|
out.WriteString("<region>")
|
|
writeEntity(out, []byte(a.Address.Postal.Region))
|
|
out.WriteString("</region>\n")
|
|
for _, region := range a.Address.Postal.Regions {
|
|
out.WriteString("<region>")
|
|
writeEntity(out, []byte(region))
|
|
out.WriteString("</region>\n")
|
|
}
|
|
|
|
out.WriteString("</postal>\n")
|
|
|
|
out.WriteString("<phone>" + a.Address.Phone + "</phone>\n")
|
|
out.WriteString("<email>" + a.Address.Email + "</email>\n")
|
|
out.WriteString("<uri>" + a.Address.Uri + "</uri>\n")
|
|
|
|
out.WriteString("</address>\n")
|
|
out.WriteString("</author>\n")
|
|
}
|
|
|
|
// titleBlockTOMLDate outputs the date from the TOML title block.
|
|
func titleBlockTOMLDate(out *bytes.Buffer, d time.Time) {
|
|
year := ""
|
|
if d.Year() > 0 {
|
|
year = " year=\"" + strconv.Itoa(d.Year()) + "\""
|
|
}
|
|
month := ""
|
|
if d.Month() > 0 {
|
|
month = " month=\"" + time.Month(d.Month()).String() + "\""
|
|
}
|
|
day := ""
|
|
if d.Day() > 0 {
|
|
day = " day=\"" + strconv.Itoa(d.Day()) + "\""
|
|
}
|
|
out.WriteString("<date" + year + month + day + "/>\n\n")
|
|
}
|
|
|
|
// titleBlockTOMLKeyword outputs the keywords from the TOML title block.
|
|
func titleBlockTOMLKeyword(out *bytes.Buffer, keywords []string) {
|
|
for _, k := range keywords {
|
|
out.WriteString("<keyword>" + k + "</keyword>\n")
|
|
}
|
|
}
|
|
|
|
// titleBlockTOMLPI returns "yes" or "no" or a stringified number
|
|
// for use as process instruction. If version is 3 they are returned
|
|
// as attributes for use *inside* the <rfc> tag.
|
|
func titleBlockTOMLPI(pi pi, name string, version int) string {
|
|
if version == 2 {
|
|
switch name {
|
|
case "toc":
|
|
return "<?rfc toc=\"" + yesno(pi.Toc, "yes") + "\"?>\n"
|
|
case "symrefs":
|
|
return "<?rfc symrefs=\"" + yesno(pi.Symrefs, "yes") + "\"?>\n"
|
|
case "sortrefs":
|
|
return "<?rfc sortrefs=\"" + yesno(pi.Sortrefs, "yes") + "\"?>\n"
|
|
case "compact":
|
|
return "<?rfc compact=\"" + yesno(pi.Compact, "yes") + "\"?>\n"
|
|
case "topblock":
|
|
return "<?rfc topblock=\"" + yesno(pi.Topblock, "yes") + "\"?>\n"
|
|
case "comments":
|
|
return "<?rfc comments=\"" + yesno(pi.Comments, "no") + "\"?>\n"
|
|
case "subcompact":
|
|
return "<?rfc subcompact=\"" + yesno(pi.Subcompact, "no") + "\"?>\n"
|
|
case "private":
|
|
return "<?rfc private=\"" + yesno(pi.Private, "") + "\"?>\n"
|
|
case "header":
|
|
if pi.Header == piNotSet {
|
|
return ""
|
|
}
|
|
return "<?rfc header=\"" + pi.Header + "\"?>\n"
|
|
case "footer":
|
|
if pi.Footer == piNotSet {
|
|
return ""
|
|
}
|
|
return "<?rfc footer=\"" + pi.Footer + "\"?>\n"
|
|
default:
|
|
printf(nil, "unhandled or unknown PI seen: %s", name)
|
|
return ""
|
|
}
|
|
}
|
|
// version 3
|
|
return ""
|
|
}
|
|
|
|
func yesno(s, def string) string {
|
|
if s == "" {
|
|
return def
|
|
}
|
|
if s == "yes" {
|
|
return "yes"
|
|
}
|
|
return "no"
|
|
}
|