7 Commits
v0.3 ... v0.4

Author SHA1 Message Date
Alex Schroeder
9e6d59cefa Run go fmt 2023-08-25 00:29:43 +02:00
Alex Schroeder
2a4902b1b4 Add footer and add note change its email address 2023-08-25 00:28:37 +02:00
Alex Schroeder
efc54f1524 Add title 2023-08-24 18:24:25 +02:00
Alex Schroeder
8fc5bd30e3 Link Home 2023-08-24 18:24:17 +02:00
Alex Schroeder
40855ea442 More documentation 2023-08-24 18:24:09 +02:00
Alex Schroeder
29af9a4cfa Add cleanup to tests 2023-08-24 14:34:03 +02:00
Alex Schroeder
146f4c9f57 Allow creation of subdirectories 2023-08-24 14:30:19 +02:00
11 changed files with 130 additions and 44 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/oddmu
test.md
/testdata/

View File

@@ -22,6 +22,9 @@ Feel free to change the templates `view.html` and `edit.html` and
restart the server. Modifying the styles in the templates would be a
good start to get a feel for it.
The first change you should make is to replace the email address in
`view.html`. 😄
The templates can refer to the following properties of a page:
`{{.Title}}` is the page title. If the page doesn't provide its own
@@ -77,9 +80,11 @@ As root, on your server:
adduser --system --home /home/oddmu oddmu
```
Copy all the files into `/home/oddmu` to your server: `oddmu`, `oddmu.service`, `view.html` and `edit.html`.
Copy all the files into `/home/oddmu` to your server: `oddmu`,
`oddmu.service`, `view.html` and `edit.html`.
Edit the `oddmu.service` file. These are the three lines you most likely have to take care of:
Edit the `oddmu.service` file. These are the three lines you most
likely have to take care of:
```
ExecStart=/home/oddmu/oddmu
@@ -236,6 +241,38 @@ and without needing a wiki page.
[Wikipedia](https://en.wikipedia.org/wiki/Robot_exclusion_standard)
has more information.
## Different logins for different access rights
What if you have a site with various subdirectories and each
subdirectory is for a different group of friends? You can set this up
using your webserver. One way to do this is to require specific
usernames (which must have a password in the password file mentioned
above.
This requires a valid login by the user "alex" or "berta":
```apache
<LocationMatch "^/(edit|save)/intetebi/">
Require user alex berta
</LocationMatch>
```
## Private wikis
Based on the above, you can prevent people from reading the wiki, too.
The `LocationMatch` must cover the `/view/` URLs. In order to protect
*everything*, use a [Location directive](https://httpd.apache.org/docs/current/mod/core.html#location)
that matches everything:
```apache
<Location />
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
Require valid-user
</Location>
```
## Customization (with recompilation)
The Markdown parser can be customized and

View File

@@ -1,13 +1,13 @@
package main
import (
"strings"
"regexp"
"strings"
)
// highlight splits the query string q into terms and highlights them
// using the bold tag. Return the highlighted string and a score.
func highlight (q string, s string) (string, int) {
func highlight(q string, s string) (string, int) {
c := 0
re, err := regexp.Compile("(?i)" + q)
if err == nil {
@@ -32,9 +32,15 @@ func highlight (q string, s string) (string, int) {
// Terms matching at the beginning and
// end of words and matching entire
// words increase the score further.
if len(m[1]) == 0 { c++ }
if len(m[3]) == 0 { c++ }
if len(m[1]) == 0 && len(m[3]) == 0 { c++ }
if len(m[1]) == 0 {
c++
}
if len(m[3]) == 0 {
c++
}
if len(m[1]) == 0 && len(m[3]) == 0 {
c++
}
r[m[2]] = "<b>" + m[2] + "</b>"
}
for old, new := range r {

View File

@@ -1,3 +1,5 @@
# Welcome to Oddµ
Hello! 🙃
Check out the [README](README).

26
page.go
View File

@@ -1,14 +1,16 @@
package main
import (
"bytes"
"fmt"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"github.com/microcosm-cc/bluemonday"
"html/template"
"strings"
"bytes"
"os"
"path/filepath"
"strings"
)
// Page is a struct containing information about a single page. Title
@@ -35,6 +37,14 @@ func (p *Page) save() error {
s := bytes.ReplaceAll(p.Body, []byte{'\r'}, []byte{})
p.Body = s
p.updateIndex()
d := filepath.Dir(filename)
if d != "." {
err := os.MkdirAll(d, 0700)
if err != nil {
fmt.Printf("Creating directory %s failed", d)
return err
}
}
return os.WriteFile(filename, s, 0600)
}
@@ -55,7 +65,7 @@ func loadPage(name string) (*Page, error) {
// handleTitle extracts the title from a Page and sets Page.Title, if
// any. If replace is true, the page title is also removed from
// Page.Body. Make sure not to save this! This is only for rendering.
func (p* Page) handleTitle(replace bool) {
func (p *Page) handleTitle(replace bool) {
s := string(p.Body)
m := titleRegexp.FindStringSubmatch(s)
if m != nil {
@@ -67,16 +77,16 @@ func (p* Page) handleTitle(replace bool) {
}
// renderHtml renders the Page.Body to HTML and sets Page.Html.
func (p* Page) renderHtml() {
func (p *Page) renderHtml() {
maybeUnsafeHTML := markdown.ToHTML(p.Body, nil, nil)
html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
p.Html = template.HTML(html);
p.Html = template.HTML(html)
}
// 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 {
func (p *Page) plainText() string {
parser := parser.New()
doc := markdown.Parse(p.Body, parser)
text := []byte("")
@@ -95,13 +105,13 @@ func (p* Page) plainText() string {
}
// Remove trailing space
for text[len(text)-1] == ' ' {
text = text[0:len(text)-1]
text = text[0 : len(text)-1]
}
return string(text)
}
// summarize for query string q sets Page.Html to an extract.
func (p* Page) summarize(q string) {
func (p *Page) summarize(q string) {
p.handleTitle(true)
s, c := snippets(q, p.plainText())
p.Score = c

View File

@@ -1,11 +1,12 @@
package main
import (
"os"
"strings"
"testing"
)
func TestPageTitle (t *testing.T) {
func TestPageTitle(t *testing.T) {
p := &Page{Body: []byte(`# Ache
My back aches for you
I sit, stare and type for hours
@@ -26,7 +27,7 @@ But yearn for blue sky`)}
}
}
func TestPagePlainText (t *testing.T) {
func TestPagePlainText(t *testing.T) {
p := &Page{Body: []byte(`# Water
The air will not come
To inhale is an effort
@@ -39,7 +40,7 @@ The summer heat kills`)}
}
}
func TestPageHtml (t *testing.T) {
func TestPageHtml(t *testing.T) {
p := &Page{Body: []byte(`# Sun
Silver leaves shine bright
They droop, boneless, weak and sad
@@ -57,3 +58,21 @@ A cruel sun stares down</p>
t.Fail()
}
}
func TestPageDir(t *testing.T) {
_ = os.RemoveAll("testdata")
loadIndex()
p := &Page{Name: "testdata/moon", Body: []byte(`# Moon
From bed to bathroom
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()
}
t.Cleanup(func() {
_ = os.RemoveAll("testdata")
})
}

View File

@@ -1,12 +1,12 @@
package main
import (
trigram "github.com/dgryski/go-trigram"
"path/filepath"
"strings"
"slices"
"io/fs"
"fmt"
trigram "github.com/dgryski/go-trigram"
"io/fs"
"path/filepath"
"slices"
"strings"
)
// Search is a struct containing the result of a search. Query is the

View File

@@ -1,15 +1,15 @@
package main
import (
"testing"
"strings"
"os"
"strings"
"testing"
)
var name string = "test"
// TestIndex relies on README.md being indexed
func TestIndex (t *testing.T) {
func TestIndex(t *testing.T) {
_ = os.Remove(name + ".md")
loadIndex()
q := "Oddµ"
@@ -68,4 +68,7 @@ func TestIndex (t *testing.T) {
t.Logf("Page '%s' not found using the new content: %s", name, p.Body)
t.Fail()
}
t.Cleanup(func() {
_ = os.Remove(name + ".md")
})
}

View File

@@ -1,11 +1,11 @@
package main
import (
"strings"
"regexp"
"strings"
)
func snippets (q string, s string) (string, int) {
func snippets(q string, s string) (string, int) {
// Look for Snippets
snippetlen := 100
maxsnippets := 4
@@ -45,7 +45,7 @@ func snippets (q string, s string) (string, int) {
if j > -1 {
// get the substring containing the start of
// the match, ending on word boundaries
from := j - snippetlen / 2
from := j - snippetlen/2
if from < 0 {
from = 0
}
@@ -55,7 +55,7 @@ func snippets (q string, s string) (string, int) {
} else {
start += from
}
to := j + snippetlen / 2
to := j + snippetlen/2
if to > len(s) {
to = len(s)
}
@@ -69,8 +69,8 @@ func snippets (q string, s string) (string, int) {
end += to
}
}
t = s[start : end];
res = res + t + " …";
t = s[start:end]
res = res + t + " …"
// truncate text to avoid rematching the same string.
s = s[end:]
}

View File

@@ -8,20 +8,28 @@
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
form { display: inline-block; padding-left: 1em; }
footer { border-top: 1px solid #888 }
img { max-width: 100%; }
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<div>
<header>
<a href="#main">Skip navigation</a>
<a href="/view/index">Home</a>
<a href="/edit/{{.Name}}">Edit this page</a>
<form role="search" action="/search" method="GET">
<input type="text" spellcheck="false" name="q" required>
<button>Search</button>
</form>
</div>
<div>
{{.Html}}
</div>
</header>
<main id="main">
<h1>{{.Title}}</h1>
{{.Html}}
</main>
<footer>
<address>
Comments? Send mail to Alex Schroeder <<a href="mailto:alex@alexschroeder.ch">alex@alexschroeder.ch</a>>
</address>
</footer>
</body>
</html>

12
wiki.go
View File

@@ -1,12 +1,12 @@
package main
import (
"fmt"
"html/template"
"net/http"
"strings"
"regexp"
"fmt"
"os"
"regexp"
"strings"
)
// Templates are parsed at startup.
@@ -45,7 +45,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
// "view.html" template is used to show the rendered HTML.
func viewHandler(w http.ResponseWriter, r *http.Request, name string) {
// Short cut for text files
if (strings.HasSuffix(name, ".txt")) {
if strings.HasSuffix(name, ".txt") {
body, err := os.ReadFile(name)
if err == nil {
w.Write(body)
@@ -94,7 +94,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request, name string) {
// /edit/foo/bar, the editHandler is called with "foo/bar" as its
// argument. This uses the second group from the validPath regular
// expression.
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m != nil {
@@ -134,5 +134,5 @@ func main() {
loadIndex()
port := getPort()
fmt.Printf("Serving a wiki on port %s\n", port)
http.ListenAndServe(":" + port, nil)
http.ListenAndServe(":"+port, nil)
}