12 Commits
v1.7 ... v1.8

Author SHA1 Message Date
Alex Schroeder
8e35336cb3 No new features for release 1.8 2024-02-26 07:37:47 +01:00
Alex Schroeder
2a44c2a74f Make some of the types private
Don't export types unless necessary. Mostly because that looks weird
in Go documentation: when the functions acting on the types are
private the types themselves might as well be private.

Consequently, only the types that are used to write templates are
exported! For those types, the documentation has been improved (mostly
for Feed and Item).

In order to avoid nameclashes when hiding the types that didn't need
exporting, new naming schema was used. As these types were contains
for maps plus a mutex, they are called "stores" controlling access to
the maps: Accounts → accountStore, Index → indexStore, Template →
templateStore, Watches → watchStore.
2024-02-24 17:56:02 +01:00
Alex Schroeder
fe9a621f1e Document HTTP handlers in the README
Change the heading from Source to Hacking because I suspect the README
is were the notes on the source code go. The reason I don't want to
put them in code comments is because almost none of the Oddmu symbols
are exported.
2024-02-24 16:58:02 +01:00
Alex Schroeder
be663eed32 Makefile improvements
Introduce a PREFIX variable for the install target so one can override
this. Document this in the README.

Add rules for a build target and for a binary target that depends on
all the Go files so that these can be used as prerequisites for other
targets. The goal is to avoid unnecessary recompilations.

Designate all the non-file targets as phony targets.
2024-02-24 16:54:31 +01:00
Alex Schroeder
86ef305e9c Fix oddmu-list man page format
The optional string provided on the command line is set in italics.
2024-02-24 16:52:46 +01:00
Alex Schroeder
1fd97ae717 Document -full for the version subcommand 2024-02-23 12:32:27 +01:00
Alex Schroeder
d0fdf8c3c6 Highlight template name in the man page 2024-02-23 12:32:27 +01:00
Alex Schroeder
1786050e72 Reorganize the list of man page links
Use two groups, one for the web server and one for command-line use.
2024-02-23 12:32:27 +01:00
Alex Schroeder
f12252e148 Add missing source file to README
Add a test for that, too.
2024-02-23 00:28:41 +01:00
Alex Schroeder
f5f997261e Add missing man page link to the README
Add a test for that!
2024-02-23 00:23:52 +01:00
Alex Schroeder
43408707c5 Add package documentation 2024-02-23 00:09:07 +01:00
Alex Schroeder
50ce79d60d Update RELEASE instructions 2024-02-21 07:32:41 +01:00
23 changed files with 323 additions and 121 deletions

View File

@@ -1,4 +1,7 @@
SHELL=/bin/bash
PREFIX=${HOME}/.local
.PHONY: help build test run upload docs install missing
help:
@echo Help for Oddmu
@@ -13,7 +16,7 @@ help:
@echo make docs
@echo " create man pages from text files"
@echo
@echo go build
@echo make build
@echo " just build it"
@echo
@echo make install
@@ -22,14 +25,18 @@ help:
@echo make upload
@echo " this is how I upgrade my server"
run:
go run .
build: oddmu
oddmu: *.go
go build
test:
go test -shuffle on .
upload:
go build
run:
go run .
upload: build
rsync --itemize-changes --archive oddmu sibirocobombus.root:/home/oddmu/
ssh sibirocobombus.root "systemctl restart oddmu; systemctl restart alex; systemctl restart claudia; systemctl restart campaignwiki"
@echo Changes to the template files need careful consideration
@@ -37,11 +44,6 @@ upload:
docs:
cd man; make
install:
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
missing:
for f in man/*.txt; do grep --quiet "$$f" README.md || echo $$f is not in the README; done
install: build docs
for n in 1 5 7; do install -D -t ${PREFIX}/share/man/man$$n man/*.$$n; done
install -D -t ${PREFIX}/.local/bin oddmu

View File

@@ -48,6 +48,18 @@ 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)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
This man page lists all the Oddmu versions and their user-visible
changes.
[oddmu-releases(7)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
This man page lists all the Oddmu versions and their user-visible
changes.
[oddmu-version(1)](/oddmu.git/blob/main/man/oddmu-version.1.txt): This
man page documents the "version" subcommand which you can use to get
installed Oddmu version.
[oddmu-list(1)](/oddmu.git/blob/main/man/oddmu-list.1.txt): This man
page documents the "list" subcommand which you can use to get page
names and page titles.
@@ -91,8 +103,12 @@ This man page documents how the templates can be changed (how they
templates.
[oddmu-apache(5)](/oddmu.git/blob/main/man/oddmu-apache.5.txt): This
man page documents how to set up the web server for various common
tasks such as using logins to limit what visitors can edit.
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-nginx(5)](/oddmu.git/blob/main/man/oddmu-nginx.5.txt): 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)](/oddmu.git/blob/main/man/oddmu.service.5.txt): This
man page documents how to setup a systemd unit and have it manage
@@ -100,15 +116,28 @@ Oddmu. “Great configurability brings great burdens.”
## 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.
## 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 start it in the source directory:
Here's how to build and run straight from the source directory:
```sh
go run .
@@ -117,11 +146,43 @@ 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
```
To install it elsewhere, 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
```
## Bugs
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.
## Source
## Hacking
If you're interested in making changes to the code, here's a
high-level introduction to the various source files.
@@ -134,6 +195,8 @@ high-level introduction to the various source files.
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
@@ -153,6 +216,8 @@ high-level introduction to the various source files.
- `watch.go` implements the filesystem notification watch
- `wiki.go` implements the main function
### 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
@@ -161,6 +226,8 @@ 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
@@ -178,6 +245,21 @@ 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.
## References
[Writing Web Applications](https://golang.org/doc/articles/wiki/)

View File

@@ -3,10 +3,12 @@ When preparing a new release
1. Run tests
2. Make docs
2. Update man/oddmu-releases.7.txt
3. Make sure all files are checked in
3. make docs
4. Update man/oddmu-releases.7.txt
4. Make sure all files are checked in
5. Tag the release and push the tag to all remotes
6. cd man && make upload

View File

@@ -15,8 +15,8 @@ import (
// environment variable ODDMU_WEBFINGER to "1".
var useWebfinger = false
// Accounts contains the map used to set the usernames. Make sure to lock and unlock as appropriate.
type Accounts struct {
// accountStore controlls access to the usernames. Make sure to lock and unlock as appropriate.
type accountStore struct {
sync.RWMutex
// uris is a map, mapping account names likes "@alex@alexschroeder.ch" to URIs like
@@ -25,7 +25,7 @@ type Accounts struct {
}
// accounts holds the global mapping of accounts to profile URIs.
var accounts Accounts
var accounts accountStore
// 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.
@@ -36,11 +36,11 @@ func init() {
}
}
// 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
// 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 account(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
func accountLink(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
data = data[offset:]
i := 1 // skip @ of username
n := len(data)
@@ -105,7 +105,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 +121,24 @@ 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.
func parseWebFinger(body []byte) (string, error) {
var wf WebFinger
var wf webFinger
err := json.Unmarshal(body, &wf)
if err != nil {
return "", err

17
feed.go
View File

@@ -11,16 +11,33 @@ import (
"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

View File

@@ -16,9 +16,8 @@ import (
type docid uint
// Index contains the two maps used for search. Make sure to lock and
// unlock as appropriate.
type Index struct {
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
type indexStore struct {
sync.RWMutex
// next_id is the number of the next document added to the index
@@ -34,14 +33,14 @@ type Index struct {
titles map[string]string
}
var index Index
var index indexStore
func init() {
index.reset()
}
// reset the index. This assumes that the index is locked. It's useful for tests.
func (idx *Index) reset() {
func (idx *indexStore) reset() {
idx.next_id = 0
idx.token = make(map[string][]docid)
idx.documents = make(map[docid]string)
@@ -49,7 +48,7 @@ func (idx *Index) reset() {
}
// addDocument adds the text as a new document. This assumes that the index is locked!
func (idx *Index) addDocument(text []byte) docid {
func (idx *indexStore) addDocument(text []byte) docid {
id := idx.next_id
idx.next_id++
for _, token := range hashtags(text) {
@@ -66,7 +65,7 @@ func (idx *Index) addDocument(text []byte) docid {
}
// deleteDocument deletes all references to the id. The id can no longer be used. This assumes that the index is locked.
func (idx *Index) deleteDocument(id docid) {
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 {
@@ -87,7 +86,7 @@ func (idx *Index) deleteDocument(id docid) {
// 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 *Index) deletePageName(name string) {
func (idx *indexStore) deletePageName(name string) {
idx.Lock()
defer idx.Unlock()
var id docid
@@ -106,12 +105,12 @@ func (idx *Index) deletePageName(name string) {
}
// remove the page from the index. Do this when deleting a page. This assumes that the index is unlocked.
func (idx *Index) remove(p *Page) {
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 *Index) load() (int, error) {
func (idx *indexStore) load() (int, error) {
idx.Lock()
defer idx.Unlock()
err := filepath.Walk(".", idx.walk)
@@ -123,7 +122,7 @@ func (idx *Index) load() (int, error) {
}
// walk reads a file and adds it to the index. This assumes that the index is locked.
func (idx *Index) walk(path string, info fs.FileInfo, err error) error {
func (idx *indexStore) walk(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
@@ -149,7 +148,7 @@ func (idx *Index) walk(path string, info fs.FileInfo, err error) error {
}
// addPage adds a page to the index. This assumes that the index is locked.
func (idx *Index) addPage(p *Page) {
func (idx *indexStore) addPage(p *Page) {
id := idx.addDocument(p.Body)
idx.documents[id] = p.Name
p.handleTitle(false)
@@ -157,14 +156,14 @@ func (idx *Index) addPage(p *Page) {
}
// add a page to the index. This assumes that the index is unlocked.
func (idx *Index) add(p *Page) {
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 *Index) dump() {
func (idx *indexStore) dump() {
idx.RLock()
defer idx.RUnlock()
for token, ids := range idx.token {
@@ -173,14 +172,14 @@ func (idx *Index) dump() {
}
// updateIndex updates the index for a single page.
func (idx *Index) update(p *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 *Index) search(q string) []string {
func (idx *indexStore) search(q string) []string {
idx.RLock()
defer idx.RUnlock()
names := make([]string, 0)

View File

@@ -7,7 +7,7 @@ import (
)
func TestIndexAdd(t *testing.T) {
idx := &Index{}
idx := &indexStore{}
idx.reset()
idx.Lock()
defer idx.Unlock()

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HTML" "1" "2024-02-17"
.TH "ODDMU-HTML" "1" "2024-02-26"
.PP
.SH NAME
.PP

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-LIST" "1" "2024-02-17"
.TH "ODDMU-LIST" "1" "2024-02-24"
.PP
.SH NAME
.PP
@@ -13,7 +13,7 @@ oddmu-list - list page names and titles from the command-line
.PP
.SH SYNOPSIS
.PP
\fBoddmu list\fR [-dir string]
\fBoddmu list\fR [-dir \fIstring\fR]
.PP
.SH DESCRIPTION
.PP

View File

@@ -6,7 +6,7 @@ oddmu-list - list page names and titles from the command-line
# SYNOPSIS
*oddmu list* [-dir string]
*oddmu list* [-dir _string_]
# DESCRIPTION

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-02-20"
.TH "ODDMU-RELEASES" "7" "2024-02-26"
.PP
.SH NAME
.PP
@@ -15,9 +15,13 @@ oddmu-releases - what'\&s new in this releases?\&
.PP
This page lists user-visible features and template changes to consider.\&
.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 upload.\&html
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

View File

@@ -8,9 +8,13 @@ oddmu-releases - what's new in this releases?
This page lists user-visible features and template changes to consider.
## 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
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".

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2024-02-17"
.TH "ODDMU-STATIC" "1" "2024-02-26"
.PP
.SH NAME
.PP

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-VERSION" "1" "2024-02-17"
.TH "ODDMU-VERSION" "1" "2024-02-23"
.PP
.SH NAME
.PP
@@ -13,22 +13,22 @@ oddmu-version - print build info on the command-line
.PP
.SH SYNOPSIS
.PP
\fBoddmu version\fR
\fBoddmu version\fR [-full]
.PP
.SH DESCRIPTION
.PP
The "version" subcommand prints a lot of stuff used to build the binary,
including the git revision, git repository, versions of dependencies used and
more.\&
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
It'\&s the equivalent of running this:
.SH OPTIONS
.PP
.nf
\fB-full\fR
.RS 4
go version -m oddmu
.fi
.RE
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)

View File

@@ -6,19 +6,19 @@ oddmu-version - print build info on the command-line
# SYNOPSIS
*oddmu version*
*oddmu version* [-full]
# DESCRIPTION
The "version" subcommand prints a lot of stuff used to build the binary,
including the git revision, git repository, versions of dependencies used and
more.
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.
It's the equivalent of running this:
# OPTIONS
```
go version -m oddmu
```
*-full*
Print a lot more information, including the versions of dependencies
used. It's the equivalent of running "go version -m oddmu".
# SEE ALSO

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-02-19"
.TH "ODDMU" "1" "2024-02-23"
.PP
.SH NAME
.PP
@@ -353,11 +353,30 @@ Note that some HTML file names are special: they act as templates.\& See
\fIoddmu\fR(5), about the markup syntax and how feeds are generated based on link
lists
.IP \(bu 4
\fIoddmu.\&service\fR(5), on how to run the service under systemd
\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-filter\fR(7), on how to treat subdirectories as separate sites
\fIoddmu-nginx\fR(5), on how to set up freenginx as a reverse proxy
.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-html\fR(1), on how to render a page from the command-line
.IP \(bu 4
@@ -365,23 +384,15 @@ lists
.IP \(bu 4
\fIoddmu-missing\fR(1), on how to find broken local links from the command-line
.IP \(bu 4
\fIoddmu-nginx\fR(5), on how to set up freenginx as a reverse proxy
.IP \(bu 4
\fIoddmu-releases\fR(7), on what features are part of the latest release
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages from the
command-line
.IP \(bu 4
\fIoddmu-replace\fR(1), on how to search and replace text from the command-line
.IP \(bu 4
\fIoddmu-search\fR(1), on how to run a search from the command-line
.IP \(bu 4
\fIoddmu-search\fR(7), on how search works
.IP \(bu 4
\fIoddmu-static\fR(1), on generating a static site from the command-line
.IP \(bu 4
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages from the
command-line
.IP \(bu 4
\fIoddmu-templates\fR(5), on how to write the HTML templates
.IP \(bu 4
\fIoddmu-version\fR(1), on how to get all the build information from the binary
.PD
.PP

View File

@@ -295,21 +295,28 @@ _oddmu-templates_(5) for their names and their use.
- _oddmu_(5), about the markup syntax and how feeds are generated based on link
lists
- _oddmu.service_(5), on how to run the service under systemd
- _oddmu-apache_(5), on how to set up Apache as a reverse proxy
- _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.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-html_(1), on how to render a page from the command-line
- _oddmu-list_(1), on how to list pages and titles from the command-line
- _oddmu-missing_(1), on how to find broken local links from the command-line
- _oddmu-nginx_(5), on how to set up freenginx as a reverse proxy
- _oddmu-releases_(7), on what features are part of the latest release
- _oddmu-replace_(1), on how to search and replace text from the command-line
- _oddmu-search_(1), on how to run a search from the command-line
- _oddmu-search_(7), on how search works
- _oddmu-static_(1), on generating a static site from the command-line
- _oddmu-notify_(1), on updating index, changes and hashtag pages from the
command-line
- _oddmu-templates_(5), on how to write the HTML templates
- _oddmu-replace_(1), on how to search and replace text from the command-line
- _oddmu-search_(1), on how to run a search from the command-line
- _oddmu-static_(1), on generating a static site from the command-line
- _oddmu-version_(1), on how to get all the build information from the binary
# AUTHORS

View File

@@ -28,3 +28,35 @@ func TestManPages(t *testing.T) {
return nil
})
}
func TestReadme(t *testing.T) {
b, err := os.ReadFile("README.md")
main := string(b)
assert.NoError(t, err)
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".txt") {
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
})
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") {
s := strings.TrimPrefix(path, "./")
ref := "`" + s + "`"
assert.Contains(t, main, ref, ref)
}
return nil
})
}

View File

@@ -68,7 +68,7 @@ func wikiParser() (*parser.Parser, *[]string) {
fn, hashtags := hashtag()
parser.RegisterInline('#', fn)
if useWebfinger {
parser.RegisterInline('@', account)
parser.RegisterInline('@', accountLink)
}
return parser, hashtags
}

View File

@@ -17,15 +17,19 @@ import (
var templateFiles = []string{"edit.html", "add.html", "view.html",
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
// templates are the parsed HTML templates used. See renderTemplate and loadTemplates. Subdirectories may contain their
// own templates which override the templates in the root directory. If so, they are not filepaths. Use
// filepath.ToSlash() if necessary.
type Template struct {
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See
// renderTemplate and loadTemplates.
type templateStore struct {
sync.RWMutex
// template is a map of parsed HTML templates. The key is their path name. By default, the map only contains
// top-level templates like "view.html". Subdirectories may contain their own templates which override the
// templates in the root directory. If so, they are paths like "dir/view.html", not filepaths. Use
// filepath.ToSlash() if necessary.
template map[string]*template.Template
}
var templates Template
var templates templateStore
// loadTemplates loads the templates. If templates have already been loaded, return immediately.
func loadTemplates() {

View File

@@ -19,7 +19,7 @@ import (
"strings"
)
type Upload struct {
type upload struct {
Dir string
Name string
Last string
@@ -34,7 +34,7 @@ var lastRe = regexp.MustCompile(`^(.*)([0-9]+)(.*)$`)
// parameters are used to copy name, maxwidth and quality from the previous upload. If the previous name contains a
// number, this is incremented by one.
func uploadHandler(w http.ResponseWriter, r *http.Request, dir string) {
data := &Upload{Dir: dir}
data := &upload{Dir: dir}
maxwidth := r.FormValue("maxwidth")
if maxwidth != "" {
data.MaxWidth = maxwidth

View File

@@ -12,18 +12,33 @@ import (
"time"
)
// Watches holds a map and a mutex. The map contains the template names that have been requested and the exact time at
// which they have been requested. Adding the same file multiple times, such as when the watch function sees multiple
// Write events for the same file, the time keeps getting updated so that when the go routine runs, it only acts on
// files that haven't been updated in the last second. The go routine is what forces us to use the RWMutex for the map.
type Watches struct {
// watchStore controls access to the maps used by the filesystem watches. Make sure to lock and unlock as appropriate.
// The maps are used to control a sort of queue for files that need reloading (if a template) or reindexing (if a page).
// File system notifications add files to the queue in order to handle changes made without Oddmu, while Oddmu is
// running.
type watchStore struct {
sync.RWMutex
ignores map[string]time.Time
// files contains the filenames that have been queued for reloading (if a template) or reindexing (if a page)
// and the exact time at which they have been added. When the same file is added multiple times, such as when
// the watchStore function sees multiple Write events for the same file, the time keeps getting updated so that
// when the watchTimer runs, it only acts on files that haven't been updated in the last second.
files map[string]time.Time
// ignores contains the files that some code intends to change, knowing that subsequent writes events would
// result in file system notifications that would end up adding the filenames to the queue for reloading (if a
// template) or reindexing (if a page). When Oddmu is making the changes, it can ignore the corresponding
// notifications by the file system. Those notifications are consequences of Oddmu doing its job. In other
// words, Oddmu does not rely on file system notification even it is Oddmu doing the changes. This avoids a 1s
// when changing templates, for example.
ignores map[string]time.Time
// watcher is the pointer to the actual watcher doing the file system watching. It watches a set of paths.
// Whenever Oddmu creates a new subdirectory, it adds the path for this subdirectory to the watcher.
watcher *fsnotify.Watcher
}
var watches Watches
var watches watchStore
func init() {
watches.ignores = make(map[string]time.Time)
@@ -31,7 +46,7 @@ func init() {
}
// install initializes watches and installs watchers for all directories and subdirectories.
func (w *Watches) install() (int, error) {
func (w *watchStore) install() (int, error) {
// create a watcher for the root directory and never close it
var err error
w.watcher, err = fsnotify.NewWatcher()
@@ -48,7 +63,7 @@ func (w *Watches) install() (int, error) {
}
// add installs a watch for every directory that isn't hidden. Note that the root directory (".") is not skipped.
func (w *Watches) add(path string, info fs.FileInfo, err error) error {
func (w *watchStore) add(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
@@ -72,7 +87,7 @@ func (w *Watches) add(path string, info fs.FileInfo, err error) error {
// adds an entry to the files map, or updates the file's time, and starts a go routine. Example: If a file gets three
// consecutive Write events, the first two go routine invocations won't do anything, since the time kept getting
// updated. Only the last invocation will act upon the event.
func (w *Watches) watch() {
func (w *watchStore) watch() {
for {
select {
case err, ok := <-w.watcher.Errors:
@@ -94,7 +109,7 @@ func (w *Watches) watch() {
// Incidentally, this also prevents rsync updates from generating activity ("stat ./.index.md.tTfPFg: no such file or
// directory"). Note the painful details: If moving a file into a watched directory, a Create event is received. If a
// new file is created in a watched directory, a Create event and one or more Write events is received.
func (w *Watches) watchHandle(e fsnotify.Event) {
func (w *watchStore) watchHandle(e fsnotify.Event) {
path := strings.TrimPrefix(e.Name, "./")
if strings.HasPrefix(filepath.Base(path), ".") {
return
@@ -130,7 +145,7 @@ func (w *Watches) watchHandle(e fsnotify.Event) {
// watchTimer checks if the file hasn't been updated in 1s and if so, it calls watchDoUpdate. If another write has
// updated the file, do nothing because another watchTimer will run at the appropriate time and check again.
func (w *Watches) watchTimer(path string) {
func (w *watchStore) watchTimer(path string) {
t, ok := w.files[path]
if ok && t.Add(time.Second).Before(time.Now().Add(time.Nanosecond)) {
delete(w.files, path)
@@ -141,7 +156,7 @@ func (w *Watches) watchTimer(path string) {
// Do the right thing right now. For Create events such as directories being created or files being moved into a watched
// directory, this is the right thing to do. When a file is being written to, watchHandle will have started a timer and
// will call this function after 1s of no more writes. If, however, the path is in the ignores map, do nothing.
func (w *Watches) watchDoUpdate(path string) {
func (w *watchStore) watchDoUpdate(path string) {
_, ignored := w.ignores[path]
if ignored {
return
@@ -170,7 +185,7 @@ func (w *Watches) watchDoUpdate(path string) {
// watchDoRemove removes files from the index or discards templates. If the path in question is in the ignores map, do
// nothing.
func (w *Watches) watchDoRemove(path string) {
func (w *watchStore) watchDoRemove(path string) {
_, ignored := w.ignores[path]
if ignored {
return
@@ -189,7 +204,7 @@ func (w *Watches) watchDoRemove(path string) {
// ignore is before code that is known suspected save files and trigger watchHandle eventhough the code already handles
// this. This is achieved by adding the path to the ignores map for 1s.
func (w *Watches) ignore(path string) {
func (w *watchStore) ignore(path string) {
w.Lock()
defer w.Unlock()
w.ignores[path] = time.Now()

23
wiki.go
View File

@@ -1,3 +1,6 @@
// Oddmu is a wiki web server and a static site generator.
//
// The types exported are the ones needed to write the templates.
package main
import (
@@ -139,6 +142,24 @@ func scheduleInstallWatcher() {
}
}
// serve starts the web server using [http.Serve]. The listener is determined via [getListener]. The various handlers
// are created using [makeHandler] if their path starts with an action segment. For example, the URL path "/view/index"
// is understood to contain the "view" action and so [viewHandler] is called with the argument "index". The one handler
// that doesn't need this is [rootHandler].
//
// The handlers often come in pairs. One handler to show the user interface and one handler to make the change:
// - [editHandler] shows the edit form and [saveHandler] saves changes to a page
// - [addHandler] shows the add form and [appendHandler] appends the addition to a page
// - [uploadHandler] shows the upload form and [dropHandler] saves the uploaded files
//
// Some handlers only do something and the links or forms to call them is expected to be part of the view template:
// - [archiveHandler] zips up the current directory
// - [diffHandler] shows the changes made in the last 60min to a page
// - [searchHandler] shows search results
//
// At the same time as the server starts up, pages are indexed via [scheduleLoadIndex], languages are loaded via
// [scheduleLoadLanguages] and the current directory and its subdirectories is watched for changes using watchers
// installed via [scheduleInstallWatcher].
func serve() {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/archive/", makeHandler(archiveHandler, true))
@@ -185,6 +206,8 @@ func commands() {
os.Exit(int(subcommands.Execute(ctx)))
}
// main runs [serve] if called without arguments and it runs [commands] if called with arguments.
// The first argument is the subcommand.
func main() {
if len(os.Args) == 1 {
serve()