2 Commits

Author SHA1 Message Date
Alex Schroeder
8eb700fb0b Use full text search 2023-09-25 14:07:00 +02:00
Alex Schroeder
7514c2173b Add exact serch from the command line 2023-09-25 09:21:13 +02:00
185 changed files with 1796 additions and 12663 deletions

View File

@@ -1,4 +1,4 @@
This software is Copyright (c) 20152024 by Alex Schroeder.
This software is Copyright (c) 20152023 by Alex Schroeder.
This is free software, licensed under:

View File

@@ -1,78 +1,44 @@
SHELL=/bin/bash
PREFIX=${HOME}/.local
.PHONY: help build test run upload docs install priv
help:
@echo Help for Oddmu
@echo ==============
@echo =====================
@echo
@echo make run
@echo " runs program, offline"
@echo
@echo make test
@echo " runs the tests without log output"
@echo " runs the tests"
@echo
@echo make docs
@echo " create man pages from text files"
@echo make build
@echo
@echo go build
@echo " just build it"
@echo
@echo make install
@echo " install the files to ~/.local"
@echo
@echo make upload
@echo " this is how I upgrade my server"
@echo make dist
@echo " cross compile for other systems"
@echo make clean
@echo " remove built files"
build: oddmu
oddmu: *.go
go build
test:
rm -rf testdata/*
go test -shuffle on .
run:
go run .
upload: build
test:
go test
upload:
go 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; systemctl restart community"
ssh sibirocobombus.root "systemctl restart oddmu; systemctl restart alex"
@echo Changes to the template files need careful consideration
docs:
cd man; make man
cd man; make
install:
for n in 1 5 7; do install -D -t ${PREFIX}/share/man/man$$n man/*.$$n; done
install -D -t ${PREFIX}/bin oddmu
clean:
rm --force oddmu oddmu.exe oddmu-{linux,darwin,windows}-{amd64,arm64}{,.tar.gz}
cd man && make clean
dist: oddmu-linux-amd64.tar.gz oddmu-linux-arm64.tar.gz oddmu-darwin-amd64.tar.gz oddmu-windows-amd64.tar.gz
oddmu-linux-amd64: *.go
GOOS=linux GOARCH=amd64 go build -o $@
oddmu-linux-arm64: *.go
env GOOS=linux GOARCH=arm64 GOARM=5 go build -o $@
oddmu-darwin-amd64: *.go
GOOS=darwin GOARCH=arm64 go build -o $@
oddmu.exe: *.go
GOOS=windows GOARCH=amd64 go build -o $@
oddmu-windows-amd64.tar.gz: oddmu.exe
cd man && make html
tar --create --file $@ --transform='s/^/oddmu\//' --exclude='*~' \
$< *.md man/*.[157].{html,md} themes/
%.tar.gz: %
tar --create --file $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
$< Makefile *.socket *.service *.md man/Makefile man/*.1 man/*.5 man/*.7 themes/
priv:
sudo setcap 'cap_net_bind_service=+ep' oddmu
make docs
for n in 1 5 7; do install -D -t $$HOME/.local/share/man/man$$n man/*.$$n; done
go build
install -D -t $$HOME/.local/bin oddmu

378
README.md
View File

@@ -1,185 +1,41 @@
# Oddµ: A minimal wiki
This program helps you run a minimal wiki, blog, digital garden, memex
or Zettelkasten. There is no version history.
This program runs a wiki. It serves all the Markdown files (ending in
`.md`) into web pages and allows you to edit them. If your files don't
provide their own title (`# title`), the file name (without `.md`) is
used for the title. Subdirectories are created as necessary.
It's well suited as a self-hosted, single-user web application, when
there is no need for collaboration on the site itself. Links and email
connect you to the rest of the net. The wiki can be public or private.
Perhaps it just runs on your local machine, unreachable from the
Internet.
This is a minimal wiki. There is no version history. It's well suited
as a *secondary* medium: collaboration and conversation happens
elsewhere, in chat, on social media. The wiki serves as the text
repository that results from these discussions.
It's well suited as a secondary medium for a close-knit group:
collaboration and conversation happens elsewhere, in chat, on social
media. The wiki serves as the text repository that results from these
discussions. As there are no logins and no version histories, it is
not possible to undo vandalism and spam. Only allow people you trust
write-access to the site.
The wiki lists no recent changes. The expectation is that the people
that care were involved in the discussions beforehand.
It's well suited as a simple static site generator. There are no
plugins.
This wiki uses a [Markdown
library](https://github.com/gomarkdown/markdown) to generate the web
pages from Markdown. There are two extensions Oddmu adds to the
library: local links `[[like this]]` and hashtags `#Like_This`.
When Oddµ runs as a web server, it serves all the Markdown files
(ending in `.md`) as web pages. These pages can be edited via the web.
This wiki uses the [lingua](github.com/pemistahl/lingua-go) library to
detect languages in order to get hyphenation right.
Oddmu adds the following extensions to Markdown: local links `[[like
this]]`, hashtags `#Like_This` and fediverse account links like
`@alex@alexschroeder.ch`.
If your pages don't provide their own title (`# title`), the file name
(without `.md`) is used as the title. Subdirectories are created as
necessary.
Other files can be uploaded and images (ending in `.jpg`, `.jpeg`,
`.png`, `.heic` or `.webp`) can be resized when they are uploaded
(resulting in `.jpg` or `.png` files).
## Documentation
This project uses man(1) pages. They are generated from text files
using [scdoc](https://git.sr.ht/~sircmpwn/scdoc). These are the files
available:
[oddmu(1)](https://alexschroeder.ch/view/oddmu/oddmu.1): This man page
has a short introduction to Oddmu, its configuration via templates and
environment variables, plus points to the other man pages.
[oddmu(5)](https://alexschroeder.ch/view/oddmu/oddmu.5): This man page
talks about the Markdown and includes some examples for the
non-standard features such as table markup. It also talks about the
Oddmu extensions to Markdown: wiki links, hashtags and fediverse
account links. Local links must use percent encoding for page names so
there is a section about percent encoding. The man page also explains
how feeds are generated.
[oddmu-releases(7)](https://alexschroeder.ch/view/oddmu/oddmu-releases.7):
This man page lists all the Oddmu versions and their user-visible
changes.
[oddmu-version(1)](https://alexschroeder.ch/view/oddmu/oddmu-version.1):
This man page documents the "version" subcommand which you can use to
get the installed Oddmu version.
Working locally:
[oddmu-links(1)](https://alexschroeder.ch/view/oddmu/oddmu-links.1):
This man page documents the "links" subcommand which you can use to
get the outgoing links for a page.
[oddmu-list(1)](https://alexschroeder.ch/view/oddmu/oddmu-list.1):
This man page documents the "list" subcommand which you can use to get
page names and page titles.
[oddmu-replace(1)](https://alexschroeder.ch/view/oddmu/oddmu-replace.1):
This man page documents the "replace" subcommand to make mass changes
to the files much like find(1), grep(1) and sed(1) or perl(1).
[oddmu-search(1)](https://alexschroeder.ch/view/oddmu/oddmu-search.1):
This man page documents the "search" subcommand which you can use to
build indexes lists of page links. These are important for feeds.
[oddmu-search(7)](https://alexschroeder.ch/view/oddmu/oddmu-search.7):
This man page documents how search and scoring work.
[oddmu-toc(1)](https://alexschroeder.ch/view/oddmu/oddmu-toc.1): This
man page documents the "toc" subcommand which you can use to generate
a table of contents linking to all the headings on the page.
Reporting:
[oddmu-missing(1)](https://alexschroeder.ch/view/oddmu/oddmu-missing.1):
This man page documents the "missing" subcommand to list local links
that don't point to any existing pages or files.
[oddmu-hashtags(1)](https://alexschroeder.ch/view/oddmu/oddmu-hashtags.1):
This man page documents the "hashtags" subcommand to count the
hashtags used from the command line.
Static site generator:
[oddmu-html(1)](https://alexschroeder.ch/view/oddmu/oddmu-html.1):
This man page documents the "html" subcommand to generate HTML from
Markdown pages from the command line.
[oddmu-static(1)](https://alexschroeder.ch/view/oddmu/oddmu-static.1):
This man page documents the "static" subcommand to generate an entire
static website from the command line, avoiding the need to run Oddmu
as a server. Also great for archiving.
[oddmu-notify(1)](https://alexschroeder.ch/view/oddmu/oddmu-notify.1):
This man page documents the "notify" subcommand to add links to
hashtag pages, index and changes for a given page. This is useful when
you edit the Markdown files locally.
Configuration:
[oddmu-templates(5)](https://alexschroeder.ch/view/oddmu/oddmu-templates.5):
This man page documents how the templates can be changed (how they
*must* be changed) and lists the attributes available for the various
templates.
System administration:
[oddmu-apache(5)](https://alexschroeder.ch/view/oddmu/oddmu-apache.5):
This man page documents how to set up the Apache web server for
various common tasks such as using logins to limit what visitors can
edit.
[oddmu-filter(7)](https://alexschroeder.ch/view/oddmu/oddmu-filter.7):
This man page documents how to exclude subdirectories from search and
archiving.
[oddmu-nginx(5)](https://alexschroeder.ch/view/oddmu/oddmu-nginx.5):
This man page documents how to set up the freenginx web server for
various common tasks such as using logins to limit what visitors can
edit.
[oddmu.service(5)](https://alexschroeder.ch/view/oddmu/oddmu.service.5):
This man page documents how to setup a systemd unit and have it manage
Oddmu. “Great configurability brings great burdens.”
[oddmu-webdav(5)](https://alexschroeder.ch/view/oddmu/oddmu-webdav.5):
This man page documents how to set up the Apache web server so that
the wiki can be accessed via Web-DAV.
Leaving:
[oddmu-export(1)](https://alexschroeder.ch/view/oddmu/oddmu-export.1):
This man page documents how to export all the pages as one RSS feed so
that you can import them all into a new platform that doesn't use
Markdown files.
This wiki uses the standard
[html/template](https://pkg.go.dev/html/template) library to generate
HTML.
## Building
To build the binary:
```sh
go build
```
The man pages are already built. If you want to rebuild them, you need
to have [scdoc](https://git.sr.ht/~sircmpwn/scdoc) installed.
```sh
make docs
```
The `Makefile` in the `man` directory has targets to create Markdown
and HTML files.
The HEIC library uses C code and prevents cross-compilation.
As the repository changed URLs a few times (from GitHub, to
self-hosted using `cgit` to self-hosted using `legit`), there is no
way to install it using `go install`. You need to `git clone` the
repository and build it locally.
## Running
The working directory is where pages are saved and where templates are
loaded from. You need a copy of the template files in this directory.
Here's how to build and run straight from the source directory:
Here's how to start it in the source directory:
```sh
go run .
@@ -188,200 +44,6 @@ go run .
The program serves the local directory as a wiki on port 8080. Point
your browser to http://localhost:8080/ to use it.
Once the `oddmu` binary is built, you can run it instead:
```sh
./oddmu
```
To read the main man page witihout installing Oddmu:
```sh
man -l man/oddmu.1
```
## Installing
This installs `oddmu` into `$HOME/.local/bin` and the manual pages
into `$HOME/.local/share/man/`.
```sh
make install
```
Here's an example using [GNU Stow](https://www.gnu.org/software/stow/)
to install it into `/usr/local/stow` in a way that allows you to
uninstall it later:
```sh
sudo mkdir /usr/local/stow/oddmu
sudo make install PREFIX=/usr/local/stow/oddmu/
cd /usr/local/stow
sudo stow oddmu
```
## Hacking
If you're interested in making changes to the code, here's a
high-level introduction to the various source files.
- `*_test.go` are the test files; a few library functions are defined
in `wiki_test.go`.
- `*_cmd.go` are the files implementing the various subcommands with
matching names
- `accounts.go` implements the webfinger code to fetch fediverse
account link destinations with the URI provided by webfinger
- `add_append.go` implements the `/add` and `/append` handlers
- `archive.go` implements the `/archive` handler
- `changes.go` implements the "notifications": the automatic addition
of links to index, changes and hashtag files when pages are edited
- `diff.go` implements the `/diff` handler
- `edit_save.go` implements the `/edit` and `/save` handlers
- `feed.go` implements the feed for a page based on the links it lists
- `highlight.go` implements the bold tags for matches when showing
search results
- `index.go` implements the index of all the hashtags
- `languages.go` implements the language detection
- `list.go` implements the file list page
- `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
- `templates.go` implements template loading and reloading
- `tokenizer.go` implements the various tokenizers used
- `upload_drop.go` implements the `/upload` and `/drop` handlers
- `view.go` implements the `/view` handler
- `watch.go` implements the filesystem notification watch
- `wiki.go` implements the main function
The code of this package is licensed to you under the
AGPL-3.0-or-later license. If you do make changes and your site is
public, be aware of section 13:
> … if you modify the Program, your modified version must prominently
> offer all users interacting with it remotely through a computer
> network (if your version supports such interaction) an opportunity
> to receive the Corresponding Source of your version by providing
> access to the Corresponding Source from a network server at no
> charge, through some standard or customary means of facilitating
> copying of software.
### Changing the markup rules
If you want to change the markup rules, your starting point should be
`parser.go`. Make sure you read the documentation of [Go
Markdown](https://github.com/gomarkdown/markdown) and note that it
offers MathJax support (needs a change to the `view.html` template so
that the MathJax Javascript gets loaded) and
[MMark](https://mmark.miek.nl/post/syntax/) support, and it shows how
extensions can be added.
### Filenames and URL path
One of the sad parts of the code is the distinction between path and
filepath. On a Linux system, this doesn't matter. I suspect that it
also doesn't matter on MacOS and Windows because the file systems
handle forward slashes just fine. The code still tries to do the right
thing. A path that is derived from a URL is a path with slashes.
Before accessing a file, it has to be turned into a filepath using
`filepath.FromSlashes` and in the rare case where the inverse happens,
use `filepath.ToSlashes`. Any path received via the URL path uses
slashes and needs to be converted to a filepath before passing it to
any `os` function. Any path received within a `path/filepath.WalkFunc`
is a filepath and needs to be converted to use slashes when used in
HTML output.
If you need to access the page name in code that is used from a
template, you have to decode the path. See the code in `diff.go` for
an example.
### HTTP handlers
The URL paths all have the form `/action/directory/pagename` (with
directory being optional and pagename sometimes being optional). If
you need to limit access in Apache or nginx or some other web server
acting as a [reverse
proxy](https://en.wikipedia.org/wiki/Reverse_proxy), you can do that.
See `man oddmu-apache` and `man oddmu-nginx` for some configuration
examples.
This is how you can prevent some actions by simply not passing them on
to Oddmu, or you can require authentication for certain actions.
Furthermore, you can do the same for directories, allowing you to use
subdirectories as separate sites, each with their own editors.
### Templates
The `themes` folder has some ideas of how to tweak the HTML templates.
### Permissions
An unexplored idea would be to parse a config file that has usernames
and passwords, groups usernames into roles, and assigns access to the
various actions based on these roles. This would obviate the need for
a web server acting as a reverse proxy.
Then again, not having to care about roles and permissions has been a
relief.
## Dependencies
This section lists the non-standard libraries Oddmu uses and their
respective licenses.
[github.com/gomarkdown/markdown](https://github.com/gomarkdown/markdown)
is used to generate the web pages from Markdown. BSD-2-Clause.
[github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday)
is used to strip rendered search results of all HTML except for the
bold tag. Regular HTML generated from pages is *not* sanitized. Don't
give people you don't trust access to your wiki. BSD-3-Clause.
[github.com/pemistahl/lingua-go](https://github.com/pemistahl/lingua-go)
detects languages in order to set the language tag in templates. This
in turn can be used by browsers to get hyphenation right. Apache-2.0.
[github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype)
is used to sniff the MIME type of files with unknown filename
extensions. MIT.
[github.com/gen2brain/heic](https://github.com/gen2brain/heic) is
used to decode HEIC files (the new default file format for photos on
iPhones). LGPL-3.0-only.
[github.com/disintegration/imaging](https://github.com/disintegration/imaging)
is used to resize images. MIT.
[github.com/edwvee/exiffix](https://github.com/edwvee/exiffix) is used
to rotate images before resizing them if the EXIF data says the image
wasn't taken with the default orientation of the camera. This is
necessary because after resizing, the EXIF data is gone. MIT.
[github.com/google/subcommands](https://github.com/google/subcommands)
is used for the parsing and documenting of subcommands. Apache-2.0.
[github.com/muesli/reflow/wordwrap](https://github.com/muesli/reflow/wordwrap)
is used to wrap the search subcommand output. MIT.
[github.com/hexops/gotextdiff](https://github.com/hexops/gotextdiff)
is used to show a compact unified diff on the command line before
doing any replacements. BSD-3-Clause.
[github.com/sergi/go-diff/diffmatchpatch](https://github.com/sergi/go-diff/diffmatchpatch)
is used to show the page diffs on the web. MIT.
[github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)
is used to watch the filesystem for changes. BSD-3-Clause.
[golang.org/x/exp/constraints](https://golang.org/x/exp/constraints)
for the computation of the intersection between two sets of pages.
BSD-3-Clause.
[github.com/stretchr/testify/assert](https://github.com/stretchr/testify/assert)
is used for testing. MIT.
## Bugs
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.

16
RELEASE
View File

@@ -1,16 +0,0 @@
When preparing a new release
----------------------------
1. Run tests
2. Update man/oddmu-releases.7.txt
- add missing items
- change "(unreleased)"
3. make docs
4. Make sure all files are checked in
5. Tag the release and push the tag to all remotes
6. cd man && make upload

6
TODO.md Normal file
View File

@@ -0,0 +1,6 @@
Upload files should use path info so that we can use Apache to
restrict access to directories.
Automatically scale or process files.
Post by Delta Chat? That is, allow certain encrypted emails to post.

View File

@@ -2,45 +2,52 @@ package main
import (
"encoding/json"
"log"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"io"
"log"
"net/http"
"os"
"sync"
)
// useWebfinger indicates whether Oddmu looks up the profile pages of fediverse accounts. To enable this, set the
// environment variable ODDMU_WEBFINGER to "1".
// useWebfinger indicates whether Oddmu looks up the profile pages of
// fediverse accounts. To enable this, set the environment variable
// ODDMU_WEBFINGER to "1".
var useWebfinger = false
// accountStore controlls access to the usernames. Make sure to lock and unlock as appropriate.
type accountStore struct {
// Accounts contains the map used to set the usernames. Make sure to
// lock and unlock as appropriate.
type Accounts struct {
sync.RWMutex
// uris is a map, mapping account names likes "@alex@alexschroeder.ch" to URIs like
// uris is a map, mapping account names likes
// "@alex@alexschroeder.ch" to URIs like
// "https://social.alexschroeder.ch/@alex".
uris map[string]string
}
// accounts holds the global mapping of accounts to profile URIs.
var accounts accountStore
var accounts Accounts
// This is called once at startup and therefore does not need to be locked. On every restart, this map starts empty and
// is slowly repopulated as pages are visited.
func init() {
// initAccounts sets up the accounts map. This is called once at
// startup and therefore does not need to be locked. On ever restart,
// this map starts empty and is slowly repopulated as pages are
// visited.
func initAccounts() {
if os.Getenv("ODDMU_WEBFINGER") == "1" {
accounts.uris = make(map[string]string)
useWebfinger = true
}
}
// accountLink links a social media accountLink like @accountLink@domain to a profile page like https://domain/user/accountLink. Any
// accountLink seen for the first time uses a best guess profile URI. It is also looked up using webfinger, in parallel. See
// lookUpAccountUri. If the lookup succeeds, the best guess is replaced with the new URI so on subsequent requests, the
// URI is correct.
func accountLink(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
// account links a social media account like @account@domain to a
// profile page like https://domain/user/account. Any account seen for
// the first time uses a best guess profile URI. It is also looked up
// using webfinger, in parallel. See lookUpAccountUri. If the lookup
// succeeds, the best guess is replaced with the new URI so on
// subsequent requests, the URI is correct.
func account(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
data = data[offset:]
i := 1 // skip @ of username
n := len(data)
@@ -50,7 +57,6 @@ func accountLink(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
data[i] >= '0' && data[i] <= '9' ||
data[i] == '@' ||
data[i] == '.' ||
data[i] == '_' ||
data[i] == '-') {
if data[i] == '@' {
if d != 0 {
@@ -82,16 +88,17 @@ func accountLink(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
go lookUpAccountUri(string(account), string(domain))
}
link := &ast.Link{
AdditionalAttributes: []string{`class="account"`},
Destination: []byte(uri),
Title: data[0:i],
Destination: []byte(uri),
Title: data[0:i],
}
ast.AppendChild(link, &ast.Text{Leaf: ast.Leaf{Literal: data[0 : d-1]}})
return i, link
}
// lookUpAccountUri is called for accounts that haven't been seen before. It calls webfinger and parses the JSON. If
// possible, it extracts the link to the profile page and replaces the entry in accounts.
// lookUpAccountUri is called for accounts that haven't been seen
// before. It calls webfinger and parses the JSON. If possible, it
// extracts the link to the profile page and replaces the entry in
// accounts.
func lookUpAccountUri(account, domain string) {
uri := "https://" + domain + "/.well-known/webfinger"
resp, err := http.Get(uri + "?resource=acct:" + account)
@@ -105,7 +112,7 @@ func lookUpAccountUri(account, domain string) {
log.Printf("Failed to read from %s: %s", account, err)
return
}
var wf webFinger
var wf WebFinger
err = json.Unmarshal([]byte(body), &wf)
if err != nil {
log.Printf("Failed to parse the JSON from %s: %s", account, err)
@@ -121,24 +128,25 @@ func lookUpAccountUri(account, domain string) {
accounts.uris[account] = uri
}
// link a link in the WebFinger JSON.
type link struct {
// Link a link in the WebFinger JSON.
type Link struct {
Rel string `json:"rel"`
Type string `json:"type"`
Href string `json:"href"`
}
// webFinger is a structure used to unmarshall JSON.
type webFinger struct {
// WebFinger is a structure used to unmarshall JSON.
type WebFinger struct {
Subject string `json:"subject"`
Aliases []string `json:"aliases"`
Links []link `json:"links"`
Links []Link `json:"links"`
}
// parseWebFinger parses the web finger JSON and returns the profile page URI. For unmarshalling the JSON, it uses the
// Link and WebFinger structs.
// parseWebFinger parses the web finger JSON and returns the profile
// page URI. For unmarshalling the JSON, it uses the Link and
// WebFinger structs.
func parseWebFinger(body []byte) (string, error) {
var wf webFinger
var wf WebFinger
err := json.Unmarshal(body, &wf)
if err != nil {
return "", err

View File

@@ -5,6 +5,16 @@ import (
"testing"
)
// This causes network access!
// func TestPageAccount(t *testing.T) {
// initAccounts()
// p := &Page{Body: []byte(`@alex, @alex@alexschroeder.ch said`)}
// p.renderHtml()
// r := `<p>@alex, <a href="https://alexschroeder.ch/users/alex" rel="nofollow">@alex</a> said</p>
// `
// assert.Equal(t, r, string(p.Html))
// }
func TestWebfingerParsing(t *testing.T) {
body := []byte(`{
"subject": "acct:Gargron@mastodon.social",

View File

@@ -6,15 +6,14 @@
<meta name="viewport" content="width=device-width">
<title>Add to {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
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">
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="{{.Language}}" autofocus required></textarea>
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="" autofocus required></textarea>
<p><input type="submit" value="Add">
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
</form>

View File

@@ -1,8 +1,6 @@
package main
import (
"bytes"
"log"
"net/http"
)
@@ -16,48 +14,24 @@ func addHandler(w http.ResponseWriter, r *http.Request, name string) {
} else {
p.handleTitle(false)
}
renderTemplate(w, p.Dir(), "add", p)
renderTemplate(w, "add", p)
}
// appendHandler takes the "body" form parameter and appends it. The browser is redirected to the page view. This is
// similar to the saveHandler.
// 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{Name: name, Body: []byte(body)}
p = &Page{Title: name, Name: name, Body: []byte(body)}
} else {
p.append([]byte(body))
p.handleTitle(false)
p.Body = append(p.Body, []byte(body)...)
}
p.handleTitle(false)
err = p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
username, _, ok := r.BasicAuth()
if ok {
log.Println("Save", name, "by", username)
} else {
log.Println("Save", name)
}
if r.FormValue("notify") == "on" {
err = p.notify()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/view/"+name, http.StatusFound)
}
func (p *Page) append(body []byte) {
// ensure an empty line at the end
if bytes.HasSuffix(p.Body, []byte("\n\n")) {
} else if bytes.HasSuffix(p.Body, []byte("\n")) {
p.Body = append(p.Body, '\n')
} else {
p.Body = append(p.Body, '\n', '\n')
}
p.Body = append(p.Body, body...)
}

View File

@@ -6,67 +6,30 @@ import (
"os"
"regexp"
"testing"
"time"
)
func TestEmptyLineAdd(t *testing.T) {
p := &Page{Name: "testdata/add/fire", Body: []byte(`# Coal
Black rocks light as foam
Shaking, puring, shoveling`)}
p.append([]byte("Into the oven"))
assert.Equal(t, string(p.Body), `# Coal
Black rocks light as foam
Shaking, puring, shoveling
Into the oven`)
}
// wipes testdata
func TestAddAppend(t *testing.T) {
cleanup(t, "testdata/add")
_ = os.RemoveAll("testdata")
index.load()
p := &Page{Name: "testdata/add/fire", Body: []byte(`# Fire
p := &Page{Name: "testdata/fire", Body: []byte(`# Fire
Orange sky above
Reflects a distant fire
It's not `)}
p.save()
data := url.Values{}
data.Set("body", "barbecue")
assert.Regexp(t, regexp.MustCompile("a distant fire"),
assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/add/fire", nil))
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/fire", nil))
assert.NotRegexp(t, regexp.MustCompile("a distant fire"),
assert.HTTPBody(makeHandler(addHandler, true),
"GET", "/add/testdata/add/fire", nil))
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/add/fire", data, "/view/testdata/add/fire")
assert.Regexp(t, regexp.MustCompile(`not</p>\s*<p>barbecue`),
assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/add/fire", nil))
}
func TestAddAppendChanges(t *testing.T) {
cleanup(t, "testdata/append")
today := time.Now().Format(time.DateOnly)
p := &Page{Name: "testdata/append/" + today + "-water", Body: []byte(`# Water
Sunlight dancing fast
Blue and green and pebbles gray
`)}
p.save()
data := url.Values{}
data.Set("body", "Stand in cold water")
data.Add("notify", "on")
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/append/"+today+"-water",
data, "/view/testdata/append/"+today+"-water")
// The changes.md file was created
s, err := os.ReadFile("testdata/append/changes.md")
assert.NoError(t, err)
assert.Equal(t, "# Changes\n\n## "+today+"\n* [Water]("+today+"-water)\n", string(s))
// Link added to index.md file
s, err = os.ReadFile("testdata/append/index.md")
assert.NoError(t, err)
// New index contains just the link
assert.Equal(t, string(s), "* [Water]("+today+"-water)\n")
assert.HTTPBody(makeHandler(addHandler, true), "GET", "/add/testdata/fire", nil))
HTTPRedirectTo(t, makeHandler(appendHandler, true), "POST", "/append/testdata/fire", data, "/view/testdata/fire")
assert.Regexp(t, regexp.MustCompile("Its not barbecue"),
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/fire", nil))
t.Cleanup(func() {
_ = os.RemoveAll("testdata")
})
}

View File

@@ -1,69 +0,0 @@
package main
import (
"archive/zip"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
// archiveHandler serves a zip file. Directories starting with a period are skipped. Filenames starting with a period
// are skipped. If the environment variable ODDMU_FILTER is a regular expression that matches the starting directory,
// this is a "separate site"; if the regular expression does not match, this is the "main site" and page names must also
// not match the regular expression.
func archiveHandler(w http.ResponseWriter, r *http.Request, path string) {
filter := os.Getenv("ODDMU_FILTER")
re, err := regexp.Compile(filter)
if err != nil {
log.Println("ODDMU_FILTER does not compile:", filter, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
matches := re.MatchString(path)
dir := filepath.Dir(filepath.FromSlash(path))
z := zip.NewWriter(w)
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
return filepath.SkipDir
}
} else if !strings.HasPrefix(filepath.Base(path), ".") &&
(matches || !re.MatchString(path)) {
zf, err := z.Create(path)
if err != nil {
log.Println(err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Println(err)
return err
}
_, err = io.Copy(zf, file)
if err != nil {
log.Println(err)
return err
}
}
return nil
})
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = z.Close()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@@ -1,27 +0,0 @@
package main
import (
"archive/zip"
"github.com/stretchr/testify/assert"
"os"
"strings"
"testing"
)
func TestArchive(t *testing.T) {
cleanup(t, "testdata/archive")
assert.NoError(t, os.MkdirAll("testdata/archive/public", 0755))
assert.NoError(t, os.MkdirAll("testdata/archive/secret", 0755))
assert.NoError(t, os.WriteFile("testdata/archive/public/index.md", []byte("# Public\nChurch tower bells ringing\nA cold wind biting my ears\nWalk across the square"), 0644))
assert.NoError(t, os.WriteFile("testdata/archive/secret/index.md", []byte("# Secret\nMany years ago I danced\nSpending nights in clubs and bars\nIt is my secret"), 0644))
os.Setenv("ODDMU_FILTER", "^testdata/archive/secret/")
body := assert.HTTPBody(makeHandler(archiveHandler, true), "GET", "/archive/testdata/data.zip", nil)
r, err := zip.NewReader(strings.NewReader(body), int64(len(body)))
assert.NoError(t, err, "Unzip")
names := []string{}
for _, file := range r.File {
names = append(names, file.Name)
}
assert.Contains(t, names, "testdata/archive/public/index.md")
assert.NotContains(t, names, "testdata/archive/secret/index.md")
}

View File

@@ -1,187 +0,0 @@
package main
import (
"log"
"path"
"regexp"
"strings"
"time"
)
// notify adds a link to the "changes" page, the "index" page, as well as to all the existing hashtag pages. The link to
// the "index" page is only added if the page being edited is a blog page for the current year. The link to existing
// hashtag pages is only added for blog pages. If the "changes" page does not exist, it is created. If the hashtag page
// does not exist, it is not. Hashtag pages are considered optional. If the page that's being edited is in a
// subdirectory, then the "changes", "index" and hashtag pages of that particular subdirectory are affected. Every
// subdirectory is treated like a potentially independent wiki. Errors are logged before being returned because the
// error messages are confusing from the point of view of the saveHandler.
func (p *Page) notify() error {
p.handleTitle(false)
if p.Title == "" {
p.Title = p.Name
}
esc := nameEscape(path.Base(p.Name))
link := "* [" + p.Title + "](" + esc + ")\n"
re := regexp.MustCompile(`(?m)^\* \[[^\]]+\]\(` + esc + `\)\n`)
dir := path.Dir(p.Name)
err := addLinkWithDate(path.Join(dir, "changes"), link, re)
if err != nil {
log.Printf("Updating changes in %s failed: %s", dir, err)
return err
}
if p.IsBlog() {
// Add to the index only if the blog post is for the current year
if strings.HasPrefix(path.Base(p.Name), time.Now().Format("2006")) {
err := addLink(path.Join(dir, "index"), true, link, re)
if err != nil {
log.Printf("Updating index in %s failed: %s", dir, err)
return err
}
}
p.renderHtml() // to set hashtags
for _, hashtag := range p.Hashtags {
err := addLink(path.Join(dir, hashtag), false, link, re)
if err != nil {
log.Printf("Updating hashtag %s in %s failed: %s", hashtag, dir, err)
return err
}
}
}
return nil
}
// addLinkWithDate adds the link to a page, with date header for today. If a match already exists, it is removed. If
// this leaves a date header without any links, it is removed as well. If a list is found, the link is added at the top
// of the list. Lists must use the asterisk, not the minus character.
func addLinkWithDate(name, link string, re *regexp.Regexp) error {
date := time.Now().Format(time.DateOnly)
org := ""
p, err := loadPage(name)
if err != nil {
// create a new page
p = &Page{Name: name, Body: []byte("# Changes\n\n## " + date + "\n" + link)}
} else {
org = string(p.Body)
// remove the old match, if one exists
loc := re.FindIndex(p.Body)
if loc != nil {
r := p.Body[:loc[0]]
if loc[1] < len(p.Body) {
r = append(r, p.Body[loc[1]:]...)
}
p.Body = r
if loc[0] >= 14 && len(p.Body) >= loc[0]+15 {
// remove the preceding date if there are now two dates following each other
re := regexp.MustCompile(`(?m)^## (\d\d\d\d-\d\d-\d\d)\n\n## (\d\d\d\d-\d\d-\d\d)\n`)
if re.Match(p.Body[loc[0]-14 : loc[0]+15]) {
p.Body = append(p.Body[0:loc[0]-14], p.Body[loc[0]+1:]...)
}
} else if len(p.Body) == loc[0] {
// remove a trailing date
re := regexp.MustCompile(`## (\d\d\d\d-\d\d-\d\d)\n`)
if re.Match(p.Body[loc[0]-14 : loc[0]]) {
p.Body = p.Body[0 : loc[0]-14]
}
}
}
// locate the beginning of the list to insert the line
re := regexp.MustCompile(`(?m)^\* \[[^\]]+\]\([^\)]+\)\n`)
loc = re.FindIndex(p.Body)
if loc == nil {
// if no list was found, use the end of the page
loc = []int{len(p.Body)}
}
// start with new page content
r := []byte("")
// check if there is a date right before the insertion point
addDate := true
if loc[0] >= 14 {
re := regexp.MustCompile(`(?m)^## (\d\d\d\d-\d\d-\d\d)\n`)
m := re.Find(p.Body[loc[0]-14 : loc[0]])
if m == nil {
// not a date: insert date, don't move insertion point
} else if string(p.Body[loc[0]-11:loc[0]-1]) == date {
// if the date is our date, don't add it, don't move insertion point
addDate = false
} else {
// if the date is not out date, move the insertion point
loc[0] -= 14
}
}
// append up to the insertion point
r = append(r, p.Body[:loc[0]]...)
// append date, if necessary
if addDate {
// ensure paragraph break
if len(r) > 0 && r[len(r)-1] != '\n' {
r = append(r, '\n')
}
if len(r) > 1 && r[len(r)-2] != '\n' {
r = append(r, '\n')
}
r = append(r, []byte("## ")...)
r = append(r, []byte(date)...)
r = append(r, '\n')
}
// append link
r = append(r, []byte(link)...)
// if we just added a date, add an empty line after the single-element list
if len(p.Body) > loc[0] && p.Body[loc[0]] != '*' {
r = append(r, '\n')
}
// append the rest
r = append(r, p.Body[loc[0]:]...)
p.Body = r
}
// only save if something changed
if string(p.Body) != org {
return p.save()
}
return nil
}
// addLink adds a link to a named page, if the page exists and doesn't contain the link. If the link exists but with a
// different title, the title is fixed.
func addLink(name string, mandatory bool, link string, re *regexp.Regexp) error {
p, err := loadPage(name)
if err != nil {
if mandatory {
p = &Page{Name: name, Body: []byte(link)}
return p.save()
} else {
// Skip non-existing files: no error
return nil
}
}
org := string(p.Body)
// if a link exists, that's the place to insert the new link (in which case loc[0] and loc[1] differ)
loc := re.FindIndex(p.Body)
// if no link exists, find a good place to insert it
if loc == nil {
// locate the beginning of the list to insert the line
re = regexp.MustCompile(`(?m)^\* \[[^\]]+\]\([^\)]+\)\n`)
loc = re.FindIndex(p.Body)
if loc == nil {
// if no list was found, use the end of the page
m := len(p.Body)
loc = []int{m, m}
} else {
// if a list item was found, use just the beginning as insertion point
loc[1] = loc[0]
}
}
// start with new page content
r := []byte("")
// append up to the insertion point
r = append(r, p.Body[:loc[0]]...)
// append link
r = append(r, []byte(link)...)
// append the rest
r = append(r, p.Body[loc[1]:]...)
p.Body = r
// only save if something changed
if string(p.Body) != org {
return p.save()
}
return nil
}

View File

@@ -1,206 +0,0 @@
package main
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
)
// Note TestEditSaveChanges and TestAddAppendChanges.
func TestChanges(t *testing.T) {
cleanup(t, "testdata/washing")
today := time.Now().Format(time.DateOnly)
p := &Page{Name: "testdata/washing/" + today + "-machine",
Body: []byte(`# Washing machine
Churning growling thing
Water spraying in a box
Out of sight and dark`)}
p.notify()
// Link added to changes.md file
s, err := os.ReadFile("testdata/washing/changes.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "[Washing machine]("+today+"-machine)")
// Link added to index.md file
s, err = os.ReadFile("testdata/washing/index.md")
assert.NoError(t, err)
// New index contains just the link
assert.Equal(t, string(s), "* [Washing machine]("+today+"-machine)\n")
}
func TestChangesWithHashtag(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Haiku\n"
line := "* [Hotel room](2023-10-27-hotel)\n"
h := &Page{Name: "testdata/changes/Haiku", Body: []byte(intro)}
h.save()
p := &Page{Name: "testdata/changes/2023-10-27-hotel",
Body: []byte(`# Hotel room
White linen and white light
Wooden floor and painted walls
Home away from home
#Haiku #Poetry`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
assert.Contains(t, string(s), line)
s, err = os.ReadFile("testdata/changes/Haiku.md")
assert.NoError(t, err)
assert.Equal(t, intro+line, string(s))
assert.NoFileExists(t, "testdata/changes/Poetry.md")
}
func TestChangesWithList(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
line := "* [a change](change)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list
assert.Equal(t, intro+d+new_line+line, string(s))
}
func TestChangesWithOldList(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](change)\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list
assert.Equal(t, intro+d+new_line+"\n"+y+line, string(s))
}
func TestChangesWithOldDisappearingListAtTheEnd(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](alex)\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list, with the new date, and the old date disappeared
assert.Equal(t, intro+d+new_line, string(s))
}
func TestChangesWithOldDisappearingListInTheMiddle(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
yy := "## " + time.Now().Add(-48*time.Hour).Format(time.DateOnly) + "\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line+"\n"+yy+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list, with the new date, and the old date disappeared
assert.Equal(t, intro+d+new_line+"\n"+yy+other, string(s))
}
func TestChangesWithListAtTheTop(t *testing.T) {
cleanup(t, "testdata/changes")
line := "* [a change](change)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the top, no error due to missing introduction
assert.Equal(t, d+new_line+line, string(s))
}
func TestChangesWithNoList(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph."
d := "## " + time.Now().Format(time.DateOnly) + "\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// into is still there and a new list was started
assert.Equal(t, intro+"\n\n"+d+new_line, string(s))
}
func TestChangesWithUpdate(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
other := "* [other change](whatever)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
line := "* [a change](alex)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+other+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// the change was already listed, but now it moved up and has a new title
assert.Equal(t, intro+d+new_line+other, string(s))
}
func TestChangesWithNoChangeToTheOrder(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](alex)\n"
// the change was already listed at the top, so just use the new title
assert.Equal(t, intro+d+new_line+other, string(s))
// since the file has changed, a backup was necessary
assert.FileExists(t, "testdata/changes/changes.md~")
}
func TestChangesWithNoChanges(t *testing.T) {
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte("# a change\nHallo!")}
p.notify()
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
// the change was already listed at the top, so no change was necessary
assert.Equal(t, intro+d+line+other, string(s))
// since the file hasn't changed, no backup was necessary
assert.NoFileExists(t, "testdata/changes/changes.md~")
}

15
concurrency_test.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
// Use go test -race to see whether this is a race condition.
func TestLoadAndSearch(t *testing.T) {
index.reset()
go index.load()
q := "Oddµ"
pages, _, _ := search(q, 1)
assert.Zero(t, len(pages))
}

68
diff.go
View File

@@ -1,68 +0,0 @@
package main
import (
"bytes"
"github.com/sergi/go-diff/diffmatchpatch"
"html"
"html/template"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
func diffHandler(w http.ResponseWriter, r *http.Request, name string) {
p, err := loadPage(name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
p.handleTitle(true)
p.renderHtml()
renderTemplate(w, p.Dir(), "diff", p)
}
// Diff computes the diff for a page. At this point, renderHtml has already been called so the Name is escaped.
func (p *Page) Diff() template.HTML {
path, err := url.PathUnescape(p.Name)
if err != nil {
return template.HTML("Cannot unescape " + p.Name)
}
fp := filepath.FromSlash(path)
a := fp + ".md~"
t1, err := os.ReadFile(a)
if err != nil {
return template.HTML("Cannot read " + a + ", so the page is new.")
}
b := fp + ".md"
t2, err := os.ReadFile(b)
if err != nil {
return template.HTML("Cannot read " + b + ", so the page was deleted.")
}
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(t1), string(t2), false)
return template.HTML(diff2html(dmp.DiffCleanupSemantic(diffs)))
}
func diff2html(diffs []diffmatchpatch.Diff) string {
var buf bytes.Buffer
for _, item := range diffs {
text := strings.ReplaceAll(html.EscapeString(item.Text), "\n", "<br>")
switch item.Type {
case diffmatchpatch.DiffInsert:
_, _ = buf.WriteString("<ins>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</ins>")
case diffmatchpatch.DiffDelete:
_, _ = buf.WriteString("<del>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</del>")
case diffmatchpatch.DiffEqual:
_, _ = buf.WriteString("<span>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</span>")
}
}
return buf.String()
}

View File

@@ -1,28 +0,0 @@
<!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>{{.Title}}</title>
<style>
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
body { hyphens: auto; }
del { background-color: #fab }
ins { background-color: #af8 }
pre { white-space: normal; background-color: white; border: 1px solid #eee; padding: 1ch }
</style>
</head>
<body>
<header>
<a href="/view/{{.Name}}">Back</a>
</header>
<main id="main">
<h1>{{.Title}}</h1>
<p>This is the diff between <a href="/view/{{.Name}}.md~">the backup</a> and <a href="/view/{{.Name}}.md">the current copy</a>.</p>
<pre>
{{.Diff}}
</pre>
</main>
</body>
</html>

View File

@@ -1,96 +0,0 @@
package main
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
)
func TestDiff(t *testing.T) {
cleanup(t, "testdata/diff")
index.load()
s := `# Bread
The oven breathes
Fills us with the thought of bread
Oh so fresh, so warm.`
r := `# Bread
The oven whispers
Fills us with the thought of bread
Oh so fresh, so warm.`
p := &Page{Name: "testdata/diff/bread", Body: []byte(s)}
p.save()
p.Body = []byte(r)
p.save()
body := assert.HTTPBody(makeHandler(diffHandler, true),
"GET", "/diff/testdata/diff/bread", nil)
assert.Contains(t, body, `<del>breathe</del>`)
assert.Contains(t, body, `<ins>whisper</ins>`)
}
func TestDiffPercentEncoded(t *testing.T) {
cleanup(t, "testdata/diff")
index.load()
s := `# Coup de Gras
Playing D&D
We talk about a killing
Mispronouncing words`
r := `# Coup de Grace
Playing D&D
We talk about a killing
Mispronouncing words`
p := &Page{Name: "testdata/diff/coup de grace", Body: []byte(s)}
p.save()
p.Body = []byte(r)
p.save()
body := assert.HTTPBody(makeHandler(diffHandler, true),
"GET", "/diff/testdata/diff/coup%20de%20grace", nil)
assert.Contains(t, body, `<del>s</del>`)
assert.Contains(t, body, `<ins>ce</ins>`)
}
func TestDiffBackup(t *testing.T) {
cleanup(t, "testdata/backup")
s := `# Cold Rooms
I shiver at home
the monitor glares and moans
fear or cold, who knows?`
r := `# Cold Rooms
I shiver at home
the monitor glares and moans
I hate the machine!`
u := `# Cold Rooms
I shiver at home
the monitor glares and moans
my grey heart grows cold`
p := &Page{Name: "testdata/backup/cold", Body: []byte(s)}
p.save()
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
p.save()
body := string(p.Diff())
// diff from s to r:
assert.Contains(t, body, `<del>fear or cold, who knows?</del>`)
assert.Contains(t, body, `<ins>I hate the machine!</ins>`)
p = &Page{Name: "testdata/backup/cold", Body: []byte(u)}
p.save()
body = string(p.Diff())
// diff from s to u since r was not 60 min or older
assert.Contains(t, body, `<del>fear or cold, who knows?</del>`)
assert.Contains(t, body, `<ins>my grey heart grows cold</ins>`)
// set timestamp 2h in the past
ts := time.Now().Add(-2 * time.Hour)
assert.NoError(t, os.Chtimes("testdata/backup/cold.md~", ts, ts))
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
p.save()
body = string(p.Diff())
// diff from u to r:
assert.Contains(t, body, `<del>my grey heart grows cold</del>`)
assert.Contains(t, body, `<ins>I hate the machine!</ins>`)
}

View File

@@ -4,10 +4,9 @@
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<base href="/view/{{.Dir}}">
<title>Editing {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
form, textarea { width: 100%; }
</style>
</head>
@@ -16,10 +15,8 @@ form, textarea { width: 100%; }
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="# Title
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>
Text" lang="" autofocus>{{printf "%s" .Body}}</textarea>
<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>

View File

@@ -1,12 +1,13 @@
package main
import (
"log"
"net/http"
)
// 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. The edit is saved using the saveHandler.
// 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. The edit is saved using the
// saveHandler.
func editHandler(w http.ResponseWriter, r *http.Request, name string) {
p, err := loadPage(name)
if err != nil {
@@ -14,32 +15,18 @@ func editHandler(w http.ResponseWriter, r *http.Request, name string) {
} else {
p.handleTitle(false)
}
renderTemplate(w, p.Dir(), "edit", p)
renderTemplate(w, "edit", p)
}
// saveHandler takes the "body" form parameter and saves it. The browser is redirected to the page view. This is similar
// to the appendHandler.
// saveHandler takes the "body" form parameter and saves it. The
// browser is redirected to the page view.
func saveHandler(w http.ResponseWriter, r *http.Request, name string) {
body := r.FormValue("body")
p := &Page{Name: name, Body: []byte(body)}
err := p.save()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
username, _, ok := r.BasicAuth()
if ok {
log.Println("Save", name, "by", username)
} else {
log.Println("Save", name)
}
if r.FormValue("notify") == "on" {
err = p.notify() // errors have already been logged, so no logging here
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/view/"+name, http.StatusFound)
}

View File

@@ -2,86 +2,26 @@ package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"net/url"
"os"
"regexp"
"testing"
"time"
)
// wipes testdata
func TestEditSave(t *testing.T) {
cleanup(t, "testdata/save")
_ = os.RemoveAll("testdata")
data := url.Values{}
data.Set("body", "Hallo!")
// View of the non-existing page redirects to the edit page
HTTPRedirectTo(t, makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil, "/edit/testdata/save/alex")
// Edit page can be fetched
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
"GET", "/edit/testdata/save/alex", nil, 200)
// Posting to the save URL saves a page
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/save/alex", data, "/view/testdata/save/alex")
// Page now contains the text
assert.Contains(t, assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil),
"Hallo!")
// Delete the page and you're sent to the empty page
data.Set("body", "")
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/save/alex", data, "/view/testdata/save/alex")
// Viewing the non-existing page redirects to the edit page (like in the beginning)
HTTPRedirectTo(t, makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil, "/edit/testdata/save/alex")
}
HTTPRedirectTo(t, makeHandler(viewHandler, true), "GET", "/view/testdata/alex", nil, "/edit/testdata/alex")
assert.HTTPStatusCode(t, makeHandler(editHandler, true), "GET", "/edit/testdata/alex", nil, 200)
HTTPRedirectTo(t, makeHandler(saveHandler, true), "POST", "/save/testdata/alex", data, "/view/testdata/alex")
assert.Regexp(t, regexp.MustCompile("Hallo!"),
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/alex", nil))
func TestEditSaveChanges(t *testing.T) {
cleanup(t, "testdata/notification")
data := url.Values{}
data.Set("body", "Hallo!")
data.Add("notify", "on")
today := time.Now().Format("2006-01-02")
// Posting to the save URL saves a page
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/notification/"+today,
data, "/view/testdata/notification/"+today)
// The changes.md file was created
s, err := os.ReadFile("testdata/notification/changes.md")
assert.NoError(t, err)
d := time.Now().Format(time.DateOnly)
assert.Equal(t, "# Changes\n\n## "+d+
"\n* [testdata/notification/"+today+"]("+today+")\n",
string(s))
// Link added to index.md file
s, err = os.ReadFile("testdata/notification/index.md")
assert.NoError(t, err)
// New index contains just the link
assert.Equal(t, string(s), "* [testdata/notification/"+today+"]("+today+")\n")
}
// Test the following view.html:
// <form action="/edit/" method="GET">
//
// <label for="id">New page:</label>
// <input id="id" type="text" spellcheck="false" name="id" accesskey="g" value="{{.Dir}}/{{.Today}}" required>
// <button>Edit</button>
//
// </form>
func TestEditId(t *testing.T) {
cleanup(t, "testdata/id")
data := url.Values{}
data.Set("id", "testdata/id/alex")
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
"GET", "/edit/", data, http.StatusBadRequest,
"No slashes in id")
data.Set("id", ".alex")
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
"GET", "/edit/", data, http.StatusForbidden,
"No hidden files")
data.Set("id", "alex")
assert.Contains(t, assert.HTTPBody(makeHandler(editHandler, true),
"GET", "/edit/testdata/id/", data),
"Editing testdata/id/alex")
t.Cleanup(func() {
_ = os.RemoveAll("testdata")
})
}

View File

@@ -1,112 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
htmlTemplate "html/template"
"io"
"os"
"strings"
textTemplate "text/template"
"time"
)
type exportCmd struct {
templateName string
}
func (cmd *exportCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&cmd.templateName, "template", "feed.html", "template filename")
}
func (*exportCmd) Name() string { return "export" }
func (*exportCmd) Synopsis() string { return "export the whole site as one big RSS feed" }
func (*exportCmd) Usage() string {
return `export:
Export the entire site as one big RSS feed. This may allow you to
import the whole site into a different content management system.
The feed contains every page, in HTML format, so the Markdown files
are part of the feed, but none of the other files.
The RSS feed is printed to stdout so you probably want to redirect
it:
oddmu export > /tmp/export.rss
Options:
-template "filename" specifies the template to use (default: feed.html)
`
}
func (cmd *exportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
index.load()
return exportCli(os.Stdout, cmd.templateName, &index)
}
// exportCli runs the export command on the command line. In order to make testing easier, it takes a Writer and an
// indexStore. The Writer is important so that test code can provide a buffer instead of os.Stdout; the indexStore is
// important so that test code can ensure no other test running in parallel can interfere with the list of known pages
// (by adding or deleting pages).
func exportCli(w io.Writer, templateName string, idx *indexStore) subcommands.ExitStatus {
loadLanguages()
feed := new(Feed)
items := []Item{}
// feed.Name remains unset
feed.Date = time.Now().Format(time.RFC3339)
for name, title := range idx.titles {
if name == "index" {
feed.Title = title
}
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
p.handleTitle(false)
p.renderHtml()
fi, err := os.Stat(name + ".md")
if err != nil {
fmt.Fprintf(os.Stderr, "Stat %s: %s\n", name, err)
return subcommands.ExitFailure
}
it := Item{Date: fi.ModTime().Format(time.RFC3339)}
it.Title = p.Title
it.Name = p.Name
it.Body = p.Body
it.Html = htmlTemplate.HTML(htmlTemplate.HTMLEscaper(p.Html))
it.Hashtags = p.Hashtags
items = append(items, it)
}
feed.Items = items
// No effort is made to work with the templates var.
if strings.HasSuffix(templateName, ".html") ||
strings.HasSuffix(templateName, ".xml") ||
strings.HasSuffix(templateName, ".rss") {
w.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"))
t, err := htmlTemplate.ParseFiles(templateName)
if err != nil {
fmt.Fprintf(os.Stderr, "Parsing %s: %s\n", templateName, err)
return subcommands.ExitFailure
}
err = t.Execute(w, feed)
if err != nil {
fmt.Fprintf(os.Stderr, "Writing feed: %s\n", err)
return subcommands.ExitFailure
}
} else {
t, err := textTemplate.ParseFiles(templateName)
if err != nil {
fmt.Fprintf(os.Stderr, "Parsing %s: %s\n", templateName, err)
return subcommands.ExitFailure
}
err = t.Execute(w, feed)
if err != nil {
fmt.Fprintf(os.Stderr, "Writing feed: %s\n", err)
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}

View File

@@ -1,55 +0,0 @@
package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"os"
"regexp"
"testing"
)
func TestExportCmd(t *testing.T) {
b := new(bytes.Buffer)
s := exportCli(b, "feed.html", minimalIndex(t))
assert.Equal(t, subcommands.ExitSuccess, s)
assert.Contains(t, b.String(), "<title>Oddµ: A minimal wiki</title>")
assert.Contains(t, b.String(), "<title>Welcome to Oddµ</title>")
}
func TestExportCmdLanguage(t *testing.T) {
os.Setenv("ODDMU_LANGUAGES", "de,en")
loadLanguages()
p := Page{Body: []byte("This is an English text. All right then!")}
it := Item{Page: p}
assert.Equal(t, "en", it.Language())
}
func TestExportCmdJsonFeed(t *testing.T) {
cleanup(t, "testdata/json")
os.Mkdir("testdata/json", 0755)
assert.NoError(t, os.WriteFile("testdata/json/template.json", []byte(`{
"version": "https://jsonfeed.org/version/1.1",
"title": "{{.Title}}",
"home_page_url": "https://alexschroeder.ch",
"others": [],
"items": [{{range .Items}}
{
"id": "{{.Name}}",
"url": "https://alexschroeder.ch/view/{{.Name}}",
"title": "{{.Title}}",
"language": "{{.Language}}"
"date_modified": "{{.Date}}",
"content_html": "{{.Html}}",
"tags": [{{range .Hashtags}}"{{.}}",{{end}}""],
},{{end}}
{}
]
}
`), 0644))
b := new(bytes.Buffer)
s := exportCli(b, "testdata/json/template.json", minimalIndex(t))
assert.Equal(t, subcommands.ExitSuccess, s)
assert.Contains(t, b.String(), `"title": "Oddµ: A minimal wiki"`)
assert.Regexp(t, regexp.MustCompile("&lt;h1.*&gt;Welcome to Oddµ&lt;/h1&gt;"), b.String()) // skip id
}

25
feed.go
View File

@@ -1,49 +1,32 @@
package main
import (
"bytes"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"html/template"
"bytes"
"os"
"path"
"path/filepath"
"time"
)
// Item is a Page plus a Date.
type Item struct {
// Page is the page being used as the feed item.
Page
// Date is the last modification date of the file storing the page. As the pages used by Oddmu are plain
// Markdown files, they don't contain any metadata. Instead, the last modification date of the file is used.
// This makes it work well with changes made to the files outside of Oddmu.
Date string
}
// Feed is an Item used for the feed itself, plus an array of items based on the linked pages.
type Feed struct {
// Item is the page containing the list of links. It's title is used for the feed and it's last modified time is
// used for the publication date. Thus, if linked pages change but the page with the links doesn't change, the
// publication date remains unchanged.
Item
// Items are based on the pages linked in list items starting with an asterisk ("*"). Links in
// list items starting with a minus ("-") are ignored!
Items []Item
}
// feed returns a RSS 2.0 feed for any page. The feed items it contains are the pages linked from in list items starting
// with an asterisk ("*").
func feed(p *Page, ti time.Time) *Feed {
feed := new(Feed)
feed.Name = p.Name
feed.Title = p.Title
feed.Date = ti.Format(time.RFC1123Z)
parser, _ := wikiParser()
parser := parser.New()
doc := markdown.Parse(p.Body, parser)
items := make([]Item, 0)
inListItem := false
@@ -64,7 +47,7 @@ func feed(p *Page, ti time.Time) *Feed {
return ast.GoToNext
}
name := path.Join(path.Dir(p.Name), string(link.Destination))
fi, err := os.Stat(filepath.FromSlash(name) + ".md")
fi, err := os.Stat(name + ".md")
if err != nil {
return ast.GoToNext
}

View File

@@ -2,26 +2,22 @@ package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"os"
"testing"
)
func TestFeed(t *testing.T) {
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index.rss", nil),
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/index.rss", nil),
"Welcome to Oddµ")
}
func TestNoFeed(t *testing.T) {
assert.HTTPStatusCode(t,
makeHandler(viewHandler, false), "GET", "/view/no-feed.rss", nil, http.StatusNotFound)
}
// wipes testdata
func TestFeedItems(t *testing.T) {
cleanup(t, "testdata/feed")
_ = os.RemoveAll("testdata")
index.load()
p1 := &Page{Name: "testdata/feed/cactus", Body: []byte(`# Cactus
p1 := &Page{Name: "testdata/cactus", Body: []byte(`# Cactus
Green head and white hair
A bench in the evening sun
Unmoved by the news
@@ -29,7 +25,7 @@ Unmoved by the news
#Succulent`)}
p1.save()
p2 := &Page{Name: "testdata/feed/dragon", Body: []byte(`# Dragon
p2 := &Page{Name: "testdata/dragon", Body: []byte(`# Dragon
My palm tree grows straight
Up and up to touch the sky
Ignoring the roof
@@ -37,19 +33,23 @@ Ignoring the roof
#Palmtree`)}
p2.save()
p3 := &Page{Name: "testdata/feed/plants", Body: []byte(`# Plants
p3 := &Page{Name: "testdata/plants", Body: []byte(`# Plants
Writing poems about plants.
* [My Cactus](cactus)
* [My Dragon Tree](dragon)`)}
p3.save()
body := assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/feed/plants.rss", nil)
body := assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/plants.rss", nil)
assert.Contains(t, body, "<title>Plants</title>")
assert.Contains(t, body, "<title>Cactus</title>")
assert.Contains(t, body, "<title>Dragon</title>")
assert.Contains(t, body, "&lt;h1 id=&#34;cactus&#34;&gt;Cactus&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1 id=&#34;dragon&#34;&gt;Dragon&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1&gt;Cactus&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1&gt;Dragon&lt;/h1&gt;")
assert.Contains(t, body, "<category>Succulent</category>")
assert.Contains(t, body, "<category>Palmtree</category>")
t.Cleanup(func() {
_ = os.RemoveAll("testdata")
})
}

35
go.mod
View File

@@ -1,40 +1,29 @@
module alexschroeder.ch/cgit/oddmu
go 1.22
toolchain go1.22.3
go 1.21.0
require (
github.com/disintegration/imaging v1.6.2
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
github.com/fsnotify/fsnotify v1.7.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gomarkdown/markdown v0.0.0-20240930133403-7e0a027d98c5
github.com/anthonynsimon/bild v0.13.0
github.com/dgryski/go-trigram v0.0.0-20160407183937-79ec494e1ad0
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538
github.com/google/subcommands v1.2.0
github.com/hexops/gotextdiff v1.0.3
github.com/microcosm-cc/bluemonday v1.0.26
github.com/muesli/reflow v0.3.0
github.com/microcosm-cc/bluemonday v1.0.25
github.com/pemistahl/lingua-go v1.4.0
github.com/sergi/go-diff v1.3.1
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.7.1 // indirect
github.com/gen2brain/heic v0.3.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/tetratelabs/wazero v1.7.3 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.21.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 // indirect
golang.org/x/net v0.12.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

101
go.sum
View File

@@ -1,84 +1,77 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gen2brain/heic v0.0.0-20230113233934-ca402e77a786 h1:zvgtcRb2B5gynWjm+Fc9oJZPHXwmcgyH0xCcNm6Rmo4=
github.com/gen2brain/heic v0.0.0-20230113233934-ca402e77a786/go.mod h1:aKVJoQ0cc9K5Xb058XSnnAxXLliR97qbSqWBlm5ca1E=
github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs=
github.com/gen2brain/heic v0.3.1/go.mod h1:m2sVIf02O7wfO8mJm+PvE91lnq4QYJy2hseUon7So10=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 h1:ZPy+2XJ8u0bB3sNFi+I72gMEMS7MTg7aZCCXPOjV8iw=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20240930133403-7e0a027d98c5 h1:qIhG9h8tUzKsVHn0iHtWUohq7Ve7btgA8rGp7TvrIHw=
github.com/gomarkdown/markdown v0.0.0-20240930133403-7e0a027d98c5/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538 h1:ePDpFu7l0QUV46/9A7icfL2wvIOzTJLCWh4RO2NECzE=
github.com/gomarkdown/markdown v0.0.0-20230912175223-14b07df9d538/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/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,59 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"io"
"os"
"sort"
)
type hashtagsCmd struct {
}
func (cmd *hashtagsCmd) SetFlags(f *flag.FlagSet) {
}
func (*hashtagsCmd) Name() string { return "hashtags" }
func (*hashtagsCmd) Synopsis() string { return "hashtag overview" }
func (*hashtagsCmd) Usage() string {
return `hashtags:
Count the use of all hashtags and list them, separated by a tabulator.
`
}
func (cmd *hashtagsCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return hashtagsCli(os.Stdout)
}
// hashtagsCli runs the hashtags command on the command line. It is used
// here with an io.Writer for easy testing.
func hashtagsCli(w io.Writer) subcommands.ExitStatus {
index.load()
index.RLock()
defer index.RUnlock()
type hashtag struct {
label string
count int
}
hashtags := []hashtag{}
for token, docids := range index.token {
hashtags = append(hashtags, hashtag{label: token, count: len(docids)})
}
sort.Slice(hashtags, func(i, j int) bool {
return hashtags[i].count > hashtags[j].count
})
fmt.Fprintln(w, "Rank\tHashtag\tCount")
for i, hashtag := range hashtags {
fmt.Fprintf(w, "%d\t%s\t%d\n", i+1, hashtag.label, hashtag.count)
}
return subcommands.ExitSuccess
}

View File

@@ -1,16 +0,0 @@
package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"testing"
)
func TestHashtagsCmd(t *testing.T) {
b := new(bytes.Buffer)
s := hashtagsCli(b)
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "#like_this\t")
}

View File

@@ -4,8 +4,10 @@ import (
"regexp"
)
// highlight matches for the regular expression using the bold tag.
func highlight(re *regexp.Regexp, s string) string {
// 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 {
s = re.ReplaceAllString(s, "<b>$1</b>")
return s
}

View File

@@ -16,7 +16,7 @@ No birds to be heard.`
q := "window"
re, _ := re(q)
r := highlight(re, s)
r := highlight(q, 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(re, s)
r := highlight(q, re, s)
if r != h {
t.Logf("The highlighting is wrong in 「%s」", r)
t.Fail()

View File

@@ -14,16 +14,15 @@ type htmlCmd struct {
}
func (*htmlCmd) Name() string { return "html" }
func (*htmlCmd) Synopsis() string { return "render a page as HTML" }
func (*htmlCmd) Synopsis() string { return "Render a page as HTML." }
func (*htmlCmd) Usage() string {
return `html [-view] <page name> ...:
Render one or more pages as HTML.
Use a single - to read Markdown from stdin.
return `html [-view] <page name>:
Render a page as HTML.
`
}
func (cmd *htmlCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&cmd.useTemplate, "view", false, "use the 'view.html' template.")
f.BoolVar(&cmd.useTemplate, "view", false, "Use the 'view.html' template.")
}
func (cmd *htmlCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
@@ -31,44 +30,27 @@ func (cmd *htmlCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
}
func htmlCli(w io.Writer, useTemplate bool, args []string) subcommands.ExitStatus {
if len(args) == 1 && args[0] == "-" {
body, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(w, "Cannot read from stdin: %s\n", err)
return subcommands.ExitFailure
}
p := &Page{Name: "stdin", Body: body}
return p.printHtml(w, useTemplate)
}
for _, arg := range args {
for _, arg := range args {
p, err := loadPage(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", arg, err)
fmt.Fprintf(w, "Cannot load %s: %s\n", arg, err)
return subcommands.ExitFailure
}
status := p.printHtml(w, useTemplate)
if status != subcommands.ExitSuccess {
return status
initAccounts()
if useTemplate {
p.handleTitle(true)
p.renderHtml()
t := "view.html"
err := templates.ExecuteTemplate(w, t, p)
if err != nil {
fmt.Fprintf(w, "Cannot execute %s template for %s: %s\n", t, arg, err)
return subcommands.ExitFailure
}
} else {
// do not handle title
p.renderHtml()
fmt.Fprintln(w, p.Html)
}
}
return subcommands.ExitSuccess
}
func (p *Page) printHtml(w io.Writer, useTemplate bool) subcommands.ExitStatus {
if useTemplate {
t := "view.html"
loadTemplates()
p.handleTitle(true)
p.renderHtml()
err := templates.template[t].Execute(w, p)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", t, p.Name, err)
return subcommands.ExitFailure
}
} else {
// do not handle title
p.renderHtml()
fmt.Fprintln(w, p.Html)
}
return subcommands.ExitSuccess
}

View File

@@ -2,23 +2,21 @@ package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"testing"
"github.com/google/subcommands"
)
func TestHtmlCmd(t *testing.T) {
b := new(bytes.Buffer)
s := htmlCli(b, false, []string{"index"})
assert.Equal(t, subcommands.ExitSuccess, s)
r := `<h1 id="welcome-to-oddµ">Welcome to Oddµ</h1>
r := `<h1>Welcome to Oddµ</h1>
<p>Hello! 🙃</p>
<p>Check out the <a href="README">README</a> and <a href="themes">themes</a>.</p>
<p>Or <a href="test">create a new page</a>.</p>
<p>Check out the <a href="README" rel="nofollow">README</a>.</p>
`
assert.Equal(t, b.String(), r)
assert.Equal(t, r, b.String())
}

307
index.go
View File

@@ -4,12 +4,10 @@
package main
import (
"golang.org/x/exp/constraints"
"html/template"
import(
"io/fs"
"log"
"path/filepath"
"log"
"sort"
"strings"
"sync"
@@ -17,17 +15,9 @@ import (
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 {
// Index contains the two maps used for search. Make sure to lock and
// unlock as appropriate.
type Index struct {
sync.RWMutex
// next_id is the number of the next document added to the index
@@ -41,36 +31,27 @@ 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
var index Index
func init() {
index.reset()
// reset resets the Index. This assumes that the index is locked!
func (idx *Index) reset() {
idx.token = nil
idx.documents = nil
idx.titles = nil
}
// reset the index. This assumes that the index is locked. It's useful for tests.
func (idx *indexStore) reset() {
idx.next_id = 0
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!
func (idx *indexStore) addDocument(text []byte) docid {
id := idx.next_id
idx.next_id++
for _, token := range hashtags(text) {
// addDocument adds the text as a new document. This assumes that the
// index is locked!
func (idx *Index) addDocument(text string) docid {
id := idx.next_id; idx.next_id++
for _, token := range tokens(text) {
ids := idx.token[token]
// Don't add same ID more than once. Checking the last
// position of the []docid works because the id is
// always a new one, i.e. the last one, if at all.
if len(ids) > 0 && ids[len(ids)-1] == id {
if ids != nil && ids[len(ids)-1] == id {
continue
}
idx.token[token] = append(ids, id)
@@ -78,161 +59,177 @@ func (idx *indexStore) addDocument(text []byte) docid {
return id
}
// deleteDocument deletes all references to the id. The id can no longer be used. This assumes that the index is locked.
func (idx *indexStore) deleteDocument(id docid) {
// Looping through all tokens makes sense if there are few tokens (like hashtags). It doesn't make sense if the
// number of tokens is large (like for full-text search or a trigram index).
for token, ids := range idx.token {
// If the token appears only in this document, remove the whole entry.
// deleteDocument deletes the text as a new document. The id can no
// longer be used. This assumes that the index is locked!
func (idx *Index) deleteDocument(text string, id docid) {
for _, token := range tokens(text) {
ids := index.token[token]
// Tokens can appear multiple times in a text but they
// can only be deleted once. deleted.
if ids == nil {
continue
}
// If the token appears only in this document, remove
// the whole entry.
if len(ids) == 1 && ids[0] == id {
delete(idx.token, token)
delete(index.token, token)
continue
}
// Otherwise, remove the token from the index.
i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
if i != -1 && i < len(ids) && ids[i] == id {
copy(ids[i:], ids[i+1:])
idx.token[token] = ids[:len(ids)-1]
index.token[token] = ids[:len(ids)-1]
continue
}
// If none of the above, then our docid wasn't
// indexed. This shouldn't happen, either.
log.Printf("The index for token %s does not contain doc id %d", token, id)
}
delete(index.documents, id)
}
// deletePageName determines the document id based on the page name and calls deleteDocument to delete all references.
// This assumes that the index is unlocked.
func (idx *indexStore) deletePageName(name string) {
idx.Lock()
defer idx.Unlock()
var id docid
// Reverse lookup! At least it's in memory.
for key, value := range idx.documents {
if value == name {
id = key
break
}
}
if id != 0 {
idx.deleteDocument(id)
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.
func (idx *indexStore) remove(p *Page) {
idx.deletePageName(p.Name)
}
// load loads all the pages and indexes them. This takes a while. It returns the number of pages indexed.
func (idx *indexStore) load() (int, error) {
idx.Lock()
defer idx.Unlock()
err := filepath.Walk(".", idx.walk)
// add reads a file and adds it to the index. This must happen while
// the idx is locked.
func (idx *Index) add(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
filename := path
if info.IsDir() || strings.HasPrefix(filename, ".") || !strings.HasSuffix(filename, ".md") {
return nil
}
name := strings.TrimSuffix(filename, ".md")
p, err := loadPage(name)
if err != nil {
return err
}
p.handleTitle(false)
id := idx.addDocument(string(p.Body))
idx.documents[id] = p.Name
idx.titles[p.Name] = p.Title
return nil
}
// load loads all the pages and indexes them. This takes a while.
// It returns the number of pages indexed.
func (idx *Index) load() (int, error) {
idx.Lock()
defer idx.Unlock()
idx.token = make(map[string][]docid)
idx.documents = make(map[docid]string)
idx.titles = make(map[string]string)
err := filepath.Walk(".", idx.add)
if err != nil {
idx.reset()
return 0, err
}
n := len(idx.documents)
return n, nil
}
// walk reads a file and adds it to the index. This assumes that the index is locked.
func (idx *indexStore) walk(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// skip hidden directories and files
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
if info.IsDir() {
return filepath.SkipDir
} else {
return nil
}
}
// skipp all but page files
if !strings.HasSuffix(path, ".md") {
return nil
}
p, err := loadPage(strings.TrimSuffix(path, ".md"))
if err != nil {
return err
}
p.handleTitle(false)
idx.addPage(p)
return nil
}
// addPage adds a page to the index. This assumes that the index is locked.
func (idx *indexStore) addPage(p *Page) {
id := idx.addDocument(p.Body)
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.
func (idx *indexStore) add(p *Page) {
idx.Lock()
defer idx.Unlock()
idx.addPage(p)
}
// dump prints the index to the log for debugging.
func (idx *indexStore) dump() {
idx.RLock()
defer idx.RUnlock()
// dump prints the index to the log for debugging. Must already be readlocked.
func (idx *Index) dump() {
index.RLock()
defer index.RUnlock()
for token, ids := range idx.token {
log.Printf("%s: %v", token, ids)
}
}
}
// updateIndex updates the index for a single page.
func (idx *indexStore) update(p *Page) {
idx.remove(p)
idx.add(p)
}
// search searches the index for a query string and returns page
// names.
func (idx *indexStore) search(q string) []string {
idx.RLock()
defer idx.RUnlock()
names := make([]string, 0)
hashtags := hashtags([]byte(q))
if len(hashtags) > 0 {
var r []docid
for _, token := range hashtags {
if ids, ok := idx.token[token]; ok {
if r == nil {
r = ids
} else {
r = intersection(r, ids)
}
} else {
// Token doesn't exist therefore abort search.
return nil
}
}
for _, id := range r {
names = append(names, idx.documents[id])
// updateIndex updates the index for a single page. The old text is
// loaded from the disk and removed from the index first, if it
// exists.
func (p *Page) updateIndex() {
index.Lock()
defer index.Unlock()
var id docid
// Reverse lookup! At least it's in memory.
for docId, name := range index.documents {
if name == p.Name {
id = docId
break
}
}
if id == 0 {
id = index.addDocument(string(p.Body))
index.documents[id] = p.Name
index.titles[p.Name] = p.Title
} else {
for _, name := range idx.documents {
names = append(names, name)
if o, err := loadPage(p.Name); err == nil {
index.deleteDocument(string(o.Body), id)
}
// Do not reuse the old id. We need a new one for
// indexing to work.
id = index.addDocument(string(p.Body))
index.documents[id] = p.Name
p.handleTitle(false)
// The page name stays the same but the title may have
// changed.
index.titles[p.Name] = p.Title
}
}
// removeFromIndex removes the page from the index. Do this when
// deleting a page.
func (p *Page) removeFromIndex() {
index.Lock()
defer index.Unlock()
var id docid
// Reverse lookup! At least it's in memory.
for docId, name := range index.documents {
if name == p.Name {
id = docId
break
}
}
if id == 0 {
log.Printf("Page %s is not indexed", p.Name)
return
}
o, err := loadPage(p.Name)
if err != nil {
log.Printf("Page %s cannot removed from the index: %s", p.Name, err)
return
}
index.deleteDocument(string(o.Body), id)
}
// searchDocuments searches the index for a query string and returns
// page names.
func (idx *Index) search(q string) []string {
index.RLock()
defer index.RUnlock()
var r []docid
for _, token := range tokens(q) {
if ids, ok := idx.token[token]; ok {
if r == nil {
r = ids
} else {
r = intersection(r, ids)
}
} else {
// Token doesn't exist therefore abort search.
return nil
}
}
names := make([]string, 0)
for _, id := range r {
names = append(names, idx.documents[id])
}
return names
}
// intersection returns the set intersection between a and b.
// a and b have to be sorted in ascending order and contain no duplicates.
func intersection[T constraints.Ordered](a []T, b []T) []T {
func intersection(a []docid, b []docid) []docid {
maxLen := len(a)
if len(b) > maxLen {
maxLen = len(b)
}
r := make([]T, 0, maxLen)
r := make([]docid, 0, maxLen)
var i, j int
for i < len(a) && j < len(b) {
if a[i] < b[j] {

View File

@@ -2,6 +2,4 @@
Hello! 🙃
Check out the [[README]] and [[themes]].
Or [create a new page](test).
Check out the [README](README).

View File

@@ -2,27 +2,16 @@ package main
import (
"github.com/stretchr/testify/assert"
"os"
"strings"
"testing"
)
func TestIndexAdd(t *testing.T) {
idx := &indexStore{}
idx.reset()
idx.Lock()
defer idx.Unlock()
tag := "#hello"
id := idx.addDocument([]byte("oh hi " + tag))
assert.Contains(t, idx.token, tag)
idx.deleteDocument(id)
assert.NotContains(t, idx.token, tag)
}
// TestIndex relies on README.md being indexed
func TestIndex(t *testing.T) {
index.load()
q := "Oddµ"
pages, _ := search(q, "", "", 1, false)
pages, _, _ := search(q, 1)
assert.NotZero(t, len(pages))
for _, p := range pages {
assert.NotContains(t, p.Title, "<b>")
@@ -34,19 +23,20 @@ func TestIndex(t *testing.T) {
func TestSearchHashtag(t *testing.T) {
index.load()
q := "#like_this"
pages, _ := search(q, "", "", 1, false)
pages, _, _ := search(q, 1)
assert.NotZero(t, len(pages))
}
// wipes testdata
func TestIndexUpdates(t *testing.T) {
cleanup(t, "testdata/update")
name := "testdata/update/test"
_ = os.RemoveAll("testdata")
name := "testdata/test"
index.load()
p := &Page{Name: name, Body: []byte("#Old Name\nThis is a test.")}
p := &Page{Name: name, Body: []byte("This is a test.")}
p.save()
// Find the phrase
pages, _ := search("This is a test", "", "", 1, false)
pages, _, _ := search("This is a test", 1)
found := false
for _, p := range pages {
if p.Name == name {
@@ -57,7 +47,7 @@ func TestIndexUpdates(t *testing.T) {
assert.True(t, found)
// Find the phrase, case insensitive
pages, _ = search("this is a test", "", "", 1, false)
pages, _, _ = search("this is a test", 1)
found = false
for _, p := range pages {
if p.Name == name {
@@ -68,7 +58,7 @@ func TestIndexUpdates(t *testing.T) {
assert.True(t, found)
// Find some words
pages, _ = search("this test", "", "", 1, false)
pages, _, _ = search("this test", 1)
found = false
for _, p := range pages {
if p.Name == name {
@@ -79,9 +69,9 @@ func TestIndexUpdates(t *testing.T) {
assert.True(t, found)
// Update the page and no longer find it with the old phrase
p = &Page{Name: name, Body: []byte("# New page\nGuvf vf n grfg.")}
p = &Page{Name: name, Body: []byte("Guvf vf n grfg.")}
p.save()
pages, _ = search("This is a test", "", "", 1, false)
pages, _, _ = search("This is a test", 1)
found = false
for _, p := range pages {
if p.Name == name {
@@ -92,7 +82,7 @@ func TestIndexUpdates(t *testing.T) {
assert.False(t, found)
// Find page using a new word
pages, _ = search("Guvf", "", "", 1, false)
pages, _, _ = search("Guvf", 1)
found = false
for _, p := range pages {
if p.Name == name {
@@ -102,8 +92,7 @@ func TestIndexUpdates(t *testing.T) {
}
assert.True(t, found)
// Make sure the title was updated
index.RLock()
defer index.RUnlock()
assert.Equal(t, "New page", index.titles[name])
t.Cleanup(func() {
_ = os.RemoveAll("testdata")
})
}

View File

@@ -7,7 +7,8 @@ import (
"strings"
)
// getLanguages returns the environment variable ODDMU_LANGUAGES or all languages.
// getLangauges returns the environment variable ODDMU_LANGUAGES or
// all languages.
func getLanguages() ([]lingua.Language, error) {
v := os.Getenv("ODDMU_LANGUAGES")
if v == "" {
@@ -28,9 +29,8 @@ func getLanguages() ([]lingua.Language, error) {
// detector is the LanguageDetector initialized at startup by loadLanguages.
var detector lingua.LanguageDetector
// loadLanguages initializes the detector using the languages returned by getLanguages and returns the number of
// languages loaded. If this is skipped, no language detection happens and the templates cannot use {{.Language}} to use
// this. Usually this is used for correct hyphenation by the browser.
// loadLanguages initializes the detector using the languages returned
// by getLanguages and returns the number of languages loaded.
func loadLanguages() int {
langs, err := getLanguages()
if err == nil {
@@ -56,9 +56,3 @@ func language(s string) string {
}
return ""
}
// Language returns the language used for the page, as a lower case
// ISO 639-1 string, e.g. "en" or "de".
func (p *Page) Language() string {
return language(p.plainText())
}

View File

@@ -1,56 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"io"
"os"
)
type linksCmd struct {
}
func (cmd *linksCmd) SetFlags(f *flag.FlagSet) {
}
func (*linksCmd) Name() string { return "links" }
func (*linksCmd) Synopsis() string { return "list outgoing links for a page" }
func (*linksCmd) Usage() string {
return `links <page name> ...:
Lists all the links on a page. Use a single - to read Markdown from stdin.
`
}
func (cmd *linksCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return linksCli(os.Stdout, f.Args())
}
// linksCli runs the links command on the command line. It is used
// here with an io.Writer for easy testing.
func linksCli(w io.Writer, args []string) subcommands.ExitStatus {
if len(args) == 1 && args[0] == "-" {
body, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(w, "Cannot read from stdin: %s\n", err)
return subcommands.ExitFailure
}
p := &Page{Body: body}
for _, link := range p.links() {
fmt.Fprintln(w, link)
}
return subcommands.ExitSuccess
}
for _, name := range args {
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(w, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
for _, link := range p.links() {
fmt.Fprintln(w, link)
}
}
return subcommands.ExitSuccess
}

View File

@@ -1,16 +0,0 @@
package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"testing"
)
func TestLinksCmd(t *testing.T) {
b := new(bytes.Buffer)
s := linksCli(b, []string{"README"})
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "https://alexschroeder.ch/view/oddmu/oddmu.1\n")
}

102
list.go
View File

@@ -1,102 +0,0 @@
package main
import (
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
// ListItem is used to display the list of files.
type File struct {
Name, Title string
IsDir, IsUp bool
// Date is the last modification date of the file storing the page. As the pages used by Oddmu are plain
// Markdown files, they don't contain any metadata. Instead, the last modification date of the file is used.
// This makes it work well with changes made to the files outside of Oddmu.
Date string
}
type List struct {
Dir string
Files []File
}
// listHandler uses the "list.html" template to enable file management in a particular directory.
func listHandler(w http.ResponseWriter, r *http.Request, dir string) {
files := []File{}
d := filepath.FromSlash(dir)
if d == "" {
d = "."
} else if !strings.HasSuffix(d, "/") {
http.Redirect(w, r, "/list/"+d+"/", http.StatusFound)
return
} else {
it := File{Name: "..", IsUp: true, IsDir: true }
files = append(files, it)
}
err := filepath.Walk(d, func (path string, fi fs.FileInfo, err error) error {
if err != nil {
return err
}
isDir := false
if fi.IsDir() {
if d == path {
return nil
}
isDir = true
}
name := filepath.ToSlash(path)
base := filepath.Base(name)
title := ""
if !isDir && strings.HasSuffix(name, ".md") {
index.RLock()
defer index.RUnlock()
title = index.titles[name[:len(name)-3]]
}
if isDir {
base += "/"
}
it := File{Name: base, Title: title, Date: fi.ModTime().Format(time.DateTime), IsDir: isDir }
files = append(files, it)
if isDir {
return filepath.SkipDir
}
return nil
})
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
renderTemplate(w, dir, "list", &List{Dir: dir, Files: files})
}
// deleteHandler deletes the named file and then redirects back to the list
func deleteHandler(w http.ResponseWriter, r *http.Request, path string) {
fn := filepath.Clean(filepath.FromSlash(path))
err := os.RemoveAll(fn) // and all its children!
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/list/"+filepath.Dir(fn)+"/", http.StatusFound)
}
// renameHandler renames the named file and then redirects back to the list
func renameHandler(w http.ResponseWriter, r *http.Request, path string) {
fn := filepath.Clean(filepath.FromSlash(path))
target := filepath.Join(filepath.Dir(fn), r.FormValue("name"))
err := os.Rename(fn, target)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/list/"+filepath.Dir(target)+"/", http.StatusFound)
}

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Manage Files</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe }
body { hyphens: auto }
form { width: 100% }
table { border-collapse: collapse }
th:nth-child(3) { max-width: 3ex; overflow: visible }
td form { display: inline }
td { padding-right: 1ch }
td:last-child { padding-right: 0 }
td:first-child { max-width: 30ch; overflow: hidden }
tr:nth-child(odd) { background-color: #eed }
td:first-child, td:last-child { white-space: nowrap }
</style>
</head>
<body lang="en">
<header>
<a href="#main">Skip navigation</a>
<a href="/view/index">Home</a>
<a href="/archive/{{.Dir}}data.zip" accesskey="z">Zip</a>
<a href="/upload/{{.Dir}}?filename=image-1.jpg" accesskey="u">Upload</a>
<form role="search" action="/search/{{.Dir}}" method="GET">
<label for="search">Search:</label>
<input id="search" type="text" spellcheck="false" name="q" accesskey="f" placeholder="term #tag title:term blog:true" required>
<button>Go</button>
</form>
</header>
<main>
<h1>Manage Files</h1>
<form id="manage">
<p><mark>Deletions and renamings take effect immediately and there is no undo!</mark></p>
</form>
<table>
<tr>
<th>Name</th>
<th>Title</th>
<th>Delete</th>
<th>Rename</th>
</tr>{{range .Files}}
<tr>
<td>{{if .IsDir}}<a href="/list/{{$.Dir}}{{.Name}}">{{.Name}}</a>{{else}}<a href="/view/{{$.Dir}}{{.Name}}">{{.Name}}</a>{{end}}</td>
<td>{{.Title}}</td>
<td>{{if .IsUp}}{{else}}<button form="manage" formaction="/delete/{{$.Dir}}{{.Name}}" title="Delete {{.Name}}">🗑</button>{{end}}</td>
<td>{{if .IsUp}}{{else}}
<form action="/rename/{{$.Dir}}{{.Name}}">
<input name="name" placeholder="New name"/>
<button title="Rename {{.Name}}"></button>
</form>{{end}}</td>
</tr>{{end}}
</table>
</main>
</body>
</html>

View File

@@ -1,71 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"io"
"os"
"path/filepath"
"strings"
)
type listCmd struct {
dir string
}
func (cmd *listCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&cmd.dir, "dir", "", "list only pages within this sub-directory")
}
func (*listCmd) Name() string { return "list" }
func (*listCmd) Synopsis() string { return "list pages with name and title" }
func (*listCmd) Usage() string {
return `list [-dir string]:
List all pages with name and title, separated by a tabulator.
`
}
func (cmd *listCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return listCli(os.Stdout, cmd.dir, f.Args())
}
// listCli runs the list command on the command line. It is used
// here with an io.Writer for easy testing.
func listCli(w io.Writer, dir string, args []string) subcommands.ExitStatus {
dir, err := checkDir(dir)
if err != nil {
return subcommands.ExitFailure
}
index.load()
index.RLock()
defer index.RUnlock()
for name, title := range index.titles {
if strings.HasPrefix(name, dir) {
name = strings.Replace(name, dir, "", 1)
fmt.Fprintf(w, "%s\t%s\n", name, title)
}
}
return subcommands.ExitSuccess
}
// checkDir returns an error if the directory doesn't exist. If if exists, it returns a copy ending in a slash suiteable
// for substring matching of page names.
func checkDir(dir string) (string, error) {
if dir != "" {
fi, err := os.Stat(filepath.FromSlash(dir))
if err != nil {
fmt.Println(err)
return "", err
}
if !fi.IsDir() {
fmt.Println("This is not a sub-directory:", dir)
return "", err
}
if !strings.HasSuffix(dir, "/") {
dir += "/"
}
}
return dir, nil
}

View File

@@ -1,31 +0,0 @@
package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"testing"
)
func TestListCmd(t *testing.T) {
b := new(bytes.Buffer)
s := listCli(b, "", nil)
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "README\tOddµ: A minimal wiki\n")
assert.Contains(t, x, "index\tWelcome to Oddµ\n")
}
func TestListSubdirCmd(t *testing.T) {
cleanup(t, "testdata/list")
p := &Page{Name: "testdata/list/red", Body: []byte(`# Red
Shifting darkness waits
I open my eyes in fear
And see the red dot`)}
p.save()
b := new(bytes.Buffer)
s := listCli(b, "testdata/list", nil)
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "red\tRed\n")
}

View File

@@ -1,30 +0,0 @@
package main
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// relies on index.md in the current directory!
func TestListHandler(t *testing.T) {
assert.Contains(t,
assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/", nil),
"index.md")
}
func TestDeleteHandler(t *testing.T) {
cleanup(t, "testdata/delete")
assert.NoError(t, os.Mkdir("testdata/delete", 0755))
p := &Page{Name: "testdata/delete/haiku", Body: []byte(`# Sunset
Walk the fields outside
See the forest loom above
And an orange sky
`)}
p.save()
list := assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/delete/", nil)
assert.Contains(t, list, `<a href="/view/testdata/delete/haiku.md">haiku.md</a>`)
assert.Contains(t, list, `<td>Sunset</td>`)
assert.Contains(t, list, `<button form="manage" formaction="/delete/testdata/delete/haiku.md" title="Delete haiku.md">`)
}

1
man/.gitignore vendored
View File

@@ -1 +0,0 @@
*.md

View File

@@ -1,55 +1,5 @@
TEXT=$(wildcard *.txt)
MAN=$(patsubst %.txt,%,${TEXT})
HTML=$(patsubst %.txt,%.html,${TEXT})
MD=$(patsubst %.txt,%.md,${TEXT})
docs: oddmu.1 oddmu.5 oddmu-templates.5 oddmu-apache.5 oddmu.service.5 oddmu-replace.1 \
oddmu-search.1 oddmu-search.7 oddmu-html.1
help:
@echo Help for Oddmu Documentation
@echo ============================
@echo make man
@echo " regenerate man pages"
@echo make html
@echo " generate HTML pages"
@echo make md
@echo " generate Markdown pages"
@echo make clean
@echo " delete HTML and Markdown pages"
@echo make realclean
@echo " delete HTML, Markdown and man pages"
man: ${MAN}
%: %.txt
oddmu%: oddmu%.txt
scdoc < $< > $@
html: ${HTML}
%.html: %.md
@echo Making $@
@echo '<!DOCTYPE html>' > $@
@oddmu html $(basename $<) | sed --regexp-extended \
-e 's/<a href="(oddmu[a-z.-]*.[1-9])">([^<>]*)<\/a>/<a href="\1.html">\2<\/a>/g' >> $@
md: ${MD}
%.md: %.txt
@echo Making $@
@perl scdoc-to-markdown < $< > $@
README.md: ../README.md
@echo Making $@
@sed --regexp-extended \
-e 's/\]\(.*\/(.*)\.txt\)/](\1)/' \
< $< > $@
upload: ${MD} README.md
rsync --itemize-changes --archive *.md sibirocobombus:alexschroeder.ch/wiki/oddmu/
make clean
clean:
@echo Removing HTML and Markdown files
@rm --force ${HTML} ${MD} README.md
realclean: clean
@echo Removing man pages
@rm --force ${MAN}

View File

@@ -1,77 +1,67 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2024-09-25"
.TH "ODDMU-APACHE" "5" "2023-09-18"
.PP
.SH NAME
.PP
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
.PP
.SH DESCRIPTION
.SS 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.\&
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
as shown later.\&
The oddmu program serves the current working directory as a wiki on
port 8080.\& This is an unpriviledged port so an ordinary use account
can do this.\&
.PP
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.\&
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
.SH CONFIGURATION
.SS 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
use "mod_md" to manage your domain.\&
HTTPS is not part of the wiki.\& You probably want to configure this in
your webserver.\& I guess you could use stunnel, too.\& If you'\&re using
Apache, you can use "mod_md" to manage your domain.\&
.PP
The examples below use the domain "transjovian.\&org" and the Apache installation
is the one that comes with Debian.\&
.PP
The site itself is configured in a file called
"/etc/apache2/sites-available/transjovian.\&conf" and a link points there from
"/etc/apache2/sites-enabled".\& Create this link using \fIa2ensite\fR(1).\&
In the example below, the site is configured in a file called
"/etc/apache2/sites-available/500-transjovian.\&conf" and a link poins
there from "/etc/apache2/sites-enabled".\& Create this link using
\fIa2ensite\fR(1).\&
.PP
.nf
.RS 4
MDomain transjovian\&.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:80>
ServerName transjovian\&.org
Redirect "/" "https://transjovian\&.org/"
ServerName transjovian\&.org
RewriteEngine on
RewriteRule ^/(\&.*) https://%{HTTP_HOST}/$1 [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
ServerAdmin alex@alexschroeder\&.ch
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch ^/(search|(view|edit|save|add|append|upload|drop)/(\&.*))?$ http://localhost:8080/$1
</VirtualHost>
.fi
.RE
.PP
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 proxies the requests for Oddmu to port 8080.\& Importantly, it
doesn'\&t send \fIall\fR the requests to Oddmu.\& This allows us to still host static
files using the web server (see \fBServe static files\fR).\&
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 proxies the requests for the wiki to
port 8080.\&
.PP
This is what happens:
Thus, this is what happens:
.PP
.PD 0
.IP \(bu 4
The user tells the browser to visit "transjovian.\&org"
.IP \(bu 4
The browser sends a request for "http://transjovian.\&org" (on port 80)
.IP \(bu 4
Apache redirects this to "https://transjovian.\&org/" by default (now on port 443)
.IP \(bu 4
This is proxied to "http://transjovian.\&org:8080/" (now on port 8080)
.PD
\fB The user tells the browser to visit `transjovian.\&org`
\fR The browser sends a request for `http://transjovian.\&org` (on port 80)
\fB Apache redirects this to `https://transjovian.\&org/` by default (now on port 443)
\fR This is proxied to `http://transjovian.\&org:8080/` (no encryption, on port 8080)
.PP
Restart the server, gracefully:
.PP
@@ -81,133 +71,18 @@ apachectl graceful
.fi
.RE
.PP
In a situation where Apache acts as a reverse proxy, you can prevent some
actions from being proxied.\& If you don'\&t want to allow strangers to make
changes, search or archive the site, use a limited setup like the following:
.PP
.nf
.RS 4
MDomain transjovian\&.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:80>
ServerName transjovian\&.org
Redirect "/" "https://transjovian\&.org/"
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/(view/\&.*)?$" "http://localhost:8080/$1"
</VirtualHost>
.fi
.RE
.PP
You'\&ll need to edit the source pages some other way.\& Edit them locally and
upload them using rsync; edit them remotely using an editor that can do this;
use SSHFS to mount the remote directory locally for editing; use \fIstunnel\fR(8) to
access the remote wiki on the local port 8080 for editing.\& There are probably a
lot more such options available.\& All of them have the drawback that they'\&re
probably not easy to use when on a mobile phone.\&
.PP
.SS Allow HTTP for viewing
.PP
When looking at pages, you might want to allow HTTP since no password is
required.\& Therefore, proxy the read-only requests from the virtual host on port
80 to the wiki instead of redirecting them to port 443.\&
.PP
.nf
.RS 4
MDomain transjovian\&.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:80>
ServerName transjovian\&.org
ProxyPassMatch "^/((view|diff|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
RedirectMatch "^/((edit|save|add|append|upload|drop|list|delete|rename)/(\&.*))?$"
"https://transjovian\&.org/$1"
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
.RE
.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(5)\fR.\&
.PP
On the Apache side, you can proxy to the socket directly.\& This sends all
requests to the socket:
.PP
.nf
.RS 4
ProxyPass "/" "unix:/run/oddmu/oddmu\&.sock|http://localhost/"
.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.\& Make sure to provide
the correct servername!\&
.PP
.nf
.RS 4
curl http://transjovian\&.org/view/index
.fi
.RE
.PP
You probably want to serve some static files as well (see \fBServe static files\fR).\&
In that case, you need to use the ProxyPassMatch directive.\&
.PP
.nf
.RS 4
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))?$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
.PP
There'\&s a curious problem with this expression, however.\& If you use \fIcurl(1)\fR to
get the root path, Apache hangs:
.PP
.nf
.RS 4
curl http://transjovian\&.org/
.fi
.RE
.PP
A workaround is to add the redirect manually and drop the question-mark:
.PP
.nf
.RS 4
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
.PP
If you know why this is happening, let me know.\&
To serve both HTTP and HTTPS, don'\&t redirect from the first virtual
host to the second instead just proxy to the wiki like you did for
the second virtual host: use a copy of the "ProxyPassMatch" directive
instead of "RewriteEngine on" and "RewriteRule".\&
.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.\&
Access control is not part of the wiki.\& 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.\&
.PP
The following 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
Create a new password file called ".\&htpasswd" and add the user "alex".\& The "-c"
flag creates the file.\&
Create a new password file called ".\&htpasswd" and add the user "alex":
.PP
.nf
.RS 4
@@ -216,8 +91,9 @@ htpasswd -c \&.htpasswd alex
.fi
.RE
.PP
To add more users, don'\&t use the "-c" option or you will overwrite the existing
file.\& To add another user, use no option at all.\&
To add more users, don'\&t use the "-c" option or you will overwrite it!\&
.PP
To add another user:
.PP
.nf
.RS 4
@@ -225,7 +101,7 @@ htpasswd \&.htpasswd berta
.fi
.RE
.PP
To remove a user, use the "-D" option.\&
To remove a user:
.PP
.nf
.RS 4
@@ -233,48 +109,13 @@ htpasswd -D \&.htpasswd berta
.fi
.RE
.PP
Modify your site configuration and protect the "/edit/", "/save/", "/add/",
"/append/", "/upload/", "/drop/", "/list/", "/delete/" and "/rename/" URLs with
a password by adding the following to your "<VirtualHost *:443>" section:
Modify your site configuration and protect the "/edit/", "/save/",
"/add/", "/append/", "/upload/" and "/drop/" URLs with a password by
adding the following to your "<VirtualHost *:443>" section:
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd
Require valid-user
</LocationMatch>
.fi
.RE
.PP
The way Oddmu handles subdirectories is that all files and directories are
visible, except for "hidden" files and directories (whose name starts with a
period).\& Specifically, do not rely on Apache to hide locations in subdirectories
from public view.\& Search reveals the existence of these pages and produces an
extract, even if users cannot follow the links.\& Archive links pack all the
subdirectories, including locations you may have hidden from view using Apache.\&
.PP
If you to treat subdirectories as separate sites, you need to set the
environment variable ODDMU_FILTER to a regular expression matching the those
directories.\& If search starts in a directory that doesn'\&t match the regular
expression, all directories matching the regular expression are excluded.\& See
\fIoddmu-filter\fR(7).\&
.PP
In the following example, ODDMU_FILTER is set to "^secret/".\&
.PP
"http://transjovian.\&org/search/index?\&q=something" does not search the "secret/"
directory and its subdirectories are excluded.\&
.PP
"http://transjovian.\&org/search/secret/index?\&q=something" searches just the
"secret" directory and its subdirectories.\&
.PP
You need to configure the web server to prevent access to the "secret/"
directory:
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename|(view|preview|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd
@@ -285,24 +126,24 @@ directory:
.PP
.SS Serve static files
.PP
If you want to serve static files as well, add a document root to your webserver
configuration.\& In this case, the document root is the directory where all the
data files are.\& Apache does not serve files such as ".\&htpasswd".\&
If you want to serve static files as well, add a document root to your
webserver configuration.\& In this case, the document root is the
directory where all the data files are.\& Apache will not serve files
such as ".\&htpasswd".\&
.PP
.nf
.RS 4
DocumentRoot /home/oddmu
<Directory /home/oddmu>
Require all granted
Require all granted
</Directory>
.fi
.RE
.PP
Make sure that none of the subdirectories look like the wiki paths "/view/",
"/diff/", "/edit/", "/save/", "/add/", "/append/", "/upload/", "/drop/",
"/list", "/delete/", "/rename/" "/search/" or "/archive/".\& For example, create a
file called "robots.\&txt" containing the following, telling all robots that
they'\&re not welcome.\&
Make sure that none of the subdirectories look like the wiki paths
"/view/", "/edit/", "/save/", "/add/", "/append/", "/upload/",
"/drop/" or "/search".\& For example, create a file called "robots.\&txt"
containing the following, telling all robots that they'\&re not welcome.\&
.PP
.nf
.RS 4
@@ -311,46 +152,22 @@ Disallow: /
.fi
.RE
.PP
Your site now serves "/robots.\&txt" without interfering with the wiki, and
without needing a wiki page.\&
.PP
Another option would be to create a CSS file and use it with a <link> element in
all the templates instead of relying on the <style> element.\&
.PP
The "view.\&html" template would start as follows:
.PP
.nf
.RS 4
<!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>{{\&.Title}}</title>
<link href="/css/oddmu-2023\&.css" rel="stylesheet" />
<link rel="alternate" type="application/rss+xml" title="Alex Schroeder: {{\&.Title}}" href="/view/{{\&.Name}}\&.rss" />
</head>
.fi
.RE
.PP
In this case, "/css/oddmu-2023.\&css" would be the name of your stylesheet.\& If
your document root is "/home/oddmu", then the filename of your stylesheet would
have to be "/home/oddmu/css/oddmu-2023.\&css" for this to work.\&
You site now serves "/robots.\&txt" without interfering with the wiki,
and without needing a wiki page.\&
.PP
.SS Different logins for different access rights
.PP
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.\&
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.\&
.PP
This requires a valid login by the user "alex" or "berta":
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/intetebi/">
<LocationMatch "^/(edit|save|add|append|upload|drop)/intetebi/">
Require user alex berta
</LocationMatch>
.fi
@@ -358,8 +175,9 @@ This requires a valid login by the user "alex" or "berta":
.PP
.SS Private wikis
.PP
Based on the above, you can prevent people from \fIreading\fR the wiki.\& The location
must cover all the URLs in order to protect everything.\&
Based on the above, you can prevent people from \fIreading\fR the wiki.\&
The "LocationMatch" must cover the "/view/" URLs in order to protect
\fBeverything\fR.\&
.PP
.nf
.RS 4
@@ -374,15 +192,16 @@ must cover all the URLs in order to protect everything.\&
.PP
.SS Virtual hosting
.PP
Virtual hosting in this context means that the program serves two different
sites for two different domains from the same machine.\& Oddmu doesn'\&t support
that, but your webserver does.\& Therefore, start an Oddmu instance for every
domain name, each listening on a different port.\& Then set up your web server
such that ever domain acts as a reverse proxy to a different Oddmu instance.\&
Virtual hosting in this context means that the program serves two
different sites for two different domains from the same machine.\& Oddmu
doesn'\&t support that, but your webserver does.\& Therefore, start an
Oddmu instance for every domain name, each listening on a different
port.\& Then set up your web server such that ever domain acts as a
reverse proxy to a different Oddmu instance.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-filter\fR(7), \fIoddmu-nginx\fR(5)
\fIoddmu\fR(1)
.PP
"Apache Core Features".\&
https://httpd.\&apache.\&org/docs/current/mod/core.\&html
@@ -396,12 +215,6 @@ https://httpd.\&apache.\&org/docs/current/mod/mod_proxy.\&html
"Robot exclusion standard" on Wikipedia.\&
https://en.\&wikipedia.\&org/wiki/Robot_exclusion_standard
.PP
"<style>: The Style Information element"
https://developer.\&mozilla.\&org/en-US/docs/Web/HTML/Element/style
.PP
"<link>: The External Resource Link element"
https://developer.\&mozilla.\&org/en-US/docs/Web/HTML/Element/link
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -4,59 +4,55 @@ 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.
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
as shown later.
The oddmu program serves the current working directory as a wiki on
port 8080. This is an unpriviledged port so an ordinary use account
can do this.
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.
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
use "mod_md" to manage your domain.
HTTPS is not part of the wiki. You probably want to configure this in
your webserver. I guess you could use stunnel, too. If you're using
Apache, you can use "mod_md" to manage your domain.
The examples below use the domain "transjovian.org" and the Apache installation
is the one that comes with Debian.
The site itself is configured in a file called
"/etc/apache2/sites-available/transjovian.conf" and a link points there from
"/etc/apache2/sites-enabled". Create this link using _a2ensite_(1).
In the example below, the site is configured in a file called
"/etc/apache2/sites-available/500-transjovian.conf" and a link poins
there from "/etc/apache2/sites-enabled". Create this link using
_a2ensite_(1).
```
MDomain transjovian.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder.ch
<VirtualHost *:80>
ServerName transjovian.org
Redirect "/" "https://transjovian.org/"
ServerName transjovian.org
RewriteEngine on
RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
SSLEngine on
ProxyPassMatch ^/(search|(view|edit|save|add|append|upload|drop)/(.*))?$ http://localhost:8080/$1
</VirtualHost>
```
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 proxies the requests for Oddmu to port 8080. Importantly, it
doesn't send _all_ the requests to Oddmu. This allows us to still host static
files using the web server (see *Serve static files*).
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 proxies the requests for the wiki to
port 8080.
This is what happens:
Thus, this is what happens:
- The user tells the browser to visit "transjovian.org"
- The browser sends a request for "http://transjovian.org" (on port 80)
- Apache redirects this to "https://transjovian.org/" by default (now on port 443)
- This is proxied to "http://transjovian.org:8080/" (now on port 8080)
* The user tells the browser to visit `transjovian.org`
* The browser sends a request for `http://transjovian.org` (on port 80)
* Apache redirects this to `https://transjovian.org/` by default (now on port 443)
* This is proxied to `http://transjovian.org:8080/` (no encryption, on port 8080)
Restart the server, gracefully:
@@ -64,177 +60,44 @@ Restart the server, gracefully:
apachectl graceful
```
In a situation where Apache acts as a reverse proxy, you can prevent some
actions from being proxied. If you don't want to allow strangers to make
changes, search or archive the site, use a limited setup like the following:
```
MDomain transjovian.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder.ch
<VirtualHost *:80>
ServerName transjovian.org
Redirect "/" "https://transjovian.org/"
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/(view/.*)?$" "http://localhost:8080/$1"
</VirtualHost>
```
You'll need to edit the source pages some other way. Edit them locally and
upload them using rsync; edit them remotely using an editor that can do this;
use SSHFS to mount the remote directory locally for editing; use _stunnel_(8) to
access the remote wiki on the local port 8080 for editing. There are probably a
lot more such options available. All of them have the drawback that they're
probably not easy to use when on a mobile phone.
## Allow HTTP for viewing
When looking at pages, you might want to allow HTTP since no password is
required. Therefore, proxy the read-only requests from the virtual host on port
80 to the wiki instead of redirecting them to port 443.
```
MDomain transjovian.org
MDCertificateAgreement accepted
ServerAdmin alex@alexschroeder.ch
<VirtualHost *:80>
ServerName transjovian.org
ProxyPassMatch "^/((view|diff|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
RedirectMatch "^/((edit|save|add|append|upload|drop|list|delete|rename)/(.*))?$" \
"https://transjovian.org/$1"
</VirtualHost>
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
## 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 Apache side, you can proxy to the socket directly. This sends all
requests to the socket:
```
ProxyPass "/" "unix:/run/oddmu/oddmu.sock|http://localhost/"
```
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)_. Make sure to provide
the correct servername!
```
curl http://transjovian.org/view/index
```
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|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
There's a curious problem with this expression, however. If you use _curl(1)_ to
get the root path, Apache hangs:
```
curl http://transjovian.org/
```
A workaround is to add the redirect manually and drop the question-mark:
```
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
If you know why this is happening, let me know.
To serve both HTTP and HTTPS, don't redirect from the first virtual
host to the second instead just proxy to the wiki like you did for
the second virtual host: use a copy of the "ProxyPassMatch" directive
instead of "RewriteEngine on" and "RewriteRule".
## 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.
Access control is not part of the wiki. 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.
The following 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.
Create a new password file called ".htpasswd" and add the user "alex". The "-c"
flag creates the file.
Create a new password file called ".htpasswd" and add the user "alex":
```
cd /home/oddmu
htpasswd -c .htpasswd alex
```
To add more users, don't use the "-c" option or you will overwrite the existing
file. To add another user, use no option at all.
To add more users, don't use the "-c" option or you will overwrite it!
To add another user:
```
htpasswd .htpasswd berta
```
To remove a user, use the "-D" option.
To remove a user:
```
htpasswd -D .htpasswd berta
```
Modify your site configuration and protect the "/edit/", "/save/", "/add/",
"/append/", "/upload/", "/drop/", "/list/", "/delete/" and "/rename/" URLs with
a password by adding the following to your "<VirtualHost \*:443>" section:
Modify your site configuration and protect the "/edit/", "/save/",
"/add/", "/append/", "/upload/" and "/drop/" URLs with a password by
adding the following to your "<VirtualHost \*:443>" section:
```
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
Require valid-user
</LocationMatch>
```
The way Oddmu handles subdirectories is that all files and directories are
visible, except for "hidden" files and directories (whose name starts with a
period). Specifically, do not rely on Apache to hide locations in subdirectories
from public view. Search reveals the existence of these pages and produces an
extract, even if users cannot follow the links. Archive links pack all the
subdirectories, including locations you may have hidden from view using Apache.
If you to treat subdirectories as separate sites, you need to set the
environment variable ODDMU_FILTER to a regular expression matching the those
directories. If search starts in a directory that doesn't match the regular
expression, all directories matching the regular expression are excluded. See
_oddmu-filter_(7).
In the following example, ODDMU_FILTER is set to "^secret/".
"http://transjovian.org/search/index?q=something" does not search the "secret/"
directory and its subdirectories are excluded.
"http://transjovian.org/search/secret/index?q=something" searches just the
"secret" directory and its subdirectories.
You need to configure the web server to prevent access to the "secret/"
directory:
```
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename|(view|preview|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
@@ -244,73 +107,52 @@ directory:
## Serve static files
If you want to serve static files as well, add a document root to your webserver
configuration. In this case, the document root is the directory where all the
data files are. Apache does not serve files such as ".htpasswd".
If you want to serve static files as well, add a document root to your
webserver configuration. In this case, the document root is the
directory where all the data files are. Apache will not serve files
such as ".htpasswd".
```
DocumentRoot /home/oddmu
<Directory /home/oddmu>
Require all granted
Require all granted
</Directory>
```
Make sure that none of the subdirectories look like the wiki paths "/view/",
"/diff/", "/edit/", "/save/", "/add/", "/append/", "/upload/", "/drop/",
"/list", "/delete/", "/rename/" "/search/" or "/archive/". For example, create a
file called "robots.txt" containing the following, telling all robots that
they're not welcome.
Make sure that none of the subdirectories look like the wiki paths
"/view/", "/edit/", "/save/", "/add/", "/append/", "/upload/",
"/drop/" or "/search". For example, create a file called "robots.txt"
containing the following, telling all robots that they're not welcome.
```
User-agent: *
Disallow: /
```
Your site now serves "/robots.txt" without interfering with the wiki, and
without needing a wiki page.
Another option would be to create a CSS file and use it with a \<link\> element in
all the templates instead of relying on the \<style\> element.
The "view.html" template would start as follows:
```
<!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>{{.Title}}</title>
<link href="/css/oddmu-2023.css" rel="stylesheet" />
<link rel="alternate" type="application/rss+xml" title="Alex Schroeder: {{.Title}}" href="/view/{{.Name}}.rss" />
</head>
```
In this case, "/css/oddmu-2023.css" would be the name of your stylesheet. If
your document root is "/home/oddmu", then the filename of your stylesheet would
have to be "/home/oddmu/css/oddmu-2023.css" for this to work.
You site now serves "/robots.txt" without interfering with the wiki,
and without needing a wiki page.
## 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.
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":
```
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/intetebi/">
<LocationMatch "^/(edit|save|add|append|upload|drop)/intetebi/">
Require user alex berta
</LocationMatch>
```
## Private wikis
Based on the above, you can prevent people from _reading_ the wiki. The location
must cover all the URLs in order to protect everything.
Based on the above, you can prevent people from _reading_ the wiki.
The "LocationMatch" must cover the "/view/" URLs in order to protect
*everything*.
```
<Location />
@@ -323,15 +165,16 @@ must cover all the URLs in order to protect everything.
## Virtual hosting
Virtual hosting in this context means that the program serves two different
sites for two different domains from the same machine. Oddmu doesn't support
that, but your webserver does. Therefore, start an Oddmu instance for every
domain name, each listening on a different port. Then set up your web server
such that ever domain acts as a reverse proxy to a different Oddmu instance.
Virtual hosting in this context means that the program serves two
different sites for two different domains from the same machine. Oddmu
doesn't support that, but your webserver does. Therefore, start an
Oddmu instance for every domain name, each listening on a different
port. Then set up your web server such that ever domain acts as a
reverse proxy to a different Oddmu instance.
# SEE ALSO
_oddmu_(1), _oddmu-filter_(7), _oddmu-nginx_(5)
_oddmu_(1)
"Apache Core Features".
https://httpd.apache.org/docs/current/mod/core.html
@@ -345,12 +188,6 @@ https://httpd.apache.org/docs/current/mod/mod_proxy.html
"Robot exclusion standard" on Wikipedia.
https://en.wikipedia.org/wiki/Robot_exclusion_standard
"\<style\>: The Style Information element"
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
"\<link\>: The External Resource Link element"
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,79 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-EXPORT" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-export - export all pages into one file
.PP
.SH SYNOPSIS
.PP
\fBoddmu export\fR [\fB-template\fR \fIfilename\fR]
.PP
.SH DESCRIPTION
.PP
The "export" subcommand prints a RSS file containing all the pages to stdout.\&
You probably want to redirect this into a file so that you can upload and import
it somewhere.\&
.PP
Note that this only handles pages (Markdown files).\& All other files (images,
PDFs, whatever else you uploaded) are not part of the feed and has to be
uploaded to the new platform in some other way.\&
.PP
The \fB-template\fR option specifies the template to use.\& If the template filename
ends in \fI.\&xml\fR, \fI.\&html\fR or \fI.\&rss\fR, it is assumed to contain XML and the optional
XML preamble is printed and appropriate escaping rules are used.\&
.PP
.SH FILES
.PP
By default, the export uses the \fB\fRfeed.\&html\fB\fR template in the current directory.\&
.PP
.SH EXAMPLES
.PP
Export all the pages into a big XML file:
.PP
.nf
.RS 4
env ODDMU_LANGUAGES=de,en oddmu export > /tmp/export\&.xml
.fi
.RE
.PP
Alternatively, consider a template file like the following, to generate a JSON
feed.\& The rule to disallow a comma at the end of arrays means that we need to
add an empty tag and an empty item, unfortunately:
.PP
.nf
.RS 4
{
"version": "https://jsonfeed\&.org/version/1\&.1",
"title": "{{\&.Title}}",
"home_page_url": "https://alexschroeder\&.ch",
"others": [],
"items": [{{range \&.Items}}
{
"id": "{{\&.Name}}",
"url": "https://alexschroeder\&.ch/view/{{\&.Name}}",
"title": "{{\&.Title}}",
"content_html": "{{\&.Html}}",
"date_modified": "{{\&.Date}}",
"tags": [{{range \&.Hashtags}}"{{\&.}}",{{end}}""],
"language": "{{\&.Language}}"
},{{end}}
{}
]
}
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-templates\fR(5), \fIoddmu-static\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,68 +0,0 @@
ODDMU-EXPORT(1)
# NAME
oddmu-export - export all pages into one file
# SYNOPSIS
*oddmu export* [*-template* _filename_]
# DESCRIPTION
The "export" subcommand prints a RSS file containing all the pages to stdout.
You probably want to redirect this into a file so that you can upload and import
it somewhere.
Note that this only handles pages (Markdown files). All other files (images,
PDFs, whatever else you uploaded) are not part of the feed and has to be
uploaded to the new platform in some other way.
The *-template* option specifies the template to use. If the template filename
ends in _.xml_, _.html_ or _.rss_, it is assumed to contain XML and the optional
XML preamble is printed and appropriate escaping rules are used.
# FILES
By default, the export uses the **feed.html** template in the current directory.
# EXAMPLES
Export all the pages into a big XML file:
```
env ODDMU_LANGUAGES=de,en oddmu export > /tmp/export.xml
```
Alternatively, consider a template file like the following, to generate a JSON
feed. The rule to disallow a comma at the end of arrays means that we need to
add an empty tag and an empty item, unfortunately:
```
{
"version": "https://jsonfeed.org/version/1.1",
"title": "{{.Title}}",
"home_page_url": "https://alexschroeder.ch",
"others": [],
"items": [{{range .Items}}
{
"id": "{{.Name}}",
"url": "https://alexschroeder.ch/view/{{.Name}}",
"title": "{{.Title}}",
"content_html": "{{.Html}}",
"date_modified": "{{.Date}}",
"tags": [{{range .Hashtags}}"{{.}}",{{end}}""],
"language": "{{.Language}}"
},{{end}}
{}
]
}
```
# SEE ALSO
_oddmu_(1), _oddmu-templates_(5), _oddmu-static_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,58 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-FILTER" "7" "2024-09-30"
.PP
.SH NAME
.PP
oddmu-filter - keeping subdirectories separate
.PP
.SH DESCRIPTION
.PP
There are actions such as searching and archiving that act on multiple pages,
not just a single page.\& These actions walk the directory tree, including all
subdirectories.\& In some cases, this is not desirable.\&
.PP
Sometimes, subdirectories are separate sites, like the sites of other projects
or different people.\& Depending on how you think about it, you might not want to
include those "sites" in searches or archives of the whole site.\&
.PP
Since directory tree actions always start in the directory the visitor is
currently looking at, directory tree actions starting in a "separate site"
automatically act as expected.\& The action is limited to that subdirectory tree.\&
.PP
When visitors look at a page in the "main site", however, directory tree actions
must skip any sub directories that are part of a "separate site".\&
.PP
The way to identify separate sites is via the environment variable ODDMU_FILTER.\&
It'\&s value is a regular expression matching separate sites.\&
.PP
.SH EXAMPLES
.PP
"ODDMU_FILTER=^project/" means that a directory tree action outside the
"project/" directory does not include pages in the "project/" directory.\&
.PP
In other words, http://localhost:8080/search/?\&q=oddmu skips any pages in
"project/".\&
.PP
At the same time, http://localhost:8080/search/project/?\&q=oddmu works like it
always does: search is limited to "project/" and its subdirectories.\&
.PP
.SH SECURITY
.PP
If the subdirectory is a private site, then you need to use ODDMU_FILTER to
exclude it from directory tree actions in the main site, and you need to
configure your web server such that it doesn'\&t allow visitors access to the
directory tree without authentication.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(7), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,51 +0,0 @@
ODDMU-FILTER(7)
# NAME
oddmu-filter - keeping subdirectories separate
# DESCRIPTION
There are actions such as searching and archiving that act on multiple pages,
not just a single page. These actions walk the directory tree, including all
subdirectories. In some cases, this is not desirable.
Sometimes, subdirectories are separate sites, like the sites of other projects
or different people. Depending on how you think about it, you might not want to
include those "sites" in searches or archives of the whole site.
Since directory tree actions always start in the directory the visitor is
currently looking at, directory tree actions starting in a "separate site"
automatically act as expected. The action is limited to that subdirectory tree.
When visitors look at a page in the "main site", however, directory tree actions
must skip any sub directories that are part of a "separate site".
The way to identify separate sites is via the environment variable ODDMU_FILTER.
It's value is a regular expression matching separate sites.
# EXAMPLES
"ODDMU_FILTER=^project/" means that a directory tree action outside the
"project/" directory does not include pages in the "project/" directory.
In other words, http://localhost:8080/search/?q=oddmu skips any pages in
"project/".
At the same time, http://localhost:8080/search/project/?q=oddmu works like it
always does: search is limited to "project/" and its subdirectories.
# SECURITY
If the subdirectory is a private site, then you need to use ODDMU_FILTER to
exclude it from directory tree actions in the main site, and you need to
configure your web server such that it doesn't allow visitors access to the
directory tree without authentication.
# SEE ALSO
_oddmu_(1), _oddmu-search_(7), _oddmu-apache_(5), _oddmu-nginx_(5)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,39 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HASHTAGS" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-hashtags - count the hashtags used
.PP
.SH SYNOPSIS
.PP
\fBoddmu hashtags\fR
.PP
.SH DESCRIPTION
.PP
The "hashtags" subcommand counts all the hashtags used and lists them, separated
by a TAB character.\&
.PP
.SH EXAMPLES
.PP
List the top 10 hashtags.\& This requires 11 lines because of the header line.\&
.PP
.nf
.RS 4
oddmu hashtags | head -n 11
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,30 +0,0 @@
ODDMU-HASHTAGS(1)
# NAME
oddmu-hashtags - count the hashtags used
# SYNOPSIS
*oddmu hashtags*
# DESCRIPTION
The "hashtags" subcommand counts all the hashtags used and lists them, separated
by a TAB character.
# EXAMPLES
List the top 10 hashtags. This requires 11 lines because of the header line.
```
oddmu hashtags | head -n 11
```
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,15 +1,15 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HTML" "1" "2024-08-29"
.TH "ODDMU-HTML" "1" "2023-09-22"
.PP
.SH NAME
.PP
oddmu-html - render Oddmu page HTML
oddmu-html - render Oddmu page HTML from the command-line
.PP
.SH SYNOPSIS
.PP
@@ -17,42 +17,32 @@ oddmu-html - render Oddmu page HTML
.PP
.SH DESCRIPTION
.PP
The "html" subcommand opens the Markdown file for the given page name (appending
the ".\&md" extension) and prints the HTML to STDOUT without invoking the
"view.\&html" template.\& Use "-" as the page name if you want to read Markdown from
\fBstdin\fR.\&
The "html" subcommand opens the Markdown file for the given page name
(appending the ".\&md" extension) and prints the HTML to STDOUT without
invoking the "view.\&html" template.\&
.PP
.SH OPTIONS
.PP
\fB-view\fR
.RS 4
Use the "view.\&html" template to render the page.\& Without this, the HTML
lacks html and body tags.\&
Use the "view.\&html" template to render the page.\& Without this,
the HTML will lack html and body tags.\&
.PP
.RE
.SH EXAMPLES
.SH EXAMPLE
.PP
Generate "README.\&html" from "README.\&md":
Generate the HTML for "README.\&md":
.PP
.nf
.RS 4
oddmu html README > README\&.html
oddmu html README
.fi
.RE
.PP
Alternatively:
.PP
.nf
.RS 4
oddmu html - < README\&.md > README\&.html
.fi
.RE
.PP
.PP
.SH ENVIRONMENT
.PP
The ODDMU_WEBFINGER environment variable has no effect in this situation.\&
Fediverse accounts are not linked to their profile pages.\&
The ODDMU_WEBFINGER environment variable has no effect in this
situation.\& Fediverse accounts are not linked to their profile pages.\&
.PP
.SH SEE ALSO
.PP

View File

@@ -2,7 +2,7 @@ ODDMU-HTML(1)
# NAME
oddmu-html - render Oddmu page HTML
oddmu-html - render Oddmu page HTML from the command-line
# SYNOPSIS
@@ -10,36 +10,28 @@ oddmu-html - render Oddmu page HTML
# DESCRIPTION
The "html" subcommand opens the Markdown file for the given page name (appending
the ".md" extension) and prints the HTML to STDOUT without invoking the
"view.html" template. Use "-" as the page name if you want to read Markdown from
*stdin*.
The "html" subcommand opens the Markdown file for the given page name
(appending the ".md" extension) and prints the HTML to STDOUT without
invoking the "view.html" template.
# OPTIONS
*-view*
Use the "view.html" template to render the page. Without this, the HTML
lacks html and body tags.
Use the "view.html" template to render the page. Without this,
the HTML will lack html and body tags.
# EXAMPLES
# EXAMPLE
Generate "README.html" from "README.md":
Generate the HTML for "README.md":
```
oddmu html README > README.html
oddmu html README
```
Alternatively:
```
oddmu html - < README.md > README.html
```
# ENVIRONMENT
The ODDMU_WEBFINGER environment variable has no effect in this situation.
Fediverse accounts are not linked to their profile pages.
The ODDMU_WEBFINGER environment variable has no effect in this
situation. Fediverse accounts are not linked to their profile pages.
# SEE ALSO

View File

@@ -1,29 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-LINKS" "1" "2024-08-15"
.PP
.SH NAME
.PP
oddmu-links - list outgoing links for pages
.PP
.SH SYNOPSIS
.PP
\fBoddmu links\fR \fIpage names.\&.\&.\&\fR
.PP
.SH DESCRIPTION
.PP
The "links" subcommand lists outgoing links for one or more page names.\& Use "-"
as the page name if you want to read Markdown from \fBstdin\fR.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-missing\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,22 +0,0 @@
ODDMU-LINKS(1)
# NAME
oddmu-links - list outgoing links for pages
# SYNOPSIS
*oddmu links* _page names..._
# DESCRIPTION
The "links" subcommand lists outgoing links for one or more page names. Use "-"
as the page name if you want to read Markdown from *stdin*.
# SEE ALSO
_oddmu_(1), _oddmu-missing_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,56 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-LIST" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-list - list page names and titles
.PP
.SH SYNOPSIS
.PP
\fBoddmu list\fR [-dir \fIstring\fR]
.PP
.SH DESCRIPTION
.PP
The "list" subcommand lists page names and their titles, separated by a TAB
character.\& This saves you from opening and parsing all the files yourself if you
need the page titles.\&
.PP
If a directory is provided, only files from the tree starting at that
subdirectory are listed, and the directory is stripped from the page name.\&
.PP
.SH OPTIONS
.PP
\fB-dir\fR \fIstring\fR
.RS 4
Limit the list to a particular directory.\&
.PP
.RE
.SH EXAMPLES
.PP
Create list of links to pages in the "dad" directory, filter it for date pages
(starting with "2"), format it as a list of links and sort in reverse order.\&
This is a list of links you could append to "dad/index.\&md" if it doesn'\&t already
have a list of links.\&
.PP
.nf
.RS 4
oddmu list -dir dad
| grep \&'^2\&'
| awk -F "t" -e \&'{ print "* [" $2 "](" $1 ")" }\&'
| sort -r
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,45 +0,0 @@
ODDMU-LIST(1)
# NAME
oddmu-list - list page names and titles
# SYNOPSIS
*oddmu list* [-dir _string_]
# DESCRIPTION
The "list" subcommand lists page names and their titles, separated by a TAB
character. This saves you from opening and parsing all the files yourself if you
need the page titles.
If a directory is provided, only files from the tree starting at that
subdirectory are listed, and the directory is stripped from the page name.
# OPTIONS
*-dir* _string_
Limit the list to a particular directory.
# EXAMPLES
Create list of links to pages in the "dad" directory, filter it for date pages
(starting with "2"), format it as a list of links and sort in reverse order.
This is a list of links you could append to "dad/index.md" if it doesn't already
have a list of links.
```
oddmu list -dir dad \
| grep '^2' \
| awk -F "\t" -e '{ print "* [" $2 "](" $1 ")" }' \
| sort -r
```
# SEE ALSO
_oddmu_(1), _oddmu-search_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,57 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-MISSING" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-missing - list missing pages
.PP
.SH SYNOPSIS
.PP
\fBoddmu missing\fR
.PP
.SH DESCRIPTION
.PP
The "missing" subcommand lists pages and their local links that are missing.\&
.PP
Any links that seem like they might point outside the wiki are ignored: links
that start with a slash "/" and links that start with a known URL schema
(currently: "http:", "https:", "ftp:", "mailto:", "gopher:", "gemini:",
"finger:").\&
.PP
Notably, links that start with ".\&.\&/" are reported as missing.\&
.PP
.SH EXAMPLES
.PP
Looking for broken links:
.PP
.nf
.RS 4
oddmu missing
.fi
.RE
.PP
Result:
.PP
.nf
.RS 4
Page Missing
README github\&.com/pemistahl/lingua-go
.fi
.RE
.PP
This shows how the README file had a link where the URL was missing the scheme
"https://".\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-replace\fR(1), \fIoddmu-missing\fR(7)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,46 +0,0 @@
ODDMU-MISSING(1)
# NAME
oddmu-missing - list missing pages
# SYNOPSIS
*oddmu missing*
# DESCRIPTION
The "missing" subcommand lists pages and their local links that are missing.
Any links that seem like they might point outside the wiki are ignored: links
that start with a slash "/" and links that start with a known URL schema
(currently: "http:", "https:", "ftp:", "mailto:", "gopher:", "gemini:",
"finger:").
Notably, links that start with "../" are reported as missing.
# EXAMPLES
Looking for broken links:
```
oddmu missing
```
Result:
```
Page Missing
README github.com/pemistahl/lingua-go
```
This shows how the README file had a link where the URL was missing the scheme
"https://".
# SEE ALSO
_oddmu_(1), _oddmu-replace_(1), _oddmu-missing_(7)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,140 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NGINX" "5" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
.PP
.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.\&
.PP
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
.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|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://localhost:8080;
}
.fi
.RE
.PP
If you remove an action from the regular expression, those requests no longer
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
.RS 4
# public
location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|list|delete|rename|archive)/ {
auth_basic "Oddmu author";
auth_basic_user_file /etc/nginx/conf\&.d/htpasswd;
proxy_pass http://localhost:8080;
}
.fi
.RE
.PP
The passwords in "/etc/nginx/conf.\&d/htpasswd" are generated using \fIopenssl\fR(1).\&
Assuming the password is "CPTk&qO[Y@?\&M~L>qKOkd", this is how you encrypt it:
.PP
.nf
.RS 4
openssl passwd \&'CPTk&qO[Y@?M~L>qKOkd\&'
.fi
.RE
.PP
The output gets used in "/etc/nginx/conf.\&d/htpasswd".\& Here'\&s the user "alex"
using this password:
.PP
.nf
.RS 4
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|list|delete|rename|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)
.PP
"freenginx"
http://freenginx.\&org/
.PP
"freenginx ngx_http_proxy_module", proxy_pass
http://freenginx.\&org/en/docs/http/ngx_http_proxy_module.\&html#proxy_pass
.PP
"freenginx ngx_http_auth_basic_module"
http://freenginx.\&org/en/docs/http/ngx_http_auth_basic_module.\&html
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,119 +0,0 @@
ODDMU-NGINX(5)
# NAME
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
# 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.
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
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|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://localhost:8080;
}
```
If you remove an action from the regular expression, those requests no longer
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:
```
# public
location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|list|delete|rename|archive)/ {
auth_basic "Oddmu author";
auth_basic_user_file /etc/nginx/conf.d/htpasswd;
proxy_pass http://localhost:8080;
}
```
The passwords in "/etc/nginx/conf.d/htpasswd" are generated using _openssl_(1).
Assuming the password is "CPTk&qO[Y@?M~L>qKOkd", this is how you encrypt it:
```
openssl passwd 'CPTk&qO[Y@?M~L>qKOkd'
```
The output gets used in "/etc/nginx/conf.d/htpasswd". Here's the user "alex"
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|list|delete|rename|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)
"freenginx"
http://freenginx.org/
"freenginx ngx_http_proxy_module", proxy_pass
http://freenginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
"freenginx ngx_http_auth_basic_module"
http://freenginx.org/en/docs/http/ngx_http_auth_basic_module.html
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,108 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NOTIFY" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-notify - add links to changes.\&md, index.\&md, and hashtag pages
.PP
.SH SYNOPSIS
.PP
\fBoddmu notify\fR \fIpage names.\&.\&.\&\fR
.PP
.SH DESCRIPTION
.PP
The "notify" subcommand takes all the page names provided (without the ".\&md"
extension) and adds links to it from other pages.\&
.PP
A new link is added to the \fBchanges\fR page in the current directory if it doesn'\&t
exist.\& The current date of the machine Oddmu is running on is used as the
heading.\& If the requested link already exists on the changes page, it is moved
up to the current date.\& If that leaves an old date without any links, that date
heading is removed.\&
.PP
A page whose name starts with an ISO date (YYYY-MM-DD, e.\&g.\& "2023-10-28") is
called a \fBblog\fR page.\&
.PP
A link is created from the \fBindex\fR page in the current directory to blog pages
if and only if the blog pages are from the current year.\& The idea is that the
front page contains a lot of links to blog posts but eventually the blog post
links are moved onto archive pages (one per year, for example), or simply
deleted.\& As when editing older pages, links to those pages should not get added
to the index as if those older pages were new again.\& A link on the changes page
is enough.\&
.PP
For every \fBhashtag\fR used on the pages named, another link might be created.\& If a
page named like the hashtag exists, a backlink is added to it.\& A hashtag
consists of a number sign ('\&#'\&) followed by Unicode letters, numbers or the
underscore ('\&_'\&).\& Thus, a hashtag ends with punctuation or whitespace.\&
.PP
If a link already exists but it'\&s title is no longer correct, it is updated.\&
.PP
New links added for blog pages are added at the top of the first unnumbered list
using the asterisk ('\&*'\&).\& If no such list exists, a new one is started at the
bottom of the page.\& This allows you to have a different unnumbered list further
up on the page, as long as it uses the minus for items ('\&-'\&).\&
.PP
.SH EXAMPLES
.PP
After writing the file "2023-11-05-climate.\&md" containing the hashtag
"#Climate", add links to it from "index.\&md", "changes.\&md", and "Climate.\&md" (if
it exists):
.PP
.nf
.RS 4
oddmu notify 2023-11-05-climate
.fi
.RE
.PP
The changes file might look as follows:
.PP
.nf
.RS 4
# Changes
This page lists all the changes made to the wiki\&.
## 2023-11-05
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
The index file might look as follows:
.PP
.nf
.RS 4
# Blog
This page links to all the blog posts\&.
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
The hashtag file might look as follows:
.PP
.nf
.RS 4
# Climate
This page links to all the blog posts tagged #Climate\&.
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,93 +0,0 @@
ODDMU-NOTIFY(1)
# NAME
oddmu-notify - add links to changes.md, index.md, and hashtag pages
# SYNOPSIS
*oddmu notify* _page names..._
# DESCRIPTION
The "notify" subcommand takes all the page names provided (without the ".md"
extension) and adds links to it from other pages.
A new link is added to the *changes* page in the current directory if it doesn't
exist. The current date of the machine Oddmu is running on is used as the
heading. If the requested link already exists on the changes page, it is moved
up to the current date. If that leaves an old date without any links, that date
heading is removed.
A page whose name starts with an ISO date (YYYY-MM-DD, e.g. "2023-10-28") is
called a *blog* page.
A link is created from the *index* page in the current directory to blog pages
if and only if the blog pages are from the current year. The idea is that the
front page contains a lot of links to blog posts but eventually the blog post
links are moved onto archive pages (one per year, for example), or simply
deleted. As when editing older pages, links to those pages should not get added
to the index as if those older pages were new again. A link on the changes page
is enough.
For every *hashtag* used on the pages named, another link might be created. If a
page named like the hashtag exists, a backlink is added to it. A hashtag
consists of a number sign ('#') followed by Unicode letters, numbers or the
underscore ('\_'). Thus, a hashtag ends with punctuation or whitespace.
If a link already exists but it's title is no longer correct, it is updated.
New links added for blog pages are added at the top of the first unnumbered list
using the asterisk ('\*'). If no such list exists, a new one is started at the
bottom of the page. This allows you to have a different unnumbered list further
up on the page, as long as it uses the minus for items ('-').
# EXAMPLES
After writing the file "2023-11-05-climate.md" containing the hashtag
"#Climate", add links to it from "index.md", "changes.md", and "Climate.md" (if
it exists):
```
oddmu notify 2023-11-05-climate
```
The changes file might look as follows:
```
# Changes
This page lists all the changes made to the wiki.
## 2023-11-05
* [Global warming](2023-11-05-climate)
```
The index file might look as follows:
```
# Blog
This page links to all the blog posts.
* [Global warming](2023-11-05-climate)
```
The hashtag file might look as follows:
```
# Climate
This page links to all the blog posts tagged #Climate.
* [Global warming](2023-11-05-climate)
```
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,335 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-11-15"
.PP
.SH NAME
.PP
oddmu-releases - what'\&s new?\&
.PP
.SH DESCRIPTION
.PP
This page lists user-visible features and template changes to consider.\&
.PP
.SS 1.14 (2024)
.PP
Add \fIlist\fR, \fIdelete\fR and \fIrename\fR actions.\&
.PP
This requires a change to your web server setup if you are using a it as a
reverse proxy because you need to pass these new actions along to Oddmu,
together with appropriate permission checks.\&
.PP
See \fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example.\&
.PP
In addition to that, you might want a link to the \fIlist\fR action from one of the
existing templates.\& For example, from upload.\&html:
.PP
.nf
.RS 4
<p>You can rename and delete files <a href="/list/{{\&.Dir}}">from the file list</a>\&.
.fi
.RE
.PP
The following line was added to the "preview.\&html" and "edit.\&html" template:
.PP
.nf
.RS 4
<base href="/view/{{\&.Dir}}">
.fi
.RE
.PP
You might want to do that as well, if you have your own.\& Without this, links in
the preview cannot be followed as they all point to \fB/preview\fR instead of
\fB/view\fR and the link to the list of changes cannot be followed from the edit
page: it leads to editing the list of changes.\&
.PP
.SS 1.13 (2024)
.PP
Add \fIexport\fR subcommand.\&
.PP
.SS 1.12 (2024)
.PP
Add \fIhashtags\fR, \fIlinks\fR and \fItoc\fR subcommands.\&
.PP
Support searching for multiple words using all sorts of quotation marks.\& That
means that it is now impossible to search for words that begin with such a
quotation mark.\&
.PP
These are the quotation marks currently supported: '\&foo'\& "foo" foo foo foo
“foo” „foo“ ”foo” «foo» »foo« foo foo 「foo」 「foo」 『foo』 any such
quoted text is searched as-is, including whitespace.\&
.PP
Add loading="lazy" for images in search.\&html
.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 loading="lazy" src="/view/{{\&.Name}}"></a><br/>{{\&.Html}}
{{end}}
</article>
{{end}}
.fi
.RE
.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.\&10179781009157926.\&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
used.\& The \fILast\fR property no longer contains the directory.\& It has to be added
to the template as follows:
.PP
.nf
.RS 4
{{if ne \&.Last ""}}
<p>Previous upload: <a href="/view/{{\&.Dir}}{{\&.Last}}">{{\&.Last}}</a></p>
{{if \&.Image}}
<p><img class="last" src="/view/{{\&.Dir}}{{\&.Last}}"></p>
{{end}}
{{end}}
.fi
.RE
.PP
You can use the \fILast\fR property without a directory to suggest the markup to
use, for example:
.PP
.nf
.RS 4
<p>Use the following for <a href="/view/{{\&.Dir}}{{\&.Today}}">{{\&.Today}}</a>:
<pre>![]({{\&.Last}})</a></pre>
.fi
.RE
.PP
The upload template can use the \fIToday\fR property.\&
.PP
The upload template comes with JavaScript that allows users to paste images or
drag and drop files.\&
.PP
The upload template changed the id for the filename field from `text` to `name`.\&
.PP
The source repository now comes with example templates.\&
.PP
.SS 1.8 (2024)
.PP
No user-visible changes.\& Documentation and code comments got better.\&
.PP
.SS 1.7 (2024)
.PP
Allow upload of multiple files.\& This requires an update to the \fIupload.\&html\fR
template: Add the \fImultiple\fR attribute to the file input element and change the
label from "file" to "files".\&
.PP
Fix orientation of uploaded images.\& JPG and HEIC images have EXIF data telling a
viewer how to orient the image.\& Oddmu now uses this information to rotate the
image correctly before stripping it.\&
.PP
The version command now displays much less information unless given the -full
argument.\&
.PP
.SS 1.6 (2024)
.PP
Add \fIarchive\fR action to serve a zip file.\&
.PP
.SS 1.5 (2024)
.PP
Filtering separate sites in subdirectories via the ODDMU_FILTER environment
variable in order to exclude them from the \fIsearch\fR action.\&
.PP
Add \fIversion\fR subcommand.\&
.PP
Add filesystem watchers to automatically reindex changed pages and reload
changed templates.\&
.PP
When rendering a page, use templates in the same directory, if available.\&
.PP
Delete uploaded files by uploading a file with zero bytes.\&
.PP
.SS 1.4 (2024)
.PP
If stdin is a Unix-domain socket, use that to serve the site.\& Otherwise, allow
specifying a listen address via the ODDMU_ADDRESS environment variable.\&
.PP
.SS 1.3 (2024)
.PP
Add support for resizing HEIC images (and saving them as JPG files).\&
.PP
.SS 1.2 (2023)
.PP
Add \fIlist\fR subcommand.\&
.PP
.SS 1.1 (2023)
.PP
Rewrote most of the README into man pages.\&
.PP
Add fediverse account rendering if ODDMU_WEBFINGER is set.\&
.PP
Add notifications when saving files: adding links to \fIindex\fR, \fIchanges\fR and
\fIhashtag\fR pages.\&
.PP
Add \fIreplace\fR subcommand.\& Add \fImissing\fR subcommand.\& Add \fInotify\fR command.\& Add
\fIstatic\fR command.\&
.PP
Add \fIdiff\fR action.\&
.PP
Add feed generation based on the local links from a page.\&
.PP
Add caching support by considering the If-Modified-Since header in requests and
providing a Last-Modified header in responses.\&
.PP
Handle HEAD requests.\&
.PP
Remove HTML sanitization.\&
.PP
Remove MathJax support from the wiki parser.\& The templates never included the
necessary MathJax JavaScript anyway so the special handling of $ was just an
annoyance.\&
.PP
Drop trigram index and just search all the files.\& This takes much less RAM and
doesn'\&t take too much time even with a few thousand pages.\&
.PP
Add "blog:true" and "blog:false" predicates to search.\&
.PP
Limit search to the current directory tree.\&
.PP
Do not overwrite fresh backups: there must be a 1h break before the backup is
overwritten.\&
.PP
.SS 1.0 (2023)
.PP
Paginate search results and no longer sort search results by score.\&
.PP
.SS 0.9 (2023)
.PP
Add image resizing.\&
.PP
Add wiki links in double square brackets to the parser.\&
.PP
.SS 0.8 (2023)
.PP
Rename files to backups before saving.\&
.PP
Rename the \fIsaveUpload\fR action to \fIdrop\fR.\&
.PP
Add the \fIsearch\fR subcommand.\&
.PP
.SS 0.7 (2023)
.PP
Add \fIupload\fR and \fIsaveUpload\fR action so that one can upload files.\&
.PP
Add \fIhtml\fR subcommand.\&
.PP
.SS 0.6 (2003)
.PP
Add \fIadd\fR and \fIappend\fR action so that one can add to an existing page.\& This is
important for me as editing pages on the phone can be cumbersome but leaving
comments on my own site has always been easy to do.\&
.PP
Serve all existing files, not just text files.\&
.PP
Save an empty page to delete it.\&
.PP
Changed default permissions from 600 to 644 for files and from 700 to 755 for
directories.\&
.PP
Make language detection configurable using an environment variable.\&
.PP
.SS 0.5 (2023)
.PP
Add hyphenation to templates using Peter M.\& Stahl'\&s Lingua library.\&
.PP
.SS 0.4 (2023)
.PP
Create subdirectories as necessary.\&
.PP
.SS 0.3 (2023)
.PP
Add \fIsearch\fR action using Damian Gryski'\&s trigram indexing, with scoring,
highlighting and snippet extraction.\&
.PP
.SS 0.2 (2023)
.PP
Switch to Krzysztof Kowalczyk'\&s Go Markdown fork of Blackfriday to render
Markdown.\& Use Dee'\&s Bluemonday to sanitize HTML.\&
.PP
Switch to GNU Affero GPL 3 license.\&
.PP
Serve text files (.\&txt).\&
.PP
Support serving on any port via the environment variable ODDMU_PORT.\&
.PP
.SS 0.1 (2015)
.PP
A web server that allows editing files in Wiki Creole Matt Self'\&s Cajun library.\&
Supported actions are \fIedit\fR, \fIsave\fR, and \fIview\fR.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,310 +0,0 @@
ODDMU-RELEASES(7)
# NAME
oddmu-releases - what's new?
# DESCRIPTION
This page lists user-visible features and template changes to consider.
## 1.14 (2024)
Add _list_, _delete_ and _rename_ actions.
This requires a change to your web server setup if you are using a it as a
reverse proxy because you need to pass these new actions along to Oddmu,
together with appropriate permission checks.
See _oddmu-apache_(5) or _oddmu-nginx_(5) for example.
In addition to that, you might want a link to the _list_ action from one of the
existing templates. For example, from upload.html:
```
<p>You can rename and delete files <a href="/list/{{.Dir}}">from the file list</a>.
```
The following line was added to the "preview.html" and "edit.html" template:
```
<base href="/view/{{.Dir}}">
```
You might want to do that as well, if you have your own. Without this, links in
the preview cannot be followed as they all point to */preview* instead of
*/view* and the link to the list of changes cannot be followed from the edit
page: it leads to editing the list of changes.
## 1.13 (2024)
Add _export_ subcommand.
## 1.12 (2024)
Add _hashtags_, _links_ and _toc_ subcommands.
Support searching for multiple words using all sorts of quotation marks. That
means that it is now impossible to search for words that begin with such a
quotation mark.
These are the quotation marks currently supported: 'foo' "foo" foo foo foo
“foo” „foo“ ”foo” «foo» »foo« foo foo 「foo」 「foo」 『foo』 any such
quoted text is searched as-is, including whitespace.
Add loading="lazy" for images in search.html
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 loading="lazy" src="/view/{{.Name}}"></a><br/>{{.Html}}
{{end}}
</article>
{{end}}
```
## 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.10179781009157926.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
used. The _Last_ property no longer contains the directory. It has to be added
to the template as follows:
```
{{if ne .Last ""}}
<p>Previous upload: <a href="/view/{{.Dir}}{{.Last}}">{{.Last}}</a></p>
{{if .Image}}
<p><img class="last" src="/view/{{.Dir}}{{.Last}}"></p>
{{end}}
{{end}}
```
You can use the _Last_ property without a directory to suggest the markup to
use, for example:
```
<p>Use the following for <a href="/view/{{.Dir}}{{.Today}}">{{.Today}}</a>:
<pre>![]({{.Last}})</a></pre>
```
The upload template can use the _Today_ property.
The upload template comes with JavaScript that allows users to paste images or
drag and drop files.
The upload template changed the id for the filename field from `text` to `name`.
The source repository now comes with example templates.
## 1.8 (2024)
No user-visible changes. Documentation and code comments got better.
## 1.7 (2024)
Allow upload of multiple files. This requires an update to the _upload.html_
template: Add the _multiple_ attribute to the file input element and change the
label from "file" to "files".
Fix orientation of uploaded images. JPG and HEIC images have EXIF data telling a
viewer how to orient the image. Oddmu now uses this information to rotate the
image correctly before stripping it.
The version command now displays much less information unless given the -full
argument.
## 1.6 (2024)
Add _archive_ action to serve a zip file.
## 1.5 (2024)
Filtering separate sites in subdirectories via the ODDMU_FILTER environment
variable in order to exclude them from the _search_ action.
Add _version_ subcommand.
Add filesystem watchers to automatically reindex changed pages and reload
changed templates.
When rendering a page, use templates in the same directory, if available.
Delete uploaded files by uploading a file with zero bytes.
## 1.4 (2024)
If stdin is a Unix-domain socket, use that to serve the site. Otherwise, allow
specifying a listen address via the ODDMU_ADDRESS environment variable.
## 1.3 (2024)
Add support for resizing HEIC images (and saving them as JPG files).
## 1.2 (2023)
Add _list_ subcommand.
## 1.1 (2023)
Rewrote most of the README into man pages.
Add fediverse account rendering if ODDMU_WEBFINGER is set.
Add notifications when saving files: adding links to _index_, _changes_ and
_hashtag_ pages.
Add _replace_ subcommand. Add _missing_ subcommand. Add _notify_ command. Add
_static_ command.
Add _diff_ action.
Add feed generation based on the local links from a page.
Add caching support by considering the If-Modified-Since header in requests and
providing a Last-Modified header in responses.
Handle HEAD requests.
Remove HTML sanitization.
Remove MathJax support from the wiki parser. The templates never included the
necessary MathJax JavaScript anyway so the special handling of $ was just an
annoyance.
Drop trigram index and just search all the files. This takes much less RAM and
doesn't take too much time even with a few thousand pages.
Add "blog:true" and "blog:false" predicates to search.
Limit search to the current directory tree.
Do not overwrite fresh backups: there must be a 1h break before the backup is
overwritten.
## 1.0 (2023)
Paginate search results and no longer sort search results by score.
## 0.9 (2023)
Add image resizing.
Add wiki links in double square brackets to the parser.
## 0.8 (2023)
Rename files to backups before saving.
Rename the _saveUpload_ action to _drop_.
Add the _search_ subcommand.
## 0.7 (2023)
Add _upload_ and _saveUpload_ action so that one can upload files.
Add _html_ subcommand.
## 0.6 (2003)
Add _add_ and _append_ action so that one can add to an existing page. This is
important for me as editing pages on the phone can be cumbersome but leaving
comments on my own site has always been easy to do.
Serve all existing files, not just text files.
Save an empty page to delete it.
Changed default permissions from 600 to 644 for files and from 700 to 755 for
directories.
Make language detection configurable using an environment variable.
## 0.5 (2023)
Add hyphenation to templates using Peter M. Stahl's Lingua library.
## 0.4 (2023)
Create subdirectories as necessary.
## 0.3 (2023)
Add _search_ action using Damian Gryski's trigram indexing, with scoring,
highlighting and snippet extraction.
## 0.2 (2023)
Switch to Krzysztof Kowalczyk's Go Markdown fork of Blackfriday to render
Markdown. Use Dee's Bluemonday to sanitize HTML.
Switch to GNU Affero GPL 3 license.
Serve text files (.txt).
Support serving on any port via the environment variable ODDMU_PORT.
## 0.1 (2015)
A web server that allows editing files in Wiki Creole Matt Self's Cajun library.
Supported actions are _edit_, _save_, and _view_.
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,48 +1,42 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-REPLACE" "1" "2024-08-29"
.TH "ODDMU-REPLACE" "1" "2023-09-22"
.PP
.SH NAME
.PP
oddmu-replace - replace text in Oddmu pages
oddmu-replace - replace text in Oddmu pages from the command-line
.PP
.SH SYNOPSIS
.PP
\fBoddmu replace\fR [-confirm] [-regexp] \fIterm\fR \fIreplacement\fR
\fBoddmu replace\fR [-confirm] \fIregexp\fR \fIreplacement\fR
.PP
.SH DESCRIPTION
.PP
The "replace" subcommand does a search and replace on all the Markdown files in
the current directory and its subdirectories.\&
The "replace" subcommand replaces the Markdown files in the current
directory (!\&), returning the replace result as a Markdown-formatted
list.\&
.PP
.SH OPTIONS
.PP
\fB-confirm\fR
.RS 4
By default, the replacement doesn'\&t save the changes made.\& Instead, a
unified diff is produced and printed.\& Given this option, the changed
Markdown files are saved to disk.\&
By default, the replacement doesn'\&t save the changes made.\&
Instead, a unified diff is produced and printed.\& Given this
option, the changed Markdown files are saved to disk.\&
.PP
.RE
\fB-regexp\fR
.RS 4
By default, the term to be replaced is just a string.\& With this flag,
the term is a regular expression and the replacement can contain
backreferences ($1, $2, $3, etc.\&) to capture groups.\&
.SH EXAMPLE
.PP
.RE
.SH EXAMPLES
.PP
Replace "Oddmu" in the Markdown files of the current directory:
Replace for "oddmu" in the Markdown files of the current directory:
.PP
.nf
.RS 4
oddmu replace Oddmu Oddµ
oddmu replace oddmu
.fi
.RE
.PP
@@ -50,51 +44,18 @@ Result:
.PP
.nf
.RS 4
--- README\&.md~
+++ README\&.md
(diff omitted)
1 file would be changed\&.
This is a dry run\&. Use -confirm to make it happen\&.
Replace oddmu: 1 result
* [Oddµ: A minimal wiki](README) (5)
.fi
.RE
.PP
.SH NOTES
.PP
This is the equivalent of using \fIsed\fR(1) with the --quiet, --regexp-extended,
--in-place=~ and --expression command with the s command
"s/regexp/replacement/g" except that it prints a unified diff per default
instead of making any changes and the regexp rules differ slightly.\&
.PP
The search is case-sensitive.\& To make it case-insensitive, search for a regular
expression that sets the case-insensitive flag, e.\&g.\& "(?\&i)oddmu".\&
.PP
.SH SECURITY
.PP
Consider creating a backup before doing replacements!\&
.PP
The following Bash script creates a copy of the current directory using hard
links.\& If you'\&re in a directory called "wiki", it creates a sibling directory
called "wiki-2023-11-24" (using the current date) full of links.\& This takes
little space and time.\& It works as a backup as long as you don'\&t use an
application that edits files in place.\& Most programs overwrite old files by
creating new files with the same name, so you should be safe.\&
.PP
.nf
.RS 4
#!/usr/bin/bash
d=$(basename $(pwd))
t=$(date --iso-8601)
echo Creating a snapshot of $d in \&.\&./$d-$t
rsync --link-dest "\&.\&./$d" --archive \&. "\&.\&./$d-$t/"
.fi
.RE
.PP
The above wouldn'\&t work for database files, for example.\& There, the database
changes the file in place thus the file is changed in the backup directory as
well.\& For Oddmu and the usual text editors, it works.\& If you use Emacs, don'\&t
set \fIbackup-by-copying\fR, \fIbackup-by-copying-when-linked\fR and related variables.\&
This is the equivalent of using \fIsed\fR(1) with the --quiet,
--regexp-extended, --in-place=~ and --expression command with the s
command "s/regexp/replacement/g" except that it prints a unified diff
per default instead of making any changes and the regexp rules differ
slightly.\&
.PP
.SH SEE ALSO
.PP

View File

@@ -2,82 +2,47 @@ ODDMU-REPLACE(1)
# NAME
oddmu-replace - replace text in Oddmu pages
oddmu-replace - replace text in Oddmu pages from the command-line
# SYNOPSIS
*oddmu replace* [-confirm] [-regexp] _term_ _replacement_
*oddmu replace* [-confirm] _regexp_ _replacement_
# DESCRIPTION
The "replace" subcommand does a search and replace on all the Markdown files in
the current directory and its subdirectories.
The "replace" subcommand replaces the Markdown files in the current
directory (!), returning the replace result as a Markdown-formatted
list.
# OPTIONS
*-confirm*
By default, the replacement doesn't save the changes made. Instead, a
unified diff is produced and printed. Given this option, the changed
Markdown files are saved to disk.
By default, the replacement doesn't save the changes made.
Instead, a unified diff is produced and printed. Given this
option, the changed Markdown files are saved to disk.
*-regexp*
By default, the term to be replaced is just a string. With this flag,
the term is a regular expression and the replacement can contain
backreferences ($1, $2, $3, etc.) to capture groups.
# EXAMPLE
# EXAMPLES
Replace "Oddmu" in the Markdown files of the current directory:
Replace for "oddmu" in the Markdown files of the current directory:
```
oddmu replace Oddmu Oddµ
oddmu replace oddmu
```
Result:
```
--- README.md~
+++ README.md
(diff omitted)
1 file would be changed.
This is a dry run. Use -confirm to make it happen.
Replace oddmu: 1 result
* [Oddµ: A minimal wiki](README) (5)
```
# NOTES
This is the equivalent of using _sed_(1) with the --quiet, --regexp-extended,
\--in-place=~ and --expression command with the s command
"s/regexp/replacement/g" except that it prints a unified diff per default
instead of making any changes and the regexp rules differ slightly.
The search is case-sensitive. To make it case-insensitive, search for a regular
expression that sets the case-insensitive flag, e.g. "(?i)oddmu".
# SECURITY
Consider creating a backup before doing replacements!
The following Bash script creates a copy of the current directory using hard
links. If you're in a directory called "wiki", it creates a sibling directory
called "wiki-2023-11-24" (using the current date) full of links. This takes
little space and time. It works as a backup as long as you don't use an
application that edits files in place. Most programs overwrite old files by
creating new files with the same name, so you should be safe.
```
#!/usr/bin/bash
d=$(basename $(pwd))
t=$(date --iso-8601)
echo Creating a snapshot of $d in ../$d-$t
rsync --link-dest "../$d" --archive . "../$d-$t/"
```
The above wouldn't work for database files, for example. There, the database
changes the file in place thus the file is changed in the backup directory as
well. For Oddmu and the usual text editors, it works. If you use Emacs, don't
set _backup-by-copying_, _backup-by-copying-when-linked_ and related variables.
This is the equivalent of using _sed_(1) with the --quiet,
\--regexp-extended, --in-place=~ and --expression command with the s
command "s/regexp/replacement/g" except that it prints a unified diff
per default instead of making any changes and the regexp rules differ
slightly.
# SEE ALSO

View File

@@ -1,85 +1,54 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-SEARCH" "1" "2024-08-29"
.TH "ODDMU-SEARCH" "1" "2023-09-19"
.PP
.SH NAME
.PP
oddmu-search - search the Oddmu pages
oddmu-search - search the Oddmu pages from the command-line
.PP
.SH SYNOPSIS
.PP
\fBoddmu search\fR [-extract] [-page \fIn\fR] \fIterms.\&.\&.\&\fR
\fBoddmu search\fR [-page \fIn\fR] \fIterms.\&.\&.\&\fR
.PP
.SH DESCRIPTION
.PP
The "search" subcommand resursively searches the Markdown files in the current
directory tree.\& That is, the files in the current directory and all its child
directories are searched.\&
The "search" subcommand searches the Markdown files in the current
directory (!\&), returning the search result as a Markdown-formatted
list.\&
.PP
Be default, this returns a Markdown-formatted list suitable for pasting into
Oddmu pages.\&
.PP
If a directory is provided, only files from the tree starting at that
subdirectory are listed, and the directory is stripped from the page name.\&
.PP
If multiple terms are provided, they are all concatenated into a single,
space-separated query string.\& That is, searching for the terms A B and the term
"A B" is equivalent.\&
.PP
See \fIoddmu-search\fR(7) for more information of how pages are searched, sorted and
scored.\&
The use of a trigram index makes it possible to find substrings and
for the word order not to matter, but it also makes the search results
a bit harder to understand.\& See \fIoddmu-search\fR(7) for more.\&
.PP
.SH OPTIONS
.PP
\fB-dir\fR \fIstring\fR
.RS 4
Limit search to a particular directory.\&
.RE
\fB-extract\fR
.RS 4
Print search extracts for interactive use
.RE
\fB-page\fR \fIn\fR
.RS 4
Search results are paginated and by default only the first page is
shown.\& This option allows you to view other pages.\&
.RE
\fB-all\fR
.RS 4
Ignore pagination and just print a long list of results.\&
Search results are paginated and by default only the first
page is shown.\& This option allows you to view other pages.\&
.PP
.RE
.SH EXAMPLES
.SH EXAMPLE
.PP
Search for the two words "Alex" and "Schroeder".\& All of the following are
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".\&
The ordering of terms does not matter.\&
Search for "oddmu" in the Markdown files of the current directory:
.PP
.nf
.RS 4
~/src/oddmu $ oddmu search Alex Schroeder
Search for Alex Schroeder, page 1: 3 results
* [Alex Schroeder theme](themes/alexschroeder\&.ch/README)
* [Oddµ: A minimal wiki](README)
* [Themes](themes/index)
oddmu search oddmu
.fi
.RE
.PP
Search for the exact phrase "Alex Schroeder".\& In order to pass the quotes to
Oddmu, a second level of quotes is required.\& All of the following are
equivalent: '\&"Alex Schroeder"'\&, "'\&Alex Schroeder'\&", \e"Alex\e Schroeder\e",
\e"Alex Schroeder\e".\&
Result:
.PP
.nf
.RS 4
~/src/oddmu $ oddmu search "\&'Alex Schroeder\&'"
Search for \&'Alex Schroeder\&', page 1: 1 result
* [Alex Schroeder theme](themes/alexschroeder\&.ch/README)
Search oddmu: 1 result
* [Oddµ: A minimal wiki](README) (5)
.fi
.RE
.PP

View File

@@ -2,66 +2,41 @@ ODDMU-SEARCH(1)
# NAME
oddmu-search - search the Oddmu pages
oddmu-search - search the Oddmu pages from the command-line
# SYNOPSIS
*oddmu search* [-extract] [-page _n_] _terms..._
*oddmu search* [-page _n_] _terms..._
# DESCRIPTION
The "search" subcommand resursively searches the Markdown files in the current
directory tree. That is, the files in the current directory and all its child
directories are searched.
The "search" subcommand searches the Markdown files in the current
directory (!), returning the search result as a Markdown-formatted
list.
Be default, this returns a Markdown-formatted list suitable for pasting into
Oddmu pages.
If a directory is provided, only files from the tree starting at that
subdirectory are listed, and the directory is stripped from the page name.
If multiple terms are provided, they are all concatenated into a single,
space-separated query string. That is, searching for the terms A B and the term
"A B" is equivalent.
See _oddmu-search_(7) for more information of how pages are searched, sorted and
scored.
The use of a trigram index makes it possible to find substrings and
for the word order not to matter, but it also makes the search results
a bit harder to understand. See _oddmu-search_(7) for more.
# OPTIONS
*-dir* _string_
Limit search to a particular directory.
*-extract*
Print search extracts for interactive use
*-page* _n_
Search results are paginated and by default only the first page is
shown. This option allows you to view other pages.
*-all*
Ignore pagination and just print a long list of results.
Search results are paginated and by default only the first
page is shown. This option allows you to view other pages.
# EXAMPLES
# EXAMPLE
Search for the two words "Alex" and "Schroeder". All of the following are
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".
The ordering of terms does not matter.
Search for "oddmu" in the Markdown files of the current directory:
```
~/src/oddmu $ oddmu search Alex Schroeder
Search for Alex Schroeder, page 1: 3 results
* [Alex Schroeder theme](themes/alexschroeder.ch/README)
* [Oddµ: A minimal wiki](README)
* [Themes](themes/index)
oddmu search oddmu
```
Search for the exact phrase "Alex Schroeder". In order to pass the quotes to
Oddmu, a second level of quotes is required. All of the following are
equivalent: '"Alex Schroeder"', "'Alex Schroeder'", \\"Alex\\ Schroeder\\",
\\"Alex Schroeder\\".
Result:
```
~/src/oddmu $ oddmu search "'Alex Schroeder'"
Search for 'Alex Schroeder', page 1: 1 result
* [Alex Schroeder theme](themes/alexschroeder.ch/README)
Search oddmu: 1 result
* [Oddµ: A minimal wiki](README) (5)
```
# SEE ALSO

View File

@@ -1,72 +1,39 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-SEARCH" "7" "2024-02-19"
.TH "ODDMU-SEARCH" "7" "2023-09-18"
.PP
.SH NAME
.PP
oddmu-search - understanding the Oddmu search engine
.PP
.SH SYNOPSIS
.PP
\fBoddmu search\fR \fIterms\fR.\&.\&.\&
.PP
.SH DESCRIPTION
.PP
The wiki keeps an index of all the hash tags and page titles in memory.\& Using
hashtags and predicates in your queries speeds them up because fewer files are
opened.\&
The index indexes trigrams.\& Each group of three characters is a
trigram.\& A document with content "This is a test" is turned to lower
case and indexed under the trigrams "thi", "his", "is ", "s i", " is",
"is ", "s a", " a ", "a t", " te", "tes", "est".\&
.PP
A hashtag starts with a number sign ('\&#'\&) and contains numbers, letters, and the
underscore ('\&_'\&).\&
Each query is split into words and then processed the same way.\& A
query with the words "this test" is turned to lower case and produces
the trigrams "thi", "his", "tes", "est".\& This means that the word
order is not considered when searching for documents.\&
.PP
Example: #old_school random encounter
This also means that there is no stemming.\& Searching for "testing"
won'\&t find "This is a test" because there are no matches for the
trigrams "sti", "tin", "ing".\&
.PP
The title predicate filters for pages where the term is contained in the page
title.\&
.PP
Example: title:geo title:cache zürich
.PP
The blog predicate filters for pages where the page name begins with an ISO date
like "2023-09-26" if true, or doesn'\&t begin with an ISO date if false.\&
.PP
Example: blog:false fountain
.PP
The sorting of all the pages does not depend on the number of matches or any
kind of score because computing the score is expensive as this requires the page
to be loaded from disk.\& Therefore, results are sorted by title:
.PP
.PD 0
.IP \(bu 4
If a page title matches the query string exactly, it gets sorted first.\&
.IP \(bu 4
If the page title contains the query string, it gets sorted next.\&
.IP \(bu 4
If the page name starts with a number, it is sorted descending.\&
.IP \(bu 4
All other pages follow, sorted ascending.\&
.PD
.PP
The effect is that first, the pages with matches in the page title are shown,
and then all the others.\& Within these two groups, the most recent blog posts are
shown first.\& This assumes that blog pages start with an ISO date like
"2023-09-16".\&
.PP
When searching for a hashtag, a page name (not the title!\&) matching the hashtag
exactly (without the leading '\&#'\&) is listed first, even if it doesn'\&t contain
the hashtag.\& It is assumed that this page offers some kind of introduction to
people searching for the hashtag.\&
.PP
Example: When people click on the hashtag "#Oddµ" and a page named "Oddµ" exists
(in other words, the file "Oddµ.\&md" exists), it is prepended to the results even
if it doesn'\&t have the hashtag "#Oddµ" and even if it has a title of "Oddµ, a
minimal wiki" (which wouldn'\&t be an exact match).\&
.PP
The score and highlighting of snippets is used to help visitors decide which
links to click.\&
.PP
Each document found is scored.\& Each of the following increases the score by one
point:
These trigrams are looked up in the index, resulting in the list of
documents.\& Each document found is then scored.\& Each of the following
increases the score by one point:
.PP
.PD 0
.IP \(bu 4
@@ -81,26 +48,41 @@ a word matches at the end of a word
a word matches as a whole word
.PD
.PP
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.\&
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.\&
.PP
.SH ENVIRONMENT
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.\&
.PP
To exclude subdirectories from searches, use the ODDMU_FILTER environment
variable.\& Set it to a regular expression matching sub-directories such as
"^projects/".\& If search starts in a directory matching the regular expression,
it is limited to the directory tree, as always.\& However, if search starts in a
directory that doesn'\&t match, subdirectories that do match are skipped.\& See
\fIoddmu-filter\fR(7).\&
The sorting of all the pages, however, does not depend on scoring!\&
Computing the score is expensive because the page must be loaded from
disk.\& Therefore, results are sorted by title:
.PP
To prevent access to a private directory tree, you must configure the web server
in addition to setting the ODDMU_FILTER environment variable.\&
.PD 0
.IP \(bu 4
If the page title contains the query string, it gets sorted first.\&
.IP \(bu 4
If the page name (the filename!\&) begins with a number, it is sorted
descending.\&
.IP \(bu 4
All other pages follow, sorted ascending.\&
.PD
.PP
The effect is that first, the pages with matches in the page title are
shown, and then all the others.\& Within these two groups, the most
recent blog posts are shown first.\& This assumes that blog pages start
with an ISO date like "2023-09-16".\&
.PP
The score and highlighting of snippets is used to help visitors decide
which links to click.\& A score of 0 indicates that all the trigrams
were found but \fIno exact matches\fR for any of the terms.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(1), \fIoddmu-filter\fR(7), \fIoddmu-apache\fR(5),
\fIoddmu-nginx\fR(5)
\fIoddmu\fR(1), \fIoddmu-search\fR(1)
.PP
.SH AUTHORS
.PP

View File

@@ -4,56 +4,29 @@ ODDMU-SEARCH(7)
oddmu-search - understanding the Oddmu search engine
# SYNOPSIS
*oddmu search* _terms_...
# DESCRIPTION
The wiki keeps an index of all the hash tags and page titles in memory. Using
hashtags and predicates in your queries speeds them up because fewer files are
opened.
The index indexes trigrams. Each group of three characters is a
trigram. A document with content "This is a test" is turned to lower
case and indexed under the trigrams "thi", "his", "is ", "s i", " is",
"is ", "s a", " a ", "a t", " te", "tes", "est".
A hashtag starts with a number sign ('#') and contains numbers, letters, and the
underscore ('\_').
Each query is split into words and then processed the same way. A
query with the words "this test" is turned to lower case and produces
the trigrams "thi", "his", "tes", "est". This means that the word
order is not considered when searching for documents.
Example: #old_school random encounter
This also means that there is no stemming. Searching for "testing"
won't find "This is a test" because there are no matches for the
trigrams "sti", "tin", "ing".
The title predicate filters for pages where the term is contained in the page
title.
Example: title:geo title:cache zürich
The blog predicate filters for pages where the page name begins with an ISO date
like "2023-09-26" if true, or doesn't begin with an ISO date if false.
Example: blog:false fountain
The sorting of all the pages does not depend on the number of matches or any
kind of score because computing the score is expensive as this requires the page
to be loaded from disk. Therefore, results are sorted by title:
- If a page title matches the query string exactly, it gets sorted first.
- If the page title contains the query string, it gets sorted next.
- If the page name starts with a number, it is sorted descending.
- All other pages follow, sorted ascending.
The effect is that first, the pages with matches in the page title are shown,
and then all the others. Within these two groups, the most recent blog posts are
shown first. This assumes that blog pages start with an ISO date like
"2023-09-16".
When searching for a hashtag, a page name (not the title!) matching the hashtag
exactly (without the leading '#') is listed first, even if it doesn't contain
the hashtag. It is assumed that this page offers some kind of introduction to
people searching for the hashtag.
Example: When people click on the hashtag "#Oddµ" and a page named "Oddµ" exists
(in other words, the file "Oddµ.md" exists), it is prepended to the results even
if it doesn't have the hashtag "#Oddµ" and even if it has a title of "Oddµ, a
minimal wiki" (which wouldn't be an exact match).
The score and highlighting of snippets is used to help visitors decide which
links to click.
Each document found is scored. Each of the following increases the score by one
point:
These trigrams are looked up in the index, resulting in the list of
documents. Each document found is then scored. Each of the following
increases the score by one point:
- the entire phrase matches
- a word matches
@@ -61,26 +34,36 @@ point:
- a word matches at the end of a word
- a word matches as a whole word
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.
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.
# ENVIRONMENT
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.
To exclude subdirectories from searches, use the ODDMU_FILTER environment
variable. Set it to a regular expression matching sub-directories such as
"^projects/". If search starts in a directory matching the regular expression,
it is limited to the directory tree, as always. However, if search starts in a
directory that doesn't match, subdirectories that do match are skipped. See
_oddmu-filter_(7).
The sorting of all the pages, however, does not depend on scoring!
Computing the score is expensive because the page must be loaded from
disk. Therefore, results are sorted by title:
To prevent access to a private directory tree, you must configure the web server
in addition to setting the ODDMU_FILTER environment variable.
- If the page title contains the query string, it gets sorted first.
- If the page name (the filename!) begins with a number, it is sorted
descending.
- All other pages follow, sorted ascending.
The effect is that first, the pages with matches in the page title are
shown, and then all the others. Within these two groups, the most
recent blog posts are shown first. This assumes that blog pages start
with an ISO date like "2023-09-16".
The score and highlighting of snippets is used to help visitors decide
which links to click. A score of 0 indicates that all the trigrams
were found but _no exact matches_ for any of the terms.
# SEE ALSO
_oddmu_(1), _oddmu-search_(1), _oddmu-filter_(7), _oddmu-apache_(5),
_oddmu-nginx_(5)
_oddmu_(1), _oddmu-search_(1)
# AUTHORS

View File

@@ -1,96 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-static - create a static copy of the site
.PP
.SH SYNOPSIS
.PP
\fBoddmu static\fR \fIdir-name\fR
.PP
.SH DESCRIPTION
.PP
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory.\& Existing files are
only overwritten if they are older than the source file.\&
.PP
All pages (files with the ".\&md" extension) are turned into HTML files (with the
".\&html" extension) using the "static.\&html" template.\& Links pointing to existing
pages get ".\&html" appended.\&
.PP
If a page has a name case-insensitively matching a hashtag, a feed file is
generated (ending with ".\&rss") if any suitable links are found.\& A suitable link
for a feed item must appear in a bullet list item using an asterisk ("*").\& If
no feed items are found, no feed is written.\&
.PP
Hidden files and directories (starting with a ".\&") and backup files (ending with
a "~") are skipped.\&
.PP
All other files are \fIhard linked\fR.\& This is done to save space: on a typical blog
the images take a lot more space than the text.\& On my blog in 2023 I had 2.\&62
GiB of JPG files and 0.\&02 GiB of Markdown files.\& There is no point in copying
all those images, most of the time.\&
.PP
Note, however: Hard links cannot span filesystems.\& A hard link is just an extra
name for the same file.\& This is why the destination directory for the static
site has to be on same filesystem as the current directory, if it contains any
other files besides Markdown files.\&
.PP
Furthermore, in-place editing changes the file for all names.\& Avoid editing the
hard-linked files (anything that'\&s not a HTML file) in the destination
directory, just to be on the safe side.\& Usually you should be fine, as an editor
moves the file that'\&s being edited to a backup file and creates a new file.\& But
then again, who knows.\& A SQLite file, for example, would change in-place, and
therefore making changes to it in the destination directory would change the
original, too.\&
.PP
.SH EXAMPLES
.PP
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
.PP
.nf
.RS 4
env ODDMU_LANGUAGES=de,en oddmu static \&.\&./archive
.fi
.RE
.PP
.SH LIMITATIONS
.PP
There can be nameclashes with generated HTML and RSS files and existing files
ending in ".\&html" and ".\&rss".\& Instead of overwriting existing files in these
cases, a warning is printed.\&
.PP
Links from files to pages do not get ".\&html" appended.\& This affects existing
HTML or XML files including SVG files.\&
.PP
Links to absolute URLs (starting with "/") are not changed at all.\& It is up to
you to migrate static folders and applications.\&
.PP
.SH ENVIRONMENT
.PP
The ODDMU_WEBFINGER environment variable has no effect in this situation.\&
Fediverse accounts are not linked to their profile pages.\& Since the data isn'\&t
cached, every run of this command would trigger a webfinger request for every
fediverse account mentioned.\&
.PP
If the site is large, determining the language of a page slows things down.\& Set
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
codes, e.\&g.\& "en" or "en,de,fr,pt" to limit the languages loaded and thereby
speed language determination up.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-templates\fR(5)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,87 +0,0 @@
ODDMU-STATIC(1)
# NAME
oddmu-static - create a static copy of the site
# SYNOPSIS
*oddmu static* _dir-name_
# DESCRIPTION
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory. Existing files are
only overwritten if they are older than the source file.
All pages (files with the ".md" extension) are turned into HTML files (with the
".html" extension) using the "static.html" template. Links pointing to existing
pages get ".html" appended.
If a page has a name case-insensitively matching a hashtag, a feed file is
generated (ending with ".rss") if any suitable links are found. A suitable link
for a feed item must appear in a bullet list item using an asterisk ("\*"). If
no feed items are found, no feed is written.
Hidden files and directories (starting with a ".") and backup files (ending with
a "~") are skipped.
All other files are _hard linked_. This is done to save space: on a typical blog
the images take a lot more space than the text. On my blog in 2023 I had 2.62
GiB of JPG files and 0.02 GiB of Markdown files. There is no point in copying
all those images, most of the time.
Note, however: Hard links cannot span filesystems. A hard link is just an extra
name for the same file. This is why the destination directory for the static
site has to be on same filesystem as the current directory, if it contains any
other files besides Markdown files.
Furthermore, in-place editing changes the file for all names. Avoid editing the
hard-linked files (anything that's not a HTML file) in the destination
directory, just to be on the safe side. Usually you should be fine, as an editor
moves the file that's being edited to a backup file and creates a new file. But
then again, who knows. A SQLite file, for example, would change in-place, and
therefore making changes to it in the destination directory would change the
original, too.
# EXAMPLES
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
```
env ODDMU_LANGUAGES=de,en oddmu static ../archive
```
# LIMITATIONS
There can be nameclashes with generated HTML and RSS files and existing files
ending in ".html" and ".rss". Instead of overwriting existing files in these
cases, a warning is printed.
Links from files to pages do not get ".html" appended. This affects existing
HTML or XML files including SVG files.
Links to absolute URLs (starting with "/") are not changed at all. It is up to
you to migrate static folders and applications.
# ENVIRONMENT
The ODDMU_WEBFINGER environment variable has no effect in this situation.
Fediverse accounts are not linked to their profile pages. Since the data isn't
cached, every run of this command would trigger a webfinger request for every
fediverse account mentioned.
If the site is large, determining the language of a page slows things down. Set
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
codes, e.g. "en" or "en,de,fr,pt" to limit the languages loaded and thereby
speed language determination up.
# SEE ALSO
_oddmu_(1), _oddmu-templates_(5)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,298 +1,133 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-TEMPLATES" "5" "2024-08-30" "File Formats Manual"
.TH "ODDMU-TEMPLATES" "5" "2023-09-22" "File Formats Manual"
.PP
.SH NAME
.PP
oddmu-templates - how to write the templates
.PP
.SH SYNOPSIS
.SH SYNTAX
.PP
Some HTML files act as templates.\& They contain special placeholders in double
bracers {{like this}}.\&
The templates can refer to the following properties of a page:
.PP
.SH DESCRIPTION
\fI{{.\&Title}}\fR is the page title.\& If the page doesn'\&t provide its own
title, the page name is used.\&
.PP
Each template receives an object and uses the object'\&s properties to replace the
placeholders.\&
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\& More
specifically, it is URI escaped except for the slashes.\& The page name
doesn'\&t include the \fI.\&md\fR extension.\&
.PP
.PD 0
.IP \(bu 4
\fIadd.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIdiff.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIedit.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIfeed.\&html\fR uses a \fIfeed\fR
.IP \(bu 4
\fIlist.\&html\fR uses a \fIlist\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
.IP \(bu 4
\fIupload.\&html\fR uses an \fIupload\fR
.IP \(bu 4
\fIview.\&html\fR uses a \fIpage\fR
.PD
For the \fIview.\&html\fR template:
.PP
.SS Page
.PP
A page has the following properties:
.PP
\fI{{.\&Title}}\fR is the page title.\& If the page doesn'\&t provide its own title, the
page name is used.\&
.PP
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\& More specifically, it is
percent-escaped except for the slashes.\& The page name doesn'\&t include the \fI.\&md\fR
extension.\&
.PP
\fI{{.\&Dir}}\fR is the page directory, percent-escaped except for the slashes.\&
.PP
\fI{{.\&Base}}\fR is the basename of the current file (without the directory and
without the \fI.\&md\fR extension), escaped for use in URLs.\&
.PP
\fI{{.\&Language}}\fR is the suspected language of the page.\& This is used to set the
language on the \fIview.\&html\fR template.\& See "Non-English hyphenation" below.\&
.PP
\fI{{.\&Body}}\fR is the raw byte content of the page.\& Use \fI{{printf "%s" .\&Body}}\fR to
get the Markdown, as a string.\& This is used for the text area of the \fIedit.\&html\fR
template.\&
\fI{{.\&Html}}\fR is the rendered Markdown, as HTML.\&
.PP
\fI{{.\&Hashtags}}\fR is an array of strings.\&
.PP
\fI{{.\&Html}}\fR contains some sort of HTML that depends on the template used.\&
For the \fIedit.\&html\fR template:
.PP
.PD 0
.IP \(bu 4
For \fIview.\&html\fR, it is the rendered Markdown, as HTML.\&
.IP \(bu 4
For \fIsearch.\&html\fR, it is a page summary, with bold matches, as HTML.\&
.IP \(bu 4
For \fIfeed.\&html\fR, it is the escaped (!\&) HTML of the feed item.\&
.PD
\fI{{printf "%s" .\&Body}}\fR is the Markdown, as a string (the data itself
is a byte array and that'\&s why we need to call \fIprintf\fR).\&
.PP
\fI{{.\&IsBlog}}\fR says whether the current page has a name starting with an ISO
date.\&
For the \fIsearch.\&html\fR template only:
.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.\&
.PP
.SS Feed
.PP
The feed contains an item for the head of the feed and an array of items.\&
.PP
\fI{{.\&Items}}\fR is the array of feed items.\& To refer to them, you need to use a
\fI{{range .\&Items}}\fR\fI{{end}}\fR construct.\&
.PP
If page A links to pages B and C, the head of the feed is based on page A and
the list of items contains B and C.\&
.PP
An item is a page plus a date.\& All the properties of a page can be used (see
\fBPage\fR above).\&
.PP
\fI{{.\&Date}}\fR is the date of the last update to the page, in RFC 822 format.\&
.PP
.SS List
.PP
The list contains a directory name and an array of files.\&
.PP
\fI{{.\&Dir}}\fR is the directory name that is being listed.\&
.PP
\fI{{.\&Files}}\fR is the array of files.\& To refer to them, you need to use a \fI{{range
Files}}\fR\fI{{end}}\fR construct.\&
.PP
Each file has the following attributes:
.PP
\fI{{.\&Name}}\fR is the filename.\& The ".\&md" suffix for Markdown files is part of the
name (unlike page names).\&
.PP
\fI{{.\&Title}}\fR is the page title, if the file in question is a Markdown file.\&
.PP
\fI{{.\&IsDir}}\fR is a boolean used to indicate that this file is a directory.\&
.PP
\fI{{.\&IsUp}}\fR is a boolean used to indicate the entry for the parent directory
(the first file in the array, unless the directory being listed is the top
directory).\& The filename of this file is ".\&.\&".\&
.PP
\fI{{.\&Date}}\fR is the last modification date of the file.\&
.PP
.SS Search
.PP
\fI{{.\&Query}}\fR is the query string.\&
.PP
\fI{{.\&Dir}}\fR is the directory in which the search starts, percent-escaped except
for the slashes.\&
.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
available.\&
\fI{{.\&Previous}}\fR, \fI{{.\&Page}}\fR, \fI{{.\&Next}}\fR and \fI{{.\&Last}}\fR are the
previous, current, next and last page number in the results since
doing arithmetics in templates is hard.\& The first page number is 1.\&
.PP
\fI{{.\&More}}\fR indicates if there are any more search results.\&
.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
\fI{{.\&Items}}\fR is an array of pages, each containing a search result.\& A
search result is a page (with the properties seen above).\& Thus, to
refer to them, you need to use a \fI{{range .\&Items}}\fR\fI{{end}}\fR
construct.\&
.PP
Each image has three properties:
For items in the search result:
.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.\&
\fI{{.\&Html}}\fR is the rendered Markdown of a page summary, as HTML.\&
.PP
\fI{{.\&Name}}\fR is the file name for use in URLs.\&
\fI{{.\&Score}}\fR is a numerical score for search results.\&
.PP
\fI{{.\&Html}}\fR the image alt-text with a bold tag used to highlight the first
search term that matched.\&
For the \fIfeed.\&html\fR template:
.PP
.SS Upload
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\&
.PP
\fI{{.\&Dir}}\fR is the directory where the uploaded file ends up, based on the URL
path, percent-escaped except for the slashes.\&
\fI{{.\&Title}}\fR is the title of the underlying main page.\&
.PP
\fI{{.\&Name}}\fR is the \fIfilename\fR query parameter.\&
\fI{{.\&Date}}\fR is the date of the last update to the underlying main page,
in RFC 822 format.\&
.PP
\fI{{.\&Last}}\fR is the filename of the last file uploaded.\&
\fI{{.\&Items}}\fR is an array of feed items.\&
.PP
\fI{{.\&Actual}}\fR is an array of filenames of all the files uploaded.\& Use {{range
Actual}} … {{.\&}} … {{end}} to loop over all the filenames.\&
For items in the feed:
.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.\&
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\&
.PP
\fI{{.\&Title}}\fR is the title of the basename, if it exists.\&
\fI{{.\&Title}}\fR is the title of the page.\&
.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
can be shown by the template, for example.\&
\fI{{.\&Html}}\fR is the rendered Markdown, as HTML.\&
.PP
\fI{{.\&MaxWidth}}\fR is the \fImaxwidth\fR query parameter, i.\&e.\& the value used for the
previous image uploaded.\&
\fI{{.\&Hashtags}}\fR is an array of strings.\&
.PP
\fI{{.\&Quality}}\fR is the \fIquality\fR query parameter, i.\&e.\& the value used for the
previous image uploaded.\&
\fI{{.\&Date}}\fR, the date of the last update to this page, in RFC 822
format.\&
.PP
\fI{{.\&Today}}\fR is the current date, in ISO format.\&
The \fIupload.\&html\fR template cannot refer to anything.\&
.PP
When calling the \fIsave\fR and \fIappend\fR action, the page name is taken
from the URL path and the page content is taken from the \fIbody\fR form
parameter.\& To illustrate, here'\&s how to edit the "welcome" page using
\fIcurl\fR:
.PP
.nf
.RS 4
curl --form body="Did you bring a towel?"
http://localhost:8080/save/welcome
.fi
.RE
.PP
When calling the \fIsearch\fR action, the query is taken from the URL
parameter \fIq\fR.\&
.PP
.nf
.RS 4
curl http://localhost:8080/search?q=towel
.fi
.RE
.PP
.SS Non-English hyphenation
.PP
Automatic hyphenation by the browser requires two things: The style sheet must
indicate "hyphen: auto" for an HTML element such as "body", and that element
must have a "lang" set (usually a two letter language code such as "de" for
German).\&
Automatic hyphenation by the browser requires two things: The style
sheet must indicate "hyphen: auto" for an HTML element such as "body",
and that element must have a "lang" set (usually a two letter language
code such as "de" for German).\&
.PP
Oddmu attempts to detect the correct language for each page.\& It assumes that
languages are not mixed on the same page.\& If you know that you'\&re only going to
use a small number of languages or just a single language!\& you can set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.\&g.\& "en" or "en,de,fr,pt".\&
Oddmu attempts to detect the correct language for each page.\& It
assumes that languages are not mixed on the same page.\& If you know
that you'\&re only going to use a small number of languages or just a
single language!\& you can set the environment variable
ODDMU_LANGUAGES to a comma-separated list of ISO 639-1 codes, e.\&g.\&
"en" or "en,de,fr,pt".\&
.PP
"view.\&html" is used the template to render a single page and so the language
detected is added to the "html" element.\&
"view.\&html" is used the template to render a single page and so the
language detected is added to the "html" element.\&
.PP
"search.\&html" is the template used to render search results and so "en" is used
for the "html" element and the language detected for every page in the search
result is added to the "article" element for each snippet.\&
"search.\&html" is the template used to render search results and so
"en" is used for the "html" element and the language detected for
every page in the search result is added to the "article" element for
each snippet.\&
.PP
"edit.\&html" and "add.\&html" are the templates used to edit a page and at that
point, the language isn'\&t known, so "en" is used for the "html" element and no
language is used for the "textarea" element.\&
"edit.\&html" and "add.\&html" are the templates used to edit a page and
at that point, the language isn'\&t known, so "en" is used for the
"html" element and no language is used for the "textarea" element.\&
.PP
.SH EXAMPLES
.PP
The following link in a template takes people to today'\&s page.\& If no such page
exists, they are redirected to the edit form where it can be created.\&
.PP
.nf
.RS 4
<a href="/view/{{\&.Today}}" accesskey="t">Today</a>
.fi
.RE
.PP
The following form allows people to edit the suggested page name.\&
.PP
.nf
.RS 4
<form role="new" action="/edit/{{\&.Dir}}" method="GET">
<label for="id">New page:</label>
<input id="id" type="text" spellcheck="false" name="id"
accesskey="g" value="{{\&.Today}}" required>
<button>Edit</button>
</form>
.fi
.RE
.PP
The following puts the current date into the text area if and only if the page
itself is a blog page.\& Useful for \fIadd.\&html\fR:
.PP
.nf
.RS 4
<textarea name="body" rows="20" cols="80" placeholder="Text" lang=""
autofocus required>{{- if \&.IsBlog}}**{{\&.Today}}**\&. {{end}}</textarea>
.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.\&
Therefore, a link to a specific page must be \fIabsolute\fR or it'\&ll point to a
different page depending on the current directory.\&
.PP
Consider the link to "/view/index".\& No matter what page a visitor is looking,
this takes visitors to the top "index" page.\& If the link points to "index"
instead, it takes a visitor to the "index" page of the current directory.\& In
this case, a visitor looking at "/view/projects/wiki" following a link to
"index" ends up on "/view/projects/index", not on "/view/index".\&
.PP
It'\&s up to you to decide what'\&s best for your site, of course.\&
.PP
If you want a link on \fIupload.\&html\fR to point to the current directory'\&s "index"
page, you need to use "/view/{{.\&Dir}}index" because if you link to "index" the
result points to "/upload/{{.\&Dir}}index".\&
.PP
Templates can be changed by uploading new copies of the template files.\&
.PP
Subdirectories can have their own copies of template files.\& One example use for
this is that they can point to a different CSS file.\&
.PP
.SH SEE ALSO
SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
@@ -302,15 +137,16 @@ https://developer.\&mozilla.\&org/en-US/docs/Learn/HTML
"Learn to style HTML using CSS"
https://developer.\&mozilla.\&org/en-US/docs/Learn/CSS
.PP
The "text/template" library explains how to write templates from a programmer
perspective.\& https://pkg.\&go.\&dev/text/template
The "text/template" library explains how to write templates from a
programmer perspective.\& https://pkg.\&go.\&dev/text/template
.PP
The "html/template" library explains how the templates are made more secure in a
HTML context.\& https://pkg.\&go.\&dev/html/template
The "html/template" library explains how the templates are made more
secure in a HTML context.\& https://pkg.\&go.\&dev/html/template
.PP
"Lingua" is the library used to detect languages.\&
https://github.\&com/pemistahl/lingua-go
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
Maintained by Alex Schroeder <alex@gnu.\&org>.\& Up-to-date sources can be
found at https://alexschroeder.\&ch/cgit/oddmu/.\&

View File

@@ -4,263 +4,119 @@ ODDMU-TEMPLATES(5) "File Formats Manual"
oddmu-templates - how to write the templates
# SYNOPSIS
# SYNTAX
Some HTML files act as templates. They contain special placeholders in double
bracers {{like this}}.
The templates can refer to the following properties of a page:
# DESCRIPTION
_{{.Title}}_ is the page title. If the page doesn't provide its own
title, the page name is used.
Each template receives an object and uses the object's properties to replace the
placeholders.
_{{.Name}}_ is the page name, escaped for use in URLs. More
specifically, it is URI escaped except for the slashes. The page name
doesn't include the _.md_ extension.
- _add.html_ uses a _page_
- _diff.html_ uses a _page_
- _edit.html_ uses a _page_
- _feed.html_ uses a _feed_
- _list.html_ uses a _list_
- _preview.html_ uses a _page_
- _search.html_ uses a _search_
- _static.html_ uses a _page_
- _upload.html_ uses an _upload_
- _view.html_ uses a _page_
For the _view.html_ template:
## Page
A page has the following properties:
_{{.Title}}_ is the page title. If the page doesn't provide its own title, the
page name is used.
_{{.Name}}_ is the page name, escaped for use in URLs. More specifically, it is
percent-escaped except for the slashes. The page name doesn't include the _.md_
extension.
_{{.Dir}}_ is the page directory, percent-escaped except for the slashes.
_{{.Base}}_ is the basename of the current file (without the directory and
without the _.md_ extension), escaped for use in URLs.
_{{.Language}}_ is the suspected language of the page. This is used to set the
language on the _view.html_ template. See "Non-English hyphenation" below.
_{{.Body}}_ is the raw byte content of the page. Use _{{printf "%s" .Body}}_ to
get the Markdown, as a string. This is used for the text area of the _edit.html_
template.
_{{.Html}}_ is the rendered Markdown, as HTML.
_{{.Hashtags}}_ is an array of strings.
_{{.Html}}_ contains some sort of HTML that depends on the template used.
For the _edit.html_ template:
- For _view.html_, it is the rendered Markdown, as HTML.
- 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.
_{{printf "%s" .Body}}_ is the Markdown, as a string (the data itself
is a byte array and that's why we need to call _printf_).
_{{.IsBlog}}_ says whether the current page has a name starting with an ISO
date.
For the _search.html_ template only:
_{{.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.
## Feed
The feed contains an item for the head of the feed and an array of items.
_{{.Items}}_ is the array of feed items. To refer to them, you need to use a
_{{range .Items}}_ … _{{end}}_ construct.
If page A links to pages B and C, the head of the feed is based on page A and
the list of items contains B and C.
An item is a page plus a date. All the properties of a page can be used (see
*Page* above).
_{{.Date}}_ is the date of the last update to the page, in RFC 822 format.
## List
The list contains a directory name and an array of files.
_{{.Dir}}_ is the directory name that is being listed.
_{{.Files}}_ is the array of files. To refer to them, you need to use a _{{range
.Files}}_ … _{{end}}_ construct.
Each file has the following attributes:
_{{.Name}}_ is the filename. The ".md" suffix for Markdown files is part of the
name (unlike page names).
_{{.Title}}_ is the page title, if the file in question is a Markdown file.
_{{.IsDir}}_ is a boolean used to indicate that this file is a directory.
_{{.IsUp}}_ is a boolean used to indicate the entry for the parent directory
(the first file in the array, unless the directory being listed is the top
directory). The filename of this file is "..".
_{{.Date}}_ is the last modification date of the file.
## Search
_{{.Query}}_ is the query string.
_{{.Dir}}_ is the directory in which the search starts, percent-escaped except
for the slashes.
_{{.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
available.
_{{.Previous}}_, _{{.Page}}_, _{{.Next}}_ and _{{.Last}}_ are the
previous, current, next and last page number in the results since
doing arithmetics in templates is hard. The first page number is 1.
_{{.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}}_
_{{.Items}}_ is an array of pages, each containing a search result. A
search result is a page (with the properties seen above). Thus, to
refer to them, you need to use a _{{range .Items}}_ … _{{end}}_
construct.
Each image has three properties:
For items in the search result:
_{{.Title}}_ is the alt-text of the image. It can never be empty because images
are only listed if a search term matches.
_{{.Html}}_ is the rendered Markdown of a page summary, as HTML.
_{{.Name}}_ is the file name for use in URLs.
_{{.Score}}_ is a numerical score for search results.
_{{.Html}}_ the image alt-text with a bold tag used to highlight the first
search term that matched.
For the _feed.html_ template:
## Upload
_{{.Name}}_ is the page name, escaped for use in URLs.
_{{.Dir}}_ is the directory where the uploaded file ends up, based on the URL
path, percent-escaped except for the slashes.
_{{.Title}}_ is the title of the underlying main page.
_{{.Name}}_ is the _filename_ query parameter.
_{{.Date}}_ is the date of the last update to the underlying main page,
in RFC 822 format.
_{{.Last}}_ is the filename of the last file uploaded.
_{{.Items}}_ is an array of feed items.
_{{.Actual}}_ is an array of filenames of all the files uploaded. Use {{range
.Actual}} … {{.}} … {{end}} to loop over all the filenames.
For items in the feed:
_{{.Base}}_ is the basename of the first file uploaded (without the directory,
extension and numeric part at the end), escaped for use in URLs.
_{{.Name}}_ is the page name, escaped for use in URLs.
_{{.Title}}_ is the title of the basename, if it exists.
_{{.Title}}_ is the title of the page.
_{{.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
can be shown by the template, for example.
_{{.Html}}_ is the rendered Markdown, as escaped (!) HTML.
_{{.MaxWidth}}_ is the _maxwidth_ query parameter, i.e. the value used for the
previous image uploaded.
_{{.Hashtags}}_ is an array of strings.
_{{.Quality}}_ is the _quality_ query parameter, i.e. the value used for the
previous image uploaded.
_{{.Date}}_, the date of the last update to this page, in RFC 822
format.
_{{.Today}}_ is the current date, in ISO format.
The _upload.html_ template cannot refer to anything.
When calling the _save_ and _append_ action, the page name is taken
from the URL path and the page content is taken from the _body_ form
parameter. To illustrate, here's how to edit the "welcome" page using
_curl_:
```
curl --form body="Did you bring a towel?" \
http://localhost:8080/save/welcome
```
When calling the _search_ action, the query is taken from the URL
parameter _q_.
```
curl http://localhost:8080/search?q=towel
```
## Non-English hyphenation
Automatic hyphenation by the browser requires two things: The style sheet must
indicate "hyphen: auto" for an HTML element such as "body", and that element
must have a "lang" set (usually a two letter language code such as "de" for
German).
Automatic hyphenation by the browser requires two things: The style
sheet must indicate "hyphen: auto" for an HTML element such as "body",
and that element must have a "lang" set (usually a two letter language
code such as "de" for German).
Oddmu attempts to detect the correct language for each page. It assumes that
languages are not mixed on the same page. If you know that you're only going to
use a small number of languages or just a single language! you can set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.g. "en" or "en,de,fr,pt".
Oddmu attempts to detect the correct language for each page. It
assumes that languages are not mixed on the same page. If you know
that you're only going to use a small number of languages or just a
single language! you can set the environment variable
ODDMU_LANGUAGES to a comma-separated list of ISO 639-1 codes, e.g.
"en" or "en,de,fr,pt".
"view.html" is used the template to render a single page and so the language
detected is added to the "html" element.
"view.html" is used the template to render a single page and so the
language detected is added to the "html" element.
"search.html" is the template used to render search results and so "en" is used
for the "html" element and the language detected for every page in the search
result is added to the "article" element for each snippet.
"search.html" is the template used to render search results and so
"en" is used for the "html" element and the language detected for
every page in the search result is added to the "article" element for
each snippet.
"edit.html" and "add.html" are the templates used to edit a page and at that
point, the language isn't known, so "en" is used for the "html" element and no
language is used for the "textarea" element.
"edit.html" and "add.html" are the templates used to edit a page and
at that point, the language isn't known, so "en" is used for the
"html" element and no language is used for the "textarea" element.
# EXAMPLES
The following link in a template takes people to today's page. If no such page
exists, they are redirected to the edit form where it can be created.
```
<a href="/view/{{.Today}}" accesskey="t">Today</a>
```
The following form allows people to edit the suggested page name.
```
<form role="new" action="/edit/{{.Dir}}" method="GET">
<label for="id">New page:</label>
<input id="id" type="text" spellcheck="false" name="id"
accesskey="g" value="{{.Today}}" required>
<button>Edit</button>
</form>
```
The following puts the current date into the text area if and only if the page
itself is a blog page. Useful for _add.html_:
```
<textarea name="body" rows="20" cols="80" placeholder="Text" lang=""
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.
Therefore, a link to a specific page must be _absolute_ or it'll point to a
different page depending on the current directory.
Consider the link to "/view/index". No matter what page a visitor is looking,
this takes visitors to the top "index" page. If the link points to "index"
instead, it takes a visitor to the "index" page of the current directory. In
this case, a visitor looking at "/view/projects/wiki" following a link to
"index" ends up on "/view/projects/index", not on "/view/index".
It's up to you to decide what's best for your site, of course.
If you want a link on _upload.html_ to point to the current directory's "index"
page, you need to use "/view/{{.Dir}}index" because if you link to "index" the
result points to "/upload/{{.Dir}}index".
Templates can be changed by uploading new copies of the template files.
Subdirectories can have their own copies of template files. One example use for
this is that they can point to a different CSS file.
# SEE ALSO
SEE ALSO
_oddmu_(1)
@@ -270,15 +126,16 @@ https://developer.mozilla.org/en-US/docs/Learn/HTML
"Learn to style HTML using CSS"
https://developer.mozilla.org/en-US/docs/Learn/CSS
The "text/template" library explains how to write templates from a programmer
perspective. https://pkg.go.dev/text/template
The "text/template" library explains how to write templates from a
programmer perspective. https://pkg.go.dev/text/template
The "html/template" library explains how the templates are made more secure in a
HTML context. https://pkg.go.dev/html/template
The "html/template" library explains how the templates are made more
secure in a HTML context. https://pkg.go.dev/html/template
"Lingua" is the library used to detect languages.
https://github.com/pemistahl/lingua-go
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.
Maintained by Alex Schroeder <alex@gnu.org>. Up-to-date sources can be
found at https://alexschroeder.ch/cgit/oddmu/.

View File

@@ -1,33 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-TOC" "1" "2024-08-15"
.PP
.SH NAME
.PP
oddmu-toc - print the table of contents (toc) for pages
.PP
.SH SYNOPSIS
.PP
\fBoddmu toc\fR \fIpage names.\&.\&.\&\fR
.PP
.SH DESCRIPTION
.PP
The "toc" subcommand prints the table of contents for one or more page
names.\& Use "-" as the page name if you want to read Markdown from
\fBstdin\fR.\&
.PP
This can be useful for very long pages that need a table of contents
at the beginning.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,26 +0,0 @@
ODDMU-TOC(1)
# NAME
oddmu-toc - print the table of contents (toc) for pages
# SYNOPSIS
*oddmu toc* _page names..._
# DESCRIPTION
The "toc" subcommand prints the table of contents for one or more page
names. Use "-" as the page name if you want to read Markdown from
*stdin*.
This can be useful for very long pages that need a table of contents
at the beginning.
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,38 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-VERSION" "1" "2024-02-23"
.PP
.SH NAME
.PP
oddmu-version - print build info on the command-line
.PP
.SH SYNOPSIS
.PP
\fBoddmu version\fR [-full]
.PP
.SH DESCRIPTION
.PP
The "version" subcommand prints information related to the version control
system state when it was built: what remote was used, what commit was checked
out, whether there were any local changes were made.\&
.PP
.SH OPTIONS
.PP
\fB-full\fR
.RS 4
Print a lot more information, including the versions of dependencies
used.\& It'\&s the equivalent of running "go version -m oddmu".\&
.PP
.RE
.SH SEE ALSO
.PP
\fIoddmu\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,29 +0,0 @@
ODDMU-VERSION(1)
# NAME
oddmu-version - print build info on the command-line
# SYNOPSIS
*oddmu version* [-full]
# DESCRIPTION
The "version" subcommand prints information related to the version control
system state when it was built: what remote was used, what commit was checked
out, whether there were any local changes were made.
# OPTIONS
*-full*
Print a lot more information, including the versions of dependencies
used. It's the equivalent of running "go version -m oddmu".
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,188 +0,0 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-WEBDAV" "5" "2024-09-25"
.PP
.SH NAME
.PP
oddmu-webdav - how to setup Web-DAV using Apache for Oddmu
.PP
.SH DESCRIPTION
.PP
With the Apache Web-DAV module enabled, users can mount the wiki as a remote
file system and edit files using their favourite text editor.\& If you want to
offer users direct file access to the wiki, this can be accomplished via ssh,
sftp or Web-DAV.\&
.PP
The benefit of using the Apache Web-DAV module is that access has to be
configured only once.\&
.PP
.SH CONFIGURATION
.PP
In the following example, "data" is not an action provided by Oddmu but an
actual directory for Oddmu files.\& In the example below,
"/home/alex/campaignwiki.\&org/data" is both the document root for static files
and the data directory for Oddmu.\& This is the directory where Oddmu needs to
run.\& When users request the "/data" path, authentication is required but the
request is not proxied to Oddmu since the "ProxyPassMatch" directive doesn'\&t
handle "/data".\& Instead, Apache gets to handle it.\& Since "data" is part of all
the "LocationMatch" directives, credentials are required to save (PUT) files.\&
.PP
"Dav On" enables Web-DAV for the "knochentanz" wiki.\& It is enabled for all the
actions, but since only "/data" is handled by Apache, this has no effect for all
the other actions, allowing us to specify the required users only once.\&
.PP
.nf
.RS 4
MDomain campaignwiki\&.org
<VirtualHost *:80>
ServerName campaignwiki\&.org
Redirect permanent / https://campaignwiki\&.org/
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@campaignwiki\&.org
ServerName campaignwiki\&.org
DocumentRoot /home/alex/campaignwiki\&.org
<Directory /home/alex/campaignwiki\&.org>
Options Includes Indexes MultiViews SymLinksIfOwnerMatch
AllowOverride All
Require all granted
</Directory>
SSLEngine on
ProxyPassMatch
"^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|search|archive/\&.+)/(\&.*))$"
"unix:/home/oddmu/campaignwiki\&.sock|http://localhost/$1"
# /archive only for subdirectories
Redirect "/archive/data\&.zip" "/view/archive"
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|list|delete)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd
Require user admin alex
</LocationMatch>
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|list|delete|archive)/knochentanz">
Require user admin alex knochentanz
Dav On
</LocationMatch>
</VirtualHost>
.fi
.RE
.PP
In order for this to work, you must enable the mod_dav_fs module.\& This
automatically enables to the mod_dav module, too.\& Restart the server after
installing enabling a module.\&
.PP
.nf
.RS 4
sudo a2enmod mod_dav_fs
sudo apachectl restart
.fi
.RE
.PP
Check the permissions for the data directory.\& If the Oddmu service uses the
"oddmu" user and Apache uses the "www-data" user, you could add the data
directory to the "www-data" group and give it write permissions:
.PP
.nf
.RS 4
sudo chown oddmu:www-data /home/alex/campaignwiki\&.org/data/knochentanz
sudo chmod g+w /home/alex/campaignwiki\&.org/data/knochentanz
.fi
.RE
.PP
.SH EXAMPLES
.PP
Web-DAV clients are often implemented such that they only work with servers that
exactly match their assumptions.\& If you'\&re trying to use \fIgvfs\fR(7), the Windows
File Explorer or the macOS Finder to edit Oddmu pages using Web-DAV, you'\&re on
your own.\&
.PP
This section has examples sessions using tools that work.\&
.PP
.SS cadaver
.PP
Here'\&s how to use \fIcadaver\fR(1).\& The "edit" command uses the editor specified in
the EDITOR environment variable.\& In this example, that'\&s
"emacsclient --alternate-editor= ".\&
.PP
.nf
.RS 4
cadaver https://campaignwiki\&.org/data/knochentanz/
Authentication required for Password Required on server `campaignwiki\&.org\&':
Username: knochentanz
Password:
dav:/data/knochentanz/> edit index\&.md
Locking `index\&.md\&': succeeded\&.
Downloading `/data/knochentanz/index\&.md\&' to /tmp/cadaver-edit-fHTllt\&.md
Progress: [=============================>] 100\&.0% of 2725 bytes succeeded\&.
Running editor: `emacsclient --alternate-editor= /tmp/cadaver-edit-fHTllt\&.md\&'\&.\&.\&.
Waiting for Emacs\&.\&.\&.
Changes were made\&.
Uploading changes to `/data/knochentanz/index\&.md\&'
Progress: [=============================>] 100\&.0% of 2726 bytes succeeded\&.
Unlocking `index\&.md\&': succeeded\&.
.fi
.RE
.PP
.SS curl and hdav
.PP
Here'\&s how to use \fIcurl\fR(1) to get the file from the public "/view" location and
how to use \fIhdav\fR(1) to put the file to the protected "/data" location.\& In this
example, \fIed\fR(1) is used to append the word "test" to the file.\&
.PP
.nf
.RS 4
alex@melanobombus ~> curl --output index\&.md https://campaignwiki\&.org/view/knochentanz/index\&.md
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2726 100 2726 0 0 36662 0 --:--:-- --:--:-- --:--:-- 37861
alex@melanobombus ~> ed index\&.md
2726
a
test
\&.
w
2731
q
alex@melanobombus ~> hdav put index\&.md https://campaignwiki\&.org/data/knochentanz/index\&.md --username knochentanz
hDAV version 1\&.3\&.4, Copyright (C) 2012-2016 Clint Adams
hDAV comes with ABSOLUTELY NO WARRANTY\&.
This is free software, and you are welcome to redistribute it
under certain conditions\&.
Password for knochentanz at URL https://campaignwiki\&.org/data/knochentanz/index\&.md: ********
.fi
.RE
.PP
.SS davfs2
.PP
Here'\&s how to use \fIdavfs2\fR(1) using \fImount\fR(1).\& Now the whole wiki is mounted
and can be edited like local files.\& In this example, \fIecho\fR(1) and redirection
is used to append the word "test" to a file.\&
.PP
.nf
.RS 4
alex@melanobombus ~> mkdir knochentanz
alex@melanobombus ~> sudo mount -t davfs -o username=knochentanz,uid=alex
https://campaignwiki\&.org/data/knochentanz/ knochentanz/
Password: ********
alex@melanobombus ~> echo test >> knochentanz/index\&.md
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-apache\fR(5)
.PP
"Apache Module mod_dav".\&
https://httpd.\&apache.\&org/docs/current/mod/mod_dav.\&html
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -1,169 +0,0 @@
ODDMU-WEBDAV(5)
# NAME
oddmu-webdav - how to setup Web-DAV using Apache for Oddmu
# DESCRIPTION
With the Apache Web-DAV module enabled, users can mount the wiki as a remote
file system and edit files using their favourite text editor. If you want to
offer users direct file access to the wiki, this can be accomplished via ssh,
sftp or Web-DAV.
The benefit of using the Apache Web-DAV module is that access has to be
configured only once.
# CONFIGURATION
In the following example, "data" is not an action provided by Oddmu but an
actual directory for Oddmu files. In the example below,
"/home/alex/campaignwiki.org/data" is both the document root for static files
and the data directory for Oddmu. This is the directory where Oddmu needs to
run. When users request the "/data" path, authentication is required but the
request is not proxied to Oddmu since the "ProxyPassMatch" directive doesn't
handle "/data". Instead, Apache gets to handle it. Since "data" is part of all
the "LocationMatch" directives, credentials are required to save (PUT) files.
"Dav On" enables Web-DAV for the "knochentanz" wiki. It is enabled for all the
actions, but since only "/data" is handled by Apache, this has no effect for all
the other actions, allowing us to specify the required users only once.
```
MDomain campaignwiki.org
<VirtualHost *:80>
ServerName campaignwiki.org
Redirect permanent / https://campaignwiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@campaignwiki.org
ServerName campaignwiki.org
DocumentRoot /home/alex/campaignwiki.org
<Directory /home/alex/campaignwiki.org>
Options Includes Indexes MultiViews SymLinksIfOwnerMatch
AllowOverride All
Require all granted
</Directory>
SSLEngine on
ProxyPassMatch \
"^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|search|archive/.+)/(.*))$" \
"unix:/home/oddmu/campaignwiki.sock|http://localhost/$1"
# /archive only for subdirectories
Redirect "/archive/data.zip" "/view/archive"
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|list|delete)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
Require user admin alex
</LocationMatch>
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|list|delete|archive)/knochentanz">
Require user admin alex knochentanz
Dav On
</LocationMatch>
</VirtualHost>
```
In order for this to work, you must enable the mod_dav_fs module. This
automatically enables to the mod_dav module, too. Restart the server after
installing enabling a module.
```
sudo a2enmod mod_dav_fs
sudo apachectl restart
```
Check the permissions for the data directory. If the Oddmu service uses the
"oddmu" user and Apache uses the "www-data" user, you could add the data
directory to the "www-data" group and give it write permissions:
```
sudo chown oddmu:www-data /home/alex/campaignwiki.org/data/knochentanz
sudo chmod g+w /home/alex/campaignwiki.org/data/knochentanz
```
# EXAMPLES
Web-DAV clients are often implemented such that they only work with servers that
exactly match their assumptions. If you're trying to use _gvfs_(7), the Windows
File Explorer or the macOS Finder to edit Oddmu pages using Web-DAV, you're on
your own.
This section has examples sessions using tools that work.
## cadaver
Here's how to use _cadaver_(1). The "edit" command uses the editor specified in
the EDITOR environment variable. In this example, that's
"emacsclient --alternate-editor= ".
```
cadaver https://campaignwiki.org/data/knochentanz/
Authentication required for Password Required on server `campaignwiki.org':
Username: knochentanz
Password:
dav:/data/knochentanz/> edit index.md
Locking `index.md': succeeded.
Downloading `/data/knochentanz/index.md' to /tmp/cadaver-edit-fHTllt.md
Progress: [=============================>] 100.0% of 2725 bytes succeeded.
Running editor: `emacsclient --alternate-editor= /tmp/cadaver-edit-fHTllt.md'...
Waiting for Emacs...
Changes were made.
Uploading changes to `/data/knochentanz/index.md'
Progress: [=============================>] 100.0% of 2726 bytes succeeded.
Unlocking `index.md': succeeded.
```
## curl and hdav
Here's how to use _curl_(1) to get the file from the public "/view" location and
how to use _hdav_(1) to put the file to the protected "/data" location. In this
example, _ed_(1) is used to append the word "test" to the file.
```
alex@melanobombus ~> curl --output index.md https://campaignwiki.org/view/knochentanz/index.md
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2726 100 2726 0 0 36662 0 --:--:-- --:--:-- --:--:-- 37861
alex@melanobombus ~> ed index.md
2726
a
test
.
w
2731
q
alex@melanobombus ~> hdav put index.md https://campaignwiki.org/data/knochentanz/index.md --username knochentanz
hDAV version 1.3.4, Copyright (C) 2012-2016 Clint Adams
hDAV comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions.
Password for knochentanz at URL https://campaignwiki.org/data/knochentanz/index.md: ********
```
## davfs2
Here's how to use _davfs2_(1) using _mount_(1). Now the whole wiki is mounted
and can be edited like local files. In this example, _echo_(1) and redirection
is used to append the word "test" to a file.
```
alex@melanobombus ~> mkdir knochentanz
alex@melanobombus ~> sudo mount -t davfs -o username=knochentanz,uid=alex \
https://campaignwiki.org/data/knochentanz/ knochentanz/
Password: ********
alex@melanobombus ~> echo test >> knochentanz/index.md
```
# SEE ALSO
_oddmu_(1), _oddmu-apache_(5)
"Apache Module mod_dav".
https://httpd.apache.org/docs/current/mod/mod_dav.html
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -1,11 +1,11 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-09-25"
.TH "ODDMU" "1" "2023-09-22"
.PP
.SH NAME
.PP
@@ -17,143 +17,38 @@ Oddmu is sometimes written Oddµ because µ is the letter mu.\&
.PP
\fBoddmu\fR
.PP
\fBoddmu\fR \fIsubcommand\fR [\fIarguments\fR.\&.\&.\&]
.PP
.SH DESCRIPTION
.PP
Oddmu can be used as a static site generator, turning Markdown files into HTML
files, or it can be used as a public or a private wiki server.\& If it runs as a
public wiki server, a regular webserver should be used as reverse proxy.\&
The oddmu program serves the current working directory as a wiki on port 8080.\&
Point your browser to http://localhost:8080/ to get started.\& This is equivalent
to http://localhost:8080/view/index the first page you'\&ll create, most likely.\&
.PP
Run Oddmu without any arguments to serve the current working directory as a wiki
on port 8080.\& Point your browser to http://localhost:8080/ to use it.\& This
redirects you to http://localhost:8080/view/index the first page you'\&ll
create, most likely.\&
.PP
See \fIoddmu\fR(5) for details about the page formatting.\&
.PP
If you request a page that doesn'\&t exist, Oddmu tries to find a matching
If you request a page that doesn'\&t exist, oddmu tries to find a matching
Markdown file by appending the extension ".\&md" to the page name.\& In the example
above, the page name requested is "index" and the file name Oddmu tries to read
is "index.\&md".\& If no such file exists, Oddmu offers you to create the page.\&
above, the page name requested is "index" and the file name oddmu tries to read
is "index.\&md".\& If no such file exists, oddmu offers you to create the page.\&
.PP
If your files don'\&t provide their own title ("# title"), the file name (without
".\&md") is used for the page title.\&
.PP
Every file can be viewed as feed by using the extension ".\&rss".\& The
feed items are based on links in bullet lists using the asterix
("*").\&
.PP
Subdirectories are created as necessary.\&
.PP
The wiki knows the following actions for a given page name and (optional)
directory:
.PP
.PD 0
.IP \(bu 4
\fI/\fR redirects to /view/index
.IP \(bu 4
\fI/view/dir/\fR redirects to /view/dir/index
.IP \(bu 4
\fI/view/dir/name\fR shows a page
.IP \(bu 4
\fI/view/dir/name.\&md\fR shows the source text of a page
.IP \(bu 4
\fI/view/dir/name.\&rss\fR shows the RSS feed for the pages linked
.IP \(bu 4
\fI/diff/dir/name\fR shows the last change to a page
.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
.IP \(bu 4
\fI/append/dir/name\fR appends an addition to a page
.IP \(bu 4
\fI/upload/dir/name\fR shows a form to upload a file
.IP \(bu 4
\fI/drop/dir/name\fR saves an upload
.IP \(bu 4
\fI/list/dir/\fR lists the files in a directory
.IP \(bu 4
\fI/delete/dir/name\fR deletes a file or directory
.IP \(bu 4
\fI/rename/dir/name?\&name=new\fR renames a file or directory
.IP \(bu 4
\fI/search/dir/?\&q=term\fR to search for a term
.IP \(bu 4
\fI/archive/dir/name.\&zip\fR to download a zip file of a directory
.PD
.PP
When calling the \fIsave\fR and \fIappend\fR action, the page name is taken from the URL
path and the page content is taken from the \fIbody\fR form parameter.\& To
illustrate, here'\&s how to edit the "welcome" page using \fIcurl\fR:
.PP
.nf
.RS 4
curl --form body="Did you bring a towel?"
http://localhost:8080/save/welcome
.fi
.RE
.PP
When calling the \fIdrop\fR action, the query parameters used are \fIname\fR for the
target filename and \fIfile\fR for the file to upload.\& If the query parameter
\fImaxwidth\fR is set, an attempt is made to decode and resize the image.\& JPG, PNG,
WEBP and HEIC files can be decoded.\& Only JPG and PNG files can be encoded,
however.\& If the target name ends in \fI.\&jpg\fR, the \fIquality\fR query parameter is
also taken into account.\& To upload some thumbnails:
.PP
.nf
.RS 4
for f in *\&.jpg; do
curl --form name="$f" --form file=@"$f" --form maxwidth=100
http://localhost:8080/drop/
done
.fi
.RE
.PP
When calling the \fIsearch\fR action, the search terms are taken from the query
parameter \fIq\fR.\&
.PP
.nf
.RS 4
curl \&'http://localhost:8080/search/?q=towel\&'
.fi
.RE
.PP
The page name to act upon is optionally taken from the query parameter \fIid\fR.\& In
this case, the directory must also be part of the query parameter and not of the
URL path.\&
.PP
.nf
.RS 4
curl \&'http://localhost:8080/view/?id=man/oddmu\&.1\&.txt\&'
.fi
.RE
.PP
The base name for the \fIarchive\fR action is used by the browser to save the
downloaded file.\& For Oddmu, only the directory is important.\& The following zips
the \fIman\fR directory and saves it as \fIman.\&zip\fR.\&
.PP
.nf
.RS 4
curl --remote-name \&'http://localhost:8080/archive/man/man\&.zip
.fi
.RE
See \fIoddmu\fR(5) for details about the page formatting.\&
.PP
.SH CONFIGURATION
.PP
The template files are the HTML files in the working directory.\& Please change
these templates!\&
The template files are the HTML files in the working directory: "add.\&html",
"edit.\&html", "search.\&html", "upload.\&html" and "view.\&html".\& Feel free to change
the templates and restart the server.\&
.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".\&
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".\&
.PP
The second change you should make is to replace the name, email address and
domain name in "feed.\&html".\& Look for "Your Name" and "example.\&org".\&
The second change you should make is to replace the name, email
address and domain name in "feed.\&html".\& Look for "Your Name" and
"example.\&org".\& This second template is used to generate the RSS feeds
(despite it'\&s ".\&html" extension).\&
.PP
See \fIoddmu-templates\fR(5) for more.\&
.PP
@@ -161,21 +56,6 @@ See \fIoddmu-templates\fR(5) for more.\&
.PP
You can change the port served by setting the ODDMU_PORT environment variable.\&
.PP
You can change the address served by setting the ODDMU_ADDRESS environment
variable to either an IPv4 address or an IPv6 address.\& If ODDMU_ADDRESS is
unset, then the program listens on all available unicast addresses, both IPv4
and IPv6.\& Here are a few example addresses:
.PP
.nf
.RS 4
ODDMU_ADDRESS=127\&.0\&.0\&.1 # The loopback IPv4 address\&.
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address\&.
.fi
.RE
.PP
See the Socket Activation section for an alternative method of listening which
supports Unix-domain sockets.\&
.PP
In order to limit language-detection to the languages you actually use, set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.\&g.\& "en" or "en,de,fr,pt".\&
@@ -183,75 +63,35 @@ codes, e.\&g.\& "en" or "en,de,fr,pt".\&
You can enable webfinger to link fediverse accounts to their correct profile
pages by setting ODDMU_WEBFINGER to "1".\& See \fIoddmu\fR(5).\&
.PP
If you use secret subdirectories, you cannot rely on the web server to hide
those pages because some actions such as searching and archiving include
subdirectories.\& They act upon a whole tree of pages, not just a single page.\& The
ODDMU_FILTER can be used to exclude subdirectories from such tree actions.\& See
\fIoddmu-filter\fR(7) and \fIoddmu-apache\fR(5).\&
.PP
.SH Socket Activation
.PP
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),
\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.\& See \fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for
example configurations.\&
server as a reverse proxy.\&
.PP
Oddmu assumes that all the users that can edit pages or upload files are trusted
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.\&
See \fIoddmu-apache\fR(5) for an example.\&
.PP
.SH OPTIONS
.PP
Oddmu can be run on the command-line using various subcommands.\&
The oddmu program can be run on the command-line using various subcommands.\&
.PP
.PD 0
.IP \(bu 4
to generate the HTML for a single page, see \fIoddmu-html\fR(1)
.IP \(bu 4
to generate the HTML for the entire site, using Oddmu as a static site
generator, see \fIoddmu-static\fR(1)
.IP \(bu 4
to export the HTML for the entire site in one big feed, see \fIoddmu-export\fR(1)
.IP \(bu 4
to emulate a search of the files, see \fIoddmu-search\fR(1); to understand how the
search engine indexes pages and how it sorts and scores results, see
\fIoddmu-search\fR(7)
to generate the HTML for a page, see \fIoddmu-html\fR(1)
.IP \(bu 4
to search a regular expression and replace it across all files, see
\fIoddmu-replace\fR(1)
.IP \(bu 4
to learn what the most popular hashtags are, see \fIoddmu-hashtags\fR(1)
.IP \(bu 4
to print a table of contents (TOC) for a page, see \fIoddmu-toc\fR(1)
.IP \(bu 4
to list the outgoing links for a page, see \fIoddmu-links\fR(1)
.IP \(bu 4
to find missing pages (local links that go nowhere), see \fIoddmu-missing\fR(1)
.IP \(bu 4
to list all the pages with name and title, see \fIoddmu-list\fR(1)
.IP \(bu 4
to add links to changes, index and hashtag pages to pages you created locally,
see \fIoddmu-notify\fR(1)
.IP \(bu 4
to display build information, see \fIoddmu-version\fR(1)
to emulate a search of the files, see \fIoddmu-search\fR(1); to understand how the
search engine indexes pages and how it sorts and scores results, see
\fIoddmu-search\fR(7)
.PD
.PP
.SH EXAMPLES
.SH EXAMPLE
.PP
When saving a page, the page name is take from the URL and the page content is
taken from the "body" form parameter.\& To illustrate, here'\&s how to edit a page
using \fIcurl\fR(1):
When saving a page, the page name is take from the URL and the page
content is taken from the "body" form parameter.\& To illustrate, here'\&s
how to edit a page using \fIcurl\fR(1):
.PP
.nf
.RS 4
@@ -260,14 +100,6 @@ curl --form body="Did you bring a towel?"
.fi
.RE
.PP
To compute the space used by your setup, use regular tools:
.PP
.nf
.RS 4
du --exclude=\&'*/.*\&' --exclude \&'*~\&' --block-size=M
.fi
.RE
.PP
.SH DESIGN
.PP
This is a minimal wiki.\& There is no version history.\& It'\&s well suited as a
@@ -275,6 +107,13 @@ This is a minimal wiki.\& There is no version history.\& It'\&s well suited as a
on social media.\& The wiki serves as the text repository that results from these
discussions.\&
.PP
The wiki lists no recent changes.\& The expectation is that the people that care
were involved in the discussions beforehand.\&
.PP
The wiki also produces no feed.\& The assumption is that announcements are made on
social media: blogs, news aggregators, discussion forums, the fediverse, but
humans.\&
.PP
The idea is that the webserver handles as many tasks as possible.\& It logs
requests, does rate limiting, handles encryption, gets the certificates, and so
on.\& The web server acts as a reverse proxy and the wiki ends up being a content
@@ -284,140 +123,24 @@ on your point of view.\& See \fIoddmu-apache\fR(5).\&
.SH NOTES
.PP
Page names are filenames with ".\&md" appended.\& If your filesystem cannot handle
it, it can'\&t be a page name.\& Filenames can contain slashes and Oddmu creates
subdirectories as necessary.\&
it, it can'\&t be a page name.\&
.PP
Files may not end with a tilde ('\&~'\&) these are backup files.\& When saving pages
and file uploads, the old file renamed to the backup file unless the backup file
is less than an hour old, thus collapsing all edits made in an hour into a
single diff when comparing backup and current version.\&
The pages are indexed as the server starts and the index is kept in memory.\& If
you have a ton of pages, this takes a lot of memory.\&
.PP
The \fBindex\fR page is the default page.\& People visiting the "root" of the site are
redirected to "/view/index".\&
.PP
The \fBchanges\fR page is where links to new and changed files are added.\& As an
author, you can prevent this from happening by deselecting the checkbox "Add
link to the list of changes.\&" The changes page can be edited like every other
page, so it'\&s easy to undo mistakes.\&
.PP
Links on the changes page are grouped by date.\& When new links are added, the
current date of the machine Oddmu is running on is used.\& If a link already
exists on the changes page, it is moved up to the current date.\& If that leaves
an old date without any links, that date heading is removed.\&
.PP
If you want to link to the changes page, you need to do this yourself.\& Add a
link from the index, for example.\& The "view.\&html" template currently doesn'\&t do
it.\& See \fIoddmu-templates\fR(5) if you want to add the link to the template.\&
.PP
A page whose name starts with an ISO date (YYYY-MM-DD, e.\&g.\& "2023-10-28") is
called a \fBblog\fR page.\& When creating or editing blog pages, links to it are added
from other pages.\&
.PP
If the blog page name starts with the current year, a link is created from the
index page back to the blog page being created or edited.\& Again, you can prevent
this from happening by deselecting the checkbox "Add link to the list of
changes.\&" The index page can be edited like every other page, so it'\&s easy to
undo mistakes.\&
.PP
For every \fBhashtag\fR used, another link might be created.\& If a page named like
the hashtag exists, a backlink is added to it, linking to the new or edited blog
page.\&
.PP
If a link to the new or edited blog page already exists but it'\&s title is no
longer correct, it is updated.\&
.PP
New links added for blog pages are added at the top of the first unnumbered list
using the asterisk ('\&*'\&).\& If no such list exists, a new one is started at the
bottom of the page.\& This allows you to have a different unnumbered list further
up on the page, as long as it uses the minus for items ('\&-'\&).\&
.PP
Changes made locally do not create any links on the changes page, the index page
or on any hashtag pages.\& See \fIoddmu-notify\fR(1) for a way to add the necessary
links to the changes page and possibly to the index and hashtag pages.\&
.PP
A hashtag consists of a number sign ('\&#'\&) followed by Unicode letters, numbers
or the underscore ('\&_'\&).\& Thus, a hashtag ends with punctuation or whitespace.\&
.PP
The page names, titles and hashtags are loaded into memory when the server
starts.\& If you have a lot of pages, this takes a lot of memory.\&
.PP
Oddmu watches the working directory and any subdirectories for changes made
directly.\& Thus, in theory, it'\&s not necessary to restart it after making such
changes.\&
Files may not end with a tilde ('\&~'\&) these are backup files.\&
.PP
You cannot edit uploaded files.\& If you upload a file called "hello.\&txt" and
attempt to edit it by using "/edit/hello.\&txt" you create a page with the name
"hello.\&txt.\&md" instead.\&
attempt to edit it by using "/edit/hello.\&txt" you will create a page with the
name "hello.\&txt.\&md" instead.\&
.PP
In order to delete uploaded files via the web, create an empty file and upload
it.\& In order to delete a wiki page, save an empty page.\&
.PP
Note that some HTML file names are special: they act as templates.\& See
\fIoddmu-templates\fR(5) for their names and their use.\&
You cannot delete uploaded files via the web but you can delete regular wiki
pages by saving an empty file.\&
.PP
.SH SEE ALSO
.PP
.PD 0
.IP \(bu 4
\fIoddmu\fR(5), about the markup syntax and how feeds are generated based on link
lists
.IP \(bu 4
\fIoddmu-releases\fR(7), on what features are part of the latest release
.IP \(bu 4
\fIoddmu-filter\fR(7), on how to treat subdirectories as separate sites
.IP \(bu 4
\fIoddmu-search\fR(7), on how search works
.IP \(bu 4
\fIoddmu-templates\fR(5), on how to write the HTML templates
.PD
.PP
If you run Oddmu as a web server:
.PP
.PD 0
.IP \(bu 4
\fIoddmu-apache\fR(5), on how to set up Apache as a reverse proxy
.IP \(bu 4
\fIoddmu-nginx\fR(5), on how to set up freenginx as a reverse proxy
.IP \(bu 4
\fIoddmu-webdav\fR(5), on how to set up Apache as a Web-DAV server
.IP \(bu 4
\fIoddmu.\&service\fR(5), on how to run the service under systemd
.PD
.PP
If you run Oddmu as a static site generator or pages offline and sync them with
Oddmu running as a webserver:
.PP
.PD 0
.IP \(bu 4
\fIoddmu-hashtags\fR(1), on how to count the hashtags used
.IP \(bu 4
\fIoddmu-html\fR(1), on how to render a page
.IP \(bu 4
\fIoddmu-list\fR(1), on how to list pages and titles
.IP \(bu 4
\fIoddmu-links\fR(1), on how to list the outgoing links for a page
.IP \(bu 4
\fIoddmu-missing\fR(1), on how to find broken local links
.IP \(bu 4
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages
.IP \(bu 4
\fIoddmu-replace\fR(1), on how to search and replace text
.IP \(bu 4
\fIoddmu-search\fR(1), on how to run a search
.IP \(bu 4
\fIoddmu-static\fR(1), on generating a static site
.IP \(bu 4
\fIoddmu-toc\fR(1), on how to list the table of contents (toc) a page
.IP \(bu 4
\fIoddmu-version\fR(1), on how to get all the build information from the binary
.PD
.PP
If you want to stop using Oddmu:
.PP
.PD 0
.IP \(bu 4
\fIoddmu-export\fR(1), on how to export all the files as one big RSS file
.PD
\fIoddmu\fR(5), \fIoddmu.\&service\fR(5), oddmu-apache_(5), \fIoddmu-html\fR(1),
\fIoddmu-replace\fR(1), \fIoddmu-search\fR(1), \fIoddmu-search\fR(7), \fIoddmu-feed\fR(1)
.PP
.SH AUTHORS
.PP

View File

@@ -10,113 +10,41 @@ Oddmu is sometimes written Oddµ because µ is the letter mu.
*oddmu*
*oddmu* _subcommand_ [_arguments_...]
# DESCRIPTION
Oddmu can be used as a static site generator, turning Markdown files into HTML
files, or it can be used as a public or a private wiki server. If it runs as a
public wiki server, a regular webserver should be used as reverse proxy.
The oddmu program serves the current working directory as a wiki on port 8080.
Point your browser to http://localhost:8080/ to get started. This is equivalent
to http://localhost:8080/view/index the first page you'll create, most likely.
Run Oddmu without any arguments to serve the current working directory as a wiki
on port 8080. Point your browser to http://localhost:8080/ to use it. This
redirects you to http://localhost:8080/view/index the first page you'll
create, most likely.
See _oddmu_(5) for details about the page formatting.
If you request a page that doesn't exist, Oddmu tries to find a matching
If you request a page that doesn't exist, oddmu tries to find a matching
Markdown file by appending the extension ".md" to the page name. In the example
above, the page name requested is "index" and the file name Oddmu tries to read
is "index.md". If no such file exists, Oddmu offers you to create the page.
above, the page name requested is "index" and the file name oddmu tries to read
is "index.md". If no such file exists, oddmu offers you to create the page.
If your files don't provide their own title ("# title"), the file name (without
".md") is used for the page title.
Every file can be viewed as feed by using the extension ".rss". The
feed items are based on links in bullet lists using the asterix
("\*").
feed items are based on links in bullet lists using the asterix ("*").
Subdirectories are created as necessary.
The wiki knows the following actions for a given page name and (optional)
directory:
- _/_ redirects to /view/index
- _/view/dir/_ redirects to /view/dir/index
- _/view/dir/name_ shows a page
- _/view/dir/name.md_ shows the source text of a page
- _/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
- _/upload/dir/name_ shows a form to upload a file
- _/drop/dir/name_ saves an upload
- _/list/dir/_ lists the files in a directory
- _/delete/dir/name_ deletes a file or directory
- _/rename/dir/name?name=new_ renames a file or directory
- _/search/dir/?q=term_ to search for a term
- _/archive/dir/name.zip_ to download a zip file of a directory
When calling the _save_ and _append_ action, the page name is taken from the URL
path and the page content is taken from the _body_ form parameter. To
illustrate, here's how to edit the "welcome" page using _curl_:
```
curl --form body="Did you bring a towel?" \
http://localhost:8080/save/welcome
```
When calling the _drop_ action, the query parameters used are _name_ for the
target filename and _file_ for the file to upload. If the query parameter
_maxwidth_ is set, an attempt is made to decode and resize the image. JPG, PNG,
WEBP and HEIC files can be decoded. Only JPG and PNG files can be encoded,
however. If the target name ends in _.jpg_, the _quality_ query parameter is
also taken into account. To upload some thumbnails:
```
for f in *.jpg; do
curl --form name="$f" --form file=@"$f" --form maxwidth=100 \
http://localhost:8080/drop/
done
```
When calling the _search_ action, the search terms are taken from the query
parameter _q_.
```
curl 'http://localhost:8080/search/?q=towel'
```
The page name to act upon is optionally taken from the query parameter _id_. In
this case, the directory must also be part of the query parameter and not of the
URL path.
```
curl 'http://localhost:8080/view/?id=man/oddmu.1.txt'
```
The base name for the _archive_ action is used by the browser to save the
downloaded file. For Oddmu, only the directory is important. The following zips
the _man_ directory and saves it as _man.zip_.
```
curl --remote-name 'http://localhost:8080/archive/man/man.zip
```
See _oddmu_(5) for details about the page formatting.
# CONFIGURATION
The template files are the HTML files in the working directory. Please change
these templates!
The template files are the HTML files in the working directory: "add.html",
"edit.html", "search.html", "upload.html" and "view.html". Feel free to change
the templates and restart the server.
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".
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".
The second change you should make is to replace the name, email address and
domain name in "feed.html". Look for "Your Name" and "example.org".
The second change you should make is to replace the name, email
address and domain name in "feed.html". Look for "Your Name" and
"example.org". This second template is used to generate the RSS feeds
(despite it's ".html" extension).
See _oddmu-templates_(5) for more.
@@ -124,19 +52,6 @@ See _oddmu-templates_(5) for more.
You can change the port served by setting the ODDMU_PORT environment variable.
You can change the address served by setting the ODDMU_ADDRESS environment
variable to either an IPv4 address or an IPv6 address. If ODDMU_ADDRESS is
unset, then the program listens on all available unicast addresses, both IPv4
and IPv6. Here are a few example addresses:
```
ODDMU_ADDRESS=127.0.0.1 # The loopback IPv4 address.
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address.
```
See the Socket Activation section for an alternative method of listening which
supports Unix-domain sockets.
In order to limit language-detection to the languages you actually use, set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.g. "en" or "en,de,fr,pt".
@@ -144,73 +59,36 @@ codes, e.g. "en" or "en,de,fr,pt".
You can enable webfinger to link fediverse accounts to their correct profile
pages by setting ODDMU_WEBFINGER to "1". See _oddmu_(5).
If you use secret subdirectories, you cannot rely on the web server to hide
those pages because some actions such as searching and archiving include
subdirectories. They act upon a whole tree of pages, not just a single page. The
ODDMU_FILTER can be used to exclude subdirectories from such tree actions. See
_oddmu-filter_(7) and _oddmu-apache_(5).
# Socket Activation
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),
_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) and _oddmu-nginx_(5) for
example configurations.
server as a reverse proxy.
Oddmu assumes that all the users that can edit pages or upload files are trusted
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.
See _oddmu-apache_(5) for an example.
# OPTIONS
Oddmu can be run on the command-line using various subcommands.
The oddmu program can be run on the command-line using various subcommands.
- to generate the HTML for a single page, see _oddmu-html_(1)
- to generate the HTML for the entire site, using Oddmu as a static site
generator, see _oddmu-static_(1)
- to export the HTML for the entire site in one big feed, see _oddmu-export_(1)
- to generate the HTML for a page, see _oddmu-html_(1)
- to search a regular expression and replace it across all files, see
_oddmu-replace_(1)
- to emulate a search of the files, see _oddmu-search_(1); to understand how the
search engine indexes pages and how it sorts and scores results, see
_oddmu-search_(7)
- to search a regular expression and replace it across all files, see
_oddmu-replace_(1)
- to learn what the most popular hashtags are, see _oddmu-hashtags_(1)
- to print a table of contents (TOC) for a page, see _oddmu-toc_(1)
- to list the outgoing links for a page, see _oddmu-links_(1)
- to find missing pages (local links that go nowhere), see _oddmu-missing_(1)
- to list all the pages with name and title, see _oddmu-list_(1)
- to add links to changes, index and hashtag pages to pages you created locally,
see _oddmu-notify_(1)
- to display build information, see _oddmu-version_(1)
# EXAMPLES
# EXAMPLE
When saving a page, the page name is take from the URL and the page content is
taken from the "body" form parameter. To illustrate, here's how to edit a page
using _curl_(1):
When saving a page, the page name is take from the URL and the page
content is taken from the "body" form parameter. To illustrate, here's
how to edit a page using _curl_(1):
```
curl --form body="Did you bring a towel?" \
http://localhost:8080/save/welcome
```
To compute the space used by your setup, use regular tools:
```
du --exclude='*/\.*' --exclude '*~' --block-size=M
```
# DESIGN
This is a minimal wiki. There is no version history. It's well suited as a
@@ -218,6 +96,9 @@ _secondary_ medium: collaboration and conversation happens elsewhere, in chat,
on social media. The wiki serves as the text repository that results from these
discussions.
The wiki lists no recent changes. The expectation is that the people that care
were involved in the discussions beforehand.
The idea is that the webserver handles as many tasks as possible. It logs
requests, does rate limiting, handles encryption, gets the certificates, and so
on. The web server acts as a reverse proxy and the wiki ends up being a content
@@ -227,111 +108,24 @@ on your point of view. See _oddmu-apache_(5).
# NOTES
Page names are filenames with ".md" appended. If your filesystem cannot handle
it, it can't be a page name. Filenames can contain slashes and Oddmu creates
subdirectories as necessary.
it, it can't be a page name.
Files may not end with a tilde ('~') these are backup files. When saving pages
and file uploads, the old file renamed to the backup file unless the backup file
is less than an hour old, thus collapsing all edits made in an hour into a
single diff when comparing backup and current version.
The pages are indexed as the server starts and the index is kept in memory. If
you have a ton of pages, this takes a lot of memory.
The *index* page is the default page. People visiting the "root" of the site are
redirected to "/view/index".
The *changes* page is where links to new and changed files are added. As an
author, you can prevent this from happening by deselecting the checkbox "Add
link to the list of changes." The changes page can be edited like every other
page, so it's easy to undo mistakes.
Links on the changes page are grouped by date. When new links are added, the
current date of the machine Oddmu is running on is used. If a link already
exists on the changes page, it is moved up to the current date. If that leaves
an old date without any links, that date heading is removed.
If you want to link to the changes page, you need to do this yourself. Add a
link from the index, for example. The "view.html" template currently doesn't do
it. See _oddmu-templates_(5) if you want to add the link to the template.
A page whose name starts with an ISO date (YYYY-MM-DD, e.g. "2023-10-28") is
called a *blog* page. When creating or editing blog pages, links to it are added
from other pages.
If the blog page name starts with the current year, a link is created from the
index page back to the blog page being created or edited. Again, you can prevent
this from happening by deselecting the checkbox "Add link to the list of
changes." The index page can be edited like every other page, so it's easy to
undo mistakes.
For every *hashtag* used, another link might be created. If a page named like
the hashtag exists, a backlink is added to it, linking to the new or edited blog
page.
If a link to the new or edited blog page already exists but it's title is no
longer correct, it is updated.
New links added for blog pages are added at the top of the first unnumbered list
using the asterisk ('\*'). If no such list exists, a new one is started at the
bottom of the page. This allows you to have a different unnumbered list further
up on the page, as long as it uses the minus for items ('-').
Changes made locally do not create any links on the changes page, the index page
or on any hashtag pages. See _oddmu-notify_(1) for a way to add the necessary
links to the changes page and possibly to the index and hashtag pages.
A hashtag consists of a number sign ('#') followed by Unicode letters, numbers
or the underscore ('\_'). Thus, a hashtag ends with punctuation or whitespace.
The page names, titles and hashtags are loaded into memory when the server
starts. If you have a lot of pages, this takes a lot of memory.
Oddmu watches the working directory and any subdirectories for changes made
directly. Thus, in theory, it's not necessary to restart it after making such
changes.
Files may not end with a tilde ('~') these are backup files.
You cannot edit uploaded files. If you upload a file called "hello.txt" and
attempt to edit it by using "/edit/hello.txt" you create a page with the name
"hello.txt.md" instead.
attempt to edit it by using "/edit/hello.txt" you will create a page with the
name "hello.txt.md" instead.
In order to delete uploaded files via the web, create an empty file and upload
it. In order to delete a wiki page, save an empty page.
Note that some HTML file names are special: they act as templates. See
_oddmu-templates_(5) for their names and their use.
You cannot delete uploaded files via the web but you can delete regular wiki
pages by saving an empty file.
# SEE ALSO
- _oddmu_(5), about the markup syntax and how feeds are generated based on link
lists
- _oddmu-releases_(7), on what features are part of the latest release
- _oddmu-filter_(7), on how to treat subdirectories as separate sites
- _oddmu-search_(7), on how search works
- _oddmu-templates_(5), on how to write the HTML templates
If you run Oddmu as a web server:
- _oddmu-apache_(5), on how to set up Apache as a reverse proxy
- _oddmu-nginx_(5), on how to set up freenginx as a reverse proxy
- _oddmu-webdav_(5), on how to set up Apache as a Web-DAV server
- _oddmu.service_(5), on how to run the service under systemd
If you run Oddmu as a static site generator or pages offline and sync them with
Oddmu running as a webserver:
- _oddmu-hashtags_(1), on how to count the hashtags used
- _oddmu-html_(1), on how to render a page
- _oddmu-list_(1), on how to list pages and titles
- _oddmu-links_(1), on how to list the outgoing links for a page
- _oddmu-missing_(1), on how to find broken local links
- _oddmu-notify_(1), on updating index, changes and hashtag pages
- _oddmu-replace_(1), on how to search and replace text
- _oddmu-search_(1), on how to run a search
- _oddmu-static_(1), on generating a static site
- _oddmu-toc_(1), on how to list the table of contents (toc) a page
- _oddmu-version_(1), on how to get all the build information from the binary
If you want to stop using Oddmu:
- _oddmu-export_(1), on how to export all the files as one big RSS file
_oddmu_(5), _oddmu.service_(5), oddmu-apache_(5), _oddmu-html_(1),
_oddmu-replace_(1), _oddmu-search_(1), _oddmu-search_(7)
# AUTHORS

View File

@@ -1,11 +1,11 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "5" "2024-09-30" "File Formats Manual"
.TH "ODDMU" "5" "2023-09-21" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -13,40 +13,26 @@ oddmu - text formatting of wiki pages
.PP
.SH SYNTAX
.PP
The wiki pages are UTF-8 encoded Markdown files (with the ".\&md" extension).\&
Oddmu links are regular Markdown links to page names (without the ".\&md"
extension):
The wiki pages are UTF-8 encoded Markdown files.\&
.PP
.nf
.RS 4
[link text](page-name)
.fi
.RE
.PP
The page name has to be percent-encoded.\& See the section "Percent Encoding".\&
.PP
If you link to the actual Markdown file (with the ".\&md" extension), then Oddmu
serves the Markdown file!\&
.PP
There are three Oddµ-specific extensions: local links, hashtags and fediverse
account links.\& The Markdown library used features some additional extensions,
most importantly tables and definition lists.\&
There are three Oddµ-specific extensions: local links, hashtags and
fediverse account links.\& The Markdown library used features some
additional extensions, most importantly tables and definition lists.\&
.PP
.SS Local links
.PP
Local links use double square brackets.\& Oddmu does not treat underscores like
spaces, so "[[like this]]" and "[[like_this]]" link to different destinations
and are served by different files: "like this.\&md" and "like_this.\&md".\&
Local links use double square brackets [[like this]].\&
.PP
.SS Hashtags
.PP
Hashtags are single word links to searches for themselves.\& Use the underscore to
use hashtags consisting of multiple words.\& Hashtags are distinguished from page
titles because there is no space after the hash.\&
Hashtags are single word links to searches for themselves.\& Use the
underscore to use hashtags consisting of multiple words.\& Hashtags are
distinguished from page titles because there is no space after the
hash.\&
.PP
.nf
.RS 4
# Example
# Title
Text
@@ -54,11 +40,6 @@ Text
.fi
.RE
.PP
When a page containing hashtags is saved, a link to that page is added to every
page with the same name as the hashtag, if it exists.\& In the example above, if
the file "Tag.\&md" or the file "Another_Tag.\&md" exists, a link to the Example
page is added.\&
.PP
.SS Tables
.PP
A table with footers and a columnspan:
@@ -88,117 +69,58 @@ Internet
.PP
.SS Fediverse account links
.PP
Fediverse accounts look a bit like an at sign followed by an email address, e.\&g.\&
"@alex@alexschroeder.\&ch".\& When rendering a page, these turn into a username
linked to a profile page.\& In this case, "@alex" would be linked to
"https://alexschroeder.\&ch/users/alex".\&
Fediverse accounts look a bit like an at sign followed by an email
address, e.\&g.\& @alex@alexschroeder.\&ch.\& When rendering a page, these
turn into a username linked to a profile page.\& In this case, "@alex"
would be linked to "https://alexschroeder.\&ch/users/alex".\&
.PP
In many cases, this works as is.\& In reality, however, the link to the profile
page needs to be retrieved via webfinger.\& Oddµ does that in the background, and
as soon as the information is available, the actual profile link is used when
pages are rendered.\& In the example above, the result would be
"https://social.\&alexschroeder.\&ch/@alex".\&
In many cases, this will work as is.\& In reality, however, the link to
the profile page needs to be retrieved via webfinger.\& Oddµ does that
in the background, and as soon as the information is available, the
actual profile link is used when pages are rendered.\& In the example
above, the result would be "https://social.\&alexschroeder.\&ch/@alex".\&
.PP
As this sort of packground network activity is surprising, it is not enabled by
default.\& Set the environment variable ODDMU_WEBFINGER to "1" in order to enable
this.\&
As this sort of packground network activity is surprising, it is not
enabled by default.\& Set the environment variable ODDMU_WEBFINGER to
"1" in order to enable this.\&
.PP
.SS Other extensions
.PP
The Markdown processor comes with a few extensions:
.PP
.PD 0
.IP \(bu 4
emphasis markers inside words are ignored
.IP \(bu 4
fenced code blocks are supported
.IP \(bu 4
autolinking of "naked" URLs are supported
.IP \(bu 4
strikethrough using two tildes is supported (~~like this~~)
.IP \(bu 4
a space is required between the last # and the text for headings
.IP \(bu 4
you can specify an id for headings ({#id})
.IP \(bu 4
trailing backslashes turn into line breaks
.PD
.PP
.SH FEEDS
.PP
Every file can be viewed as a feed by using the extension ".\&rss".\& The feed items
are based on links in bullet lists using the asterix ("*").\& The items must
point to local pages.\& This is why the link may not contain two forward slashes
("//").\&
.PP
Below is an example index page.\& The feed would be "/view/index.\&rss".\& It would
contain the pages "Arianism", "Donatism" and "Monophysitism" but it would not
contain the pages "Feed" and "About" since the list items don'\&t start with an
asterix.\&
.PP
.nf
.RS 4
# Main Page
Hello and welcome! Here are some important links:
- [Feed](index\&.rss)
- [About](about)
Recent posts:
* [Arianism](arianism)
* [Donatism](donatism)
* [Monophysitism](monophysitism)
.fi
.RE
.PP
The feed contains at most 10 items, starting at the top.\& Thus, new items must be
added at the beginning of the list.\&
\fB emphasis markers inside words are ignored
\fR fenced code blocks are supported
\fB autolinking of "naked" URLs are supported
\fR strikethrough using two tildes is supported (~~like this~~)
\fB it is strict about prefix heading rules
\fR you can specify an id for headings ({#id})
\fB trailing backslashes turn into line breaks
\fR MathJax is supported (but needs a separte setup)
.PP
.SH PERCENT ENCODING
.PP
If you use Markdown links to local pages, you must percent-encode the link
target.\& Any character that is not an "unreserved character" according to RFC
3986 might need to be encoded.\& The unreserved characters are a-z, A-Z, 0-9, as
well as the four characters '\&-'\&, '\&_'\&, '\&.\&'\& and '\&~'\&.\&
If you use Markdown links to local pages, you must percent-encode the
link target.\& Any character that is not an "unreserved character"
according to RFC 3986 might need to be encoded.\& The unreserved
characters are a-z, A-Z, 0-9, as well as the four characters '\&-'\&,
\&'\&_'\&, '\&.\&'\& and '\&~'\&.\&
.PP
Percent-encoding means that each character is converted into one or more bytes,
and each byte is represented as a percent character followed by a hexadecimal
representation.\&
Percent-encoding means that each character is converted into one or
more bytes, and each byte is represented as a percent character
followed by a hexadecimal representation.\&
.PP
Realistically, what probably works best is to use a browser.\& If you type
"http://example.\&org/Alex Schröder" into the address bar, you'\&ll get sent to the
example domain.\& If you now copy the address and paste it back into a text
editor, you'\&ll get "http://example.\&org/Alex%20Schr%C3%B6der" and that'\&s how
you'\&ll learn that the Space is encoded by %20 and that the character '\&ö'\& is
encoded by %C3%B6.\& To link to the page "Alex Schröder" you would write something
like this: "[Alex](Alex%20Schr%C3%B6der)".\&
.PP
Another thing that'\&s common is that your page name contains a colon.\&
This is legal.\& The URL parser might still reject it.\& If you run the
"missing" subcommand, you'\&ll get to see error: "first path segment in
URL cannot contain colon".\& The solution is to prepend ".\&/"!\&
.PP
Example:
.PP
.nf
.RS 4
[2021-10-15 Re: Mark It Down](2021-10-15_Re:_Mark_It_Down)
.fi
.RE
.PP
Fixed:
.PP
.nf
.RS 4
[2021-10-15 Re: Mark It Down](\&./2021-10-15_Re:_Mark_It_Down)
.fi
.RE
Realistically, what probably works best is to use a browser.\& If you
type "http://example.\&org/Alex Schröder" into the address bar, you'\&ll
get sent to the example domain.\& If you now copy the address and paste
it back into a text editor, you'\&ll get
"http://example.\&org/Alex%20Schr%C3%B6der" and that'\&s how you'\&ll learn
that the Space is encoded by %20 and that the character '\&ö'\& is encoded
by %C3%B6.\& To link to the page "Alex Schröder" you would write
something like this: "[Alex](Alex%20Schr%C3%B6der)".\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-missing\fR(1)
\fIoddmu\fR(1)
.PP
This wiki uses the Go Markdown library.\&
https://github.\&com/gomarkdown/markdown

View File

@@ -6,48 +6,31 @@ oddmu - text formatting of wiki pages
# SYNTAX
The wiki pages are UTF-8 encoded Markdown files (with the ".md" extension).
Oddmu links are regular Markdown links to page names (without the ".md"
extension):
The wiki pages are UTF-8 encoded Markdown files.
```
[link text](page-name)
```
The page name has to be percent-encoded. See the section "Percent Encoding".
If you link to the actual Markdown file (with the ".md" extension), then Oddmu
serves the Markdown file!
There are three Oddµ-specific extensions: local links, hashtags and fediverse
account links. The Markdown library used features some additional extensions,
most importantly tables and definition lists.
There are three Oddµ-specific extensions: local links, hashtags and
fediverse account links. The Markdown library used features some
additional extensions, most importantly tables and definition lists.
## Local links
Local links use double square brackets. Oddmu does not treat underscores like
spaces, so "[[like this]]" and "[[like_this]]" link to different destinations
and are served by different files: "like this.md" and "like_this.md".
Local links use double square brackets [[like this]].
## Hashtags
Hashtags are single word links to searches for themselves. Use the underscore to
use hashtags consisting of multiple words. Hashtags are distinguished from page
titles because there is no space after the hash.
Hashtags are single word links to searches for themselves. Use the
underscore to use hashtags consisting of multiple words. Hashtags are
distinguished from page titles because there is no space after the
hash.
```
# Example
# Title
Text
#Tag #Another_Tag
```
When a page containing hashtags is saved, a link to that page is added to every
page with the same name as the hashtag, if it exists. In the example above, if
the file "Tag.md" or the file "Another_Tag.md" exists, a link to the Example
page is added.
## Tables
A table with footers and a columnspan:
@@ -73,44 +56,45 @@ Internet
## Fediverse account links
Fediverse accounts look a bit like an at sign followed by an email address, e.g.
"\@alex@alexschroeder.ch". When rendering a page, these turn into a username
linked to a profile page. In this case, "@alex" would be linked to
"https://alexschroeder.ch/users/alex".
Fediverse accounts look a bit like an at sign followed by an email
address, e.g. @alex@alexschroeder.ch. When rendering a page, these
turn into a username linked to a profile page. In this case, "@alex"
would be linked to "https://alexschroeder.ch/users/alex".
In many cases, this works as is. In reality, however, the link to the profile
page needs to be retrieved via webfinger. Oddµ does that in the background, and
as soon as the information is available, the actual profile link is used when
pages are rendered. In the example above, the result would be
"https://social.alexschroeder.ch/@alex".
In many cases, this will work as is. In reality, however, the link to
the profile page needs to be retrieved via webfinger. Oddµ does that
in the background, and as soon as the information is available, the
actual profile link is used when pages are rendered. In the example
above, the result would be "https://social.alexschroeder.ch/@alex".
As this sort of packground network activity is surprising, it is not enabled by
default. Set the environment variable ODDMU_WEBFINGER to "1" in order to enable
this.
As this sort of packground network activity is surprising, it is not
enabled by default. Set the environment variable ODDMU_WEBFINGER to
"1" in order to enable this.
## Other extensions
The Markdown processor comes with a few extensions:
- emphasis markers inside words are ignored
- fenced code blocks are supported
- autolinking of "naked" URLs are supported
- strikethrough using two tildes is supported (~~like this~~)
- a space is required between the last # and the text for headings
- you can specify an id for headings ({#id})
- trailing backslashes turn into line breaks
* emphasis markers inside words are ignored
* fenced code blocks are supported
* autolinking of "naked" URLs are supported
* strikethrough using two tildes is supported (~~like this~~)
* it is strict about prefix heading rules
* you can specify an id for headings ({#id})
* trailing backslashes turn into line breaks
* MathJax is supported (but needs a separte setup)
# FEEDS
Every file can be viewed as a feed by using the extension ".rss". The feed items
are based on links in bullet lists using the asterix ("\*"). The items must
point to local pages. This is why the link may not contain two forward slashes
("//").
Every file can be viewed as feed by using the extension ".rss". The
feed items are based on links in bullet lists using the asterix ("*").
The items must point to local pages. This is why the link may not
contain two forward slashes ("//").
Below is an example index page. The feed would be "/view/index.rss". It would
contain the pages "Arianism", "Donatism" and "Monophysitism" but it would not
contain the pages "Feed" and "About" since the list items don't start with an
asterix.
Assume this is the index page. The feed would be "/view/index.rss". It
would contain the pages "Arianism", "Donatism" and "Monophysitism" but
it would not contain the pages "Feed" and "About" since the list items
don't start with an asterix.
```
# Main Page
@@ -127,48 +111,32 @@ Recent posts:
* [Monophysitism](monophysitism)
```
The feed contains at most 10 items, starting at the top. Thus, new items must be
added at the beginning of the list.
The feed contains at most 10 items, starting at the top.
# PERCENT ENCODING
If you use Markdown links to local pages, you must percent-encode the link
target. Any character that is not an "unreserved character" according to RFC
3986 might need to be encoded. The unreserved characters are a-z, A-Z, 0-9, as
well as the four characters '-', '\_', '.' and '~'.
If you use Markdown links to local pages, you must percent-encode the
link target. Any character that is not an "unreserved character"
according to RFC 3986 might need to be encoded. The unreserved
characters are a-z, A-Z, 0-9, as well as the four characters '-',
'\_', '.' and '~'.
Percent-encoding means that each character is converted into one or more bytes,
and each byte is represented as a percent character followed by a hexadecimal
representation.
Percent-encoding means that each character is converted into one or
more bytes, and each byte is represented as a percent character
followed by a hexadecimal representation.
Realistically, what probably works best is to use a browser. If you type
"http://example.org/Alex Schröder" into the address bar, you'll get sent to the
example domain. If you now copy the address and paste it back into a text
editor, you'll get "http://example.org/Alex%20Schr%C3%B6der" and that's how
you'll learn that the Space is encoded by %20 and that the character 'ö' is
encoded by %C3%B6. To link to the page "Alex Schröder" you would write something
like this: "[Alex](Alex%20Schr%C3%B6der)".
Another thing that's common is that your page name contains a colon.
This is legal. The URL parser might still reject it. If you run the
"missing" subcommand, you'll get to see error: "first path segment in
URL cannot contain colon". The solution is to prepend "./"!
Example:
```
[2021-10-15 Re: Mark It Down](2021-10-15_Re:_Mark_It_Down)
```
Fixed:
```
[2021-10-15 Re: Mark It Down](./2021-10-15_Re:_Mark_It_Down)
```
Realistically, what probably works best is to use a browser. If you
type "http://example.org/Alex Schröder" into the address bar, you'll
get sent to the example domain. If you now copy the address and paste
it back into a text editor, you'll get
"http://example.org/Alex%20Schr%C3%B6der" and that's how you'll learn
that the Space is encoded by %20 and that the character 'ö' is encoded
by %C3%B6. To link to the page "Alex Schröder" you would write
something like this: "[Alex](Alex%20Schr%C3%B6der)".
# SEE ALSO
_oddmu_(1), _oddmu-missing_(1)
_oddmu_(1)
This wiki uses the Go Markdown library.
https://github.com/gomarkdown/markdown

View File

@@ -1,11 +1,11 @@
.\" Generated by scdoc 1.11.3
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU.SERVICE" "5" "2024-08-23"
.TH "ODDMU.SERVICE" "5" "2023-09-21"
.PP
.SH NAME
.PP
@@ -13,11 +13,12 @@ oddmu.\&service - how to setup Oddmu using systemd
.PP
.SS DESCRIPTION
.PP
Here'\&s how to setup a wiki using systemd such that it starts automatically when
the system boots and gets restarted automatically when it crashes.\&
Here'\&s how to setup a wiki using systemd such that it starts
automatically when the system boots and gets restarted automatically
when it crashes.\&
.PP
First, create a new user called "oddmu" with it'\&s own home directory but without
a login.\&
First, create a new user called "oddmu" with it'\&s own home directory
but without a login.\&
.PP
.nf
.RS 4
@@ -25,21 +26,20 @@ adduser --system --home /home/oddmu oddmu
.fi
.RE
.PP
The directory "/home/oddmu" contains the templates and all the data files.\& Copy
all the templates files ending in ".\&html" from the source distribution to
"/home/oddmu".\&
The directory "/home/oddmu" contains the templates and all the data
files.\& Copy all the tempaltes files ending in ".\&html" from the source
distribution to "/home/oddmu".\&
.PP
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.\&service" to "/home/oddmu", too.\&
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
care of:
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
@@ -49,11 +49,13 @@ Install the service file and enable it:
.PP
.nf
.RS 4
sudo systemctl enable --now \&./oddmu\&.service
ln -s /home/oddmu/oddmu\&.service /etc/systemd/system/
systemctl enable --now oddmu
.fi
.RE
.PP
You should be able to visit the wiki at http://localhost:8080/.\&
You should be able to visit the wiki at
http://localhost:8080/.\&
.PP
Check the log:
.PP
@@ -71,144 +73,9 @@ journalctl --follow --unit oddmu
.fi
.RE
.PP
.SH Socket Activation
.PP
Alternatively, you can let systemd handle the creation of the listening socket,
passing it to Oddmu.\& See "oddmu-unix-domain.\&service" and
"oddmu-unix-domain.\&socket" for a fully worked example of how to do this with a
Unix domain socket.\& Take note of "Accept=no" in the .\&socket file and
"StandardInput=socket" in the .\&service file.\& The option "StandardInput=socket"
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
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
sudo systemctl enable --now \&./oddmu-unix-domain\&.socket
sudo 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
.SS A personal wiki
.PP
On a single user machine, it might be useful to have a single wiki for the main
user available.\& In order to do this, setup a "user" unit using systemd and save
the following as "user-unix-domain.\&service":
.PP
.nf
.RS 4
[Unit]
Description=Oddmu
After=network\&.target
[Install]
WantedBy=default\&.target
[Service]
Type=simple
Restart=always
StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_LANGUAGES=de,en"
.fi
.RE
.PP
Since this is a user service, the same user can edit the files using their
favourite text editor.\&
.PP
Install it:
.PP
.nf
.RS 4
systemctl --user enable --now \&./user-unix-domain\&.service
.fi
.RE
.PP
To examine the log:
.PP
.nf
.RS 4
journalctl --user --unit user-unix-domain\&.service
.fi
.RE
.PP
Note that no sudo is required!\&
.PP
.SS Using the priviledged port 80
.PP
When running a personal wiki, you can have the oddmu binary listen on port 80,
the standard HTTP port.\& It is not really worth the effort: It means that you can
visit "http://localhost/" instead of "http://localhost:8080".\& Nevertheless, if
you'\&re interested in giving it a try, here'\&s how to do it.\&
.PP
The service definition must specify the new port:
.PP
Environment="ODDMU_PORT=80"
.PP
Since this is a privileged port, the binary needs an extra capability for an
ordinary user to do this.\&
.PP
.nf
.RS 4
sudo setcap \&'cap_net_bind_service=+ep\&' oddmu
.fi
.RE
.PP
Note that as soon as you recompile, the capability is gone again and the above
must be repeated.\&
.PP
.SH SECURITY
.PP
Only allow direct access to Oddmu on systems and networks where you'\&re OK with
every user editing the pages.\& On the open web, this is not true.\& If your server
is on the open web, always run Oddmu behind a regular web server acting as a
reverse proxy, limiting regular visitors to read-only access.\& This means that
the regular web server listens on the regular privileged ports (80 for HTTP,
443 for HTTPS) and passes requests to Oddmu on some other port.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
\fIsystemd.\&socket\fR(5), \fIcapabilities\fR(7)
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIcapabilities\fR(7)
.PP
.SH AUTHORS
.PP

View File

@@ -6,30 +6,30 @@ oddmu.service - how to setup Oddmu using systemd
## DESCRIPTION
Here's how to setup a wiki using systemd such that it starts automatically when
the system boots and gets restarted automatically when it crashes.
Here's how to setup a wiki using systemd such that it starts
automatically when the system boots and gets restarted automatically
when it crashes.
First, create a new user called "oddmu" with it's own home directory but without
a login.
First, create a new user called "oddmu" with it's own home directory
but without a login.
```
adduser --system --home /home/oddmu oddmu
```
The directory "/home/oddmu" contains the templates and all the data files. Copy
all the templates files ending in ".html" from the source distribution to
"/home/oddmu".
The directory "/home/oddmu" contains the templates and all the data
files. Copy all the tempaltes files ending in ".html" from the source
distribution to "/home/oddmu".
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.service" to "/home/oddmu", too.
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
care of:
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"
```
@@ -37,10 +37,12 @@ Environment="ODDMU_WEBFINGER=1"
Install the service file and enable it:
```
sudo systemctl enable --now ./oddmu.service
ln -s /home/oddmu/oddmu.service /etc/systemd/system/
systemctl enable --now oddmu
```
You should be able to visit the wiki at http://localhost:8080/.
You should be able to visit the wiki at
http://localhost:8080/.
Check the log:
@@ -54,128 +56,9 @@ Follow the log:
journalctl --follow --unit oddmu
```
# Socket Activation
Alternatively, you can let systemd handle the creation of the listening socket,
passing it to Oddmu. See "oddmu-unix-domain.service" and
"oddmu-unix-domain.socket" for a fully worked example of how to do this with a
Unix domain socket. Take note of "Accept=no" in the .socket file and
"StandardInput=socket" in the .service file. The option "StandardInput=socket"
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.
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:
```
sudo systemctl enable --now ./oddmu-unix-domain.socket
sudo 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.
## A personal wiki
On a single user machine, it might be useful to have a single wiki for the main
user available. In order to do this, setup a "user" unit using systemd and save
the following as "user-unix-domain.service":
```
[Unit]
Description=Oddmu
After=network.target
[Install]
WantedBy=default.target
[Service]
Type=simple
Restart=always
StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_LANGUAGES=de,en"
```
Since this is a user service, the same user can edit the files using their
favourite text editor.
Install it:
```
systemctl --user enable --now ./user-unix-domain.service
```
To examine the log:
```
journalctl --user --unit user-unix-domain.service
```
Note that no sudo is required!
## Using the priviledged port 80
When running a personal wiki, you can have the oddmu binary listen on port 80,
the standard HTTP port. It is not really worth the effort: It means that you can
visit "http://localhost/" instead of "http://localhost:8080". Nevertheless, if
you're interested in giving it a try, here's how to do it.
The service definition must specify the new port:
Environment="ODDMU_PORT=80"
Since this is a privileged port, the binary needs an extra capability for an
ordinary user to do this.
```
sudo setcap 'cap_net_bind_service=+ep' oddmu
```
Note that as soon as you recompile, the capability is gone again and the above
must be repeated.
# SECURITY
Only allow direct access to Oddmu on systems and networks where you're OK with
every user editing the pages. On the open web, this is not true. If your server
is on the open web, always run Oddmu behind a regular web server acting as a
reverse proxy, limiting regular visitors to read-only access. This means that
the regular web server listens on the regular privileged ports (80 for HTTP,
443 for HTTPS) and passes requests to Oddmu on some other port.
# SEE ALSO
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
_systemd.socket_(5), _capabilities_(7)
_oddmu_(1), _systemd.exec_(5), _capabilities_(7)
# AUTHORS

View File

@@ -1,31 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
my $literal = 0;
while (<>) {
# switch literal style
$literal = !$literal if /^```$/;
if ($literal) {
print;
next;
}
# bold
s/\*([^*]+)\*/**$1**/g;
# link to oddmu man pages (before italics)
s/_(oddmu[a-z.-]*)_\(([1-9])\)/[$1($2)]($1.$2)/g;
# italic
s/\b_([^_]+)_\b/*$1*/g;
# move all H1 headers to H2
s/^# /## /;
# the new H1 title
s/^([A-Z.-]*\([1-9]\))( ".*")?$/# $1/;
# quoted URLs
s/"(http.*?)"/`$1`/g;
# quoted wiki links
s/"(\[\[[^]]*\]\])"/`$1`/g;
# quoted Markdown links
s/"(\[.*?\]\(.*?\))"/`$1`/g;
# protect hashtags
s/#([^ #])/\\#$1/;
print;
}

View File

@@ -1,161 +0,0 @@
package main
import (
"github.com/stretchr/testify/assert"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"testing"
)
// Does oddmu(1) link to all the other man pages?
func TestManPages(t *testing.T) {
b, err := os.ReadFile("man/oddmu.1.txt")
main := string(b)
assert.NoError(t, err)
count := 0
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".txt") &&
path != "man/oddmu.1.txt" {
count++
s := strings.TrimPrefix(path, "man/")
s = strings.TrimSuffix(s, ".txt")
i := strings.LastIndex(s, ".")
ref := "_" + s[:i] + "_(" + s[i+1:] + ")"
assert.Contains(t, main, ref, ref)
}
return nil
})
assert.Greater(t, count, 0, "no man pages were found")
}
// Does oddmu-templates(5) mention all the templates?
func TestManTemplates(t *testing.T) {
b, err := os.ReadFile("man/oddmu-templates.5.txt")
man := string(b)
assert.NoError(t, err)
count := 0
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".html") {
count++
assert.Contains(t, man, path, path)
}
if path != "." && info.IsDir() {
return filepath.SkipDir
}
return nil
})
assert.Greater(t, count, 0, "no templates were found")
}
// Does oddmu(1) mention all the actions? We're not going to parse the go file and make sure to catch them all. I tried
// it, and it's convoluted.
func TestManActions(t *testing.T) {
b, err := os.ReadFile("man/oddmu.1.txt")
assert.NoError(t, err)
main := string(b)
b, err = os.ReadFile("wiki.go")
assert.NoError(t, err)
wiki := string(b)
count := 0
// this doesn't match the root handler
re := regexp.MustCompile(`http.HandleFunc\("(/[a-z]+/)", makeHandler\([a-z]+Handler, (true|false)\)\)`)
for _, match := range re.FindAllStringSubmatch(wiki, -1) {
count++
var path string
if match[2] == "true" {
path = "_" + match[1] + "dir/name"
} else {
path = "_" + match[1] + "dir/"
}
assert.Contains(t, main, path, path)
}
assert.Greater(t, count, 0, "no handlers were found")
// root handler is manual
assert.Contains(t, main, "\n- _/_", "root")
}
// Does the README link to all the man pages and all the Go source files,
// excluding the command and test files?
func TestReadme(t *testing.T) {
b, err := os.ReadFile("README.md")
readme := string(b)
assert.NoError(t, err)
count := 0
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".txt") {
count++
s := strings.TrimPrefix(path, "man/")
s = strings.TrimSuffix(s, ".txt")
i := strings.LastIndex(s, ".")
ref := "[" + s[:i] + "(" + s[i+1:] + ")]"
assert.Contains(t, readme, ref, ref)
}
return nil
})
assert.Greater(t, count, 0, "no man pages were found")
count = 0
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".go") &&
!strings.HasSuffix(path, "_test.go") &&
!strings.HasSuffix(path, "_cmd.go") {
count++
s := strings.TrimPrefix(path, "./")
ref := "`" + s + "`"
assert.Contains(t, readme, ref, ref)
}
return nil
})
assert.Greater(t, count, 0, "no source pages were found")
}
// Does the README document all the dependecies, checking all the all the packages with names containing a period?
func TestDocumentDependencies(t *testing.T) {
b, err := os.ReadFile("README.md")
readme := string(b)
assert.NoError(t, err)
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, ".", nil, parser.ImportsOnly)
assert.NoError(t, err)
imports := []string{}
for _, pkg := range pkgs {
for _, file := range pkg.Files {
for _, imp := range file.Imports {
name := imp.Path.Value[1 : len(imp.Path.Value)-1]
if strings.Contains(name, ".") && !slices.Contains(imports, name) {
imports = append(imports, name)
}
}
}
}
assert.Greater(t, len(imports), 0, "no imports found")
sort.Slice(imports, func(i, j int) bool { return len(imports[i]) < len(imports[j]) })
IMPORT:
for _, name := range imports {
for _, other := range imports {
if strings.HasPrefix(name, other) && name != other {
continue IMPORT
}
}
ok := strings.Contains(readme, name)
assert.True(t, ok, name)
}
}

View File

@@ -1,116 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/google/subcommands"
"io"
"net/url"
"os"
"path"
"strings"
)
type missingCmd struct {
}
func (*missingCmd) Name() string { return "missing" }
func (*missingCmd) Synopsis() string { return "list missing pages" }
func (*missingCmd) Usage() string {
return `missing:
Listing pages with links to missing pages. This command does not
understand links to directories being redirected to index pages.
A link such as [up](..) is reported as a link to a missing page.
Rewrite it as [up](../index) for it to work as intended.
`
}
func (cmd *missingCmd) SetFlags(f *flag.FlagSet) {
}
func (cmd *missingCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return missingCli(os.Stdout, &index)
}
// missingCli implements the finding of links to missing pages. In order to make testing easier, it takes a Writer and
// an indexStore. The Writer is important so that test code can provide a buffer instead of os.Stdout; the indexStore is
// important so that test code can ensure no other test running in parallel can interfere with the list of known pages
// (by adding or deleting pages).
func missingCli(w io.Writer, idx *indexStore) subcommands.ExitStatus {
found := false
for name := range idx.titles {
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
for _, link := range p.links() {
u, err := url.Parse(link)
if err != nil {
fmt.Fprintln(os.Stderr, p.Name, err)
return subcommands.ExitFailure
}
if u.Scheme == "" && u.Path != "" && !strings.HasPrefix(u.Path, "/") {
// feeds can work if the matching page works
u.Path = strings.TrimSuffix(u.Path, ".rss")
// links to the source file can work
u.Path = strings.TrimSuffix(u.Path, ".md")
// pages containing a colon need the ./ prefix
u.Path = strings.TrimPrefix(u.Path, "./")
// check whether the destination is a known page
destination, err := url.PathUnescape(u.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot decode %s: %s\n", link, err)
return subcommands.ExitFailure
}
_, ok := idx.titles[destination]
// links to directories can work
if !ok {
_, ok = idx.titles[path.Join(destination, "index")]
}
if !ok {
if !found {
fmt.Fprintln(w, "Page\tMissing")
found = true
}
fmt.Fprintf(w, "%s\t%s\n", p.Name, link)
}
}
}
}
if !found {
fmt.Fprintln(w, "No missing pages found.")
}
return subcommands.ExitSuccess
}
// links parses the page content and returns an array of link destinations.
func (p *Page) links() []string {
var links []string
parser, _ := wikiParser()
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.Link:
link := string(v.Destination)
url, err := url.Parse(link)
if err != nil {
// no error reporting
return ast.GoToNext
}
if url.IsAbs() {
links = append(links, link)
} else {
dir := p.Dir()
links = append(links, path.Join(dir, link))
}
}
}
return ast.GoToNext
})
return links
}

View File

@@ -1,18 +0,0 @@
package main
import (
"bytes"
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMissingCmd(t *testing.T) {
b := new(bytes.Buffer)
s := missingCli(b, minimalIndex(t))
assert.Equal(t, subcommands.ExitSuccess, s)
r := `Page Missing
index test
`
assert.Equal(t, r, b.String())
}

View File

@@ -1,47 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"io"
"os"
)
type notifyCmd struct {
}
func (*notifyCmd) Name() string { return "notify" }
func (*notifyCmd) Synopsis() string { return "add links to changes.md, index.md, and hashtag pages" }
func (*notifyCmd) Usage() string {
return `notify <page name> ...:
For each page, add entries to changes.md, index.md, and hashtag pages.
This is useful when writing pages offline and replicates the behaviour
triggered by the "Add link to the list of changes" checkbox, online.
`
}
func (cmd *notifyCmd) SetFlags(f *flag.FlagSet) {
}
func (cmd *notifyCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return notifyCli(os.Stdout, f.Args())
}
func notifyCli(w io.Writer, args []string) subcommands.ExitStatus {
index.load()
for _, name := range args {
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(w, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
err = p.notify()
if err != nil {
fmt.Fprintf(w, "%s: %s\n", name, err)
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}

View File

@@ -1,53 +0,0 @@
[Unit]
Description=Oddmu
After=network.target
Requires=oddmu-unix-domain.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
DynamicUser=true
MemoryMax=256M
MemoryHigh=128M
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
# (man "systemd.exec")
ReadWritePaths=/home/oddmu
ProtectHostname=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
MemoryDenyWriteExecute=yes
# Sandboxing options to harden security
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
DevicePolicy=closed
ProtectSystem=full
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap
# Denying access to capabilities that should not be relevant
# (man "capabilities")
CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE
CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT
CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK
CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM
CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG
CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE
CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Oddmu server socket
[Socket]
ListenStream=/run/oddmu/oddmu.sock
SocketGroup=www-data
# Systemd manages the socket, so may as well let it be owned by root.
SocketUser=root
# But it needs to be readable and writable by the web server.
SocketMode=0660
Accept=no
[Install]
WantedBy=sockets.target

View File

@@ -7,8 +7,8 @@ WantedBy=multi-user.target
Type=simple
Restart=always
DynamicUser=true
MemoryMax=120M
MemoryHigh=100M
MemoryMax=100M
MemoryHigh=120M
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
Environment="ODDMU_PORT=8080"

View File

@@ -1,30 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
heigh="320" width="320"
viewBox="0 0 320 320">
<g>
<circle id="hand1" cx="60" cy="190" r="15" fill="white" stroke="black" stroke-width="2"/>
<path id="stick" d="M40,40 L60,300 H66 L46,40 Z" fill="white" stroke="black" stroke-width="2"/>
<ellipse id="fingers" cx="50" cy="195" rx="5" ry="15" fill="white" stroke="black" stroke-width="2"/>
<path id="thumb" d="M60,180 Q40,180 60,190 Z" fill="white" stroke="black" stroke-width="2"/>
</g>
<g id="hand2">
<circle cx="240" cy="200" r="15" fill="white" stroke="black" stroke-width="2"/>
</g>
<g id="body">
<path d="M60,170 H170 L240,180 V230 L190,220
L200,222 L220,290 H120 V215
L125,215 H60 V170" fill="white" stroke="black" stroke-width="2"/>
</g>
<g id="face">
<circle cx="150" cy="150" r="30" fill="white" stroke="black" stroke-width="2"/>
<circle cx="138" cy="145" r="2" fill="black" stroke="black"/>
<circle cx="158" cy="145" r="2" fill="black" stroke="black"/>
<path d="M132,158 Q145,175 170,155 " fill="none" stroke="black" stroke-width="2"/>
</g>
<g id="foot1">
<path d="M120,300 C100,270 180,270 160,300 Z" fill="white" stroke="black" stroke-width="2"/>
</g>
<g id="foot1">
<path d="M180,300 V290 H190 C210,270 250,280 240,300 Z" fill="white" stroke="black" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

174
page.go
View File

@@ -2,56 +2,46 @@ package main
import (
"bytes"
"log"
"github.com/microcosm-cc/bluemonday"
"html/template"
"log"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"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.
// Page is a struct containing information about a single page. Title
// is the title extracted from the page content using titleRegexp.
// Name is the filename without extension (so a filename 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.
type Page struct {
Title string
Name string
Language 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
// used for snippets.
// santize uses bluemonday to sanitize the HTML.
// No exceptions are made because this is used for snippets.
func sanitizeStrict(s string) template.HTML {
policy := bluemonday.StrictPolicy()
policy.AllowElements("b")
return template.HTML(policy.Sanitize(s))
return template.HTML(bluemonday.StrictPolicy().Sanitize(s))
}
// unsafeBytes does not use bluemonday to sanitize the HTML used for pages. This is where you make changes if you want
// to be more lenient. If you look at the git repository, there are older versions containing the function sanitizeBytes
// which would do elaborate checking.
func unsafeBytes(bytes []byte) template.HTML {
return template.HTML(bytes)
// santizeBytes uses bluemonday to sanitize the HTML.
func sanitizeBytes(bytes []byte) template.HTML {
policy := bluemonday.UGCPolicy()
policy.AllowAttrs("class").OnElements("a") // for hashtags
return template.HTML(policy.SanitizeBytes(bytes))
}
// nameEscape returns the page name safe for use in URLs. That is, percent escaping is used except for the slashes.
// nameEscape returns the page name safe for use in URLs. That is,
// percent escaping is used except for the slashes.
func nameEscape(s string) string {
parts := strings.Split(s, "/")
for i, part := range parts {
@@ -60,65 +50,51 @@ func nameEscape(s string) string {
return strings.Join(parts, "/")
}
// save saves a Page. The path is based on the Page.Name and gets the ".md" extension. Page.Body is saved, without any
// carriage return characters ("\r"). Page.Title and Page.Html are not saved. There is no caching. Before removing or
// writing a file, the old copy is renamed to a backup, appending "~". Errors are not logged but returned.
// save saves a Page. The filename is based on the Page.Name and gets
// the ".md" extension. Page.Body is saved, without any carriage
// return characters ("\r"). Page.Title and Page.Html are not saved.
// There is no caching. Before removing or writing a file, the old
// copy is renamed to a backup, appending "~". There is no error
// checking for this.
func (p *Page) save() error {
fp := filepath.FromSlash(p.Name + ".md")
watches.ignore(fp)
filename := p.Name + ".md"
s := bytes.ReplaceAll(p.Body, []byte{'\r'}, []byte{})
if len(s) == 0 {
log.Println("Delete", p.Name)
index.remove(p)
return os.Rename(fp, fp+"~")
p.removeFromIndex()
_ = os.Rename(filename, filename+"~")
return os.Remove(filename)
}
p.Body = s
index.update(p)
d := filepath.Dir(fp)
p.updateIndex()
d := filepath.Dir(filename)
if d != "." {
err := os.MkdirAll(d, 0755)
if err != nil {
log.Printf("Creating directory %s failed: %s", d, err)
return err
}
}
err := backup(fp)
if err != nil {
return err
}
return os.WriteFile(fp, s, 0644)
_ = os.Rename(filename, filename+"~")
return os.WriteFile(filename, s, 0644)
}
// backup a file by renaming (!) it unless the existing backup is less than an hour old. A backup gets a tilde appended
// to it ("~"). This is true even if the file refers to a binary file like "image.png" and most applications don't know
// what to do with a file called "image.png~". This expects a file path. Use filepath.FromSlash(path) if necessary.
func backup(fp string) error {
_, err := os.Stat(fp)
if err != nil {
return nil
}
bp := fp + "~"
fi, err := os.Stat(bp)
if err != nil || time.Since(fi.ModTime()).Minutes() >= 60 {
return os.Rename(fp, bp)
}
return nil
}
// loadPage loads a Page given a name. The path loaded is that Page.Name with the ".md" extension. The Page.Title is set
// to the Page.Name (and possibly changed, later). The Page.Body is set to the file content. The Page.Html remains
// undefined (there is no caching).
func loadPage(path string) (*Page, error) {
path = strings.TrimPrefix(path, "./") // result of a filepath.TreeWalk starting with "."
body, err := os.ReadFile(filepath.FromSlash(path + ".md"))
// loadPage loads a Page given a name. The filename loaded is that
// Page.Name with the ".md" extension. The Page.Title is set to the
// Page.Name (and possibly changed, later). The Page.Body is set to
// the file content. The Page.Html remains undefined (there is no
// caching).
func loadPage(name string) (*Page, error) {
filename := name + ".md"
body, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: path, Name: path, Body: body}, nil
return &Page{Title: name, Name: name, Body: body, Language: ""}, nil
}
// 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. In a template, the title is a
// separate attribute and is not repeated in the HTML.
// 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) {
s := string(p.Body)
m := titleRegexp.FindStringSubmatch(s)
@@ -130,64 +106,24 @@ func (p *Page) handleTitle(replace bool) {
}
}
// summarize sets Page.Html to an extract.
// 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 and sets Page.Language.
func (p *Page) summarize(q string) {
t := p.plainText()
p.Name = nameEscape(p.Name)
p.Html = sanitizeStrict(snippets(q, t))
p.Language = language(t)
}
// IsBlog returns true if the page name starts with an ISO date
func (p *Page) IsBlog() bool {
name := path.Base(p.Name)
return blogRe.MatchString(name)
}
// Dir returns the directory the page is in. It's either the empty string if the page is in the Oddmu working directory,
// or it ends in a slash. This is used to create the upload link in "view.html", for example.
func (p *Page) Dir() string {
d := filepath.Dir(p.Name)
if d == "." {
return ""
}
return d + "/"
}
// 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 == "." {
return ""
}
return n
}
// Today returns the date, as a string, for use in templates.
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
return d
}

Some files were not shown because too many files have changed in this diff Show More