diff options
| -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> |