Update kjol website with C documentation
This commit is contained in:
@@ -3137,7 +3137,7 @@ const (
|
||||
|
||||
FORMULA_EDIT_BASE = "block w-full h-[30px] font-mono text-sm p-1 rounded-default box-border whitespace-pre"
|
||||
FORMULA_OVERLAY_CLS = FORMULA_EDIT_BASE + " absolute inset-0 overflow-hidden pointer-events-none border border-transparent text-ink"
|
||||
FORMULA_TEXTAREA_CLS = FORMULA_EDIT_BASE + " relative bg-transparent text-transparent caret-neutral-800 resize-none overflow-x-auto overflow-y-hidden shadow-xs border border-line-strong focus:border-sky-500 outline-hidden"
|
||||
FORMULA_TEXTAREA_CLS = FORMULA_EDIT_BASE + " relative bg-transparent text-transparent caret-ink resize-none overflow-x-auto overflow-y-hidden shadow-xs border border-line-strong focus:border-sky-500 outline-hidden"
|
||||
|
||||
formulaPlaceholder = `<span class="text-ink-faint">e.g. [Revenue] / SUM({Revenue}) * 100</span>`
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func BorderCutCornerCard(class string, children ...*vdom.VNode) *vdom.VNode {
|
||||
}
|
||||
|
||||
const cardHeader = "text-xl tracking-tight text-ink mb-5"
|
||||
const cardHeaderHR = "text-neutral-200 mt-1 mb-3"
|
||||
const cardHeaderHR = "text-line mt-1 mb-3"
|
||||
|
||||
// CardHeader renders a card title followed by a divider.
|
||||
func CardHeader(class string, children ...*vdom.VNode) *vdom.VNode {
|
||||
|
||||
174
go/webui/code.go
174
go/webui/code.go
@@ -1,174 +0,0 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Go syntax highlighting, for the code samples a documentation page shows.
|
||||
//
|
||||
// It is a LEXER, not a parser: it classifies tokens and gives up gracefully on anything
|
||||
// it does not understand, because a highlighter that can fail to render is worse than
|
||||
// one that occasionally paints an identifier the wrong colour. Unterminated strings and
|
||||
// comments run to the end of the input rather than throwing.
|
||||
//
|
||||
// Output is HTML, and every run of source text passes through html.EscapeString on the
|
||||
// way out — the input is Go source, which is full of `<`, `>` and `&`, and one of the
|
||||
// snippets this is meant to display is literally a block of HTML.
|
||||
|
||||
// A code block is dark in BOTH themes — a light code block on a light page is a
|
||||
// different kind of thing, and switching it with the theme means the snippet you were
|
||||
// reading changes colour under you. So these are fixed on-dark colours, not theme
|
||||
// tokens: the surface they sit on never changes.
|
||||
const (
|
||||
goCommentClass = "text-neutral-400"
|
||||
goStringClass = "text-emerald-300"
|
||||
goKeywordClass = "text-sky-300"
|
||||
goNumberClass = "text-amber-300"
|
||||
goFuncClass = "text-violet-300"
|
||||
)
|
||||
|
||||
var goKeywords = map[string]bool{
|
||||
"break": true, "case": true, "chan": true, "const": true, "continue": true,
|
||||
"default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
|
||||
"func": true, "go": true, "goto": true, "if": true, "import": true,
|
||||
"interface": true, "map": true, "package": true, "range": true, "return": true,
|
||||
"select": true, "struct": true, "switch": true, "type": true, "var": true,
|
||||
// Not keywords to the Go spec — predeclared identifiers — but every editor colours
|
||||
// them, and a reader looking for `nil` is looking for the same kind of thing.
|
||||
"nil": true, "true": true, "false": true, "iota": true,
|
||||
"string": true, "int": true, "int64": true, "float64": true, "bool": true,
|
||||
"byte": true, "rune": true, "any": true, "error": true,
|
||||
}
|
||||
|
||||
// HighlightGo turns Go source into HTML with the tokens wrapped in coloured spans.
|
||||
//
|
||||
// The result is meant for vdom.Raw inside a <pre>: it contains no block elements and
|
||||
// preserves the source's whitespace exactly, so the <pre> does the layout.
|
||||
func HighlightGo(src string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(src) * 2)
|
||||
|
||||
i := 0
|
||||
for i < len(src) {
|
||||
c := src[i]
|
||||
|
||||
switch {
|
||||
// Line comment — including the //gowasm: directives, which are the most
|
||||
// important line in several of these snippets.
|
||||
case c == '/' && i+1 < len(src) && src[i+1] == '/':
|
||||
end := strings.IndexByte(src[i:], '\n')
|
||||
if end < 0 {
|
||||
end = len(src)
|
||||
} else {
|
||||
end += i
|
||||
}
|
||||
span(&b, goCommentClass, src[i:end])
|
||||
i = end
|
||||
|
||||
// Block comment.
|
||||
case c == '/' && i+1 < len(src) && src[i+1] == '*':
|
||||
end := strings.Index(src[i+2:], "*/")
|
||||
if end < 0 {
|
||||
end = len(src)
|
||||
} else {
|
||||
end = i + 2 + end + 2
|
||||
}
|
||||
span(&b, goCommentClass, src[i:end])
|
||||
i = end
|
||||
|
||||
// Interpreted string. Ends at the closing quote or the line's end — an
|
||||
// unterminated string is a typo in a snippet, not a reason to paint the rest of
|
||||
// the file green.
|
||||
case c == '"':
|
||||
i = quoted(&b, src, i, '"', true)
|
||||
|
||||
// Raw string: no escapes, and it may span lines.
|
||||
case c == '`':
|
||||
i = quoted(&b, src, i, '`', false)
|
||||
|
||||
// Rune literal.
|
||||
case c == '\'':
|
||||
i = quoted(&b, src, i, '\'', true)
|
||||
|
||||
case isDigit(c):
|
||||
j := i
|
||||
for j < len(src) && (isDigit(src[j]) || isHexish(src[j])) {
|
||||
j++
|
||||
}
|
||||
span(&b, goNumberClass, src[i:j])
|
||||
i = j
|
||||
|
||||
case isIdentStart(c):
|
||||
j := i
|
||||
for j < len(src) && isIdentPart(src[j]) {
|
||||
j++
|
||||
}
|
||||
word := src[i:j]
|
||||
switch {
|
||||
case goKeywords[word]:
|
||||
span(&b, goKeywordClass, word)
|
||||
case callAhead(src, j):
|
||||
// An identifier immediately followed by "(" is being called (or is a type
|
||||
// being converted to). Colouring it is what makes the shape of a snippet
|
||||
// readable at a glance.
|
||||
span(&b, goFuncClass, word)
|
||||
default:
|
||||
b.WriteString(html.EscapeString(word))
|
||||
}
|
||||
i = j
|
||||
|
||||
default:
|
||||
b.WriteString(html.EscapeString(string(c)))
|
||||
i++
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// quoted consumes a quoted literal starting at i and writes it as a string span.
|
||||
// escapes reports whether a backslash escapes the next byte (false for raw strings).
|
||||
func quoted(b *strings.Builder, src string, i int, quote byte, escapes bool) int {
|
||||
j := i + 1
|
||||
for j < len(src) {
|
||||
if escapes && src[j] == '\\' && j+1 < len(src) {
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
if src[j] == quote {
|
||||
j++
|
||||
break
|
||||
}
|
||||
if escapes && src[j] == '\n' {
|
||||
break // unterminated: stop at the line end rather than eating the file
|
||||
}
|
||||
j++
|
||||
}
|
||||
span(b, goStringClass, src[i:j])
|
||||
return j
|
||||
}
|
||||
|
||||
// callAhead reports whether the next non-space byte at or after i is an opening paren.
|
||||
func callAhead(src string, i int) bool {
|
||||
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
return i < len(src) && src[i] == '('
|
||||
}
|
||||
|
||||
func span(b *strings.Builder, class, text string) {
|
||||
b.WriteString(`<span class="`)
|
||||
b.WriteString(class)
|
||||
b.WriteString(`">`)
|
||||
b.WriteString(html.EscapeString(text))
|
||||
b.WriteString(`</span>`)
|
||||
}
|
||||
|
||||
// isDigit already exists in the package (autotable.go) — reused rather than shadowed.
|
||||
|
||||
func isHexish(c byte) bool {
|
||||
return c == '.' || c == 'x' || c == 'X' || c == '_' ||
|
||||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
||||
}
|
||||
func isIdentStart(c byte) bool { return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') }
|
||||
func isIdentPart(c byte) bool { return isIdentStart(c) || isDigit(c) }
|
||||
@@ -1,99 +0,0 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The highlighter emits HTML, and its input is Go source — which is full of <, > and &.
|
||||
// Anything that reaches the page unescaped is markup injection into your own docs page:
|
||||
// a snippet containing `<div>` would render a div.
|
||||
func TestHighlightGoEscapes(t *testing.T) {
|
||||
got := HighlightGo(`s := "<div class=\"x\">" // a & b`)
|
||||
|
||||
if strings.Contains(got, "<div") {
|
||||
t.Errorf("a `<div>` in the SOURCE reached the output as markup:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "<div") {
|
||||
t.Errorf("the angle bracket was not escaped:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "&") {
|
||||
t.Errorf("the ampersand was not escaped:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighlightGoClassifies(t *testing.T) {
|
||||
got := HighlightGo("func main() { x := 42 // note\n}")
|
||||
|
||||
for _, want := range []struct{ what, class, text string }{
|
||||
{"keyword", goKeywordClass, "func"},
|
||||
{"call", goFuncClass, "main"},
|
||||
{"number", goNumberClass, "42"},
|
||||
{"comment", goCommentClass, "// note"},
|
||||
} {
|
||||
if !strings.Contains(got, `<span class="`+want.class+`">`+want.text+`</span>`) {
|
||||
t.Errorf("%s %q was not highlighted:\n%s", want.what, want.text, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The //gowasm: directives are the most important line in half these snippets. They are
|
||||
// comments, and must survive as such.
|
||||
func TestHighlightGoKeepsDirectives(t *testing.T) {
|
||||
got := HighlightGo("//gowasm:page / static layout=public\nfunc HomePage() {}")
|
||||
if !strings.Contains(got, `<span class="`+goCommentClass+`">//gowasm:page / static layout=public</span>`) {
|
||||
t.Errorf("the directive was not kept whole as a comment:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A lexer that can hang or eat the rest of the file on malformed input would take the
|
||||
// whole page down with it. Unterminated literals stop; they do not run away.
|
||||
func TestHighlightGoSurvivesMalformedInput(t *testing.T) {
|
||||
for _, src := range []string{
|
||||
`x := "unterminated`,
|
||||
"y := `unterminated raw",
|
||||
"/* unterminated block",
|
||||
`z := '`,
|
||||
"",
|
||||
} {
|
||||
got := HighlightGo(src)
|
||||
// The text must all still be there — mangling is not an acceptable failure mode
|
||||
// either. Compare on the visible characters, ignoring the spans.
|
||||
if plain := stripTags(got); plain != src {
|
||||
t.Errorf("input %q came out as %q", src, plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is dropped: every byte of the source is still on the page, in order.
|
||||
func TestHighlightGoIsLossless(t *testing.T) {
|
||||
src := "package app\n\nimport \"strings\"\n\nfunc f(n int) string {\n\treturn strings.Repeat(\"x\", n) // pad\n}\n"
|
||||
if plain := stripTags(HighlightGo(src)); plain != src {
|
||||
t.Errorf("the highlighter changed the source.\n got: %q\nwant: %q", plain, src)
|
||||
}
|
||||
}
|
||||
|
||||
// stripTags removes the spans and unescapes, recovering the original source.
|
||||
func stripTags(s string) string {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] == '<' {
|
||||
j := strings.IndexByte(s[i:], '>')
|
||||
if j < 0 {
|
||||
break
|
||||
}
|
||||
i += j + 1
|
||||
continue
|
||||
}
|
||||
b.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
out := b.String()
|
||||
// Reverse html.EscapeString, innermost last.
|
||||
out = strings.ReplaceAll(out, "<", "<")
|
||||
out = strings.ReplaceAll(out, ">", ">")
|
||||
out = strings.ReplaceAll(out, """, `"`)
|
||||
out = strings.ReplaceAll(out, "'", "'")
|
||||
out = strings.ReplaceAll(out, "&", "&")
|
||||
return out
|
||||
}
|
||||
@@ -11,7 +11,7 @@ func PageContainer(children ...*vdom.VNode) *vdom.VNode {
|
||||
}
|
||||
|
||||
// Divider is a thin horizontal rule.
|
||||
func Divider() *vdom.VNode { return vdom.Hr(vdom.Attr("class", "text-neutral-200 mt-1 mb-3")) }
|
||||
func Divider() *vdom.VNode { return vdom.Hr(vdom.Attr("class", "text-line mt-1 mb-3")) }
|
||||
|
||||
// CodeBox renders a dark monospace code block.
|
||||
func CodeBox(code, class string) *vdom.VNode {
|
||||
@@ -25,7 +25,7 @@ func PageHeader(text, class string) *vdom.VNode {
|
||||
return vdom.Header(vdom.Attr("class", class),
|
||||
vdom.Div(vdom.Attr("class", "mt-1"),
|
||||
vdom.H1(vdom.Attr("class", "text-center text-2xl font-light text-ink mb-2"), vdom.Text(text)),
|
||||
vdom.Hr(vdom.Attr("class", "text-neutral-200 mb-2")),
|
||||
vdom.Hr(vdom.Attr("class", "text-line mb-2")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user