blob: 057ccec7402041d890b73f0cde977e7908d377f9 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package render
import (
"fmt"
"go/doc/comment"
"regexp"
"strings"
"unicode"
safe "github.com/google/safehtml"
"github.com/google/safehtml/template"
)
type link struct {
Class string
Href string
Text any // string or safe.HTML
}
type heading struct {
ID safe.Identifier
Title safe.HTML
}
var (
// tocTemplate expects a []heading.
tocTemplate = template.Must(template.New("toc").Parse(`<div role="navigation" aria-label="Table of Contents">
<ul class="Documentation-toc{{if gt (len .) 5}} Documentation-toc-columns{{end}}">
{{range . -}}
<li class="Documentation-tocItem">
<a href="#{{.ID}}">{{.Title}}</a>
</li>
{{end -}}
</ul>
</div>
`))
italicTemplate = template.Must(template.New("italics").Parse(`<i>{{.}}</i>`))
codeTemplate = template.Must(template.New("code").Parse(`<pre>{{.}}</pre>`))
paraTemplate = template.Must(template.New("para").Parse("<p>{{.}}\n</p>"))
headingTemplate = template.Must(template.New("heading").Parse(
`<h4 id="{{.ID}}">{{.Title}} <a class="Documentation-idLink" href="#{{.ID}}">ΒΆ</a></h4>`))
linkTemplate = template.Must(template.New("link").Parse(
`<a{{with .Class}}class="{{.}}" {{end}} href="{{.Href}}">{{.Text}}</a>`))
uListTemplate = template.Must(template.New("ulist").Parse(
`<ul>
{{- range .}}
{{.}}
{{- end}}
</ul>`))
oListTemplate = template.Must(template.New("olist").Parse(
`<ol>
{{range .}}
{{.}}
{{end}}
</ol>`))
listItemTemplate = template.Must(template.New("li").Parse(
`<li{{with .Number}}value="{{.}}" {{end}}>{{.Content}}</li>`))
)
func (r *Renderer) formatDocHTML(text string, extractLinks bool) safe.HTML {
p := comment.Parser{}
doc := p.Parse(text)
if extractLinks {
r.removeLinks(doc)
}
var headings []heading
for _, b := range doc.Content {
if h, ok := b.(*comment.Heading); ok {
headings = append(headings, r.newHeading(h))
}
}
h := r.blocksToHTML(doc.Content, true, extractLinks)
if r.enableCommandTOC && len(headings) > 0 {
h = safe.HTMLConcat(ExecuteToHTML(tocTemplate, headings), h)
}
return h
}
func (r *Renderer) removeLinks(doc *comment.Doc) {
var bs []comment.Block
inLinks := false
for _, b := range doc.Content {
switch b := b.(type) {
case *comment.Heading:
if textsToString(b.Text) == "Links" {
inLinks = true
} else {
inLinks = false
bs = append(bs, b)
}
case *comment.List:
if inLinks {
for _, item := range b.Items {
fmt.Println(" ", item)
if link, ok := itemLink(item); ok {
r.links = append(r.links, link)
}
}
} else {
bs = append(bs, b)
}
case *comment.Paragraph:
if inLinks {
// Links section doesn't require leading whitespace, so
// the link may be in a paragraph.
s := textsToString(b.Text)
r.links = append(r.links, parseLinks(strings.Split(s, "\n"))...)
} else {
bs = append(bs, b)
}
default:
if !inLinks {
bs = append(bs, b)
}
}
}
doc.Content = bs
}
func itemLink(item *comment.ListItem) (l Link, ok bool) {
// Should be a single Paragraph.
if len(item.Content) != 1 {
return l, false
}
p, ok := item.Content[0].(*comment.Paragraph)
if !ok {
return l, false
}
// TODO: clean up.
if lp := parseLink("- " + textsToString(p.Text)); lp != nil {
return *lp, true
}
return l, false
}
func (r *Renderer) blocksToHTML(bs []comment.Block, useParagraph, extractLinks bool) safe.HTML {
return concatHTML(bs, func(b comment.Block) safe.HTML {
return r.blockToHTML(b, useParagraph, extractLinks)
})
}
func (r *Renderer) blockToHTML(b comment.Block, useParagraph, extractLinks bool) safe.HTML {
switch b := b.(type) {
case *comment.Paragraph:
th := r.textsToHTML(b.Text)
if useParagraph {
return ExecuteToHTML(paraTemplate, th)
}
return th
case *comment.Code:
return ExecuteToHTML(codeTemplate, b.Text)
case *comment.Heading:
return ExecuteToHTML(headingTemplate, r.newHeading(b))
case *comment.List:
var items []safe.HTML
useParagraph = b.BlankBetween()
for _, item := range b.Items {
items = append(items, ExecuteToHTML(listItemTemplate, struct {
Number string
Content safe.HTML
}{item.Number, r.blocksToHTML(item.Content, useParagraph, false)}))
}
t := oListTemplate
if b.Items[0].Number == "" {
t = uListTemplate
}
return ExecuteToHTML(t, items)
default:
return badType(b)
}
}
func (r *Renderer) newHeading(h *comment.Heading) heading {
return heading{headingID(h), r.textsToHTML(h.Text)}
}
func (r *Renderer) textsToHTML(ts []comment.Text) safe.HTML {
return concatHTML(ts, r.textToHTML)
}
func (r *Renderer) textToHTML(t comment.Text) safe.HTML {
switch t := t.(type) {
case comment.Plain:
// Don't auto-link URLs. The doc/comment package already does that.
return linkRFCs(string(t))
case comment.Italic:
return ExecuteToHTML(italicTemplate, t)
case *comment.Link:
return ExecuteToHTML(linkTemplate, link{"", t.URL, r.textsToHTML(t.Text)})
case *comment.DocLink:
url := r.docLinkURL(t)
return ExecuteToHTML(linkTemplate, link{"", url, r.textsToHTML(t.Text)})
default:
return badType(t)
}
}
func (r *Renderer) docLinkURL(dl *comment.DocLink) string {
var url string
if dl.ImportPath != "" {
url = "/" + dl.ImportPath
if r.packageURL != nil {
url = r.packageURL(dl.ImportPath)
}
}
id := dl.Name
if dl.Recv != "" {
id = dl.Recv + "." + id
}
if id != "" {
url += "#" + id
}
return url
}
// TODO: any -> *comment.Text | *comment.Block
func concatHTML[T any](xs []T, toHTML func(T) safe.HTML) safe.HTML {
var hs []safe.HTML
for _, x := range xs {
hs = append(hs, toHTML(x))
}
return safe.HTMLConcat(hs...)
}
func badType(x interface{}) safe.HTML {
return safe.HTMLEscaped(fmt.Sprintf("bad type %T", x))
}
func headingID(h *comment.Heading) safe.Identifier {
s := textsToString(h.Text)
id := badAnchorRx.ReplaceAllString(s, "_")
return safe.IdentifierFromConstantPrefix("hdr", id)
}
func textsToString(ts []comment.Text) string {
var b strings.Builder
for _, t := range ts {
switch t := t.(type) {
case comment.Plain:
b.WriteString(string(t))
case comment.Italic:
b.WriteString(string(t))
case *comment.Link:
b.WriteString(textsToString(t.Text))
case *comment.DocLink:
b.WriteString(textsToString(t.Text))
default:
fmt.Fprintf(&b, "bad text type %T", t)
}
}
return b.String()
}
var rfcRegexp = regexp.MustCompile(rfcRx)
// TODO: merge/replace Renderer.formatLineHTML.
// TODO: make more efficient.
func linkRFCs(s string) safe.HTML {
var hs []safe.HTML
for len(s) > 0 {
m0, m1 := len(s), len(s)
if m := rfcRegexp.FindStringIndex(s); m != nil {
m0, m1 = m[0], m[1]
}
if m0 > 0 {
hs = append(hs, safe.HTMLEscaped(s[:m0]))
}
if m1 > m0 {
word := s[m0:m1]
// Strip all characters except for letters, numbers, and '.' to
// obtain RFC fields.
rfcFields := strings.FieldsFunc(word, func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '.'
})
var url string
if len(rfcFields) >= 4 {
// RFC x Section y
url = fmt.Sprintf("https://rfc-editor.org/rfc/rfc%s.html#section-%s",
rfcFields[1], rfcFields[3])
} else if len(rfcFields) >= 2 {
url = fmt.Sprintf("https://rfc-editor.org/rfc/rfc%s.html", rfcFields[1])
}
if url != "" {
hs = append(hs, ExecuteToHTML(linkTemplate, link{"", url, word}))
}
}
s = s[m1:]
}
return safe.HTMLConcat(hs...)
}