forked from mirror/oddmu
Compare commits
6 Commits
full-text-
...
blackfrida
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a093fc5378 | ||
|
|
471cd3c6ec | ||
|
|
da361284e8 | ||
|
|
6215d2a842 | ||
|
|
47c727c00d | ||
|
|
91381e474c |
80
README.md
80
README.md
@@ -47,8 +47,8 @@ some of which are enable by default:
|
||||
* MathJax is supported (but needs a separte setup)
|
||||
|
||||
See the section on
|
||||
[extensions](https://github.com/gomarkdown/markdown#extensions) in the
|
||||
Markdown library for information on the various extensions.
|
||||
[extensions](https://github.com/russross/blackfriday#extensions) in
|
||||
the Blackfriday library for information on the various extensions.
|
||||
|
||||
A table with footers and a columnspan:
|
||||
|
||||
@@ -217,8 +217,9 @@ MDCertificateAgreement accepted
|
||||
First, it manages the domain, getting the necessary certificates. It
|
||||
redirects regular HTTP traffic from port 80 to port 443. It turns on
|
||||
the SSL engine for port 443. It redirects `/` to `/view/index` and any
|
||||
path that starts with `/view/`, `/edit/`, `/save/` or `/search/` is
|
||||
proxied to port 8080 where the Oddmu program can handle it.
|
||||
path that starts with `/view/`, `/edit/`, `/save/`, `/add/`,
|
||||
`/append/` or `/search/` is proxied to port 8080 where the Oddmu
|
||||
program can handle it.
|
||||
|
||||
Thus, this is what happens:
|
||||
|
||||
@@ -265,12 +266,12 @@ To delete remove a user:
|
||||
htpasswd -D .htpasswd berta
|
||||
```
|
||||
|
||||
Modify your site configuration and protect the `/edit/` and `/save/`
|
||||
URLs with a password by adding the following to your `<VirtualHost
|
||||
*:443>` section:
|
||||
Modify your site configuration and protect the `/edit/`, `/save/`,
|
||||
`/add/` and `/append/` URLs with a password by adding the following to
|
||||
your `<VirtualHost *:443>` section:
|
||||
|
||||
```apache
|
||||
<LocationMatch "^/(edit|save)/">
|
||||
<LocationMatch "^/(edit|save|add|append)/">
|
||||
AuthType Basic
|
||||
AuthName "Password Required"
|
||||
AuthUserFile /home/oddmu/.htpasswd
|
||||
@@ -295,10 +296,9 @@ webserver can read (world readable file, world readable and executable
|
||||
directory). Populate it with files.
|
||||
|
||||
Make sure that none of the static files look like the wiki paths
|
||||
`/view/`, `/edit/`, `/save/` or `/search/`.
|
||||
|
||||
For example, create a file called `robots.txt` containing the
|
||||
following, tellin all robots that they're not welcome.
|
||||
`/view/`, `/edit/`, `/save/`, `/add/`, `/append/` or `/search/`. For
|
||||
example, create a file called `robots.txt` containing the following,
|
||||
tellin all robots that they're not welcome.
|
||||
|
||||
```text
|
||||
User-agent: *
|
||||
@@ -346,50 +346,8 @@ that matches everything:
|
||||
## Customization (with recompilation)
|
||||
|
||||
The Markdown parser can be customized and
|
||||
[extensions](https://pkg.go.dev/github.com/gomarkdown/markdown/parser#Extensions)
|
||||
can be added. There's an example in the
|
||||
[usage](https://github.com/gomarkdown/markdown#usage) section. You'll
|
||||
need to make changes to the `viewHandler` yourself.
|
||||
|
||||
### Render Gemtext
|
||||
|
||||
In a first approximation, Gemtext is valid Markdown except for the
|
||||
rocket links (`=>`). Here's how to modify the `loadPage` so that a
|
||||
`.gmi` file is loaded if no `.md` is found, and the rocket links are
|
||||
translated into Markdown:
|
||||
|
||||
```go
|
||||
func loadPage(name string) (*Page, error) {
|
||||
filename := name + ".md"
|
||||
body, err := os.ReadFile(filename)
|
||||
if err == nil {
|
||||
return &Page{Title: name, Name: name, Body: body}, nil
|
||||
}
|
||||
filename = name + ".gmi"
|
||||
body, err = os.ReadFile(filename)
|
||||
if err == nil {
|
||||
re := regexp.MustCompile(`(?m)^=>\s*(\S+)\s+(.+)`)
|
||||
body = []byte(re.ReplaceAllString(string(body), `* [$2]($1)`))
|
||||
return &Page{Title: name, Name: name, Body: body}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
```
|
||||
|
||||
There is a small problem, however: By default, Markdown expects an
|
||||
empty line before a list begins. The following change to `renderHtml`
|
||||
uses the `NoEmptyLineBeforeBlock` extension for the parser:
|
||||
|
||||
```go
|
||||
func (p* Page) renderHtml() {
|
||||
// Here is where a new extension is added!
|
||||
extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock
|
||||
markdownParser := parser.NewWithExtensions(extensions)
|
||||
maybeUnsafeHTML := markdown.ToHTML(p.Body, markdownParser, nil)
|
||||
html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
|
||||
p.Html = template.HTML(html);
|
||||
}
|
||||
```
|
||||
[extensions](https://github.com/russross/blackfriday#extensions) can
|
||||
be added. You'll need to make changes to `renderHtml` yourself.
|
||||
|
||||
## Understanding search
|
||||
|
||||
@@ -421,15 +379,15 @@ A document with content "This is a test" when searched with the phrase
|
||||
"this test" therefore gets a score of 8: the entire phrase does not
|
||||
match but each word gets four points.
|
||||
|
||||
Trigrams are sometimes strange: In a text containing the words
|
||||
"software" and "#socialmedia", a search for "#software" returns a
|
||||
result because the trigram "#so" is part of "#socialmedia".
|
||||
Trigrams are sometimes strange: In a text containing the words "main"
|
||||
and "rail", a search for "mail" returns a match because the trigrams
|
||||
"mai" and "ail" are found. In this situation, the result has a score
|
||||
of 0.
|
||||
|
||||
## Limitations
|
||||
|
||||
Page titles are filenames with `.md` appended. If your filesystem
|
||||
cannot handle it, it can't be a page title. Specifically, *no slashes*
|
||||
in filenames.
|
||||
cannot handle it, it can't be a page name.
|
||||
|
||||
The pages are indexed as the server starts and the index is kept in
|
||||
memory. If you have a ton of pages, this surely wastes a lot of
|
||||
|
||||
11
TODO.md
11
TODO.md
@@ -1,6 +1,3 @@
|
||||
Easily prepend or append text for use with a mobile browser. Like
|
||||
comments.
|
||||
|
||||
Upload files.
|
||||
|
||||
Automatically scale or process files.
|
||||
@@ -9,15 +6,7 @@ Post by Delta Chat? That is, allow certain encrypted emails to post.
|
||||
|
||||
Convert the existing wiki.
|
||||
|
||||
Investigate how to run a multi-lingual wiki where an appropriate
|
||||
template is used based on the language of the page. This is important
|
||||
because the template needs to use the appropriate `lang` attribute for
|
||||
hyphenation to work.
|
||||
|
||||
Investigate how to run a multi-linugual wiki where an appropriate
|
||||
version of a page is served based on language preferences of the user.
|
||||
This is a low priority issue since it's probably only of interest for
|
||||
corporate or governmental sites.
|
||||
|
||||
Switch from trigram search to a simple full text search engine?
|
||||
https://artem.krylysov.com/blog/2020/07/28/lets-build-a-full-text-search-engine/
|
||||
|
||||
21
add.html
Normal file
21
add.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Add to {{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
|
||||
form, textarea { width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Adding to {{.Title}}</h1>
|
||||
<form action="/append/{{.Name}}" method="POST">
|
||||
<div><textarea name="body" rows="20" cols="80" placeholder="Text" autofocus></textarea></div>
|
||||
<p><input type="submit" value="Add">
|
||||
<a href="/view/{{.Name}}"><button>Cancel</button></a></p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,9 +12,10 @@ form, textarea { width: 100%; }
|
||||
</head>
|
||||
<body>
|
||||
<h1>Editing {{.Title}}</h1>
|
||||
|
||||
<form action="/save/{{.Name}}" method="POST">
|
||||
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
|
||||
<div><textarea name="body" rows="20" cols="80" placeholder="# Title
|
||||
|
||||
Text" autofocus>{{printf "%s" .Body}}</textarea></div>
|
||||
<p><input type="submit" value="Save">
|
||||
<a href="/view/{{.Name}}"><button>Cancel</button></a></p>
|
||||
</form>
|
||||
|
||||
8
go.mod
8
go.mod
@@ -4,16 +4,20 @@ go 1.21.0
|
||||
|
||||
require (
|
||||
github.com/dgryski/go-trigram v0.0.0-20160407183937-79ec494e1ad0
|
||||
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/pemistahl/lingua-go v1.4.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/pemistahl/lingua-go v1.4.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
15
go.sum
15
go.sum
@@ -1,19 +1,27 @@
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-trigram v0.0.0-20160407183937-79ec494e1ad0 h1:b+7JSiBM+hnLQjP/lXztks5hnLt1PS46hktG9VOJgzo=
|
||||
github.com/dgryski/go-trigram v0.0.0-20160407183937-79ec494e1ad0/go.mod h1:qzKC/DpcxK67zaSHdCmIv3L9WJViHVinYXN2S7l3RM8=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
|
||||
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw=
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
@@ -22,3 +30,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
57
page.go
57
page.go
@@ -3,11 +3,9 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/pemistahl/lingua-go"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -103,35 +101,39 @@ func (p *Page) handleTitle(replace bool) {
|
||||
|
||||
// renderHtml renders the Page.Body to HTML and sets Page.Html.
|
||||
func (p *Page) renderHtml() {
|
||||
maybeUnsafeHTML := markdown.ToHTML(p.Body, nil, nil)
|
||||
maybeUnsafeHTML := blackfriday.Run(p.Body)
|
||||
p.Html = sanitizeBytes(maybeUnsafeHTML)
|
||||
p.Language = p.language(p.plainText())
|
||||
p.Language = language(p.plainText())
|
||||
}
|
||||
|
||||
// plainText renders the Page.Body to plain text and returns it,
|
||||
// ignoring all the Markdown and all the newlines. The result is one
|
||||
// long single line of text.
|
||||
func (p *Page) plainText() string {
|
||||
parser := parser.New()
|
||||
doc := markdown.Parse(p.Body, parser)
|
||||
optList := []blackfriday.Option{blackfriday.WithExtensions(blackfriday.CommonExtensions)}
|
||||
parser := blackfriday.New(optList...)
|
||||
ast := parser.Parse(p.Body)
|
||||
text := []byte("")
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
if entering && node.AsLeaf() != nil {
|
||||
text = append(text, node.AsLeaf().Literal...)
|
||||
text = append(text, []byte(" ")...)
|
||||
ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
if entering && node.Type == blackfriday.Text {
|
||||
s := node.Literal
|
||||
if len(s) == 0 {
|
||||
return blackfriday.GoToNext
|
||||
}
|
||||
// Some Markdown still contains newlines
|
||||
for i, c := range s {
|
||||
if c == '\n' {
|
||||
s[i] = ' '
|
||||
}
|
||||
}
|
||||
if len(text) > 0 && text[len(text)-1] != ' ' && s[0] != ' ' {
|
||||
text = append(text, ' ')
|
||||
}
|
||||
|
||||
text = append(text, s...)
|
||||
}
|
||||
return ast.GoToNext
|
||||
return blackfriday.GoToNext
|
||||
})
|
||||
// Some Markdown still contains newlines
|
||||
for i, c := range text {
|
||||
if c == '\n' {
|
||||
text[i] = ' '
|
||||
}
|
||||
}
|
||||
// Remove trailing space
|
||||
for len(text) > 0 && text[len(text)-1] == ' ' {
|
||||
text = text[0 : len(text)-1]
|
||||
}
|
||||
return string(text)
|
||||
}
|
||||
|
||||
@@ -141,17 +143,12 @@ func (p *Page) summarize(q string) {
|
||||
p.Score = score(q, string(p.Body)) + score(q, p.Title)
|
||||
t := p.plainText()
|
||||
p.Html = sanitize(snippets(q, t))
|
||||
p.Language = p.language(t)
|
||||
p.Language = language(t)
|
||||
}
|
||||
|
||||
func (p *Page) language (s string) string {
|
||||
func language(s string) string {
|
||||
if language, ok := detector.DetectLanguageOf(s); ok {
|
||||
switch language {
|
||||
case lingua.English:
|
||||
return "en"
|
||||
case lingua.German:
|
||||
return "de"
|
||||
}
|
||||
return strings.ToLower(language.IsoCode639_1().String())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
44
page_test.go
44
page_test.go
@@ -2,8 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"regexp"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPageTitle(t *testing.T) {
|
||||
@@ -12,19 +13,10 @@ My back aches for you
|
||||
I sit, stare and type for hours
|
||||
But yearn for blue sky`)}
|
||||
p.handleTitle(false)
|
||||
if p.Title != "Ache" {
|
||||
t.Logf("The page title was not extracted correctly: %s", p.Title)
|
||||
t.Fail()
|
||||
}
|
||||
if !strings.HasPrefix(string(p.Body), "# Ache") {
|
||||
t.Logf("The page title was removed: %s", p.Body)
|
||||
t.Fail()
|
||||
}
|
||||
assert.Equal(t, "Ache", p.Title)
|
||||
assert.Regexp(t, regexp.MustCompile("^# Ache"), string(p.Body))
|
||||
p.handleTitle(true)
|
||||
if !strings.HasPrefix(string(p.Body), "My back") {
|
||||
t.Logf("The page title was not removed: %s", p.Body)
|
||||
t.Fail()
|
||||
}
|
||||
assert.Regexp(t, regexp.MustCompile("^My back"), string(p.Body))
|
||||
}
|
||||
|
||||
func TestPagePlainText(t *testing.T) {
|
||||
@@ -32,12 +24,8 @@ func TestPagePlainText(t *testing.T) {
|
||||
The air will not come
|
||||
To inhale is an effort
|
||||
The summer heat kills`)}
|
||||
s := p.plainText()
|
||||
r := "Water The air will not come To inhale is an effort The summer heat kills"
|
||||
if s != r {
|
||||
t.Logf("The plain text version is wrong: %s", s)
|
||||
t.Fail()
|
||||
}
|
||||
assert.Equal(t, r, p.plainText())
|
||||
}
|
||||
|
||||
func TestPageHtml(t *testing.T) {
|
||||
@@ -46,17 +34,13 @@ Silver leaves shine bright
|
||||
They droop, boneless, weak and sad
|
||||
A cruel sun stares down`)}
|
||||
p.renderHtml()
|
||||
s := string(p.Html)
|
||||
r := `<h1>Sun</h1>
|
||||
|
||||
<p>Silver leaves shine bright
|
||||
They droop, boneless, weak and sad
|
||||
A cruel sun stares down</p>
|
||||
`
|
||||
if s != r {
|
||||
t.Logf("The HTML is wrong: %s", s)
|
||||
t.Fail()
|
||||
}
|
||||
assert.Equal(t, r, string(p.Html))
|
||||
}
|
||||
|
||||
func TestPageDir(t *testing.T) {
|
||||
@@ -68,11 +52,17 @@ A slow shuffle in the dark
|
||||
Moonlight floods the aisle`)}
|
||||
p.save()
|
||||
o, err := loadPage("testdata/moon")
|
||||
if err != nil || string(o.Body) != string(p.Body) {
|
||||
t.Logf("File in subdirectory not loaded: %s", p.Name)
|
||||
t.Fail()
|
||||
}
|
||||
assert.NoError(t, err, "load page")
|
||||
assert.Equal(t, p.Body, o.Body)
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll("testdata")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLanguage(t *testing.T) {
|
||||
l := language(`
|
||||
My back hurts at night
|
||||
My shoulders won't budge today
|
||||
Winter bones I say`)
|
||||
assert.Equal(t, "en", l)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ img { max-width: 100%; }
|
||||
<header>
|
||||
<a href="#main">Skip navigation</a>
|
||||
<a href="/view/index">Home</a>
|
||||
<a href="/edit/{{.Name}}">Edit this page</a>
|
||||
<a href="/edit/{{.Name}}">Edit</a>
|
||||
<a href="/add/{{.Name}}">Add</a>
|
||||
<form role="search" action="/search" method="GET">
|
||||
<input type="text" spellcheck="false" name="q" required>
|
||||
<button>Search</button>
|
||||
|
||||
40
wiki.go
40
wiki.go
@@ -10,7 +10,8 @@ import (
|
||||
)
|
||||
|
||||
// Templates are parsed at startup.
|
||||
var templates = template.Must(template.ParseFiles("edit.html", "view.html", "search.html"))
|
||||
var templates = template.Must(
|
||||
template.ParseFiles("edit.html", "add.html", "view.html", "search.html"))
|
||||
|
||||
// validPath is a regular expression where the second group matches a
|
||||
// page, so when the handler for "/edit/" is called, a URL path of
|
||||
@@ -65,7 +66,8 @@ func viewHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
|
||||
// editHandler uses the "edit.html" template to present an edit page.
|
||||
// When editing, the page title is not overriden by a title in the
|
||||
// text. Instead, the page name is used.
|
||||
// text. Instead, the page name is used. The edit is saved using the
|
||||
// saveHandler.
|
||||
func editHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
@@ -89,6 +91,38 @@ func saveHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
http.Redirect(w, r, "/view/"+name, http.StatusFound)
|
||||
}
|
||||
|
||||
// addHandler uses the "add.html" template to present an empty edit
|
||||
// page. What you type there is appended to the page using the
|
||||
// appendHandler.
|
||||
func addHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
p = &Page{Title: name, Name: name}
|
||||
} else {
|
||||
p.handleTitle(false)
|
||||
}
|
||||
renderTemplate(w, "add", p)
|
||||
}
|
||||
|
||||
// appendHandler takes the "body" form parameter and appends it. The
|
||||
// browser is redirected to the page view.
|
||||
func appendHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
body := r.FormValue("body")
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
p = &Page{Title: name, Name: name, Body: []byte(body)}
|
||||
} else {
|
||||
p.handleTitle(false)
|
||||
p.Body = append(p.Body, []byte(body)...)
|
||||
}
|
||||
err = p.save()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+name, http.StatusFound)
|
||||
}
|
||||
|
||||
// makeHandler returns a handler that uses the URL path without the
|
||||
// first path element as its argument, e.g. if the URL path is
|
||||
// /edit/foo/bar, the editHandler is called with "foo/bar" as its
|
||||
@@ -130,6 +164,8 @@ func main() {
|
||||
http.HandleFunc("/view/", makeHandler(viewHandler))
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler))
|
||||
http.HandleFunc("/add/", makeHandler(addHandler))
|
||||
http.HandleFunc("/append/", makeHandler(appendHandler))
|
||||
http.HandleFunc("/search", searchHandler)
|
||||
fmt.Print("Indexing all pages\n")
|
||||
loadIndex()
|
||||
|
||||
Reference in New Issue
Block a user