forked from mirror/oddmu
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44213e1d43 | ||
|
|
ae9698aae3 | ||
|
|
24871eee99 | ||
|
|
5f44853bab | ||
|
|
f0a3d2c5a0 | ||
|
|
b8f916b7c9 | ||
|
|
db8a060d65 | ||
|
|
9d216f37ee | ||
|
|
39c2fe6dfd | ||
|
|
3151fe63fa | ||
|
|
abd3ceae2e | ||
|
|
edad64e76c | ||
|
|
71315bc662 | ||
|
|
27509bcdd4 | ||
|
|
04e8cb3ee8 | ||
|
|
2be4fe503d | ||
|
|
7a405b22b8 | ||
|
|
3f6fce7165 | ||
|
|
721d5907d8 | ||
|
|
cb0dbedaed | ||
|
|
3ba967781e | ||
|
|
e985707b51 | ||
|
|
a5e7dca7d8 | ||
|
|
74387910d8 | ||
|
|
121408d6d9 | ||
|
|
1b7419466a | ||
|
|
8a513746d5 | ||
|
|
e736802da5 |
2
Makefile
2
Makefile
@@ -38,7 +38,7 @@ run:
|
||||
|
||||
upload: build
|
||||
rsync --itemize-changes --archive oddmu sibirocobombus.root:/home/oddmu/
|
||||
ssh sibirocobombus.root "systemctl restart oddmu; systemctl restart alex; systemctl restart claudia; systemctl restart campaignwiki"
|
||||
ssh sibirocobombus.root "systemctl restart oddmu; systemctl restart alex; systemctl restart claudia; systemctl restart campaignwiki; systemctl restart community"
|
||||
@echo Changes to the template files need careful consideration
|
||||
|
||||
docs:
|
||||
|
||||
@@ -213,6 +213,7 @@ high-level introduction to the various source files.
|
||||
- `languages.go` implements the language detection
|
||||
- `page.go` implements the page loading and saving
|
||||
- `parser.go` implements the Markdown parsing
|
||||
- `preview.go` implements the `/preview` handler
|
||||
- `score.go` implements the page scoring when showing search results
|
||||
- `search.go` implements the `/search` handler
|
||||
- `snippets.go` implements the page summaries for search results
|
||||
|
||||
22
diff.go
22
diff.go
@@ -46,23 +46,23 @@ func (p *Page) Diff() template.HTML {
|
||||
}
|
||||
|
||||
func diff2html(diffs []diffmatchpatch.Diff) string {
|
||||
var buff bytes.Buffer
|
||||
var buf bytes.Buffer
|
||||
for _, item := range diffs {
|
||||
text := strings.ReplaceAll(html.EscapeString(item.Text), "\n", "<br>")
|
||||
switch item.Type {
|
||||
case diffmatchpatch.DiffInsert:
|
||||
_, _ = buff.WriteString("<ins>")
|
||||
_, _ = buff.WriteString(text)
|
||||
_, _ = buff.WriteString("</ins>")
|
||||
_, _ = buf.WriteString("<ins>")
|
||||
_, _ = buf.WriteString(text)
|
||||
_, _ = buf.WriteString("</ins>")
|
||||
case diffmatchpatch.DiffDelete:
|
||||
_, _ = buff.WriteString("<del>")
|
||||
_, _ = buff.WriteString(text)
|
||||
_, _ = buff.WriteString("</del>")
|
||||
_, _ = buf.WriteString("<del>")
|
||||
_, _ = buf.WriteString(text)
|
||||
_, _ = buf.WriteString("</del>")
|
||||
case diffmatchpatch.DiffEqual:
|
||||
_, _ = buff.WriteString("<span>")
|
||||
_, _ = buff.WriteString(text)
|
||||
_, _ = buff.WriteString("</span>")
|
||||
_, _ = buf.WriteString("<span>")
|
||||
_, _ = buf.WriteString(text)
|
||||
_, _ = buf.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
return buff.String()
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ form, textarea { width: 100%; }
|
||||
Text" lang="{{.Language}}" autofocus>{{printf "%s" .Body}}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Save">
|
||||
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// highlight splits the query string q into terms and highlights them
|
||||
// using the bold tag. Return the highlighted string.
|
||||
// This assumes that q already has all its meta characters quoted.
|
||||
func highlight(q string, re *regexp.Regexp, s string) string {
|
||||
// highlight matches for the regular expression using the bold tag.
|
||||
func highlight(re *regexp.Regexp, s string) string {
|
||||
s = re.ReplaceAllString(s, "<b>$1</b>")
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ No birds to be heard.`
|
||||
|
||||
q := "window"
|
||||
re, _ := re(q)
|
||||
r := highlight(q, re, s)
|
||||
r := highlight(re, s)
|
||||
if r != h {
|
||||
t.Logf("The highlighting is wrong in 「%s」", r)
|
||||
t.Fail()
|
||||
@@ -35,7 +35,7 @@ I hear the fountain`
|
||||
|
||||
q := "shout out"
|
||||
re, _ := re(q)
|
||||
r := highlight(q, re, s)
|
||||
r := highlight(re, s)
|
||||
if r != h {
|
||||
t.Logf("The highlighting is wrong in 「%s」", r)
|
||||
t.Fail()
|
||||
|
||||
16
index.go
16
index.go
@@ -12,10 +12,20 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type docid uint
|
||||
|
||||
// ImageData holds the data used to search for images using the alt-text. Title is the alt-text; Name is the complete
|
||||
// URL including path (which is important since the image link itself only has the URL relative to the page in which it
|
||||
// is found; and Html is a copy of the Title with highlighting of a term as applied when searching. This is temporary.
|
||||
// It depends on the fact that Title is always plain text.
|
||||
type ImageData struct {
|
||||
Title, Name string
|
||||
Html template.HTML
|
||||
}
|
||||
|
||||
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
|
||||
type indexStore struct {
|
||||
sync.RWMutex
|
||||
@@ -31,6 +41,9 @@ type indexStore struct {
|
||||
|
||||
// titles is a map, mapping page names to titles.
|
||||
titles map[string]string
|
||||
|
||||
// images is a map, mapping pages names to alt text to an array of image data.
|
||||
images map[string][]ImageData
|
||||
}
|
||||
|
||||
var index indexStore
|
||||
@@ -45,6 +58,7 @@ func (idx *indexStore) reset() {
|
||||
idx.token = make(map[string][]docid)
|
||||
idx.documents = make(map[docid]string)
|
||||
idx.titles = make(map[string]string)
|
||||
idx.images = make(map[string][]ImageData)
|
||||
}
|
||||
|
||||
// addDocument adds the text as a new document. This assumes that the index is locked!
|
||||
@@ -102,6 +116,7 @@ func (idx *indexStore) deletePageName(name string) {
|
||||
delete(idx.documents, id)
|
||||
}
|
||||
delete(idx.titles, name)
|
||||
delete(idx.images, name)
|
||||
}
|
||||
|
||||
// remove the page from the index. Do this when deleting a page. This assumes that the index is unlocked.
|
||||
@@ -153,6 +168,7 @@ func (idx *indexStore) addPage(p *Page) {
|
||||
idx.documents[id] = p.Name
|
||||
p.handleTitle(false)
|
||||
idx.titles[p.Name] = p.Title
|
||||
idx.images[p.Name] = p.images()
|
||||
}
|
||||
|
||||
// add a page to the index. This assumes that the index is unlocked.
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-APACHE" "5" "2024-02-19"
|
||||
.TH "ODDMU-APACHE" "5" "2024-05-09"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
|
||||
.PP
|
||||
.SS DESCRIPTION
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.\&
|
||||
This is an unpriviledged port so an ordinary user account can do this.\&
|
||||
@@ -22,7 +22,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
|
||||
web server as reverse proxy.\& This page explains how to setup Apache on Debian to
|
||||
do this.\&
|
||||
.PP
|
||||
.SS CONFIGURATION
|
||||
.SH CONFIGURATION
|
||||
.PP
|
||||
HTTPS is not part of Oddmu.\& You probably want to configure this in your
|
||||
webserver.\& I guess you could use stunnel, too.\& If you'\&re using Apache, you can
|
||||
@@ -48,7 +48,7 @@ ServerAdmin alex@alexschroeder\&.ch
|
||||
<VirtualHost *:443>
|
||||
ServerName transjovian\&.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
.fi
|
||||
@@ -132,7 +132,7 @@ ServerAdmin alex@alexschroeder\&.ch
|
||||
<VirtualHost *:443>
|
||||
ServerName transjovian\&.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
.fi
|
||||
@@ -144,15 +144,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket.\& This requires socket activation.\& An example of configuring
|
||||
the service is given in \fIoddmu.\&service(5)\fR.\&
|
||||
.PP
|
||||
To test just the unix domain socket, use \fIncat(1)\fR:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
|
||||
| ncat --unixsock /run/oddmu/oddmu\&.sock
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
On the Apache side, you can proxy to the socket directly.\& This sends all
|
||||
requests to the socket:
|
||||
.PP
|
||||
@@ -179,7 +170,7 @@ In that case, you need to use the ProxyPassMatch directive.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
|
||||
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
|
||||
.fi
|
||||
.RE
|
||||
@@ -198,7 +189,7 @@ A workaround is to add the redirect manually and drop the question-mark:
|
||||
.nf
|
||||
.RS 4
|
||||
RedirectMatch "^/$" "/view/index"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))$"
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))$"
|
||||
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
|
||||
.fi
|
||||
.RE
|
||||
@@ -283,7 +274,7 @@ directory:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|search|archive)/secret)/">
|
||||
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|preview|search|archive)/secret)/">
|
||||
AuthType Basic
|
||||
AuthName "Password Required"
|
||||
AuthUserFile /home/oddmu/\&.htpasswd
|
||||
|
||||
@@ -4,7 +4,7 @@ ODDMU-APACHE(5)
|
||||
|
||||
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
|
||||
|
||||
## DESCRIPTION
|
||||
# DESCRIPTION
|
||||
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.
|
||||
This is an unpriviledged port so an ordinary user account can do this.
|
||||
@@ -15,7 +15,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
|
||||
web server as reverse proxy. This page explains how to setup Apache on Debian to
|
||||
do this.
|
||||
|
||||
## CONFIGURATION
|
||||
# CONFIGURATION
|
||||
|
||||
HTTPS is not part of Oddmu. You probably want to configure this in your
|
||||
webserver. I guess you could use stunnel, too. If you're using Apache, you can
|
||||
@@ -40,7 +40,7 @@ ServerAdmin alex@alexschroeder.ch
|
||||
<VirtualHost *:443>
|
||||
ServerName transjovian.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
```
|
||||
@@ -112,7 +112,7 @@ ServerAdmin alex@alexschroeder.ch
|
||||
<VirtualHost *:443>
|
||||
ServerName transjovian.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
```
|
||||
@@ -123,13 +123,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket. This requires socket activation. An example of configuring
|
||||
the service is given in _oddmu.service(5)_.
|
||||
|
||||
To test just the unix domain socket, use _ncat(1)_:
|
||||
|
||||
```
|
||||
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
|
||||
| ncat --unixsock /run/oddmu/oddmu.sock
|
||||
```
|
||||
|
||||
On the Apache side, you can proxy to the socket directly. This sends all
|
||||
requests to the socket:
|
||||
|
||||
@@ -151,7 +144,7 @@ You probably want to serve some static files as well (see *Serve static files*).
|
||||
In that case, you need to use the ProxyPassMatch directive.
|
||||
|
||||
```
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
|
||||
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
|
||||
```
|
||||
|
||||
@@ -166,7 +159,7 @@ A workaround is to add the redirect manually and drop the question-mark:
|
||||
|
||||
```
|
||||
RedirectMatch "^/$" "/view/index"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))$" \
|
||||
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))$" \
|
||||
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
|
||||
```
|
||||
|
||||
@@ -241,7 +234,7 @@ You need to configure the web server to prevent access to the "secret/"
|
||||
directory:
|
||||
|
||||
```
|
||||
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|search|archive)/secret)/">
|
||||
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|preview|search|archive)/secret)/">
|
||||
AuthType Basic
|
||||
AuthName "Password Required"
|
||||
AuthUserFile /home/oddmu/.htpasswd
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-NGINX" "5" "2024-02-19"
|
||||
.TH "ODDMU-NGINX" "5" "2024-05-09"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
|
||||
.PP
|
||||
.SS DESCRIPTION
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.\&
|
||||
This is an unpriviledged port so an ordinary user account can do this.\&
|
||||
@@ -20,14 +20,14 @@ This page explains how to setup NGINX on Debian to act as a reverse proxy for
|
||||
Oddmu.\& Once this is done, you can use NGINX to provide HTTPS, request users to
|
||||
authenticate themselves, and so on.\&
|
||||
.PP
|
||||
.SS CONFIGURATION
|
||||
.SH CONFIGURATION
|
||||
.PP
|
||||
The site is defined in "/etc/nginx/sites-available/default", in the \fIserver\fR
|
||||
section.\& Add a new \fIlocation\fR section after the existing \fIlocation\fR section:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
.fi
|
||||
@@ -38,6 +38,12 @@ get passed on to Oddmu.\& They are essentially disabled.\& Somebody on the same
|
||||
machine pointing their browser at http://localhost:8080/ directly would still
|
||||
have access to all the actions, of course.\&
|
||||
.PP
|
||||
.SS Access
|
||||
.PP
|
||||
Access control is not part of Oddmu.\& By default, the wiki is editable by all.\&
|
||||
This is most likely not what you want unless you'\&re running it stand-alone,
|
||||
unconnected to the Internet – a personal memex on your laptop, for example.\&
|
||||
.PP
|
||||
To restrict access to some actions, use two different \fIlocation\fR sections:
|
||||
.PP
|
||||
.nf
|
||||
@@ -73,6 +79,49 @@ alex:$1$DOwphABk$W4VmR9p8t2\&.htxF6ctXHX\&.
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
These instructions create user accounts with passwords just for Oddmu.\&
|
||||
These users are not real users on the web server and don'\&t have access to a
|
||||
shell, mail, or any other service.\&
|
||||
.PP
|
||||
.SS Using a Unix-domain Socket
|
||||
.PP
|
||||
Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket.\& This requires socket activation.\& An example of configuring
|
||||
the service is given in \fIoddmu.\&service\fR(5).\&
|
||||
.PP
|
||||
On the nginx side, you can proxy to the socket using an \fIupstream\fR section.\& This
|
||||
sends all requests to the socket.\& Use the upstream name as the server name for
|
||||
\fIproxy_pass\fR.\& Add something like the configuration below to your existing nginx
|
||||
server configuration.\& On a Debian system, that'\&d be in
|
||||
"/etc/nginx/sites-available/default".\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
proxy_pass http://unix:/run/oddmu/oddmu\&.sock:;
|
||||
}
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Reload the configuration:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
sudo systemd reload nginx
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Now, all traffic between the web server and the wiki goes over the socket at
|
||||
"/run/oddmu/oddmu.\&sock".\&
|
||||
.PP
|
||||
To test it on the command-line, use a tool like \fIcurl(1)\fR.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
curl http://localhost/view/index
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIoddmu-apache\fR(5)
|
||||
|
||||
@@ -4,7 +4,7 @@ ODDMU-NGINX(5)
|
||||
|
||||
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
|
||||
|
||||
## DESCRIPTION
|
||||
# DESCRIPTION
|
||||
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.
|
||||
This is an unpriviledged port so an ordinary user account can do this.
|
||||
@@ -13,13 +13,13 @@ This page explains how to setup NGINX on Debian to act as a reverse proxy for
|
||||
Oddmu. Once this is done, you can use NGINX to provide HTTPS, request users to
|
||||
authenticate themselves, and so on.
|
||||
|
||||
## CONFIGURATION
|
||||
# CONFIGURATION
|
||||
|
||||
The site is defined in "/etc/nginx/sites-available/default", in the _server_
|
||||
section. Add a new _location_ section after the existing _location_ section:
|
||||
|
||||
```
|
||||
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
```
|
||||
@@ -29,6 +29,12 @@ get passed on to Oddmu. They are essentially disabled. Somebody on the same
|
||||
machine pointing their browser at http://localhost:8080/ directly would still
|
||||
have access to all the actions, of course.
|
||||
|
||||
## Access
|
||||
|
||||
Access control is not part of Oddmu. By default, the wiki is editable by all.
|
||||
This is most likely not what you want unless you're running it stand-alone,
|
||||
unconnected to the Internet – a personal memex on your laptop, for example.
|
||||
|
||||
To restrict access to some actions, use two different _location_ sections:
|
||||
|
||||
```
|
||||
@@ -58,6 +64,43 @@ using this password:
|
||||
alex:$1$DOwphABk$W4VmR9p8t2.htxF6ctXHX.
|
||||
```
|
||||
|
||||
These instructions create user accounts with passwords just for Oddmu.
|
||||
These users are not real users on the web server and don't have access to a
|
||||
shell, mail, or any other service.
|
||||
|
||||
## Using a Unix-domain Socket
|
||||
|
||||
Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket. This requires socket activation. An example of configuring
|
||||
the service is given in _oddmu.service_(5).
|
||||
|
||||
On the nginx side, you can proxy to the socket using an _upstream_ section. This
|
||||
sends all requests to the socket. Use the upstream name as the server name for
|
||||
_proxy_pass_. Add something like the configuration below to your existing nginx
|
||||
server configuration. On a Debian system, that'd be in
|
||||
"/etc/nginx/sites-available/default".
|
||||
|
||||
```
|
||||
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
|
||||
proxy_pass http://unix:/run/oddmu/oddmu.sock:;
|
||||
}
|
||||
```
|
||||
|
||||
Reload the configuration:
|
||||
|
||||
```
|
||||
sudo systemd reload nginx
|
||||
```
|
||||
|
||||
Now, all traffic between the web server and the wiki goes over the socket at
|
||||
"/run/oddmu/oddmu.sock".
|
||||
|
||||
To test it on the command-line, use a tool like _curl(1)_.
|
||||
|
||||
```
|
||||
curl http://localhost/view/index
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _oddmu-apache_(5)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-RELEASES" "7" "2024-03-10"
|
||||
.TH "ODDMU-RELEASES" "7" "2024-07-21"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -15,6 +15,60 @@ oddmu-releases - what'\&s new in this releases?\&
|
||||
.PP
|
||||
This page lists user-visible features and template changes to consider.\&
|
||||
.PP
|
||||
.SS 1.11 (2024)
|
||||
.PP
|
||||
The HTML renderer option for smart fractions support was removed.\& Therefore, 1/8
|
||||
no longer turns into ⅛ or ¹⁄₈.\& The benefit is that something like "doi:
|
||||
10.\&1017/9781009157926.\&007" doesn'\&t turn into "doi: 10.\&1017⁄9781009157926.\&007".\&
|
||||
If you need to change this, take a look at the \fIwikiRenderer\fR function.\&
|
||||
.PP
|
||||
When search terms (excluding hashtags) match the alt text given for an image,
|
||||
that image is part of the data available to the search template.\&
|
||||
.PP
|
||||
If you want to take advantage of this, you'\&ll need to adapt your "search.\&html"
|
||||
template accordingly.\& Use like this, for example:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
{{range \&.Items}}
|
||||
<article lang="{{\&.Language}}">
|
||||
<p><a class="result" href="/view/{{\&.Name}}">{{\&.Title}}</a>
|
||||
<span class="score">{{\&.Score}}</span></p>
|
||||
<blockquote>{{\&.Html}}</blockquote>
|
||||
{{range \&.Images}}
|
||||
<p class="image"><a href="/view/{{\&.Name}}"><img class="last" src="/view/{{\&.Name}}"></a><br/>{{\&.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SS 1.10 (2024)
|
||||
.PP
|
||||
You can now preview edits instead of saving them.\&
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
a preview button was added to "edit.\&html"
|
||||
.IP \(bu 4
|
||||
a new "preview.\&html" was added
|
||||
.PD
|
||||
.PP
|
||||
If you want to take advantage of this, you'\&ll need to adapt your templates
|
||||
accordingly.\& The "preview.\&html" template is a mix of "view.\&html" and
|
||||
"edit.\&html".\&
|
||||
.PP
|
||||
There is an optional change to make to copies of \fIupload.\&html\fR if you upload
|
||||
multiple images at a time.\& Instead of showing just the link to the last upload,
|
||||
you can now show the link (and the images or links, if you want to) to all the
|
||||
files uploaded.\& Use like this, for example:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
Links:<tt>{{range \&.Actual}}<br>{{end}}</tt>
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SS 1.9 (2024)
|
||||
.PP
|
||||
There is a change to make to copies of \fIupload.\&html\fR if subdirectories are being
|
||||
|
||||
@@ -8,6 +8,52 @@ oddmu-releases - what's new in this releases?
|
||||
|
||||
This page lists user-visible features and template changes to consider.
|
||||
|
||||
## 1.11 (2024)
|
||||
|
||||
The HTML renderer option for smart fractions support was removed. Therefore, 1/8
|
||||
no longer turns into ⅛ or ¹⁄₈. The benefit is that something like "doi:
|
||||
10.1017/9781009157926.007" doesn't turn into "doi: 10.1017⁄9781009157926.007".
|
||||
If you need to change this, take a look at the _wikiRenderer_ function.
|
||||
|
||||
When search terms (excluding hashtags) match the alt text given for an image,
|
||||
that image is part of the data available to the search template.
|
||||
|
||||
If you want to take advantage of this, you'll need to adapt your "search.html"
|
||||
template accordingly. Use like this, for example:
|
||||
|
||||
```
|
||||
{{range .Items}}
|
||||
<article lang="{{.Language}}">
|
||||
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
|
||||
<span class="score">{{.Score}}</span></p>
|
||||
<blockquote>{{.Html}}</blockquote>
|
||||
{{range .Images}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img class="last" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
## 1.10 (2024)
|
||||
|
||||
You can now preview edits instead of saving them.
|
||||
|
||||
- a preview button was added to "edit.html"
|
||||
- a new "preview.html" was added
|
||||
|
||||
If you want to take advantage of this, you'll need to adapt your templates
|
||||
accordingly. The "preview.html" template is a mix of "view.html" and
|
||||
"edit.html".
|
||||
|
||||
There is an optional change to make to copies of _upload.html_ if you upload
|
||||
multiple images at a time. Instead of showing just the link to the last upload,
|
||||
you can now show the link (and the images or links, if you want to) to all the
|
||||
files uploaded. Use like this, for example:
|
||||
|
||||
```
|
||||
Links:<tt>{{range .Actual}}<br>{{end}}</tt>
|
||||
```
|
||||
|
||||
## 1.9 (2024)
|
||||
|
||||
There is a change to make to copies of _upload.html_ if subdirectories are being
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-TEMPLATES" "5" "2024-03-14" "File Formats Manual"
|
||||
.TH "ODDMU-TEMPLATES" "5" "2024-07-21" "File Formats Manual"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -14,8 +14,8 @@ oddmu-templates - how to write the templates
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
These files act as HTML templates: \fIadd.\&html\fR, \fIdiff.\&html\fR, \fIedit.\&html\fR,
|
||||
\fIfeed.\&html\fR, \fIsearch.\&html\fR, \fIstatic.\&html\fR, \fIupload.\&html\fR and \fIview.\&html\fR.\& They
|
||||
contain special placeholders in double bracers {{like this}}.\&
|
||||
\fIfeed.\&html\fR, \fIpreview.\&html\fR, \fIsearch.\&html\fR, \fIstatic.\&html\fR, \fIupload.\&html\fR and
|
||||
\fIview.\&html\fR.\& They contain special placeholders in double bracers {{like this}}.\&
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
@@ -32,6 +32,8 @@ placeholders.\&
|
||||
.IP \(bu 4
|
||||
\fIfeed.\&html\fR uses a \fIfeed\fR
|
||||
.IP \(bu 4
|
||||
\fIpreview.\&html\fR uses a \fIpage\fR
|
||||
.IP \(bu 4
|
||||
\fIsearch.\&html\fR uses a \fIsearch\fR
|
||||
.IP \(bu 4
|
||||
\fIstatic.\&html\fR uses a \fIpage\fR
|
||||
@@ -77,14 +79,16 @@ For \fIsearch.\&html\fR, it is a page summary, with bold matches, as HTML.\&
|
||||
For \fIfeed.\&html\fR, it is the escaped (!\&) HTML of the feed item.\&
|
||||
.PD
|
||||
.PP
|
||||
\fI{{.\&Score}}\fR is a numerical score.\& It is only computed for \fIsearch.\&html\fR.\&
|
||||
.PP
|
||||
\fI{{.\&IsBlog}}\fR says whether the current page has a name starting with an ISO
|
||||
date.\&
|
||||
.PP
|
||||
\fI{{.\&Today}}\fR is the current date, in ISO format.\& This is useful for "new page"
|
||||
like links or forms (see \fBEXAMPLE\fR below).\&
|
||||
.PP
|
||||
\fI{{.\&Parents}}\fR is the array of links to parent pages (see \fBEXAMPLE\fR below).\& To
|
||||
refer to them, you need to use a \fI{{range .\&Parents}}\fR … \fI{{end}}\fR construct.\& A
|
||||
link has to properties, \fI{{.\&Title}}\fR and \fI{{.\&Url}}\fR.\&
|
||||
.PP
|
||||
\fI{{.\&Diff}}\fR is the page diff for \fIdiff.\&html\fR.\& It is only computed on demand so
|
||||
it can be used in other templates, too.\& It probably doesn'\&t make much sense to
|
||||
do so, however.\&
|
||||
@@ -111,9 +115,6 @@ An item is a page plus a date.\& All the properties of a page can be used (see
|
||||
\fI{{.\&Dir}}\fR is the directory in which the search starts, percent-escaped except
|
||||
for the slashes.\&
|
||||
.PP
|
||||
\fI{{.\&Items}}\fR is an array of pages (see \fBPage\fR above).\& To refer to them, you need
|
||||
to use a \fI{{range .\&Items}}\fR … \fI{{end}}\fR construct.\&
|
||||
.PP
|
||||
\fI{{.\&Previous}}\fR, \fI{{.\&Page}}\fR and \fI{{.\&Next}}\fR are the previous, current and next
|
||||
page number in the results since doing arithmetics in templates is hard.\& The
|
||||
first page number is 1.\& The last page is expensive to dermine and so that is not
|
||||
@@ -123,6 +124,29 @@ available.\&
|
||||
.PP
|
||||
\fI{{.\&Results}}\fR indicates if there were any search results at all.\&
|
||||
.PP
|
||||
\fI{{.\&Items}}\fR is an array of results.\& To refer to them, you need to use a
|
||||
\fI{{range .\&Items}}\fR … \fI{{end}}\fR construct.\&
|
||||
.PP
|
||||
A result is a page plus a score and possibly images.\& All the properties of a
|
||||
page can be used (see \fBPage\fR above).\&
|
||||
.PP
|
||||
\fI{{.\&Score}}\fR is a numerical score.\& It is only computed for \fIsearch.\&html\fR.\&
|
||||
.PP
|
||||
\fI{{.\&Images}}\fR are the images where the alt-text matches at least one of the
|
||||
query terms (but not predicates and not hashtags since those apply to the page
|
||||
as a whole).\& To refer to them, you need to use a \fI{{range .\&Images}}\fR … \fI{{end}}\fR
|
||||
construct.\&
|
||||
.PP
|
||||
Each image has three properties:
|
||||
.PP
|
||||
\fI{{.\&Title}}\fR is the alt-text of the image.\& It can never be empty because images
|
||||
are only listed if a search term matches.\&
|
||||
.PP
|
||||
\fI{{.\&Name}}\fR is the file name for use in URLs.\&
|
||||
.PP
|
||||
\fI{{.\&Html}}\fR the image alt-text with a bold tag used to highlight the first
|
||||
search term that matched.\&
|
||||
.PP
|
||||
.SS Upload
|
||||
.PP
|
||||
\fI{{.\&Dir}}\fR is the directory where the uploaded file ends up, based on the URL
|
||||
@@ -130,7 +154,15 @@ path, percent-escaped except for the slashes.\&
|
||||
.PP
|
||||
\fI{{.\&Name}}\fR is the \fIfilename\fR query parameter.\&
|
||||
.PP
|
||||
\fI{{.\&Last}}\fR is the filename of the last image uploaded.\&
|
||||
\fI{{.\&Last}}\fR is the filename of the last file uploaded.\&
|
||||
.PP
|
||||
\fI{{.\&Actual}}\fR is an array of filenames of all the files uploaded.\& Use {{range
|
||||
Actual}} … {{.\&}} … {{end}} to loop over all the filenames.\&
|
||||
.PP
|
||||
\fI{{.\&Base}}\fR is the basename of the first file uploaded (without the directory,
|
||||
extension and numeric part at the end), escaped for use in URLs.\&
|
||||
.PP
|
||||
\fI{{.\&Title}}\fR is the title of the basename, if it exists.\&
|
||||
.PP
|
||||
\fI{{.\&Image}}\fR is a boolean to indicate whether the last file uploaded has a file
|
||||
name indicating an image or not (such as ending in \fI.\&jpg\fR).\& If so, a thumbnail
|
||||
@@ -202,6 +234,16 @@ itself is a blog page.\& Useful for \fIadd.\&html\fR:
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The following adds a list of links to parent directories.\& Useful for \fIview.\&html\fR:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
<nav>
|
||||
{{range \&.Parents}}/ <a href="{{\&.Url}}">{{\&.Title}}</a>{{end}}
|
||||
</nav>
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH NOTES
|
||||
.PP
|
||||
The templates are always used as-is, irrespective of the current directory.\&
|
||||
|
||||
@@ -7,8 +7,8 @@ oddmu-templates - how to write the templates
|
||||
# SYNOPSIS
|
||||
|
||||
These files act as HTML templates: _add.html_, _diff.html_, _edit.html_,
|
||||
_feed.html_, _search.html_, _static.html_, _upload.html_ and _view.html_. They
|
||||
contain special placeholders in double bracers {{like this}}.
|
||||
_feed.html_, _preview.html_, _search.html_, _static.html_, _upload.html_ and
|
||||
_view.html_. They contain special placeholders in double bracers {{like this}}.
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
@@ -19,6 +19,7 @@ placeholders.
|
||||
- _diff.html_ uses a _page_
|
||||
- _edit.html_ uses a _page_
|
||||
- _feed.html_ uses a _feed_
|
||||
- _preview.html_ uses a _page_
|
||||
- _search.html_ uses a _search_
|
||||
- _static.html_ uses a _page_
|
||||
- _upload.html_ uses an _upload_
|
||||
@@ -55,14 +56,16 @@ _{{.Html}}_ contains some sort of HTML that depends on the template used.
|
||||
- For _search.html_, it is a page summary, with bold matches, as HTML.
|
||||
- For _feed.html_, it is the escaped (!) HTML of the feed item.
|
||||
|
||||
_{{.Score}}_ is a numerical score. It is only computed for _search.html_.
|
||||
|
||||
_{{.IsBlog}}_ says whether the current page has a name starting with an ISO
|
||||
date.
|
||||
|
||||
_{{.Today}}_ is the current date, in ISO format. This is useful for "new page"
|
||||
like links or forms (see *EXAMPLE* below).
|
||||
|
||||
_{{.Parents}}_ is the array of links to parent pages (see *EXAMPLE* below). To
|
||||
refer to them, you need to use a _{{range .Parents}}_ … _{{end}}_ construct. A
|
||||
link has to properties, _{{.Title}}_ and _{{.Url}}_.
|
||||
|
||||
_{{.Diff}}_ is the page diff for _diff.html_. It is only computed on demand so
|
||||
it can be used in other templates, too. It probably doesn't make much sense to
|
||||
do so, however.
|
||||
@@ -89,9 +92,6 @@ _{{.Query}}_ is the query string.
|
||||
_{{.Dir}}_ is the directory in which the search starts, percent-escaped except
|
||||
for the slashes.
|
||||
|
||||
_{{.Items}}_ is an array of pages (see *Page* above). To refer to them, you need
|
||||
to use a _{{range .Items}}_ … _{{end}}_ construct.
|
||||
|
||||
_{{.Previous}}_, _{{.Page}}_ and _{{.Next}}_ are the previous, current and next
|
||||
page number in the results since doing arithmetics in templates is hard. The
|
||||
first page number is 1. The last page is expensive to dermine and so that is not
|
||||
@@ -101,6 +101,29 @@ _{{.More}}_ indicates if there are any more search results.
|
||||
|
||||
_{{.Results}}_ indicates if there were any search results at all.
|
||||
|
||||
_{{.Items}}_ is an array of results. To refer to them, you need to use a
|
||||
_{{range .Items}}_ … _{{end}}_ construct.
|
||||
|
||||
A result is a page plus a score and possibly images. All the properties of a
|
||||
page can be used (see *Page* above).
|
||||
|
||||
_{{.Score}}_ is a numerical score. It is only computed for _search.html_.
|
||||
|
||||
_{{.Images}}_ are the images where the alt-text matches at least one of the
|
||||
query terms (but not predicates and not hashtags since those apply to the page
|
||||
as a whole). To refer to them, you need to use a _{{range .Images}}_ … _{{end}}_
|
||||
construct.
|
||||
|
||||
Each image has three properties:
|
||||
|
||||
_{{.Title}}_ is the alt-text of the image. It can never be empty because images
|
||||
are only listed if a search term matches.
|
||||
|
||||
_{{.Name}}_ is the file name for use in URLs.
|
||||
|
||||
_{{.Html}}_ the image alt-text with a bold tag used to highlight the first
|
||||
search term that matched.
|
||||
|
||||
## Upload
|
||||
|
||||
_{{.Dir}}_ is the directory where the uploaded file ends up, based on the URL
|
||||
@@ -108,7 +131,15 @@ path, percent-escaped except for the slashes.
|
||||
|
||||
_{{.Name}}_ is the _filename_ query parameter.
|
||||
|
||||
_{{.Last}}_ is the filename of the last image uploaded.
|
||||
_{{.Last}}_ is the filename of the last file uploaded.
|
||||
|
||||
_{{.Actual}}_ is an array of filenames of all the files uploaded. Use {{range
|
||||
.Actual}} … {{.}} … {{end}} to loop over all the filenames.
|
||||
|
||||
_{{.Base}}_ is the basename of the first file uploaded (without the directory,
|
||||
extension and numeric part at the end), escaped for use in URLs.
|
||||
|
||||
_{{.Title}}_ is the title of the basename, if it exists.
|
||||
|
||||
_{{.Image}}_ is a boolean to indicate whether the last file uploaded has a file
|
||||
name indicating an image or not (such as ending in _.jpg_). If so, a thumbnail
|
||||
@@ -174,6 +205,14 @@ itself is a blog page. Useful for _add.html_:
|
||||
autofocus required>{{- if .IsBlog}}**{{.Today}}**. {{end}}</textarea>
|
||||
```
|
||||
|
||||
The following adds a list of links to parent directories. Useful for _view.html_:
|
||||
|
||||
```
|
||||
<nav>
|
||||
{{range .Parents}}/ <a href="{{.Url}}">{{.Title}}</a>{{end}}
|
||||
</nav>
|
||||
```
|
||||
|
||||
# NOTES
|
||||
|
||||
The templates are always used as-is, irrespective of the current directory.
|
||||
|
||||
43
man/oddmu.1
43
man/oddmu.1
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "1" "2024-03-07"
|
||||
.TH "ODDMU" "1" "2024-05-11"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -63,6 +63,8 @@ directory:
|
||||
.IP \(bu 4
|
||||
\fI/edit/dir/name\fR shows a form to edit a page
|
||||
.IP \(bu 4
|
||||
\fI/preview/dir/name\fR shows a preview of a page edit and the form to edit it
|
||||
.IP \(bu 4
|
||||
\fI/save/dir/name\fR saves an edit
|
||||
.IP \(bu 4
|
||||
\fI/add/dir/name\fR shows a form to add to a page
|
||||
@@ -136,28 +138,8 @@ curl --remote-name \&'http://localhost:8080/archive/man/man\&.zip
|
||||
.PP
|
||||
.SH CONFIGURATION
|
||||
.PP
|
||||
The template files are the HTML files in the working directory:
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
\fIview.\&html\fR shows a page
|
||||
.IP \(bu 4
|
||||
\fIdiff.\&html\fR shows the last change to a page
|
||||
.IP \(bu 4
|
||||
\fIedit.\&html\fR shows a form to edit a page
|
||||
.IP \(bu 4
|
||||
\fIadd.\&html\fR shows a form to add to a page
|
||||
.IP \(bu 4
|
||||
\fIupload.\&html\fR shows a form to upload a file
|
||||
.IP \(bu 4
|
||||
\fIsearch.\&html\fR shows the search results
|
||||
.IP \(bu 4
|
||||
\fIstatic.\&html\fR is used to generate a static site
|
||||
.IP \(bu 4
|
||||
\fIfeed.\&html\fR is used to generate a RSS feed
|
||||
.PD
|
||||
.PP
|
||||
Please change the templates!\&
|
||||
The template files are the HTML files in the working directory.\& Please change
|
||||
these templates!\&
|
||||
.PP
|
||||
The first change you should make is to replace the name and email address in the
|
||||
footer of \fIview.\&html\fR.\& Look for "Your Name" and "example.\&org".\&
|
||||
@@ -204,21 +186,20 @@ ODDMU_FILTER can be used to exclude subdirectories from such tree actions.\& See
|
||||
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
|
||||
through socket activation.\& The advantage of this method is that you can use a
|
||||
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
|
||||
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5) and
|
||||
\fIoddmu-apache\fR(5) for an example of how to use socket activation with a
|
||||
Unix-domain socket under systemd and Apache.\&
|
||||
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5),
|
||||
\fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for an example of how to use socket
|
||||
activation with a Unix-domain socket under systemd and Apache.\&
|
||||
.PP
|
||||
.SH SECURITY
|
||||
.PP
|
||||
If the machine you are running Oddmu on is accessible from the Internet, you
|
||||
must secure your installation.\& The best way to do this is use a regular web
|
||||
server as a reverse proxy.\&
|
||||
.PP
|
||||
See \fIoddmu-apache\fR(5) for an example.\&
|
||||
server as a reverse proxy.\& See \fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for
|
||||
example configurations.\&
|
||||
.PP
|
||||
Oddmu assumes that all the users that can edit pages or upload files are trusted
|
||||
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
|
||||
HTML sanitization!\&
|
||||
users and therefore their content is trusted.\& Oddmu does not perform HTML
|
||||
sanitization!\&
|
||||
.PP
|
||||
For an extra dose of security, consider using a Unix-domain socket.\&
|
||||
.PP
|
||||
|
||||
@@ -47,6 +47,7 @@ directory:
|
||||
- _/view/dir/name.rss_ shows the RSS feed for the pages linked
|
||||
- _/diff/dir/name_ shows the last change to a page
|
||||
- _/edit/dir/name_ shows a form to edit a page
|
||||
- _/preview/dir/name_ shows a preview of a page edit and the form to edit it
|
||||
- _/save/dir/name_ saves an edit
|
||||
- _/add/dir/name_ shows a form to add to a page
|
||||
- _/append/dir/name_ appends an addition to a page
|
||||
@@ -103,18 +104,8 @@ curl --remote-name 'http://localhost:8080/archive/man/man.zip
|
||||
|
||||
# CONFIGURATION
|
||||
|
||||
The template files are the HTML files in the working directory:
|
||||
|
||||
- _view.html_ shows a page
|
||||
- _diff.html_ shows the last change to a page
|
||||
- _edit.html_ shows a form to edit a page
|
||||
- _add.html_ shows a form to add to a page
|
||||
- _upload.html_ shows a form to upload a file
|
||||
- _search.html_ shows the search results
|
||||
- _static.html_ is used to generate a static site
|
||||
- _feed.html_ is used to generate a RSS feed
|
||||
|
||||
Please change the templates!
|
||||
The template files are the HTML files in the working directory. Please change
|
||||
these templates!
|
||||
|
||||
The first change you should make is to replace the name and email address in the
|
||||
footer of _view.html_. Look for "Your Name" and "example.org".
|
||||
@@ -159,21 +150,20 @@ _oddmu-filter_(7) and _oddmu-apache_(5).
|
||||
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
|
||||
through socket activation. The advantage of this method is that you can use a
|
||||
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
|
||||
the socket are set before the program starts. See _oddmu.service_(5) and
|
||||
_oddmu-apache_(5) for an example of how to use socket activation with a
|
||||
Unix-domain socket under systemd and Apache.
|
||||
the socket are set before the program starts. See _oddmu.service_(5),
|
||||
_oddmu-apache_(5) and _oddmu-nginx_(5) for an example of how to use socket
|
||||
activation with a Unix-domain socket under systemd and Apache.
|
||||
|
||||
# SECURITY
|
||||
|
||||
If the machine you are running Oddmu on is accessible from the Internet, you
|
||||
must secure your installation. The best way to do this is use a regular web
|
||||
server as a reverse proxy.
|
||||
|
||||
See _oddmu-apache_(5) for an example.
|
||||
server as a reverse proxy. See _oddmu-apache_(5) and _oddmu-nginx_(5) for
|
||||
example configurations.
|
||||
|
||||
Oddmu assumes that all the users that can edit pages or upload files are trusted
|
||||
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
|
||||
HTML sanitization!
|
||||
users and therefore their content is trusted. Oddmu does not perform HTML
|
||||
sanitization!
|
||||
|
||||
For an extra dose of security, consider using a Unix-domain socket.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU.SERVICE" "5" "2024-02-17"
|
||||
.TH "ODDMU.SERVICE" "5" "2024-04-21"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -32,13 +32,14 @@ all the templates files ending in ".\&html" from the source distribution to
|
||||
If you want to keep everything in one place, copy the binary "oddmu" and the
|
||||
service file "oddmu.\&service" to "/home/oddmu", too.\&
|
||||
.PP
|
||||
Edit the `oddmu.\&service` file.\& These are the lines you most likely have to take
|
||||
Edit the "oddmu.\&service" file.\& These are the lines you most likely have to take
|
||||
care of:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
ReadWritePaths=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
Environment="ODDMU_WEBFINGER=1"
|
||||
.fi
|
||||
@@ -71,15 +72,6 @@ journalctl --follow --unit oddmu
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
For it to restart when the server reboots:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
sudo ln -sf /home/oddmu/oddmu\&.service
|
||||
/etc/systemd/system/multi-user\&.target\&.wants/
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH Socket Activation
|
||||
.PP
|
||||
Alternatively, you can let systemd handle the creation of the listening socket,
|
||||
@@ -91,19 +83,58 @@ tells systemd to pass the socket to the service as its standard input.\&
|
||||
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
|
||||
Oddmu for each connection.\&
|
||||
.PP
|
||||
The instructions for starting and enabling the systemd service are almost
|
||||
exactly the same as those in the previous section, with "oddmu.\&service" replaced
|
||||
by "oddmu-unix-domain.\&service".\& You'\&ll also need to run the following:
|
||||
Instead of using "oddmu.\&service", you need to use "oddmu-unix-domain.\&socket" and
|
||||
"oddmu-unix-domain.\&service".\&
|
||||
.PP
|
||||
The unit file for the socket defines a file name.\& You probably need to create
|
||||
the directory or change the file name.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
sudo mkdir /run/oddmu
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The unit file for the service defines where the "oddmu" is and where the data
|
||||
directory is.\& These are the lines you most likely have to take care of:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
ReadWritePaths=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
Environment="ODDMU_WEBFINGER=1"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
To install, enable and start both units:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
|
||||
ln -s /home/oddmu/oddmu-unix-domain\&.service /etc/systemd/system
|
||||
systemctl enable --now oddmu-unix-domain\&.socket
|
||||
systemctl enable --now oddmu-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
To test just the unix domain socket, use \fIncat(1)\fR:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
|
||||
| ncat --unixsock /run/oddmu/oddmu\&.sock
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Now you need to set up your web browser to use the Unix domain socket.\& See
|
||||
\fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example configurations.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIsystemd.\&socket(5), \fRcapabilities_(7)
|
||||
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
|
||||
\fIsystemd.\&socket(5), \fRcapabilities_(7)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
|
||||
@@ -23,12 +23,13 @@ all the templates files ending in ".html" from the source distribution to
|
||||
If you want to keep everything in one place, copy the binary "oddmu" and the
|
||||
service file "oddmu.service" to "/home/oddmu", too.
|
||||
|
||||
Edit the `oddmu.service` file. These are the lines you most likely have to take
|
||||
Edit the "oddmu.service" file. These are the lines you most likely have to take
|
||||
care of:
|
||||
|
||||
```
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
ReadWritePaths=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
Environment="ODDMU_WEBFINGER=1"
|
||||
```
|
||||
@@ -54,13 +55,6 @@ Follow the log:
|
||||
journalctl --follow --unit oddmu
|
||||
```
|
||||
|
||||
For it to restart when the server reboots:
|
||||
|
||||
```
|
||||
sudo ln -sf /home/oddmu/oddmu.service \
|
||||
/etc/systemd/system/multi-user.target.wants/
|
||||
```
|
||||
|
||||
# Socket Activation
|
||||
|
||||
Alternatively, you can let systemd handle the creation of the listening socket,
|
||||
@@ -72,17 +66,50 @@ tells systemd to pass the socket to the service as its standard input.
|
||||
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
|
||||
Oddmu for each connection.
|
||||
|
||||
The instructions for starting and enabling the systemd service are almost
|
||||
exactly the same as those in the previous section, with "oddmu.service" replaced
|
||||
by "oddmu-unix-domain.service". You'll also need to run the following:
|
||||
Instead of using "oddmu.service", you need to use "oddmu-unix-domain.socket" and
|
||||
"oddmu-unix-domain.service".
|
||||
|
||||
The unit file for the socket defines a file name. You probably need to create
|
||||
the directory or change the file name.
|
||||
|
||||
```
|
||||
sudo mkdir /run/oddmu
|
||||
```
|
||||
|
||||
The unit file for the service defines where the "oddmu" is and where the data
|
||||
directory is. These are the lines you most likely have to take care of:
|
||||
|
||||
```
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
ReadWritePaths=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
Environment="ODDMU_WEBFINGER=1"
|
||||
```
|
||||
|
||||
To install, enable and start both units:
|
||||
|
||||
```
|
||||
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
|
||||
ln -s /home/oddmu/oddmu-unix-domain.service /etc/systemd/system
|
||||
systemctl enable --now oddmu-unix-domain.socket
|
||||
systemctl enable --now oddmu-unix-domain.service
|
||||
```
|
||||
|
||||
To test just the unix domain socket, use _ncat(1)_:
|
||||
|
||||
```
|
||||
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
|
||||
| ncat --unixsock /run/oddmu/oddmu.sock
|
||||
```
|
||||
|
||||
Now you need to set up your web browser to use the Unix domain socket. See
|
||||
_oddmu-apache_(5) or _oddmu-nginx_(5) for example configurations.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _systemd.exec_(5), _systemd.socket(5), _capabilities_(7)
|
||||
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
|
||||
_systemd.socket(5), _capabilities_(7)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ WantedBy=multi-user.target
|
||||
Type=simple
|
||||
Restart=always
|
||||
DynamicUser=true
|
||||
MemoryMax=100M
|
||||
MemoryHigh=120M
|
||||
MemoryMax=120M
|
||||
MemoryHigh=100M
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
|
||||
53
page.go
53
page.go
@@ -14,21 +14,26 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Page is a struct containing information about a single page. Title
|
||||
// is the title extracted from the page content using titleRegexp.
|
||||
// Name is the path without extension (so a path of "foo.md"
|
||||
// results in the Name "foo"). Body is the Markdown content of the
|
||||
// page and Html is the rendered HTML for that Markdown. Score is a
|
||||
// number indicating how well the page matched for a search query.
|
||||
// Page is a struct containing information about a single page. Title is the title extracted from the page content using
|
||||
// titleRegexp. Name is the path without extension (so a path of "foo.md" results in the Name "foo"). Body is the
|
||||
// Markdown content of the page and Html is the rendered HTML for that Markdown.
|
||||
type Page struct {
|
||||
Title string
|
||||
Name string
|
||||
Body []byte
|
||||
Html template.HTML
|
||||
Score int
|
||||
Hashtags []string
|
||||
}
|
||||
|
||||
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
|
||||
// the Name "foo").
|
||||
type Link struct {
|
||||
Title string
|
||||
Url string
|
||||
}
|
||||
|
||||
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
|
||||
// (YYYY-MM-DD), then it's a blog page.
|
||||
var blogRe = regexp.MustCompile(`^\d\d\d\d-\d\d-\d\d`)
|
||||
|
||||
// santizeStrict uses bluemonday to sanitize the HTML away. No elements are allowed except for the b tag because this is
|
||||
@@ -125,12 +130,6 @@ func (p *Page) handleTitle(replace bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// score sets Page.Title and computes Page.Score.
|
||||
func (p *Page) score(q string) {
|
||||
p.handleTitle(true)
|
||||
p.Score = score(q, string(p.Body)) + score(q, p.Title)
|
||||
}
|
||||
|
||||
// summarize sets Page.Html to an extract.
|
||||
func (p *Page) summarize(q string) {
|
||||
t := p.plainText()
|
||||
@@ -154,8 +153,8 @@ func (p *Page) Dir() string {
|
||||
return d + "/"
|
||||
}
|
||||
|
||||
// Base returns the basename of the page name: no directory and no extension. This is used to create the upload link
|
||||
// in "view.html", for example.
|
||||
// Base returns the basename of the page name: no directory. This is used to create the upload link in "view.html", for
|
||||
// example.
|
||||
func (p *Page) Base() string {
|
||||
n := filepath.Base(p.Name)
|
||||
if n == "." {
|
||||
@@ -168,3 +167,27 @@ func (p *Page) Base() string {
|
||||
func (p *Page) Today() string {
|
||||
return time.Now().Format(time.DateOnly)
|
||||
}
|
||||
|
||||
// Parents returns a Link array to parent pages, up the directory structure.
|
||||
func (p *Page) Parents() []*Link {
|
||||
links := make([]*Link, 0)
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
// foo/bar/baz ⇒ index, foo/index
|
||||
elems := strings.Split(p.Name, "/")
|
||||
if len(elems) == 1 {
|
||||
return links
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < len(elems) - 1; i++ {
|
||||
name := s + "index"
|
||||
title, ok := index.titles[name]
|
||||
if !ok {
|
||||
title = "…"
|
||||
}
|
||||
link := &Link{ Title: title, Url: strings.Repeat("../", len(elems) - i - 1) + "index" }
|
||||
links = append(links, link)
|
||||
s += elems[i] + "/"
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
27
page_test.go
27
page_test.go
@@ -40,3 +40,30 @@ Moonlight floods the aisle`)}
|
||||
// But the backup still exists.
|
||||
assert.FileExists(t, "testdata/dir/moon.md~")
|
||||
}
|
||||
|
||||
func TestPageParents(t *testing.T) {
|
||||
cleanup(t, "testdata/parents")
|
||||
index.load()
|
||||
p := &Page{Name: "testdata/parents/index", Body: []byte(`# Solar
|
||||
The air dances here
|
||||
Water puddles flicker and
|
||||
disappear anon`)}
|
||||
p.save()
|
||||
p = &Page{Name: "testdata/parents/children/index", Body: []byte(`# Lunar
|
||||
Behind running clouds
|
||||
Shines cold light from ages past
|
||||
And untouchable`)}
|
||||
p.save()
|
||||
p = &Page{Name: "testdata/parents/children/something/other"}
|
||||
// "testdata/parents/children/something/index" is a sibling and doesn't count!
|
||||
parents := p.Parents()
|
||||
assert.Equal(t, "Welcome to Oddµ", parents[0].Title)
|
||||
assert.Equal(t, "../../../../index", parents[0].Url)
|
||||
assert.Equal(t, "…", parents[1].Title)
|
||||
assert.Equal(t, "../../../index", parents[1].Url)
|
||||
assert.Equal(t, "Solar", parents[2].Title)
|
||||
assert.Equal(t, "../../index", parents[2].Url)
|
||||
assert.Equal(t, "Lunar", parents[3].Title)
|
||||
assert.Equal(t, "../index", parents[3].Url)
|
||||
assert.Equal(t, 4, len(parents))
|
||||
}
|
||||
|
||||
58
parser.go
58
parser.go
@@ -7,12 +7,14 @@ import (
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// wikiLink returns an inline parser function. This indirection is
|
||||
// required because we want to call the previous definition in case
|
||||
// this is not a wikiLink.
|
||||
func wikiLink(p *parser.Parser, fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
|
||||
func wikiLink(fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
|
||||
return func(p *parser.Parser, original []byte, offset int) (int, ast.Node) {
|
||||
data := original[offset:]
|
||||
n := len(data)
|
||||
@@ -44,7 +46,7 @@ func hashtag() (func(p *parser.Parser, data []byte, offset int) (int, ast.Node),
|
||||
for i < n && !parser.IsSpace(data[i]) {
|
||||
i++
|
||||
}
|
||||
if i == 0 {
|
||||
if i <= 1 {
|
||||
return 0, nil
|
||||
}
|
||||
hashtags = append(hashtags, string(data[1:i]))
|
||||
@@ -64,7 +66,7 @@ func wikiParser() (*parser.Parser, *[]string) {
|
||||
extensions := (parser.CommonExtensions | parser.AutoHeadingIDs | parser.Attributes) & ^parser.MathJax
|
||||
parser := parser.NewWithExtensions(extensions)
|
||||
prev := parser.RegisterInline('[', nil)
|
||||
parser.RegisterInline('[', wikiLink(parser, prev))
|
||||
parser.RegisterInline('[', wikiLink(prev))
|
||||
fn, hashtags := hashtag()
|
||||
parser.RegisterInline('#', fn)
|
||||
if useWebfinger {
|
||||
@@ -73,17 +75,17 @@ func wikiParser() (*parser.Parser, *[]string) {
|
||||
return parser, hashtags
|
||||
}
|
||||
|
||||
// wikiRenderer is a Renderer for Markdown that adds lazy loading of images. This in turn requires an exception for the
|
||||
// sanitization policy!
|
||||
// wikiRenderer is a Renderer for Markdown that adds lazy loading of images and disables fractions support. Remember
|
||||
// that there is no HTML sanitization.
|
||||
func wikiRenderer() *html.Renderer {
|
||||
htmlFlags := html.CommonFlags | html.LazyLoadImages
|
||||
// sync with staticPage
|
||||
htmlFlags := html.CommonFlags & ^html.SmartypantsFractions | html.LazyLoadImages
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
return renderer
|
||||
}
|
||||
|
||||
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Language, Page.Hashtags, and escapes Page.Name.
|
||||
// Note: If the rendered HTML doesn't contain the attributes or elements you expect it to contain, check sanitizeBytes!
|
||||
func (p *Page) renderHtml() {
|
||||
parser, hashtags := wikiParser()
|
||||
renderer := wikiRenderer()
|
||||
@@ -119,3 +121,45 @@ func (p *Page) plainText() string {
|
||||
}
|
||||
return string(text)
|
||||
}
|
||||
|
||||
// images returns an array of ImageData.
|
||||
func (p *Page) images() []ImageData {
|
||||
dir := path.Dir(filepath.ToSlash(p.Name))
|
||||
images := make([]ImageData, 0)
|
||||
parser := parser.New()
|
||||
doc := markdown.Parse(p.Body, parser)
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
if entering {
|
||||
switch v := node.(type) {
|
||||
case *ast.Image:
|
||||
// not an absolute URL, not a full URL, not a mailto: URI
|
||||
text := toString(v)
|
||||
if len(text) > 0 {
|
||||
name := path.Join(dir, string(v.Destination))
|
||||
image := ImageData{Title: text, Name: name}
|
||||
images = append(images, image)
|
||||
}
|
||||
return ast.SkipChildren
|
||||
}
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
return images
|
||||
}
|
||||
|
||||
// toString for a node returns the text nodes' literals, concatenated. There is no whitespace added so the expectation
|
||||
// is that there is only one child node. Otherwise, there may be a space missing between the literals, depending on the
|
||||
// exact child nodes they belong to.
|
||||
func toString(node ast.Node) string {
|
||||
b := new(bytes.Buffer)
|
||||
ast.WalkFunc(node, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
if entering {
|
||||
switch v := node.(type) {
|
||||
case *ast.Text:
|
||||
b.Write(v.Literal)
|
||||
}
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -48,6 +48,18 @@ I am cold, alone</p>
|
||||
assert.Equal(t, r, string(p.Html))
|
||||
}
|
||||
|
||||
func TestPageHtmlHashtagCornerCases(t *testing.T) {
|
||||
p := &Page{Body: []byte(`#
|
||||
|
||||
ok # #o #ok`)}
|
||||
p.renderHtml()
|
||||
r := `<p>#</p>
|
||||
|
||||
<p>ok # <a class="tag" href="/search/?q=%23o">#o</a> <a class="tag" href="/search/?q=%23ok">#ok</a></p>
|
||||
`
|
||||
assert.Equal(t, r, string(p.Html))
|
||||
}
|
||||
|
||||
func TestPageHtmlWikiLink(t *testing.T) {
|
||||
p := &Page{Body: []byte(`# Photos and Books
|
||||
Blue and green and black
|
||||
@@ -83,3 +95,17 @@ func TestLazyLoadImages(t *testing.T) {
|
||||
p.renderHtml()
|
||||
assert.Contains(t, string(p.Html), "lazy")
|
||||
}
|
||||
|
||||
// The fractions available in Latin 1 (?) are rendered.
|
||||
func TestFractions(t *testing.T) {
|
||||
p := &Page{Body: []byte(`1/4`)}
|
||||
p.renderHtml()
|
||||
assert.Contains(t, string(p.Html), "¼")
|
||||
}
|
||||
|
||||
// Other fractions are not rendered.
|
||||
func TestNoFractions(t *testing.T) {
|
||||
p := &Page{Body: []byte(`1/6`)}
|
||||
p.renderHtml()
|
||||
assert.Contains(t, string(p.Html), "1/6")
|
||||
}
|
||||
|
||||
20
preview.go
Normal file
20
preview.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// previewHandler is a bit like saveHandler and viewHandler. Instead of saving the date to a page, we create a synthetic
|
||||
// Page and render it. Note that when saving, the carriage returns (\r) are removed. We need to do this as well,
|
||||
// otherwise the rendered template has garbage bytes at the end. Note also that we need to remove the title from the
|
||||
// page so that the preview works as intended (and much like the "view.html" template) where as the editing requires the
|
||||
// page content including the header… which is why it needs to be added in the "preview.html" template. This makes me
|
||||
// sad.
|
||||
func previewHandler(w http.ResponseWriter, r *http.Request, path string) {
|
||||
body := strings.ReplaceAll(r.FormValue("body"), "\r", "")
|
||||
p := &Page{Name: path, Body: []byte(body)}
|
||||
p.handleTitle(true)
|
||||
p.renderHtml()
|
||||
renderTemplate(w, p.Dir(), "preview", p)
|
||||
}
|
||||
39
preview.html
Normal file
39
preview.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{.Language}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Preview: {{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
input#search { width: 12ch; }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
|
||||
footer { border-top: 1px solid #888 }
|
||||
img { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="#edit">Skip to edit form</a>
|
||||
</header>
|
||||
<main>
|
||||
<h1>Previewing {{.Title}}</h1>
|
||||
{{.Html}}
|
||||
</main>
|
||||
<hr>
|
||||
<section id="edit">
|
||||
<h2>Editing {{.Title}}</h2>
|
||||
<form action="/save/{{.Name}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" lang="{{.Language}}" autofocus>{{printf "# %s\n\n%s" .Title .Body}}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Save">
|
||||
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -92,13 +92,16 @@ func TestScoreSubstring(t *testing.T) {
|
||||
|
||||
func TestScorePageAndMarkup(t *testing.T) {
|
||||
s := `The Transjovian Council accepts new members. If you think we'd be a good fit, apply for an account. Contact [Alex Schroeder](https://alexschroeder.ch/wiki/Contact). Mail is best. Encrypted mail is best. [Delta Chat](https://delta.chat/de/) is a messenger app that uses encrypted mail. It's the bestest best.`
|
||||
p := &Page{Title: "Test", Name: "Test", Body: []byte(s)}
|
||||
r := &Result{}
|
||||
r.Title = "Test"
|
||||
r.Name = "Test"
|
||||
r.Body = []byte(s)
|
||||
q := "wiki"
|
||||
p.score(q)
|
||||
r.score(q)
|
||||
// "wiki" is not visible in the plain text but the score is no affected:
|
||||
// - wiki, all, whole, beginning, end (5)
|
||||
if p.Score != 5 {
|
||||
t.Logf("%s score is %d", q, p.Score)
|
||||
if r.Score != 5 {
|
||||
t.Logf("%s score is %d", q, r.Score)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
60
search.go
60
search.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -13,6 +14,14 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Result is a page plus image data. Page is the page being used as the search result. Score is a number indicating how
|
||||
// well the page matched for a search query. Images are the images whose description match the query.
|
||||
type Result struct {
|
||||
Page
|
||||
Score int
|
||||
Images []ImageData
|
||||
}
|
||||
|
||||
// Search is a struct containing the result of a search. Query is the
|
||||
// query string and Items is the array of pages with the result.
|
||||
// Currently there is no pagination of results! When a page is part of
|
||||
@@ -20,7 +29,7 @@ import (
|
||||
type Search struct {
|
||||
Query string
|
||||
Dir string
|
||||
Items []*Page
|
||||
Items []*Result
|
||||
Previous int
|
||||
Page int
|
||||
Next int
|
||||
@@ -94,9 +103,9 @@ const itemsPerPage = 20
|
||||
// size is 20. Specify either the page number to return, or that all the results should be returned. Only ask for all
|
||||
// results if runtime is not an issue, like on the command line. The boolean return value indicates whether there are
|
||||
// more results.
|
||||
func search(q, dir, filter string, page int, all bool) ([]*Page, bool) {
|
||||
func search(q, dir, filter string, page int, all bool) ([]*Result, bool) {
|
||||
if len(q) == 0 {
|
||||
return make([]*Page, 0), false
|
||||
return make([]*Result, 0), false
|
||||
}
|
||||
names := index.search(q) // hashtags or all names
|
||||
names = filterPath(names, dir, filter)
|
||||
@@ -104,16 +113,51 @@ func search(q, dir, filter string, page int, all bool) ([]*Page, bool) {
|
||||
names = filterNames(names, predicates)
|
||||
index.RLock()
|
||||
slices.SortFunc(names, sortNames(terms))
|
||||
index.RUnlock()
|
||||
index.RUnlock() // unlock because grep takes long
|
||||
names, keepFirst := prependQueryPage(names, dir, q)
|
||||
from := itemsPerPage * (page - 1)
|
||||
to := from + itemsPerPage - 1
|
||||
items, more := grep(terms, names, from, to, all, keepFirst)
|
||||
for _, p := range items {
|
||||
p.score(q)
|
||||
p.summarize(q)
|
||||
results := make([]*Result, len(items))
|
||||
for i, p := range items {
|
||||
r := &Result{}
|
||||
r.Title = p.Title
|
||||
r.Name = p.Name
|
||||
r.Body = p.Body
|
||||
// Hashtags aren't computed and Html is getting overwritten anyway
|
||||
r.summarize(q)
|
||||
r.score(q)
|
||||
results[i] = r
|
||||
}
|
||||
return items, more
|
||||
if len(terms) > 0 {
|
||||
index.RLock()
|
||||
res := make([]ImageData, 0)
|
||||
for _, r := range results {
|
||||
ImageLoop:
|
||||
for _, img := range index.images[r.Name] {
|
||||
title := strings.ToLower(img.Title)
|
||||
for _, term := range terms {
|
||||
if strings.Contains(title, term) {
|
||||
re, err := re(term)
|
||||
if err == nil {
|
||||
img.Html = template.HTML(highlight(re, img.Title))
|
||||
}
|
||||
res = append(res, img)
|
||||
continue ImageLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
r.Images = res
|
||||
}
|
||||
index.RUnlock()
|
||||
}
|
||||
return results, more
|
||||
}
|
||||
|
||||
// score sets Page.Title and computes Page.Score.
|
||||
func (r *Result) score(q string) {
|
||||
r.handleTitle(true)
|
||||
r.Score = score(q, string(r.Body)) + score(q, r.Title)
|
||||
}
|
||||
|
||||
// filterPath filters the names by prefix and by a regular expression. A prefix of "." means that all the names are
|
||||
|
||||
@@ -12,9 +12,10 @@ header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
input#search { width: 20ch; }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
|
||||
img { max-width: 20%; }
|
||||
.result { font-size: larger }
|
||||
.score { font-size: smaller; opacity: 0.8; }
|
||||
.image { display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); font-size: small; }
|
||||
.image img { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -40,6 +41,9 @@ img { max-width: 20%; }
|
||||
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
|
||||
<span class="score">{{.Score}}</span></p>
|
||||
<blockquote>{{.Html}}</blockquote>
|
||||
{{range .Images}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img class="last" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
<p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -82,7 +83,7 @@ func searchCli(w io.Writer, dir string, n int, all, extract bool, quiet bool, ar
|
||||
}
|
||||
|
||||
// searchExtract prints the search extracts to stdout with highlighting for a terminal.
|
||||
func searchExtract(w io.Writer, items []*Page) {
|
||||
func searchExtract(w io.Writer, items []*Result) {
|
||||
heading := func (s string) string { return "\x1b[1;4m" + s + "\x1b[0m" } // bold + underline
|
||||
match := func (s string) string { return "\x1b[1m" + s + "\x1b[0m" } // bold
|
||||
re := regexp.MustCompile(`<b>(.*?)</b>`)
|
||||
@@ -95,5 +96,15 @@ func searchExtract(w io.Writer, items []*Page) {
|
||||
for _, s := range strings.Split(wordwrap.String(s, 72), "\n") {
|
||||
fmt.Fprintln(w, " ", s)
|
||||
}
|
||||
for _, img := range p.Images {
|
||||
name, err := url.PathUnescape(img.Name)
|
||||
if err != nil {
|
||||
name = img.Name
|
||||
}
|
||||
fmt.Fprintln(w, " - ", name);
|
||||
for _, s := range strings.Split(wordwrap.String(img.Title, 70), "\n") {
|
||||
fmt.Fprintln(w, " ", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,28 @@ A quick sip too quick
|
||||
assert.Equal(t, "Tea", items[1].Title, items[1].Name)
|
||||
}
|
||||
|
||||
func TestImageSearch(t *testing.T) {
|
||||
cleanup(t, "testdata/images")
|
||||
|
||||
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# Pictures
|
||||
|
||||

|
||||
|
||||
Pictures in the box
|
||||
Tiny windows to our past
|
||||
Where are you, my love?
|
||||
|
||||
`)}
|
||||
p.save()
|
||||
|
||||
items, _ := search("phone", "testdata/images", "", 1, false)
|
||||
assert.Equal(t, 1, len(items), "one page found")
|
||||
assert.Equal(t, "Pictures", items[0].Title)
|
||||
assert.Equal(t, "phone", items[0].Images[0].Title)
|
||||
assert.Equal(t, "<b>phone</b>", string(items[0].Images[0].Html))
|
||||
assert.Equal(t, "testdata/images/2024-07-21.jpg", items[0].Images[0].Name)
|
||||
}
|
||||
|
||||
func TestSearchQuestionmark(t *testing.T) {
|
||||
cleanup(t, "testdata/question")
|
||||
p := &Page{Name: "testdata/question/Odd?", Body: []byte(`# Even?
|
||||
|
||||
@@ -35,7 +35,7 @@ func snippets(q string, s string) string {
|
||||
}
|
||||
// Short cut for short pages
|
||||
if len(s) <= snippetlen {
|
||||
return highlight(q, re, s)
|
||||
return highlight(re, s)
|
||||
}
|
||||
// show a snippet from the beginning of the document
|
||||
j := strings.LastIndex(s[:snippetlen], " ")
|
||||
@@ -47,7 +47,7 @@ func snippets(q string, s string) string {
|
||||
if len(s) > 400 {
|
||||
s = s[0:400] + " …"
|
||||
}
|
||||
return highlight(q, re, s)
|
||||
return highlight(re, s)
|
||||
}
|
||||
}
|
||||
t := s[0:j]
|
||||
@@ -98,5 +98,5 @@ func snippets(q string, s string) string {
|
||||
s = s[end:]
|
||||
}
|
||||
}
|
||||
return highlight(q, re, res)
|
||||
return highlight(re, res)
|
||||
}
|
||||
|
||||
@@ -196,7 +196,8 @@ func staticPage(source, target string) (*Page, error) {
|
||||
doc := markdown.Parse(p.Body, parser)
|
||||
ast.WalkFunc(doc, staticLinks)
|
||||
opts := html.RendererOptions{
|
||||
Flags: html.CommonFlags,
|
||||
// sync with wikiRenderer
|
||||
Flags: html.CommonFlags & ^html.SmartypantsFractions | html.LazyLoadImages,
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
maybeUnsafeHTML := markdown.Render(doc, renderer)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// templateFiles are the various HTML template files used. These files must exist in the root directory for Oddmu to be
|
||||
// able to generate HTML output. This always requires a template.
|
||||
var templateFiles = []string{"edit.html", "add.html", "view.html",
|
||||
var templateFiles = []string{"edit.html", "add.html", "view.html", "preview.html",
|
||||
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
|
||||
|
||||
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<h1>Adding to {{.Title}}</h1>
|
||||
<form action="/append/{{.Name}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="" autofocus required>**{{.Today}}**. </textarea>
|
||||
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="{{.Language}}" autofocus required>{{if .IsBlog}}**{{.Today}}**. {{end}}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Add">
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<form action="/save/{{.Name}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" placeholder="# Title
|
||||
|
||||
Text" lang="" autofocus>{{ or .Body (printf "# %s " .Today) | printf "%s" }}</textarea>
|
||||
Text" lang="{{.Language}}" autofocus>{{ or .Body (printf "# %s " .Today) | printf "%s" }}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Save">
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
|
||||
@@ -4,10 +4,18 @@ A theme that focuses on appending short paragraphs to existing pages.
|
||||
|
||||
This theme makes it all look like chat. 😍
|
||||
|
||||
Type and submit. 🥳
|
||||
|
||||
Hm. 🤔
|
||||
|
||||
> Oh, and quotes are messages from other people! 😲
|
||||
|
||||
I think I like it! 😄
|
||||
|
||||
More [themes](../index)! 👀
|
||||
> You are not alone. 👍
|
||||
|
||||
Type and submit in the textarea. 🥳
|
||||
|
||||
It really does feel like chat. 🍵 😌
|
||||
|
||||
> Check out the other [themes](../index)! 👀
|
||||
|
||||
No. 🙃
|
||||
|
||||
@@ -14,8 +14,21 @@ label { width: 7ch; display: inline-block; }
|
||||
button { font-size: large; background-color: #eee; color: inherit; border-radius: 6px; border-width: 1px; }
|
||||
main > *, footer { clear: both; }
|
||||
main p {
|
||||
float: right; text-align: right;
|
||||
float: right;
|
||||
color: #000; background: #8fd;
|
||||
padding: 3px 1ch; margin: 1pt auto 1pt 5ch;
|
||||
border-radius: 6px; border: 1px outset #eee; }
|
||||
main blockquote {
|
||||
padding: 0; margin: 0; }
|
||||
main blockquote p {
|
||||
float: left;
|
||||
color: #000; background: #ccc;
|
||||
padding: 3px 1ch; margin: 1pt 5ch 1pt 0;
|
||||
border-radius: 6px; border: 1px outset #eee; }
|
||||
p + blockquote > p, blockquote + p { margin-top: 5pt; }
|
||||
main ul, main ol, main dl {
|
||||
float: left;
|
||||
color: #000; background: #4ed;
|
||||
padding: 3px 1ch; margin: 1pt 0; border-radius: 6px; border: 1px outset #eee; }
|
||||
footer p { margin: 0.5ch 0 0 0; }
|
||||
textarea {
|
||||
|
||||
@@ -69,7 +69,7 @@ func highlightTokens(q string) []string {
|
||||
}
|
||||
|
||||
// hashtags returns a slice of hashtags. Use this to extract hashtags
|
||||
// from a page body.
|
||||
// from a page body. This ignores Markdown completely.
|
||||
func hashtags(s []byte) []string {
|
||||
hashtags := make([]string, 0)
|
||||
for {
|
||||
@@ -77,6 +77,10 @@ func hashtags(s []byte) []string {
|
||||
if i == -1 {
|
||||
return hashtags
|
||||
}
|
||||
if i > 0 && s[i-1] == '\\' {
|
||||
s = s[i+1:]
|
||||
continue
|
||||
}
|
||||
from := i
|
||||
i++
|
||||
for {
|
||||
|
||||
@@ -9,6 +9,14 @@ func TestHashtags(t *testing.T) {
|
||||
assert.EqualValues(t, []string{"#truth"}, hashtags([]byte("This is boring. #Truth")), "hashtags")
|
||||
}
|
||||
|
||||
func TestEscapedHashtags(t *testing.T) {
|
||||
assert.EqualValues(t, []string{}, hashtags([]byte("This is not a hashtag: \\#False")), "escaped hashtags")
|
||||
}
|
||||
|
||||
func TestBorkedHashtags(t *testing.T) {
|
||||
assert.EqualValues(t, []string{}, hashtags([]byte("This is borked: \\#")), "borked hashtag")
|
||||
}
|
||||
|
||||
func TestTokensAndPredicates(t *testing.T) {
|
||||
predicates, terms := predicatesAndTokens("foo title:bar")
|
||||
assert.EqualValues(t, []string{"foo"}, terms)
|
||||
|
||||
10
upload.html
10
upload.html
@@ -76,10 +76,16 @@ window.addEventListener('load', uploadFiles.init);
|
||||
<p>Previous upload: <a href="/view/{{.Dir}}{{.Last}}">{{.Last}}</a>
|
||||
{{if .Image}}
|
||||
<p><img class="last" src="/view/{{.Dir}}{{.Last}}"><br>
|
||||
<tt>Link: </tt>
|
||||
Links:<tt>{{range .Actual}}<br>{{end}}</tt>
|
||||
{{else}}
|
||||
<p>Link: <tt>[text]({{.Last}})</tt>
|
||||
{{end}}
|
||||
<form id="add" action="/append/{{.Dir}}{{.Base}}" method="POST">
|
||||
<input type="hidden" name="body" value="{{range .Actual}}
|
||||
{{end}}">
|
||||
<p>Append this to <a href="/view/{{.Dir}}{{.Base}}">{{.Title}}</a>?
|
||||
<input type="submit" value="Add">
|
||||
</form>
|
||||
{{end}}
|
||||
<form id="upload" action="/drop/{{.Dir}}" method="POST" enctype="multipart/form-data">
|
||||
<p>When uploading a picture from a phone, its filename is going to be something like IMG_1234.JPG.
|
||||
@@ -99,7 +105,7 @@ window.addEventListener('load', uploadFiles.init);
|
||||
<p>Finally, pick the files or photos to upload.
|
||||
Picture metadata is only removed if the pictures gets resized.
|
||||
Providing a new max width is recommended for all pictures.
|
||||
If you're uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
|
||||
If you’re uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
|
||||
<p>To delete a file, upload an empty file.
|
||||
<p><label for="file">Files to upload:</label>
|
||||
<input type="file" name="file" required multiple>
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -28,9 +29,11 @@ type upload struct {
|
||||
Image bool
|
||||
MaxWidth string
|
||||
Quality string
|
||||
Actual []string
|
||||
}
|
||||
|
||||
var lastRe = regexp.MustCompile(`^(.*)([0-9]+)(.*)$`)
|
||||
var lastRe = regexp.MustCompile(`^(.*?)([0-9]+)([^0-9]*)$`)
|
||||
var baseRe = regexp.MustCompile(`^(.*?)-[0-9]+$`)
|
||||
|
||||
// uploadHandler uses the "upload.html" template to enable uploads. The file is saved using the dropHandler. URL
|
||||
// parameters are used to copy name, maxwidth and quality from the previous upload. If the previous name contains a
|
||||
@@ -54,6 +57,7 @@ func uploadHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(last))
|
||||
data.Image = strings.HasPrefix(mimeType, "image/")
|
||||
data.Name, err = next(dir, last, 1)
|
||||
data.Actual = r.Form["actual"]
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -129,7 +133,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data.Add("maxwidth", maxwidth)
|
||||
data.Set("maxwidth", maxwidth)
|
||||
// determine how the file will be written
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
@@ -143,7 +147,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data.Add("quality", q)
|
||||
data.Set("quality", q)
|
||||
}
|
||||
format = imaging.JPEG
|
||||
default:
|
||||
@@ -223,6 +227,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
log.Println("Delete", path)
|
||||
}
|
||||
}
|
||||
data.Add("actual", filename)
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok {
|
||||
log.Println("Save", path, "by", username)
|
||||
@@ -234,6 +239,31 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
data.Set("last", filename) // has no slashes
|
||||
http.Redirect(w, r, "/upload/"+dir+"?"+data.Encode(), http.StatusFound)
|
||||
}
|
||||
// Base returns a page name matching the first uploaded file: no extension and no appended number. If the name
|
||||
// refers to a directory, returns "index". This is used to create the form target in "upload.html", for example.
|
||||
func (u *upload) Base() string {
|
||||
n := u.Name[:strings.LastIndex(u.Name, ".")]
|
||||
m := baseRe.FindStringSubmatch(n)
|
||||
if m != nil {
|
||||
return m[1]
|
||||
}
|
||||
if n == "." {
|
||||
return "index"
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Title returns the title of the matching page, if it exists.
|
||||
func (u *upload) Title() string {
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
name := path.Join(u.Dir, u.Base())
|
||||
title, ok := index.titles[name]
|
||||
if ok {
|
||||
return title
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Today returns the date, as a string, for use in templates.
|
||||
func (u *upload) Today() string {
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestUpload(t *testing.T) {
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/files/",
|
||||
writer.FormDataContentType(), form, "/upload/testdata/files/?last=ok.txt")
|
||||
writer.FormDataContentType(), form, "/upload/testdata/files/?actual=ok.txt&last=ok.txt")
|
||||
assert.Contains(t,
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/files/ok.txt", nil),
|
||||
"Hello!")
|
||||
@@ -50,7 +50,7 @@ func TestUploadPng(t *testing.T) {
|
||||
png.Encode(file, img)
|
||||
writer.Close()
|
||||
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/png/",
|
||||
writer.FormDataContentType(), form, "/upload/testdata/png/?last=ok.png")
|
||||
writer.FormDataContentType(), form, "/upload/testdata/png/?actual=ok.png&last=ok.png")
|
||||
}
|
||||
|
||||
func TestUploadJpg(t *testing.T) {
|
||||
@@ -66,7 +66,7 @@ func TestUploadJpg(t *testing.T) {
|
||||
jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
|
||||
writer.Close()
|
||||
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/jpg/",
|
||||
writer.FormDataContentType(), form, "/upload/testdata/jpg/?last=ok.jpg")
|
||||
writer.FormDataContentType(), form, "/upload/testdata/jpg/?actual=ok.jpg&last=ok.jpg")
|
||||
}
|
||||
|
||||
func TestUploadHeic(t *testing.T) {
|
||||
@@ -94,7 +94,7 @@ YXQAAAApKAGvEyE1mvXho5qH3STtzcWnOxedwNIXAKNDaJNqz3uONoCHeUhi/HA=`
|
||||
file.Write(img)
|
||||
writer.Close()
|
||||
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/heic/",
|
||||
writer.FormDataContentType(), form, "/upload/testdata/heic/?last=ok.jpg")
|
||||
writer.FormDataContentType(), form, "/upload/testdata/heic/?actual=ok.jpg&last=ok.jpg")
|
||||
}
|
||||
|
||||
func TestDeleteFile(t *testing.T) {
|
||||
@@ -116,7 +116,7 @@ What happened just now?`), 0644))
|
||||
file.Write([]byte(""))
|
||||
writer.Close()
|
||||
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/delete/",
|
||||
writer.FormDataContentType(), form, "/upload/testdata/delete/?last=nothing.txt")
|
||||
writer.FormDataContentType(), form, "/upload/testdata/delete/?actual=nothing.txt&last=nothing.txt")
|
||||
// check that it worked
|
||||
assert.NoFileExists(t, "testdata/delete/nothing.txt")
|
||||
}
|
||||
@@ -255,3 +255,19 @@ func TestUploadTwoInOneAgain(t *testing.T) {
|
||||
assert.FileExists(t, "testdata/zwei/image.jpg")
|
||||
assert.FileExists(t, "testdata/zwei/image-1.jpg")
|
||||
}
|
||||
|
||||
func TestUploadNext(t *testing.T) {
|
||||
cleanup(t, "testdata/next")
|
||||
os.MkdirAll("testdata/next", 0755)
|
||||
s := []string{}
|
||||
nm := "test-1.jpg"
|
||||
var err error
|
||||
for i := 0; i < 25; i++ {
|
||||
nm, err = next("testdata/next", nm, 0)
|
||||
assert.NoError(t, err)
|
||||
s = append(s, nm)
|
||||
os.Create("testdata/next/" + nm)
|
||||
}
|
||||
r := []string{ "test-1.jpg", "test-2.jpg", "test-3.jpg", "test-4.jpg", "test-5.jpg", "test-6.jpg", "test-7.jpg", "test-8.jpg", "test-9.jpg", "test-10.jpg", "test-11.jpg", "test-12.jpg", "test-13.jpg", "test-14.jpg", "test-15.jpg", "test-16.jpg", "test-17.jpg", "test-18.jpg", "test-19.jpg", "test-20.jpg", "test-21.jpg", "test-22.jpg", "test-23.jpg", "test-24.jpg", "test-25.jpg" }
|
||||
assert.Equal(t, r, s)
|
||||
}
|
||||
|
||||
1
wiki.go
1
wiki.go
@@ -171,6 +171,7 @@ func serve() {
|
||||
http.HandleFunc("/", rootHandler)
|
||||
http.HandleFunc("/archive/", makeHandler(archiveHandler, true))
|
||||
http.HandleFunc("/view/", makeHandler(viewHandler, false))
|
||||
http.HandleFunc("/preview/", makeHandler(previewHandler, false))
|
||||
http.HandleFunc("/diff/", makeHandler(diffHandler, true))
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler, true))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler, true))
|
||||
|
||||
Reference in New Issue
Block a user