blob: ce0e5bc7ac459c7bd760b8f50c6b681cb8a84d56 [file] [log] [blame]
// Copyright 2024 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 golang
// This file defines a simple HTML rendering of package documentation
// in imitation of the style of pkg.go.dev.
//
// The current implementation is just a starting point and a
// placeholder for a more sophisticated one.
//
// TODO(adonovan):
// - rewrite using html/template.
// Or factor with golang.org/x/pkgsite/internal/godoc/dochtml.
// - emit breadcrumbs for parent + sibling packages.
// - list promoted methods---we have type information!
// - gather Example tests, following go/doc and pkgsite.
// - add option for doc.AllDecls: show non-exported symbols too.
// - abbreviate long signatures by replacing parameters 4 onwards with "...".
// - style the <li> bullets in the index as invisible.
// - add push notifications such as didChange -> reload.
// - there appears to be a maximum file size beyond which the
// "source.doc" code action is not offered. Remove that.
// - modify JS httpGET function to give a transient visual indication
// when clicking a source link that the editor is being navigated
// (in case it doesn't raise itself, like VS Code).
// - move this into a new package, golang/pkgdoc, and then
// split out the various helpers without fear of polluting
// the golang package namespace.
// - show "Deprecated" chip when appropriate.
import (
"bytes"
"fmt"
"go/ast"
"go/doc"
"go/doc/comment"
"go/format"
"go/token"
"go/types"
"html"
"path/filepath"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/typesinternal"
)
// TODO(adonovan): factor these two functions into an interface.
type (
// A PkgURLFunc forms URLs of package or symbol documentation.
PkgURLFunc = func(path PackagePath, fragment string) protocol.URI
// A PosURLFunc forms URLs that cause the editor to navigate to a position.
PosURLFunc = func(filename string, line, col8 int) protocol.URI
)
// RenderPackageDoc formats the package documentation page.
//
// The posURL function returns a URL that when visited, has the side
// effect of causing gopls to direct the client editor to navigate to
// the specified file/line/column position, in UTF-8 coordinates.
//
// The pkgURL function returns a URL for the documentation of the
// specified package and symbol.
//
// TODO(adonovan): "Render" is a client-side verb; rename to PackageDocHTML.
func RenderPackageDoc(pkg *cache.Package, posURL PosURLFunc, pkgURL PkgURLFunc) ([]byte, error) {
// We can't use doc.NewFromFiles (even with doc.PreserveAST
// mode) as it calls ast.NewPackage which assumes that each
// ast.File has an ast.Scope and resolves identifiers to
// (deprecated) ast.Objects. (This is golang/go#66290.)
// But doc.New only requires pkg.{Name,Files},
// so we just boil it down.
//
// The only loss is doc.classifyExamples.
// TODO(adonovan): simulate that too.
fileMap := make(map[string]*ast.File)
for _, f := range pkg.Syntax() {
fileMap[pkg.FileSet().File(f.Pos()).Name()] = f
}
astpkg := &ast.Package{
Name: pkg.Types().Name(),
Files: fileMap,
}
// PreserveAST mode only half works (golang/go#66449): it still
// mutates ASTs when filtering out non-exported symbols.
// As a workaround, enable AllDecls to suppress filtering,
// and do it ourselves.
mode := doc.PreserveAST | doc.AllDecls
docpkg := doc.New(astpkg, pkg.Types().Path(), mode)
// Discard non-exported symbols.
// TODO(adonovan): do this conditionally, and expose option in UI.
const showUnexported = false
if !showUnexported {
var (
unexported = func(name string) bool { return !token.IsExported(name) }
filterValues = func(slice *[]*doc.Value) {
delValue := func(v *doc.Value) bool {
v.Names = slices.DeleteFunc(v.Names, unexported)
return len(v.Names) == 0
}
*slice = slices.DeleteFunc(*slice, delValue)
}
filterFuncs = func(funcs *[]*doc.Func) {
*funcs = slices.DeleteFunc(*funcs, func(v *doc.Func) bool {
return unexported(v.Name)
})
}
)
filterValues(&docpkg.Consts)
filterValues(&docpkg.Vars)
filterFuncs(&docpkg.Funcs)
docpkg.Types = slices.DeleteFunc(docpkg.Types, func(t *doc.Type) bool {
filterValues(&t.Consts)
filterValues(&t.Vars)
filterFuncs(&t.Funcs)
filterFuncs(&t.Methods)
return unexported(t.Name)
})
}
var docHTML func(comment string) []byte
{
// Adapt doc comment parser and printer
// to our representation of Go packages
// so that doc links (e.g. "[fmt.Println]")
// become valid links.
printer := docpkg.Printer()
printer.DocLinkURL = func(link *comment.DocLink) string {
path := pkg.Metadata().PkgPath
if link.ImportPath != "" {
path = PackagePath(link.ImportPath)
}
fragment := link.Name
if link.Recv != "" {
fragment = link.Recv + "." + link.Name
}
return pkgURL(path, fragment)
}
parser := docpkg.Parser()
parser.LookupPackage = func(name string) (importPath string, ok bool) {
// Ambiguous: different files in the same
// package may have different import mappings,
// but the hook doesn't provide the file context.
// TODO(adonovan): conspire with docHTML to
// pass the doc comment's enclosing file through
// a shared variable, so that we can compute
// the correct per-file mapping.
//
// TODO(adonovan): check for PkgName.Name
// matches, but also check for
// PkgName.Imported.Namer matches, since some
// packages are typically imported under a
// non-default name (e.g. pathpkg "path") but
// may be referred to in doc links using their
// canonical name.
for _, f := range pkg.Syntax() {
for _, imp := range f.Imports {
pkgName, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), imp)
if ok && pkgName.Name() == name {
return pkgName.Imported().Path(), true
}
}
}
return "", false
}
parser.LookupSym = func(recv, name string) (ok bool) {
// package-level decl?
if recv == "" {
return pkg.Types().Scope().Lookup(name) != nil
}
// method?
tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName)
if !ok {
return false
}
m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name)
return is[*types.Func](m)
}
docHTML = func(comment string) []byte {
return printer.HTML(parser.Parse(comment))
}
}
scope := pkg.Types().Scope()
escape := html.EscapeString
title := fmt.Sprintf("%s package - %s - Gopls packages",
pkg.Types().Name(), escape(pkg.Types().Path()))
var buf bytes.Buffer
buf.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>` + pkgDocStyle + `</style>
<title>` + title + `</title>
<script type='text/javascript'>
// httpGET requests a URL for its effects only.
function httpGET(url) {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", url, true);
xhttp.send();
return false; // disable usual <a href=...> behavior
}
window.onload = () => {
// Hook up the navigation selector.
document.getElementById('hdr-Selector').onchange = (e) => {
window.location.href = e.target.value;
};
};
// Start a GET /hang request. If it ever completes, the server
// has disconnected. Show a banner in that case.
{
var x = new XMLHttpRequest();
x.open("GET", "/hang", true);
x.onloadend = () => {
document.getElementById("disconnected").style.display = 'block';
};
x.send();
};
</script>
</head>
<body>
<header>
<div id='disconnected'>Gopls server has terminated. Page is inactive.</div>
<select id='hdr-Selector'>
<optgroup label="Documentation">
<option label="Overview" value="#hdr-Overview"/>
<option label="Index" value="#hdr-Index"/>
<option label="Constants" value="#hdr-Constants"/>
<option label="Variables" value="#hdr-Variables"/>
<option label="Functions" value="#hdr-Functions"/>
<option label="Types" value="#hdr-Types"/>
<option label="Source Files" value="#hdr-SourceFiles"/>
</optgroup>
`)
// -- header select element --
// option emits an <option> for the specified symbol.
//
// recvType is the apparent receiver type, which may
// differ from ReceiverNamed(obj.Signature.Recv).Name
// for promoted methods.
option := func(obj types.Object, recvType string) {
// Render functions/methods as "(recv) Method(p1, ..., pN)".
fragment := obj.Name()
// format parameter names (p1, ..., pN)
label := obj.Name() // for a type
if fn, ok := obj.(*types.Func); ok {
var buf strings.Builder
sig := fn.Type().(*types.Signature)
if sig.Recv() != nil {
fmt.Fprintf(&buf, "(%s) ", sig.Recv().Name())
fragment = recvType + "." + fn.Name()
}
fmt.Fprintf(&buf, "%s(", fn.Name())
for i := 0; i < sig.Params().Len(); i++ {
if i > 0 {
buf.WriteString(", ")
}
name := sig.Params().At(i).Name()
if name == "" {
name = "_"
}
buf.WriteString(name)
}
buf.WriteByte(')')
label = buf.String()
}
fmt.Fprintf(&buf, " <option label='%s' value='#%s'/>\n", label, fragment)
}
// index of functions
fmt.Fprintf(&buf, "<optgroup label='Functions'>\n")
for _, fn := range docpkg.Funcs {
option(scope.Lookup(fn.Name), "")
}
fmt.Fprintf(&buf, "</optgroup>\n")
// index of types
fmt.Fprintf(&buf, "<optgroup label='Types'>\n")
for _, doctype := range docpkg.Types {
option(scope.Lookup(doctype.Name), "")
}
fmt.Fprintf(&buf, "</optgroup>\n")
// index of constructors and methods of each type
for _, doctype := range docpkg.Types {
tname := scope.Lookup(doctype.Name).(*types.TypeName)
if len(doctype.Funcs)+len(doctype.Methods) > 0 {
fmt.Fprintf(&buf, "<optgroup label='type %s'>\n", doctype.Name)
for _, docfn := range doctype.Funcs {
option(scope.Lookup(docfn.Name), "")
}
for _, docmethod := range doctype.Methods {
method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name)
option(method, doctype.Name)
}
fmt.Fprintf(&buf, "</optgroup>\n")
}
}
fmt.Fprintf(&buf, "</select>\n")
fmt.Fprintf(&buf, "</header>\n")
// -- main element --
// sourceLink returns HTML for a link to open a file in the client editor.
sourceLink := func(text, url string) string {
// The /open URL returns nothing but has the side effect
// of causing the LSP client to open the requested file.
// So we use onclick to prevent the browser from navigating.
// We keep the href attribute as it causes the <a> to render
// as a link: blue, underlined, with URL hover information.
return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`,
escape(url), escape(text))
}
// objHTML returns HTML for obj.Name(), possibly as a link.
objHTML := func(obj types.Object) string {
text := obj.Name()
if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() {
return sourceLink(text, posURL(posn.Filename, posn.Line, posn.Column))
}
return text
}
// nodeHTML returns HTML markup for a syntax tree.
// It replaces referring identifiers with links,
// and adds style spans for strings and comments.
nodeHTML := func(n ast.Node) string {
// linkify returns the appropriate URL (if any) for an identifier.
linkify := func(id *ast.Ident) protocol.URI {
if obj, ok := pkg.TypesInfo().Uses[id]; ok && obj.Pkg() != nil {
// imported package name?
if pkgname, ok := obj.(*types.PkgName); ok {
// TODO(adonovan): do this for Defs of PkgName too.
return pkgURL(PackagePath(pkgname.Imported().Path()), "")
}
// package-level symbol?
if obj.Parent() == obj.Pkg().Scope() {
if obj.Pkg() == pkg.Types() {
return "#" + obj.Name() // intra-package ref
} else {
return pkgURL(PackagePath(obj.Pkg().Path()), obj.Name())
}
}
// method of package-level named type?
if fn, ok := obj.(*types.Func); ok {
sig := fn.Type().(*types.Signature)
if sig.Recv() != nil {
_, named := typesinternal.ReceiverNamed(sig.Recv())
if named != nil {
fragment := named.Obj().Name() + "." + fn.Name()
return pkgURL(PackagePath(fn.Pkg().Path()), fragment)
}
}
return ""
}
// TODO(adonovan): field of package-level named struct type.
// (Requires an index, since there's no way to
// get from Var to Named.)
}
return ""
}
// Splice spans into HTML-escaped segments of the
// original source buffer (which is usually but not
// necessarily formatted).
//
// (For expedience we don't use the more sophisticated
// approach taken by cmd/godoc and pkgsite's render
// package, which emit the text, spans, and comments
// in one traversal of the syntax tree.)
//
// TODO(adonovan): splice styled spans around comments too.
//
// TODO(adonovan): pkgsite prints specs from grouped
// type decls like "type ( T1; T2 )" to make them
// appear as separate decls. We should too.
var buf bytes.Buffer
for _, file := range pkg.CompiledGoFiles() {
if astutil.NodeContains(file.File, n.Pos()) {
pos := n.Pos()
// emit emits source in the interval [pos:to] and updates pos.
emit := func(to token.Pos) {
// Ident and BasicLit always have a valid pos.
// (Failure means the AST has been corrupted.)
if !to.IsValid() {
bug.Reportf("invalid Pos")
}
start, err := safetoken.Offset(file.Tok, pos)
if err != nil {
bug.Reportf("invalid start Pos: %v", err)
}
end, err := safetoken.Offset(file.Tok, to)
if err != nil {
bug.Reportf("invalid end Pos: %v", err)
}
buf.WriteString(escape(string(file.Src[start:end])))
pos = to
}
ast.Inspect(n, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.Ident:
emit(n.Pos())
pos = n.End()
if url := linkify(n); url != "" {
fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", url, escape(n.Name))
} else {
buf.WriteString(escape(n.Name)) // plain
}
case *ast.BasicLit:
emit(n.Pos())
pos = n.End()
fmt.Fprintf(&buf, "<span class='lit'>%s</span>", escape(n.Value))
}
return true
})
emit(n.End())
return buf.String()
}
}
// Original source not found.
// Format the node without adornments.
if err := format.Node(&buf, pkg.FileSet(), n); err != nil {
// e.g. BadDecl?
buf.Reset()
fmt.Fprintf(&buf, "formatting error: %v", err)
}
return escape(buf.String())
}
// fnString is like fn.String() except that it:
// - shows the receiver name;
// - uses space "(T) M()" not dot "(T).M()" after receiver;
// - doesn't bother with the special case for interface receivers
// since it is unreachable for the methods in go/doc.
// - elides parameters after the first three: f(a, b, c, ...).
fnString := func(fn *types.Func) string {
// pkgRelative qualifies types by package name alone
pkgRelative := func(other *types.Package) string {
if pkg.Types() == other {
return "" // same package; unqualified
}
return other.Name()
}
sig := fn.Type().(*types.Signature)
// Emit "func (recv T) F".
var buf bytes.Buffer
buf.WriteString("func ")
if recv := sig.Recv(); recv != nil {
buf.WriteByte('(')
if recv.Name() != "" {
buf.WriteString(recv.Name())
buf.WriteByte(' ')
}
types.WriteType(&buf, recv.Type(), pkgRelative)
buf.WriteByte(')')
buf.WriteByte(' ') // (ObjectString uses a '.' here)
} else if pkg := fn.Pkg(); pkg != nil {
if s := pkgRelative(pkg); s != "" {
buf.WriteString(s)
buf.WriteByte('.')
}
}
buf.WriteString(fn.Name())
// Emit signature.
//
// Elide parameters after the third one.
// WriteSignature is too complex to fork, so we replace
// parameters 4+ with "invalid type", format,
// then post-process the string.
if sig.Params().Len() > 3 {
// Clone each TypeParam as NewSignatureType modifies them (#67294).
cloneTparams := func(seq *types.TypeParamList) []*types.TypeParam {
slice := make([]*types.TypeParam, seq.Len())
for i := range slice {
tparam := seq.At(i)
slice[i] = types.NewTypeParam(tparam.Obj(), tparam.Constraint())
}
return slice
}
sig = types.NewSignatureType(
sig.Recv(),
cloneTparams(sig.RecvTypeParams()),
cloneTparams(sig.TypeParams()),
types.NewTuple(append(
typesSeqToSlice[*types.Var](sig.Params())[:3],
types.NewVar(0, nil, "", types.Typ[types.Invalid]))...),
sig.Results(),
false) // any final ...T parameter is truncated
}
types.WriteSignature(&buf, sig, pkgRelative)
return strings.ReplaceAll(buf.String(), ", invalid type)", ", ...)")
}
fmt.Fprintf(&buf, "<main>\n")
// package name
fmt.Fprintf(&buf, "<h1 id='hdr-Overview'>Package %s</h1>\n", pkg.Types().Name())
// import path
fmt.Fprintf(&buf, "<pre class='code'>import %q</pre>\n", pkg.Types().Path())
// link to same package in pkg.go.dev
fmt.Fprintf(&buf, "<div><a href=%q title='View in pkg.go.dev'><img id='pkgsite' src='/assets/go-logo-blue.svg'/></a>\n",
"https://pkg.go.dev/"+string(pkg.Types().Path()))
// package doc
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docpkg.Doc))
// symbol index
fmt.Fprintf(&buf, "<h2 id='hdr-Index'>Index</h2>\n")
fmt.Fprintf(&buf, "<ul>\n")
if len(docpkg.Consts) > 0 {
fmt.Fprintf(&buf, "<li><a href='#hdr-Constants'>Constants</a></li>\n")
}
if len(docpkg.Vars) > 0 {
fmt.Fprintf(&buf, "<li><a href='#hdr-Variables'>Variables</a></li>\n")
}
for _, fn := range docpkg.Funcs {
obj := scope.Lookup(fn.Name).(*types.Func)
fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n",
obj.Name(), escape(fnString(obj)))
}
for _, doctype := range docpkg.Types {
tname := scope.Lookup(doctype.Name).(*types.TypeName)
fmt.Fprintf(&buf, "<li><a href='#%[1]s'>type %[1]s</a></li>\n",
tname.Name())
if len(doctype.Funcs)+len(doctype.Methods) > 0 {
fmt.Fprintf(&buf, "<ul>\n")
// constructors
for _, docfn := range doctype.Funcs {
obj := scope.Lookup(docfn.Name).(*types.Func)
fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n",
docfn.Name, escape(fnString(obj)))
}
// methods
for _, docmethod := range doctype.Methods {
method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name)
fmt.Fprintf(&buf, "<li><a href='#%s.%s'>%s</a></li>\n",
doctype.Name,
docmethod.Name,
escape(fnString(method.(*types.Func))))
}
fmt.Fprintf(&buf, "</ul>\n")
}
}
// TODO(adonovan): add index of Examples here.
fmt.Fprintf(&buf, "</ul>\n")
// constants and variables
values := func(vals []*doc.Value) {
for _, v := range vals {
// anchors
for _, name := range v.Names {
fmt.Fprintf(&buf, "<a id='%s'></a>\n", escape(name))
}
// declaration
decl2 := *v.Decl // shallow copy
decl2.Doc = nil
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2))
// comment (if any)
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(v.Doc))
}
}
fmt.Fprintf(&buf, "<h2 id='hdr-Constants'>Constants</h2>\n")
if len(docpkg.Consts) == 0 {
fmt.Fprintf(&buf, "<div>(no constants)</div>\n")
} else {
values(docpkg.Consts)
}
fmt.Fprintf(&buf, "<h2 id='hdr-Variables'>Variables</h2>\n")
if len(docpkg.Vars) == 0 {
fmt.Fprintf(&buf, "<div>(no variables)</div>\n")
} else {
values(docpkg.Vars)
}
// package-level functions
fmt.Fprintf(&buf, "<h2 id='hdr-Functions'>Functions</h2>\n")
// funcs emits a list of package-level functions,
// possibly organized beneath the type they construct.
funcs := func(funcs []*doc.Func) {
for _, docfn := range funcs {
obj := scope.Lookup(docfn.Name).(*types.Func)
fmt.Fprintf(&buf, "<h3 id='%s'>func %s</h3>\n",
docfn.Name, objHTML(obj))
// decl: func F(params) results
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n",
nodeHTML(docfn.Decl.Type))
// comment (if any)
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docfn.Doc))
}
}
funcs(docpkg.Funcs)
// types and their subelements
fmt.Fprintf(&buf, "<h2 id='hdr-Types'>Types</h2>\n")
for _, doctype := range docpkg.Types {
tname := scope.Lookup(doctype.Name).(*types.TypeName)
// title and source link
fmt.Fprintf(&buf, "<h3 id='%s'>type %s</a></h3>\n", doctype.Name, objHTML(tname))
// declaration
// TODO(adonovan): excise non-exported struct fields somehow.
decl2 := *doctype.Decl // shallow copy
decl2.Doc = nil
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2))
// comment (if any)
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(doctype.Doc))
// subelements
values(doctype.Consts) // constants of type T
values(doctype.Vars) // vars of type T
funcs(doctype.Funcs) // constructors of T
// methods on T
for _, docmethod := range doctype.Methods {
method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name)
fmt.Fprintf(&buf, "<h4 id='%s.%s'>func (%s) %s</h4>\n",
doctype.Name, docmethod.Name,
doctype.Name, objHTML(method))
// decl: func (x T) M(params) results
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n",
nodeHTML(docmethod.Decl.Type))
// comment (if any)
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n",
docHTML(docmethod.Doc))
}
}
// source files
fmt.Fprintf(&buf, "<h2 id='hdr-SourceFiles'>Source files</h2>\n")
for _, filename := range docpkg.Filenames {
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n",
sourceLink(filepath.Base(filename), posURL(filename, 1, 1)))
}
fmt.Fprintf(&buf, "</main>\n")
fmt.Fprintf(&buf, "</body>\n")
fmt.Fprintf(&buf, "</html>\n")
return buf.Bytes(), nil
}
// typesSeq abstracts various go/types sequence types:
// MethodSet, Tuple, TypeParamList, TypeList.
// TODO(adonovan): replace with go1.23 iterators.
type typesSeq[T any] interface {
Len() int
At(int) T
}
func typesSeqToSlice[T any](seq typesSeq[T]) []T {
slice := make([]T, seq.Len())
for i := range slice {
slice[i] = seq.At(i)
}
return slice
}
// (partly taken from pkgsite's typography.css)
const pkgDocStyle = `
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 1rem;
line-height: normal;
}
h1 {
font-size: 1.5rem;
}
h2 {
font-size: 1.375rem;
}
h3 {
font-size: 1.25rem;
}
h4 {
font-size: 1.125rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.875rem;
}
h1,
h2,
h3,
h4 {
font-weight: 600;
line-height: 1.25em;
word-break: break-word;
}
h5,
h6 {
font-weight: 500;
line-height: 1.3em;
word-break: break-word;
}
p {
font-size: 1rem;
line-height: 1.5rem;
max-width: 60rem;
}
strong {
font-weight: 600;
}
code,
pre,
textarea.code {
font-family: Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.875rem;
line-height: 1.5em;
}
pre,
textarea.code {
background-color: #eee;
border: 3px;
border-radius: 3px
color: black;
overflow-x: auto;
padding: 0.625rem;
tab-size: 4;
white-space: pre;
}
button,
input,
select,
textarea {
font: inherit;
}
a,
a:link,
a:visited {
color: rgb(0, 125, 156);
text-decoration: none;
}
a:hover,
a:focus {
color: rgb(0, 125, 156);
text-decoration: underline;
}
a:hover > * {
text-decoration: underline;
}
.lit { color: darkgreen; }
#pkgsite { height: 1.5em; }
header {
position: sticky;
top: 0;
left: 0;
width: 100%;
padding: 0.3em;
}
#hdr-Selector {
margin-right: 0.3em;
float: right;
min-width: 25em;
padding: 0.3em;
}
#disconnected {
position: fixed;
top: 1em;
left: 1em;
display: none; /* initially */
background-color: white;
border: thick solid red;
padding: 2em;
}
`