237 lines
5.8 KiB
Go
237 lines
5.8 KiB
Go
package site
|
|
|
|
import (
|
|
"html"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"aego/editor/core/code/highlight"
|
|
)
|
|
|
|
const defaultDiagFile = "demo/main.aes"
|
|
|
|
type diagGroup struct {
|
|
id, title string
|
|
codes []string
|
|
}
|
|
|
|
// diagnostics renders the code entries under e grouped by phase; false when e has none with examples.
|
|
func (b *builder) diagnostics(c *strings.Builder, e *Entry, root string) bool {
|
|
var groups []*diagGroup
|
|
byPhase := map[string]*diagGroup{}
|
|
var rest []string
|
|
for _, k := range b.kids[e.ID] {
|
|
ke := b.byID[k]
|
|
if ke.Kind != KindCode || ke.Diag == nil {
|
|
rest = append(rest, k)
|
|
continue
|
|
}
|
|
g := byPhase[ke.Diag.Phase]
|
|
if g == nil {
|
|
g = &diagGroup{id: "phase-" + slug(ke.Diag.Phase), title: ke.Diag.Phase}
|
|
byPhase[ke.Diag.Phase] = g
|
|
groups = append(groups, g)
|
|
}
|
|
g.codes = append(g.codes, k)
|
|
}
|
|
if len(groups) == 0 {
|
|
return false
|
|
}
|
|
c.WriteString(`<ul class="contents diag-index">` + "\n")
|
|
for _, g := range groups {
|
|
c.WriteString(`<li><a href="#` + esc(g.id) + `">` + esc(g.title) + `</a><span class="summary">`)
|
|
for i, k := range g.codes {
|
|
if i > 0 {
|
|
c.WriteString(" · ")
|
|
}
|
|
c.WriteString(`<a href="#` + esc(k) + `">` + esc(k) + "</a>")
|
|
}
|
|
c.WriteString("</span></li>\n")
|
|
}
|
|
c.WriteString("</ul>\n")
|
|
for _, g := range groups {
|
|
c.WriteString(`<h2 id="` + esc(g.id) + `">` + esc(g.title) + "</h2>\n")
|
|
for _, k := range g.codes {
|
|
b.diagCode(c, b.byID[k], root)
|
|
}
|
|
}
|
|
for _, k := range rest {
|
|
b.section(c, k, root, 2)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func slug(s string) string {
|
|
var b strings.Builder
|
|
dash := false
|
|
for _, r := range strings.ToLower(s) {
|
|
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
|
|
b.WriteRune(r)
|
|
dash = false
|
|
} else if !dash && b.Len() > 0 {
|
|
b.WriteByte('-')
|
|
dash = true
|
|
}
|
|
}
|
|
return strings.TrimSuffix(b.String(), "-")
|
|
}
|
|
|
|
func (b *builder) diagCode(c *strings.Builder, e *Entry, root string) {
|
|
d := e.Diag
|
|
sev := d.Severity
|
|
if sev != "warning" {
|
|
sev = "error"
|
|
}
|
|
c.WriteString(`<section class="member diag">` + "\n")
|
|
c.WriteString(`<h3 class="diag-head" id="` + esc(e.ID) + `"><a class="anchor" href="#` + esc(e.ID) + `">` + esc(e.Title) + `</a><span class="sev sev-` + sev + `">` + sev + "</span></h3>\n")
|
|
if e.Summary != "" {
|
|
c.WriteString("<p>" + inlineMD(e.Summary) + "</p>\n")
|
|
}
|
|
if d.Why != "" {
|
|
c.WriteString(`<p class="label">No example</p>` + "\n<p>" + inlineMD(d.Why) + "</p>\n")
|
|
}
|
|
if d.Bad != "" {
|
|
b.diagFiles(c, d, root, false)
|
|
if d.Message != "" {
|
|
cls := "diag-msg"
|
|
if sev == "warning" {
|
|
cls += " warning"
|
|
}
|
|
c.WriteString(`<div class="` + cls + `">` + esc(diagLine(d, e.ID)) + "</div>\n")
|
|
}
|
|
}
|
|
if d.Fix != "" {
|
|
c.WriteString("<p><strong>Fix:</strong> " + inlineMD(d.Fix) + "</p>\n")
|
|
}
|
|
if d.Good != "" {
|
|
b.diagFiles(c, d, root, true)
|
|
}
|
|
c.WriteString("</section>\n")
|
|
}
|
|
|
|
func diagLine(d *DiagInfo, code string) string {
|
|
msg := code + " " + d.Message
|
|
if d.At == "" || d.Line == 0 {
|
|
return msg
|
|
}
|
|
at := d.At + ":" + strconv.Itoa(d.Line)
|
|
if d.Col > 0 {
|
|
at += ":" + strconv.Itoa(d.Col)
|
|
}
|
|
return at + ": " + msg
|
|
}
|
|
|
|
func (b *builder) diagFiles(c *strings.Builder, d *DiagInfo, root string, good bool) {
|
|
label := "Broken"
|
|
if good {
|
|
label = "Fixed"
|
|
}
|
|
type file struct{ path, src string }
|
|
var files []file
|
|
for _, f := range d.Files {
|
|
src := f.Bad
|
|
if good {
|
|
src = f.Good
|
|
}
|
|
files = append(files, file{f.Path, src})
|
|
}
|
|
main := d.Bad
|
|
if good {
|
|
main = d.Good
|
|
}
|
|
files = append(files, file{d.File, main})
|
|
for _, f := range files {
|
|
title := label
|
|
if len(files) > 1 || f.path != defaultDiagFile {
|
|
title += " · " + f.path
|
|
}
|
|
c.WriteString(`<p class="label">` + esc(title) + "</p>\n")
|
|
body := ""
|
|
if !good && f.path == d.At && d.Line > 0 {
|
|
body = highlightMarked(f.src, d.Line, d.Col, d.EndLine, d.EndCol)
|
|
} else {
|
|
body = b.code(root, f.src)
|
|
}
|
|
c.WriteString(`<pre class="code" data-lang="aes"><code class="lang-aes">` + body + "</code></pre>\n")
|
|
}
|
|
}
|
|
|
|
// highlightMarked highlights src and marks line, underlining columns col to endCol (1-based, in runes) when known.
|
|
func highlightMarked(src string, line, col, endLine, endCol int) string {
|
|
var b strings.Builder
|
|
var spans []highlight.Span
|
|
state := highlight.Normal
|
|
lines := strings.SplitAfter(src, "\n")
|
|
for i, raw := range lines {
|
|
text := strings.TrimSuffix(raw, "\n")
|
|
spans, state = highlight.Line([]byte(text), state, spans)
|
|
if i+1 != line {
|
|
writeTokens(&b, text, spans, -1, -1)
|
|
} else {
|
|
from, to := -1, -1
|
|
if col > 0 {
|
|
from = byteCol(text, col)
|
|
to = len(text)
|
|
if endLine == line && endCol > col {
|
|
to = byteCol(text, endCol)
|
|
}
|
|
if to <= from && from < len(text) {
|
|
_, n := utf8.DecodeRuneInString(text[from:])
|
|
to = from + n
|
|
}
|
|
}
|
|
b.WriteString(`<span class="hl-line">`)
|
|
writeTokens(&b, text, spans, from, to)
|
|
b.WriteString("</span>")
|
|
}
|
|
if len(text) < len(raw) {
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func byteCol(text string, col int) int {
|
|
off := 0
|
|
for n := 1; n < col && off < len(text); n++ {
|
|
_, size := utf8.DecodeRuneInString(text[off:])
|
|
off += size
|
|
}
|
|
return off
|
|
}
|
|
|
|
func writeTokens(b *strings.Builder, text string, spans []highlight.Span, from, to int) {
|
|
cls := make([]string, len(text))
|
|
for _, sp := range spans {
|
|
for i := sp.Start; i < sp.End && i < len(text); i++ {
|
|
cls[i] = tokenClass[sp.Kind]
|
|
}
|
|
}
|
|
under := false
|
|
for i := 0; i < len(text); {
|
|
in := i >= from && i < to
|
|
if in != under {
|
|
if in {
|
|
b.WriteString(`<span class="hl-span">`)
|
|
} else {
|
|
b.WriteString("</span>")
|
|
}
|
|
under = in
|
|
}
|
|
j := i + 1
|
|
for j < len(text) && cls[j] == cls[i] && (j >= from && j < to) == in {
|
|
j++
|
|
}
|
|
chunk := html.EscapeString(text[i:j])
|
|
if cls[i] != "" {
|
|
chunk = `<span class="t-` + cls[i] + `">` + chunk + "</span>"
|
|
}
|
|
b.WriteString(chunk)
|
|
i = j
|
|
}
|
|
if under {
|
|
b.WriteString("</span>")
|
|
}
|
|
}
|