diff options
author | clsr <clsr@clsr.net> | 2017-08-18 14:08:04 +0200 |
---|---|---|
committer | clsr <clsr@clsr.net> | 2017-08-18 14:08:04 +0200 |
commit | 1c15fe67c72b4591feaceeffec0951e34a6c2e46 (patch) | |
tree | c22393533916300e73799b9fe630c392a48a1c6b | |
download | cn-http-1c15fe67c72b4591feaceeffec0951e34a6c2e46.tar.gz cn-http-1c15fe67c72b4591feaceeffec0951e34a6c2e46.zip |
Initial commitv0.1.0
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | cn-http.service | 24 | ||||
-rw-r--r-- | cnhttp.go | 645 | ||||
-rw-r--r-- | static/hlraw.js | 39 | ||||
-rw-r--r-- | static/script.js | 59 | ||||
-rw-r--r-- | static/style.css | 167 | ||||
-rw-r--r-- | templates/content.html | 97 | ||||
-rw-r--r-- | templates/page.html | 112 |
8 files changed, 1145 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62a5f44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/cn-http +/static/highlight diff --git a/cn-http.service b/cn-http.service new file mode 100644 index 0000000..debeeb5 --- /dev/null +++ b/cn-http.service @@ -0,0 +1,24 @@ +[Unit] +Description=CNP-HTTP and CNM-HTML translating reverse proxy +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/cn-http -listen localhost:9000 -cnp-host contnet.org +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +InaccessibleDirectories=/home +ReadOnlyDirectories=/ +CapabilityBoundingSet= +LimitFSIZE=0 +DeviceAllow=/dev/null rw +MemoryDenyWriteExecute=yes +User=nobody +Group=nogroup +WorkingDirectory=/var/contnet/cn-http +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target diff --git a/cnhttp.go b/cnhttp.go new file mode 100644 index 0000000..42f723d --- /dev/null +++ b/cnhttp.go @@ -0,0 +1,645 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "html/template" + "io" + "log" + "net" + "net/http" + "net/url" + "path" + "sort" + "strconv" + "strings" + "time" + + "github.com/kballard/go-shellquote" + + "contnet.org/lib/cnm-go" + "contnet.org/lib/cnm-go/cnmfmt" + "contnet.org/lib/cnp-go" +) + +var ( + listen = flag.String("listen", "localhost:8080", "address for HTTP server to listen on") + cnphost = flag.String("cnp-host", "", "the CNP host to proxy (disables browser mode)") + nohighlight = flag.Bool("no-highlight", false, "do not include highlight.js scripts and stylesheets") + nostatic = flag.Bool("no-static", false, "do not serve static files on /static") + + templates = template.Must(template.New("").Funcs(map[string]interface{}{ + "inc": func(s string) string { return "" }, + "dec": func() string { return "" }, + "depth": func() int { return 0 }, + "sanchor": func() string { return "" }, + "lanchor": func() string { return "" }, + "anchor": func() string { return "" }, + "lang": func(s string) string { + return strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' { + return r + } + return '_' + }, s) + }, + "href": func(s string) template.HTMLAttr { + return "href=\"" + template.HTMLAttr(template.HTMLEscapeString(s)) + "\"" + }, + "cnmfmt": (func(cnmfmt.Text) template.HTML)(nil), + "tourl": (func(string) string)(nil), + }).ParseGlob("templates/*.html")) +) + +const ( + anchorCNPSchema = "cnp://" + anchorHTTPSchema = "http://" + anchorPrefix = `<a href="` + anchorSuffix = `">` +) + +var ( + anchorTemplate = template.Must(template.New("").Parse(anchorPrefix + "{{.}}" + anchorSuffix)) +) + +func escapeURL(urlStr string) string { + isCNP := len(urlStr) >= len(anchorCNPSchema) && strings.EqualFold(urlStr[:len(anchorCNPSchema)], anchorCNPSchema) + if isCNP { + urlStr = anchorHTTPSchema + urlStr[len(anchorCNPSchema):] + } + var buf bytes.Buffer + anchorTemplate.Execute(&buf, urlStr) + urlStr = buf.String() + urlStr = urlStr[len(anchorPrefix) : len(urlStr)-len(anchorSuffix)] + if isCNP { + urlStr = anchorCNPSchema + urlStr[len(anchorHTTPSchema):] + } + return urlStr +} + +type server struct { + host string + highlighting bool +} + +func newServer(host string, highlighting bool) *server { + return &server{host, highlighting} +} + +func (srv *server) handleError(w http.ResponseWriter, err error) { + code := http.StatusInternalServerError + switch err.(type) { + case cnp.ErrorDenied: + code = http.StatusForbidden + case cnp.ErrorInvalid: + code = http.StatusInternalServerError + case cnp.ErrorNotFound: + code = http.StatusNotFound + case cnp.ErrorNotSupported: + code = http.StatusInternalServerError + case cnp.ErrorRejected: + code = http.StatusUnprocessableEntity + case cnp.ErrorServerError: + code = http.StatusBadGateway + case cnp.ErrorSyntax: + code = http.StatusInternalServerError + case cnp.ErrorTooLarge: + code = http.StatusRequestEntityTooLarge + case cnp.ErrorVersion: + code = http.StatusInternalServerError + } + log.Printf("error: %T %v", err, err) + http.Error(w, http.StatusText(code), code) +} + +func (srv *server) handleIndex(w http.ResponseWriter, r *http.Request) { + req, _ := cnp.NewRequest("", "/", nil) + resp, _ := cnp.NewResponse(cnp.IntentOK, []byte{}) + srv.cnpCNMToHTML(w, r, req, resp) +} + +func (srv *server) handleCNP(w http.ResponseWriter, r *http.Request, path string) { + host := srv.host + if host == "" { + ss := strings.SplitN(strings.TrimLeft(path, "/"), "/", 2) + if len(ss) != 2 { + if len(ss[0]) > 0 { + http.Redirect(w, r, path+"/", http.StatusFound) + return + } + srv.handleIndex(w, r) + return + } + host = ss[0] + path = "/" + ss[1] + } + + req, err := cnp.NewRequest(host, path, nil) + if err != nil { + srv.handleError(w, err) + return + } + + if ims := r.Header.Get("If-Modified-Since"); ims != "" { + var t time.Time + t, err = http.ParseTime(ims) + if err == nil && !t.IsZero() { + req.SetIfModified(t) + } + } + + resp, err := cnp.Send(req) + intent := "n/a" + if resp != nil { + intent = resp.Intent() + } + log.Printf("req: %q -> %q", req.Intent(), intent) + if err != nil { + if e, ok := err.(*net.OpError); ok && e.Op == "dial" { + err = cnp.NewError(cnp.ReasonServerError) + } + srv.handleError(w, err) + return + } + + r.ParseForm() + _, preq := r.Form["req"] + _, phdr := r.Form["hdr"] + _, presp := r.Form["resp"] + _, praw := r.Form["raw"] + + if preq || phdr || presp { + srv.cnpParamsToHTTP(w, resp) + w.Header().Set("Content-Type", "text/plain") + w.Header().Del("Content-Length") + + if preq { + req.Write(w) + } + if presp { + resp.Write(w) + } else if phdr { + resp.Header.Write(w) + } + } else { + srv.cnpToWeb(w, r, req, resp, praw) + } +} + +func (srv *server) cnpToWeb(w http.ResponseWriter, r *http.Request, req *cnp.Request, resp *cnp.Response, raw bool) { + if err := resp.Validate(); err != nil { + srv.handleError(w, err) + return + } + + switch resp.ResponseIntent() { + case cnp.IntentOK: + srv.cnpParamsToHTTP(w, resp) + if !raw && resp.Type() == "text/cnm" { + srv.cnpCNMToHTML(w, r, req, resp) + } else { + io.Copy(w, resp.Body) + } + case cnp.IntentNotModified: + w.WriteHeader(http.StatusNotModified) + case cnp.IntentRedirect: + host, pth, _ := resp.Location() // already validated + loc, err := srv.cnpRedirectToHTTP(req, host, pth) + if err != nil { + srv.handleError(w, err) + return + } + log.Println("redirecting to", loc) + http.Redirect(w, r, loc, http.StatusFound) + case cnp.IntentError: + srv.handleError(w, cnp.NewError(resp.Reason())) + default: + srv.handleError(w, errors.New("unknown CNP response intent: "+resp.ResponseIntent())) + } +} + +func (srv *server) cnpRedirectToHTTP(req *cnp.Request, host, pth string) (string, error) { + var loc string + if host == "" || host == "." || host == srv.host { + loc = pth + if host == "." { + loc = path.Join(path.Dir(req.Path()), loc) + } + if srv.host == "" { + loc = path.Join("/", req.Host(), loc) + } + } else { + rr, err := cnp.NewRequest(host, pth, nil) + if err != nil { + return "", err + } + if srv.host == "" { + loc = path.Join("?", rr.Host(), rr.Path()) + } else { + loc = rr.URL().String() + } + } + if strings.HasSuffix(pth, "/") { + loc = loc + "/" + } + return loc, nil +} + +func (srv *server) cnpParamsToHTTP(w http.ResponseWriter, resp *cnp.Response) { + if l := resp.Length(); l > 0 { + w.Header().Set("Content-Length", strconv.FormatInt(l, 10)) + } + if t := resp.Type(); t != "" { + w.Header().Set("Content-Type", t) + } + if m := resp.Modified(); !m.IsZero() { + w.Header().Set("Last-Modified", m.Format(http.TimeFormat)) + } + if t := resp.Time(); !t.IsZero() { + w.Header().Set("Date", t.Format(http.TimeFormat)) + } + if n := resp.Name(); n != "" { + w.Header().Set("Content-Disposition", "inline; filename=\""+strings.Map(func(r rune) rune { + if r < ' ' || r == ' ' || r == '\'' { // filter out nulls, control codes, newlines and quotes + return -1 + } + return r + }, n)+"\"") + } +} + +type cnmPage struct { + URL string + Req string + Resp string + Doc *cnm.Document + Site []site + Netcat string + Toc tocSection + Highlight bool + Browser bool + depth int +} + +type tocSection struct { + Title string + Children []tocSection +} + +type site struct { + Path string + Name string + Children []site +} + +func genToc(b cnm.Block) []tocSection { + var res []tocSection + switch v := b.(type) { + case *cnm.SectionBlock: + if t := v.Title(); t != "" { + var ch []tocSection + for _, c := range v.Children() { + ch = append(ch, genToc(c)...) + } + res = append(res, tocSection{ + Title: v.Title(), + Children: ch, + }) + } else { + for _, bl := range v.Children() { + res = append(res, genToc(bl)...) + } + } + case *cnm.TableBlock: + for _, bl := range v.Rows() { + res = append(res, genToc(bl)...) + } + case *cnm.HeaderBlock: + for _, bl := range v.Children() { + res = append(res, genToc(bl)...) + } + case *cnm.RowBlock: + for _, bl := range v.Children() { + res = append(res, genToc(bl)...) + } + case *cnm.ContentBlock: + if v == nil { + break + } + for _, bl := range v.Children() { + res = append(res, genToc(bl)...) + } + case *cnm.ListBlock: + for _, bl := range v.Children() { + res = append(res, genToc(bl)...) + } + } + return res +} + +func cnmSite(prefix string, s cnm.Site) (st site) { + if ss := strings.Split(path.Clean(s.Path), "/"); len(ss) > 1 { + s.Path = strings.Join(ss[1:], "/") + return site{ + Path: path.Join(prefix, ss[0]), + Name: ss[0], + Children: []site{ + cnmSite(path.Join(prefix, ss[0]), s), + }, + } + } + + st.Path = path.Join(prefix, s.Path) + if s.Name == "" { + st.Name = s.Path + } else { + st.Name = s.Name + } + if len(s.Children) > 0 { + chs := make(map[string]site) + for _, ch := range s.Children { + chs[ch.Name] = cnmSite(st.Path, ch) + } + keys := make([]string, len(chs)) + i := 0 + for k := range chs { + keys[i] = k + i++ + } + sort.Slice(keys, func(i, j int) bool { + a, b := chs[keys[i]], chs[keys[j]] + da, db := strings.HasSuffix(a.Name, "/"), strings.HasSuffix(b.Name, "/") + if da && !db { + return true + } + if !da && db { + return false + } + return a.Name < b.Name + }) + st.Children = make([]site, len(keys)) + for i := range keys { + st.Children[i] = chs[keys[i]] + } + } + return +} + +func (srv *server) cnpCNMToHTML(w http.ResponseWriter, r *http.Request, req *cnp.Request, resp *cnp.Response) { + doc, err := cnm.ParseDocument(resp.Body) + if err != nil { + srv.handleError(w, err) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Header().Del("Content-Length") + + var breq, bresp, buf bytes.Buffer + req.Header.Write(&breq) + resp.Header.Write(&bresp) + var st []site + if len(doc.Site.Children) > 0 { + st = []site{ + site{"/", "/", cnmSite("/", doc.Site).Children}, + } + } + + u := req.URL() + port := u.Port() + if port == "" { + port = strconv.Itoa(cnp.DefaultPort) + } + hdrs := req.Header.String() + hdrs = hdrs[:len(hdrs)-1] + host, _, _ := net.SplitHostPort(r.Host) + if host == "" && r.Host != "" { + host = r.Host + } + if host == "" { + host = r.URL.Hostname() + } + if host == "" { + host = req.Host() + } + sections := []string{} + sanchors := map[string]bool{} + lanchors := map[string]bool{} + anchor := func() template.URL { + var secs []string + for _, s := range sections { + if s = url.PathEscape(s); s != "" { + secs = append(secs, s) + } + } + return template.URL(strings.Join(secs, "/")) + } + err = templates.Funcs(map[string]interface{}{ + "inc": func(s string) string { sections = append(sections, s); return "" }, + "dec": func() string { sections = sections[:len(sections)-1]; return "" }, + "depth": func() int { + d := len(sections) + 1 + if d > 6 { + return 6 + } + return d + }, + "anchor": anchor, + "sanchor": func() template.URL { + s := url.PathEscape(sections[len(sections)-1]) + if sanchors[s] { + return "" + } + sanchors[s] = true + return template.URL(s) + }, + "lanchor": func() template.URL { + s := string(anchor()) + if lanchors[s] { + return "" + } + lanchors[s] = true + return template.URL(s) + }, + "cnmfmt": func(txt cnmfmt.Text) template.HTML { + return srv.fmtToHTML(req, txt) + }, + "tourl": func(s string) string { + u, _ := srv.linkToURL(req, s) + return u + }, + }).ExecuteTemplate(&buf, "page.html", cnmPage{ + URL: u.String(), + Req: breq.String(), + Resp: bresp.String(), + Doc: doc, + Netcat: shellquote.Join("echo", hdrs) + " | " + shellquote.Join("nc", host, port), + Site: st, + Toc: tocSection{Children: genToc(doc.Content)}, + Highlight: srv.highlighting, + Browser: srv.host == "", + }) + if err != nil { + srv.handleError(w, err) + return + } + + io.Copy(w, &buf) +} + +func (srv *server) fmtToHTML(req *cnp.Request, text cnmfmt.Text) template.HTML { + if len(text.Spans) == 0 { + return "" + } + + var last cnmfmt.Span + var buf bytes.Buffer + hadText := true + + spans := make([]cnmfmt.Span, len(text.Spans)+1) + copy(spans, text.Spans) + spans[len(spans)-1] = cnmfmt.Span{} // end all formats + + var tags []string + for _, span := range spans { + if last.Format.Link != span.Format.Link { + if !hadText && last.Format.Link != "" { // no text in link + template.HTMLEscape(&buf, []byte(last.Format.Link)) + } + hadText = false + } + srv.handleTags(req, &buf, &tags, last.Format, span.Format) + if span.Text != "" { + hadText = true + template.HTMLEscape(&buf, []byte(span.Text)) + } + last = span + } + + return template.HTML("<p>" + buf.String() + "</p>") +} + +func (srv *server) handleTags(req *cnp.Request, buf *bytes.Buffer, tags *[]string, old, new cnmfmt.Format) { + if old == new { + return + } + + close := map[string]bool{ + "b": old.Bold && !new.Bold, + "i": old.Italic && !new.Italic, + "u": old.Underline && !new.Underline, + "code": old.Monospace && !new.Monospace, + "a": old.Link != "" && old.Link != new.Link, + } + + open := map[string]bool{ + "b": !old.Bold && new.Bold, + "i": !old.Italic && new.Italic, + "u": !old.Underline && new.Underline, + "code": !old.Monospace && new.Monospace, + "a": new.Link != "" && old.Link != new.Link, + } + + t := *tags + + pop := len(t) + for i := len(t) - 1; i >= 0; i-- { + if close[t[i]] { + pop = i + } + } + for i := len(t) - 1; i >= pop; i-- { + if !close[t[i]] { + open[t[i]] = true + } + buf.WriteString("</" + t[i] + ">") + } + *tags = t[:pop] + + tagPush(buf, tags, open, "b", "<b>") + tagPush(buf, tags, open, "i", "<i>") + tagPush(buf, tags, open, "u", "<u>") + tagPush(buf, tags, open, "code", "<code>") + + link, extern := srv.linkToURL(req, new.Link) + tagPush(buf, tags, open, "a", "<a"+extern+" href=\""+escapeURL(link)+"\">") +} + +func (srv *server) linkToURL(req *cnp.Request, link string) (urlStr, extern string) { + urlStr = "#ZcnpfmtZ" + host := srv.host + if host == "" { + host = req.Host() + } + if u, err := url.Parse(link); err == nil { + if u.Scheme == "cnp" { + var lhost, lpath string + lhost = u.Host + lpath = u.Path + if u.Host == srv.host { + lhost = "" + } else { + extern = " class=\"cnp-external cnp-external-cnp\"" + } + urlStr = path.Join("/", lhost, lpath) + if strings.HasSuffix(link, "/") { + urlStr += "/" + } + if u.Fragment != "" { + urlStr += "#" + u.Fragment + } + } else if u.Scheme != "" { + extern = " class=\"cnp-external cnp-external-" + template.HTMLEscapeString(u.Scheme) + "\"" + urlStr = u.String() + } else { + urlStr = u.Path + if srv.host == "" { + if !strings.HasPrefix(urlStr, "/") { + urlStr = req.Path() + "/" + urlStr + } + urlStr = "/" + host + cnp.Clean("/"+urlStr) + } + if u.Fragment != "" { + urlStr += "#" + u.Fragment + } + } + } + return +} + +func tagPush(buf *bytes.Buffer, tags *[]string, ok map[string]bool, name, tag string) { + if ok[name] { + buf.WriteString(tag) + *tags = append(*tags, name) + } +} + +func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Printf("access: %s %s %q", r.RemoteAddr, r.Method, r.URL) + if r.Method == http.MethodPost { + u := r.FormValue("url") + req, err := cnp.NewRequestURL(u, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + http.Redirect(w, r, path.Join("/", req.Host(), req.Path()), http.StatusFound) + } else { + srv.handleCNP(w, r, r.URL.Path) + } +} + +func main() { + flag.Parse() + + srv := newServer(*cnphost, !*nohighlight) + + if !*nostatic { + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + } + http.Handle("/", srv) + + log.Printf("listening on %s", *listen) + panic(http.ListenAndServe(*listen, nil)) +} diff --git a/static/hlraw.js b/static/hlraw.js new file mode 100644 index 0000000..8d57bb4 --- /dev/null +++ b/static/hlraw.js @@ -0,0 +1,39 @@ +(function() { + 'use strict'; + + var highlight = function(block, lang) { + var langs = lang.split('_'); + if (langs.length > 1) { + lang = langs[langs.length-1]; + } + if (!lang || typeof hljs.getLanguage(lang) === 'undefined') { + return; + } + var hl = hljs.highlight(lang, block.textContent, true); + block.innerHTML = hl.value; + }; + + var highlightAll = function() { + var rawBlocks = document.querySelectorAll('pre.cnm-raw code'); + for (var i=0; i<rawBlocks.length; i++) { + var code = rawBlocks[i]; + var classes = code.className.split(); + var lang = ''; + for (var j=0; j<classes.length; j++) { + if (classes[j].startsWith('cnm-raw-')) { + lang = classes[j].slice('cnm-raw-'.length); + break; + } + } + if (lang) { + try { + highlight(code, lang); + } catch (e) { + console.error(e); + } + } + } + }; + + highlightAll(); +})(); diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..36e2b18 --- /dev/null +++ b/static/script.js @@ -0,0 +1,59 @@ +(function() { + 'use strict'; + + var init = function() { + var toggle = document.createElement('button'); + + var clickToggle = function() { + toggle.open = !toggle.open; + var open = toggle.open ? 'open' : ''; + var secs = document.querySelectorAll('section>details'); + for (var i=0; i<secs.length; i++) { + secs[i].open = open; + } + toggle.textContent = (toggle.open ? 'Collapse' : 'Expand') + ' all sections'; + }; + + toggle.addEventListener('click', clickToggle); + + var hashchange = function() { + var selected = []; + if (location.hash != '' && location.hash != '#') { + var h = location.hash.slice(1); + var el = document.getElementById(h); + while (el) { + if (el.tagName == 'DETAILS') { + selected.push(el); + } + el = el.parentNode; + } + } + + if (selected.length > 0) { + toggle.open = true; + clickToggle(); + for (var i=0; i<selected.length; i++) { + selected[i].open = 'open'; + } + selected[0].scrollIntoView(); + } else { + toggle.open = false; + clickToggle(); + } + }; + window.addEventListener('hashchange', hashchange); + + hashchange(); + if (document.querySelectorAll('main section').length > 0) { + var main = document.querySelector('main'); + main.insertBefore(toggle, main.firstChild); + } + + }; + + if (document.readyState !== 'loading') { + init(); + } else { + document.addEventListener('DOMContentLoaded', init); + } +})(); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..752d538 --- /dev/null +++ b/static/style.css @@ -0,0 +1,167 @@ +html { + height: 100%; + font-family: sans-serif; + color: black; + background-color: white; +} + +body { + margin: 1em auto; + max-width: 50em; + padding: 0 0.5em; +} + +section { + margin: 0.25em; +} + +section h1, section h2, section h3, +section h4, section h5, section h6 { + padding: 0; + margin: 0; + display: inline-block; +} + +.sec-link { + display: none; +} + +:hover>*>.sec-link { + display: inline; +} + +a.cnp-external:after { + text-decoration: none; + color: gray; + display: inline-block; + font-size: 0.8em; + content: '\01f517'; +} + +a.cnp-external-cnp:after { + content: '[cnp]'; +} + +a.cnp-external-http:after { + content: '[http]'; +} + +a.cnp-external-https:after { + content: '[https]'; +} + +main { + border: 1px dashed #aaa; + tab-size: 4; + -moz-tab-size: 4; /* Pale Moon and older FF need prefix */ +} + +main, section { + padding: 0.75em; +} + +main:hover, section:hover { +} + +pre, code { + font-family: monospace, monospace; + font-size: 1em; +} + +pre { + margin: 0; + display: inline-block; + white-space: pre-wrap; +} + +code { + background-color: #f8f8f8; + border: 1px solid #ccc; +} + +pre.cnm-raw { + display: block; +} + +pre>code { + background-color: #fbfbfb; + display: block; + border: 1px solid black; + width: auto; + padding: 0.5em; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +pre>code:hover { + background-color: #f4f4f4; +} + +p, figcaption, pre { + margin-bottom: 1em; +} + +figure { + border: 1px solid #aaa; + display: inline-block; + padding: 0 0.5em; + margin: 0 auto; +} + +summary { + cursor: pointer; +} + +details>summary>* { + text-decoration: underline; +} + +details[open]>summary>* { + text-decoration: none; +} + +header { +} + +footer { + padding-top: 0.5em; + font-size: 0.8em; +} + +footer details { + display: inline-block; +} + +table { + margin-top: 0.5em; + margin-bottom: 0.5em; + border-collapse: collapse; +} + +li p, li pre, li code, table p, table pre, table code { + padding: 0; + margin: 0; +} + +table, th, td { + border: 1px solid black; +} + +img { + padding-top: 0.5em; + max-width: 100%; +} + +#browser { + width: 100%; + border: 1px solid black; + padding: 1em; + display: flex; +} +#browser input[type=text] { + flex: 1; +} +#browser input[type=submit] { + border: 1px solid black; + margin-left: 0.5em; +} diff --git a/templates/content.html b/templates/content.html new file mode 100644 index 0000000..73f18c7 --- /dev/null +++ b/templates/content.html @@ -0,0 +1,97 @@ +{{- if eq .Name "section" "content" -}} +{{- if .Title}} +<section class="cnm-section"><details open> + {{- inc .Title -}} + <summary> + {{- with .Title -}} + {{- if $l := lanchor -}} + <h{{depth}}>{{.}}<a class="sec-link" id="/{{$l}}" href="#/{{$l}}">ΒΆ</a> + {{- if $s := sanchor}}{{if ne $s $l}}<a id="#{{$s}}"></a>{{end}}{{end -}} + </h{{depth}}> + {{- end -}} + {{- end -}} + </summary> + {{- range .Children -}} + {{- template "content.html" . -}} + {{- end -}} + {{- dec -}} +</details></section> +{{else -}} +{{- range .Children -}} + {{- template "content.html" . -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- if eq .Name "list" -}} +{{if .Ordered}}<ol{{else}}<ul{{end}} class="cnm-list"> + {{- range .Children -}} + {{- if eq .Name "list" -}} + {{- template "content.html" . -}} + {{- else -}} + <li>{{- template "content.html" . -}}</li> + {{- end -}} + {{- end -}} +{{- if .Ordered}}</ol>{{else}}</ul>{{end -}} +{{- end -}} + +{{- if eq .Name "table" -}} +<table class="cnm-table"> + {{- range .Rows -}} + {{- template "content.html" . -}} + {{- end -}} +</table> +{{- end -}} + +{{- if eq .Name "header" -}} +<tr class="cnm-header"> + {{- range .Children -}} + <th>{{template "content.html" .}}</th> + {{- end -}} +</tr> +{{- end -}} + +{{- if eq .Name "row" -}} +<tr class="cnm-row"> + {{- range .Children -}} + <td>{{template "content.html" .}}</td> + {{- end -}} +</tr> +{{- end -}} + +{{- if eq .Name "embed" -}} +<figure class="cnm-embed"> + {{- if eq .Type "image/png" "image/jpeg" "image/webp" "image/*" -}} + <a href="{{tourl .URL}}"><img src="{{tourl .URL}}" alt="{{with .Description}}{{.}}{{else}}embedded {{.Type}}{{end}}" {{with .Description}}title="{{.}}" {{end}}/></a> + {{- end -}} + {{- if eq .Type "video/mp4" "video/webm" "video/*" -}} + <video src="{{tourl .URL}}" controls{{with .Description}} title="{{.}}"{{end}}> + Video content: <a href="{{tourl .URL}}">{{.URL}}</a> + </video> + {{- end -}} + {{- if not (eq .Type "image/png" "image/jpeg" "image/webp" "image/*" "video/mp4" "video/webm" "video/*") -}} + <p> + Embedded <code>{{.Type}}</code> content: <a href="{{tourl .URL}}"{{with .Description}} title="{{.}}"{{end}}>{{.URL}}</a> + </p> + {{- end -}} + {{- with .Description}}<figcaption>{{.}}</figcaption>{{end -}} +</figure> +{{- end -}} + +{{- if eq .Name "text" -}} + {{- if eq .Format "" "plain"}}{{range .Contents.Paragraphs -}} +<p class="cnm-text">{{.}}</p> + {{- end}}{{end -}} + {{- if eq .Format "fmt" -}} + {{- range .Contents.Paragraphs -}} + {{- cnmfmt . -}} + {{- end -}} + {{- end -}} + {{- if not (eq .Format "" "plain" "fmt") -}} +<pre class="cnm-text cnm-text-pre">{{.Contents.Text}}</pre> + {{- end -}} +{{- end -}} + +{{- if eq .Name "raw" -}} +<pre class="cnm-raw"><code class="cnm-raw-{{lang .Syntax}}">{{.Contents}}</code></pre> +{{- end -}} diff --git a/templates/page.html b/templates/page.html new file mode 100644 index 0000000..0f89e2d --- /dev/null +++ b/templates/page.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{{with .Doc.Title}}{{.}}{{else}} {{end}}</title> + <link rel="stylesheet" href="/static/style.css" /> + <style>main p,main summary,h1,h2,h3,h4,h5,h6{white-space: pre-wrap;}</style> + <script async src="/static/script.js"></script> + {{if .Highlight}}<link rel="stylesheet" href="/static/highlight/style.css" />{{end}} + </head> + <body> + <header> + {{if .Browser -}} + <form id="browser" action="/" method="post"> + <input type="text" name="url" placeholder="cnp://example.com/" value="{{if ne .URL "cnp:///"}}{{.URL}}{{end}}" /> + <input type="submit" value="Go" /> + </form> + {{- end}} + + {{with .Doc.Title}}<h1>{{.}}</h1>{{end -}} + + {{with .Doc.Links -}} + <nav> + <details open> + <summary><b>Links</b></summary> + <ul> + {{- range .}} + <li><a href="{{tourl .URL}}"{{with .Description}} title="{{.}}"{{end}}>{{with .Name}}{{.}}{{else}}{{.URL}}{{end}}</a></li> + {{- end}} + </ul> + </details> + </nav> + {{- end -}} + + {{with .Site -}} + <nav> + <details> + <summary><b>Sitemap</b></summary> + {{- block "site" . -}} + <ul> + {{- range . -}} + <li> + <a href="{{tourl .Path}}">{{.Name}}</a> + {{- template "site" .Children -}} + </li> + {{- end -}} + </ul> + {{- end -}} + </details> + </nav> + {{- end -}} + + {{if .Toc.Children -}} + <nav> + <details> + <summary><b>Table of Contents</b></summary> + <ul> + {{- range .Toc.Children -}} + {{- block "toc" . -}} + {{- inc .Title -}} + <li> + <a href="#/{{anchor}}">{{.Title}}</a> + {{- with .Children -}} + <ul> + {{- range . -}} + {{- template "toc" . -}} + {{- end -}} + </ul> + {{- end -}} + </li> + {{- dec -}} + {{- end -}} + {{- end -}} + </ul> + </details> + </nav> + {{- end}} + </header> + + <main> + {{- with .Doc.Content}} + {{- range .Children}} + {{- template "content.html" .}} + {{- end}}{{end}} + </main> + + <footer> + <p>This is a <a href="https://contnet.org/">ContNet</a> <code>text/cnm</code> page retrieved over CNP from <a {{href .URL}}>{{.URL}}</a> + - + <a href="?req&resp">See request/response</a> + - + <a href="?raw">Raw CNM document</a> + </p> + <details> + <summary>CNP request header (<a href="?req">try</a>)</summary> + <pre><code class="lang-{{lang "text/cnp"}}">{{.Req}}</code></pre> + </details> + <details> + <summary>CNP response header (<a href="?hdr">try</a>)</summary> + <pre><code class="lang-{{lang "text/cnp"}}">{{.Resp}}</code></pre> + </details> + <details> + <summary>Try with <a href="https://en.wikipedia.org/wiki/Netcat">Netcat</a></summary> + <pre><code class="lang-{{lang "application/x-sh"}}">{{.Netcat}}</code></pre> + </details> + </footer> + + {{if .Highlight}}<script src="/static/highlight/highlight.pack.js"></script> + <script src="/static/hlraw.js"></script>{{end}} + </body> +</html> |