118 Commits
v1.8 ... v1.13

Author SHA1 Message Date
Alex Schroeder
486c3f8620 Release 1.13 2024-08-24 14:16:03 +02:00
Alex Schroeder
5b0fcdd69f Update Makefiles
Help is more compact.
docs/Makefile comes with help.
Fixed make install.
Added clean target.
Use long options for tar and rm.
2024-08-24 11:52:19 +02:00
Alex Schroeder
bb99d05a0d Add more cross-compilation targets
- oddmu-darwin-amd64.tar.gz
- oddmu-windows-amd64.tar.gz
- oddmu-linux-arm64.tar.gz
2024-08-24 11:38:04 +02:00
Alex Schroeder
98358a008b Switched HEIC decoder again
This time I'm using github.com/gen2brain/heic. "Based on libheif and
libde265 compiled to WASM and used with wazero runtime (CGo-free)."
Amazing.
2024-08-23 17:37:54 +02:00
Alex Schroeder
51c8348ef7 Switched HEIC decoder fork
Moving from github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd
to github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786.

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

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

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

Add a test for language in the export feed.

Add a test for a JSON feed.

Both of these tests used to fail because the exportCli funtion did not
copy p.Body to it.Body so when the template calls .Languages the
result was always empty.
2024-08-16 18:27:34 +02:00
Alex Schroeder
e347a59603 Fix documentation of renderHtml
Page.Language hasn't been a member of the Page type for a long time.
2024-08-16 18:24:54 +02:00
Alex Schroeder
964dc3bf4a Ensure cleanup of testdata before running tests 2024-08-16 18:24:28 +02:00
Alex Schroeder
d5f8b280ac Support for JSON exports 2024-08-16 13:41:21 +02:00
Alex Schroeder
8ee5705ae7 Wording for man-pages
Remove "from the command-line" in a few places.
2024-08-16 12:33:11 +02:00
Alex Schroeder
43bf1574c9 Add export subcommand 2024-08-16 12:32:58 +02:00
Alex Schroeder
1c8af9fcdb missing subcommand no longer walks the file-system
Instead, rely on the code that loads the index. Delete the
existingPages function.
2024-08-16 11:23:34 +02:00
Alex Schroeder
f6fa76bd5f Update alexschroeder.ch theme 2024-08-16 11:13:31 +02:00
Alex Schroeder
111c617556 Ready to release 1.12 2024-08-15 14:52:58 +02:00
Alex Schroeder
66fe28062d Update oddmu-releases man page 2024-08-15 14:51:45 +02:00
Alex Schroeder
7e03b67267 Add "toc" subcommand 2024-08-15 14:49:39 +02:00
Alex Schroeder
11343067af links subcommand accepts "-" as input file 2024-08-15 14:46:37 +02:00
Alex Schroeder
a0ff3ed03c Fix the help synopsis
Start lowercase, no period.
2024-08-15 12:32:12 +02:00
Alex Schroeder
ccead37f44 Add links subcommand
The code for missing links was improved. The links function was fixed
so that it only joined page directory and link destination for
relative URLs.
2024-08-12 08:20:54 +02:00
Alex Schroeder
a8b4ec9acd Link to man pages on the web
Don't link to blobs in the repo.
2024-08-12 07:16:09 +02:00
Alex Schroeder
2531a469bf Update man pages, specially the HTML copy
Unlink URLs and links in double-quotes.
2024-07-31 16:43:35 +02:00
Alex Schroeder
51808bc1fb Hashtag ranking starts with 1, not 0 2024-07-31 16:17:21 +02:00
Alex Schroeder
2375dad845 Run go fmt 2024-07-31 11:40:57 +02:00
Alex Schroeder
0ca53690d8 Updated to the newest gomarkdown/markdown
This is necessary for toplevel SVG elements.
2024-07-31 11:39:50 +02:00
Alex Schroeder
a0c7517e8a Make test for filewatching more rebust
Wait for 10ms instead of just 1ms occasionally the tests would fail
because of this.
2024-07-31 11:37:43 +02:00
Alex Schroeder
912b6baad0 Do not use Chdir
Using os.Chdir in the tests confuses the test process. This means
rewriting staticCli so that it accepts an input directory. When called
from the command-line, that input directory is always the current
working directory. For the tests, however, that is not necessarily
true.
2024-07-31 11:37:18 +02:00
Alex Schroeder
b6c068c72f Update oddmu-releases man page 2024-07-31 11:30:15 +02:00
Alex Schroeder
89ef292736 New command: hashtags 2024-07-31 10:25:58 +02:00
Alex Schroeder
c658de5a6f Add searching for multi-word phrases 2024-07-30 14:02:16 +02:00
Alex Schroeder
4bab25e2ac Explain how to run a private wiki on port 80 2024-07-24 15:22:18 +02:00
Alex Schroeder
c518a193d0 Create new image list for every page
With out this, any page listed after one with images continues to show
the same images.
2024-07-23 13:13:29 +02:00
Alex Schroeder
2dc950cb5e Add loading="lazy" for images in search.html 2024-07-21 16:59:25 +02:00
Alex Schroeder
87d1e72f0f Remove unnecessary "last" class for search.html 2024-07-21 16:55:29 +02:00
Alex Schroeder
44213e1d43 Add the ability to search for image alt text
Page no longer has Score.

Search contains an array of Result.

Result is like Page plus Score and an array of image data.

Image data is collected during startup just as page titles are.

The search.html template has a section listing files with matching
alt-text.
2024-07-21 16:46:21 +02:00
Alex Schroeder
ae9698aae3 Rename internal variable from buff to buf 2024-07-21 14:11:40 +02:00
Alex Schroeder
24871eee99 Index image data 2024-07-21 12:43:34 +02:00
Alex Schroeder
5f44853bab The tokenizer respects backslashes
That is, #tag is a hashtag, \#tag is not.
2024-07-21 12:43:34 +02:00
Alex Schroeder
f0a3d2c5a0 List styling for chat theme 2024-07-21 12:24:54 +02:00
Alex Schroeder
b8f916b7c9 Add new wiki to upload target 2024-07-21 12:24:18 +02:00
Alex Schroeder
db8a060d65 Fix SmartypantsFractions bit code 2024-05-12 14:12:27 +02:00
Alex Schroeder
9d216f37ee Remove "smart fractions" from the HTML renderer 2024-05-12 14:02:55 +02:00
Alex Schroeder
39c2fe6dfd The nginx man page talks about Unix-domain sockets, too 2024-05-11 14:43:57 +02:00
Alex Schroeder
3151fe63fa Prevent hashtags for just the hash 2024-05-11 14:38:04 +02:00
Alex Schroeder
abd3ceae2e Add preview.go to the README
This fixes a failing test.
2024-05-11 14:25:37 +02:00
Alex Schroeder
edad64e76c Document preview.html 2024-05-09 21:26:07 +02:00
Alex Schroeder
71315bc662 Split previewHandler from view.go to preview.go 2024-05-09 21:13:05 +02:00
Alex Schroeder
27509bcdd4 Ready for next release 2024-05-09 16:34:14 +02:00
Alex Schroeder
04e8cb3ee8 Prepare for release 2024-05-09 16:31:02 +02:00
Alex Schroeder
2be4fe503d Add preview action 2024-05-09 16:27:45 +02:00
Alex Schroeder
7a405b22b8 Fix markup in nginx man page 2024-05-09 10:40:38 +02:00
Alex Schroeder
3f6fce7165 Switch MemoryHigh and MemoryMax 2024-05-09 10:40:05 +02:00
Alex Schroeder
721d5907d8 wikiLink doesn't need the parser argument 2024-05-09 10:39:41 +02:00
Alex Schroeder
cb0dbedaed Update the chat theme
Support blockquotes as coming from other people.
2024-05-09 10:39:05 +02:00
Alex Schroeder
3ba967781e Add Unix domain socket setup for nginx 2024-04-21 12:43:29 +02:00
Alex Schroeder
e985707b51 Replace ' with ’ in the upload template 2024-04-21 12:42:22 +02:00
Alex Schroeder
a5e7dca7d8 Fixed tests for the "actual" parameter 2024-04-02 14:08:58 +02:00
Alex Schroeder
74387910d8 Append links to file uploads, directly
This adds two new functions for the upload template, Base and Title.
The upload template uses these to offer to append files just uploaded
to the suspected page they belong to.
2024-04-02 13:58:22 +02:00
Alex Schroeder
121408d6d9 Report all the links of uploaded images 2024-03-24 22:37:10 +01:00
Alex Schroeder
1b7419466a Fix regexp for file uploads
Without this fix, only the last digit gets incremented and the
filenames used is 1 .. 9, 10, 11 .. 19, 110, 111 .. 119, 1110, …
2024-03-24 22:11:38 +01:00
Alex Schroeder
8a513746d5 Links to parent pages 2024-03-17 14:17:33 +01:00
Alex Schroeder
e736802da5 Homepage theme uses the new template features
Use {{.Language}} for add.html and edit.html so that the edit area has
the language of the original text, if any.

Use {{.IsBlog}} to only add the date to an empty textarea for add.html
if the original page is a blog page.
2024-03-15 09:43:50 +01:00
Alex Schroeder
380692d616 Replace the .Language property with p.Language()
This computes it on demand for the templates and it is not used
anywhere else.

Also use it for the add.html and edit.html templates.
2024-03-15 00:28:40 +01:00
Alex Schroeder
7ad04e561c Rename p.isBlog to p.IsBlog
This makes it usable in templates.
2024-03-15 00:21:23 +01:00
Alex Schroeder
0e0c9f3bb5 Use oddmu instead of groff for HTML output 2024-03-13 14:57:50 +01:00
Alex Schroeder
7d4530383e Don't require an empty target dir for static site 2024-03-12 23:46:43 +01:00
Alex Schroeder
df9439c356 Enable AutoHeadingIDs 2024-03-10 23:12:33 +01:00
Alex Schroeder
b4ee637600 Update the upload template for templates 2024-03-10 19:36:30 +01:00
Alex Schroeder
2cd5a38885 Revert id change for the upload form 2024-03-10 19:09:02 +01:00
Alex Schroeder
149f4cf7a4 Add drop to the upload template 2024-03-10 18:34:10 +01:00
Alex Schroeder
d26e3479c9 Add paste to the upload template 2024-03-10 14:04:03 +01:00
Alex Schroeder
2e94abfabb Default upload template shows Markdown link 2024-03-10 10:59:37 +01:00
Alex Schroeder
9116095f69 File upload tries to use non-existing files 2024-03-09 19:39:58 +01:00
Alex Schroeder
a82bbba62b Add note on the AGPL to the README 2024-03-08 23:08:59 +01:00
Alex Schroeder
3066574167 Add the Oddmu mascot 2024-03-08 18:26:35 +01:00
Alex Schroeder
e065888279 Switch from lipgloss to wordwrap 2024-03-08 18:26:07 +01:00
Alex Schroeder
4e81401b8c Update themes 2024-03-08 12:42:31 +01:00
Alex Schroeder
acac745e1f Rewrite staticCmd to use a lot of go routines
Starting a go routine that walks the directory tree, adding files to a
tasks channel and then closing it; starting a bunch of worker go
routines that take files from the tasks channel until there is nothing
left to do; putting errors on a results channel and when they’re done,
putting a true value on a done channel; a watcher go routine that
checks the done channel and if every worker is done, close the results
channel; and the main program goes through all the values on the
results channel and sets up some short-circuiting in the case of
errors and otherwise it prints the counter. What a glorious spaghetti
mess of code!
2024-03-08 12:42:08 +01:00
Alex Schroeder
3f542388db Warn of existing files instead of overwriting them 2024-03-08 00:34:44 +01:00
Alex Schroeder
8a275c103a Fix static cmd setup 2024-03-07 15:25:22 +01:00
Alex Schroeder
2934471ed3 Don't link template files 2024-03-07 15:12:44 +01:00
Alex Schroeder
86cd3baa89 Document that webp images are decoded 2024-03-07 15:01:34 +01:00
Alex Schroeder
95b0c6951d Static subcommand does not write empty feeds
Also improve the oddmu-static(1) man page and note name-clash
problems.
2024-03-07 09:13:18 +01:00
Alex Schroeder
f935237fcb Fix static feed generation 2024-03-06 19:33:56 +01:00
Alex Schroeder
50d603781e Improve the scdoc → Markdown 2024-03-06 19:18:18 +01:00
Alex Schroeder
0a37ac34d5 Fix encoding errors when writing static .rss files
Using p.Name as a filepath is not cool because the name comes percent
escaped.
2024-03-06 14:39:37 +01:00
Alex Schroeder
14f9f783eb Static command writes RSS files, too
For every index and for every potential hashtag file, the feed is
written as well.
2024-03-06 08:41:22 +01:00
Alex Schroeder
def0534771 Notify command uses Writer
The output of notify doesn't need to go to Stderr.
2024-03-06 08:40:44 +01:00
Alex Schroeder
8c70bd5c7b go fmt 2024-03-05 09:46:00 +01:00
Alex Schroeder
81f2dc3ace Fix handling of id query parameter
With the id query parameter, the viewHandler actually doesn't require
a page. Therefore, all the calls to makeHandler(viewHandler, true)
were changed to makeHandler(viewHandler, false). The code of
makeHandler was rewritten so that r.ParseForm only called for GET
requests such as not to confuse dropHandler.
2024-03-05 09:44:58 +01:00
Alex Schroeder
07aa4f0064 clean target removes man/README.md
This is now also a generated file.
2024-03-05 08:38:19 +01:00
Alex Schroeder
926c4faf06 Don't print imports in man test 2024-03-05 08:38:01 +01:00
Alex Schroeder
4636b10cce Remove id attribute from main element
This isn't used in the CSS and I'm trying to make the HTML and CSS as
simple as possible.
2024-03-05 08:11:39 +01:00
Alex Schroeder
114e2b8790 Document how to list space requirements 2024-03-04 15:54:09 +01:00
Alex Schroeder
1fa26ab6f9 Rewrite oddmu-templates man page 2024-03-04 09:22:27 +01:00
Alex Schroeder
69d2f452d1 Upload a modified README to the documentation site 2024-03-03 09:40:09 +01:00
Alex Schroeder
c3b3bbb1c4 Add license info to the list of non-standard licenses 2024-03-02 22:29:54 +01:00
Alex Schroeder
07d395ab1e Updated year for the license 2024-03-02 22:15:35 +01:00
Alex Schroeder
6ed4e17767 Discuss all the external libraries in the README
Add a test to make sure that all the libraries used directly are
mentioned in the README.
2024-03-02 22:14:03 +01:00
Alex Schroeder
10bbda4a6e Whitespace 2024-03-02 20:29:33 +01:00
Alex Schroeder
006b8211f4 Fix version command test 2024-03-02 20:29:24 +01:00
Alex Schroeder
1885fdaaad More work on the chat theme 2024-03-02 14:26:34 +01:00
Alex Schroeder
cd6fb187b4 Add more emoji 2024-03-02 13:19:27 +01:00
Alex Schroeder
4f5b49a065 Changes to the upload template
And the "chat" theme that makes use of this change.
2024-03-02 12:13:26 +01:00
Alex Schroeder
7fbdac3f9a Add a target for a binary package
The "install" target no longer depends on build and docs so that it
can run from a binary-only installation. In that case, there are no Go
source files and there's no need to recreate the man pages.
2024-03-01 22:56:57 +01:00
Alex Schroeder
38f13dc8f8 Mention problems cross-compiling the HEIC library 2024-03-01 22:55:26 +01:00
Alex Schroeder
e3ceaf031e The archive link in the campaign wiki theme 2024-03-01 21:23:55 +01:00
Alex Schroeder
5feb5f9b21 Add upload target for themes
Add --itemize-changes to the download target, too.
2024-03-01 15:10:09 +01:00
Alex Schroeder
086e65c304 Add themes subdirectory 2024-03-01 13:23:15 +01:00
Alex Schroeder
ffaae10f94 Serve files with a MIME-type, if possible
This adds a new dependency: https://github.com/gabriel-vasile/mimetype
2024-03-01 12:55:08 +01:00
Alex Schroeder
6c6d326c59 Add handling of relative links to missing command
Links such as [up](../index) are now handled correctly by joining it
to the directory of the page containing this link. A page foo/bar with
such a link no longer registers as linking to a missing page if the
index page exists. Note that [up](..) still registers as a link to a
missing page because the code does not take redirects from directories
to index pages into account.
2024-03-01 12:52:52 +01:00
Alex Schroeder
f41d6b8e9e Rework the missing pages command
Move finding of existing pages into a separate function.
2024-03-01 12:52:32 +01:00
146 changed files with 5020 additions and 750 deletions

View File

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

View File

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

261
README.md
View File

@@ -1,34 +1,38 @@
# Oddµ: A minimal wiki
This program helps you run a minimal wiki. There is no version
history. It's well suited as a *secondary* medium: collaboration and
conversation happens elsewhere, in chat, on social media. The wiki
serves as the text repository that results from these discussions.
This program helps you run a minimal wiki, blog, digital garden, memex
or Zettelkasten. There is no version history.
If you're the only user and it just runs on your laptop, then you can
think of it as a [memex](https://en.wikipedia.org/wiki/Memex), a
memory extender.
It's well suited as a self-hosted, single-user web application, when
there is no need for collaboration on the site itself. Links and email
connect you to the rest of the net. The wiki can be public or private.
Perhaps it just runs on your local machine, unreachable from the
Internet.
Oddµ can be used as a web server behind a reverse proxy such as Apache
or it can be used as a static site generator.
It's well suited as a secondary medium for a close-knit group:
collaboration and conversation happens elsewhere, in chat, on social
media. The wiki serves as the text repository that results from these
discussions. As there are no logins and no version histories, it is
not possible to undo vandalism and spam. Only allow people you trust
write-access to the site.
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
(ending in `.md`) as web pages and allows you to edit them.
(ending in `.md`) as web pages. These pages can be edited via the web.
If your files don't provide their own title (`# title`), the file name
(without `.md`) is used for the title. Subdirectories are created as
Oddmu adds the following extensions to Markdown: local links `[[like
this]]`, hashtags `#Like_This` and fediverse account links like
`@alex@alexschroeder.ch`.
If your pages don't provide their own title (`# title`), the file name
(without `.md`) is used as the title. Subdirectories are created as
necessary.
Oddµ uses a [Markdown library](https://github.com/gomarkdown/markdown)
to generate the web pages from Markdown. Oddmu adds the following
extensions: local links `[[like this]]`, hashtags `#Like_This` and
fediverse account links like `@alex@alexschroeder.ch`.
The [lingua](https://github.com/pemistahl/lingua-go) library detects
languages in order to get hyphenation right.
The standard [html/template](https://pkg.go.dev/html/template) library
is used to generate HTML.
Other files can be uploaded and images (ending in `.jpg`, `.jpeg`,
`.png`, `.heic` or `.webp`) can be resized when they are uploaded
(resulting in `.jpg` or `.png` files).
## Documentation
@@ -36,84 +40,111 @@ 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.”
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:
@@ -132,6 +163,13 @@ make docs
The `Makefile` in the `man` directory has targets to create Markdown
and HTML files.
The HEIC library uses C code and prevents cross-compilation.
As the repository changed URLs a few times (from GitHub, to
self-hosted using `cgit` to self-hosted using `legit`), there is no
way to install it using `go install`. You need to `git clone` the
repository and build it locally.
## Running
The working directory is where pages are saved and where templates are
@@ -178,10 +216,6 @@ cd /usr/local/stow
sudo stow oddmu
```
## Bugs
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.
## Hacking
If you're interested in making changes to the code, here's a
@@ -206,6 +240,7 @@ high-level introduction to the various source files.
- `languages.go` implements the language detection
- `page.go` implements the page loading and saving
- `parser.go` implements the Markdown parsing
- `preview.go` implements the `/preview` handler
- `score.go` implements the page scoring when showing search results
- `search.go` implements the `/search` handler
- `snippets.go` implements the page summaries for search results
@@ -216,6 +251,18 @@ high-level introduction to the various source files.
- `watch.go` implements the filesystem notification watch
- `wiki.go` implements the main function
The code of this package is licensed to you under the
AGPL-3.0-or-later license. If you do make changes and your site is
public, be aware of section 13:
> … if you modify the Program, your modified version must prominently
> offer all users interacting with it remotely through a computer
> network (if your version supports such interaction) an opportunity
> to receive the Corresponding Source of your version by providing
> access to the Corresponding Source from a network server at no
> charge, through some standard or customary means of facilitating
> copying of software.
### Changing the markup rules
If you want to change the markup rules, your starting point should be
@@ -260,6 +307,66 @@ 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.
## Dependencies
This section lists the non-standard libraries Oddmu uses and their
respective licenses.
[github.com/gomarkdown/markdown](https://github.com/gomarkdown/markdown)
is used to generate the web pages from Markdown. BSD-2-Clause.
[github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday)
is used to strip rendered search results of all HTML except for the
bold tag. Regular HTML generated from pages is *not* sanitized. Don't
give people you don't trust access to your wiki. BSD-3-Clause.
[github.com/pemistahl/lingua-go](https://github.com/pemistahl/lingua-go)
detects languages in order to set the language tag in templates. This
in turn can be used by browsers to get hyphenation right. Apache-2.0.
[github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype)
is used to sniff the MIME type of files with unknown filename
extensions. MIT.
[github.com/gen2brain/heic](https://github.com/gen2brain/heic) is
used to decode HEIC files (the new default file format for photos on
iPhones). LGPL-3.0-only.
[github.com/disintegration/imaging](https://github.com/disintegration/imaging)
is used to resize images. MIT.
[github.com/edwvee/exiffix](https://github.com/edwvee/exiffix) is used
to rotate images before resizing them if the EXIF data says the image
wasn't taken with the default orientation of the camera. This is
necessary because after resizing, the EXIF data is gone. MIT.
[github.com/google/subcommands](https://github.com/google/subcommands)
is used for the parsing and documenting of subcommands. Apache-2.0.
[github.com/muesli/reflow/wordwrap](https://github.com/muesli/reflow/wordwrap)
is used to wrap the search subcommand output. MIT.
[github.com/hexops/gotextdiff](https://github.com/hexops/gotextdiff)
is used to show a compact unified diff on the command line before
doing any replacements. BSD-3-Clause.
[github.com/sergi/go-diff/diffmatchpatch](https://github.com/sergi/go-diff/diffmatchpatch)
is used to show the page diffs on the web. MIT.
[github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)
is used to watch the filesystem for changes. BSD-3-Clause.
[golang.org/x/exp/constraints](https://golang.org/x/exp/constraints)
for the computation of the intersection between two sets of pages.
BSD-3-Clause.
[github.com/stretchr/testify/assert](https://github.com/stretchr/testify/assert)
is used for testing. MIT.
## Bugs
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.
## References
[Writing Web Applications](https://golang.org/doc/articles/wiki/)

View File

@@ -13,7 +13,7 @@ form, textarea { width: 100%; }
<body>
<h1>Adding to {{.Title}}</h1>
<form action="/append/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="" autofocus required></textarea>
<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>

View File

@@ -34,7 +34,7 @@ It's not `)}
data.Set("body", "barbecue")
assert.Regexp(t, regexp.MustCompile("a distant fire"),
assert.HTTPBody(makeHandler(viewHandler, true),
assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/add/fire", nil))
assert.NotRegexp(t, regexp.MustCompile("a distant fire"),
assert.HTTPBody(makeHandler(addHandler, true),
@@ -42,7 +42,7 @@ It's not `)}
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/add/fire", data, "/view/testdata/add/fire")
assert.Regexp(t, regexp.MustCompile(`not</p>\s*<p>barbecue`),
assert.HTTPBody(makeHandler(viewHandler, true),
assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/add/fire", nil))
}

View File

@@ -29,7 +29,7 @@ func (p *Page) notify() error {
log.Printf("Updating changes in %s failed: %s", dir, err)
return err
}
if p.isBlog() {
if p.IsBlog() {
// Add to the index only if the blog post is for the current year
if strings.HasPrefix(path.Base(p.Name), time.Now().Format("2006")) {
err := addLink(path.Join(dir, "index"), true, link, re)

22
diff.go
View File

@@ -46,23 +46,23 @@ func (p *Page) Diff() template.HTML {
}
func diff2html(diffs []diffmatchpatch.Diff) string {
var buff bytes.Buffer
var buf bytes.Buffer
for _, item := range diffs {
text := strings.ReplaceAll(html.EscapeString(item.Text), "\n", "<br>")
switch item.Type {
case diffmatchpatch.DiffInsert:
_, _ = buff.WriteString("<ins>")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</ins>")
_, _ = buf.WriteString("<ins>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</ins>")
case diffmatchpatch.DiffDelete:
_, _ = buff.WriteString("<del>")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</del>")
_, _ = buf.WriteString("<del>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</del>")
case diffmatchpatch.DiffEqual:
_, _ = buff.WriteString("<span>")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</span>")
_, _ = buf.WriteString("<span>")
_, _ = buf.WriteString(text)
_, _ = buf.WriteString("</span>")
}
}
return buff.String()
return buf.String()
}

View File

@@ -15,9 +15,10 @@ form, textarea { width: 100%; }
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="# Title
Text" lang="" autofocus>{{printf "%s" .Body}}</textarea>
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>
</form>
</body>

View File

@@ -2,6 +2,7 @@ package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"net/url"
"os"
"testing"
@@ -15,7 +16,7 @@ func TestEditSave(t *testing.T) {
data.Set("body", "Hallo!")
// View of the non-existing page redirects to the edit page
HTTPRedirectTo(t, makeHandler(viewHandler, true),
HTTPRedirectTo(t, makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil, "/edit/testdata/save/alex")
// Edit page can be fetched
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
@@ -24,7 +25,7 @@ func TestEditSave(t *testing.T) {
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/save/alex", data, "/view/testdata/save/alex")
// Page now contains the text
assert.Contains(t, assert.HTTPBody(makeHandler(viewHandler, true),
assert.Contains(t, assert.HTTPBody(makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil),
"Hallo!")
// Delete the page and you're sent to the empty page
@@ -32,7 +33,7 @@ func TestEditSave(t *testing.T) {
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/save/alex", data, "/view/testdata/save/alex")
// Viewing the non-existing page redirects to the edit page (like in the beginning)
HTTPRedirectTo(t, makeHandler(viewHandler, true),
HTTPRedirectTo(t, makeHandler(viewHandler, false),
"GET", "/view/testdata/save/alex", nil, "/edit/testdata/save/alex")
}
@@ -70,7 +71,17 @@ func TestEditSaveChanges(t *testing.T) {
// </form>
func TestEditId(t *testing.T) {
cleanup(t, "testdata/id")
data := url.Values{}
data.Set("id", "testdata/id/alex")
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
"GET", "/edit/", data, http.StatusBadRequest,
"No slashes in id")
data.Set("id", ".alex")
assert.HTTPStatusCode(t, makeHandler(editHandler, true),
"GET", "/edit/", data, http.StatusForbidden,
"No hidden files")
data.Set("id", "alex")
assert.Contains(t, assert.HTTPBody(makeHandler(editHandler, true),
"GET", "/edit/?id=testdata/id/alex", nil),
"GET", "/edit/testdata/id/", data),
"Editing testdata/id/alex")
}

112
export_cmd.go Normal file
View File

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

55
export_cmd_test.go Normal file
View File

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

View File

@@ -13,10 +13,10 @@ import (
// Item is a Page plus a Date.
type Item struct {
// Page is the page being used as the feed item.
Page
// Date is the last modification date of the file storing the page. As the pages used by Oddmu are plain
// Markdown files, they don't contain any metadata. Instead, the last modification date of the file is used.
// This makes it work well with changes made to the files outside of Oddmu.

View File

@@ -8,13 +8,13 @@ import (
func TestFeed(t *testing.T) {
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/index.rss", nil),
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index.rss", nil),
"Welcome to Oddµ")
}
func TestNoFeed(t *testing.T) {
assert.HTTPStatusCode(t,
makeHandler(viewHandler, true), "GET", "/view/no-feed.rss", nil, http.StatusNotFound)
makeHandler(viewHandler, false), "GET", "/view/no-feed.rss", nil, http.StatusNotFound)
}
func TestFeedItems(t *testing.T) {
@@ -44,12 +44,12 @@ Writing poems about plants.
* [My Dragon Tree](dragon)`)}
p3.save()
body := assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/feed/plants.rss", nil)
body := assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/feed/plants.rss", nil)
assert.Contains(t, body, "<title>Plants</title>")
assert.Contains(t, body, "<title>Cactus</title>")
assert.Contains(t, body, "<title>Dragon</title>")
assert.Contains(t, body, "&lt;h1&gt;Cactus&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1&gt;Dragon&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1 id=&#34;cactus&#34;&gt;Cactus&lt;/h1&gt;")
assert.Contains(t, body, "&lt;h1 id=&#34;dragon&#34;&gt;Dragon&lt;/h1&gt;")
assert.Contains(t, body, "<category>Succulent</category>")
assert.Contains(t, body, "<category>Palmtree</category>")
}

20
go.mod
View File

@@ -1,17 +1,19 @@
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/charmbracelet/lipgloss v0.9.1
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/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6
github.com/google/subcommands v1.2.0
github.com/hexops/gotextdiff v1.0.3
github.com/microcosm-cc/bluemonday v1.0.26
github.com/muesli/reflow v0.3.0
github.com/pemistahl/lingua-go v1.4.0
github.com/sergi/go-diff v1.3.1
github.com/stretchr/testify v1.8.4
@@ -19,22 +21,20 @@ require (
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.7.1 // indirect
github.com/gen2brain/heic v0.3.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/tetratelabs/wazero v1.7.3 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.21.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

29
go.sum
View File

@@ -1,22 +1,24 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
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/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gen2brain/heic v0.0.0-20230113233934-ca402e77a786 h1:zvgtcRb2B5gynWjm+Fc9oJZPHXwmcgyH0xCcNm6Rmo4=
github.com/gen2brain/heic v0.0.0-20230113233934-ca402e77a786/go.mod h1:aKVJoQ0cc9K5Xb058XSnnAxXLliR97qbSqWBlm5ca1E=
github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs=
github.com/gen2brain/heic v0.3.1/go.mod h1:m2sVIf02O7wfO8mJm+PvE91lnq4QYJy2hseUon7So10=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 h1:ZPy+2XJ8u0bB3sNFi+I72gMEMS7MTg7aZCCXPOjV8iw=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/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=
@@ -30,10 +32,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -41,8 +39,6 @@ github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3r
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -61,6 +57,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -68,9 +66,10 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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
View 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
}

16
hashtags_cmd_test.go Normal file
View File

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

View File

@@ -4,10 +4,8 @@ import (
"regexp"
)
// highlight splits the query string q into terms and highlights them
// using the bold tag. Return the highlighted string.
// This assumes that q already has all its meta characters quoted.
func highlight(q string, re *regexp.Regexp, s string) string {
// highlight matches for the regular expression using the bold tag.
func highlight(re *regexp.Regexp, s string) string {
s = re.ReplaceAllString(s, "<b>$1</b>")
return s
}

View File

@@ -16,7 +16,7 @@ No birds to be heard.`
q := "window"
re, _ := re(q)
r := highlight(q, re, s)
r := highlight(re, s)
if r != h {
t.Logf("The highlighting is wrong in 「%s」", r)
t.Fail()
@@ -35,7 +35,7 @@ I hear the fountain`
q := "shout out"
re, _ := re(q)
r := highlight(q, re, s)
r := highlight(re, s)
if r != h {
t.Logf("The highlighting is wrong in 「%s」", r)
t.Fail()

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ package main
import (
"golang.org/x/exp/constraints"
"html/template"
"io/fs"
"log"
"path/filepath"
@@ -16,6 +17,15 @@ import (
type docid uint
// ImageData holds the data used to search for images using the alt-text. Title is the alt-text; Name is the complete
// URL including path (which is important since the image link itself only has the URL relative to the page in which it
// is found; and Html is a copy of the Title with highlighting of a term as applied when searching. This is temporary.
// It depends on the fact that Title is always plain text.
type ImageData struct {
Title, Name string
Html template.HTML
}
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
type indexStore struct {
sync.RWMutex
@@ -31,6 +41,9 @@ type indexStore struct {
// titles is a map, mapping page names to titles.
titles map[string]string
// images is a map, mapping pages names to alt text to an array of image data.
images map[string][]ImageData
}
var index indexStore
@@ -45,6 +58,7 @@ func (idx *indexStore) reset() {
idx.token = make(map[string][]docid)
idx.documents = make(map[docid]string)
idx.titles = make(map[string]string)
idx.images = make(map[string][]ImageData)
}
// addDocument adds the text as a new document. This assumes that the index is locked!
@@ -102,6 +116,7 @@ func (idx *indexStore) deletePageName(name string) {
delete(idx.documents, id)
}
delete(idx.titles, name)
delete(idx.images, name)
}
// remove the page from the index. Do this when deleting a page. This assumes that the index is unlocked.
@@ -153,6 +168,7 @@ func (idx *indexStore) addPage(p *Page) {
idx.documents[id] = p.Name
p.handleTitle(false)
idx.titles[p.Name] = p.Title
idx.images[p.Name] = p.images()
}
// add a page to the index. This assumes that the index is unlocked.

View File

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

View File

@@ -56,3 +56,9 @@ func language(s string) string {
}
return ""
}
// Language returns the language used for the page, as a lower case
// ISO 639-1 string, e.g. "en" or "de".
func (p *Page) Language() string {
return language(p.plainText())
}

56
links_cmd.go Normal file
View File

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

16
links_cmd_test.go Normal file
View 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"})
assert.Equal(t, subcommands.ExitSuccess, s)
x := b.String()
assert.Contains(t, x, "https://alexschroeder.ch/view/oddmu/oddmu.1\n")
}

View File

@@ -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.

View File

@@ -3,6 +3,20 @@ MAN=$(patsubst %.txt,%,${TEXT})
HTML=$(patsubst %.txt,%.html,${TEXT})
MD=$(patsubst %.txt,%.md,${TEXT})
help:
@echo Help for Oddmu Documentation
@echo ============================
@echo make man
@echo " regenerate man pages"
@echo make html
@echo " generate HTML pages"
@echo make md
@echo " generate Markdown pages"
@echo make clean
@echo " delete HTML and Markdown pages"
@echo make realclean
@echo " delete HTML, Markdown and man pages"
man: ${MAN}
%: %.txt
@@ -10,26 +24,32 @@ man: ${MAN}
html: ${HTML}
%.html: %
groff -mandoc -Dutf8 -Thtml $< | sed 's/<style type="text\/css">/<style type="text\/css">\n body {font-family: mono; max-width: 80ch }/' > $@
%.html: %.md
@echo Making $@
@echo '<!DOCTYPE html>' > $@
@oddmu html $(basename $<) | sed --regexp-extended \
-e 's/<a href="(oddmu[a-z.-]*.[1-9])">([^<>]*)<\/a>/<a href="\1.html">\2<\/a>/g' >> $@
md: ${MD}
%.md: %.txt
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/^([A-Z.-]*\([1-9]\))( ".*")?$$/# \1/' \
@echo Making $@
@perl scdoc-to-markdown < $< > $@
README.md: ../README.md
@echo Making $@
@sed --regexp-extended \
-e 's/\]\(.*\/(.*)\.txt\)/](\1)/' \
< $< > $@
upload: ${MD}
upload: ${MD} README.md
rsync --itemize-changes --archive *.md sibirocobombus:alexschroeder.ch/wiki/oddmu/
make clean
clean:
rm --force ${HTML} ${MD}
@echo Removing HTML and Markdown files
@rm --force ${HTML} ${MD} README.md
realclean: clean
rm --force ${MAN}
@echo Removing man pages
@rm --force ${MAN}

View File

@@ -5,13 +5,13 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2024-02-19"
.TH "ODDMU-APACHE" "5" "2024-05-09"
.PP
.SH NAME
.PP
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
.PP
.SS DESCRIPTION
.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.\&
@@ -22,7 +22,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy.\& This page explains how to setup Apache on Debian to
do this.\&
.PP
.SS CONFIGURATION
.SH CONFIGURATION
.PP
HTTPS is not part of Oddmu.\& You probably want to configure this in your
webserver.\& I guess you could use stunnel, too.\& If you'\&re using Apache, you can
@@ -48,7 +48,7 @@ ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
@@ -132,7 +132,7 @@ ServerAdmin alex@alexschroeder\&.ch
<VirtualHost *:443>
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
@@ -144,15 +144,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket.\& This requires socket activation.\& An example of configuring
the service is given in \fIoddmu.\&service(5)\fR.\&
.PP
To test just the unix domain socket, use \fIncat(1)\fR:
.PP
.nf
.RS 4
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
| ncat --unixsock /run/oddmu/oddmu\&.sock
.fi
.RE
.PP
On the Apache side, you can proxy to the socket directly.\& This sends all
requests to the socket:
.PP
@@ -179,7 +170,7 @@ In that case, you need to use the ProxyPassMatch directive.\&
.PP
.nf
.RS 4
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))?$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
@@ -198,7 +189,7 @@ A workaround is to add the redirect manually and drop the question-mark:
.nf
.RS 4
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))$"
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(\&.*))$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
@@ -283,7 +274,7 @@ directory:
.PP
.nf
.RS 4
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|preview|search|archive)/secret)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/\&.htpasswd

View File

@@ -4,7 +4,7 @@ ODDMU-APACHE(5)
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
## DESCRIPTION
# DESCRIPTION
The oddmu program serves the current working directory as a wiki on port 8080.
This is an unpriviledged port so an ordinary user account can do this.
@@ -15,7 +15,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy. This page explains how to setup Apache on Debian to
do this.
## CONFIGURATION
# CONFIGURATION
HTTPS is not part of Oddmu. You probably want to configure this in your
webserver. I guess you could use stunnel, too. If you're using Apache, you can
@@ -40,7 +40,7 @@ ServerAdmin alex@alexschroeder.ch
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
@@ -112,7 +112,7 @@ ServerAdmin alex@alexschroeder.ch
<VirtualHost *:443>
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
@@ -123,13 +123,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket. This requires socket activation. An example of configuring
the service is given in _oddmu.service(5)_.
To test just the unix domain socket, use _ncat(1)_:
```
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/oddmu/oddmu.sock
```
On the Apache side, you can proxy to the socket directly. This sends all
requests to the socket:
@@ -151,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|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))?$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
@@ -166,7 +159,7 @@ A workaround is to add the redirect manually and drop the question-mark:
```
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search|archive)/(.*))$" \
ProxyPassMatch "^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive)/(.*))$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
@@ -241,7 +234,7 @@ You need to configure the web server to prevent access to the "secret/"
directory:
```
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|search|archive)/secret)/">
<LocationMatch "^/(edit|save|add|append|upload|drop|(view|preview|search|archive)/secret)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd

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

@@ -0,0 +1,79 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-EXPORT" "1" "2024-08-16"
.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 EXAMPLE
.PP
Export all the pages into a big XML file:
.PP
.nf
.RS 4
env ODDMU_LANGUAGES=de,en oddmu export > /tmp/export\&.xml
.fi
.RE
.PP
Alternatively, consider a template file like the following, to generate a JSON
feed.\& The rule to disallow a comma at the end of arrays means that we need to
add an empty tag and an empty item, unfortunately:
.PP
.nf
.RS 4
{
"version": "https://jsonfeed\&.org/version/1\&.1",
"title": "{{\&.Title}}",
"home_page_url": "https://alexschroeder\&.ch",
"others": [],
"items": [{{range \&.Items}}
{
"id": "{{\&.Name}}",
"url": "https://alexschroeder\&.ch/view/{{\&.Name}}",
"title": "{{\&.Title}}",
"content_html": "{{\&.Html}}",
"date_modified": "{{\&.Date}}",
"tags": [{{range \&.Hashtags}}"{{\&.}}",{{end}}""],
"language": "{{\&.Language}}"
},{{end}}
{}
]
}
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-templates\fR(5), \fIoddmu-static\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

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

@@ -0,0 +1,68 @@
ODDMU-EXPORT(1)
# NAME
oddmu-export - export all pages into one file
# SYNOPSIS
*oddmu export* [*-template* _filename_]
# DESCRIPTION
The "export" subcommand prints a RSS file containing all the pages to stdout.
You probably want to redirect this into a file so that you can upload and import
it somewhere.
Note that this only handles pages (Markdown files). All other files (images,
PDFs, whatever else you uploaded) are not part of the feed and has to be
uploaded to the new platform in some other way.
The *-template* option specifies the template to use. If the template filename
ends in _.xml_, _.html_ or _.rss_, it is assumed to contain XML and the optional
XML preamble is printed and appropriate escaping rules are used.
# FILES
By default, the export uses the **feed.html** template in the current directory.
# EXAMPLE
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>.

39
man/oddmu-hashtags.1 Normal file
View 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-16"
.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 EXAMPLE
.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
View 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.
# EXAMPLE
List the top 10 hashtags. This requires 11 lines because of the header line.
```
oddmu hashtags | head -n 11
```
# SEE ALSO
_oddmu_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-HTML" "1" "2024-02-26"
.TH "ODDMU-HTML" "1" "2024-08-21"
.PP
.SH NAME
.PP
oddmu-html - render Oddmu page HTML from the command-line
oddmu-html - render Oddmu page HTML
.PP
.SH SYNOPSIS
.PP
@@ -19,7 +19,8 @@ oddmu-html - render Oddmu page HTML from the command-line
.PP
The "html" subcommand opens the Markdown file for the given page name (appending
the ".\&md" extension) and prints the HTML to STDOUT without invoking the
"view.\&html" template.\&
"view.\&html" template.\& Use "-" as the page name if you want to read Markdown from
\fBstdin\fR.\&
.PP
.SH OPTIONS
.PP
@@ -31,14 +32,23 @@ lacks html and body tags.\&
.RE
.SH EXAMPLE
.PP
Generate the HTML for "README.\&md":
Generate "README.\&html" from "README.\&md":
.PP
.nf
.RS 4
oddmu html README
oddmu html README > README\&.html
.fi
.RE
.PP
Alternatively:
.PP
.nf
.RS 4
oddmu html - < README\&.md > README\&.html
.fi
.RE
.PP
.PP
.SH ENVIRONMENT
.PP
The ODDMU_WEBFINGER environment variable has no effect in this situation.\&

View File

@@ -2,7 +2,7 @@ ODDMU-HTML(1)
# NAME
oddmu-html - render Oddmu page HTML from the command-line
oddmu-html - render Oddmu page HTML
# SYNOPSIS
@@ -12,7 +12,8 @@ oddmu-html - render Oddmu page HTML from the command-line
The "html" subcommand opens the Markdown file for the given page name (appending
the ".md" extension) and prints the HTML to STDOUT without invoking the
"view.html" template.
"view.html" template. Use "-" as the page name if you want to read Markdown from
*stdin*.
# OPTIONS
@@ -22,12 +23,19 @@ the ".md" extension) and prints the HTML to STDOUT without invoking the
# EXAMPLE
Generate the HTML for "README.md":
Generate "README.html" from "README.md":
```
oddmu html README
oddmu html README > README.html
```
Alternatively:
```
oddmu html - < README.md > README.html
```
# ENVIRONMENT
The ODDMU_WEBFINGER environment variable has no effect in this situation.

29
man/oddmu-links.1 Normal file
View 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" "2024-08-15"
.PP
.SH NAME
.PP
oddmu-links - list outgoing links for pages
.PP
.SH SYNOPSIS
.PP
\fBoddmu links\fR \fIpage names.\&.\&.\&\fR
.PP
.SH DESCRIPTION
.PP
The "links" subcommand lists outgoing links for one or more page names.\& Use "-"
as the page name if you want to read Markdown from \fBstdin\fR.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-missing\fR(1)
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

22
man/oddmu-links.1.txt Normal file
View 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 page names. Use "-"
as the page name if you want to read Markdown from *stdin*.
# SEE ALSO
_oddmu_(1), _oddmu-missing_(1)
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-LIST" "1" "2024-02-24"
.TH "ODDMU-LIST" "1" "2024-08-16"
.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

View File

@@ -2,7 +2,7 @@ ODDMU-LIST(1)
# NAME
oddmu-list - list page names and titles from the command-line
oddmu-list - list page names and titles
# SYNOPSIS

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-MISSING" "1" "2024-02-17"
.TH "ODDMU-MISSING" "1" "2024-08-16"
.PP
.SH NAME
.PP
oddmu-missing - list missing pages from the command-line
oddmu-missing - list missing pages
.PP
.SH SYNOPSIS
.PP

View File

@@ -2,7 +2,7 @@ ODDMU-MISSING(1)
# NAME
oddmu-missing - list missing pages from the command-line
oddmu-missing - list missing pages
# SYNOPSIS

View File

@@ -5,13 +5,13 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NGINX" "5" "2024-02-19"
.TH "ODDMU-NGINX" "5" "2024-05-09"
.PP
.SH NAME
.PP
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
.PP
.SS DESCRIPTION
.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.\&
@@ -20,14 +20,14 @@ 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
.SS CONFIGURATION
.SH CONFIGURATION
.PP
The site is defined in "/etc/nginx/sites-available/default", in the \fIserver\fR
section.\& Add a new \fIlocation\fR section after the existing \fIlocation\fR section:
.PP
.nf
.RS 4
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://localhost:8080;
}
.fi
@@ -38,6 +38,12 @@ get passed on to Oddmu.\& They are essentially disabled.\& Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.\&
.PP
.SS Access
.PP
Access control is not part of Oddmu.\& By default, the wiki is editable by all.\&
This is most likely not what you want unless you'\&re running it stand-alone,
unconnected to the Internet a personal memex on your laptop, for example.\&
.PP
To restrict access to some actions, use two different \fIlocation\fR sections:
.PP
.nf
@@ -73,6 +79,49 @@ alex:$1$DOwphABk$W4VmR9p8t2\&.htxF6ctXHX\&.
.fi
.RE
.PP
These instructions create user accounts with passwords just for Oddmu.\&
These users are not real users on the web server and don'\&t have access to a
shell, mail, or any other service.\&
.PP
.SS Using a Unix-domain Socket
.PP
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket.\& This requires socket activation.\& An example of configuring
the service is given in \fIoddmu.\&service\fR(5).\&
.PP
On the nginx side, you can proxy to the socket using an \fIupstream\fR section.\& This
sends all requests to the socket.\& Use the upstream name as the server name for
\fIproxy_pass\fR.\& Add something like the configuration below to your existing nginx
server configuration.\& On a Debian system, that'\&d be in
"/etc/nginx/sites-available/default".\&
.PP
.nf
.RS 4
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu\&.sock:;
}
.fi
.RE
.PP
Reload the configuration:
.PP
.nf
.RS 4
sudo systemd reload nginx
.fi
.RE
.PP
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.\&sock".\&
.PP
To test it on the command-line, use a tool like \fIcurl(1)\fR.\&
.PP
.nf
.RS 4
curl http://localhost/view/index
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-apache\fR(5)

View File

@@ -4,7 +4,7 @@ ODDMU-NGINX(5)
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
## DESCRIPTION
# DESCRIPTION
The oddmu program serves the current working directory as a wiki on port 8080.
This is an unpriviledged port so an ordinary user account can do this.
@@ -13,13 +13,13 @@ 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
# CONFIGURATION
The site is defined in "/etc/nginx/sites-available/default", in the _server_
section. Add a new _location_ section after the existing _location_ section:
```
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://localhost:8080;
}
```
@@ -29,6 +29,12 @@ get passed on to Oddmu. They are essentially disabled. Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.
## Access
Access control is not part of Oddmu. By default, the wiki is editable by all.
This is most likely not what you want unless you're running it stand-alone,
unconnected to the Internet a personal memex on your laptop, for example.
To restrict access to some actions, use two different _location_ sections:
```
@@ -58,6 +64,43 @@ using this password:
alex:$1$DOwphABk$W4VmR9p8t2.htxF6ctXHX.
```
These instructions create user accounts with passwords just for Oddmu.
These users are not real users on the web server and don't have access to a
shell, mail, or any other service.
## Using a Unix-domain Socket
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket. This requires socket activation. An example of configuring
the service is given in _oddmu.service_(5).
On the nginx side, you can proxy to the socket using an _upstream_ section. This
sends all requests to the socket. Use the upstream name as the server name for
_proxy_pass_. Add something like the configuration below to your existing nginx
server configuration. On a Debian system, that'd be in
"/etc/nginx/sites-available/default".
```
location ~ ^/(view|preview|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu.sock:;
}
```
Reload the configuration:
```
sudo systemd reload nginx
```
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.sock".
To test it on the command-line, use a tool like _curl(1)_.
```
curl http://localhost/view/index
```
# SEE ALSO
_oddmu_(1), _oddmu-apache_(5)

View File

@@ -5,16 +5,142 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-02-26"
.TH "ODDMU-RELEASES" "7" "2024-08-24"
.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.13 (2024)
.PP
Add \fIexport\fR subcommand.\&
.PP
.SS 1.12 (2024)
.PP
Add \fIhashtags\fR, \fIlinks\fR and \fItoc\fR subcommands.\&
.PP
Support searching for multiple words using all sorts of quotation marks.\& That
means that it is now impossible to search for words that begin with such a
quotation mark.\&
.PP
These are the quotation marks currently supported: '\&foo'\& "foo" foo foo foo
“foo” „foo“ ”foo” «foo» »foo« foo foo 「foo」 「foo」 『foo』 any such
quoted text is searched as-is, including whitespace.\&
.PP
Add loading="lazy" for images in search.\&html
.PP
If you want to take advantage of this, you'\&ll need to adapt your "search.\&html"
template accordingly.\& Use like this, for example:
.PP
.nf
.RS 4
{{range \&.Items}}
<article lang="{{\&.Language}}">
<p><a class="result" href="/view/{{\&.Name}}">{{\&.Title}}</a>
<span class="score">{{\&.Score}}</span></p>
<blockquote>{{\&.Html}}</blockquote>
{{range \&.Images}}
<p class="image"><a href="/view/{{\&.Name}}"><img loading="lazy" src="/view/{{\&.Name}}"></a><br/>{{\&.Html}}
{{end}}
</article>
{{end}}
.fi
.RE
.PP
.SS 1.11 (2024)
.PP
The HTML renderer option for smart fractions support was removed.\& Therefore, 1/8
no longer turns into ⅛ or ¹⁄₈.\& The benefit is that something like "doi:
10.\&1017/9781009157926.\&007" doesn'\&t turn into "doi: 10.\&10179781009157926.\&007".\&
If you need to change this, take a look at the \fIwikiRenderer\fR function.\&
.PP
When search terms (excluding hashtags) match the alt text given for an image,
that image is part of the data available to the search template.\&
.PP
If you want to take advantage of this, you'\&ll need to adapt your "search.\&html"
template accordingly.\& Use like this, for example:
.PP
.nf
.RS 4
{{range \&.Items}}
<article lang="{{\&.Language}}">
<p><a class="result" href="/view/{{\&.Name}}">{{\&.Title}}</a>
<span class="score">{{\&.Score}}</span></p>
<blockquote>{{\&.Html}}</blockquote>
{{range \&.Images}}
<p class="image"><a href="/view/{{\&.Name}}"><img class="last" src="/view/{{\&.Name}}"></a><br/>{{\&.Html}}
{{end}}
</article>
{{end}}
.fi
.RE
.PP
.SS 1.10 (2024)
.PP
You can now preview edits instead of saving them.\&
.PP
.PD 0
.IP \(bu 4
a preview button was added to "edit.\&html"
.IP \(bu 4
a new "preview.\&html" was added
.PD
.PP
If you want to take advantage of this, you'\&ll need to adapt your templates
accordingly.\& The "preview.\&html" template is a mix of "view.\&html" and
"edit.\&html".\&
.PP
There is an optional change to make to copies of \fIupload.\&html\fR if you upload
multiple images at a time.\& Instead of showing just the link to the last upload,
you can now show the link (and the images or links, if you want to) to all the
files uploaded.\& Use like this, for example:
.PP
.nf
.RS 4
Links:<tt>{{range \&.Actual}}<br>![]({{\&.}}){{end}}</tt>
.fi
.RE
.PP
.SS 1.9 (2024)
.PP
There is a change to make to copies of \fIupload.\&html\fR if subdirectories are being
used.\& The \fILast\fR property no longer contains the directory.\& It has to be added
to the template as follows:
.PP
.nf
.RS 4
{{if ne \&.Last ""}}
<p>Previous upload: <a href="/view/{{\&.Dir}}{{\&.Last}}">{{\&.Last}}</a></p>
{{if \&.Image}}
<p><img class="last" src="/view/{{\&.Dir}}{{\&.Last}}"></p>
{{end}}
{{end}}
.fi
.RE
.PP
You can use the \fILast\fR property without a directory to suggest the markup to
use, for example:
.PP
.nf
.RS 4
<p>Use the following for <a href="/view/{{\&.Dir}}{{\&.Today}}">{{\&.Today}}</a>:
<pre>![]({{\&.Last}})</a></pre>
.fi
.RE
.PP
The upload template can use the \fIToday\fR property.\&
.PP
The upload template comes with JavaScript that allows users to paste images or
drag and drop files.\&
.PP
The upload template changed the id for the filename field from `text` to `name`.\&
.PP
The source repository now comes with example templates.\&
.PP
.SS 1.8 (2024)
.PP
No user-visible changes.\& Documentation and code comments got better.\&

View File

@@ -2,12 +2,124 @@ 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.13 (2024)
Add _export_ subcommand.
## 1.12 (2024)
Add _hashtags_, _links_ and _toc_ subcommands.
Support searching for multiple words using all sorts of quotation marks. That
means that it is now impossible to search for words that begin with such a
quotation mark.
These are the quotation marks currently supported: 'foo' "foo" foo foo foo
“foo” „foo“ ”foo” «foo» »foo« foo foo 「foo」 「foo」 『foo』 any such
quoted text is searched as-is, including whitespace.
Add loading="lazy" for images in search.html
If you want to take advantage of this, you'll need to adapt your "search.html"
template accordingly. Use like this, for example:
```
{{range .Items}}
<article lang="{{.Language}}">
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
<span class="score">{{.Score}}</span></p>
<blockquote>{{.Html}}</blockquote>
{{range .Images}}
<p class="image"><a href="/view/{{.Name}}"><img loading="lazy" src="/view/{{.Name}}"></a><br/>{{.Html}}
{{end}}
</article>
{{end}}
```
## 1.11 (2024)
The HTML renderer option for smart fractions support was removed. Therefore, 1/8
no longer turns into ⅛ or ¹⁄₈. The benefit is that something like "doi:
10.1017/9781009157926.007" doesn't turn into "doi: 10.10179781009157926.007".
If you need to change this, take a look at the _wikiRenderer_ function.
When search terms (excluding hashtags) match the alt text given for an image,
that image is part of the data available to the search template.
If you want to take advantage of this, you'll need to adapt your "search.html"
template accordingly. Use like this, for example:
```
{{range .Items}}
<article lang="{{.Language}}">
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
<span class="score">{{.Score}}</span></p>
<blockquote>{{.Html}}</blockquote>
{{range .Images}}
<p class="image"><a href="/view/{{.Name}}"><img class="last" src="/view/{{.Name}}"></a><br/>{{.Html}}
{{end}}
</article>
{{end}}
```
## 1.10 (2024)
You can now preview edits instead of saving them.
- a preview button was added to "edit.html"
- a new "preview.html" was added
If you want to take advantage of this, you'll need to adapt your templates
accordingly. The "preview.html" template is a mix of "view.html" and
"edit.html".
There is an optional change to make to copies of _upload.html_ if you upload
multiple images at a time. Instead of showing just the link to the last upload,
you can now show the link (and the images or links, if you want to) to all the
files uploaded. Use like this, for example:
```
Links:<tt>{{range .Actual}}<br>![]({{.}}){{end}}</tt>
```
## 1.9 (2024)
There is a change to make to copies of _upload.html_ if subdirectories are being
used. The _Last_ property no longer contains the directory. It has to be added
to the template as follows:
```
{{if ne .Last ""}}
<p>Previous upload: <a href="/view/{{.Dir}}{{.Last}}">{{.Last}}</a></p>
{{if .Image}}
<p><img class="last" src="/view/{{.Dir}}{{.Last}}"></p>
{{end}}
{{end}}
```
You can use the _Last_ property without a directory to suggest the markup to
use, for example:
```
<p>Use the following for <a href="/view/{{.Dir}}{{.Today}}">{{.Today}}</a>:
<pre>![]({{.Last}})</a></pre>
```
The upload template can use the _Today_ property.
The upload template comes with JavaScript that allows users to paste images or
drag and drop files.
The upload template changed the id for the filename field from `text` to `name`.
The source repository now comes with example templates.
## 1.8 (2024)
No user-visible changes. Documentation and code comments got better.

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-REPLACE" "1" "2024-02-17"
.TH "ODDMU-REPLACE" "1" "2024-08-16"
.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

View File

@@ -2,7 +2,7 @@ ODDMU-REPLACE(1)
# NAME
oddmu-replace - replace text in Oddmu pages from the command-line
oddmu-replace - replace text in Oddmu pages
# SYNOPSIS

View File

@@ -5,11 +5,11 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-SEARCH" "1" "2024-02-17"
.TH "ODDMU-SEARCH" "1" "2024-08-16"
.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
@@ -51,20 +56,30 @@ Ignore pagination and just print a long list of results.\&
.RE
.SH EXAMPLE
.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

View File

@@ -2,7 +2,7 @@ ODDMU-SEARCH(1)
# NAME
oddmu-search - search the Oddmu pages from the command-line
oddmu-search - search the Oddmu pages
# SYNOPSIS
@@ -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,7 +32,7 @@ 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.
@@ -36,17 +41,27 @@ scored.
# EXAMPLE
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

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2024-02-26"
.TH "ODDMU-STATIC" "1" "2024-03-12"
.PP
.SH NAME
.PP
@@ -18,13 +18,18 @@ oddmu-static - create a static copy of the site
.SH DESCRIPTION
.PP
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory.\& The destination
directory must not exist.\&
directory and saves them in the given destination directory.\& Existing files are
only overwritten if they are older than the source file.\&
.PP
All pages (files with the ".\&md" extension) are turned into HTML files (with the
".\&html" extension) using the "static.\&html" template.\& Links pointing to existing
pages get ".\&html" appended.\&
.PP
If a page has a name case-insensitively matching a hashtag, a feed file is
generated (ending with ".\&rss") if any suitable links are found.\& A suitable link
for a feed item must appear in a bullet list item using an asterisk ("*").\& If
no feed items are found, no feed is written.\&
.PP
Hidden files and directories (starting with a ".\&") and backup files (ending with
a "~") are skipped.\&
.PP
@@ -48,16 +53,22 @@ original, too.\&
.PP
.SH EXAMPLE
.PP
Generate a static copy of the site:
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
.PP
.nf
.RS 4
oddmu static \&.\&./archive
env ODDMU_LANGUAGES=de,en oddmu static \&.\&./archive
.fi
.RE
.PP
.SH LIMITATIONS
.PP
There can be nameclashes with generated HTML and RSS files and existing files
ending in ".\&html" and ".\&rss".\& Instead of overwriting existing files in these
cases, a warning is printed.\&
.PP
Links from files to pages do not get ".\&html" appended.\& This affects existing
HTML or XML files including SVG files.\&
.PP

View File

@@ -11,13 +11,18 @@ oddmu-static - create a static copy of the site
# DESCRIPTION
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory. The destination
directory must not exist.
directory and saves them in the given destination directory. Existing files are
only overwritten if they are older than the source file.
All pages (files with the ".md" extension) are turned into HTML files (with the
".html" extension) using the "static.html" template. Links pointing to existing
pages get ".html" appended.
If a page has a name case-insensitively matching a hashtag, a feed file is
generated (ending with ".rss") if any suitable links are found. A suitable link
for a feed item must appear in a bullet list item using an asterisk ("\*"). If
no feed items are found, no feed is written.
Hidden files and directories (starting with a ".") and backup files (ending with
a "~") are skipped.
@@ -41,14 +46,20 @@ original, too.
# EXAMPLE
Generate a static copy of the site:
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
```
oddmu static ../archive
env ODDMU_LANGUAGES=de,en oddmu static ../archive
```
# LIMITATIONS
There can be nameclashes with generated HTML and RSS files and existing files
ending in ".html" and ".rss". Instead of overwriting existing files in these
cases, a warning is printed.
Links from files to pages do not get ".html" appended. This affects existing
HTML or XML files including SVG files.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-TEMPLATES" "5" "2024-02-17" "File Formats Manual"
.TH "ODDMU-TEMPLATES" "5" "2024-07-21" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -13,13 +13,39 @@ oddmu-templates - how to write the templates
.PP
.SH SYNOPSIS
.PP
These files act as HTML templates: add.\&html, diff.\&html, edit.\&html, feed.\&html,
search.\&html, static.\&html, upload.\&html and view.\&html.\& They contain special
placeholders in double bracers {{like this}}.\&
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}}.\&
.PP
.SH SYNTAX
.SH DESCRIPTION
.PP
The templates can refer to the following properties of a page:
Each template receives an object and uses the object'\&s properties to replace the
placeholders.\&
.PP
.PD 0
.IP \(bu 4
\fIadd.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIdiff.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIedit.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIfeed.\&html\fR uses a \fIfeed\fR
.IP \(bu 4
\fIpreview.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIsearch.\&html\fR uses a \fIsearch\fR
.IP \(bu 4
\fIstatic.\&html\fR uses a \fIpage\fR
.IP \(bu 4
\fIupload.\&html\fR uses an \fIupload\fR
.IP \(bu 4
\fIview.\&html\fR uses a \fIpage\fR
.PD
.PP
.SS Page
.PP
A page has the following properties:
.PP
\fI{{.\&Title}}\fR is the page title.\& If the page doesn'\&t provide its own title, the
page name is used.\&
@@ -33,71 +59,122 @@ extension.\&
\fI{{.\&Base}}\fR is the basename of the current file (without the directory and
without the \fI.\&md\fR extension), escaped for use in URLs.\&
.PP
\fI{{.\&Today}}\fR is the current date, in ISO format.\& This is useful for "new page"
like links or forms (see \fBEXAMPLE\fR below).\&
\fI{{.\&Language}}\fR is the suspected language of the page.\& This is used to set the
language on the \fIview.\&html\fR template.\& See "Non-English hyphenation" below.\&
.PP
For the \fIview.\&html\fR and \fIstatic.\&html\fR template:
.PP
\fI{{.\&Html}}\fR is the rendered Markdown, as HTML.\&
\fI{{.\&Body}}\fR is the raw byte content of the page.\& Use \fI{{printf "%s" .\&Body}}\fR to
get the Markdown, as a string.\& This is used for the text area of the \fIedit.\&html\fR
template.\&
.PP
\fI{{.\&Hashtags}}\fR is an array of strings.\&
.PP
For the \fIdiff.\&html\fR template:
\fI{{.\&Html}}\fR contains some sort of HTML that depends on the template used.\&
.PP
\fI{{.\&Diff}}\fR is the diff for this page.\& This is only computed on demand so it can
be used in other templates, too.\& It probably doesn'\&t make much sense to do so,
however.\&
.PD 0
.IP \(bu 4
For \fIview.\&html\fR, it is the rendered Markdown, as HTML.\&
.IP \(bu 4
For \fIsearch.\&html\fR, it is a page summary, with bold matches, as HTML.\&
.IP \(bu 4
For \fIfeed.\&html\fR, it is the escaped (!\&) HTML of the feed item.\&
.PD
.PP
For the \fIedit.\&html\fR template:
\fI{{.\&IsBlog}}\fR says whether the current page has a name starting with an ISO
date.\&
.PP
\fI{{printf "%s" .\&Body}}\fR is the Markdown, as a string (the data itself is a byte
array and that'\&s why we need to call \fIprintf\fR).\&
\fI{{.\&Today}}\fR is the current date, in ISO format.\& This is useful for "new page"
like links or forms (see \fBEXAMPLE\fR below).\&
.PP
For the \fIsearch.\&html\fR template only:
\fI{{.\&Parents}}\fR is the array of links to parent pages (see \fBEXAMPLE\fR below).\& To
refer to them, you need to use a \fI{{range .\&Parents}}\fR\fI{{end}}\fR construct.\& A
link has to properties, \fI{{.\&Title}}\fR and \fI{{.\&Url}}\fR.\&
.PP
\fI{{.\&Diff}}\fR is the page diff for \fIdiff.\&html\fR.\& It is only computed on demand so
it can be used in other templates, too.\& It probably doesn'\&t make much sense to
do so, however.\&
.PP
.SS Feed
.PP
The feed contains an item for the head of the feed and an array of items.\&
.PP
\fI{{.\&Items}}\fR is the array of feed items.\& To refer to them, you need to use a
\fI{{range .\&Items}}\fR\fI{{end}}\fR construct.\&
.PP
If page A links to pages B and C, the head of the feed is based on page A and
the list of items contains B and C.\&
.PP
An item is a page plus a date.\& All the properties of a page can be used (see
\fBPage\fR above).\&
.PP
\fI{{.\&Date}}\fR is the date of the last update to the page, in RFC 822 format.\&
.PP
.SS Search
.PP
\fI{{.\&Query}}\fR is the query string.\&
.PP
\fI{{.\&Dir}}\fR is the directory in which the search starts, percent-escaped except
for the slashes.\&
.PP
\fI{{.\&Previous}}\fR, \fI{{.\&Page}}\fR and \fI{{.\&Next}}\fR are the previous, current and next
page number in the results since doing arithmetics in templates is hard.\& The
first page number is 1.\& The last page is expensive to dermine and so that isn'\&t
done.\&
first page number is 1.\& The last page is expensive to dermine and so that is not
available.\&
.PP
\fI{{.\&More}}\fR indicates if there are any more search results.\&
.PP
\fI{{.\&Results}}\fR indicates if there were any search results at all.\&
.PP
\fI{{.\&Items}}\fR is an array of pages, each containing a search result.\& A search
result is a page (with the properties seen above).\& Thus, to refer to them, you
need to use a \fI{{range .\&Items}}\fR\fI{{end}}\fR construct.\&
\fI{{.\&Items}}\fR is an array of results.\& To refer to them, you need to use a
\fI{{range .\&Items}}\fR\fI{{end}}\fR construct.\&
.PP
For items in the search result:
A result is a page plus a score and possibly images.\& All the properties of a
page can be used (see \fBPage\fR above).\&
.PP
\fI{{.\&Html}}\fR is the rendered Markdown of a page summary, as HTML.\&
\fI{{.\&Score}}\fR is a numerical score.\& It is only computed for \fIsearch.\&html\fR.\&
.PP
\fI{{.\&Score}}\fR is a numerical score for search results.\&
\fI{{.\&Images}}\fR are the images where the alt-text matches at least one of the
query terms (but not predicates and not hashtags since those apply to the page
as a whole).\& To refer to them, you need to use a \fI{{range .\&Images}}\fR\fI{{end}}\fR
construct.\&
.PP
For the \fIfeed.\&html\fR template:
Each image has three properties:
.PP
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\&
\fI{{.\&Title}}\fR is the alt-text of the image.\& It can never be empty because images
are only listed if a search term matches.\&
.PP
\fI{{.\&Title}}\fR is the title of the underlying main page.\&
\fI{{.\&Name}}\fR is the file name for use in URLs.\&
.PP
\fI{{.\&Date}}\fR is the date of the last update to the underlying main page, in RFC
822 format.\&
\fI{{.\&Html}}\fR the image alt-text with a bold tag used to highlight the first
search term that matched.\&
.PP
\fI{{.\&Items}}\fR is an array of feed items.\&
.SS Upload
.PP
For items in the feed:
\fI{{.\&Dir}}\fR is the directory where the uploaded file ends up, based on the URL
path, percent-escaped except for the slashes.\&
.PP
\fI{{.\&Name}}\fR is the page name, escaped for use in URLs.\&
\fI{{.\&Name}}\fR is the \fIfilename\fR query parameter.\&
.PP
\fI{{.\&Title}}\fR is the title of the page.\&
\fI{{.\&Last}}\fR is the filename of the last file uploaded.\&
.PP
\fI{{.\&Html}}\fR is the rendered Markdown, as escaped (!\&) HTML.\&
\fI{{.\&Actual}}\fR is an array of filenames of all the files uploaded.\& Use {{range
Actual}} … {{.\&}} … {{end}} to loop over all the filenames.\&
.PP
\fI{{.\&Hashtags}}\fR is an array of strings.\&
\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.\&
.PP
\fI{{.\&Date}}\fR, the date of the last update to this page, in RFC 822 format.\&
\fI{{.\&Title}}\fR is the title of the basename, if it exists.\&
.PP
The \fIupload.\&html\fR template cannot refer to anything.\&
\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.\&
.PP
\fI{{.\&MaxWidth}}\fR is the \fImaxwidth\fR query parameter, i.\&e.\& the value used for the
previous image uploaded.\&
.PP
\fI{{.\&Quality}}\fR is the \fIquality\fR query parameter, i.\&e.\& the value used for the
previous image uploaded.\&
.PP
\fI{{.\&Today}}\fR is the current date, in ISO format.\&
.PP
.SS Non-English hyphenation
.PP
@@ -147,6 +224,26 @@ The following form allows people to edit the suggested page name.\&
.fi
.RE
.PP
The following puts the current date into the text area if and only if the page
itself is a blog page.\& Useful for \fIadd.\&html\fR:
.PP
.nf
.RS 4
<textarea name="body" rows="20" cols="80" placeholder="Text" lang=""
autofocus required>{{- if \&.IsBlog}}**{{\&.Today}}**\&. {{end}}</textarea>
.fi
.RE
.PP
The following adds a list of links to parent directories.\& Useful for \fIview.\&html\fR:
.PP
.nf
.RS 4
<nav>
{{range \&.Parents}}/ <a href="{{\&.Url}}">{{\&.Title}}</a>{{end}}
</nav>
.fi
.RE
.PP
.SH NOTES
.PP
The templates are always used as-is, irrespective of the current directory.\&
@@ -161,6 +258,10 @@ this case, a visitor looking at "/view/projects/wiki" following a link to
.PP
It'\&s up to you to decide what'\&s best for your site, of course.\&
.PP
If you want a link on \fIupload.\&html\fR to point to the current directory'\&s "index"
page, you need to use "/view/{{.\&Dir}}index" because if you link to "index" the
result points to "/upload/{{.\&Dir}}index".\&
.PP
Templates can be changed by uploading new copies of the template files.\&
.PP
Subdirectories can have their own copies of template files.\& One example use for

View File

@@ -6,13 +6,28 @@ oddmu-templates - how to write the templates
# SYNOPSIS
These files act as HTML templates: add.html, diff.html, edit.html, feed.html,
search.html, static.html, upload.html and view.html. They contain special
placeholders in double bracers {{like this}}.
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}}.
# SYNTAX
# DESCRIPTION
The templates can refer to the following properties of a page:
Each template receives an object and uses the object's properties to replace the
placeholders.
- _add.html_ uses a _page_
- _diff.html_ uses a _page_
- _edit.html_ uses a _page_
- _feed.html_ uses a _feed_
- _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_
## Page
A page has the following properties:
_{{.Title}}_ is the page title. If the page doesn't provide its own title, the
page name is used.
@@ -26,71 +41,117 @@ _{{.Dir}}_ is the page directory, percent-escaped except for the slashes.
_{{.Base}}_ is the basename of the current file (without the directory and
without the _.md_ extension), escaped for use in URLs.
_{{.Today}}_ is the current date, in ISO format. This is useful for "new page"
like links or forms (see *EXAMPLE* below).
_{{.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.
For the _view.html_ and _static.html_ template:
_{{.Html}}_ is the rendered Markdown, as HTML.
_{{.Body}}_ is the raw byte content of the page. Use _{{printf "%s" .Body}}_ to
get the Markdown, as a string. This is used for the text area of the _edit.html_
template.
_{{.Hashtags}}_ is an array of strings.
For the _diff.html_ template:
_{{.Html}}_ contains some sort of HTML that depends on the template used.
_{{.Diff}}_ is the diff for this page. This is only computed on demand so it can
be used in other templates, too. It probably doesn't make much sense to do so,
however.
- For _view.html_, it is the rendered Markdown, as HTML.
- For _search.html_, it is a page summary, with bold matches, as HTML.
- For _feed.html_, it is the escaped (!) HTML of the feed item.
For the _edit.html_ template:
_{{.IsBlog}}_ says whether the current page has a name starting with an ISO
date.
_{{printf "%s" .Body}}_ is the Markdown, as a string (the data itself is a byte
array and that's why we need to call _printf_).
_{{.Today}}_ is the current date, in ISO format. This is useful for "new page"
like links or forms (see *EXAMPLE* below).
For the _search.html_ template only:
_{{.Parents}}_ is the array of links to parent pages (see *EXAMPLE* below). To
refer to them, you need to use a _{{range .Parents}}_ … _{{end}}_ construct. A
link has to properties, _{{.Title}}_ and _{{.Url}}_.
_{{.Diff}}_ is the page diff for _diff.html_. It is only computed on demand so
it can be used in other templates, too. It probably doesn't make much sense to
do so, however.
## Feed
The feed contains an item for the head of the feed and an array of items.
_{{.Items}}_ is the array of feed items. To refer to them, you need to use a
_{{range .Items}}_ … _{{end}}_ construct.
If page A links to pages B and C, the head of the feed is based on page A and
the list of items contains B and C.
An item is a page plus a date. All the properties of a page can be used (see
*Page* above).
_{{.Date}}_ is the date of the last update to the page, in RFC 822 format.
## Search
_{{.Query}}_ is the query string.
_{{.Dir}}_ is the directory in which the search starts, percent-escaped except
for the slashes.
_{{.Previous}}_, _{{.Page}}_ and _{{.Next}}_ are the previous, current and next
page number in the results since doing arithmetics in templates is hard. The
first page number is 1. The last page is expensive to dermine and so that isn't
done.
first page number is 1. The last page is expensive to dermine and so that is not
available.
_{{.More}}_ indicates if there are any more search results.
_{{.Results}}_ indicates if there were any search results at all.
_{{.Items}}_ is an array of pages, each containing a search result. A search
result is a page (with the properties seen above). Thus, to refer to them, you
need to use a _{{range .Items}}_ … _{{end}}_ construct.
_{{.Items}}_ is an array of results. To refer to them, you need to use a
_{{range .Items}}_ … _{{end}}_ construct.
For items in the search result:
A result is a page plus a score and possibly images. All the properties of a
page can be used (see *Page* above).
_{{.Html}}_ is the rendered Markdown of a page summary, as HTML.
_{{.Score}}_ is a numerical score. It is only computed for _search.html_.
_{{.Score}}_ is a numerical score for search results.
_{{.Images}}_ are the images where the alt-text matches at least one of the
query terms (but not predicates and not hashtags since those apply to the page
as a whole). To refer to them, you need to use a _{{range .Images}}_ … _{{end}}_
construct.
For the _feed.html_ template:
Each image has three properties:
_{{.Name}}_ is the page name, escaped for use in URLs.
_{{.Title}}_ is the alt-text of the image. It can never be empty because images
are only listed if a search term matches.
_{{.Title}}_ is the title of the underlying main page.
_{{.Name}}_ is the file name for use in URLs.
_{{.Date}}_ is the date of the last update to the underlying main page, in RFC
822 format.
_{{.Html}}_ the image alt-text with a bold tag used to highlight the first
search term that matched.
_{{.Items}}_ is an array of feed items.
## Upload
For items in the feed:
_{{.Dir}}_ is the directory where the uploaded file ends up, based on the URL
path, percent-escaped except for the slashes.
_{{.Name}}_ is the page name, escaped for use in URLs.
_{{.Name}}_ is the _filename_ query parameter.
_{{.Title}}_ is the title of the page.
_{{.Last}}_ is the filename of the last file uploaded.
_{{.Html}}_ is the rendered Markdown, as escaped (!) HTML.
_{{.Actual}}_ is an array of filenames of all the files uploaded. Use {{range
.Actual}} … {{.}} … {{end}} to loop over all the filenames.
_{{.Hashtags}}_ is an array of strings.
_{{.Base}}_ is the basename of the first file uploaded (without the directory,
extension and numeric part at the end), escaped for use in URLs.
_{{.Date}}_, the date of the last update to this page, in RFC 822 format.
_{{.Title}}_ is the title of the basename, if it exists.
The _upload.html_ template cannot refer to anything.
_{{.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.
_{{.MaxWidth}}_ is the _maxwidth_ query parameter, i.e. the value used for the
previous image uploaded.
_{{.Quality}}_ is the _quality_ query parameter, i.e. the value used for the
previous image uploaded.
_{{.Today}}_ is the current date, in ISO format.
## Non-English hyphenation
@@ -136,6 +197,22 @@ The following form allows people to edit the suggested page name.
</form>
```
The following puts the current date into the text area if and only if the page
itself is a blog page. Useful for _add.html_:
```
<textarea name="body" rows="20" cols="80" placeholder="Text" lang=""
autofocus required>{{- if .IsBlog}}**{{.Today}}**. {{end}}</textarea>
```
The following adds a list of links to parent directories. Useful for _view.html_:
```
<nav>
{{range .Parents}}/ <a href="{{.Url}}">{{.Title}}</a>{{end}}
</nav>
```
# NOTES
The templates are always used as-is, irrespective of the current directory.
@@ -150,6 +227,10 @@ this case, a visitor looking at "/view/projects/wiki" following a link to
It's up to you to decide what's best for your site, of course.
If you want a link on _upload.html_ to point to the current directory's "index"
page, you need to use "/view/{{.Dir}}index" because if you link to "index" the
result points to "/upload/{{.Dir}}index".
Templates can be changed by uploading new copies of the template files.
Subdirectories can have their own copies of template files. One example use for

33
man/oddmu-toc.1 Normal file
View File

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

26
man/oddmu-toc.1.txt Normal file
View File

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

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-02-23"
.TH "ODDMU" "1" "2024-08-16"
.PP
.SH NAME
.PP
@@ -63,6 +63,8 @@ directory:
.IP \(bu 4
\fI/edit/dir/name\fR shows a form to edit a page
.IP \(bu 4
\fI/preview/dir/name\fR shows a preview of a page edit and the form to edit it
.IP \(bu 4
\fI/save/dir/name\fR saves an edit
.IP \(bu 4
\fI/add/dir/name\fR shows a form to add to a page
@@ -90,11 +92,11 @@ 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
\fImaxwidth\fR is set, an attempt is made to decode and resize the image.\& JPG, PNG
and HEIC files can be decoded.\& Only JPG and PNG files can be encoded, however.\&
If the target name ends in \fI.\&jpg\fR, the \fIquality\fR query parameter is also taken
into account.\& To upload some thumbnails:
target filename and \fIfile\fR for the file to upload.\& If the query parameter
\fImaxwidth\fR is set, an attempt is made to decode and resize the image.\& JPG, PNG,
WEBP and HEIC files can be decoded.\& Only JPG and PNG files can be encoded,
however.\& If the target name ends in \fI.\&jpg\fR, the \fIquality\fR query parameter is
also taken into account.\& To upload some thumbnails:
.PP
.nf
.RS 4
@@ -136,28 +138,8 @@ curl --remote-name \&'http://localhost:8080/archive/man/man\&.zip
.PP
.SH CONFIGURATION
.PP
The template files are the HTML files in the working directory:
.PP
.PD 0
.IP \(bu 4
\fIview.\&html\fR shows a page
.IP \(bu 4
\fIdiff.\&html\fR shows the last change to a page
.IP \(bu 4
\fIedit.\&html\fR shows a form to edit a page
.IP \(bu 4
\fIadd.\&html\fR shows a form to add to a page
.IP \(bu 4
\fIupload.\&html\fR shows a form to upload a file
.IP \(bu 4
\fIsearch.\&html\fR shows the search results
.IP \(bu 4
\fIstatic.\&html\fR is used to generate a static site
.IP \(bu 4
\fIfeed.\&html\fR is used to generate a RSS feed
.PD
.PP
Please change the templates!\&
The template files are the HTML files in the working directory.\& Please change
these templates!\&
.PP
The first change you should make is to replace the name and email address in the
footer of \fIview.\&html\fR.\& Look for "Your Name" and "example.\&org".\&
@@ -204,21 +186,20 @@ ODDMU_FILTER can be used to exclude subdirectories from such tree actions.\& See
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
through socket activation.\& The advantage of this method is that you can use a
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5) and
\fIoddmu-apache\fR(5) for an example of how to use socket activation with a
Unix-domain socket under systemd and Apache.\&
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5),
\fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for an example of how to use socket
activation with a Unix-domain socket under systemd and Apache.\&
.PP
.SH SECURITY
.PP
If the machine you are running Oddmu on is accessible from the Internet, you
must secure your installation.\& The best way to do this is use a regular web
server as a reverse proxy.\&
.PP
See \fIoddmu-apache\fR(5) for an example.\&
server as a reverse proxy.\& See \fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for
example configurations.\&
.PP
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
HTML sanitization!\&
users and therefore their content is trusted.\& Oddmu does not perform HTML
sanitization!\&
.PP
For an extra dose of security, consider using a Unix-domain socket.\&
.PP
@@ -259,6 +240,14 @@ curl --form body="Did you bring a towel?"
.fi
.RE
.PP
To compute the space used by your setup, use regular tools:
.PP
.nf
.RS 4
du --exclude=\&'*/.*\&' --exclude \&'*~\&' --block-size=M
.fi
.RE
.PP
.SH DESIGN
.PP
This is a minimal wiki.\& There is no version history.\& It'\&s well suited as a
@@ -378,24 +367,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>.\&

View File

@@ -47,6 +47,7 @@ directory:
- _/view/dir/name.rss_ shows the RSS feed for the pages linked
- _/diff/dir/name_ shows the last change to a page
- _/edit/dir/name_ shows a form to edit a page
- _/preview/dir/name_ shows a preview of a page edit and the form to edit it
- _/save/dir/name_ saves an edit
- _/add/dir/name_ shows a form to add to a page
- _/append/dir/name_ appends an addition to a page
@@ -65,11 +66,11 @@ 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
_maxwidth_ is set, an attempt is made to decode and resize the image. JPG, PNG
and HEIC files can be decoded. Only JPG and PNG files can be encoded, however.
If the target name ends in _.jpg_, the _quality_ query parameter is also taken
into account. To upload some thumbnails:
target filename and _file_ for the file to upload. If the query parameter
_maxwidth_ is set, an attempt is made to decode and resize the image. JPG, PNG,
WEBP and HEIC files can be decoded. Only JPG and PNG files can be encoded,
however. If the target name ends in _.jpg_, the _quality_ query parameter is
also taken into account. To upload some thumbnails:
```
for f in *.jpg; do
@@ -103,18 +104,8 @@ curl --remote-name 'http://localhost:8080/archive/man/man.zip
# CONFIGURATION
The template files are the HTML files in the working directory:
- _view.html_ shows a page
- _diff.html_ shows the last change to a page
- _edit.html_ shows a form to edit a page
- _add.html_ shows a form to add to a page
- _upload.html_ shows a form to upload a file
- _search.html_ shows the search results
- _static.html_ is used to generate a static site
- _feed.html_ is used to generate a RSS feed
Please change the templates!
The template files are the HTML files in the working directory. Please change
these templates!
The first change you should make is to replace the name and email address in the
footer of _view.html_. Look for "Your Name" and "example.org".
@@ -159,21 +150,20 @@ _oddmu-filter_(7) and _oddmu-apache_(5).
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
through socket activation. The advantage of this method is that you can use a
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
the socket are set before the program starts. See _oddmu.service_(5) and
_oddmu-apache_(5) for an example of how to use socket activation with a
Unix-domain socket under systemd and Apache.
the socket are set before the program starts. See _oddmu.service_(5),
_oddmu-apache_(5) and _oddmu-nginx_(5) for an example of how to use socket
activation with a Unix-domain socket under systemd and Apache.
# SECURITY
If the machine you are running Oddmu on is accessible from the Internet, you
must secure your installation. The best way to do this is use a regular web
server as a reverse proxy.
See _oddmu-apache_(5) for an example.
server as a reverse proxy. See _oddmu-apache_(5) and _oddmu-nginx_(5) for
example configurations.
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
HTML sanitization!
users and therefore their content is trusted. Oddmu does not perform HTML
sanitization!
For an extra dose of security, consider using a Unix-domain socket.
@@ -204,6 +194,12 @@ curl --form body="Did you bring a towel?" \
http://localhost:8080/save/welcome
```
To compute the space used by your setup, use regular tools:
```
du --exclude='*/\.*' --exclude '*~' --block-size=M
```
# DESIGN
This is a minimal wiki. There is no version history. It's well suited as a
@@ -309,16 +305,22 @@ If you run Oddmu as a web server:
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>.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "5" "2024-02-17" "File Formats Manual"
.TH "ODDMU" "5" "2024-08-17" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -34,10 +34,9 @@ most importantly tables and definition lists.\&
.PP
.SS Local links
.PP
Local links use double square brackets [[like this]].\& Oddmu does not treat
underscores like spaces, so [[like this]] and [[like_this]] link to different
destinations and are served by different files: "like this.\&md" and
"like_this.\&md".\&
Local links use double square brackets.\& Oddmu does not treat underscores like
spaces, so "[[like this]]" and "[[like_this]]" link to different destinations
and are served by different files: "like this.\&md" and "like_this.\&md".\&
.PP
.SS Hashtags
.PP

View File

@@ -25,10 +25,9 @@ most importantly tables and definition lists.
## Local links
Local links use double square brackets [[like this]]. Oddmu does not treat
underscores like spaces, so [[like this]] and [[like_this]] link to different
destinations and are served by different files: "like this.md" and
"like_this.md".
Local links use double square brackets. Oddmu does not treat underscores like
spaces, so "[[like this]]" and "[[like_this]]" link to different destinations
and are served by different files: "like this.md" and "like_this.md".
## Hashtags

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU.SERVICE" "5" "2024-02-17"
.TH "ODDMU.SERVICE" "5" "2024-08-23"
.PP
.SH NAME
.PP
@@ -32,13 +32,14 @@ all the templates files ending in ".\&html" from the source distribution to
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.\&service" to "/home/oddmu", too.\&
.PP
Edit the `oddmu.\&service` file.\& These are the lines you most likely have to take
Edit the "oddmu.\&service" file.\& These are the lines you most likely have to take
care of:
.PP
.nf
.RS 4
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
.fi
@@ -48,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
@@ -71,15 +71,6 @@ journalctl --follow --unit oddmu
.fi
.RE
.PP
For it to restart when the server reboots:
.PP
.nf
.RS 4
sudo ln -sf /home/oddmu/oddmu\&.service
/etc/systemd/system/multi-user\&.target\&.wants/
.fi
.RE
.PP
.SH Socket Activation
.PP
Alternatively, you can let systemd handle the creation of the listening socket,
@@ -91,19 +82,133 @@ tells systemd to pass the socket to the service as its standard input.\&
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.\&
.PP
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.\&service" replaced
by "oddmu-unix-domain.\&service".\& You'\&ll also need to run the following:
Instead of using "oddmu.\&service", you need to use "oddmu-unix-domain.\&socket" and
"oddmu-unix-domain.\&service".\&
.PP
The unit file for the socket defines a file name.\& You probably need to create
the directory or change the file name.\&
.PP
.nf
.RS 4
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
sudo mkdir /run/oddmu
.fi
.RE
.PP
The unit file for the service defines where the "oddmu" is and where the data
directory is.\& These are the lines you most likely have to take care of:
.PP
.nf
.RS 4
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
.fi
.RE
.PP
To install, enable and start both units:
.PP
.nf
.RS 4
sudo systemctl enable --now \&./oddmu-unix-domain\&.socket
sudo systemctl enable --now \&./oddmu-unix-domain\&.service
.fi
.RE
.PP
To test just the unix domain socket, use \fIncat(1)\fR:
.PP
.nf
.RS 4
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
| ncat --unixsock /run/oddmu/oddmu\&.sock
.fi
.RE
.PP
Now you need to set up your web browser to use the Unix domain socket.\& See
\fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example configurations.\&
.PP
.SS A personal wiki
.PP
On a single user machine, it might be useful to have a single wiki for the main
user available.\& In order to do this, setup a "user" unit using systemd and save
the following as "user-unix-domain.\&service":
.PP
.nf
.RS 4
[Unit]
Description=Oddmu
After=network\&.target
[Install]
WantedBy=default\&.target
[Service]
Type=simple
Restart=always
StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_LANGUAGES=de,en"
.fi
.RE
.PP
Since this is a user service, the same user can edit the files using their
favourite text editor.\&
.PP
Install it:
.PP
.nf
.RS 4
systemctl --user enable --now \&./user-unix-domain\&.service
.fi
.RE
.PP
To examine the log:
.PP
.nf
.RS 4
journalctl --user --unit user-unix-domain\&.service
.fi
.RE
.PP
Note that no sudo is required!\&
.PP
.SS Using the priviledged port 80
.PP
When running a personal wiki, you can have the oddmu binary listen on port 80,
the standard HTTP port.\& It is not really worth the effort: It means that you can
visit "http://localhost/" instead of "http://localhost:8080".\& Nevertheless, if
you'\&re interested in giving it a try, here'\&s how to do it.\&
.PP
The service definition must specify the new port:
.PP
Environment="ODDMU_PORT=80"
.PP
Since this is a privileged port, the binary needs an extra capability for an
ordinary user to do this.\&
.PP
.nf
.RS 4
sudo setcap \&'cap_net_bind_service=+ep\&' oddmu
.fi
.RE
.PP
Note that as soon as you recompile, the capability is gone again and the above
must be repeated.\&
.PP
.SH SECURITY
.PP
Only allow direct access to Oddmu on systems and networks where you'\&re OK with
every user editing the pages.\& On the open web, this is not true.\& If your server
is on the open web, always run Oddmu behind a regular web server acting as a
reverse proxy, limiting regular visitors to read-only access.\& This means that
the regular web server listens on the regular privileged ports (80 for HTTP,
443 for HTTPS) and passes requests to Oddmu on some other port.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIsystemd.\&socket(5), \fRcapabilities_(7)
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
\fIsystemd.\&socket\fR(5), \fIcapabilities\fR(7)
.PP
.SH AUTHORS
.PP

View File

@@ -23,12 +23,13 @@ all the templates files ending in ".html" from the source distribution to
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.service" to "/home/oddmu", too.
Edit the `oddmu.service` file. These are the lines you most likely have to take
Edit the "oddmu.service" file. These are the lines you most likely have to take
care of:
```
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
```
@@ -36,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/.
@@ -54,13 +54,6 @@ Follow the log:
journalctl --follow --unit oddmu
```
For it to restart when the server reboots:
```
sudo ln -sf /home/oddmu/oddmu.service \
/etc/systemd/system/multi-user.target.wants/
```
# Socket Activation
Alternatively, you can let systemd handle the creation of the listening socket,
@@ -72,17 +65,117 @@ tells systemd to pass the socket to the service as its standard input.
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.service" replaced
by "oddmu-unix-domain.service". You'll also need to run the following:
Instead of using "oddmu.service", you need to use "oddmu-unix-domain.socket" and
"oddmu-unix-domain.service".
The unit file for the socket defines a file name. You probably need to create
the directory or change the file name.
```
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
sudo mkdir /run/oddmu
```
The unit file for the service defines where the "oddmu" is and where the data
directory is. These are the lines you most likely have to take care of:
```
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
```
To install, enable and start both units:
```
sudo systemctl enable --now ./oddmu-unix-domain.socket
sudo systemctl enable --now ./oddmu-unix-domain.service
```
To test just the unix domain socket, use _ncat(1)_:
```
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/oddmu/oddmu.sock
```
Now you need to set up your web browser to use the Unix domain socket. See
_oddmu-apache_(5) or _oddmu-nginx_(5) for example configurations.
## A personal wiki
On a single user machine, it might be useful to have a single wiki for the main
user available. In order to do this, setup a "user" unit using systemd and save
the following as "user-unix-domain.service":
```
[Unit]
Description=Oddmu
After=network.target
[Install]
WantedBy=default.target
[Service]
Type=simple
Restart=always
StandardOutput=journal
StandardError=journal
ExecStart=/home/alex/src/oddmu/oddmu
WorkingDirectory=/home/alex/wiki
Environment="ODDMU_LANGUAGES=de,en"
```
Since this is a user service, the same user can edit the files using their
favourite text editor.
Install it:
```
systemctl --user enable --now ./user-unix-domain.service
```
To examine the log:
```
journalctl --user --unit user-unix-domain.service
```
Note that no sudo is required!
## Using the priviledged port 80
When running a personal wiki, you can have the oddmu binary listen on port 80,
the standard HTTP port. It is not really worth the effort: It means that you can
visit "http://localhost/" instead of "http://localhost:8080". Nevertheless, if
you're interested in giving it a try, here's how to do it.
The service definition must specify the new port:
Environment="ODDMU_PORT=80"
Since this is a privileged port, the binary needs an extra capability for an
ordinary user to do this.
```
sudo setcap 'cap_net_bind_service=+ep' oddmu
```
Note that as soon as you recompile, the capability is gone again and the above
must be repeated.
# SECURITY
Only allow direct access to Oddmu on systems and networks where you're OK with
every user editing the pages. On the open web, this is not true. If your server
is on the open web, always run Oddmu behind a regular web server acting as a
reverse proxy, limiting regular visitors to read-only access. This means that
the regular web server listens on the regular privileged ports (80 for HTTP,
443 for HTTPS) and passes requests to Oddmu on some other port.
# SEE ALSO
_oddmu_(1), _systemd.exec_(5), _systemd.socket(5), _capabilities_(7)
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
_systemd.socket_(5), _capabilities_(7)
# AUTHORS

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

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

View File

@@ -2,9 +2,13 @@ package main
import (
"github.com/stretchr/testify/assert"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"testing"
)
@@ -60,3 +64,34 @@ func TestReadme(t *testing.T) {
return nil
})
}
func TestDocumentDependencies(t *testing.T) {
b, err := os.ReadFile("README.md")
readme := string(b)
assert.NoError(t, err)
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, ".", nil, parser.ImportsOnly)
assert.NoError(t, err)
imports := []string{}
for _, pkg := range pkgs {
for _, file := range pkg.Files {
for _, imp := range file.Imports {
name := imp.Path.Value[1 : len(imp.Path.Value)-1]
if strings.Contains(name, ".") && !slices.Contains(imports, name) {
imports = append(imports, name)
}
}
}
}
sort.Slice(imports, func(i, j int) bool { return len(imports[i]) < len(imports[j]) })
IMPORT:
for _, name := range imports {
for _, other := range imports {
if strings.HasPrefix(name, other) && name != other {
continue IMPORT
}
}
ok := strings.Contains(readme, name)
assert.True(t, ok, name)
}
}

View File

@@ -8,10 +8,9 @@ import (
"github.com/gomarkdown/markdown/ast"
"github.com/google/subcommands"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"
"path"
"strings"
)
@@ -22,7 +21,10 @@ func (*missingCmd) Name() string { return "missing" }
func (*missingCmd) Synopsis() string { return "list missing pages" }
func (*missingCmd) Usage() string {
return `missing:
Listing pages with links to missing pages.
Listing pages with links to missing pages. This command does not
understand links to directories being redirected to index pages.
A link such as [up](..) is reported as a link to a missing page.
Rewrite it as [up](../index) for it to work as intended.
`
}
@@ -30,43 +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, f.Args())
return missingCli(os.Stdout, &index)
}
func missingCli(w io.Writer, args []string) subcommands.ExitStatus {
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
})
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() {
@@ -82,13 +60,17 @@ func missingCli(w io.Writer, args []string) 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")
@@ -114,7 +96,18 @@ func (p *Page) links() []string {
if entering {
switch v := node.(type) {
case *ast.Link:
links = append(links, string(v.Destination))
link := string(v.Destination)
url, err := url.Parse(link)
if err != nil {
// no error reporting
return ast.GoToNext
}
if url.IsAbs() {
links = append(links, link)
} else {
dir := p.Dir()
links = append(links, path.Join(dir, link))
}
}
}
return ast.GoToNext

View File

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

View File

@@ -34,12 +34,12 @@ func notifyCli(w io.Writer, args []string) subcommands.ExitStatus {
for _, name := range args {
p, err := loadPage(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
fmt.Fprintf(w, "Loading %s: %s\n", name, err)
return subcommands.ExitFailure
}
err = p.notify()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", name, err)
fmt.Fprintf(w, "%s: %s\n", name, err)
return subcommands.ExitFailure
}
}

View File

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

30
oddmu.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

63
page.go
View File

@@ -14,22 +14,26 @@ import (
"time"
)
// Page is a struct containing information about a single page. Title
// is the title extracted from the page content using titleRegexp.
// Name is the path without extension (so a path of "foo.md"
// results in the Name "foo"). Body is the Markdown content of the
// page and Html is the rendered HTML for that Markdown. Score is a
// number indicating how well the page matched for a search query.
// Page is a struct containing information about a single page. Title is the title extracted from the page content using
// titleRegexp. Name is the path without extension (so a path of "foo.md" results in the Name "foo"). Body is the
// Markdown content of the page and Html is the rendered HTML for that Markdown.
type Page struct {
Title string
Name string
Language string
Body []byte
Html template.HTML
Score int
Hashtags []string
}
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
// the Name "foo").
type Link struct {
Title string
Url string
}
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
// (YYYY-MM-DD), then it's a blog page.
var blogRe = regexp.MustCompile(`^\d\d\d\d-\d\d-\d\d`)
// santizeStrict uses bluemonday to sanitize the HTML away. No elements are allowed except for the b tag because this is
@@ -109,7 +113,7 @@ func loadPage(path string) (*Page, error) {
if err != nil {
return nil, err
}
return &Page{Title: path, Name: path, Body: body, Language: ""}, nil
return &Page{Title: path, Name: path, 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
@@ -126,22 +130,15 @@ func (p *Page) handleTitle(replace bool) {
}
}
// score sets Page.Title and computes Page.Score.
func (p *Page) score(q string) {
p.handleTitle(true)
p.Score = score(q, string(p.Body)) + score(q, p.Title)
}
// summarize sets Page.Html to an extract and sets Page.Language.
// 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))
p.Language = language(t)
}
// isBlog returns true if the page name starts with an ISO date
func (p *Page) isBlog() bool {
// IsBlog returns true if the page name starts with an ISO date
func (p *Page) IsBlog() bool {
name := path.Base(p.Name)
return blogRe.MatchString(name)
}
@@ -156,8 +153,8 @@ func (p *Page) Dir() string {
return d + "/"
}
// Base returns the basename of the page name: no directory and no extension. This is used to create the upload link
// in "view.html", for example.
// Base returns the basename of the page name: no directory. This is used to create the upload link in "view.html", for
// example.
func (p *Page) Base() string {
n := filepath.Base(p.Name)
if n == "." {
@@ -170,3 +167,27 @@ func (p *Page) Base() string {
func (p *Page) Today() string {
return time.Now().Format(time.DateOnly)
}
// Parents returns a Link array to parent pages, up the directory structure.
func (p *Page) Parents() []*Link {
links := make([]*Link, 0)
index.RLock()
defer index.RUnlock()
// foo/bar/baz ⇒ index, foo/index
elems := strings.Split(p.Name, "/")
if len(elems) == 1 {
return links
}
s := ""
for i := 0; i < len(elems)-1; i++ {
name := s + "index"
title, ok := index.titles[name]
if !ok {
title = "…"
}
link := &Link{Title: title, Url: strings.Repeat("../", len(elems)-i-1) + "index"}
links = append(links, link)
s += elems[i] + "/"
}
return links
}

View File

@@ -40,3 +40,30 @@ Moonlight floods the aisle`)}
// But the backup still exists.
assert.FileExists(t, "testdata/dir/moon.md~")
}
func TestPageParents(t *testing.T) {
cleanup(t, "testdata/parents")
index.load()
p := &Page{Name: "testdata/parents/index", Body: []byte(`# Solar
The air dances here
Water puddles flicker and
disappear anon`)}
p.save()
p = &Page{Name: "testdata/parents/children/index", Body: []byte(`# Lunar
Behind running clouds
Shines cold light from ages past
And untouchable`)}
p.save()
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, "../../../../index", parents[0].Url)
assert.Equal(t, "…", parents[1].Title)
assert.Equal(t, "../../../index", parents[1].Url)
assert.Equal(t, "Solar", parents[2].Title)
assert.Equal(t, "../../index", parents[2].Url)
assert.Equal(t, "Lunar", parents[3].Title)
assert.Equal(t, "../index", parents[3].Url)
assert.Equal(t, 4, len(parents))
}

View File

@@ -7,12 +7,14 @@ import (
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"net/url"
"path"
"path/filepath"
)
// wikiLink returns an inline parser function. This indirection is
// required because we want to call the previous definition in case
// this is not a wikiLink.
func wikiLink(p *parser.Parser, fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
func wikiLink(fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
return func(p *parser.Parser, original []byte, offset int) (int, ast.Node) {
data := original[offset:]
n := len(data)
@@ -44,7 +46,7 @@ func hashtag() (func(p *parser.Parser, data []byte, offset int) (int, ast.Node),
for i < n && !parser.IsSpace(data[i]) {
i++
}
if i == 0 {
if i <= 1 {
return 0, nil
}
hashtags = append(hashtags, string(data[1:i]))
@@ -61,10 +63,10 @@ func hashtag() (func(p *parser.Parser, data []byte, offset int) (int, ast.Node),
// wikiParser returns a parser with the Oddmu specific changes. Specifically: [[wiki links]], #hash_tags,
// @webfinger@accounts. It also uses the CommonExtensions and Block Attributes, and no MathJax ($).
func wikiParser() (*parser.Parser, *[]string) {
extensions := (parser.CommonExtensions | parser.Attributes) & ^parser.MathJax
extensions := (parser.CommonExtensions | parser.AutoHeadingIDs | parser.Attributes) & ^parser.MathJax
parser := parser.NewWithExtensions(extensions)
prev := parser.RegisterInline('[', nil)
parser.RegisterInline('[', wikiLink(parser, prev))
parser.RegisterInline('[', wikiLink(prev))
fn, hashtags := hashtag()
parser.RegisterInline('#', fn)
if useWebfinger {
@@ -73,24 +75,23 @@ func wikiParser() (*parser.Parser, *[]string) {
return parser, hashtags
}
// wikiRenderer is a Renderer for Markdown that adds lazy loading of images. This in turn requires an exception for the
// sanitization policy!
// wikiRenderer is a Renderer for Markdown that adds lazy loading of images and disables fractions support. Remember
// that there is no HTML sanitization.
func wikiRenderer() *html.Renderer {
htmlFlags := html.CommonFlags | html.LazyLoadImages
// sync with staticPage
htmlFlags := html.CommonFlags & ^html.SmartypantsFractions | html.LazyLoadImages
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return renderer
}
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Language, Page.Hashtags, and escapes Page.Name.
// Note: If the rendered HTML doesn't contain the attributes or elements you expect it to contain, check sanitizeBytes!
// 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.Language = language(p.plainText())
p.Hashtags = *hashtags
}
@@ -120,3 +121,45 @@ func (p *Page) plainText() string {
}
return string(text)
}
// images returns an array of ImageData.
func (p *Page) images() []ImageData {
dir := path.Dir(filepath.ToSlash(p.Name))
images := make([]ImageData, 0)
parser := parser.New()
doc := markdown.Parse(p.Body, parser)
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
if entering {
switch v := node.(type) {
case *ast.Image:
// not an absolute URL, not a full URL, not a mailto: URI
text := toString(v)
if len(text) > 0 {
name := path.Join(dir, string(v.Destination))
image := ImageData{Title: text, Name: name}
images = append(images, image)
}
return ast.SkipChildren
}
}
return ast.GoToNext
})
return images
}
// 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.
func toString(node ast.Node) string {
b := new(bytes.Buffer)
ast.WalkFunc(node, func(node ast.Node, entering bool) ast.WalkStatus {
if entering {
switch v := node.(type) {
case *ast.Text:
b.Write(v.Literal)
}
}
return ast.GoToNext
})
return b.String()
}

View File

@@ -20,7 +20,7 @@ Silver leaves shine bright
They droop, boneless, weak and sad
A cruel sun stares down`)}
p.renderHtml()
r := `<h1>Sun</h1>
r := `<h1 id="sun">Sun</h1>
<p>Silver leaves shine bright
They droop, boneless, weak and sad
@@ -37,7 +37,7 @@ I am cold, alone
#Haiku #Cold_Poets`)}
p.renderHtml()
r := `<h1>Comet</h1>
r := `<h1 id="comet">Comet</h1>
<p>Stars flicker above
Too faint to focus, so far
@@ -48,13 +48,25 @@ I am cold, alone</p>
assert.Equal(t, r, string(p.Html))
}
func TestPageHtmlHashtagCornerCases(t *testing.T) {
p := &Page{Body: []byte(`#
ok # #o #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>
`
assert.Equal(t, r, string(p.Html))
}
func TestPageHtmlWikiLink(t *testing.T) {
p := &Page{Body: []byte(`# Photos and Books
Blue and green and black
Sky and grass and [ragged cliffs](cliffs)
Our [[time together]]`)}
p.renderHtml()
r := `<h1>Photos and Books</h1>
r := `<h1 id="photos-and-books">Photos and Books</h1>
<p>Blue and green and black
Sky and grass and <a href="cliffs">ragged cliffs</a>
@@ -69,7 +81,7 @@ Dragonfly hovers
darts chases turns lands and rests
A mighty jewel`)}
p.renderHtml()
r := `<h1>No $dollar$ can buy this</h1>
r := `<h1 id="no-dollar-can-buy-this">No $dollar$ can buy this</h1>
<p>Dragonfly hovers
darts chases turns lands and rests
@@ -83,3 +95,17 @@ func TestLazyLoadImages(t *testing.T) {
p.renderHtml()
assert.Contains(t, string(p.Html), "lazy")
}
// The fractions available in Latin 1 (?) are rendered.
func TestFractions(t *testing.T) {
p := &Page{Body: []byte(`1/4`)}
p.renderHtml()
assert.Contains(t, string(p.Html), "&frac14;")
}
// Other fractions are not rendered.
func TestNoFractions(t *testing.T) {
p := &Page{Body: []byte(`1/6`)}
p.renderHtml()
assert.Contains(t, string(p.Html), "1/6")
}

20
preview.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"net/http"
"strings"
)
// previewHandler is a bit like saveHandler and viewHandler. Instead of saving the date to a page, we create a synthetic
// Page and render it. Note that when saving, the carriage returns (\r) are removed. We need to do this as well,
// otherwise the rendered template has garbage bytes at the end. Note also that we need to remove the title from the
// page so that the preview works as intended (and much like the "view.html" template) where as the editing requires the
// page content including the header… which is why it needs to be added in the "preview.html" template. This makes me
// sad.
func previewHandler(w http.ResponseWriter, r *http.Request, path string) {
body := strings.ReplaceAll(r.FormValue("body"), "\r", "")
p := &Page{Name: path, Body: []byte(body)}
p.handleTitle(true)
p.renderHtml()
renderTemplate(w, p.Dir(), "preview", p)
}

39
preview.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>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; }
footer { border-top: 1px solid #888 }
img { max-width: 100%; }
</style>
</head>
<body>
<header>
<a href="#edit">Skip to edit form</a>
</header>
<main>
<h1>Previewing {{.Title}}</h1>
{{.Html}}
</main>
<hr>
<section id="edit">
<h2>Editing {{.Title}}</h2>
<form action="/save/{{.Name}}" 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>
</form>
</section>
</body>
</html>

View File

@@ -92,13 +92,16 @@ func TestScoreSubstring(t *testing.T) {
func TestScorePageAndMarkup(t *testing.T) {
s := `The Transjovian Council accepts new members. If you think we'd be a good fit, apply for an account. Contact [Alex Schroeder](https://alexschroeder.ch/wiki/Contact). Mail is best. Encrypted mail is best. [Delta Chat](https://delta.chat/de/) is a messenger app that uses encrypted mail. It's the bestest best.`
p := &Page{Title: "Test", Name: "Test", Body: []byte(s)}
r := &Result{}
r.Title = "Test"
r.Name = "Test"
r.Body = []byte(s)
q := "wiki"
p.score(q)
r.score(q)
// "wiki" is not visible in the plain text but the score is no affected:
// - wiki, all, whole, beginning, end (5)
if p.Score != 5 {
t.Logf("%s score is %d", q, p.Score)
if r.Score != 5 {
t.Logf("%s score is %d", q, r.Score)
t.Fail()
}
}

View File

@@ -1,6 +1,7 @@
package main
import (
"html/template"
"log"
"net/http"
"os"
@@ -13,6 +14,14 @@ import (
"unicode/utf8"
)
// Result is a page plus image data. Page is the page being used as the search result. Score is a number indicating how
// well the page matched for a search query. Images are the images whose description match the query.
type Result struct {
Page
Score int
Images []ImageData
}
// Search is a struct containing the result of a search. Query is the
// query string and Items is the array of pages with the result.
// Currently there is no pagination of results! When a page is part of
@@ -20,7 +29,7 @@ import (
type Search struct {
Query string
Dir string
Items []*Page
Items []*Result
Previous int
Page int
Next int
@@ -94,9 +103,9 @@ const itemsPerPage = 20
// size is 20. Specify either the page number to return, or that all the results should be returned. Only ask for all
// results if runtime is not an issue, like on the command line. The boolean return value indicates whether there are
// more results.
func search(q, dir, filter string, page int, all bool) ([]*Page, bool) {
func search(q, dir, filter string, page int, all bool) ([]*Result, bool) {
if len(q) == 0 {
return make([]*Page, 0), false
return make([]*Result, 0), false
}
names := index.search(q) // hashtags or all names
names = filterPath(names, dir, filter)
@@ -104,16 +113,51 @@ func search(q, dir, filter string, page int, all bool) ([]*Page, bool) {
names = filterNames(names, predicates)
index.RLock()
slices.SortFunc(names, sortNames(terms))
index.RUnlock()
index.RUnlock() // unlock because grep takes long
names, keepFirst := prependQueryPage(names, dir, q)
from := itemsPerPage * (page - 1)
to := from + itemsPerPage - 1
items, more := grep(terms, names, from, to, all, keepFirst)
for _, p := range items {
p.score(q)
p.summarize(q)
results := make([]*Result, len(items))
for i, p := range items {
r := &Result{}
r.Title = p.Title
r.Name = p.Name
r.Body = p.Body
// Hashtags aren't computed and Html is getting overwritten anyway
r.summarize(q)
r.score(q)
results[i] = r
}
return items, more
if len(terms) > 0 {
index.RLock()
for _, r := range results {
res := make([]ImageData, 0)
ImageLoop:
for _, img := range index.images[r.Name] {
title := strings.ToLower(img.Title)
for _, term := range terms {
if strings.Contains(title, term) {
re, err := re(term)
if err == nil {
img.Html = template.HTML(highlight(re, img.Title))
}
res = append(res, img)
continue ImageLoop
}
}
}
r.Images = res
}
index.RUnlock()
}
return results, more
}
// score sets Page.Title and computes Page.Score.
func (r *Result) score(q string) {
r.handleTitle(true)
r.Score = score(q, string(r.Body)) + score(q, r.Title)
}
// filterPath filters the names by prefix and by a regular expression. A prefix of "." means that all the names are

View File

@@ -12,9 +12,10 @@ 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; }
img { max-width: 20%; }
.result { font-size: larger }
.score { font-size: smaller; opacity: 0.8; }
.image { display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); font-size: small; }
.image img { max-width: 100%; }
</style>
</head>
<body>
@@ -40,6 +41,9 @@ img { max-width: 20%; }
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
<span class="score">{{.Score}}</span></p>
<blockquote>{{.Html}}</blockquote>
{{range .Images}}
<p class="image"><a href="/view/{{.Name}}"><img loading="lazy" src="/view/{{.Name}}"></a><br/>{{.Html}}
{{end}}
</article>
{{end}}
<p>

View File

@@ -4,9 +4,10 @@ import (
"context"
"flag"
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/google/subcommands"
"github.com/muesli/reflow/wordwrap"
"io"
"net/url"
"os"
"regexp"
"strings"
@@ -27,7 +28,7 @@ func (cmd *searchCmd) SetFlags(f *flag.FlagSet) {
}
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>:
Search for pages matching terms and print the result set as a
@@ -82,17 +83,28 @@ 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 []*Page) {
heading := lipgloss.NewStyle().Bold(true).Underline(true)
quote := lipgloss.NewStyle().PaddingLeft(4).Width(78)
match := lipgloss.NewStyle().Bold(true)
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
re := regexp.MustCompile(`<b>(.*?)</b>`)
for _, p := range items {
s := re.ReplaceAllString(string(p.Html), match.Render(`$1`))
fmt.Fprintln(w, heading.Render(p.Title))
s := re.ReplaceAllString(string(p.Html), match(`$1`))
fmt.Fprintln(w, heading(p.Title))
if p.Name != p.Title {
fmt.Fprintln(w, p.Name)
}
fmt.Fprintln(w, quote.Render(s))
for _, s := range strings.Split(wordwrap.String(s, 72), "\n") {
fmt.Fprintln(w, " ", s)
}
for _, img := range p.Images {
name, err := url.PathUnescape(img.Name)
if err != nil {
name = img.Name
}
fmt.Fprintln(w, " - ", name)
for _, s := range strings.Split(wordwrap.String(img.Title, 70), "\n") {
fmt.Fprintln(w, " ", s)
}
}
}
}

View File

@@ -12,6 +12,7 @@ func TestSearchCmd(t *testing.T) {
s := searchCli(b, "", 1, false, false, true, []string{"oddµ"})
assert.Equal(t, subcommands.ExitSuccess, s)
r := `* [Oddµ: A minimal wiki](README)
* [Themes](themes/index)
* [Welcome to Oddµ](index)
`
assert.Equal(t, r, b.String())

View File

@@ -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) {
@@ -227,6 +231,42 @@ A quick sip too quick
assert.Equal(t, "Tea", items[1].Title, items[1].Name)
}
func TestImageSearch(t *testing.T) {
cleanup(t, "testdata/images")
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# 2024-07-21 Pictures
![phone call](2024-07-21.jpg)
Pictures in the box
Tiny windows to our past
Where are you, my love?
`)}
p.save()
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) {
cleanup(t, "testdata/question")
p := &Page{Name: "testdata/question/Odd?", Body: []byte(`# Even?

View File

@@ -35,7 +35,7 @@ func snippets(q string, s string) string {
}
// Short cut for short pages
if len(s) <= snippetlen {
return highlight(q, re, s)
return highlight(re, s)
}
// show a snippet from the beginning of the document
j := strings.LastIndex(s[:snippetlen], " ")
@@ -47,7 +47,7 @@ func snippets(q string, s string) string {
if len(s) > 400 {
s = s[0:400] + " …"
}
return highlight(q, re, s)
return highlight(re, s)
}
}
t := s[0:j]
@@ -98,5 +98,5 @@ func snippets(q string, s string) string {
s = s[end:]
}
}
return highlight(q, re, res)
return highlight(re, res)
}

View File

@@ -13,47 +13,63 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
type staticCmd struct {
jobs int
}
func (cmd *staticCmd) SetFlags(f *flag.FlagSet) {
f.IntVar(&cmd.jobs, "jobs", 2, "how many jobs to use")
}
func (*staticCmd) Name() string { return "static" }
func (*staticCmd) Synopsis() string { return "generate static HTML files for all pages" }
func (*staticCmd) Usage() string {
return `static <dir name>:
Create static copies in the given directory.
return `static [-jobs n] <dir name>:
Create static copies in the given directory. Per default, two jobs
are used to read and write files, but more can be assigned.
`
}
func (cmd *staticCmd) SetFlags(f *flag.FlagSet) {
}
func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
args := f.Args()
if len(args) != 1 {
fmt.Println("Exactly one target directory is required")
fmt.Fprintln(os.Stderr, "Exactly one target directory is required")
return subcommands.ExitFailure
}
return staticCli(filepath.Clean(args[0]), false)
dir := filepath.Clean(args[0])
return staticCli(".", dir, cmd.jobs, false)
}
type args struct {
source, target string
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, 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()
loadLanguages()
loadTemplates()
n := 0
err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
n++
if !quiet && (n < 100 || n < 1000 && n%10 == 0 || n%100 == 0) {
fmt.Fprintf(os.Stdout, "\r%d", n)
}
return staticFile(path, dir, info, err)
})
tasks := make(chan args)
results := make(chan error)
done := make(chan bool)
stop := make(chan error)
for i := 0; i < jobs; i++ {
go staticWorker(tasks, results, done)
}
go staticWalk(source, target, tasks, stop)
go staticWatch(jobs, results, done)
n, err := staticProgressIndicator(results, stop, quiet)
if !quiet {
fmt.Printf("\r%d\n", n)
fmt.Printf("\r%d files processed\n", n)
}
if err != nil {
fmt.Println(err)
@@ -62,43 +78,127 @@ func staticCli(dir string, quiet bool) subcommands.ExitStatus {
return subcommands.ExitSuccess
}
// staticFile is used to walk the file trees and do the right thing for the destination directory: create
// subdirectories, link files, render HTML files.
func staticFile(path, dir 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 {
// 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(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(source, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
select {
case err := <-stop:
return err
default:
base := filepath.Base(path)
// skip hidden directories and files
if path != "." && strings.HasPrefix(base, ".") {
if info.IsDir() {
return filepath.SkipDir
} else {
return nil
}
}
// skip backup files, avoid recursion
if strings.HasSuffix(path, "~") || strings.HasPrefix(path, 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 actual_target string
if source == "." {
actual_target = filepath.Join(target, path)
} else {
if !strings.HasPrefix(path, source) {
return fmt.Errorf("%s is not a subdirectory of %s", path, source)
}
actual_target = filepath.Join(target, path[len(source):])
}
// recreate subdirectories
if info.IsDir() {
return os.Mkdir(actual_target, 0755)
}
// do the task if the target file doesn't exist or if the source file is newer
other, err := os.Stat(actual_target)
if err != nil || info.ModTime().After(other.ModTime()) {
tasks <- args{source: path, target: actual_target, info: info}
}
return nil
}
})
close(tasks)
}
// 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)) {
for i := 0; i < jobs; i++ {
<-done
}
// skip backup files, avoid recursion
if strings.HasSuffix(path, "~") || strings.HasPrefix(path, dir) {
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
for ok {
results <- staticFile(task.source, task.target, task.info)
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) {
n := 0
t := time.Now()
var err error
for result := range results {
if result != nil {
err := result
// this stops the walker from adding more tasks
stop <- err
} else {
n++
if !quiet && n%13 == 0 {
if time.Since(t) > time.Second {
fmt.Printf("\r%d", n)
t = time.Now()
}
}
}
}
return n, err
}
// staticFile is used to walk the file trees and do the right thing for the destination directory: create
// subdirectories, link files, render HTML files.
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")
if err != nil {
return err
}
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)) {
return nil
}
// recreate subdirectories
if info.IsDir() {
return os.Mkdir(filepath.Join(dir, path), 0755)
}
// render pages
if strings.HasSuffix(path, ".md") {
return staticPage(path, dir)
}
// remaining files are linked
return os.Link(path, filepath.Join(dir, path))
return os.Link(source, target)
}
// staticPage takes the filename of a page (ending in ".md") and generates a static HTML page.
func staticPage(path, dir string) error {
name := strings.TrimSuffix(path, ".md")
p, err := loadPage(filepath.ToSlash(name))
func staticPage(source, target string) (*Page, error) {
p, err := loadPage(filepath.ToSlash(source))
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", name, err)
return err
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", source, err)
return nil, err
}
p.handleTitle(true)
// instead of p.renderHtml() we do it all ourselves, appending ".html" to all the local links
@@ -106,15 +206,29 @@ func staticPage(path, dir string) error {
doc := markdown.Parse(p.Body, parser)
ast.WalkFunc(doc, staticLinks)
opts := html.RendererOptions{
Flags: html.CommonFlags,
// sync with wikiRenderer
Flags: html.CommonFlags & ^html.SmartypantsFractions | html.LazyLoadImages,
}
renderer := html.NewRenderer(opts)
maybeUnsafeHTML := markdown.Render(doc, renderer)
p.Name = nameEscape(p.Name)
p.Html = unsafeBytes(maybeUnsafeHTML)
p.Language = language(p.plainText())
p.Hashtags = *hashtags
return p.write(filepath.Join(dir, name+".html"))
return p, write(p, target, "", "static.html")
}
// staticFeed writes a .rss file for a page, but only if it's an index page or a page that might be used as a hashtag
func staticFeed(source, target string, p *Page, ti time.Time) error {
// render feed, maybe
base := filepath.Base(source)
_, ok := index.token["#"+strings.ToLower(base)]
if base == "index" || ok {
f := feed(p, ti)
if len(f.Items) > 0 {
return write(f, target, `<?xml version="1.0" encoding="UTF-8"?>`, "feed.html")
}
}
return nil
}
// staticLinks checks a node and if it is a link to a local page, it appends ".html" to the link destination.
@@ -142,16 +256,23 @@ func staticLinks(node ast.Node, entering bool) ast.WalkStatus {
return ast.GoToNext
}
func (p *Page) write(destination string) error {
t := "static.html"
f, err := os.Create(destination)
// 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)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot create %s.html: %s\n", destination, err)
fmt.Fprintf(os.Stderr, "Cannot create %s: %s\n", path, err)
return err
}
err = templates.template[t].Execute(f, p)
_, err = file.Write([]byte(prefix))
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", t, destination, err)
fmt.Fprintf(os.Stderr, "Cannot write prefix %s: %s\n", path, 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)
return err
}
return nil

View File

@@ -3,12 +3,13 @@ package main
import (
"github.com/google/subcommands"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestStatusCmd(t *testing.T) {
func TestStaticCmd(t *testing.T) {
cleanup(t, "testdata/static")
s := staticCli("testdata/static", true)
s := staticCli(".", "testdata/static", 2, true)
assert.Equal(t, subcommands.ExitSuccess, s)
// pages
assert.FileExists(t, "testdata/static/index.html")
@@ -17,3 +18,27 @@ func TestStatusCmd(t *testing.T) {
assert.FileExists(t, "testdata/static/static_cmd.go")
assert.FileExists(t, "testdata/static/static_cmd_test.go")
}
func TestFeedStaticCmd(t *testing.T) {
cleanup(t, "testdata/static-feed")
cleanup(t, "testdata/static-feed-out")
p := &Page{Name: "testdata/static-feed/Haiku", Body: []byte("# Haiku\n")}
p.save()
h := &Page{Name: "testdata/static-feed/2024-03-07-poem",
Body: []byte(`# Rain
I cannot hear you
The birds outside are singing
And the cars so loud
#Haiku
`)}
h.save()
h.notify()
s := staticCli("testdata/static-feed", "testdata/static-feed-out", 2, true)
assert.Equal(t, subcommands.ExitSuccess, s)
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")
assert.NoError(t, err)
assert.Contains(t, string(b), "<channel>")
}

View File

@@ -14,7 +14,7 @@ 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",
var templateFiles = []string{"edit.html", "add.html", "view.html", "preview.html",
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See

View File

@@ -18,7 +18,7 @@ Memories of cold
`)}
p.save()
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/templates/snow", nil),
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/templates/snow", nil),
"Skip navigation")
// save a new view handler
html := "<body><h1>{{.Title}}</h1>{{.Html}}"
@@ -40,11 +40,11 @@ Memories of cold
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/templates/view.html", nil),
html)
// verify that it works
body := assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/templates/snow", nil)
body := assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/templates/snow", nil)
assert.Contains(t, body, "<h1>Snow</h1>")
assert.NotContains(t, body, "Skip")
// verify that the top level still uses the old template
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/index", nil),
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/index", nil),
"Skip navigation")
}

11
themes/Makefile Normal file
View File

@@ -0,0 +1,11 @@
download:
rsync --archive --delete --itemize-changes --exclude='*-*' sibirocobombus:alexschroeder.ch/wiki/'*.html' alexschroeder.ch/
rsync --archive sibirocobombus:alexschroeder.ch/css/oddmu-2023.css alexschroeder.ch/oddmu.css
sed --in-place=~ --expression='s/\/css\/oddmu-2023\.css/oddmu.css/' alexschroeder.ch/*.html
rsync --archive --delete --itemize-changes sibirocobombus:flying-carpet.ch/wiki/'*.html' flying-carpet.ch/
rsync --archive --delete --itemize-changes sibirocobombus:campaignwiki.org/data/'*.html' campaignwiki.org/
rsync --archive --delete --itemize-changes sibirocobombus.root:/home/oddmu/'*.html' transjovian.org/
upload:
rsync --archive --delete --itemize-changes --exclude=Makefile --exclude='*~' . sibirocobombus:alexschroeder.ch/wiki/oddmu/themes/

View File

@@ -0,0 +1,13 @@
# Alex Schroeder theme
At the top there's a text input to quickly create new pages.
This theme comes with an external CSS file. If you plan to use
subdirectories for your site, you need to change the URL of the CSS in
the HTML templates to `/view/oddmu.css` or serve it as a static file
from a `/css` directory.
The CSS switches between light and dark mode based on the visitor's
setup.
(Back up to the [list of themes](../index).)

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Add to {{.Title}}</title>
<link href="oddmu.css" rel="stylesheet" />
</head>
<body>
<h1>Adding to {{.Title}}</h1>
<form action="/append/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="{{.Language}}" autofocus required>{{if .IsBlog}}**{{.Today}}**. {{end}}</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>
</form>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<link href="oddmu.css" rel="stylesheet" />
</head>
<body>
<header>
<a href="/view/{{.Name}}">Back</a>
</header>
<main id="main">
<h1>{{.Title}}</h1>
<p>This is the diff between <a href="/view/{{.Name}}.md~">the backup</a> and <a href="/view/{{.Name}}.md">the current copy</a>.</p>
<pre class="diff">
{{.Diff}}
</pre>
</main>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Editing {{.Title}}</title>
<link href="oddmu.css" rel="stylesheet" />
</head>
<body>
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="# Title
Text" lang="{{.Language}}" autofocus>{{ or .Body (printf "# %s " .Today) | printf "%s" }}</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="Save">
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
</form>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<title>{{.Title}}</title>
<link>https://alexschroeder.ch/</link>
<managingEditor>alex@alexschroeder.ch (Alex Schroeder)</managingEditor>
<webMaster>alex@alexschroeder.ch (Alex Schroeder)</webMaster>
<atom:link href="https://alexschroeder.ch/view/{{.Name}}.rss" rel="self" type="application/rss+xml"/>
<description>This is the digital garden of Alex Schroeder.</description>
<image>
<url>https://alexschroeder.ch/view/logo.jpg</url>
<title>{{.Title}}</title>
<link>https://alexschroeder.ch/</link>
</image>
{{range .Items}}
<item>
<title>{{.Title}}</title>
<link>https://alexschroeder.ch/view/{{.Name}}</link>
<guid>https://alexschroeder.ch/view/{{.Name}}</guid>
<description>{{.Html}}</description>
<pubDate>{{.Date}}</pubDate>
{{range .Hashtags}}
<category>{{.}}</category>
{{end}}
</item>
{{end}}
</channel>
</rss>

View File

@@ -0,0 +1,47 @@
html { max-width: 80ch; padding: 1ch; margin: auto }
body { hyphens: auto; color: #111; background-color: #ffe }
header a { margin-right: 1ch }
h1 { text-wrap: balance }
footer { border-top: 1px solid #888 }
form, textarea { width: 97%; font-size: inherit }
pre { background-color: #fff; padding: 1ex 2ex; overflow-x: auto }
.diff { font-size: inherit; white-space: normal; overflow-wrap: break-word; background-color: white; border: 1px solid #333; padding: 1ch }
img, video { max-width: 100%; max-height: 90vh; width: auto; height: auto }
.right img { float: right; margin-left: 2em; margin-bottom: 1em; border: 1px solid #111 }
.left img { float: left; margin-right: 2em; margin-bottom: 1em; border: 1px solid #111 }
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px }
del { background-color: #fab }
ins { background-color: #af8 }
#search, #id { max-width: 30ch; width: calc(100% - 23ch) }
.result { font-size: larger }
.score { font-size: smaller; opacity: 0.8 }
.image { font-size: small; display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); }
#view form { margin-top: 2px }
#view button { width: 6ch }
#view label { display: inline-block; width: 10ch }
#upload label { display: inline-block; width: 15ch }
#upload input[type=text] { width: 30ch }
img.last { max-width: 20% }
hr { border-bottom: 1px }
th { font-weight: normal }
th + th, td + td { padding-left: 1em }
@media (prefers-color-scheme: dark) {
body { color: #eeeee8; background-color: #333 }
footer { border-top: 1px solid #666 }
.diff { background-color: inherit; border: 1px solid #666 }
.right img { border: 1px solid #111 }
.left img { border: 1px solid #111 }
pre { background-color: #000; }
button { background-color: #eee; color: inherit }
del { background-color: #f40 }
ins { background-color: #070 }
a:link { color: #1e90ff }
a:hover { color: #63b8ff }
a:visited { color: #7a67ee }
img { opacity: .75; transition: opacity .5s ease-in-out; }
img:hover { opacity: 1; }
input, input[type="text"], textarea, button, .diff {
color: #eeeee8; background-color: #555
}
}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Search for {{.Query}}</title>
<link href="oddmu.css" rel="stylesheet" />
</head>
<body>
<header>
<a href="#main">Skip navigation</a>
<a href="/view/index">Home</a>
<form role="search" action="/search/{{.Dir}}" method="GET">
<label for="search">Search:</label>
<input id="search" type="text" value="{{.Query}}" spellcheck="false" name="q" accesskey="f" placeholder="term #tag title:term blog:true" required>
<button>Go</button>
</form>
</header>
<main id="main">
<h1>Search for {{.Query}}</h1>
{{if .Results}}
<p>
{{if gt .Page 2}}<a href="/search/{{.Dir}}?q={{.Query}}&page=1">First</a>{{end}}
{{if gt .Page 1}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Previous}}">Previous</a>{{end}}
Page {{.Page}}
{{if .More}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Next}}">Next</a>{{end}}
{{range .Items}}
<article lang="{{.Language}}">
<p><a class="result" href="/view/{{.Name}}">{{.Title}}</a>
<span class="score">{{.Score}}</span></p>
<blockquote>{{.Html}}</blockquote>
</article>
{{end}}
<p>
{{if gt .Page 2}}<a href="/search/{{.Dir}}?q={{.Query}}&page=1">First</a>{{end}}
{{if gt .Page 1}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Previous}}">Previous</a>{{end}}
Page {{.Page}}
{{if .More}}<a href="/search/{{.Dir}}?q={{.Query}}&page={{.Next}}">Next</a>{{end}}
{{else}}
<p>No results.</p>
{{end}}
</main>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<style>
html { max-width: 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%; }
</style>
</head>
<body>
<main id="main">
<h1>{{.Title}}</h1>
{{.Html}}
</main>
<footer>
<address>
Comments? Send mail to Alex Schroeder <<a href="mailto:alex@alexschroeder.ch">alex@alexschroeder.ch</a>>
</address>
</footer>
</body>
</html>

View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Upload File</title>
<link href="oddmu.css" rel="stylesheet" />
<script type="text/javascript">
var uploadFiles = {
init: function() {
let e = document.getElementById('upload');
if (e) {
e.addEventListener('paste', uploadFiles.pasteHandler);
e.addEventListener('dragover', e => e.preventDefault());
e.addEventListener('drop', uploadFiles.dropHandler);
}
},
pasteHandler: function(e) {
uploadFiles.handle(e.clipboardData);
},
dropHandler: function(e) {
e.preventDefault();
uploadFiles.handle(e.dataTransfer);
},
handle: function(dataTransfer) {
let files = [];
if (dataTransfer.items) {
[...dataTransfer.items].forEach((item, i) => {
if (item.kind === "file")
files.push(item.getAsFile());
});
} else {
[...dataTransfer.files].forEach((file, i) => {
files.push(file);
});
}
if (files.length)
uploadFiles.post(files)
},
post: function(files) {
let action = document.getElementById('upload').getAttribute('action');
var fd = new FormData();
fd.append("name", document.getElementById('name').value);
fd.append("maxwidth", document.getElementById('maxwidth').value);
fd.append("quality", document.getElementById('quality').value);
for (var i = 0; i < files.length; i++) {
fd.append("file", files[i]);
}
try {
fetch(action, { method: "POST", body: fd })
.then(response => {
if (response.ok) {
window.location = response.url;
} else {
alert(response.text);
}})
} catch (e) {
alert(e);
}
},
};
window.addEventListener('load', uploadFiles.init);
</script>
</head>
<body lang="en">
<h1>Upload Files</h1>
{{if ne .Last ""}}
<p>Previous upload: <a href="/view/{{.Dir}}{{.Last}}">{{.Last}}</a>
{{if .Image}}
<p><img class="last" src="/view/{{.Dir}}{{.Last}}"><br>
<tt>Link: ![]({{.Last}})</tt>
{{else}}
<p>Link: <tt>[text]({{.Last}})</tt>
{{end}}
{{end}}
<form id="upload" action="/drop/{{.Dir}}" method="POST" enctype="multipart/form-data">
<p>When uploading a picture from a phone, its filename is going to be something like IMG_1234.JPG.
Please provide your own filename. End the base name with "-1" to auto-increment.
Use <tt>.jpg</tt> or <tt>.png</tt> as the extension if you want to resize the picture.
<p><label for="name">Filename:</label>
<input id="name" name="name" value="{{.Name}}" type="text" placeholder="image-1.jpg" autofocus required>
<p>If the uploaded file is a picture from a phone, it is going to be too big for your site.
Sadly, resizing only works when uploading <tt>.jpeg</tt>, <tt>.webp</tt>, <tt>.heic</tt> and <tt>.png</tt> files.
Feel free to specify a max width of 1200 pixels, for example.
<p><label for="maxwidth">Max width:</label>
<input id="maxwidth" name="maxwidth" value="{{.MaxWidth}}" type="number" min="10" placeholder="1200">
<p>If the filename you provided above ends in <tt>.jpg</tt>, you can specify a quality.
Typically, a quality of 60 is not too bad and a quality of 90 is more than enough.
<p><label for="quality">Quality:</label>
<input id="quality" name="quality" value="{{.Quality}}" type="number" min="1" max="99" placeholder="75">
<p>Finally, pick the files or photos to upload.
Picture metadata is only removed if the pictures gets resized.
Providing a new max width is recommended for all pictures.
If you're uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
<p>To delete a file, upload an empty file.
<p><label for="file">Files to upload:</label>
<input type="file" name="file" required multiple>
<p><input type="submit" value="Save">
<a href="/view/index"><button type="button">Cancel</button></a>
<p>You can also paste images or drag and drop files.
</form>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<link href="oddmu.css" rel="stylesheet" />
<link rel="alternate" type="application/rss+xml" title="Alex Schroeder: {{.Title}}" href="/view/{{.Name}}.rss" />
</head>
<body>
<header id="view">
<a href="#main">Skip</a>
<a href="index">Home</a>
<a href="changes">Changes</a>
<a href="/edit/{{.Name}}" accesskey="e">Edit</a>
<a href="/add/{{.Name}}" accesskey="a">Add</a>
<a href="/diff/{{.Name}}" accesskey="d">Diff</a>
<a href="/upload/{{.Dir}}?filename={{.Base}}-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>
<form action="/edit/{{.Dir}}" method="GET">
<label for="id">New page:</label>
<input id="id" type="text" spellcheck="false" name="id" accesskey="g" value="{{.Today}}" required>
<button>Edit</button>
</form>
</header>
<main id="main">
<h1>{{.Title}}</h1>
{{.Html}}
</main>
<footer>
<address>
I love getting mail. Send comments to Alex Schroeder &lt;<a href="mailto:alex@alexschroeder.ch">alex@alexschroeder.ch</a>&gt;
</address>
</footer>
</body>
</html>

View File

@@ -0,0 +1,13 @@
# Campaign Wiki theme
A beige theme that sticks close to the default. This site treats every
subdirectory as its own wiki and so there's a "Changes" link to the
local changes files and a "Zip" link to download the current
directory. This is so people can download a backup of their data.
On the real site, the link to the zip file on the top level is
unavailable because of a redirect so that people download only their
own wikis and not all the wikis, for bandwidth reasons. This is part
of the Apache config, however.
(Back up to the [list of themes](../index).)

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Add to {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #eed; }
form, textarea { width: 100%; }
</style>
</head>
<body>
<h1>Adding to {{.Title}}</h1>
<form action="/append/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="" 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>
</form>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<style>
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #eed; }
body { hyphens: auto; }
del { background-color: #fab }
ins { background-color: #af8 }
pre { white-space: normal; background-color: white; border: 1px solid #eee; padding: 1ch }
</style>
</head>
<body>
<header>
<a href="/view/{{.Name}}">Back</a>
</header>
<main id="main">
<h1>{{.Title}}</h1>
<p>This is the diff between <a href="/view/{{.Name}}.md~">the backup</a> and <a href="/view/{{.Name}}.md">the current copy</a>.</p>
<pre>
{{.Diff}}
</pre>
</main>
</body>
</html>

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