51 Commits
v1.12 ... v1.15

Author SHA1 Message Date
Alex Schroeder
26033de177 Update release-information 2025-02-09 11:46:35 +01:00
Alex Schroeder
e1ba007f97 Don't link hashtags in link text
This change depends on a change to the markdown library. Specifically,
the parser's InsideLink must be public.

This means that the #like_this hashtag from the README.md in the
source directory is no longer available, so that test had to be
rewritten.

Another change to reduce the number of false hashtags was to use the
hashtag parser for all situations: It's also used to identify hashtags
in the search query string. The parser doesn't automatically turn the
matches to lower-case, however, so that has to be done when indexing
documents and when searching for hashtags.

The hashtags command for the commandline no longer prints a hash for
all the tags.
2025-02-07 20:05:36 +01:00
Alex Schroeder
e90ff9e7dd Fix timestamp handling for backup files
The timestamp has to be updated!
2024-11-16 11:45:24 +01:00
Alex Schroeder
70356e850a Fiddle with case for the headings 2024-11-16 11:37:09 +01:00
Alex Schroeder
81a59fd6ac Add preview test 2024-11-15 19:29:05 +01:00
Alex Schroeder
52d6f26eed Add unreleased 0.15 to the docs 2024-11-15 19:28:46 +01:00
Alex Schroeder
171910ff4f Fix release notes for 0.14 2024-11-15 19:26:45 +01:00
Alex Schroeder
5fb0f57b5c Add base element to edit template 2024-11-15 19:22:56 +01:00
Alex Schroeder
d712b132cc Add base element to preview template
Without this, links in the preview cannot be followed as they all
point to /preview/ instead of /view/.
2024-11-09 23:13:50 +01:00
Alex Schroeder
199c236c08 Use parser.EscapeChars
With github.com/gomarkdown/markdown commit 7e0a027d98c5 escapeChars is
now exported as EscapeChars and therefore the whole wrapper function
is no longer necessary.
2024-10-05 14:13:12 +02:00
Alex Schroeder
0e7f7a2c05 Add quotes 2024-09-30 08:44:02 +02:00
Alex Schroeder
4af15b48db Small documentation improvements 2024-09-30 08:42:14 +02:00
Alex Schroeder
9b6c54ccb4 Allow escaping of @ 2024-09-30 08:35:02 +02:00
Alex Schroeder
83f447b643 Move mod_dav documentation to new man page 2024-09-26 01:10:43 +02:00
Alex Schroeder
d5e37fa90a Document WebDAV setup 2024-09-26 01:08:26 +02:00
Alex Schroeder
609da1fbc2 Improve the man tests
Add a check for template documentation. Add checks to prevent tests
from passing when nothing was found.
2024-08-30 17:52:59 +02:00
Alex Schroeder
ba32e0dcce Document the list template 2024-08-30 17:34:11 +02:00
Alex Schroeder
e975c527d1 Add test for all actions in the man page
Add the /list/, /delete/ and /rename/ actions to the man page.
2024-08-30 14:03:32 +02:00
Alex Schroeder
656b9490a1 Fix delete test 2024-08-30 14:03:12 +02:00
Alex Schroeder
9bd7ca59fa Add rename action 2024-08-29 14:46:10 +02:00
Alex Schroeder
56f95553d6 Update man pages 2024-08-29 13:10:06 +02:00
Alex Schroeder
76e63278d6 Add subcommands to synopsis of oddmu(1) man page
Add more subcommands to the Options section, too.
2024-08-29 13:09:25 +02:00
Alex Schroeder
1e957b5411 Use EXAMPLES instead of EXAMPLE for man pages 2024-08-29 13:08:24 +02:00
Alex Schroeder
e666fb44cb Add list and delete actions 2024-08-29 10:44:42 +02:00
Alex Schroeder
754bf11516 Fix test due to themes/plain/README
That file has no Markdown title as recognized by Oddmu and therefore
its title is "README" (equal to the page name).
2024-08-27 19:41:35 +02:00
Alex Schroeder
7eeb81fa94 README reword stow example 2024-08-27 18:30:50 +02:00
Alex Schroeder
9c70935362 More work on the plain text theme 2024-08-24 23:39:21 +02:00
Alex Schroeder
9d65c01bb0 Improve description of transjovian.org theme 2024-08-24 23:20:27 +02:00
Alex Schroeder
0179d393dd Fix the scdoc-to-markdown script
When in a literal block, be more restrictive about what gets changed
before printing.
2024-08-24 23:07:48 +02:00
Alex Schroeder
f8b97f794b Add plain text theme 2024-08-24 22:49:10 +02:00
Alex Schroeder
b801f83fe0 Fix dist target 2024-08-24 22:49:02 +02:00
Alex Schroeder
486c3f8620 Release 1.13 2024-08-24 14:16:03 +02:00
Alex Schroeder
5b0fcdd69f Update Makefiles
Help is more compact.
docs/Makefile comes with help.
Fixed make install.
Added clean target.
Use long options for tar and rm.
2024-08-24 11:52:19 +02:00
Alex Schroeder
bb99d05a0d Add more cross-compilation targets
- oddmu-darwin-amd64.tar.gz
- oddmu-windows-amd64.tar.gz
- oddmu-linux-arm64.tar.gz
2024-08-24 11:38:04 +02:00
Alex Schroeder
98358a008b Switched HEIC decoder again
This time I'm using github.com/gen2brain/heic. "Based on libheif and
libde265 compiled to WASM and used with wazero runtime (CGo-free)."
Amazing.
2024-08-23 17:37:54 +02:00
Alex Schroeder
51c8348ef7 Switched HEIC decoder fork
Moving from github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd
to github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786.

There is no cross-compiling available. If only somebody wrote a HEIC
decoder in pure Go.
2024-08-23 17:24:23 +02:00
Alex Schroeder
5e77f1332e Tiny typo in the README 2024-08-23 12:55:15 +02:00
Alex Schroeder
fbbb4a543f Split a section in oddmuse.service
Moved the part about the privileged port 80 from the "A personal wiki"
section in oddmuse.service(5) into its own section.
2024-08-23 12:44:45 +02:00
Alex Schroeder
ccc7c0bc8f Run go fmt 2024-08-22 08:05:58 +02:00
Alex Schroeder
aae2ae1265 The "html" subcommand takes - for stdin 2024-08-21 18:05:09 +02:00
Alex Schroeder
8929d72acd Fix export subcommand by actually loading the index
Without this fix, only the tests load the index.
2024-08-21 18:04:28 +02:00
Alex Schroeder
014507ce4e Move sed expressions to Perl script
This simplifies the Makefile inside the man directory.
2024-08-18 00:26:15 +02:00
Alex Schroeder
554a929bf5 Document use of ODDMU_LANGUAGES for export 2024-08-16 18:37:26 +02:00
Alex Schroeder
5f8e006594 Fixing export and missing subcommands
Both of these subcommands rely on the global index. Loading this
global index and acting upon it while tests are running is
problematic, however. Walking that index results in being unable to
load files that are being created and deleted in other tests.

Therefore, these tests get their own minimal index that only indexes
the Markdown files in the original checkout: README.md, index.md, and
the Markdown files in the the "themes" tree.

This also means that missingCli and exportCli must accept a pointer to
an index and use that instead of the global index.

Add a test for language in the export feed.

Add a test for a JSON feed.

Both of these tests used to fail because the exportCli funtion did not
copy p.Body to it.Body so when the template calls .Languages the
result was always empty.
2024-08-16 18:27:34 +02:00
Alex Schroeder
e347a59603 Fix documentation of renderHtml
Page.Language hasn't been a member of the Page type for a long time.
2024-08-16 18:24:54 +02:00
Alex Schroeder
964dc3bf4a Ensure cleanup of testdata before running tests 2024-08-16 18:24:28 +02:00
Alex Schroeder
d5f8b280ac Support for JSON exports 2024-08-16 13:41:21 +02:00
Alex Schroeder
8ee5705ae7 Wording for man-pages
Remove "from the command-line" in a few places.
2024-08-16 12:33:11 +02:00
Alex Schroeder
43bf1574c9 Add export subcommand 2024-08-16 12:32:58 +02:00
Alex Schroeder
1c8af9fcdb missing subcommand no longer walks the file-system
Instead, rely on the code that loads the index. Delete the
existingPages function.
2024-08-16 11:23:34 +02:00
Alex Schroeder
f6fa76bd5f Update alexschroeder.ch theme 2024-08-16 11:13:31 +02:00
82 changed files with 1861 additions and 401 deletions

6
.gitignore vendored
View File

@@ -1,3 +1,7 @@
/oddmu
test.md
/testdata/
/oddmu-darwin-*
/oddmu-linux-*
/oddmu-windows-*
/oddmu.exe
/oddmu

View File

@@ -1,29 +1,27 @@
SHELL=/bin/bash
PREFIX=${HOME}/.local
.PHONY: help build test run upload docs install missing
.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
@echo make docs
@echo " create man pages from text files"
@echo
@echo make 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
@@ -31,6 +29,7 @@ oddmu: *.go
go build
test:
rm -rf testdata/*
go test -shuffle on .
run:
@@ -42,20 +41,37 @@ upload: build
@echo Changes to the template files need careful consideration
docs:
cd man; make
cd man; make man
install:
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
install -D -t ${PREFIX}/bin oddmu
# More could be added, of course!
dist: oddmu-linux-amd64.tar.gz
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 czf $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
tar --create --file $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
$< Makefile *.socket *.service *.md man/Makefile man/*.1 man/*.5 man/*.7 themes/
priv:

View File

@@ -31,7 +31,7 @@ If your pages don't provide their own title (`# title`), the file name
necessary.
Other files can be uploaded and images (ending in `.jpg`, `.jpeg`,
`.png`, `.heic` or `webp`) can be resized when they are uploaded
`.png`, `.heic` or `.webp`) can be resized when they are uploaded
(resulting in `.jpg` or `.png` files).
## Documentation
@@ -138,6 +138,17 @@ edit.
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.
## Building
To build the binary:
@@ -198,9 +209,9 @@ into `$HOME/.local/share/man/`.
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:
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
@@ -231,6 +242,7 @@ high-level introduction to the various source files.
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
@@ -300,6 +312,20 @@ 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
@@ -321,7 +347,7 @@ in turn can be used by browsers to get hyphenation right. Apache-2.0.
is used to sniff the MIME type of files with unknown filename
extensions. MIT.
[github.com/bashdrew/goheif](https://github.com/bashdrew/goheif) is
[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.

View File

@@ -4,6 +4,8 @@ When preparing a new release
1. Run tests
2. Update man/oddmu-releases.7.txt
- add missing items
- change "(unreleased)"
3. make docs

View File

@@ -70,6 +70,7 @@ I hate the machine!`
I shiver at home
the monitor glares and moans
my grey heart grows cold`
// create s and overwrite it with r
p := &Page{Name: "testdata/backup/cold", Body: []byte(s)}
p.save()
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
@@ -78,19 +79,29 @@ my grey heart grows cold`
// 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>`)
// save u
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
// diff from s to u since r was not 60 min or older and so the backup is kept
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))
assert.NoError(t, os.Chtimes("testdata/backup/cold.md", ts, ts))
// save r
p = &Page{Name: "testdata/backup/cold", Body: []byte(r)}
p.save()
body = string(p.Diff())
// diff from u to r:
// diff from u to r since enough time has passed and the old backup is discarded
assert.Contains(t, body, `<del>my grey heart grows cold</del>`)
assert.Contains(t, body, `<ins>I hate the machine!</ins>`)
// save s
p = &Page{Name: "testdata/backup/cold", Body: []byte(s)}
p.save()
body = string(p.Diff())
// diff from u to s since this is still "the same" editing window
assert.Contains(t, body, `<del>my grey heart grows cold</del>`)
assert.Contains(t, body, `<ins>fear or cold, who knows?</ins>`)
}

View File

@@ -4,6 +4,7 @@
<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; }

112
export_cmd.go Normal file
View File

@@ -0,0 +1,112 @@
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
}

55
export_cmd_test.go Normal file
View File

@@ -0,0 +1,55 @@
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
}

12
go.mod
View File

@@ -1,14 +1,16 @@
module alexschroeder.ch/cgit/oddmu
go 1.21.0
go 1.22
toolchain go1.22.3
require (
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd
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-20240730141124-034f12af3bf6
github.com/gen2brain/heic v0.3.1
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e
github.com/google/subcommands v1.2.0
github.com/hexops/gotextdiff v1.0.3
github.com/microcosm-cc/bluemonday v1.0.26
@@ -22,15 +24,17 @@ require (
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/gorilla/css v1.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // 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.16.0 // indirect
golang.org/x/sys v0.21.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

18
go.sum
View File

@@ -1,20 +1,24 @@
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/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd h1:SxkQeH4jjXT0zMgiRgkiIQjIvWfe9vXuTAmE3cfcQrU=
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd/go.mod h1:p1sbxRy+MY71fEWHcfRmerC8WUYXDFCExF9A7aXwp98=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/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-20240930133403-7e0a027d98c5 h1:qIhG9h8tUzKsVHn0iHtWUohq7Ve7btgA8rGp7TvrIHw=
github.com/gomarkdown/markdown v0.0.0-20240930133403-7e0a027d98c5/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e h1:ESHlT0RVZphh4JGBz49I5R6nTdC8Qyc08vU25GQHzzQ=
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
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=
@@ -53,6 +57,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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=
@@ -60,8 +66,8 @@ 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=
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=

View File

@@ -8,9 +8,18 @@ import (
)
func TestHashtagsCmd(t *testing.T) {
cleanup(t, "testdata/hashtag")
p := &Page{Name: "testdata/hashtag/hash", Body: []byte(`# Hash
I hope for a time
not like today, relentless,
just crocus blooming
#Crocus`)}
p.save()
b := new(bytes.Buffer)
s := hashtagsCli(b)
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "#like_this\t")
assert.Contains(t, x, "crocus\t")
}

View File

@@ -18,6 +18,7 @@ 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.
`
}
@@ -30,27 +31,44 @@ 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 {
p, err := loadPage(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", arg, err)
return subcommands.ExitFailure
}
if useTemplate {
p.handleTitle(true)
p.renderHtml()
t := "view.html"
loadTemplates()
err := templates.template[t].Execute(w, p)
if err != nil {
fmt.Fprintf(os.Stderr, "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)
status := p.printHtml(w, useTemplate)
if status != subcommands.ExitSuccess {
return status
}
}
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

@@ -62,10 +62,12 @@ func (idx *indexStore) reset() {
}
// addDocument adds the text as a new document. This assumes that the index is locked!
// The hashtags (only!) are used as tokens. They are stored in lower case.
func (idx *indexStore) addDocument(text []byte) docid {
id := idx.next_id
idx.next_id++
for _, token := range hashtags(text) {
token = strings.ToLower(token)
ids := idx.token[token]
// Don't add same ID more than once. Checking the last
// position of the []docid works because the id is
@@ -193,8 +195,8 @@ func (idx *indexStore) update(p *Page) {
idx.add(p)
}
// search searches the index for a query string and returns page
// names.
// search searches the index. The query string is parsed for tokens. Each token is turned to lower cased and looked up
// in the index. Each page in the result must contain all the tokens. Returns page names.
func (idx *indexStore) search(q string) []string {
idx.RLock()
defer idx.RUnlock()
@@ -203,6 +205,7 @@ func (idx *indexStore) search(q string) []string {
if len(hashtags) > 0 {
var r []docid
for _, token := range hashtags {
token = strings.ToLower(token)
if ids, ok := idx.token[token]; ok {
if r == nil {
r = ids

View File

@@ -11,8 +11,8 @@ func TestIndexAdd(t *testing.T) {
idx.reset()
idx.Lock()
defer idx.Unlock()
tag := "#hello"
id := idx.addDocument([]byte("oh hi " + tag))
tag := "hello"
id := idx.addDocument([]byte("oh hi #" + tag))
assert.Contains(t, idx.token, tag)
idx.deleteDocument(id)
assert.NotContains(t, idx.token, tag)
@@ -31,10 +31,19 @@ func TestIndex(t *testing.T) {
}
}
// Lower case hashtag!
func TestSearchHashtag(t *testing.T) {
cleanup(t, "testdata/search-hashtag")
p := &Page{Name: "testdata/search-hashtag/search", Body: []byte(`# Search
I'm back in this room
Shelf, table, chair, and shelf again
Where are my glasses?
#Searching`)}
p.save()
index.load()
q := "#like_this"
pages, _ := search(q, "", "", 1, false)
pages, _ := search("#searching", "", "", 1, false)
assert.NotZero(t, len(pages))
}

102
list.go Normal file
View File

@@ -0,0 +1,102 @@
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)
}

59
list.html Normal file
View File

@@ -0,0 +1,59 @@
<!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>

30
list_test.go Normal file
View File

@@ -0,0 +1,30 @@
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">`)
}

View File

@@ -3,6 +3,20 @@ MAN=$(patsubst %.txt,%,${TEXT})
HTML=$(patsubst %.txt,%.html,${TEXT})
MD=$(patsubst %.txt,%.md,${TEXT})
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
@@ -20,16 +34,7 @@ md: ${MD}
%.md: %.txt
@echo Making $@
@sed --regexp-extended \
-e 's/\*([^*]+)\*/**\1**/g' \
-e 's/_(oddmu[a-z.-]*)_\(([1-9])\)/[\1(\2)](\1.\2)/g' \
-e 's/\b_([^_]+)_\b/*\1*/g' \
-e 's/^# /## /' \
-e 's/#([^ #])/\\#\1/' \
-e 's/"(http.*?)"/`\1`/' \
-e 's/"(\[.*?\]\(.*?\))"/`\1`/' \
-e 's/^([A-Z.-]*\([1-9]\))( ".*")?$$/# \1/' \
< $< > $@
@perl scdoc-to-markdown < $< > $@
README.md: ../README.md
@echo Making $@
@@ -42,7 +47,9 @@ upload: ${MD} README.md
make clean
clean:
rm --force ${HTML} ${MD} README.md
@echo Removing HTML and Markdown files
@rm --force ${HTML} ${MD} README.md
realclean: clean
rm --force ${MAN}
@echo Removing man pages
@rm --force ${MAN}

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2024-05-09"
.TH "ODDMU-APACHE" "5" "2024-09-25"
.PP
.SH NAME
.PP
@@ -48,7 +48,7 @@ ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
@@ -126,13 +126,13 @@ ServerAdmin alex@alexschroeder\&.ch
ServerName transjovian\&.org
ProxyPassMatch "^/((view|diff|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
RedirectMatch "^/((edit|save|add|append|upload|drop)/(\&.*))?$"
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|search|archive)/(\&.*))?$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
@@ -170,7 +170,7 @@ In that case, you need to use the ProxyPassMatch directive.\&
.PP
.nf
.RS 4
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
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
@@ -189,7 +189,7 @@ A workaround is to add the redirect manually and drop the question-mark:
.nf
.RS 4
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))$"
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
@@ -234,12 +234,12 @@ htpasswd -D \&.htpasswd berta
.RE
.PP
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:
"/append/", "/upload/", "/drop/", "/list/", "/delete/" and "/rename/" URLs with
a password by adding the following to your "<VirtualHost *:443>" section:
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd
@@ -263,10 +263,10 @@ expression, all directories matching the regular expression are excluded.\& See
.PP
In the following example, ODDMU_FILTER is set to "^secret/".\&
.PP
http://transjovian.\&org/search/index?\&q=something does not search the "secret/"
"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
"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/"
@@ -274,7 +274,7 @@ directory:
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|preview|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename|(view|preview|search|archive)/secret)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd
@@ -300,8 +300,9 @@ DocumentRoot /home/oddmu
.PP
Make sure that none of the subdirectories look like the wiki paths "/view/",
"/diff/", "/edit/", "/save/", "/add/", "/append/", "/upload/", "/drop/",
"/search/" or "/archive/".\& For example, create a file called "robots.\&txt"
containing the following, telling all robots that they'\&re not welcome.\&
"/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.\&
.PP
.nf
.RS 4
@@ -349,7 +350,7 @@ This requires a valid login by the user "alex" or "berta":
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop)/intetebi/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/intetebi/">
Require user alex berta
</LocationMatch>
.fi

View File

@@ -40,7 +40,7 @@ ServerAdmin alex@alexschroeder.ch
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
@@ -106,13 +106,13 @@ ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
ProxyPassMatch "^/((view|diff|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
RedirectMatch "^/((edit|save|add|append|upload|drop)/(.*))?$" \
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|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
@@ -144,7 +144,7 @@ You probably want to serve some static files as well (see *Serve static files*).
In that case, you need to use the ProxyPassMatch directive.
```
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))?$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
@@ -159,7 +159,7 @@ 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|search|archive)/(.*))$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
@@ -197,11 +197,11 @@ htpasswd -D .htpasswd berta
```
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:
"/append/", "/upload/", "/drop/", "/list/", "/delete/" and "/rename/" URLs with
a password by adding the following to your "<VirtualHost \*:443>" section:
```
<LocationMatch "^/(edit|save|add|append|upload|drop)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
@@ -224,17 +224,17 @@ _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/"
"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
"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|(view|preview|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename|(view|preview|search|archive)/secret)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
@@ -257,8 +257,9 @@ DocumentRoot /home/oddmu
Make sure that none of the subdirectories look like the wiki paths "/view/",
"/diff/", "/edit/", "/save/", "/add/", "/append/", "/upload/", "/drop/",
"/search/" or "/archive/". For example, create a file called "robots.txt"
containing the following, telling all robots that they're not welcome.
"/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.
```
User-agent: *
@@ -268,8 +269,8 @@ 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.
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:
@@ -301,7 +302,7 @@ password file mentioned above.
This requires a valid login by the user "alex" or "berta":
```
<LocationMatch "^/(edit|save|add|append|upload|drop)/intetebi/">
<LocationMatch "^/(edit|save|add|append|upload|drop|list|delete|rename)/intetebi/">
Require user alex berta
</LocationMatch>
```
@@ -344,10 +345,10 @@ 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"
"\<style\>: The Style Information element"
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
"<link>: The External Resource Link element"
"\<link\>: The External Resource Link element"
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
# AUTHORS

79
man/oddmu-export.1 Normal file
View File

@@ -0,0 +1,79 @@
.\" 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>.\&

68
man/oddmu-export.1.txt Normal file
View File

@@ -0,0 +1,68 @@
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

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-FILTER" "7" "2024-02-19"
.TH "ODDMU-FILTER" "7" "2024-09-30"
.PP
.SH NAME
.PP
@@ -18,22 +18,17 @@ 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.\& Essentially, the subdirectory acts as a different site.\&
Depending on how you think about it, you might not want to include those "sites"
in searches or archives of the whole site.\&
.PP
What'\&s important in this situation is whether the visitor is looking at the
"main site" (a page further up in the directory tree) or at a particular page in
a "separate site".\&
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
currenly looking at, directory tree actions starting in a "separate site"
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 sates is via the environment variable ODDMU_FILTER.\&
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

View File

@@ -11,22 +11,17 @@ 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. Essentially, the subdirectory acts as a different site.
Depending on how you think about it, you might not want to include those "sites"
in searches or archives of the whole site.
What's important in this situation is whether the visitor is looking at the
"main site" (a page further up in the directory tree) or at a particular page in
a "separate site".
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
currenly looking at, directory tree actions starting in a "separate site"
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 sates is via the environment variable ODDMU_FILTER.
The way to identify separate sites is via the environment variable ODDMU_FILTER.
It's value is a regular expression matching separate sites.
# EXAMPLES

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HASHTAGS" "1" "2024-07-31"
.TH "ODDMU-HASHTAGS" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-hashtags - count the hashtags used from the command-line
oddmu-hashtags - count the hashtags used
.PP
.SH SYNOPSIS
.PP
@@ -20,7 +20,7 @@ oddmu-hashtags - count the hashtags used from the command-line
The "hashtags" subcommand counts all the hashtags used and lists them, separated
by a TAB character.\&
.PP
.SH EXAMPLE
.SH EXAMPLES
.PP
List the top 10 hashtags.\& This requires 11 lines because of the header line.\&
.PP

View File

@@ -2,7 +2,7 @@ ODDMU-HASHTAGS(1)
# NAME
oddmu-hashtags - count the hashtags used from the command-line
oddmu-hashtags - count the hashtags used
# SYNOPSIS
@@ -13,7 +13,7 @@ oddmu-hashtags - count the hashtags used from the command-line
The "hashtags" subcommand counts all the hashtags used and lists them, separated
by a TAB character.
# EXAMPLE
# EXAMPLES
List the top 10 hashtags. This requires 11 lines because of the header line.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HTML" "1" "2024-02-26"
.TH "ODDMU-HTML" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-html - render Oddmu page HTML from the command-line
oddmu-html - render Oddmu page HTML
.PP
.SH SYNOPSIS
.PP
@@ -19,7 +19,8 @@ oddmu-html - render Oddmu page HTML from the command-line
.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.\&
"view.\&html" template.\& Use "-" as the page name if you want to read Markdown from
\fBstdin\fR.\&
.PP
.SH OPTIONS
.PP
@@ -29,16 +30,25 @@ Use the "view.\&html" template to render the page.\& Without this, the HTML
lacks html and body tags.\&
.PP
.RE
.SH EXAMPLE
.SH EXAMPLES
.PP
Generate the HTML for "README.\&md":
Generate "README.\&html" from "README.\&md":
.PP
.nf
.RS 4
oddmu html README
oddmu html README > README\&.html
.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.\&

View File

@@ -2,7 +2,7 @@ ODDMU-HTML(1)
# NAME
oddmu-html - render Oddmu page HTML from the command-line
oddmu-html - render Oddmu page HTML
# SYNOPSIS
@@ -12,7 +12,8 @@ oddmu-html - render Oddmu page HTML from the command-line
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.
"view.html" template. Use "-" as the page name if you want to read Markdown from
*stdin*.
# OPTIONS
@@ -20,14 +21,21 @@ the ".md" extension) and prints the HTML to STDOUT without invoking the
Use the "view.html" template to render the page. Without this, the HTML
lacks html and body tags.
# EXAMPLE
# EXAMPLES
Generate the HTML for "README.md":
Generate "README.html" from "README.md":
```
oddmu html README
oddmu html README > README.html
```
Alternatively:
```
oddmu html - < README.md > README.html
```
# ENVIRONMENT
The ODDMU_WEBFINGER environment variable has no effect in this situation.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-LIST" "1" "2024-02-24"
.TH "ODDMU-LIST" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-list - list page names and titles from the command-line
oddmu-list - list page names and titles
.PP
.SH SYNOPSIS
.PP
@@ -31,7 +31,7 @@ subdirectory are listed, and the directory is stripped from the page name.\&
Limit the list to a particular directory.\&
.PP
.RE
.SH EXAMPLE
.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.\&

View File

@@ -2,7 +2,7 @@ ODDMU-LIST(1)
# NAME
oddmu-list - list page names and titles from the command-line
oddmu-list - list page names and titles
# SYNOPSIS
@@ -22,7 +22,7 @@ subdirectory are listed, and the directory is stripped from the page name.
*-dir* _string_
Limit the list to a particular directory.
# EXAMPLE
# 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.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-MISSING" "1" "2024-02-17"
.TH "ODDMU-MISSING" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-missing - list missing pages from the command-line
oddmu-missing - list missing pages
.PP
.SH SYNOPSIS
.PP
@@ -26,7 +26,7 @@ that start with a slash "/" and links that start with a known URL schema
.PP
Notably, links that start with ".\&.\&/" are reported as missing.\&
.PP
.SH EXAMPLE
.SH EXAMPLES
.PP
Looking for broken links:
.PP

View File

@@ -2,7 +2,7 @@ ODDMU-MISSING(1)
# NAME
oddmu-missing - list missing pages from the command-line
oddmu-missing - list missing pages
# SYNOPSIS
@@ -19,7 +19,7 @@ that start with a slash "/" and links that start with a known URL schema
Notably, links that start with "../" are reported as missing.
# EXAMPLE
# EXAMPLES
Looking for broken links:

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NGINX" "5" "2024-05-09"
.TH "ODDMU-NGINX" "5" "2024-08-29"
.PP
.SH NAME
.PP
@@ -27,7 +27,7 @@ section.\& Add a new \fIlocation\fR section after the existing \fIlocation\fR se
.PP
.nf
.RS 4
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://localhost:8080;
}
.fi
@@ -53,7 +53,7 @@ location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|archive)/ {
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;
@@ -97,7 +97,7 @@ server configuration.\& On a Debian system, that'\&d be in
.PP
.nf
.RS 4
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu\&.sock:;
}
.fi

View File

@@ -19,7 +19,7 @@ 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|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://localhost:8080;
}
```
@@ -43,7 +43,7 @@ location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|archive)/ {
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;
@@ -81,7 +81,7 @@ server configuration. On a Debian system, that'd be in
"/etc/nginx/sites-available/default".
```
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|list|delete|rename|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu.sock:;
}
```

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NOTIFY" "1" "2024-02-17"
.TH "ODDMU-NOTIFY" "1" "2024-08-29"
.PP
.SH NAME
.PP
@@ -49,7 +49,7 @@ using the asterisk ('\&*'\&).\& If no such list exists, a new one is started at
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 EXAMPLE
.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

View File

@@ -42,7 +42,7 @@ 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 ('-').
# EXAMPLE
# 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

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-08-15"
.TH "ODDMU-RELEASES" "7" "2025-02-09"
.PP
.SH NAME
.PP
@@ -15,6 +15,53 @@ oddmu-releases - what'\&s new?\&
.PP
This page lists user-visible features and template changes to consider.\&
.PP
.SS 1.15 (2025)
.PP
Fix the hashtag detection.\& This was necessary to cut down on the many false
positives.\& They were most obvious with the \fIhashtags\fR subcommand.\& Now the
Markdown parser is used at startup to index the pages, making startup slower
(about twice as long with my blog).\& The Markdown parser is also used to parse
search terms (where it makes little difference).\&
.PP
Fix the timestamp for backup files.\& This was necessary because the diff didn'\&t
work as intended.\&
.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.\&

View File

@@ -8,6 +8,49 @@ oddmu-releases - what's new?
This page lists user-visible features and template changes to consider.
## 1.15 (2025)
Fix the hashtag detection. This was necessary to cut down on the many false
positives. They were most obvious with the _hashtags_ subcommand. Now the
Markdown parser is used at startup to index the pages, making startup slower
(about twice as long with my blog). The Markdown parser is also used to parse
search terms (where it makes little difference).
Fix the timestamp for backup files. This was necessary because the diff didn't
work as intended.
## 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.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-REPLACE" "1" "2024-02-17"
.TH "ODDMU-REPLACE" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-replace - replace text in Oddmu pages from the command-line
oddmu-replace - replace text in Oddmu pages
.PP
.SH SYNOPSIS
.PP
@@ -36,7 +36,7 @@ the term is a regular expression and the replacement can contain
backreferences ($1, $2, $3, etc.\&) to capture groups.\&
.PP
.RE
.SH EXAMPLE
.SH EXAMPLES
.PP
Replace "Oddmu" in the Markdown files of the current directory:
.PP

View File

@@ -2,7 +2,7 @@ ODDMU-REPLACE(1)
# NAME
oddmu-replace - replace text in Oddmu pages from the command-line
oddmu-replace - replace text in Oddmu pages
# SYNOPSIS
@@ -25,7 +25,7 @@ the current directory and its subdirectories.
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:

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-SEARCH" "1" "2024-07-30"
.TH "ODDMU-SEARCH" "1" "2024-08-29"
.PP
.SH NAME
.PP
oddmu-search - search the Oddmu pages from the command-line
oddmu-search - search the Oddmu pages
.PP
.SH SYNOPSIS
.PP
@@ -42,7 +42,7 @@ Limit search to a particular directory.\&
.RE
\fB-extract\fR
.RS 4
Print search extracts for interactive use from the command-line.\&
Print search extracts for interactive use
.RE
\fB-page\fR \fIn\fR
.RS 4
@@ -54,7 +54,7 @@ shown.\& This option allows you to view other pages.\&
Ignore pagination and just print a long list of results.\&
.PP
.RE
.SH EXAMPLE
.SH EXAMPLES
.PP
Search for the two words "Alex" and "Schroeder".\& All of the following are
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".\&

View File

@@ -2,7 +2,7 @@ ODDMU-SEARCH(1)
# NAME
oddmu-search - search the Oddmu pages from the command-line
oddmu-search - search the Oddmu pages
# SYNOPSIS
@@ -32,14 +32,14 @@ scored.
*-dir* _string_
Limit search to a particular directory.
*-extract*
Print search extracts for interactive use from the command-line.
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.
# EXAMPLE
# EXAMPLES
Search for the two words "Alex" and "Schroeder". All of the following are
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2024-03-12"
.TH "ODDMU-STATIC" "1" "2024-08-29"
.PP
.SH NAME
.PP
@@ -51,7 +51,7 @@ 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 EXAMPLE
.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

View File

@@ -44,7 +44,7 @@ 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.
# EXAMPLE
# 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

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-TEMPLATES" "5" "2024-07-21" "File Formats Manual"
.TH "ODDMU-TEMPLATES" "5" "2024-08-30" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -13,9 +13,8 @@ oddmu-templates - how to write the templates
.PP
.SH SYNOPSIS
.PP
These files act as HTML templates: \fIadd.\&html\fR, \fIdiff.\&html\fR, \fIedit.\&html\fR,
\fIfeed.\&html\fR, \fIpreview.\&html\fR, \fIsearch.\&html\fR, \fIstatic.\&html\fR, \fIupload.\&html\fR and
\fIview.\&html\fR.\& They contain special placeholders in double bracers {{like this}}.\&
Some HTML files act as templates.\& They contain special placeholders in double
bracers {{like this}}.\&
.PP
.SH DESCRIPTION
.PP
@@ -32,6 +31,8 @@ placeholders.\&
.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
@@ -108,6 +109,30 @@ An item is a page plus a date.\& All the properties of a page can be used (see
.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.\&
@@ -200,7 +225,7 @@ result is added to the "article" element for each snippet.\&
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 EXAMPLE
.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.\&

View File

@@ -6,9 +6,8 @@ oddmu-templates - how to write the templates
# SYNOPSIS
These files act as HTML templates: _add.html_, _diff.html_, _edit.html_,
_feed.html_, _preview.html_, _search.html_, _static.html_, _upload.html_ and
_view.html_. They contain special placeholders in double bracers {{like this}}.
Some HTML files act as templates. They contain special placeholders in double
bracers {{like this}}.
# DESCRIPTION
@@ -19,6 +18,7 @@ placeholders.
- _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_
@@ -85,6 +85,30 @@ An item is a page plus a date. All the properties of a page can be used (see
_{{.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.
@@ -177,7 +201,7 @@ result is added to the "article" element for each snippet.
point, the language isn't known, so "en" is used for the "html" element and no
language is used for the "textarea" element.
# EXAMPLE
# 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.

188
man/oddmu-webdav.5 Normal file
View File

@@ -0,0 +1,188 @@
.\" 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>.\&

169
man/oddmu-webdav.5.txt Normal file
View File

@@ -0,0 +1,169 @@
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

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-08-15"
.TH "ODDMU" "1" "2024-11-16"
.PP
.SH NAME
.PP
@@ -17,6 +17,8 @@ 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
@@ -75,6 +77,12 @@ directory:
.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
@@ -214,20 +222,32 @@ to generate the HTML for a single page, see \fIoddmu-html\fR(1)
to generate the HTML for the entire site, using Oddmu as a static site
generator, see \fIoddmu-static\fR(1)
.IP \(bu 4
to search a regular expression and replace it across all files, see
\fIoddmu-replace\fR(1)
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)
.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)
.PD
.PP
.SH EXAMPLE
.SH EXAMPLES
.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
@@ -268,9 +288,10 @@ it, it can'\&t be a page name.\& Filenames can contain slashes and Oddmu creates
subdirectories as necessary.\&
.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.\&
and file uploads, the old file is 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 backup also gets an
updated timestamp so that subsequent edits don'\&t immediately overwrite it.\&
.PP
The \fBindex\fR page is the default page.\& People visiting the "root" of the site are
redirected to "/view/index".\&
@@ -359,6 +380,8 @@ If you run Oddmu as a web server:
.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
@@ -367,32 +390,36 @@ Oddmu running as a webserver:
.PP
.PD 0
.IP \(bu 4
\fIoddmu-hashtags\fR(1), on how to count the hashtags used from the command-line
\fIoddmu-hashtags\fR(1), on how to count the hashtags used
.IP \(bu 4
\fIoddmu-html\fR(1), on how to render a page from the command-line
\fIoddmu-html\fR(1), on how to render a page
.IP \(bu 4
\fIoddmu-list\fR(1), on how to list pages and titles from the command-line
\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 from the
command-line
\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 from the command-line
\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 from the
command-line
\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 from the command-line
\fIoddmu-replace\fR(1), on how to search and replace text
.IP \(bu 4
\fIoddmu-search\fR(1), on how to run a search from the command-line
\fIoddmu-search\fR(1), on how to run a search
.IP \(bu 4
\fIoddmu-static\fR(1), on generating a static site from the command-line
\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 from the
command-line
\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
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

View File

@@ -10,6 +10,8 @@ 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
@@ -53,6 +55,9 @@ directory:
- _/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
@@ -174,16 +179,22 @@ Oddmu 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 search a regular expression and replace it across all files, see
_oddmu-replace_(1)
- to export the HTML for the entire site in one big feed, see _oddmu-export_(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)
# EXAMPLE
# EXAMPLES
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
@@ -220,9 +231,10 @@ it, it can't be a page name. Filenames can contain slashes and Oddmu creates
subdirectories as necessary.
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.
and file uploads, the old file is 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 backup also gets an
updated timestamp so that subsequent edits don't immediately overwrite it.
The *index* page is the default page. People visiting the "root" of the site are
redirected to "/view/index".
@@ -300,26 +312,28 @@ 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 from the command-line
- _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-links_(1), on how to list the outgoing links for a page from the
command-line
- _oddmu-missing_(1), on how to find broken local links from the command-line
- _oddmu-notify_(1), on updating index, changes and hashtag pages from the
command-line
- _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-toc_(1), on how to list the table of contents (toc) a page from the
command-line
- _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
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "5" "2024-07-31" "File Formats Manual"
.TH "ODDMU" "5" "2024-09-30" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -34,10 +34,9 @@ most importantly tables and definition lists.\&
.PP
.SS Local links
.PP
Local links use double square brackets [[like this]].\& 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.\& 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".\&
.PP
.SS Hashtags
.PP
@@ -90,8 +89,8 @@ Internet
.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
"@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
@@ -118,7 +117,7 @@ autolinking of "naked" URLs are supported
.IP \(bu 4
strikethrough using two tildes is supported (~~like this~~)
.IP \(bu 4
it is strict about prefix heading rules
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
@@ -127,12 +126,12 @@ trailing backslashes turn into line breaks
.PP
.SH FEEDS
.PP
Every file can be viewed as feed by using the extension ".\&rss".\& The feed items
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
Assume this is the index page.\& The feed would be "/view/index.\&rss".\& It would
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.\&
@@ -154,7 +153,8 @@ Recent posts:
.fi
.RE
.PP
The feed contains at most 10 items, starting at the top.\&
The feed contains at most 10 items, starting at the top.\& Thus, new items must be
added at the beginning of the list.\&
.PP
.SH PERCENT ENCODING
.PP

View File

@@ -25,10 +25,9 @@ most importantly tables and definition lists.
## Local links
Local links use double square brackets [[like this]]. 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. 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".
## Hashtags
@@ -75,8 +74,8 @@ 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
"\@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
@@ -97,18 +96,18 @@ The Markdown processor comes with a few extensions:
- 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
- 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
# FEEDS
Every file can be viewed as feed by using the extension ".rss". The feed items
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
("//").
Assume this is the index page. The feed would be "/view/index.rss". It would
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.
@@ -128,7 +127,8 @@ Recent posts:
* [Monophysitism](monophysitism)
```
The feed contains at most 10 items, starting at the top.
The feed contains at most 10 items, starting at the top. Thus, new items must be
added at the beginning of the list.
# PERCENT ENCODING

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU.SERVICE" "5" "2024-07-24"
.TH "ODDMU.SERVICE" "5" "2024-08-23"
.PP
.SH NAME
.PP
@@ -49,8 +49,7 @@ Install the service file and enable it:
.PP
.nf
.RS 4
ln -s /home/oddmu/oddmu\&.service /etc/systemd/system/
systemctl enable --now oddmu
sudo systemctl enable --now \&./oddmu\&.service
.fi
.RE
.PP
@@ -112,10 +111,8 @@ To install, enable and start both units:
.PP
.nf
.RS 4
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
ln -s /home/oddmu/oddmu-unix-domain\&.service /etc/systemd/system
systemctl enable --now oddmu-unix-domain\&.socket
systemctl enable --now oddmu-unix-domain\&.service
sudo systemctl enable --now \&./oddmu-unix-domain\&.socket
sudo systemctl enable --now \&./oddmu-unix-domain\&.service
.fi
.RE
.PP
@@ -134,8 +131,8 @@ Now you need to set up your web browser to use the Unix domain socket.\& See
.SS A personal wiki
.PP
On a single user machine, it might be useful to have a single wiki for the main
user available, on the standard port (80).\& In order to do this, setup a "user"
unit using systemd and save the following as "user-unix-domain.\&service":
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
@@ -151,30 +148,18 @@ StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_PORT=80"
Environment="ODDMU_LANGUAGES=de,en"
.fi
.RE
.PP
Since this is a priviledged port, the binary needs an extra capability for an
ordinary user to do this.\& This is necessary so that the files are created and
owned by the same user.\& Otherwise, the regular user wouldn'\&t be able to edit the
files using their favourite text editor.\&
.PP
.nf
.RS 4
sudo setcap \&'cap_net_bind_service=+ep\&' oddmu
.fi
.RE
.PP
Note that as soon as you recomile, the capability is gone again and the above
must be repeated.\&
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
systemctl --user enable --now \&./user-unix-domain\&.service
.fi
.RE
.PP
@@ -186,6 +171,40 @@ 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),
@@ -194,4 +213,3 @@ journalctl --user --unit user-unix-domain\&.service
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
.PP

View File

@@ -37,8 +37,7 @@ Environment="ODDMU_WEBFINGER=1"
Install the service file and enable it:
```
ln -s /home/oddmu/oddmu.service /etc/systemd/system/
systemctl enable --now oddmu
sudo systemctl enable --now ./oddmu.service
```
You should be able to visit the wiki at http://localhost:8080/.
@@ -90,10 +89,8 @@ Environment="ODDMU_WEBFINGER=1"
To install, enable and start both units:
```
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
ln -s /home/oddmu/oddmu-unix-domain.service /etc/systemd/system
systemctl enable --now oddmu-unix-domain.socket
systemctl enable --now oddmu-unix-domain.service
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)_:
@@ -109,8 +106,8 @@ _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, on the standard port (80). In order to do this, setup a "user"
unit using systemd and save the following as "user-unix-domain.service":
user available. In order to do this, setup a "user" unit using systemd and save
the following as "user-unix-domain.service":
```
[Unit]
@@ -125,26 +122,16 @@ StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_PORT=80"
Environment="ODDMU_LANGUAGES=de,en"
```
Since this is a priviledged port, the binary needs an extra capability for an
ordinary user to do this. This is necessary so that the files are created and
owned by the same user. Otherwise, the regular user wouldn't be able to edit the
files using their favourite text editor.
```
sudo setcap 'cap_net_bind_service=+ep' oddmu
```
Note that as soon as you recomile, the capability is gone again and the above
must be repeated.
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
systemctl --user enable --now ./user-unix-domain.service
```
To examine the log:
@@ -153,6 +140,38 @@ 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),
@@ -161,4 +180,3 @@ _systemd.socket_(5), _capabilities_(7)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

31
man/scdoc-to-markdown Executable file
View File

@@ -0,0 +1,31 @@
#!/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/^# (.*)/"## ".ucfirst(lc($1))/e;
# the new H1 title
s/^([A-Z.-]*\([1-9]\))( ".*")?$/"# ".lc($1)/e;
# 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

@@ -7,22 +7,26 @@ import (
"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, ".")
@@ -31,25 +35,81 @@ func TestManPages(t *testing.T) {
}
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")
main := string(b)
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, main, ref, ref)
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
@@ -57,14 +117,17 @@ func TestReadme(t *testing.T) {
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, main, ref, ref)
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)
@@ -83,6 +146,7 @@ func TestDocumentDependencies(t *testing.T) {
}
}
}
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 {

View File

@@ -8,11 +8,9 @@ import (
"github.com/gomarkdown/markdown/ast"
"github.com/google/subcommands"
"io"
"io/fs"
"net/url"
"os"
"path"
"path/filepath"
"strings"
)
@@ -34,23 +32,19 @@ func (cmd *missingCmd) SetFlags(f *flag.FlagSet) {
}
func (cmd *missingCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return missingCli(os.Stdout)
return missingCli(os.Stdout, &index)
}
func missingCli(w io.Writer) subcommands.ExitStatus {
names, err := existingPages()
if err != nil {
fmt.Fprintln(w, err)
return subcommands.ExitFailure
}
// 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, isPage := range names {
if !isPage {
continue
}
for name := range idx.titles {
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", p.Name, err)
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
for _, link := range p.links() {
@@ -66,13 +60,17 @@ func missingCli(w io.Writer) subcommands.ExitStatus {
u.Path = strings.TrimSuffix(u.Path, ".md")
// pages containing a colon need the ./ prefix
u.Path = strings.TrimPrefix(u.Path, "./")
// check whether the destinatino is a known page
// 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 := names[destination]
_, 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")
@@ -89,31 +87,6 @@ func missingCli(w io.Writer) subcommands.ExitStatus {
return subcommands.ExitSuccess
}
func existingPages() (map[string]bool, error) {
names := make(map[string]bool)
err := filepath.Walk(".", func(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
}
}
if strings.HasSuffix(path, ".md") {
name := filepath.ToSlash(strings.TrimSuffix(path, ".md"))
names[name] = true
} else {
names[path] = false
}
return nil
})
return names, err
}
// links parses the page content and returns an array of link destinations.
func (p *Page) links() []string {
var links []string

View File

@@ -9,7 +9,7 @@ import (
func TestMissingCmd(t *testing.T) {
b := new(bytes.Buffer)
s := missingCli(b)
s := missingCli(b, minimalIndex(t))
assert.Equal(t, subcommands.ExitSuccess, s)
r := `Page Missing
index test

14
page.go
View File

@@ -88,9 +88,10 @@ func (p *Page) save() error {
return os.WriteFile(fp, 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.
// 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. The
// backup file gets its modification time set to now so that subsequent edits don't immediately overwrite it again.
func backup(fp string) error {
_, err := os.Stat(fp)
if err != nil {
@@ -99,7 +100,12 @@ func backup(fp string) error {
bp := fp + "~"
fi, err := os.Stat(bp)
if err != nil || time.Since(fi.ModTime()).Minutes() >= 60 {
return os.Rename(fp, bp)
err = os.Rename(fp, bp)
if err != nil {
return err
}
ts := time.Now()
return os.Chtimes(bp, ts, ts)
}
return nil
}

View File

@@ -37,9 +37,13 @@ func wikiLink(fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)
// hashtag returns an inline parser function. This indirection is
// required because we want to receive an array of hashtags found.
// The hashtags in the array keep their case.
func hashtag() (func(p *parser.Parser, data []byte, offset int) (int, ast.Node), *[]string) {
hashtags := make([]string, 0)
return func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
if p.InsideLink {
return 0, nil
}
data = data[offset:]
i := 0
n := len(data)
@@ -64,15 +68,16 @@ func hashtag() (func(p *parser.Parser, data []byte, offset int) (int, ast.Node),
// @webfinger@accounts. It also uses the CommonExtensions and Block Attributes, and no MathJax ($).
func wikiParser() (*parser.Parser, *[]string) {
extensions := (parser.CommonExtensions | parser.AutoHeadingIDs | parser.Attributes) & ^parser.MathJax
parser := parser.NewWithExtensions(extensions)
prev := parser.RegisterInline('[', nil)
parser.RegisterInline('[', wikiLink(prev))
p := parser.NewWithExtensions(extensions)
prev := p.RegisterInline('[', nil)
p.RegisterInline('[', wikiLink(prev))
fn, hashtags := hashtag()
parser.RegisterInline('#', fn)
p.RegisterInline('#', fn)
if useWebfinger {
parser.RegisterInline('@', accountLink)
p.RegisterInline('@', accountLink)
parser.EscapeChars = append(parser.EscapeChars, '@')
}
return parser, hashtags
return p, hashtags
}
// wikiRenderer is a Renderer for Markdown that adds lazy loading of images and disables fractions support. Remember
@@ -85,7 +90,7 @@ func wikiRenderer() *html.Renderer {
return renderer
}
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Language, Page.Hashtags, and escapes Page.Name.
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Hashtags, and escapes Page.Name.
func (p *Page) renderHtml() {
parser, hashtags := wikiParser()
renderer := wikiRenderer()
@@ -147,6 +152,13 @@ func (p *Page) images() []ImageData {
return images
}
// hashtags returns an array of hashtags
func hashtags(s []byte) []string {
parser, hashtags := wikiParser()
markdown.Parse(s, parser)
return *hashtags
}
// toString for a node returns the text nodes' literals, concatenated. There is no whitespace added so the expectation
// is that there is only one child node. Otherwise, there may be a space missing between the literals, depending on the
// exact child nodes they belong to.

View File

@@ -51,11 +51,13 @@ I am cold, alone</p>
func TestPageHtmlHashtagCornerCases(t *testing.T) {
p := &Page{Body: []byte(`#
ok # #o #ok`)}
ok # #o #ok
[oh #ok \#nok](ok)`)}
p.renderHtml()
r := `<p>#</p>
<p>ok # <a class="tag" href="/search/?q=%23o">#o</a> <a class="tag" href="/search/?q=%23ok">#ok</a></p>
<p>ok # <a class="tag" href="/search/?q=%23o">#o</a> <a class="tag" href="/search/?q=%23ok">#ok</a>
<a href="ok">oh #ok #nok</a></p>
`
assert.Equal(t, r, string(p.Html))
}
@@ -109,3 +111,26 @@ func TestNoFractions(t *testing.T) {
p.renderHtml()
assert.Contains(t, string(p.Html), "1/6")
}
// webfinger
func TestAt(t *testing.T) {
// enable webfinger
useWebfinger = true
// prevent lookups
accounts.Lock()
accounts.uris = make(map[string]string)
accounts.uris["alex@alexschroeder.ch"] = "https://social.alexschroeder.ch/@alex";
accounts.Unlock()
// test account
p := &Page{Body: []byte(`My fedi handle is @alex@alexschroeder.ch.`)}
p.renderHtml()
assert.Contains(t,string(p.Html),
`My fedi handle is <a class="account" href="https://social.alexschroeder.ch/@alex" title="@alex@alexschroeder.ch">@alex</a>.`)
// test escaped account
p = &Page{Body: []byte(`My fedi handle is \@alex@alexschroeder.ch. \`)}
p.renderHtml()
assert.Contains(t,string(p.Html),
`My fedi handle is @alex@alexschroeder.ch.`)
// disable webfinger
useWebfinger = false
}

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<base href="/view/{{.Dir}}">
<title>Preview: {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }

17
preview_test.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestPreview(t *testing.T) {
cleanup(t, "testdata/preview")
data := url.Values{}
data.Set("body", "**Hallo**!")
r := assert.HTTPBody(makeHandler(previewHandler, false), "POST", "/view/testdata/preview/alex", data)
assert.Contains(t, r, "<strong>Hallo</strong>!")
}

View File

@@ -9,6 +9,7 @@ import (
"io"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
)
@@ -18,6 +19,8 @@ type searchCmd struct {
page int
all bool
extract bool
files bool
quiet bool
}
func (cmd *searchCmd) SetFlags(f *flag.FlagSet) {
@@ -25,12 +28,14 @@ func (cmd *searchCmd) SetFlags(f *flag.FlagSet) {
f.IntVar(&cmd.page, "page", 1, "the page in the search result set, default 1")
f.BoolVar(&cmd.all, "all", false, "show all the pages and ignore -page")
f.BoolVar(&cmd.extract, "extract", false, "print page extract instead of link list")
f.BoolVar(&cmd.files, "files", false, "show just the filenames")
f.BoolVar(&cmd.quiet, "quiet", false, "suppress summary line at the top")
}
func (*searchCmd) Name() string { return "search" }
func (*searchCmd) Synopsis() string { return "search pages and print a list of links" }
func (*searchCmd) Usage() string {
return `search [-dir string] [-page <n>|-all] [-extract] <terms>:
return `search [-dir string] [-page <n>|-all] [-extract|-files] [-quiet] <terms>:
Search for pages matching terms and print the result set as a
Markdown list. Before searching, all the pages are indexed. Thus,
startup is slow. The benefit is that the page order is exactly as
@@ -39,24 +44,24 @@ func (*searchCmd) Usage() string {
}
func (cmd *searchCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return searchCli(os.Stdout, cmd.dir, cmd.page, cmd.all, cmd.extract, false, f.Args())
return searchCli(os.Stdout, cmd, f.Args())
}
// searchCli runs the search command on the command line. It is used
// here with an io.Writer for easy testing.
func searchCli(w io.Writer, dir string, n int, all, extract bool, quiet bool, args []string) subcommands.ExitStatus {
dir, err := checkDir(dir)
func searchCli(w io.Writer, cmd *searchCmd, args []string) subcommands.ExitStatus {
dir, err := checkDir(cmd.dir)
if err != nil {
return subcommands.ExitFailure
}
index.reset()
index.load()
q := strings.Join(args, " ")
items, more := search(q, dir, "", n, true)
if !quiet {
items, more := search(q, dir, "", cmd.page, true)
if !cmd.quiet {
fmt.Fprint(os.Stderr, "Search for ", q)
if !all {
fmt.Fprint(os.Stderr, ", page ", n)
if !cmd.all {
fmt.Fprint(os.Stderr, ", page ", cmd.page)
}
fmt.Fprint(os.Stderr, ": ", len(items))
if len(items) == 1 {
@@ -65,8 +70,13 @@ func searchCli(w io.Writer, dir string, n int, all, extract bool, quiet bool, ar
fmt.Fprint(os.Stderr, " results\n")
}
}
if extract {
if cmd.extract {
searchExtract(w, items)
} else if cmd.files {
for _, p := range items {
name := filepath.FromSlash(p.Name) + ".md\n"
fmt.Fprintf(w, name)
}
} else {
for _, p := range items {
name := p.Name

View File

@@ -9,7 +9,7 @@ import (
func TestSearchCmd(t *testing.T) {
b := new(bytes.Buffer)
s := searchCli(b, "", 1, false, false, true, []string{"oddµ"})
s := searchCli(b, &searchCmd{quiet: true}, []string{"oddµ"})
assert.Equal(t, subcommands.ExitSuccess, s)
r := `* [Oddµ: A minimal wiki](README)
* [Themes](themes/index)
@@ -26,7 +26,7 @@ that before we type and speak
we hear that moment`)}
p.save()
b := new(bytes.Buffer)
s := searchCli(b, "testdata/search", 1, false, false, true, []string{"speak"})
s := searchCli(b, &searchCmd{dir: "testdata/search", quiet: true}, []string{"speak"})
assert.Equal(t, subcommands.ExitSuccess, s)
r := `* [Wait](wait)
`

View File

@@ -177,7 +177,7 @@ func TestTitleSearch(t *testing.T) {
index.load()
items, more := search("title:readme", "", "", 1, false)
assert.Equal(t, 0, len(items), "no page found")
assert.Equal(t, 1, len(items), "just one page found") // themes/plain/README
assert.False(t, more)
items, more = search("title:wel", "", "", 1, false) // README also contains "wel"

View File

@@ -221,7 +221,7 @@ func staticPage(source, target string) (*Page, error) {
func staticFeed(source, target string, p *Page, ti time.Time) error {
// render feed, maybe
base := filepath.Base(source)
_, ok := index.token["#"+strings.ToLower(base)]
_, ok := index.token[strings.ToLower(base)]
if base == "index" || ok {
f := feed(p, ti)
if len(f.Items) > 0 {

View File

@@ -15,7 +15,8 @@ import (
// templateFiles are the various HTML template files used. These files must exist in the root directory for Oddmu to be
// able to generate HTML output. This always requires a template.
var templateFiles = []string{"edit.html", "add.html", "view.html", "preview.html",
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
"diff.html", "search.html", "static.html", "upload.html", "feed.html",
"list.html" }
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See
// renderTemplate and loadTemplates.

View File

@@ -1,19 +1,21 @@
html { max-width: 80ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe }
body { hyphens: auto }
html { max-width: 80ch; padding: 1ch; margin: auto }
body { hyphens: auto; color: #111; background-color: #ffe }
header a { margin-right: 1ch }
h1 { text-wrap: balance }
footer { border-top: 1px solid #888 }
form, textarea { width: 97%; font-size: inherit }
pre { background-color: #fff; padding: 1ex 2ex; overflow-x: scroll }
pre { background-color: #fff; padding: 1ex 2ex; overflow-x: auto }
.diff { font-size: inherit; white-space: normal; overflow-wrap: break-word; background-color: white; border: 1px solid #333; padding: 1ch }
img { max-width: 100%; max-height: 90vh }
img, video { max-width: 100%; max-height: 90vh; width: auto; height: auto }
.right img { float: right; margin-left: 2em; margin-bottom: 1em; border: 1px solid #111 }
.left img { float: left; margin-right: 2em; margin-bottom: 1em; border: 1px solid #111 }
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px }
del { background-color: #fab }
ins { background-color: #af8 }
#search, #id { width: 30ch }
#search, #id { max-width: 30ch; width: calc(100% - 23ch) }
.result { font-size: larger }
.score { font-size: smaller; opacity: 0.8 }
.image { font-size: small; display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); }
#view form { margin-top: 2px }
#view button { width: 6ch }
#view label { display: inline-block; width: 10ch }
@@ -25,7 +27,7 @@ th { font-weight: normal }
th + th, td + td { padding-left: 1em }
@media (prefers-color-scheme: dark) {
html { color: #eeeee8; background-color: #333 }
body { color: #eeeee8; background-color: #333 }
footer { border-top: 1px solid #666 }
.diff { background-color: inherit; border: 1px solid #666 }
.right img { border: 1px solid #111 }

View File

@@ -12,5 +12,6 @@ your own sites.
Theoretical themes:
- [themes/chat](chat/README)
- [themes/plain](plain/README)
(Up to the [Welcome](../index) page.)

19
themes/plain/README.md Normal file
View File

@@ -0,0 +1,19 @@
Plain theme
===========
This makes it look as if the site consists mostly of editable plain
text. Accordingly, the user interface has been simplified and there
are no links to the preview, add, diff and upload actions and the
corresponding templates have been deleted. There is no special static
or feed template (mostly because the feed would depend on the list of
links that isn't rendered).
Now, the text is still saved in Markdown files and the Markdown is
still rendered to HTML but the "view" template just prints the page
body inside a "pre" block and ignores the rendered HTML.
This being text files, there are also no links to follow. That is why
there's no link here back to themes. Sorry!
This also means that you can only edit new files by editing the URL in
the address bar of your browser since you can't link to them.

19
themes/plain/edit.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Editing {{.Title}}</title>
<style>
form, textarea { width: 100% }
</style>
</head>
<body>
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" lang="{{.Language}}" autofocus>{{printf "%s" .Body}}</textarea>
<p><input type="submit" value="Save">
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
</form>
</body>
</html>

56
themes/plain/search.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Search for {{.Query}}</title>
<style>
header a { margin-right: 1ch; }
form { display: inline-block; }
input#search { width: 20ch; }
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
.result { font-size: larger }
.score { font-size: smaller; opacity: 0.8; }
.image { display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); font-size: small; }
.image img { max-width: 100%; }
</style>
</head>
<body>
<header>
<a href="#main">Skip navigation</a>
<a href="/view/index">Home</a>
<form role="search" action="/search/{{.Dir}}" method="GET">
<label for="search">Search:</label>
<input id="search" type="text" value="{{.Query}}" spellcheck="false" name="q" accesskey="f" placeholder="term #tag title:term blog:true" required>
<button>Go</button>
</form>
</header>
<main id="main">
{{if .Results}}
<p>
{{if gt .Page 2}}<a href="/search/{{.Dir}}?q={{.Query}}&page=1">First</a>{{end}}
{{if gt .Page 1}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Previous}}">Previous</a>{{end}}
Page {{.Page}}
{{if .More}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Next}}">Next</a>{{end}}
{{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}}
<p>
{{if gt .Page 2}}<a href="/search/{{.Dir}}?q={{.Query}}&page=1">First</a>{{end}}
{{if gt .Page 1}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Previous}}">Previous</a>{{end}}
Page {{.Page}}
{{if .More}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Next}}">Next</a>{{end}}
{{else}}
<p>No results.</p>
{{end}}
</main>
</body>
</html>

26
themes/plain/view.html Normal file
View File

@@ -0,0 +1,26 @@
<!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>
</head>
<body>
<header>
<a href="#main">Skip navigation</a>
<a href="/view/index">Home</a>
<a href="/edit/{{.Name}}" accesskey="e">Edit</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" required>
<button>Go</button>
</form>
</header>
<main>
<pre>
{{printf "%s" .Body}}
</pre>
</main>
</body>
</html>

View File

@@ -1,6 +1,10 @@
# Transjovian theme
This theme uses a nearly-black background and bright blue links.
This theme uses a nearly-black background and bright blue links. In
other words, it always looks like dark mode. This could be annoying,
but the site is for "a group of people living in the outer reaches of
our system, beyond Jupiter", so always dark seems appropriate.
There's an added "Raw" link that links to the Markdown file for the page.
(Back up to the [list of themes](../index).)

View File

@@ -81,8 +81,8 @@ func (p *Page) toc() Toc {
// other level one headings, this is a "regular" table of contents. For a regular table of contents, the first entry is
// skipped.
func (toc Toc) print(w io.Writer) {
minLevel := 0;
levelOneCount := 0;
minLevel := 0
levelOneCount := 0
for _, h := range toc {
if h.Level == 1 {
levelOneCount++
@@ -101,7 +101,7 @@ func (toc Toc) print(w io.Writer) {
}
fmt.Fprint(w, "* [")
for _, c := range h.GetChildren() {
fmt.Fprint(w, string(c.AsLeaf().Literal));
fmt.Fprint(w, string(c.AsLeaf().Literal))
}
fmt.Fprintf(w, "](#%s)\n", h.HeadingID)
}

View File

@@ -1,10 +1,8 @@
package main
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
// lowercaseFilter returns a slice of lower case tokens.
@@ -133,33 +131,3 @@ func highlightTokens(q string) []string {
tokens = lowercaseFilter(tokens)
return noPredicateFilter(tokens)
}
// hashtags returns a slice of hashtags. Use this to extract hashtags
// from a page body. This ignores Markdown completely.
func hashtags(s []byte) []string {
hashtags := make([]string, 0)
for {
i := bytes.IndexRune(s, '#')
if i == -1 {
return hashtags
}
if i > 0 && s[i-1] == '\\' {
s = s[i+1:]
continue
}
from := i
i++
for {
r, n := utf8.DecodeRune(s[i:])
if n > 0 && (unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_') {
i += n
} else {
break
}
}
if i > from+1 { // not just "#"
hashtags = append(hashtags, string(bytes.ToLower(s[from:i])))
}
s = s[i:]
}
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestHashtags(t *testing.T) {
assert.EqualValues(t, []string{"#truth"}, hashtags([]byte("This is boring. #Truth")), "hashtags")
assert.EqualValues(t, []string{"Truth"}, hashtags([]byte("This is boring. #Truth")), "hashtags")
}
func TestEscapedHashtags(t *testing.T) {

View File

@@ -106,7 +106,7 @@ window.addEventListener('load', uploadFiles.init);
Picture metadata is only removed if the pictures gets resized.
Providing a new max width is recommended for all pictures.
If youre uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
<p>To delete a file, upload an empty file.
<p>You can delete and rename files <a href="/list/{{.Dir}}">from the file list</a>.
<p><label for="file">Files to upload:</label>
<input type="file" name="file" required multiple>
<p><input type="submit" value="Save">

View File

@@ -5,7 +5,7 @@ package main
import (
"fmt"
_ "github.com/bashdrew/goheif"
_ "github.com/gen2brain/heic"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"io"

View File

@@ -179,6 +179,9 @@ func serve() {
http.HandleFunc("/append/", makeHandler(appendHandler, true))
http.HandleFunc("/upload/", makeHandler(uploadHandler, false))
http.HandleFunc("/drop/", makeHandler(dropHandler, false))
http.HandleFunc("/list/", makeHandler(listHandler, false))
http.HandleFunc("/delete/", makeHandler(deleteHandler, true))
http.HandleFunc("/rename/", makeHandler(renameHandler, true))
http.HandleFunc("/search/", makeHandler(searchHandler, false))
go scheduleLoadIndex()
go scheduleLoadLanguages()
@@ -200,6 +203,7 @@ func commands() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&exportCmd{}, "")
subcommands.Register(&hashtagsCmd{}, "")
subcommands.Register(&htmlCmd{}, "")
subcommands.Register(&listCmd{}, "")

View File

@@ -7,6 +7,7 @@ import (
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"testing"
@@ -114,3 +115,19 @@ func cleanup(t *testing.T, dir string) {
}
})
}
// minimalIndex creates a new indexStore containing just the known Markdown files without any additional Markdown files
// from testdata.
func minimalIndex(t *testing.T) *indexStore {
idx := &indexStore{}
idx.reset()
names := []string{"index", "README"}
for _, name := range names {
p, err := loadPage(name)
assert.NoError(t, err)
idx.addPage(p)
}
err := filepath.Walk("themes", idx.walk)
assert.NoError(t, err)
return idx
}