forked from mirror/oddmu
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7110e0af68 | ||
|
|
8841372814 | ||
|
|
fefa283775 | ||
|
|
5a09d65dab | ||
|
|
2cf0855994 | ||
|
|
f98312e12f | ||
|
|
d213ee2815 | ||
|
|
0cd09666c6 | ||
|
|
bd9364dc09 | ||
|
|
93fd49bc4c | ||
|
|
300e411960 | ||
|
|
10cea2bf2c | ||
|
|
830af140eb | ||
|
|
c758dd7df7 | ||
|
|
969df2aef9 | ||
|
|
39f414694c | ||
|
|
fa67508692 | ||
|
|
d5696135c1 | ||
|
|
284fc3094d | ||
|
|
57161bbc98 | ||
|
|
d855d9d91a | ||
|
|
ca85250514 | ||
|
|
649fde81fe | ||
|
|
8a47e9c5fe | ||
|
|
fd9a515e0f | ||
|
|
da04c6dc27 | ||
|
|
bd2da1414c | ||
|
|
6d1a5462b4 | ||
|
|
3dcaf8aca1 | ||
|
|
80ce16f873 | ||
|
|
41347ad5dc | ||
|
|
6a911b2860 | ||
|
|
1d6db77660 | ||
|
|
8a8afcb56f | ||
|
|
6803b8e90d | ||
|
|
ff357a4048 | ||
|
|
77a38ddf66 | ||
|
|
d3ffe82a90 | ||
|
|
4a12721462 | ||
|
|
07b1277764 | ||
|
|
f99a54e2ef | ||
|
|
56a4461bd6 | ||
|
|
2e38daf667 | ||
|
|
a5421372d8 | ||
|
|
7017363f6a | ||
|
|
9a7a1ee2a9 | ||
|
|
76d7598854 | ||
|
|
d7b48b975b | ||
|
|
4314a35d1d | ||
|
|
63e1c987f2 | ||
|
|
7d748f82da | ||
|
|
8385bc424a | ||
|
|
820763bf23 | ||
|
|
7d40fa4adb | ||
|
|
6e24603c27 | ||
|
|
5096627b87 | ||
|
|
0c691123ff | ||
|
|
5770966cdd | ||
|
|
a001a77692 | ||
|
|
0e29ed77ea | ||
|
|
173bb62a79 | ||
|
|
58249aac85 | ||
|
|
1fdd502e95 | ||
|
|
196ff605c3 | ||
|
|
6c1e595f13 | ||
|
|
34fdb5d9a9 | ||
|
|
b28614fa52 | ||
|
|
e7511ed059 | ||
|
|
a5d03dd136 | ||
|
|
87d5efcb7a | ||
|
|
c9bb062a04 | ||
|
|
e2eec5e052 | ||
|
|
26033de177 | ||
|
|
e1ba007f97 | ||
|
|
e90ff9e7dd | ||
|
|
70356e850a | ||
|
|
81a59fd6ac | ||
|
|
52d6f26eed | ||
|
|
171910ff4f | ||
|
|
5fb0f57b5c | ||
|
|
d712b132cc | ||
|
|
199c236c08 | ||
|
|
0e7f7a2c05 | ||
|
|
4af15b48db | ||
|
|
9b6c54ccb4 | ||
|
|
83f447b643 | ||
|
|
d5e37fa90a | ||
|
|
609da1fbc2 | ||
|
|
ba32e0dcce | ||
|
|
e975c527d1 | ||
|
|
656b9490a1 | ||
|
|
9bd7ca59fa | ||
|
|
56f95553d6 | ||
|
|
76e63278d6 | ||
|
|
1e957b5411 | ||
|
|
e666fb44cb | ||
|
|
754bf11516 | ||
|
|
7eeb81fa94 | ||
|
|
9c70935362 | ||
|
|
9d65c01bb0 | ||
|
|
0179d393dd | ||
|
|
f8b97f794b | ||
|
|
b801f83fe0 | ||
|
|
486c3f8620 | ||
|
|
5b0fcdd69f | ||
|
|
bb99d05a0d | ||
|
|
98358a008b | ||
|
|
51c8348ef7 | ||
|
|
5e77f1332e | ||
|
|
fbbb4a543f | ||
|
|
ccc7c0bc8f | ||
|
|
aae2ae1265 | ||
|
|
8929d72acd | ||
|
|
014507ce4e | ||
|
|
554a929bf5 | ||
|
|
5f8e006594 | ||
|
|
e347a59603 | ||
|
|
964dc3bf4a | ||
|
|
d5f8b280ac | ||
|
|
8ee5705ae7 | ||
|
|
43bf1574c9 | ||
|
|
1c8af9fcdb | ||
|
|
f6fa76bd5f | ||
|
|
111c617556 | ||
|
|
66fe28062d | ||
|
|
7e03b67267 | ||
|
|
11343067af | ||
|
|
a0ff3ed03c | ||
|
|
ccead37f44 | ||
|
|
a8b4ec9acd | ||
|
|
2531a469bf | ||
|
|
51808bc1fb | ||
|
|
2375dad845 | ||
|
|
0ca53690d8 | ||
|
|
a0c7517e8a | ||
|
|
912b6baad0 | ||
|
|
b6c068c72f | ||
|
|
89ef292736 | ||
|
|
c658de5a6f | ||
|
|
4bab25e2ac | ||
|
|
c518a193d0 | ||
|
|
2dc950cb5e | ||
|
|
87d1e72f0f |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
/oddmu
|
||||
test.md
|
||||
/testdata/
|
||||
/oddmu-darwin-*
|
||||
/oddmu-linux-*
|
||||
/oddmu-windows-*
|
||||
/oddmu.exe
|
||||
/oddmu
|
||||
|
||||
47
Makefile
47
Makefile
@@ -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,18 +41,38 @@ 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='*~' \
|
||||
$< Makefile *.socket *.service *.md man/Makefile man/*.1 man/*.5 man/*.7 themes/
|
||||
tar --create --file $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
|
||||
$< *.html Makefile *.socket *.service *.md man/Makefile man/*.1 man/*.5 man/*.7 themes/
|
||||
|
||||
priv:
|
||||
sudo setcap 'cap_net_bind_service=+ep' oddmu
|
||||
|
||||
189
README.md
189
README.md
@@ -1,4 +1,4 @@
|
||||
# Oddµ: A minimal wiki
|
||||
# Oddμ: A minimal wiki
|
||||
|
||||
This program helps you run a minimal wiki, blog, digital garden, memex
|
||||
or Zettelkasten. There is no version history.
|
||||
@@ -19,7 +19,7 @@ write-access to the site.
|
||||
It's well suited as a simple static site generator. There are no
|
||||
plugins.
|
||||
|
||||
When Oddµ runs as a web server, it serves all the Markdown files
|
||||
When Oddμ runs as a web server, it serves all the Markdown files
|
||||
(ending in `.md`) as web pages. These pages can be edited via the web.
|
||||
|
||||
Oddmu adds the following extensions to Markdown: local links `[[like
|
||||
@@ -31,8 +31,8 @@ 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
|
||||
(resulting in `.jpg` or `.png` files).
|
||||
`.png`, `.heic` or `.webp`) can be resized when they are uploaded
|
||||
(resulting in `.jpg`, `.png` or `.webp` files).
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -40,84 +40,115 @@ This project uses man(1) pages. They are generated from text files
|
||||
using [scdoc](https://git.sr.ht/~sircmpwn/scdoc). These are the files
|
||||
available:
|
||||
|
||||
[oddmu(1)](/oddmu.git/blob/main/man/oddmu.1.txt): This man page has a
|
||||
short introduction to Oddmu, its configuration via templates and
|
||||
[oddmu(1)](https://alexschroeder.ch/view/oddmu/oddmu.1): This man page
|
||||
has a short introduction to Oddmu, its configuration via templates and
|
||||
environment variables, plus points to the other man pages.
|
||||
|
||||
[oddmu(5)](/oddmu.git/blob/main/man/oddmu.5.txt): This man page talks
|
||||
about the Markdown and includes some examples for the non-standard
|
||||
features such as table markup. It also talks about the Oddmu
|
||||
extensions to Markdown: wiki links, hashtags and fediverse account
|
||||
links. Local links must use percent encoding for page names so there
|
||||
is a section about percent encoding. The man page also explains how
|
||||
feeds are generated.
|
||||
[oddmu(5)](https://alexschroeder.ch/view/oddmu/oddmu.5): This man page
|
||||
talks about the Markdown and includes some examples for the
|
||||
non-standard features such as table markup. It also talks about the
|
||||
Oddmu extensions to Markdown: wiki links, hashtags and fediverse
|
||||
account links. Local links must use percent encoding for page names so
|
||||
there is a section about percent encoding. The man page also explains
|
||||
how feeds are generated.
|
||||
|
||||
[oddmu-releases(7)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
|
||||
[oddmu-releases(7)](https://alexschroeder.ch/view/oddmu/oddmu-releases.7):
|
||||
This man page lists all the Oddmu versions and their user-visible
|
||||
changes.
|
||||
|
||||
[oddmu-releases(7)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
|
||||
This man page lists all the Oddmu versions and their user-visible
|
||||
changes.
|
||||
[oddmu-version(1)](https://alexschroeder.ch/view/oddmu/oddmu-version.1):
|
||||
This man page documents the "version" subcommand which you can use to
|
||||
get the installed Oddmu version.
|
||||
|
||||
[oddmu-version(1)](/oddmu.git/blob/main/man/oddmu-version.1.txt): This
|
||||
man page documents the "version" subcommand which you can use to get
|
||||
installed Oddmu version.
|
||||
Working locally:
|
||||
|
||||
[oddmu-list(1)](/oddmu.git/blob/main/man/oddmu-list.1.txt): This man
|
||||
page documents the "list" subcommand which you can use to get page
|
||||
names and page titles.
|
||||
[oddmu-links(1)](https://alexschroeder.ch/view/oddmu/oddmu-links.1):
|
||||
This man page documents the "links" subcommand which you can use to
|
||||
get the outgoing links for a page.
|
||||
|
||||
[oddmu-search(1)](/oddmu.git/blob/main/man/oddmu-search.1.txt): This
|
||||
man page documents the "search" subcommand which you can use to build
|
||||
indexes – lists of page links. These are important for feeds.
|
||||
[oddmu-list(1)](https://alexschroeder.ch/view/oddmu/oddmu-list.1):
|
||||
This man page documents the "list" subcommand which you can use to get
|
||||
page names and page titles.
|
||||
|
||||
[oddmu-search(7)](/oddmu.git/blob/main/man/oddmu-search.7.txt): This
|
||||
man page documents how search and scoring work.
|
||||
[oddmu-replace(1)](https://alexschroeder.ch/view/oddmu/oddmu-replace.1):
|
||||
This man page documents the "replace" subcommand to make mass changes
|
||||
to the files much like find(1), grep(1) and sed(1) or perl(1).
|
||||
|
||||
[oddmu-filter(7)](/oddmu.git/blob/main/man/oddmu-filter.7.txt): This
|
||||
man page documents how to exclude subdirectories from search and
|
||||
archiving.
|
||||
[oddmu-search(1)](https://alexschroeder.ch/view/oddmu/oddmu-search.1):
|
||||
This man page documents the "search" subcommand which you can use to
|
||||
build indexes – lists of page links. These are important for feeds.
|
||||
|
||||
[oddmu-replace(1)](/oddmu.git/blob/main/man/oddmu-replace.1.txt): This
|
||||
man page documents the "replace" subcommand to make mass changes to
|
||||
the files much like find(1), grep(1) and sed(1) or perl(1).
|
||||
[oddmu-search(7)](https://alexschroeder.ch/view/oddmu/oddmu-search.7):
|
||||
This man page documents how search and scoring work.
|
||||
|
||||
[oddmu-missing(1)](/oddmu.git/blob/main/man/oddmu-missing.1.txt): This
|
||||
man page documents the "missing" subcommand to list local links that
|
||||
don't point to any existing pages or files.
|
||||
[oddmu-toc(1)](https://alexschroeder.ch/view/oddmu/oddmu-toc.1): This
|
||||
man page documents the "toc" subcommand which you can use to generate
|
||||
a table of contents linking to all the headings on the page.
|
||||
|
||||
[oddmu-html(1)](/oddmu.git/blob/main/man/oddmu-html.1.txt): This man
|
||||
page documents the "html" subcommand to generate HTML from Markdown
|
||||
pages from the command line.
|
||||
Reporting:
|
||||
|
||||
[oddmu-static(1)](/oddmu.git/blob/main/man/oddmu-static.1.txt): This
|
||||
man page documents the "static" subcommand to generate an entire
|
||||
[oddmu-missing(1)](https://alexschroeder.ch/view/oddmu/oddmu-missing.1):
|
||||
This man page documents the "missing" subcommand to list local links
|
||||
that don't point to any existing pages or files.
|
||||
|
||||
[oddmu-hashtags(1)](https://alexschroeder.ch/view/oddmu/oddmu-hashtags.1):
|
||||
This man page documents the "hashtags" subcommand to count the
|
||||
hashtags used from the command line.
|
||||
|
||||
Static site generator:
|
||||
|
||||
[oddmu-html(1)](https://alexschroeder.ch/view/oddmu/oddmu-html.1):
|
||||
This man page documents the "html" subcommand to generate HTML from
|
||||
Markdown pages from the command line.
|
||||
|
||||
[oddmu-static(1)](https://alexschroeder.ch/view/oddmu/oddmu-static.1):
|
||||
This man page documents the "static" subcommand to generate an entire
|
||||
static website from the command line, avoiding the need to run Oddmu
|
||||
as a server. Also great for archiving.
|
||||
|
||||
[oddmu-notify(1)](/oddmu.git/blob/main/man/oddmu-notify.1.txt): This
|
||||
man page documents the "notify" subcommand to add links to hashtag
|
||||
pages, index and changes for a given page. This is useful when you
|
||||
edit the Markdown files locally.
|
||||
[oddmu-notify(1)](https://alexschroeder.ch/view/oddmu/oddmu-notify.1):
|
||||
This man page documents the "notify" subcommand to add links to
|
||||
hashtag pages, index and changes for a given page. This is useful when
|
||||
you edit the Markdown files locally.
|
||||
|
||||
[oddmu-templates(5)](/oddmu.git/blob/main/man/oddmu-templates.5.txt):
|
||||
Configuration:
|
||||
|
||||
[oddmu-templates(5)](https://alexschroeder.ch/view/oddmu/oddmu-templates.5):
|
||||
This man page documents how the templates can be changed (how they
|
||||
*must* be changed) and lists the attributes available for the various
|
||||
templates.
|
||||
|
||||
[oddmu-apache(5)](/oddmu.git/blob/main/man/oddmu-apache.5.txt): This
|
||||
man page documents how to set up the Apache web server for various
|
||||
common tasks such as using logins to limit what visitors can edit.
|
||||
System administration:
|
||||
|
||||
[oddmu-nginx(5)](/oddmu.git/blob/main/man/oddmu-nginx.5.txt): This man
|
||||
page documents how to set up the freenginx web server for various
|
||||
common tasks such as using logins to limit what visitors can edit.
|
||||
[oddmu-apache(5)](https://alexschroeder.ch/view/oddmu/oddmu-apache.5):
|
||||
This man page documents how to set up the Apache web server for
|
||||
various common tasks such as using logins to limit what visitors can
|
||||
edit.
|
||||
|
||||
[oddmu.service(5)](/oddmu.git/blob/main/man/oddmu.service.5.txt): This
|
||||
man page documents how to setup a systemd unit and have it manage
|
||||
[oddmu-filter(7)](https://alexschroeder.ch/view/oddmu/oddmu-filter.7):
|
||||
This man page documents how to exclude subdirectories from search and
|
||||
archiving.
|
||||
|
||||
[oddmu-nginx(5)](https://alexschroeder.ch/view/oddmu/oddmu-nginx.5):
|
||||
This man page documents how to set up the freenginx web server for
|
||||
various common tasks such as using logins to limit what visitors can
|
||||
edit.
|
||||
|
||||
[oddmu.service(5)](https://alexschroeder.ch/view/oddmu/oddmu.service.5):
|
||||
This man page documents how to setup a systemd unit and have it manage
|
||||
Oddmu. “Great configurability brings great burdens.”
|
||||
|
||||
[oddmu-webdav(5)](https://alexschroeder.ch/view/oddmu/oddmu-webdav.5):
|
||||
This man page documents how to set up the Apache web server so that
|
||||
the wiki can be accessed via Web-DAV.
|
||||
|
||||
Leaving:
|
||||
|
||||
[oddmu-export(1)](https://alexschroeder.ch/view/oddmu/oddmu-export.1):
|
||||
This man page documents how to export all the pages as one RSS feed so
|
||||
that you can import them all into a new platform that doesn't use
|
||||
Markdown files.
|
||||
|
||||
## Building
|
||||
|
||||
To build the binary:
|
||||
@@ -136,8 +167,6 @@ make docs
|
||||
The `Makefile` in the `man` directory has targets to create Markdown
|
||||
and HTML files.
|
||||
|
||||
The HEIC library uses C code and prevents cross-compilation.
|
||||
|
||||
As the repository changed URLs a few times (from GitHub, to
|
||||
self-hosted using `cgit` to self-hosted using `legit`), there is no
|
||||
way to install it using `go install`. You need to `git clone` the
|
||||
@@ -178,9 +207,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
|
||||
@@ -211,6 +240,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
|
||||
@@ -248,18 +278,10 @@ extensions can be added.
|
||||
|
||||
### Filenames and URL path
|
||||
|
||||
One of the sad parts of the code is the distinction between path and
|
||||
filepath. On a Linux system, this doesn't matter. I suspect that it
|
||||
also doesn't matter on MacOS and Windows because the file systems
|
||||
handle forward slashes just fine. The code still tries to do the right
|
||||
thing. A path that is derived from a URL is a path with slashes.
|
||||
Before accessing a file, it has to be turned into a filepath using
|
||||
`filepath.FromSlashes` and in the rare case where the inverse happens,
|
||||
use `filepath.ToSlashes`. Any path received via the URL path uses
|
||||
slashes and needs to be converted to a filepath before passing it to
|
||||
any `os` function. Any path received within a `path/filepath.WalkFunc`
|
||||
is a filepath and needs to be converted to use slashes when used in
|
||||
HTML output.
|
||||
There are some simplifications made. The code doesn't consider the
|
||||
various encodings (UTF-8 NFC on the web vs UTF-8 NFD for HFS+, for
|
||||
example; it also doesn't check for characters in page names that are
|
||||
illegal filenames on the filesystem used).
|
||||
|
||||
If you need to access the page name in code that is used from a
|
||||
template, you have to decode the path. See the code in `diff.go` for
|
||||
@@ -280,6 +302,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
|
||||
@@ -301,9 +337,12 @@ 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
|
||||
used to decode HEIC files (the new default file format for photos on
|
||||
iPhones). LGPL-3.0-only.
|
||||
[github.com/gen2brain/heic](https://github.com/gen2brain/heic) is used
|
||||
to decode HEIC files (the new default file format for photos on
|
||||
iPhones). MIT.
|
||||
|
||||
[github.com/gen2brain/webp](https://github.com/gen2brain/webp) is used
|
||||
to encode and decode WebP files. MIT.
|
||||
|
||||
[github.com/disintegration/imaging](https://github.com/disintegration/imaging)
|
||||
is used to resize images. MIT.
|
||||
|
||||
10
RELEASE
10
RELEASE
@@ -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
|
||||
|
||||
@@ -11,4 +13,10 @@ When preparing a new release
|
||||
|
||||
5. Tag the release and push the tag to all remotes
|
||||
|
||||
6. cd man && make upload
|
||||
6. cd man && make upload
|
||||
|
||||
7. make dist
|
||||
|
||||
8. create a new release at https://github.com/kensanata/oddmu/releases
|
||||
|
||||
9. upload the four .tar.gz binaries to the GitHub release
|
||||
|
||||
13
add.html
13
add.html
@@ -3,20 +3,23 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Add to {{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
form, textarea { width: 100%; }
|
||||
html { max-width: 70ch; padding: 1ch; height: calc(100% - 2ch); margin: auto }
|
||||
body { hyphens: auto; color: #111; background-color: #ffe; margin: 0; padding: 0; height: 100%; display: flex; flex-flow: column }
|
||||
form, textarea { box-sizing: border-box; width: 100%; font-size: inherit }
|
||||
#editor { flex: 1 1 auto; display: flex; flex-flow: column }
|
||||
textarea { flex: 1 1 auto }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Adding to {{.Title}}</h1>
|
||||
<form action="/append/{{.Name}}" method="POST">
|
||||
<form id="editor" action="/append/{{.Path}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="{{.Language}}" autofocus required></textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Add">
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
<a href="/view/{{.Path}}"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -48,7 +48,7 @@ func appendHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+name, http.StatusFound)
|
||||
http.Redirect(w, r, "/view/" + nameEscape(name), http.StatusFound)
|
||||
}
|
||||
|
||||
func (p *Page) append(body []byte) {
|
||||
|
||||
20
archive.go
20
archive.go
@@ -16,7 +16,7 @@ import (
|
||||
// are skipped. If the environment variable ODDMU_FILTER is a regular expression that matches the starting directory,
|
||||
// this is a "separate site"; if the regular expression does not match, this is the "main site" and page names must also
|
||||
// not match the regular expression.
|
||||
func archiveHandler(w http.ResponseWriter, r *http.Request, path string) {
|
||||
func archiveHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
filter := os.Getenv("ODDMU_FILTER")
|
||||
re, err := regexp.Compile(filter)
|
||||
if err != nil {
|
||||
@@ -24,30 +24,30 @@ func archiveHandler(w http.ResponseWriter, r *http.Request, path string) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
matches := re.MatchString(path)
|
||||
dir := filepath.Dir(filepath.FromSlash(path))
|
||||
matches := re.MatchString(name)
|
||||
dir := filepath.Dir(filepath.FromSlash(name))
|
||||
z := zip.NewWriter(w)
|
||||
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
||||
err = filepath.Walk(dir, func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
|
||||
if fp != "." && strings.HasPrefix(filepath.Base(fp), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
} else if !strings.HasPrefix(filepath.Base(path), ".") &&
|
||||
(matches || !re.MatchString(path)) {
|
||||
zf, err := z.Create(path)
|
||||
} else if !strings.HasPrefix(filepath.Base(fp), ".") &&
|
||||
(matches || !re.MatchString(filepath.ToSlash(fp))) {
|
||||
zf, err := z.Create(fp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
f, err := os.Open(fp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(zf, file)
|
||||
_, err = io.Copy(zf, f)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
|
||||
@@ -20,10 +20,10 @@ func (p *Page) notify() error {
|
||||
if p.Title == "" {
|
||||
p.Title = p.Name
|
||||
}
|
||||
esc := nameEscape(path.Base(p.Name))
|
||||
esc := nameEscape(p.Base())
|
||||
link := "* [" + p.Title + "](" + esc + ")\n"
|
||||
re := regexp.MustCompile(`(?m)^\* \[[^\]]+\]\(` + esc + `\)\n`)
|
||||
dir := path.Dir(p.Name)
|
||||
dir := p.Dir()
|
||||
err := addLinkWithDate(path.Join(dir, "changes"), link, re)
|
||||
if err != nil {
|
||||
log.Printf("Updating changes in %s failed: %s", dir, err)
|
||||
@@ -31,7 +31,7 @@ func (p *Page) notify() error {
|
||||
}
|
||||
if p.IsBlog() {
|
||||
// Add to the index only if the blog post is for the current year
|
||||
if strings.HasPrefix(path.Base(p.Name), time.Now().Format("2006")) {
|
||||
if strings.HasPrefix(p.Base(), time.Now().Format("2006")) {
|
||||
err := addLink(path.Join(dir, "index"), true, link, re)
|
||||
if err != nil {
|
||||
log.Printf("Updating index in %s failed: %s", dir, err)
|
||||
|
||||
7
diff.go
7
diff.go
@@ -6,7 +6,6 @@ import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -25,11 +24,7 @@ func diffHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
|
||||
// Diff computes the diff for a page. At this point, renderHtml has already been called so the Name is escaped.
|
||||
func (p *Page) Diff() template.HTML {
|
||||
path, err := url.PathUnescape(p.Name)
|
||||
if err != nil {
|
||||
return template.HTML("Cannot unescape " + p.Name)
|
||||
}
|
||||
fp := filepath.FromSlash(path)
|
||||
fp := filepath.FromSlash(p.Name)
|
||||
a := fp + ".md~"
|
||||
t1, err := os.ReadFile(a)
|
||||
if err != nil {
|
||||
|
||||
10
diff.html
10
diff.html
@@ -3,11 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe }
|
||||
body { hyphens: auto }
|
||||
del { background-color: #fab }
|
||||
ins { background-color: #af8 }
|
||||
pre { white-space: normal; background-color: white; border: 1px solid #eee; padding: 1ch }
|
||||
@@ -15,11 +15,11 @@ pre { white-space: normal; background-color: white; border: 1px solid #eee; padd
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/view/{{.Name}}">Back</a>
|
||||
<a href="/view/{{.Path}}">Back</a>
|
||||
</header>
|
||||
<main id="main">
|
||||
<h1>{{.Title}}</h1>
|
||||
<p>This is the diff between <a href="/view/{{.Name}}.md~">the backup</a> and <a href="/view/{{.Name}}.md">the current copy</a>.</p>
|
||||
<p>This is the diff between <a href="/view/{{.Path}}.md~">the backup</a> and <a href="/view/{{.Path}}.md">the current copy</a>.</p>
|
||||
<pre>
|
||||
{{.Diff}}
|
||||
</pre>
|
||||
|
||||
15
diff_test.go
15
diff_test.go
@@ -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>`)
|
||||
}
|
||||
|
||||
16
edit.html
16
edit.html
@@ -3,23 +3,27 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<base href="/view/{{.Dir}}">
|
||||
<title>Editing {{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
form, textarea { width: 100%; }
|
||||
html { max-width: 70ch; padding: 1ch; height: calc(100% - 2ch); margin: auto }
|
||||
body { hyphens: auto; color: #111; background-color: #ffe; margin: 0; padding: 0; height: 100%; display: flex; flex-flow: column }
|
||||
form, textarea { box-sizing: border-box; width: 100%; font-size: inherit }
|
||||
#editor { flex: 1 1 auto; display: flex; flex-flow: column }
|
||||
textarea { flex: 1 1 auto }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Editing {{.Title}}</h1>
|
||||
<form action="/save/{{.Name}}" method="POST">
|
||||
<form id="editor" action="/save/{{.Path}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" placeholder="# Title
|
||||
|
||||
Text" lang="{{.Language}}" autofocus>{{printf "%s" .Body}}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Save">
|
||||
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
<button formaction="/preview/{{.Path}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Path}}"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,5 +41,5 @@ func saveHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+name, http.StatusFound)
|
||||
http.Redirect(w, r, "/view/" + nameEscape(name), http.StatusFound)
|
||||
}
|
||||
|
||||
112
export_cmd.go
Normal file
112
export_cmd.go
Normal 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
55
export_cmd_test.go
Normal 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("<h1.*>Welcome to Oddμ</h1>"), b.String()) // skip id
|
||||
}
|
||||
2
feed.go
2
feed.go
@@ -63,7 +63,7 @@ func feed(p *Page, ti time.Time) *Feed {
|
||||
if !ok || bytes.Contains(link.Destination, []byte("//")) {
|
||||
return ast.GoToNext
|
||||
}
|
||||
name := path.Join(path.Dir(p.Name), string(link.Destination))
|
||||
name := path.Join(p.Dir(), string(link.Destination))
|
||||
fi, err := os.Stat(filepath.FromSlash(name) + ".md")
|
||||
if err != nil {
|
||||
return ast.GoToNext
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<link>https://example.org/</link>
|
||||
<managingEditor>you@example.org (Your Name)</managingEditor>
|
||||
<webMaster>you@example.org (Your Name)</webMaster>
|
||||
<atom:link href="https://example.org/view/{{.Name}}.rss" rel="self" type="application/rss+xml"/>
|
||||
<atom:link href="https://example.org/view/{{.Path}}.rss" rel="self" type="application/rss+xml"/>
|
||||
<description>This is the digital garden of Your Name.</description>
|
||||
<image>
|
||||
<url>https://example.org/view/logo.jpg</url>
|
||||
@@ -15,8 +15,8 @@
|
||||
{{range .Items}}
|
||||
<item>
|
||||
<title>{{.Title}}</title>
|
||||
<link>https://example.org/view/{{.Name}}</link>
|
||||
<guid>https://example.org/view/{{.Name}}</guid>
|
||||
<link>https://example.org/view/{{.Path}}</link>
|
||||
<guid>https://example.org/view/{{.Path}}</guid>
|
||||
<description>{{.Html}}</description>
|
||||
<pubDate>{{.Date}}</pubDate>
|
||||
{{range .Hashtags}}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func TestFeed(t *testing.T) {
|
||||
assert.Contains(t,
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index.rss", nil),
|
||||
"Welcome to Oddµ")
|
||||
"Welcome to Oddμ")
|
||||
}
|
||||
|
||||
func TestNoFeed(t *testing.T) {
|
||||
|
||||
13
go.mod
13
go.mod
@@ -1,14 +1,17 @@
|
||||
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-20231222211730-1d6d20845b47
|
||||
github.com/gen2brain/heic v0.3.1
|
||||
github.com/gen2brain/webp v0.5.2
|
||||
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 +25,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.8.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.8.1 // 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
18
go.sum
@@ -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.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/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/gen2brain/webp v0.5.2 h1:aYdjbU/2L98m+bqUdkYMOIY93YC+EN3HuZLMaqgMD9U=
|
||||
github.com/gen2brain/webp v0.5.2/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
|
||||
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.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
|
||||
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
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=
|
||||
|
||||
59
hashtags_cmd.go
Normal file
59
hashtags_cmd.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type hashtagsCmd struct {
|
||||
}
|
||||
|
||||
func (cmd *hashtagsCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (*hashtagsCmd) Name() string { return "hashtags" }
|
||||
func (*hashtagsCmd) Synopsis() string { return "hashtag overview" }
|
||||
func (*hashtagsCmd) Usage() string {
|
||||
return `hashtags:
|
||||
Count the use of all hashtags and list them, separated by a tabulator.
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *hashtagsCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return hashtagsCli(os.Stdout)
|
||||
}
|
||||
|
||||
// hashtagsCli runs the hashtags command on the command line. It is used
|
||||
// here with an io.Writer for easy testing.
|
||||
func hashtagsCli(w io.Writer) subcommands.ExitStatus {
|
||||
index.load()
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
|
||||
type hashtag struct {
|
||||
label string
|
||||
count int
|
||||
}
|
||||
|
||||
hashtags := []hashtag{}
|
||||
|
||||
for token, docids := range index.token {
|
||||
hashtags = append(hashtags, hashtag{label: token, count: len(docids)})
|
||||
}
|
||||
|
||||
sort.Slice(hashtags, func(i, j int) bool {
|
||||
return hashtags[i].count > hashtags[j].count
|
||||
})
|
||||
|
||||
fmt.Fprintln(w, "Rank\tHashtag\tCount")
|
||||
for i, hashtag := range hashtags {
|
||||
fmt.Fprintf(w, "%d\t%s\t%d\n", i+1, hashtag.label, hashtag.count)
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
25
hashtags_cmd_test.go
Normal file
25
hashtags_cmd_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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, "crocus\t")
|
||||
}
|
||||
69
html_cmd.go
69
html_cmd.go
@@ -7,50 +7,75 @@ import (
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type htmlCmd struct {
|
||||
useTemplate bool
|
||||
template string
|
||||
}
|
||||
|
||||
func (*htmlCmd) Name() string { return "html" }
|
||||
func (*htmlCmd) Synopsis() string { return "render a page as HTML" }
|
||||
func (*htmlCmd) Usage() string {
|
||||
return `html [-view] <page name> ...:
|
||||
return `html [-template <template name>] <page name> ...:
|
||||
Render one or more pages as HTML.
|
||||
Use a single - to read Markdown from stdin.
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *htmlCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.BoolVar(&cmd.useTemplate, "view", false, "use the 'view.html' template.")
|
||||
f.StringVar(&cmd.template, "template", "",
|
||||
"use the given HTML file as a template (probably view.html or static.html).")
|
||||
}
|
||||
|
||||
func (cmd *htmlCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return htmlCli(os.Stdout, cmd.useTemplate, f.Args())
|
||||
return htmlCli(os.Stdout, cmd.template, f.Args())
|
||||
}
|
||||
|
||||
func htmlCli(w io.Writer, useTemplate bool, args []string) subcommands.ExitStatus {
|
||||
for _, arg := range args {
|
||||
p, err := loadPage(arg)
|
||||
func htmlCli(w io.Writer, template string, args []string) subcommands.ExitStatus {
|
||||
if len(args) == 1 && args[0] == "-" {
|
||||
body, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", arg, err)
|
||||
fmt.Fprintf(w, "Cannot read from stdin: %s\n", 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)
|
||||
p := &Page{Name: "stdin", Body: body}
|
||||
return p.printHtml(w, template)
|
||||
}
|
||||
for _, name := range args {
|
||||
if !strings.HasSuffix(name, ".md") {
|
||||
fmt.Fprintf(os.Stderr, "%s does not end in '.md'\n", name)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
name = name[0:len(name)-3]
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", name, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
status := p.printHtml(w, template)
|
||||
if status != subcommands.ExitSuccess {
|
||||
return status
|
||||
}
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
func (p *Page) printHtml(w io.Writer, template string) subcommands.ExitStatus {
|
||||
if len(template) > 0 {
|
||||
t := template
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
|
||||
func TestHtmlCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := htmlCli(b, false, []string{"index"})
|
||||
s := htmlCli(b, "", []string{"index.md"})
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
r := `<h1 id="welcome-to-oddµ">Welcome to Oddµ</h1>
|
||||
r := `<h1 id="welcome-to-oddμ">Welcome to Oddμ</h1>
|
||||
|
||||
<p>Hello! 🙃</p>
|
||||
|
||||
|
||||
19
index.go
19
index.go
@@ -6,13 +6,13 @@ package main
|
||||
|
||||
import (
|
||||
"golang.org/x/exp/constraints"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type docid uint
|
||||
@@ -23,7 +23,7 @@ type docid uint
|
||||
// It depends on the fact that Title is always plain text.
|
||||
type ImageData struct {
|
||||
Title, Name string
|
||||
Html template.HTML
|
||||
Html template.HTML
|
||||
}
|
||||
|
||||
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
|
||||
@@ -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
|
||||
@@ -137,12 +139,12 @@ func (idx *indexStore) load() (int, error) {
|
||||
}
|
||||
|
||||
// walk reads a file and adds it to the index. This assumes that the index is locked.
|
||||
func (idx *indexStore) walk(path string, info fs.FileInfo, err error) error {
|
||||
func (idx *indexStore) walk(fp 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 fp != "." && strings.HasPrefix(filepath.Base(fp), ".") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
@@ -150,10 +152,10 @@ func (idx *indexStore) walk(path string, info fs.FileInfo, err error) error {
|
||||
}
|
||||
}
|
||||
// skipp all but page files
|
||||
if !strings.HasSuffix(path, ".md") {
|
||||
if !strings.HasSuffix(fp, ".md") {
|
||||
return nil
|
||||
}
|
||||
p, err := loadPage(strings.TrimSuffix(path, ".md"))
|
||||
p, err := loadPage(strings.TrimSuffix(filepath.ToSlash(fp), ".md"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -21,7 +21,7 @@ func TestIndexAdd(t *testing.T) {
|
||||
// TestIndex relies on README.md being indexed
|
||||
func TestIndex(t *testing.T) {
|
||||
index.load()
|
||||
q := "Oddµ"
|
||||
q := "Oddμ"
|
||||
pages, _ := search(q, "", "", 1, false)
|
||||
assert.NotZero(t, len(pages))
|
||||
for _, p := range pages {
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
62
links_cmd.go
Normal file
62
links_cmd.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type linksCmd struct {
|
||||
}
|
||||
|
||||
func (cmd *linksCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (*linksCmd) Name() string { return "links" }
|
||||
func (*linksCmd) Synopsis() string { return "list outgoing links for a page" }
|
||||
func (*linksCmd) Usage() string {
|
||||
return `links <page name> ...:
|
||||
Lists all the links on a page. Use a single - to read Markdown from stdin.
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *linksCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return linksCli(os.Stdout, f.Args())
|
||||
}
|
||||
|
||||
// linksCli runs the links command on the command line. It is used
|
||||
// here with an io.Writer for easy testing.
|
||||
func linksCli(w io.Writer, args []string) subcommands.ExitStatus {
|
||||
if len(args) == 1 && args[0] == "-" {
|
||||
body, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Cannot read from stdin: %s\n", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
p := &Page{Body: body}
|
||||
for _, link := range p.links() {
|
||||
fmt.Fprintln(w, link)
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
for _, name := range args {
|
||||
if !strings.HasSuffix(name, ".md") {
|
||||
fmt.Fprintf(os.Stderr, "%s does not end in '.md'\n", name)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
name = name[0:len(name)-3]
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Loading %s: %s\n", name, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
for _, link := range p.links() {
|
||||
fmt.Fprintln(w, link)
|
||||
}
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
16
links_cmd_test.go
Normal file
16
links_cmd_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinksCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := linksCli(b, []string{"README.md"})
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
x := b.String()
|
||||
assert.Contains(t, x, "https://alexschroeder.ch/view/oddmu/oddmu.1\n")
|
||||
}
|
||||
121
list.go
Normal file
121
list.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"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, name string) {
|
||||
files := []File{}
|
||||
d := filepath.FromSlash(name)
|
||||
if d == "" {
|
||||
d = "."
|
||||
} else if !strings.HasSuffix(d, "/") {
|
||||
http.Redirect(w, r, "/list/" + nameEscape(name) + "/", http.StatusFound)
|
||||
return
|
||||
} else {
|
||||
it := File{Name: "..", IsUp: true, IsDir: true }
|
||||
files = append(files, it)
|
||||
}
|
||||
err := filepath.Walk(d, func (fp string, fi fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isDir := false
|
||||
if fi.IsDir() {
|
||||
if d == fp {
|
||||
return nil
|
||||
}
|
||||
isDir = true
|
||||
}
|
||||
name := filepath.ToSlash(fp)
|
||||
base := filepath.Base(fp)
|
||||
title := ""
|
||||
if strings.HasPrefix(base, ".") {
|
||||
// skip dot directories and dot files
|
||||
if isDir {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
} else if !isDir && strings.HasSuffix(name, ".md") {
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
title = index.titles[name[:len(name)-3]]
|
||||
} else if isDir {
|
||||
// even on Windows, this looks like a Unix directory
|
||||
base += "/"
|
||||
}
|
||||
it := File{Name: base, Title: title, Date: fi.ModTime().Format(time.DateTime), IsDir: isDir }
|
||||
files = append(files, it)
|
||||
if isDir {
|
||||
// never descend into directories
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
renderTemplate(w, d, "list", &List{Dir: pathEncode(name), Files: files})
|
||||
}
|
||||
|
||||
|
||||
// deleteHandler deletes the named file and then redirects back to the list
|
||||
func deleteHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
fn := filepath.FromSlash(name)
|
||||
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/" + nameEscape(path.Dir(name)) + "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// renameHandler renames the named file and then redirects back to the list
|
||||
func renameHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
fn := filepath.FromSlash(name)
|
||||
dir := path.Dir(name)
|
||||
target := path.Join(dir, r.FormValue("name"))
|
||||
if (isHiddenName(target)) {
|
||||
http.Error(w, "the target file would be hidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err := os.Rename(fn, filepath.FromSlash(target))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/list/" + nameEscape(path.Dir(filepath.ToSlash(target))) + "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// Path returns the File.Name with some characters escaped because html/template doesn't escape those. This is suitable
|
||||
// for use in HTML templates.
|
||||
func (f *File) Path() string {
|
||||
return pathEncode(f.Name)
|
||||
}
|
||||
59
list.html
Normal file
59
list.html
Normal 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, initial-scale=1.0, user-scalable=no">
|
||||
<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</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}}{{.Path}}">{{.Name}}</a>{{else}}<a href="/view/{{$.Dir}}{{.Path}}">{{.Name}}</a>{{end}}</td>
|
||||
<td>{{.Title}}</td>
|
||||
<td>{{if .IsUp}}{{else}}<button form="manage" formaction="/delete/{{$.Dir}}{{.Path}}" title="Delete {{.Name}}">🗑</button>{{end}}</td>
|
||||
<td>{{if .IsUp}}{{else}}
|
||||
<form action="/rename/{{$.Dir}}{{.Path}}">
|
||||
<input name="name" placeholder="New name"/>
|
||||
<button title="Rename {{.Name}}">♺</button>
|
||||
</form>{{end}}</td>
|
||||
</tr>{{end}}
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,7 +20,7 @@ func (cmd *listCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (*listCmd) Name() string { return "list" }
|
||||
func (*listCmd) Synopsis() string { return "List pages with name and title." }
|
||||
func (*listCmd) Synopsis() string { return "list pages with name and title" }
|
||||
func (*listCmd) Usage() string {
|
||||
return `list [-dir string]:
|
||||
List all pages with name and title, separated by a tabulator.
|
||||
|
||||
@@ -12,8 +12,8 @@ func TestListCmd(t *testing.T) {
|
||||
s := listCli(b, "", nil)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
x := b.String()
|
||||
assert.Contains(t, x, "README\tOddµ: A minimal wiki\n")
|
||||
assert.Contains(t, x, "index\tWelcome to Oddµ\n")
|
||||
assert.Contains(t, x, "README\tOddμ: A minimal wiki\n")
|
||||
assert.Contains(t, x, "index\tWelcome to Oddμ\n")
|
||||
}
|
||||
|
||||
func TestListSubdirCmd(t *testing.T) {
|
||||
|
||||
76
list_test.go
Normal file
76
list_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
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 TestListDot(t *testing.T) {
|
||||
cleanup(t, "testdata/list-dot")
|
||||
p := &Page{Name: "testdata/list-dot/haiku", Body: []byte(`# Pressure
|
||||
|
||||
fingers tap and dance
|
||||
round and round they go at night
|
||||
before we go to bed
|
||||
`)}
|
||||
p.save()
|
||||
_, err := os.Create("testdata/list-dot/.secret")
|
||||
assert.NoError(t, err)
|
||||
body := assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/list-dot/", nil)
|
||||
assert.NotContains(t, body, "secret", "secret file was not found")
|
||||
assert.Contains(t, body, "haiku", "regular page was found")
|
||||
}
|
||||
|
||||
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()
|
||||
body := assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/delete/", nil)
|
||||
assert.Contains(t, body, `<a href="/view/testdata/delete/haiku.md">haiku.md</a>`)
|
||||
assert.Contains(t, body, `<td>Sunset</td>`)
|
||||
assert.Contains(t, body, `<button form="manage" formaction="/delete/testdata/delete/haiku.md" title="Delete haiku.md">`)
|
||||
// ensure that it exists
|
||||
assert.FileExists(t, "testdata/delete/haiku.md")
|
||||
// delete file
|
||||
HTTPRedirectTo(t, makeHandler(deleteHandler, false), "GET", "/delete/testdata/delete/haiku.md", nil, "/list/testdata/delete/")
|
||||
// verify that it is gone
|
||||
body = assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/delete/", nil)
|
||||
assert.NotContains(t, body, `<a href="/view/testdata/delete/haiku.md">haiku.md</a>`)
|
||||
assert.NoFileExists(t, "testdata/delete/haiku.md")
|
||||
}
|
||||
|
||||
func TestListUmlautHandler(t *testing.T) {
|
||||
cleanup(t, "testdata/list-umlaut")
|
||||
p := &Page{Name: "testdata/list-umlaut/hägar", Body: []byte(`# Hägar
|
||||
|
||||
Hägar was a man
|
||||
Loud and strong and quick to act
|
||||
he did not like it
|
||||
`)}
|
||||
p.save()
|
||||
body := assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/list-umlaut/", nil)
|
||||
assert.Contains(t, body, `<button form="manage" formaction="/delete/testdata/list-umlaut/h%c3%a4gar.md" title="Delete hägar.md">`)
|
||||
}
|
||||
|
||||
func TestListHash(t *testing.T) {
|
||||
cleanup(t, "testdata/list-#hash")
|
||||
os.Mkdir("testdata/list-#hash", 0755)
|
||||
_, err := os.Create("testdata/list-#hash/#secret")
|
||||
assert.NoError(t, err)
|
||||
body := assert.HTTPBody(makeHandler(listHandler, false), "GET", "/list/testdata/list-%23hash/", nil)
|
||||
assert.Contains(t, body, `<button form="manage" formaction="/delete/testdata/list-%23hash/%23secret" title="Delete #secret">`)
|
||||
}
|
||||
38
man/Makefile
38
man/Makefile
@@ -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
|
||||
@@ -11,24 +25,20 @@ man: ${MAN}
|
||||
html: ${HTML}
|
||||
|
||||
%.html: %.md
|
||||
echo '<!DOCTYPE html>' > $@
|
||||
oddmu html $(basename $<) | sed --regexp-extended \
|
||||
@echo Making $@
|
||||
@echo '<!DOCTYPE html>' > $@
|
||||
@oddmu html $< | sed --regexp-extended \
|
||||
-e 's/<a href="(oddmu[a-z.-]*.[1-9])">([^<>]*)<\/a>/<a href="\1.html">\2<\/a>/g' >> $@
|
||||
|
||||
md: ${MD}
|
||||
|
||||
%.md: %.txt
|
||||
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/^([A-Z.-]*\([1-9]\))( ".*")?$$/# \1/' \
|
||||
< $< > $@
|
||||
@echo Making $@
|
||||
@perl scdoc-to-markdown < $< > $@
|
||||
|
||||
README.md: ../README.md
|
||||
sed --regexp-extended \
|
||||
@echo Making $@
|
||||
@sed --regexp-extended \
|
||||
-e 's/\]\(.*\/(.*)\.txt\)/](\1)/' \
|
||||
< $< > $@
|
||||
|
||||
@@ -37,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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
79
man/oddmu-export.1
Normal 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
68
man/oddmu-export.1.txt
Normal 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>.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
39
man/oddmu-hashtags.1
Normal file
39
man/oddmu-hashtags.1
Normal file
@@ -0,0 +1,39 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-HASHTAGS" "1" "2024-08-29"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-hashtags - count the hashtags used
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu hashtags\fR
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "hashtags" subcommand counts all the hashtags used and lists them, separated
|
||||
by a TAB character.\&
|
||||
.PP
|
||||
.SH EXAMPLES
|
||||
.PP
|
||||
List the top 10 hashtags.\& This requires 11 lines because of the header line.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
oddmu hashtags | head -n 11
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
|
||||
30
man/oddmu-hashtags.1.txt
Normal file
30
man/oddmu-hashtags.1.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
ODDMU-HASHTAGS(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-hashtags - count the hashtags used
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu hashtags*
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "hashtags" subcommand counts all the hashtags used and lists them, separated
|
||||
by a TAB character.
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
List the top 10 hashtags. This requires 11 lines because of the header line.
|
||||
|
||||
```
|
||||
oddmu hashtags | head -n 11
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
Maintained by Alex Schroeder <alex@gnu.org>.
|
||||
@@ -5,40 +5,50 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-HTML" "1" "2024-02-26"
|
||||
.TH "ODDMU-HTML" "1" "2025-04-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-html - render Oddmu page HTML from the command-line
|
||||
oddmu-html - render Oddmu page HTML
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu html\fR [-view] \fIpage-name\fR
|
||||
\fBoddmu html\fR [\fB\fR-template\fB\fR \fItemplate-name\fR] \fIpage-name\fR
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "html" subcommand opens the Markdown file for the given page name (appending
|
||||
the ".\&md" extension) and prints the HTML to STDOUT without invoking the
|
||||
"view.\&html" template.\&
|
||||
The "html" subcommand opens the given Markdown file and prints the resulting
|
||||
HTML to STDOUT without invoking the "view.\&html" template.\& Use "-" as the page
|
||||
name if you want to read Markdown from \fBstdin\fR.\&
|
||||
.PP
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB-view\fR
|
||||
\fB\fR-template\fB\fR \fItemplate-name\fR
|
||||
.RS 4
|
||||
Use the "view.\&html" template to render the page.\& Without this, the HTML
|
||||
lacks html and body tags.\&
|
||||
Use the given template to render the page.\& Without this, the HTML lacks
|
||||
html and body tags.\& The only two options that make sense are "view.\&html"
|
||||
and "static.\&html".\&
|
||||
.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\&.md > 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.\&
|
||||
|
||||
@@ -2,32 +2,40 @@ ODDMU-HTML(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-html - render Oddmu page HTML from the command-line
|
||||
oddmu-html - render Oddmu page HTML
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu html* [-view] _page-name_
|
||||
*oddmu html* [**-template** _template-name_] _page-name_
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "html" subcommand opens the Markdown file for the given page name (appending
|
||||
the ".md" extension) and prints the HTML to STDOUT without invoking the
|
||||
"view.html" template.
|
||||
The "html" subcommand opens the given Markdown file and prints the resulting
|
||||
HTML to STDOUT without invoking the "view.html" template. Use "-" as the page
|
||||
name if you want to read Markdown from *stdin*.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
*-view*
|
||||
Use the "view.html" template to render the page. Without this, the HTML
|
||||
lacks html and body tags.
|
||||
**-template** _template-name_
|
||||
Use the given template to render the page. Without this, the HTML lacks
|
||||
html and body tags. The only two options that make sense are "view.html"
|
||||
and "static.html".
|
||||
|
||||
# EXAMPLE
|
||||
# EXAMPLES
|
||||
|
||||
Generate the HTML for "README.md":
|
||||
Generate "README.html" from "README.md":
|
||||
|
||||
```
|
||||
oddmu html README
|
||||
oddmu html README.md > README.html
|
||||
```
|
||||
|
||||
Alternatively:
|
||||
|
||||
```
|
||||
oddmu html - < README.md > README.html
|
||||
```
|
||||
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
The ODDMU_WEBFINGER environment variable has no effect in this situation.
|
||||
|
||||
29
man/oddmu-links.1
Normal file
29
man/oddmu-links.1
Normal file
@@ -0,0 +1,29 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-LINKS" "1" "2025-04-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-links - list outgoing links for pages
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu links\fR \fIpage names.\&.\&.\&\fR
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "links" subcommand lists outgoing links for one or more Markdown files.\& Use
|
||||
"-" as the page name if you want to read Markdown from \fBstdin\fR.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIoddmu-missing\fR(1)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
|
||||
22
man/oddmu-links.1.txt
Normal file
22
man/oddmu-links.1.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
ODDMU-LINKS(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-links - list outgoing links for pages
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu links* _page names..._
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "links" subcommand lists outgoing links for one or more Markdown files. Use
|
||||
"-" as the page name if you want to read Markdown from *stdin*.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _oddmu-missing_(1)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
Maintained by Alex Schroeder <alex@gnu.org>.
|
||||
@@ -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.\&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-NGINX" "5" "2024-05-09"
|
||||
.TH "ODDMU-NGINX" "5" "2025-03-16"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
|
||||
oddmu-nginx - how to setup nginx as a reverse proxy for Oddmu
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.\&
|
||||
This is an unpriviledged port so an ordinary user account can do this.\&
|
||||
.PP
|
||||
This page explains how to setup NGINX on Debian to act as a reverse proxy for
|
||||
Oddmu.\& Once this is done, you can use NGINX to provide HTTPS, request users to
|
||||
This page explains how to setup nginx on Debian to act as a reverse proxy for
|
||||
Oddmu.\& Once this is done, you can use nginx to provide HTTPS, request users to
|
||||
authenticate themselves, and so on.\&
|
||||
.PP
|
||||
.SH CONFIGURATION
|
||||
@@ -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
|
||||
|
||||
@@ -2,15 +2,15 @@ ODDMU-NGINX(5)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
|
||||
oddmu-nginx - how to setup nginx as a reverse proxy for Oddmu
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.
|
||||
This is an unpriviledged port so an ordinary user account can do this.
|
||||
|
||||
This page explains how to setup NGINX on Debian to act as a reverse proxy for
|
||||
Oddmu. Once this is done, you can use NGINX to provide HTTPS, request users to
|
||||
This page explains how to setup nginx on Debian to act as a reverse proxy for
|
||||
Oddmu. Once this is done, you can use nginx to provide HTTPS, request users to
|
||||
authenticate themselves, and so on.
|
||||
|
||||
# CONFIGURATION
|
||||
@@ -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:;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-NOTIFY" "1" "2024-02-17"
|
||||
.TH "ODDMU-NOTIFY" "1" "2025-04-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -17,8 +17,8 @@ oddmu-notify - add links to changes.\&md, index.\&md, and hashtag pages
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "notify" subcommand takes all the page names provided (without the ".\&md"
|
||||
extension) and adds links to it from other pages.\&
|
||||
The "notify" subcommand takes all the Markdown files provided and adds links to
|
||||
these pages from other pages.\&
|
||||
.PP
|
||||
A new link is added to the \fBchanges\fR page in the current directory if it doesn'\&t
|
||||
exist.\& The current date of the machine Oddmu is running on is used as the
|
||||
@@ -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
|
||||
@@ -57,7 +57,7 @@ it exists):
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
oddmu notify 2023-11-05-climate
|
||||
oddmu notify 2023-11-05-climate\&.md
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
|
||||
@@ -10,8 +10,8 @@ oddmu-notify - add links to changes.md, index.md, and hashtag pages
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "notify" subcommand takes all the page names provided (without the ".md"
|
||||
extension) and adds links to it from other pages.
|
||||
The "notify" subcommand takes all the Markdown files provided and adds links to
|
||||
these pages from other pages.
|
||||
|
||||
A new link is added to the *changes* page in the current directory if it doesn't
|
||||
exist. The current date of the machine Oddmu is running on is used as the
|
||||
@@ -42,14 +42,14 @@ 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
|
||||
it exists):
|
||||
|
||||
```
|
||||
oddmu notify 2023-11-05-climate
|
||||
oddmu notify 2023-11-05-climate.md
|
||||
```
|
||||
|
||||
The changes file might look as follows:
|
||||
|
||||
@@ -5,16 +5,146 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-RELEASES" "7" "2024-07-21"
|
||||
.TH "ODDMU-RELEASES" "7" "2025-04-26"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-releases - what'\&s new in this releases?\&
|
||||
oddmu-releases - what'\&s new?\&
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
This page lists user-visible features and template changes to consider.\&
|
||||
.PP
|
||||
.SS 1.17 (2025)
|
||||
.PP
|
||||
You need to update the upload template ("upload.\&html").\& Many things have
|
||||
changed!\& See \fIoddmu-templates\fR(5) for more.\&
|
||||
.PP
|
||||
You probably want to ensure that the upload link on the view template
|
||||
("view.\&html") and others, if you added it, has a \fIfilename\fR and \fIpagename\fR
|
||||
parameters.\&
|
||||
.PP
|
||||
Example:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
<a href="/upload/{{\&.Dir}}?filename={{\&.Base}}-1\&.jpg&pagename={{\&.Base}}">Upload</a>
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
You need to change {{.\&Name}} to {{.\&Path}} when it is used in URLs, in the list
|
||||
template ("list.\&html").\& If you don'\&t do this, file deleting and rename may not
|
||||
work on files containing a comma, a semicolon, a questionmark or a hash
|
||||
character.\& This fix was necessary because URLs for files containing a
|
||||
questionmark or a hash character would end the path at this character and treat
|
||||
the rest as a query parameter or fragment, respectively.\&
|
||||
.PP
|
||||
Updated the example themes.\& Some of my sites got a text area that tries to take
|
||||
all the vertical space available.\& This is great for monitors in portrait mode.\&
|
||||
.PP
|
||||
\fIlist\fR action now skips dot files.\&
|
||||
.PP
|
||||
.SS 1.16 (2025)
|
||||
.PP
|
||||
Add support for WebP images for uploading and resizing.\&
|
||||
.PP
|
||||
You need to change {{.\&Name}} to {{.\&Path}} in HTML templates where pages are
|
||||
concerned.\& If you don'\&t do this, your page names (i.\&e.\& filenames for pages) may
|
||||
not include a comma, a semicolon, a questionmark or a hash sign.\& This fix was
|
||||
necessary because file uploads of filenames with non-ASCII characters ended up
|
||||
double-encoded.\&
|
||||
.PP
|
||||
Note that on the "list.\&html" template, {{.\&Name}} refers to file instead of a
|
||||
page and File.\&Path() isn'\&t implemented, yet.\& This is fixed in the next release.\&
|
||||
.PP
|
||||
Improved the example themes.\& The chat theme got better list styling and better
|
||||
upload functionality with automatic "add" button; the plain theme got rocket
|
||||
links via JavaScript; the alexschroeder.\&ch theme got a preview button and better
|
||||
image support for upload and search; the transjovian.\&org theme got better image
|
||||
support for upload.\&
|
||||
.PP
|
||||
Switch the \fIhtml\fR, \fIlink\fR, \fInotify\fR and \fItoc\fR subcommand to take filenames
|
||||
(including the `.\&md` suffix) instead of page names (without the `.\&md` suffix).\&
|
||||
.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.\&
|
||||
.PP
|
||||
Support searching for multiple words using all sorts of quotation marks.\& That
|
||||
means that it is now impossible to search for words that begin with such a
|
||||
quotation mark.\&
|
||||
.PP
|
||||
These are the quotation marks currently supported: '\&foo'\& "foo" ‘foo’ ‚foo‘ ’foo’
|
||||
“foo” „foo“ ”foo” «foo» »foo« ‹foo› ›foo‹ 「foo」 「foo」 『foo』 – any such
|
||||
quoted text is searched as-is, including whitespace.\&
|
||||
.PP
|
||||
Add loading="lazy" for images in search.\&html
|
||||
.PP
|
||||
If you want to take advantage of this, you'\&ll need to adapt your "search.\&html"
|
||||
template accordingly.\& Use like this, for example:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
{{range \&.Items}}
|
||||
<article lang="{{\&.Language}}">
|
||||
<p><a class="result" href="/view/{{\&.Name}}">{{\&.Title}}</a>
|
||||
<span class="score">{{\&.Score}}</span></p>
|
||||
<blockquote>{{\&.Html}}</blockquote>
|
||||
{{range \&.Images}}
|
||||
<p class="image"><a href="/view/{{\&.Name}}"><img loading="lazy" src="/view/{{\&.Name}}"></a><br/>{{\&.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SS 1.11 (2024)
|
||||
.PP
|
||||
The HTML renderer option for smart fractions support was removed.\& Therefore, 1/8
|
||||
|
||||
@@ -2,12 +2,134 @@ ODDMU-RELEASES(7)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-releases - what's new in this releases?
|
||||
oddmu-releases - what's new?
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
This page lists user-visible features and template changes to consider.
|
||||
|
||||
## 1.17 (2025)
|
||||
|
||||
You need to update the upload template ("upload.html"). Many things have
|
||||
changed! See _oddmu-templates_(5) for more.
|
||||
|
||||
You probably want to ensure that the upload link on the view template
|
||||
("view.html") and others, if you added it, has a _filename_ and _pagename_
|
||||
parameters.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
<a href="/upload/{{.Dir}}?filename={{.Base}}-1.jpg&pagename={{.Base}}">Upload</a>
|
||||
```
|
||||
|
||||
You need to change {{.Name}} to {{.Path}} when it is used in URLs, in the list
|
||||
template ("list.html"). If you don't do this, file deleting and rename may not
|
||||
work on files containing a comma, a semicolon, a questionmark or a hash
|
||||
character. This fix was necessary because URLs for files containing a
|
||||
questionmark or a hash character would end the path at this character and treat
|
||||
the rest as a query parameter or fragment, respectively.
|
||||
|
||||
Updated the example themes. Some of my sites got a text area that tries to take
|
||||
all the vertical space available. This is great for monitors in portrait mode.
|
||||
|
||||
_list_ action now skips dot files.
|
||||
|
||||
## 1.16 (2025)
|
||||
|
||||
Add support for WebP images for uploading and resizing.
|
||||
|
||||
You need to change {{.Name}} to {{.Path}} in HTML templates where pages are
|
||||
concerned. If you don't do this, your page names (i.e. filenames for pages) may
|
||||
not include a comma, a semicolon, a questionmark or a hash sign. This fix was
|
||||
necessary because file uploads of filenames with non-ASCII characters ended up
|
||||
double-encoded.
|
||||
|
||||
Note that on the "list.html" template, {{.Name}} refers to file instead of a
|
||||
page and File.Path() isn't implemented, yet. This is fixed in the next release.
|
||||
|
||||
Improved the example themes. The chat theme got better list styling and better
|
||||
upload functionality with automatic "add" button; the plain theme got rocket
|
||||
links via JavaScript; the alexschroeder.ch theme got a preview button and better
|
||||
image support for upload and search; the transjovian.org theme got better image
|
||||
support for upload.
|
||||
|
||||
Switch the _html_, _link_, _notify_ and _toc_ subcommand to take filenames
|
||||
(including the `.md` suffix) instead of page names (without the `.md` suffix).
|
||||
|
||||
## 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.
|
||||
|
||||
Support searching for multiple words using all sorts of quotation marks. That
|
||||
means that it is now impossible to search for words that begin with such a
|
||||
quotation mark.
|
||||
|
||||
These are the quotation marks currently supported: 'foo' "foo" ‘foo’ ‚foo‘ ’foo’
|
||||
“foo” „foo“ ”foo” «foo» »foo« ‹foo› ›foo‹ 「foo」 「foo」 『foo』 – any such
|
||||
quoted text is searched as-is, including whitespace.
|
||||
|
||||
Add loading="lazy" for images in search.html
|
||||
|
||||
If you want to take advantage of this, you'll need to adapt your "search.html"
|
||||
template accordingly. Use like this, for example:
|
||||
|
||||
```
|
||||
{{range .Items}}
|
||||
<article lang="{{.Language}}">
|
||||
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
|
||||
<span class="score">{{.Score}}</span></p>
|
||||
<blockquote>{{.Html}}</blockquote>
|
||||
{{range .Images}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img loading="lazy" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
## 1.11 (2024)
|
||||
|
||||
The HTML renderer option for smart fractions support was removed. Therefore, 1/8
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-REPLACE" "1" "2024-02-17"
|
||||
.TH "ODDMU-REPLACE" "1" "2025-03-05"
|
||||
.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,13 +36,13 @@ 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
|
||||
.nf
|
||||
.RS 4
|
||||
oddmu replace Oddmu Oddµ
|
||||
oddmu replace Oddmu Oddμ
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
|
||||
@@ -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,12 +25,12 @@ 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:
|
||||
|
||||
```
|
||||
oddmu replace Oddmu Oddµ
|
||||
oddmu replace Oddmu Oddμ
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-SEARCH" "1" "2024-02-17"
|
||||
.TH "ODDMU-SEARCH" "1" "2025-03-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-search - search the Oddmu pages from the command-line
|
||||
oddmu-search - search the Oddmu pages
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
@@ -17,8 +17,9 @@ oddmu-search - search the Oddmu pages from the command-line
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "search" subcommand searches the Markdown files in the current
|
||||
directory.\&
|
||||
The "search" subcommand resursively searches the Markdown files in the current
|
||||
directory tree.\& That is, the files in the current directory and all its child
|
||||
directories are searched.\&
|
||||
.PP
|
||||
Be default, this returns a Markdown-formatted list suitable for pasting into
|
||||
Oddmu pages.\&
|
||||
@@ -26,6 +27,10 @@ Oddmu pages.\&
|
||||
If a directory is provided, only files from the tree starting at that
|
||||
subdirectory are listed, and the directory is stripped from the page name.\&
|
||||
.PP
|
||||
If multiple terms are provided, they are all concatenated into a single,
|
||||
space-separated query string.\& That is, searching for the terms A B and the term
|
||||
"A B" is equivalent.\&
|
||||
.PP
|
||||
See \fIoddmu-search\fR(7) for more information of how pages are searched, sorted and
|
||||
scored.\&
|
||||
.PP
|
||||
@@ -37,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
|
||||
@@ -49,22 +54,32 @@ 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 "oddmu" in the Markdown files of the current directory:
|
||||
Search for the two words "Alex" and "Schroeder".\& All of the following are
|
||||
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".\&
|
||||
The ordering of terms does not matter.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
oddmu search oddmu
|
||||
~/src/oddmu $ oddmu search Alex Schroeder
|
||||
Search for Alex Schroeder, page 1: 3 results
|
||||
* [Alex Schroeder theme](themes/alexschroeder\&.ch/README)
|
||||
* [Oddμ: A minimal wiki](README)
|
||||
* [Themes](themes/index)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Result:
|
||||
Search for the exact phrase "Alex Schroeder".\& In order to pass the quotes to
|
||||
Oddmu, a second level of quotes is required.\& All of the following are
|
||||
equivalent: '\&"Alex Schroeder"'\&, "'\&Alex Schroeder'\&", \e"Alex\e Schroeder\e",
|
||||
\e"Alex Schroeder\e".\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
Search oddmu: 1 result
|
||||
* [Oddµ: A minimal wiki](README) (5)
|
||||
~/src/oddmu $ oddmu search "\&'Alex Schroeder\&'"
|
||||
Search for \&'Alex Schroeder\&', page 1: 1 result
|
||||
* [Alex Schroeder theme](themes/alexschroeder\&.ch/README)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,8 +10,9 @@ oddmu-search - search the Oddmu pages from the command-line
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "search" subcommand searches the Markdown files in the current
|
||||
directory.
|
||||
The "search" subcommand resursively searches the Markdown files in the current
|
||||
directory tree. That is, the files in the current directory and all its child
|
||||
directories are searched.
|
||||
|
||||
Be default, this returns a Markdown-formatted list suitable for pasting into
|
||||
Oddmu pages.
|
||||
@@ -19,6 +20,10 @@ Oddmu pages.
|
||||
If a directory is provided, only files from the tree starting at that
|
||||
subdirectory are listed, and the directory is stripped from the page name.
|
||||
|
||||
If multiple terms are provided, they are all concatenated into a single,
|
||||
space-separated query string. That is, searching for the terms A B and the term
|
||||
"A B" is equivalent.
|
||||
|
||||
See _oddmu-search_(7) for more information of how pages are searched, sorted and
|
||||
scored.
|
||||
|
||||
@@ -27,26 +32,36 @@ 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 "oddmu" in the Markdown files of the current directory:
|
||||
Search for the two words "Alex" and "Schroeder". All of the following are
|
||||
equivalent: Alex Schroeder, Schroeder Alex, "Alex Schroeder", "Schroeder Alex".
|
||||
The ordering of terms does not matter.
|
||||
|
||||
```
|
||||
oddmu search oddmu
|
||||
~/src/oddmu $ oddmu search Alex Schroeder
|
||||
Search for Alex Schroeder, page 1: 3 results
|
||||
* [Alex Schroeder theme](themes/alexschroeder.ch/README)
|
||||
* [Oddμ: A minimal wiki](README)
|
||||
* [Themes](themes/index)
|
||||
```
|
||||
|
||||
Result:
|
||||
Search for the exact phrase "Alex Schroeder". In order to pass the quotes to
|
||||
Oddmu, a second level of quotes is required. All of the following are
|
||||
equivalent: '"Alex Schroeder"', "'Alex Schroeder'", \\"Alex\\ Schroeder\\",
|
||||
\\"Alex Schroeder\\".
|
||||
|
||||
```
|
||||
Search oddmu: 1 result
|
||||
* [Oddµ: A minimal wiki](README) (5)
|
||||
~/src/oddmu $ oddmu search "'Alex Schroeder'"
|
||||
Search for 'Alex Schroeder', page 1: 1 result
|
||||
* [Alex Schroeder theme](themes/alexschroeder.ch/README)
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-SEARCH" "7" "2024-02-19"
|
||||
.TH "ODDMU-SEARCH" "7" "2025-03-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -57,9 +57,9 @@ exactly (without the leading '\&#'\&) is listed first, even if it doesn'\&t cont
|
||||
the hashtag.\& It is assumed that this page offers some kind of introduction to
|
||||
people searching for the hashtag.\&
|
||||
.PP
|
||||
Example: When people click on the hashtag "#Oddµ" and a page named "Oddµ" exists
|
||||
(in other words, the file "Oddµ.\&md" exists), it is prepended to the results even
|
||||
if it doesn'\&t have the hashtag "#Oddµ" and even if it has a title of "Oddµ, a
|
||||
Example: When people click on the hashtag "#Oddμ" and a page named "Oddμ" exists
|
||||
(in other words, the file "Oddμ.\&md" exists), it is prepended to the results even
|
||||
if it doesn'\&t have the hashtag "#Oddμ" and even if it has a title of "Oddμ, a
|
||||
minimal wiki" (which wouldn'\&t be an exact match).\&
|
||||
.PP
|
||||
The score and highlighting of snippets is used to help visitors decide which
|
||||
|
||||
@@ -44,9 +44,9 @@ exactly (without the leading '#') is listed first, even if it doesn't contain
|
||||
the hashtag. It is assumed that this page offers some kind of introduction to
|
||||
people searching for the hashtag.
|
||||
|
||||
Example: When people click on the hashtag "#Oddµ" and a page named "Oddµ" exists
|
||||
(in other words, the file "Oddµ.md" exists), it is prepended to the results even
|
||||
if it doesn't have the hashtag "#Oddµ" and even if it has a title of "Oddµ, a
|
||||
Example: When people click on the hashtag "#Oddμ" and a page named "Oddμ" exists
|
||||
(in other words, the file "Oddμ.md" exists), it is prepended to the results even
|
||||
if it doesn't have the hashtag "#Oddμ" and even if it has a title of "Oddμ, a
|
||||
minimal wiki" (which wouldn't be an exact match).
|
||||
|
||||
The score and highlighting of snippets is used to help visitors decide which
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-TEMPLATES" "5" "2024-07-21" "File Formats Manual"
|
||||
.TH "ODDMU-TEMPLATES" "5" "2025-04-26" "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
|
||||
@@ -43,6 +44,29 @@ placeholders.\&
|
||||
\fIview.\&html\fR uses a \fIpage\fR
|
||||
.PD
|
||||
.PP
|
||||
The following property lists always indicate whether the property is
|
||||
percent-encoded or not.\& In theory, the html/template package would handle this.\&
|
||||
The problem is that the package gives special treatment to the semicolon, comma,
|
||||
question-mark and hash-sign as these are potential separators in a URL.\&
|
||||
.PP
|
||||
Consider the following:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
<a href="{{\&.Name}}">{{\&.Name}}</a>
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
If \fI.\&Name\fR is "#foo", the html/template package treats it as a URL fragment
|
||||
inside the attribute instead of a file path that needs to be escaped to
|
||||
"%23foo".\& The same problem arises if \fI.\&Name\fR is "foo?\&" as the questionmark is
|
||||
not escaped and therefore treated as the separator between URL path and query
|
||||
parameters instead of being part of the name.\&
|
||||
.PP
|
||||
The consequences for template authors is that the properties that are
|
||||
percent-encoded must be used in links where as the regular properties must be
|
||||
used outside of links.\&
|
||||
.PP
|
||||
.SS Page
|
||||
.PP
|
||||
A page has the following properties:
|
||||
@@ -50,14 +74,14 @@ A page has the following properties:
|
||||
\fI{{.\&Title}}\fR is the page title.\& If the page doesn'\&t provide its own title, the
|
||||
page name is used.\&
|
||||
.PP
|
||||
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\& More specifically, it is
|
||||
percent-escaped except for the slashes.\& The page name doesn'\&t include the \fI.\&md\fR
|
||||
extension.\&
|
||||
\fI{{.\&Name}}\fR is the page name.\& The page name doesn'\&t include the \fI.\&md\fR extension.\&
|
||||
.PP
|
||||
\fI{{.\&Dir}}\fR is the page directory, percent-escaped except for the slashes.\&
|
||||
\fI{{.\&Path}}\fR is the page name, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Dir}}\fR is the page directory, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Base}}\fR is the basename of the current file (without the directory and
|
||||
without the \fI.\&md\fR extension), escaped for use in URLs.\&
|
||||
without the \fI.\&md\fR extension), percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Language}}\fR is the suspected language of the page.\& This is used to set the
|
||||
language on the \fIview.\&html\fR template.\& See "Non-English hyphenation" below.\&
|
||||
@@ -108,12 +132,37 @@ 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, percent-encoded.\&
|
||||
.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{{.\&Path}}\fR is the page name, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Title}}\fR is the page title, if the file in question is a Markdown file.\&
|
||||
.PP
|
||||
\fI{{.\&IsDir}}\fR is a boolean used to indicate that this file is a directory.\&
|
||||
.PP
|
||||
\fI{{.\&IsUp}}\fR is a boolean used to indicate the entry for the parent directory
|
||||
(the first file in the array, unless the directory being listed is the top
|
||||
directory).\& The filename of this file is ".\&.\&".\&
|
||||
.PP
|
||||
\fI{{.\&Date}}\fR is the last modification date of the file.\&
|
||||
.PP
|
||||
.SS Search
|
||||
.PP
|
||||
\fI{{.\&Query}}\fR is the query string.\&
|
||||
.PP
|
||||
\fI{{.\&Dir}}\fR is the directory in which the search starts, percent-escaped except
|
||||
for the slashes.\&
|
||||
\fI{{.\&Dir}}\fR is the directory in which the search starts, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Previous}}\fR, \fI{{.\&Page}}\fR and \fI{{.\&Next}}\fR are the previous, current and next
|
||||
page number in the results since doing arithmetics in templates is hard.\& The
|
||||
@@ -150,23 +199,18 @@ search term that matched.\&
|
||||
.SS Upload
|
||||
.PP
|
||||
\fI{{.\&Dir}}\fR is the directory where the uploaded file ends up, based on the URL
|
||||
path, percent-escaped except for the slashes.\&
|
||||
path, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Name}}\fR is the \fIfilename\fR query parameter.\&
|
||||
\fI{{.\&FileName}}\fR is the \fIfilename\fR query parameter used to suggested a filename.\&
|
||||
.PP
|
||||
\fI{{.\&Last}}\fR is the filename of the last file uploaded.\&
|
||||
\fI{{.\&FilePath}}\fR is the filename, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Actual}}\fR is an array of filenames of all the files uploaded.\& Use {{range
|
||||
Actual}} … {{.\&}} … {{end}} to loop over all the filenames.\&
|
||||
\fI{{.\&Name}}\fR is the \fIpagename\fR query parameter used to indicate where to append
|
||||
links to the files.\&
|
||||
.PP
|
||||
\fI{{.\&Base}}\fR is the basename of the first file uploaded (without the directory,
|
||||
extension and numeric part at the end), escaped for use in URLs.\&
|
||||
\fI{{.\&Path}}\fR is the page name, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Title}}\fR is the title of the basename, if it exists.\&
|
||||
.PP
|
||||
\fI{{.\&Image}}\fR is a boolean to indicate whether the last file uploaded has a file
|
||||
name indicating an image or not (such as ending in \fI.\&jpg\fR).\& If so, a thumbnail
|
||||
can be shown by the template, for example.\&
|
||||
\fI{{.\&Title}}\fR is the title of the page, if it exists.\&
|
||||
.PP
|
||||
\fI{{.\&MaxWidth}}\fR is the \fImaxwidth\fR query parameter, i.\&e.\& the value used for the
|
||||
previous image uploaded.\&
|
||||
@@ -176,6 +220,22 @@ previous image uploaded.\&
|
||||
.PP
|
||||
\fI{{.\&Today}}\fR is the current date, in ISO format.\&
|
||||
.PP
|
||||
\fI{{.\&Uploads}}\fR an array of files already uploaded, based on the \fIuploads\fR query
|
||||
parameter.\& To refer to them, you need to use a \fI{{range .\&Uploads}}\fR … \fI{{end}}\fR
|
||||
construct.\& This is required because the \fIdrop\fR action redirects back to the
|
||||
\fIupload\fR action, so after saving one or more files, you can upload even more
|
||||
files.\&
|
||||
.PP
|
||||
Each upload has the following attributes:
|
||||
.PP
|
||||
\fI{{.\&Name}}\fR is the filename.\&
|
||||
.PP
|
||||
\fI{{.\&Path}}\fR is the file name, percent-encoded.\&
|
||||
.PP
|
||||
\fI{{.\&Image}}\fR is a boolean to indicate whether the upload is an image or not
|
||||
(such as ending in \fI.\&jpg\fR).\& If so, a thumbnail can be shown by the template, for
|
||||
example.\&
|
||||
.PP
|
||||
.SS Non-English hyphenation
|
||||
.PP
|
||||
Automatic hyphenation by the browser requires two things: The style sheet must
|
||||
@@ -189,18 +249,18 @@ use a small number of languages – or just a single language!\& – you can set
|
||||
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
|
||||
codes, e.\&g.\& "en" or "en,de,fr,pt".\&
|
||||
.PP
|
||||
"view.\&html" is used the template to render a single page and so the language
|
||||
detected is added to the "html" element.\&
|
||||
"view.\&html" is used to render a single page and so the language detected is
|
||||
added to the "html" element.\&
|
||||
.PP
|
||||
"search.\&html" is the template used to render search results and so "en" is used
|
||||
for the "html" element and the language detected for every page in the search
|
||||
result is added to the "article" element for each snippet.\&
|
||||
.PP
|
||||
"edit.\&html" and "add.\&html" are the templates used to edit a page and at that
|
||||
point, the language isn'\&t known, so "en" is used for the "html" element and no
|
||||
language is used for the "textarea" element.\&
|
||||
"edit.\&html" and "add.\&html" are the templates used to edit a page.\& If the page
|
||||
already exists, its language is used for the "textarea" element.\& If the page is
|
||||
new, 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.\&
|
||||
|
||||
@@ -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,12 +18,34 @@ 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_
|
||||
- _upload.html_ uses an _upload_
|
||||
- _view.html_ uses a _page_
|
||||
|
||||
The following property lists always indicate whether the property is
|
||||
percent-encoded or not. In theory, the html/template package would handle this.
|
||||
The problem is that the package gives special treatment to the semicolon, comma,
|
||||
question-mark and hash-sign as these are potential separators in a URL.
|
||||
|
||||
Consider the following:
|
||||
|
||||
```
|
||||
<a href="{{.Name}}">{{.Name}}</a>
|
||||
```
|
||||
|
||||
If _.Name_ is "#foo", the html/template package treats it as a URL fragment
|
||||
inside the attribute instead of a file path that needs to be escaped to
|
||||
"%23foo". The same problem arises if _.Name_ is "foo?" as the questionmark is
|
||||
not escaped and therefore treated as the separator between URL path and query
|
||||
parameters instead of being part of the name.
|
||||
|
||||
The consequences for template authors is that the properties that are
|
||||
percent-encoded must be used in links where as the regular properties must be
|
||||
used outside of links.
|
||||
|
||||
## Page
|
||||
|
||||
A page has the following properties:
|
||||
@@ -32,14 +53,14 @@ A page has the following properties:
|
||||
_{{.Title}}_ is the page title. If the page doesn't provide its own title, the
|
||||
page name is used.
|
||||
|
||||
_{{.Name}}_ is the page name, escaped for use in URLs. More specifically, it is
|
||||
percent-escaped except for the slashes. The page name doesn't include the _.md_
|
||||
extension.
|
||||
_{{.Name}}_ is the page name. The page name doesn't include the _.md_ extension.
|
||||
|
||||
_{{.Dir}}_ is the page directory, percent-escaped except for the slashes.
|
||||
_{{.Path}}_ is the page name, percent-encoded.
|
||||
|
||||
_{{.Dir}}_ is the page directory, percent-encoded.
|
||||
|
||||
_{{.Base}}_ is the basename of the current file (without the directory and
|
||||
without the _.md_ extension), escaped for use in URLs.
|
||||
without the _.md_ extension), percent-encoded.
|
||||
|
||||
_{{.Language}}_ is the suspected language of the page. This is used to set the
|
||||
language on the _view.html_ template. See "Non-English hyphenation" below.
|
||||
@@ -85,12 +106,37 @@ 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, percent-encoded.
|
||||
|
||||
_{{.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).
|
||||
|
||||
_{{.Path}}_ is the page name, percent-encoded.
|
||||
|
||||
_{{.Title}}_ is the page title, if the file in question is a Markdown file.
|
||||
|
||||
_{{.IsDir}}_ is a boolean used to indicate that this file is a directory.
|
||||
|
||||
_{{.IsUp}}_ is a boolean used to indicate the entry for the parent directory
|
||||
(the first file in the array, unless the directory being listed is the top
|
||||
directory). The filename of this file is "..".
|
||||
|
||||
_{{.Date}}_ is the last modification date of the file.
|
||||
|
||||
## Search
|
||||
|
||||
_{{.Query}}_ is the query string.
|
||||
|
||||
_{{.Dir}}_ is the directory in which the search starts, percent-escaped except
|
||||
for the slashes.
|
||||
_{{.Dir}}_ is the directory in which the search starts, percent-encoded.
|
||||
|
||||
_{{.Previous}}_, _{{.Page}}_ and _{{.Next}}_ are the previous, current and next
|
||||
page number in the results since doing arithmetics in templates is hard. The
|
||||
@@ -127,23 +173,18 @@ search term that matched.
|
||||
## Upload
|
||||
|
||||
_{{.Dir}}_ is the directory where the uploaded file ends up, based on the URL
|
||||
path, percent-escaped except for the slashes.
|
||||
path, percent-encoded.
|
||||
|
||||
_{{.Name}}_ is the _filename_ query parameter.
|
||||
_{{.FileName}}_ is the _filename_ query parameter used to suggested a filename.
|
||||
|
||||
_{{.Last}}_ is the filename of the last file uploaded.
|
||||
_{{.FilePath}}_ is the filename, percent-encoded.
|
||||
|
||||
_{{.Actual}}_ is an array of filenames of all the files uploaded. Use {{range
|
||||
.Actual}} … {{.}} … {{end}} to loop over all the filenames.
|
||||
_{{.Name}}_ is the _pagename_ query parameter used to indicate where to append
|
||||
links to the files.
|
||||
|
||||
_{{.Base}}_ is the basename of the first file uploaded (without the directory,
|
||||
extension and numeric part at the end), escaped for use in URLs.
|
||||
_{{.Path}}_ is the page name, percent-encoded.
|
||||
|
||||
_{{.Title}}_ is the title of the basename, if it exists.
|
||||
|
||||
_{{.Image}}_ is a boolean to indicate whether the last file uploaded has a file
|
||||
name indicating an image or not (such as ending in _.jpg_). If so, a thumbnail
|
||||
can be shown by the template, for example.
|
||||
_{{.Title}}_ is the title of the page, if it exists.
|
||||
|
||||
_{{.MaxWidth}}_ is the _maxwidth_ query parameter, i.e. the value used for the
|
||||
previous image uploaded.
|
||||
@@ -153,6 +194,22 @@ previous image uploaded.
|
||||
|
||||
_{{.Today}}_ is the current date, in ISO format.
|
||||
|
||||
_{{.Uploads}}_ an array of files already uploaded, based on the _uploads_ query
|
||||
parameter. To refer to them, you need to use a _{{range .Uploads}}_ … _{{end}}_
|
||||
construct. This is required because the _drop_ action redirects back to the
|
||||
_upload_ action, so after saving one or more files, you can upload even more
|
||||
files.
|
||||
|
||||
Each upload has the following attributes:
|
||||
|
||||
_{{.Name}}_ is the filename.
|
||||
|
||||
_{{.Path}}_ is the file name, percent-encoded.
|
||||
|
||||
_{{.Image}}_ is a boolean to indicate whether the upload is an image or not
|
||||
(such as ending in _.jpg_). If so, a thumbnail can be shown by the template, for
|
||||
example.
|
||||
|
||||
## Non-English hyphenation
|
||||
|
||||
Automatic hyphenation by the browser requires two things: The style sheet must
|
||||
@@ -166,18 +223,18 @@ use a small number of languages – or just a single language! – you can set t
|
||||
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
|
||||
codes, e.g. "en" or "en,de,fr,pt".
|
||||
|
||||
"view.html" is used the template to render a single page and so the language
|
||||
detected is added to the "html" element.
|
||||
"view.html" is used to render a single page and so the language detected is
|
||||
added to the "html" element.
|
||||
|
||||
"search.html" is the template used to render search results and so "en" is used
|
||||
for the "html" element and the language detected for every page in the search
|
||||
result is added to the "article" element for each snippet.
|
||||
|
||||
"edit.html" and "add.html" are the templates used to edit a page and at that
|
||||
point, the language isn't known, so "en" is used for the "html" element and no
|
||||
language is used for the "textarea" element.
|
||||
"edit.html" and "add.html" are the templates used to edit a page. If the page
|
||||
already exists, its language is used for the "textarea" element. If the page is
|
||||
new, 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.
|
||||
|
||||
32
man/oddmu-toc.1
Normal file
32
man/oddmu-toc.1
Normal file
@@ -0,0 +1,32 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-TOC" "1" "2025-04-05"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu-toc - print the table of contents (toc) for pages
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu toc\fR \fIpage names.\&.\&.\&\fR
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "toc" subcommand prints the table of contents for one or more Markdown
|
||||
files.\& Use "-" as the page name if you want to read Markdown from \fBstdin\fR.\&
|
||||
.PP
|
||||
This can be useful for very long pages that need a table of contents
|
||||
at the beginning.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
|
||||
25
man/oddmu-toc.1.txt
Normal file
25
man/oddmu-toc.1.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
ODDMU-TOC(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-toc - print the table of contents (toc) for pages
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu toc* _page names..._
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "toc" subcommand prints the table of contents for one or more Markdown
|
||||
files. Use "-" as the page name if you want to read Markdown from *stdin*.
|
||||
|
||||
This can be useful for very long pages that need a table of contents
|
||||
at the beginning.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
Maintained by Alex Schroeder <alex@gnu.org>.
|
||||
188
man/oddmu-webdav.5
Normal file
188
man/oddmu-webdav.5
Normal 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
169
man/oddmu-webdav.5.txt
Normal 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>.
|
||||
69
man/oddmu.1
69
man/oddmu.1
@@ -5,18 +5,20 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "1" "2024-05-11"
|
||||
.TH "ODDMU" "1" "2025-03-14"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
oddmu - a wiki server
|
||||
.PP
|
||||
Oddmu is sometimes written Oddµ because µ is the letter mu.\&
|
||||
Oddmu is sometimes written Oddμ because μ is the letter mu.\&
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.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
|
||||
@@ -92,7 +100,7 @@ curl --form body="Did you bring a towel?"
|
||||
.RE
|
||||
.PP
|
||||
When calling the \fIdrop\fR action, the query parameters used are \fIname\fR for the
|
||||
target filename, \fIfile\fR for the file to upload.\& If the query parameter
|
||||
target filename and \fIfile\fR for the file to upload.\& If the query parameter
|
||||
\fImaxwidth\fR is set, an attempt is made to decode and resize the image.\& JPG, PNG,
|
||||
WEBP and HEIC files can be decoded.\& Only JPG and PNG files can be encoded,
|
||||
however.\& If the target name ends in \fI.\&jpg\fR, the \fIquality\fR query parameter is
|
||||
@@ -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,24 +390,36 @@ Oddmu running as a webserver:
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
\fIoddmu-html\fR(1), on how to render a page from the command-line
|
||||
\fIoddmu-hashtags\fR(1), on how to count the hashtags used
|
||||
.IP \(bu 4
|
||||
\fIoddmu-list\fR(1), on how to list pages and titles from the command-line
|
||||
\fIoddmu-html\fR(1), on how to render a page
|
||||
.IP \(bu 4
|
||||
\fIoddmu-missing\fR(1), on how to find broken local links from the command-line
|
||||
\fIoddmu-list\fR(1), on how to list pages and titles
|
||||
.IP \(bu 4
|
||||
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages from the
|
||||
command-line
|
||||
\fIoddmu-links\fR(1), on how to list the outgoing links for a page
|
||||
.IP \(bu 4
|
||||
\fIoddmu-replace\fR(1), on how to search and replace text from the command-line
|
||||
\fIoddmu-missing\fR(1), on how to find broken local links
|
||||
.IP \(bu 4
|
||||
\fIoddmu-search\fR(1), on how to run a search from the command-line
|
||||
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages
|
||||
.IP \(bu 4
|
||||
\fIoddmu-static\fR(1), on generating a static site 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
|
||||
.IP \(bu 4
|
||||
\fIoddmu-static\fR(1), on generating a static site
|
||||
.IP \(bu 4
|
||||
\fIoddmu-toc\fR(1), on how to list the table of contents (toc) a page
|
||||
.IP \(bu 4
|
||||
\fIoddmu-version\fR(1), on how to get all the build information from the binary
|
||||
.PD
|
||||
.PP
|
||||
If you want to stop using Oddmu:
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
\fIoddmu-export\fR(1), on how to export all the files as one big RSS file
|
||||
.PD
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
Maintained by Alex Schroeder <alex@gnu.\&org>.\&
|
||||
|
||||
@@ -4,12 +4,14 @@ ODDMU(1)
|
||||
|
||||
oddmu - a wiki server
|
||||
|
||||
Oddmu is sometimes written Oddµ because µ is the letter mu.
|
||||
Oddmu is sometimes written Oddμ because μ is the letter mu.
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*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
|
||||
|
||||
@@ -66,7 +71,7 @@ curl --form body="Did you bring a towel?" \
|
||||
```
|
||||
|
||||
When calling the _drop_ action, the query parameters used are _name_ for the
|
||||
target filename, _file_ for the file to upload. If the query parameter
|
||||
target filename and _file_ for the file to upload. If the query parameter
|
||||
_maxwidth_ is set, an attempt is made to decode and resize the image. JPG, PNG,
|
||||
WEBP and HEIC files can be decoded. Only JPG and PNG files can be encoded,
|
||||
however. If the target name ends in _.jpg_, the _quality_ query parameter is
|
||||
@@ -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,21 +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-html_(1), on how to render a page from the command-line
|
||||
- _oddmu-list_(1), on how to list pages and titles from the command-line
|
||||
- _oddmu-missing_(1), on how to find broken local links from the command-line
|
||||
- _oddmu-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-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>.
|
||||
|
||||
26
man/oddmu.5
26
man/oddmu.5
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "5" "2024-02-17" "File Formats Manual"
|
||||
.TH "ODDMU" "5" "2025-03-05" "File Formats Manual"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -28,16 +28,15 @@ The page name has to be percent-encoded.\& See the section "Percent Encoding".\&
|
||||
If you link to the actual Markdown file (with the ".\&md" extension), then Oddmu
|
||||
serves the Markdown file!\&
|
||||
.PP
|
||||
There are three Oddµ-specific extensions: local links, hashtags and fediverse
|
||||
There are three Oddμ-specific extensions: local links, hashtags and fediverse
|
||||
account links.\& The Markdown library used features some additional extensions,
|
||||
most importantly tables and definition lists.\&
|
||||
.PP
|
||||
.SS Local links
|
||||
.PP
|
||||
Local links use double square brackets [[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,12 +89,12 @@ 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
|
||||
page needs to be retrieved via webfinger.\& Oddµ does that in the background, and
|
||||
page needs to be retrieved via webfinger.\& Oddμ does that in the background, and
|
||||
as soon as the information is available, the actual profile link is used when
|
||||
pages are rendered.\& In the example above, the result would be
|
||||
"https://social.\&alexschroeder.\&ch/@alex".\&
|
||||
@@ -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
|
||||
|
||||
@@ -19,16 +19,15 @@ The page name has to be percent-encoded. See the section "Percent Encoding".
|
||||
If you link to the actual Markdown file (with the ".md" extension), then Oddmu
|
||||
serves the Markdown file!
|
||||
|
||||
There are three Oddµ-specific extensions: local links, hashtags and fediverse
|
||||
There are three Oddμ-specific extensions: local links, hashtags and fediverse
|
||||
account links. The Markdown library used features some additional extensions,
|
||||
most importantly tables and definition lists.
|
||||
|
||||
## Local links
|
||||
|
||||
Local links use double square brackets [[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,12 +74,12 @@ 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
|
||||
page needs to be retrieved via webfinger. Oddµ does that in the background, and
|
||||
page needs to be retrieved via webfinger. Oddμ does that in the background, and
|
||||
as soon as the information is available, the actual profile link is used when
|
||||
pages are rendered. In the example above, the result would be
|
||||
"https://social.alexschroeder.ch/@alex".
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU.SERVICE" "5" "2024-04-21"
|
||||
.TH "ODDMU.SERVICE" "5" "2025-03-14"
|
||||
.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
|
||||
@@ -95,8 +94,8 @@ sudo mkdir /run/oddmu
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The unit file for the service defines where the "oddmu" is and where the data
|
||||
directory is.\& These are the lines you most likely have to take care of:
|
||||
The unit file for the service defines where the Oddmu binary is and where the
|
||||
data directory is.\& These are the lines you most likely have to take care of:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
@@ -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
|
||||
@@ -131,10 +128,90 @@ echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
|
||||
Now you need to set up your web browser to use the Unix domain socket.\& See
|
||||
\fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example configurations.\&
|
||||
.PP
|
||||
.SS A personal wiki
|
||||
.PP
|
||||
On a single user machine, it might be useful to have a single wiki for the main
|
||||
user available.\& In order to do this, setup a "user" unit using systemd and save
|
||||
the following as "user-unix-domain.\&service":
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
[Unit]
|
||||
Description=Oddmu
|
||||
After=network\&.target
|
||||
[Install]
|
||||
WantedBy=default\&.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
ExecStart=/home/alex/src/oddmu/oddmu
|
||||
WorkingDirectory=/home/alex/wiki
|
||||
Environment="ODDMU_LANGUAGES=de,en"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Make sure to change the "ExecStart" entry so that it points to your copy of the
|
||||
Oddmu binary.\&
|
||||
.PP
|
||||
Since this is a user service, the same user can edit the files using their
|
||||
favourite text editor.\&
|
||||
.PP
|
||||
Install it:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
systemctl --user enable --now \&./user-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
To examine the log:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
journalctl --user --unit user-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Note that no sudo is required!\&
|
||||
.PP
|
||||
.SS Using the priviledged port 80
|
||||
.PP
|
||||
When running a personal wiki, you can have the oddmu binary listen on port 80,
|
||||
the standard HTTP port.\& It is not really worth the effort: It means that you can
|
||||
visit "http://localhost/" instead of "http://localhost:8080".\& Nevertheless, if
|
||||
you'\&re interested in giving it a try, here'\&s how to do it.\&
|
||||
.PP
|
||||
The service definition must specify the new port:
|
||||
.PP
|
||||
Environment="ODDMU_PORT=80"
|
||||
.PP
|
||||
Since this is a privileged port, the binary needs an extra capability for an
|
||||
ordinary user to do this.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
sudo setcap \&'cap_net_bind_service=+ep\&' oddmu
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Note that as soon as you recompile, the capability is gone again and the above
|
||||
must be repeated.\&
|
||||
.PP
|
||||
.SH SECURITY
|
||||
.PP
|
||||
Only allow direct access to Oddmu on systems and networks where you'\&re OK with
|
||||
every user editing the pages.\& On the open web, this is not true.\& If your server
|
||||
is on the open web, always run Oddmu behind a regular web server acting as a
|
||||
reverse proxy, limiting regular visitors to read-only access.\& This means that
|
||||
the regular web server listens on the regular privileged ports (80 for HTTP,
|
||||
443 for HTTPS) and passes requests to Oddmu on some other port.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
|
||||
\fIsystemd.\&socket(5), \fRcapabilities_(7)
|
||||
\fIsystemd.\&socket\fR(5), \fIcapabilities\fR(7)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
|
||||
@@ -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/.
|
||||
@@ -76,8 +75,8 @@ the directory or change the file name.
|
||||
sudo mkdir /run/oddmu
|
||||
```
|
||||
|
||||
The unit file for the service defines where the "oddmu" is and where the data
|
||||
directory is. These are the lines you most likely have to take care of:
|
||||
The unit file for the service defines where the Oddmu binary is and where the
|
||||
data directory is. These are the lines you most likely have to take care of:
|
||||
|
||||
```
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
@@ -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)_:
|
||||
@@ -106,10 +103,82 @@ echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
|
||||
Now you need to set up your web browser to use the Unix domain socket. See
|
||||
_oddmu-apache_(5) or _oddmu-nginx_(5) for example configurations.
|
||||
|
||||
## A personal wiki
|
||||
|
||||
On a single user machine, it might be useful to have a single wiki for the main
|
||||
user available. In order to do this, setup a "user" unit using systemd and save
|
||||
the following as "user-unix-domain.service":
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Oddmu
|
||||
After=network.target
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
ExecStart=/home/alex/src/oddmu/oddmu
|
||||
WorkingDirectory=/home/alex/wiki
|
||||
Environment="ODDMU_LANGUAGES=de,en"
|
||||
```
|
||||
|
||||
Make sure to change the "ExecStart" entry so that it points to your copy of the
|
||||
Oddmu binary.
|
||||
|
||||
Since this is a user service, the same user can edit the files using their
|
||||
favourite text editor.
|
||||
|
||||
Install it:
|
||||
|
||||
```
|
||||
systemctl --user enable --now ./user-unix-domain.service
|
||||
```
|
||||
|
||||
To examine the log:
|
||||
|
||||
```
|
||||
journalctl --user --unit user-unix-domain.service
|
||||
```
|
||||
|
||||
Note that no sudo is required!
|
||||
|
||||
## Using the priviledged port 80
|
||||
|
||||
When running a personal wiki, you can have the oddmu binary listen on port 80,
|
||||
the standard HTTP port. It is not really worth the effort: It means that you can
|
||||
visit "http://localhost/" instead of "http://localhost:8080". Nevertheless, if
|
||||
you're interested in giving it a try, here's how to do it.
|
||||
|
||||
The service definition must specify the new port:
|
||||
|
||||
Environment="ODDMU_PORT=80"
|
||||
|
||||
Since this is a privileged port, the binary needs an extra capability for an
|
||||
ordinary user to do this.
|
||||
|
||||
```
|
||||
sudo setcap 'cap_net_bind_service=+ep' oddmu
|
||||
```
|
||||
|
||||
Note that as soon as you recompile, the capability is gone again and the above
|
||||
must be repeated.
|
||||
|
||||
# SECURITY
|
||||
|
||||
Only allow direct access to Oddmu on systems and networks where you're OK with
|
||||
every user editing the pages. On the open web, this is not true. If your server
|
||||
is on the open web, always run Oddmu behind a regular web server acting as a
|
||||
reverse proxy, limiting regular visitors to read-only access. This means that
|
||||
the regular web server listens on the regular privileged ports (80 for HTTP,
|
||||
443 for HTTPS) and passes requests to Oddmu on some other port.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
|
||||
_systemd.socket(5), _capabilities_(7)
|
||||
_systemd.socket_(5), _capabilities_(7)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
|
||||
31
man/scdoc-to-markdown
Executable file
31
man/scdoc-to-markdown
Executable 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;
|
||||
}
|
||||
98
man_test.go
98
man_test.go
@@ -7,23 +7,27 @@ 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)
|
||||
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
|
||||
count := 0
|
||||
filepath.Walk("man", func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(path, ".txt") &&
|
||||
path != "man/oddmu.1.txt" {
|
||||
s := strings.TrimPrefix(path, "man/")
|
||||
if strings.HasSuffix(fp, ".txt") &&
|
||||
fp != "man/oddmu.1.txt" {
|
||||
count++
|
||||
s := strings.TrimPrefix(fp, "man/")
|
||||
s = strings.TrimSuffix(s, ".txt")
|
||||
i := strings.LastIndex(s, ".")
|
||||
ref := "_" + s[:i] + "_(" + s[i+1:] + ")"
|
||||
@@ -31,40 +35,99 @@ func TestManPages(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
assert.Greater(t, count, 0, "no man pages were found")
|
||||
}
|
||||
|
||||
func TestReadme(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
main := string(b)
|
||||
// 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)
|
||||
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
|
||||
count := 0
|
||||
filepath.Walk(".", func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(path, ".txt") {
|
||||
s := strings.TrimPrefix(path, "man/")
|
||||
if strings.HasSuffix(fp, ".html") {
|
||||
count++
|
||||
assert.Contains(t, man, fp, fp)
|
||||
}
|
||||
if fp != "." && 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(`\.HandleFunc\("(/[a-z]+/)", makeHandler\([a-z]+Handler, (true|false)\)\)`)
|
||||
for _, match := range re.FindAllStringSubmatch(wiki, -1) {
|
||||
count++
|
||||
var path string
|
||||
if match[2] == "true" {
|
||||
path = "_" + match[1] + "dir/name"
|
||||
} else {
|
||||
path = "_" + match[1] + "dir/"
|
||||
}
|
||||
assert.Contains(t, main, path, path)
|
||||
}
|
||||
assert.Greater(t, count, 0, "no handlers were found")
|
||||
// root handler is manual
|
||||
assert.Contains(t, main, "\n- _/_", "root")
|
||||
}
|
||||
|
||||
// Does the README link to all the man pages and all the Go source files,
|
||||
// excluding the command and test files?
|
||||
func TestReadme(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
readme := string(b)
|
||||
assert.NoError(t, err)
|
||||
count := 0
|
||||
filepath.Walk("man", func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(fp, ".txt") {
|
||||
count++
|
||||
s := strings.TrimPrefix(fp, "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
|
||||
})
|
||||
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
assert.Greater(t, count, 0, "no man pages were found")
|
||||
count = 0
|
||||
filepath.Walk(".", func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(path, ".go") &&
|
||||
!strings.HasSuffix(path, "_test.go") &&
|
||||
!strings.HasSuffix(path, "_cmd.go") {
|
||||
s := strings.TrimPrefix(path, "./")
|
||||
if strings.HasSuffix(fp, ".go") &&
|
||||
!strings.HasSuffix(fp, "_test.go") &&
|
||||
!strings.HasSuffix(fp, "_cmd.go") {
|
||||
count++
|
||||
s := strings.TrimPrefix(fp, "./")
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
@@ -124,8 +97,17 @@ func (p *Page) links() []string {
|
||||
switch v := node.(type) {
|
||||
case *ast.Link:
|
||||
link := string(v.Destination)
|
||||
dir := p.Dir()
|
||||
links = append(links, path.Join(dir, link))
|
||||
url, err := url.Parse(link)
|
||||
if err != nil {
|
||||
// no error reporting
|
||||
return ast.GoToNext
|
||||
}
|
||||
if url.IsAbs() {
|
||||
links = append(links, link)
|
||||
} else {
|
||||
dir := p.Dir()
|
||||
links = append(links, path.Join(dir, link))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.GoToNext
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type notifyCmd struct {
|
||||
@@ -32,6 +33,11 @@ func (cmd *notifyCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
|
||||
func notifyCli(w io.Writer, args []string) subcommands.ExitStatus {
|
||||
index.load()
|
||||
for _, name := range args {
|
||||
if !strings.HasSuffix(name, ".md") {
|
||||
fmt.Fprintf(os.Stderr, "%s does not end in '.md'\n", name)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
name = name[0:len(name)-3]
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Loading %s: %s\n", name, err)
|
||||
|
||||
80
page.go
80
page.go
@@ -28,8 +28,8 @@ type Page struct {
|
||||
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
|
||||
// the Name "foo").
|
||||
type Link struct {
|
||||
Title string
|
||||
Url string
|
||||
Title string
|
||||
Url string
|
||||
}
|
||||
|
||||
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
|
||||
@@ -64,7 +64,7 @@ func nameEscape(s string) string {
|
||||
// carriage return characters ("\r"). Page.Title and Page.Html are not saved. There is no caching. Before removing or
|
||||
// writing a file, the old copy is renamed to a backup, appending "~". Errors are not logged but returned.
|
||||
func (p *Page) save() error {
|
||||
fp := filepath.FromSlash(p.Name + ".md")
|
||||
fp := filepath.FromSlash(p.Name) + ".md"
|
||||
watches.ignore(fp)
|
||||
s := bytes.ReplaceAll(p.Body, []byte{'\r'}, []byte{})
|
||||
if len(s) == 0 {
|
||||
@@ -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 filepath. 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
|
||||
}
|
||||
@@ -107,13 +113,13 @@ func backup(fp string) error {
|
||||
// loadPage loads a Page given a name. The path loaded is that Page.Name with the ".md" extension. The Page.Title is set
|
||||
// to the Page.Name (and possibly changed, later). The Page.Body is set to the file content. The Page.Html remains
|
||||
// undefined (there is no caching).
|
||||
func loadPage(path string) (*Page, error) {
|
||||
path = strings.TrimPrefix(path, "./") // result of a filepath.TreeWalk starting with "."
|
||||
body, err := os.ReadFile(filepath.FromSlash(path + ".md"))
|
||||
func loadPage(name string) (*Page, error) {
|
||||
name = strings.TrimPrefix(name, "./") // result of a path.TreeWalk starting with "."
|
||||
body, err := os.ReadFile(filepath.FromSlash(name) + ".md")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: path, Name: path, Body: body}, nil
|
||||
return &Page{Title: name, Name: name, Body: body}, nil
|
||||
}
|
||||
|
||||
// handleTitle extracts the title from a Page and sets Page.Title, if any. If replace is true, the page title is also
|
||||
@@ -133,7 +139,6 @@ func (p *Page) handleTitle(replace bool) {
|
||||
// summarize sets Page.Html to an extract.
|
||||
func (p *Page) summarize(q string) {
|
||||
t := p.plainText()
|
||||
p.Name = nameEscape(p.Name)
|
||||
p.Html = sanitizeStrict(snippets(q, t))
|
||||
}
|
||||
|
||||
@@ -143,20 +148,53 @@ func (p *Page) IsBlog() bool {
|
||||
return blogRe.MatchString(name)
|
||||
}
|
||||
|
||||
// Dir returns the directory the page is in. It's either the empty string if the page is in the Oddmu working directory,
|
||||
// or it ends in a slash. This is used to create the upload link in "view.html", for example.
|
||||
const upperhex = "0123456789ABCDEF"
|
||||
|
||||
// Path returns the Page.Name with some characters escaped because html/template doesn't escape those. This is suitable
|
||||
// for use in HTML templates.
|
||||
func (p *Page) Path() string {
|
||||
return pathEncode(p.Name)
|
||||
}
|
||||
|
||||
// pathEncode returns the page name with some characters escaped because html/template doesn't escape those. This is
|
||||
// suitable for use in HTML templates.
|
||||
func pathEncode(s string) string {
|
||||
n := strings.Count(s, ";") + strings.Count(s, ",") + strings.Count(s, "?") + strings.Count(s, "#")
|
||||
if n == 0 {
|
||||
return s
|
||||
}
|
||||
t := make([]byte, len(s) + 2*n)
|
||||
j := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case ';', ',', '?', '#':
|
||||
t[j] = '%'
|
||||
t[j+1] = upperhex[s[i]>>4]
|
||||
t[j+2] = upperhex[s[i]&15]
|
||||
j += 3
|
||||
default:
|
||||
t[j] = s[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
|
||||
// Dir returns the directory part of the page name, percent-escaped except for the slashes. It's either the empty string
|
||||
// if the page is in the Oddmu working directory, or it ends in a slash. This is used to create the upload link in
|
||||
// "view.html", for example.
|
||||
func (p *Page) Dir() string {
|
||||
d := filepath.Dir(p.Name)
|
||||
d := path.Dir(p.Name)
|
||||
if d == "." {
|
||||
return ""
|
||||
}
|
||||
return d + "/"
|
||||
return pathEncode(d) + "/"
|
||||
}
|
||||
|
||||
// Base returns the basename of the page name: no directory. This is used to create the upload link in "view.html", for
|
||||
// example.
|
||||
// Base returns the basename of the page name: no directory, percent-escaped except for the slashes. This is used to
|
||||
// create the upload link in "view.html", for example.
|
||||
func (p *Page) Base() string {
|
||||
n := filepath.Base(p.Name)
|
||||
n := path.Base(p.Name)
|
||||
if n == "." {
|
||||
return ""
|
||||
}
|
||||
@@ -179,13 +217,13 @@ func (p *Page) Parents() []*Link {
|
||||
return links
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < len(elems) - 1; i++ {
|
||||
for i := 0; i < len(elems)-1; i++ {
|
||||
name := s + "index"
|
||||
title, ok := index.titles[name]
|
||||
if !ok {
|
||||
title = "…"
|
||||
}
|
||||
link := &Link{ Title: title, Url: strings.Repeat("../", len(elems) - i - 1) + "index" }
|
||||
link := &Link{Title: title, Url: strings.Repeat("../", len(elems)-i-1) + "index"}
|
||||
links = append(links, link)
|
||||
s += elems[i] + "/"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ And untouchable`)}
|
||||
p = &Page{Name: "testdata/parents/children/something/other"}
|
||||
// "testdata/parents/children/something/index" is a sibling and doesn't count!
|
||||
parents := p.Parents()
|
||||
assert.Equal(t, "Welcome to Oddµ", parents[0].Title)
|
||||
assert.Equal(t, "Welcome to Oddμ", parents[0].Title)
|
||||
assert.Equal(t, "../../../../index", parents[0].Url)
|
||||
assert.Equal(t, "…", parents[1].Title)
|
||||
assert.Equal(t, "../../../index", parents[1].Url)
|
||||
|
||||
30
parser.go
30
parser.go
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// wikiLink returns an inline parser function. This indirection is
|
||||
@@ -37,9 +36,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 +67,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,12 +89,11 @@ 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()
|
||||
maybeUnsafeHTML := markdown.ToHTML(p.Body, parser, renderer)
|
||||
p.Name = nameEscape(p.Name)
|
||||
p.Html = unsafeBytes(maybeUnsafeHTML)
|
||||
p.Hashtags = *hashtags
|
||||
}
|
||||
@@ -124,7 +127,7 @@ func (p *Page) plainText() string {
|
||||
|
||||
// images returns an array of ImageData.
|
||||
func (p *Page) images() []ImageData {
|
||||
dir := path.Dir(filepath.ToSlash(p.Name))
|
||||
dir := p.Dir()
|
||||
images := make([]ImageData, 0)
|
||||
parser := parser.New()
|
||||
doc := markdown.Parse(p.Body, parser)
|
||||
@@ -147,6 +150,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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
23
preview.html
23
preview.html
@@ -3,17 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<base href="/view/{{.Dir}}">
|
||||
<title>Preview: {{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
input#search { width: 12ch; }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe }
|
||||
body { hyphens: auto }
|
||||
header a { margin-right: 1ch }
|
||||
form { display: inline-block }
|
||||
input#search { width: 12ch }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px }
|
||||
footer { border-top: 1px solid #888 }
|
||||
img { max-width: 100%; }
|
||||
img { max-width: 100% }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -27,12 +28,12 @@ img { max-width: 100%; }
|
||||
<hr>
|
||||
<section id="edit">
|
||||
<h2>Editing {{.Title}}</h2>
|
||||
<form action="/save/{{.Name}}" method="POST">
|
||||
<form action="/save/{{.Path}}" method="POST">
|
||||
<textarea name="body" rows="20" cols="80" lang="{{.Language}}" autofocus>{{printf "# %s\n\n%s" .Title .Body}}</textarea>
|
||||
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
|
||||
<p><input type="submit" value="Save">
|
||||
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
|
||||
<button formaction="/preview/{{.Path}}" type="submit">Preview</button>
|
||||
<a href="/view/{{.Path}}"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
17
preview_test.go
Normal file
17
preview_test.go
Normal 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>!")
|
||||
}
|
||||
@@ -55,12 +55,12 @@ func replaceCli(w io.Writer, isConfirmed bool, isRegexp bool, args []string) sub
|
||||
}
|
||||
repl := []byte(args[1])
|
||||
changes := 0
|
||||
err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
err := filepath.Walk(".", func(fp 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 fp != "." && strings.HasPrefix(filepath.Base(fp), ".") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
@@ -68,10 +68,10 @@ func replaceCli(w io.Writer, isConfirmed bool, isRegexp bool, args []string) sub
|
||||
}
|
||||
}
|
||||
// skipp all but page files
|
||||
if !strings.HasSuffix(path, ".md") {
|
||||
if !strings.HasSuffix(fp, ".md") {
|
||||
return nil
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
body, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,15 +79,15 @@ func replaceCli(w io.Writer, isConfirmed bool, isRegexp bool, args []string) sub
|
||||
if !slices.Equal(result, body) {
|
||||
changes++
|
||||
if isConfirmed {
|
||||
fmt.Fprintln(w, path)
|
||||
_ = os.Rename(path, path+"~")
|
||||
err = os.WriteFile(path, result, 0644)
|
||||
fmt.Fprintln(w, fp)
|
||||
_ = os.Rename(fp, fp + "~")
|
||||
err = os.WriteFile(fp, result, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
edits := myers.ComputeEdits(span.URIFromPath(path+"~"), string(body), string(result))
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(path+"~", path, string(body), edits))
|
||||
edits := myers.ComputeEdits(span.URIFromPath(fp + "~"), string(body), string(result))
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(fp + "~", fp, string(body), edits))
|
||||
fmt.Fprintln(w, diff)
|
||||
}
|
||||
}
|
||||
|
||||
12
search.go
12
search.go
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// well the page matched for a search query. Images are the images whose description match the query.
|
||||
type Result struct {
|
||||
Page
|
||||
Score int
|
||||
Score int
|
||||
Images []ImageData
|
||||
}
|
||||
|
||||
@@ -131,8 +131,8 @@ func search(q, dir, filter string, page int, all bool) ([]*Result, bool) {
|
||||
}
|
||||
if len(terms) > 0 {
|
||||
index.RLock()
|
||||
res := make([]ImageData, 0)
|
||||
for _, r := range results {
|
||||
res := make([]ImageData, 0)
|
||||
ImageLoop:
|
||||
for _, img := range index.images[r.Name] {
|
||||
title := strings.ToLower(img.Title)
|
||||
@@ -300,3 +300,9 @@ func searchHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
Results: len(items) > 0, More: more}
|
||||
renderTemplate(w, dir, "search", s)
|
||||
}
|
||||
|
||||
// Path returns the ImageData.Name with some characters escaped because html/template doesn't escape those. This is
|
||||
// suitable for use in HTML templates.
|
||||
func (img *ImageData) Path() string {
|
||||
return pathEncode(img.Name)
|
||||
}
|
||||
|
||||
26
search.html
26
search.html
@@ -3,24 +3,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Search for {{.Query}}</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
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; }
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe }
|
||||
body { hyphens: auto }
|
||||
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%; }
|
||||
.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="#main">Skip</a>
|
||||
<a href="/view/index">Home</a>
|
||||
<form role="search" action="/search/{{.Dir}}" method="GET">
|
||||
<label for="search">Search:</label>
|
||||
@@ -38,11 +38,11 @@ button { background-color: #eee; color: inherit; border-radius: 4px; border-widt
|
||||
{{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>
|
||||
<p><a class="result" href="/view/{{.Path}}">{{.Title}}</a>
|
||||
<span class="score">{{.Score}}</span></p>
|
||||
<blockquote>{{.Html}}</blockquote>
|
||||
{{range .Images}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img class="last" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
<p class="image"><a href="/view/{{.Path}}"><img loading="lazy" src="/view/{{.Path}}"></a><br/>{{.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"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) 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
|
||||
@@ -84,8 +94,8 @@ func searchCli(w io.Writer, dir string, n int, all, extract bool, quiet bool, ar
|
||||
|
||||
// searchExtract prints the search extracts to stdout with highlighting for a terminal.
|
||||
func searchExtract(w io.Writer, items []*Result) {
|
||||
heading := func (s string) string { return "\x1b[1;4m" + s + "\x1b[0m" } // bold + underline
|
||||
match := func (s string) string { return "\x1b[1m" + s + "\x1b[0m" } // bold
|
||||
heading := func(s string) string { return "\x1b[1;4m" + s + "\x1b[0m" } // bold + underline
|
||||
match := func(s string) string { return "\x1b[1m" + s + "\x1b[0m" } // bold
|
||||
re := regexp.MustCompile(`<b>(.*?)</b>`)
|
||||
for _, p := range items {
|
||||
s := re.ReplaceAllString(string(p.Html), match(`$1`))
|
||||
@@ -101,7 +111,7 @@ func searchExtract(w io.Writer, items []*Result) {
|
||||
if err != nil {
|
||||
name = img.Name
|
||||
}
|
||||
fmt.Fprintln(w, " - ", name);
|
||||
fmt.Fprintln(w, " - ", name)
|
||||
for _, s := range strings.Split(wordwrap.String(img.Title, 70), "\n") {
|
||||
fmt.Fprintln(w, " ", s)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ 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)
|
||||
r := `* [Oddμ: A minimal wiki](README)
|
||||
* [Themes](themes/index)
|
||||
* [Welcome to Oddµ](index)
|
||||
* [Welcome to Oddμ](index)
|
||||
`
|
||||
assert.Equal(t, r, b.String())
|
||||
}
|
||||
@@ -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)
|
||||
`
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestSearch(t *testing.T) {
|
||||
index.load()
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("q", "oddµ")
|
||||
data.Set("q", "oddμ")
|
||||
|
||||
body := assert.HTTPBody(makeHandler(searchHandler, false), "GET", "/search/", data)
|
||||
assert.Contains(t, body, "Welcome")
|
||||
@@ -69,6 +69,10 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
body = assert.HTTPBody(makeHandler(searchHandler, false), "GET", "/search/testdata", data)
|
||||
assert.NotContains(t, body, "Welcome")
|
||||
|
||||
data.Set("q", "'create a new page'")
|
||||
body = assert.HTTPBody(makeHandler(searchHandler, false), "GET", "/search/", data)
|
||||
assert.Contains(t, body, "Welcome")
|
||||
}
|
||||
|
||||
func TestSearchFilter(t *testing.T) {
|
||||
@@ -173,12 +177,12 @@ 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"
|
||||
assert.Equal(t, 1, len(items), "one page found")
|
||||
assert.Equal(t, "index", items[0].Name, "Welcome to Oddµ")
|
||||
assert.Equal(t, "index", items[0].Name, "Welcome to Oddμ")
|
||||
assert.Greater(t, items[0].Score, 0, "matches result in a score")
|
||||
assert.False(t, more)
|
||||
|
||||
@@ -230,9 +234,9 @@ A quick sip too quick
|
||||
func TestImageSearch(t *testing.T) {
|
||||
cleanup(t, "testdata/images")
|
||||
|
||||
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# Pictures
|
||||
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# 2024-07-21 Pictures
|
||||
|
||||

|
||||

|
||||
|
||||
Pictures in the box
|
||||
Tiny windows to our past
|
||||
@@ -241,12 +245,26 @@ Where are you, my love?
|
||||
`)}
|
||||
p.save()
|
||||
|
||||
items, _ := search("phone", "testdata/images", "", 1, false)
|
||||
assert.Equal(t, 1, len(items), "one page found")
|
||||
assert.Equal(t, "Pictures", items[0].Title)
|
||||
assert.Equal(t, "phone", items[0].Images[0].Title)
|
||||
assert.Equal(t, "<b>phone</b>", string(items[0].Images[0].Html))
|
||||
q := &Page{Name: "testdata/images/2024-07-22", Body: []byte(`# 2024-07-22 The Moon
|
||||
|
||||
When the night is light
|
||||
Behind clouds the moon is bright
|
||||
Please call me, my love.
|
||||
`)}
|
||||
q.save()
|
||||
|
||||
items, _ := search("call", "testdata/images", "", 1, false)
|
||||
assert.Equal(t, 2, len(items), "two pages found")
|
||||
|
||||
assert.Equal(t, "2024-07-21 Pictures", items[0].Title)
|
||||
assert.Equal(t, "2024-07-22 The Moon", items[1].Title)
|
||||
|
||||
assert.NotEmpty(t, items[0].Images)
|
||||
assert.Equal(t, "phone call", items[0].Images[0].Title)
|
||||
assert.Equal(t, "phone <b>call</b>", string(items[0].Images[0].Html))
|
||||
assert.Equal(t, "testdata/images/2024-07-21.jpg", items[0].Images[0].Name)
|
||||
|
||||
assert.Empty(t, items[1].Images)
|
||||
}
|
||||
|
||||
func TestSearchQuestionmark(t *testing.T) {
|
||||
@@ -259,7 +277,7 @@ The silence streches.`)}
|
||||
p.save()
|
||||
data := url.Values{}
|
||||
data.Set("q", "look")
|
||||
body := assert.HTTPBody(makeHandler(searchHandler, false), "GET", "/search/", data)
|
||||
body := assert.HTTPBody(makeHandler(searchHandler, false), "GET", "/search/testdata/question/", data)
|
||||
assert.Contains(t, body, "We <b>look</b>")
|
||||
assert.NotContains(t, body, "Odd?")
|
||||
assert.Contains(t, body, "Even?")
|
||||
|
||||
16
static.html
16
static.html
@@ -3,17 +3,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
input#search { width: 12ch; }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
|
||||
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe }
|
||||
body { hyphens: auto }
|
||||
header a { margin-right: 1ch }
|
||||
form { display: inline-block }
|
||||
input#search { width: 12ch }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px }
|
||||
footer { border-top: 1px solid #888 }
|
||||
img { max-width: 100%; }
|
||||
img { max-width: 100% }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -42,17 +42,17 @@ func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
dir := filepath.Clean(args[0])
|
||||
return staticCli(dir, cmd.jobs, false)
|
||||
return staticCli(".", dir, cmd.jobs, false)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
source, target string
|
||||
info fs.FileInfo
|
||||
info fs.FileInfo
|
||||
}
|
||||
|
||||
// staticCli generates a static site in the designated directory. The quiet flag is used to suppress output when running
|
||||
// tests.
|
||||
func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
// tests. The source directory cannot be set from the command-line. The current directory (".") is assumed.
|
||||
func staticCli(source, target string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
index.load()
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
@@ -65,7 +65,7 @@ func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
for i := 0; i < jobs; i++ {
|
||||
go staticWorker(tasks, results, done)
|
||||
}
|
||||
go staticWalk(dir, tasks, stop)
|
||||
go staticWalk(source, target, tasks, stop)
|
||||
go staticWatch(jobs, results, done)
|
||||
n, err := staticProgressIndicator(results, stop, quiet)
|
||||
if !quiet {
|
||||
@@ -78,23 +78,22 @@ func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// staticWalk walks the directory tree. Any directory it finds, it recreates in the destination directory. Any file it
|
||||
// staticWalk walks the source directory tree. Any directory it finds, it recreates in the target directory. Any file it
|
||||
// finds, it puts into the tasks channel for the staticWorker. When the directory walk is finished, the tasks channel is
|
||||
// closed. If there's an error on the stop channel, the walk returns that error.
|
||||
func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
func staticWalk(source, target string, tasks chan (args), stop chan (error)) {
|
||||
// The error returned here is what's in the stop channel but at the very end, a worker might return an error
|
||||
// even though the walk is already done. This is why we cannot rely on the return value of the walk.
|
||||
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
filepath.Walk(source, func(fp string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case err := <- stop:
|
||||
case err := <-stop:
|
||||
return err
|
||||
default:
|
||||
base := filepath.Base(path)
|
||||
// skip hidden directories and files
|
||||
if path != "." && strings.HasPrefix(base, ".") {
|
||||
if fp != "." && strings.HasPrefix(filepath.Base(fp), ".") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
@@ -102,18 +101,28 @@ func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
}
|
||||
}
|
||||
// skip backup files, avoid recursion
|
||||
if strings.HasSuffix(path, "~") || strings.HasPrefix(path, dir) {
|
||||
if strings.HasSuffix(fp, "~") || strings.HasPrefix(fp, target) {
|
||||
return nil
|
||||
}
|
||||
// determine the actual target: if source is a/ and target is b/ and path is a/file, then the
|
||||
// target is b/file
|
||||
var actualTarget string
|
||||
if source == "." {
|
||||
actualTarget = filepath.Join(target, fp)
|
||||
} else {
|
||||
if !strings.HasPrefix(fp, source) {
|
||||
return fmt.Errorf("%s is not a subdirectory of %s", fp, source)
|
||||
}
|
||||
actualTarget = filepath.Join(target, fp[len(source):])
|
||||
}
|
||||
// recreate subdirectories
|
||||
target := filepath.Join(dir, path)
|
||||
if info.IsDir() {
|
||||
return os.Mkdir(target, 0755)
|
||||
return os.Mkdir(actualTarget, 0755)
|
||||
}
|
||||
// do the task if the target file doesn't exist or if the source file is newer
|
||||
other, err := os.Stat(target)
|
||||
other, err := os.Stat(actualTarget)
|
||||
if err != nil || info.ModTime().After(other.ModTime()) {
|
||||
tasks <- args{ source: path, target: target, info: info }
|
||||
tasks <- args{source: fp, target: actualTarget, info: info}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -123,27 +132,27 @@ func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
|
||||
// staticWatch counts the values coming out of the done channel. When the count matches the number of jobs started, we
|
||||
// know that all the tasks have been processed and the results channel is closed.
|
||||
func staticWatch(jobs int, results chan(error), done chan(bool)) {
|
||||
func staticWatch(jobs int, results chan (error), done chan (bool)) {
|
||||
for i := 0; i < jobs; i++ {
|
||||
<- done
|
||||
<-done
|
||||
}
|
||||
close(results)
|
||||
}
|
||||
|
||||
// staticWorker takes arguments off the tasks channel (the file to process) and put results in the results channel (any
|
||||
// errors encountered); when they're done they send true on the done channel.
|
||||
func staticWorker(tasks chan(args), results chan(error), done chan(bool)) {
|
||||
task, ok := <- tasks
|
||||
func staticWorker(tasks chan (args), results chan (error), done chan (bool)) {
|
||||
task, ok := <-tasks
|
||||
for ok {
|
||||
results <- staticFile(task.source, task.target, task.info)
|
||||
task, ok = <- tasks
|
||||
task, ok = <-tasks
|
||||
}
|
||||
done <- true
|
||||
}
|
||||
|
||||
// staticProgressIndicator watches the results channel and does a countdown. If the result channel reports an error,
|
||||
// that is put into the stop channel so that staticWalk stops adding to the tasks channel.
|
||||
func staticProgressIndicator(results chan(error), stop chan(error), quiet bool) (int, error) {
|
||||
func staticProgressIndicator(results chan (error), stop chan (error), quiet bool) (int, error) {
|
||||
n := 0
|
||||
t := time.Now()
|
||||
var err error
|
||||
@@ -154,7 +163,7 @@ func staticProgressIndicator(results chan(error), stop chan(error), quiet bool)
|
||||
stop <- err
|
||||
} else {
|
||||
n++
|
||||
if !quiet && n % 13 == 0 {
|
||||
if !quiet && n%13 == 0 {
|
||||
if time.Since(t) > time.Second {
|
||||
fmt.Printf("\r%d", n)
|
||||
t = time.Now()
|
||||
@@ -170,11 +179,11 @@ func staticProgressIndicator(results chan(error), stop chan(error), quiet bool)
|
||||
func staticFile(source, target string, info fs.FileInfo) error {
|
||||
// render pages
|
||||
if strings.HasSuffix(source, ".md") {
|
||||
p, err := staticPage(source[:len(source)-3], target[:len(target)-3] + ".html")
|
||||
p, err := staticPage(source[:len(source)-3], target[:len(target)-3]+".html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return staticFeed(source[:len(source)-3], target[:len(target)-3] + ".rss", p, info.ModTime())
|
||||
return staticFeed(source[:len(source)-3], target[:len(target)-3]+".rss", p, info.ModTime())
|
||||
}
|
||||
// remaining files are linked unless this is a template
|
||||
if slices.Contains(templateFiles, filepath.Base(source)) {
|
||||
@@ -201,7 +210,6 @@ func staticPage(source, target string) (*Page, error) {
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
maybeUnsafeHTML := markdown.Render(doc, renderer)
|
||||
p.Name = nameEscape(p.Name)
|
||||
p.Html = unsafeBytes(maybeUnsafeHTML)
|
||||
p.Hashtags = *hashtags
|
||||
return p, write(p, target, "", "static.html")
|
||||
@@ -211,7 +219,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 {
|
||||
@@ -247,22 +255,22 @@ func staticLinks(node ast.Node, entering bool) ast.WalkStatus {
|
||||
}
|
||||
|
||||
// write a page or feed with an appropriate template to a specific destination, overwriting it.
|
||||
func write(data any, path, prefix, templateFile string) error {
|
||||
file, err := os.Create(path)
|
||||
func write(data any, fp, prefix, templateFile string) error {
|
||||
file, err := os.Create(fp)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot create %s: %s\n", path, err)
|
||||
fmt.Fprintf(os.Stderr, "Cannot create %s: %s\n", fp, err)
|
||||
return err
|
||||
}
|
||||
_, err = file.Write([]byte(prefix))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot write prefix %s: %s\n", path, err)
|
||||
fmt.Fprintf(os.Stderr, "Cannot write prefix %s: %s\n", fp, err)
|
||||
return err
|
||||
}
|
||||
templates.RLock()
|
||||
defer templates.RUnlock()
|
||||
err = templates.template[templateFile].Execute(file, data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", templateFile, path, err)
|
||||
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", templateFile, fp, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func TestStaticCmd(t *testing.T) {
|
||||
cleanup(t, "testdata/static")
|
||||
s := staticCli("testdata/static", 2, true)
|
||||
s := staticCli(".", "testdata/static", 2, true)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
// pages
|
||||
assert.FileExists(t, "testdata/static/index.html")
|
||||
@@ -34,12 +34,8 @@ And the cars so loud
|
||||
`)}
|
||||
h.save()
|
||||
h.notify()
|
||||
wd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.Chdir("testdata/static-feed"))
|
||||
s := staticCli("../static-feed-out/", 2, true)
|
||||
s := staticCli("testdata/static-feed", "testdata/static-feed-out", 2, true)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
assert.NoError(t, os.Chdir(wd))
|
||||
assert.FileExists(t, "testdata/static-feed-out/2024-03-07-poem.html")
|
||||
assert.FileExists(t, "testdata/static-feed-out/Haiku.html")
|
||||
b, err := os.ReadFile("testdata/static-feed-out/Haiku.rss")
|
||||
|
||||
18
templates.go
18
templates.go
@@ -5,7 +5,6 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -15,17 +14,17 @@ 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.
|
||||
type templateStore struct {
|
||||
sync.RWMutex
|
||||
|
||||
// template is a map of parsed HTML templates. The key is their path name. By default, the map only contains
|
||||
// template is a map of parsed HTML templates. The key is their filepath name. By default, the map only contains
|
||||
// top-level templates like "view.html". Subdirectories may contain their own templates which override the
|
||||
// templates in the root directory. If so, they are paths like "dir/view.html", not filepaths. Use
|
||||
// filepath.ToSlash() if necessary.
|
||||
// templates in the root directory. If so, they are filepaths like "dir/view.html".
|
||||
template map[string]*template.Template
|
||||
}
|
||||
|
||||
@@ -57,8 +56,7 @@ func loadTemplate(fp string, info fs.FileInfo, err error) error {
|
||||
log.Println("Cannot parse template:", fp, err)
|
||||
// ignore error
|
||||
} else {
|
||||
// log.Println("Parse template:", path)
|
||||
templates.template[filepath.ToSlash(fp)] = t
|
||||
templates.template[fp] = t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -74,7 +72,7 @@ func updateTemplate(fp string) {
|
||||
} else {
|
||||
templates.Lock()
|
||||
defer templates.Unlock()
|
||||
templates.template[filepath.ToSlash(fp)] = t
|
||||
templates.template[fp] = t
|
||||
log.Println("Parse template:", fp)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +84,7 @@ func removeTemplate(fp string) {
|
||||
filepath.Dir(fp) != "." {
|
||||
templates.Lock()
|
||||
defer templates.Unlock()
|
||||
delete(templates.template, filepath.ToSlash(fp))
|
||||
delete(templates.template, fp)
|
||||
log.Println("Discard template:", fp)
|
||||
}
|
||||
}
|
||||
@@ -98,7 +96,7 @@ func renderTemplate(w http.ResponseWriter, dir, tmpl string, data any) {
|
||||
base := tmpl + ".html"
|
||||
templates.RLock()
|
||||
defer templates.RUnlock()
|
||||
t := templates.template[path.Join(dir, base)]
|
||||
t := templates.template[filepath.Join(dir, base)]
|
||||
if t == nil {
|
||||
t = templates.template[base]
|
||||
}
|
||||
|
||||
@@ -18,13 +18,12 @@ Memories of cold
|
||||
`)}
|
||||
p.save()
|
||||
assert.Contains(t,
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/templates/snow", nil),
|
||||
"Skip navigation")
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/templates/snow", nil), "Skip")
|
||||
// save a new view handler
|
||||
html := "<body><h1>{{.Title}}</h1>{{.Html}}"
|
||||
form := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(form)
|
||||
field, err := writer.CreateFormField("name")
|
||||
field, err := writer.CreateFormField("filename")
|
||||
assert.NoError(t, err)
|
||||
field.Write([]byte("view.html"))
|
||||
file, err := writer.CreateFormFile("file", "test.html")
|
||||
@@ -45,6 +44,5 @@ Memories of cold
|
||||
assert.NotContains(t, body, "Skip")
|
||||
// verify that the top level still uses the old template
|
||||
assert.Contains(t,
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index", nil),
|
||||
"Skip navigation")
|
||||
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index", nil), "Skip")
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user