forked from mirror/oddmu
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486c3f8620 | ||
|
|
5b0fcdd69f | ||
|
|
bb99d05a0d | ||
|
|
98358a008b | ||
|
|
51c8348ef7 | ||
|
|
5e77f1332e | ||
|
|
fbbb4a543f | ||
|
|
ccc7c0bc8f | ||
|
|
aae2ae1265 | ||
|
|
8929d72acd | ||
|
|
014507ce4e | ||
|
|
554a929bf5 | ||
|
|
5f8e006594 | ||
|
|
e347a59603 | ||
|
|
964dc3bf4a | ||
|
|
d5f8b280ac | ||
|
|
8ee5705ae7 | ||
|
|
43bf1574c9 | ||
|
|
1c8af9fcdb | ||
|
|
f6fa76bd5f | ||
|
|
111c617556 | ||
|
|
66fe28062d | ||
|
|
7e03b67267 | ||
|
|
11343067af | ||
|
|
a0ff3ed03c | ||
|
|
ccead37f44 | ||
|
|
a8b4ec9acd | ||
|
|
2531a469bf | ||
|
|
51808bc1fb | ||
|
|
2375dad845 | ||
|
|
0ca53690d8 | ||
|
|
a0c7517e8a | ||
|
|
912b6baad0 | ||
|
|
b6c068c72f | ||
|
|
89ef292736 | ||
|
|
c658de5a6f | ||
|
|
4bab25e2ac | ||
|
|
c518a193d0 | ||
|
|
2dc950cb5e | ||
|
|
87d1e72f0f |
45
Makefile
45
Makefile
@@ -1,29 +1,27 @@
|
||||
SHELL=/bin/bash
|
||||
PREFIX=${HOME}/.local
|
||||
|
||||
.PHONY: help build test run upload docs install missing
|
||||
.PHONY: help build test run upload docs install priv
|
||||
|
||||
help:
|
||||
@echo Help for Oddmu
|
||||
@echo =====================
|
||||
@echo
|
||||
@echo ==============
|
||||
@echo make run
|
||||
@echo " runs program, offline"
|
||||
@echo
|
||||
@echo make test
|
||||
@echo " runs the tests without log output"
|
||||
@echo
|
||||
@echo make docs
|
||||
@echo " create man pages from text files"
|
||||
@echo
|
||||
@echo make build
|
||||
@echo " just build it"
|
||||
@echo
|
||||
@echo make install
|
||||
@echo " install the files to ~/.local"
|
||||
@echo
|
||||
@echo make upload
|
||||
@echo " this is how I upgrade my server"
|
||||
@echo make dist
|
||||
@echo " cross compile for other systems"
|
||||
@echo make clean
|
||||
@echo " remove built files"
|
||||
|
||||
build: oddmu
|
||||
|
||||
@@ -31,6 +29,7 @@ oddmu: *.go
|
||||
go build
|
||||
|
||||
test:
|
||||
rm -rf testdata/*
|
||||
go test -shuffle on .
|
||||
|
||||
run:
|
||||
@@ -42,18 +41,38 @@ upload: build
|
||||
@echo Changes to the template files need careful consideration
|
||||
|
||||
docs:
|
||||
cd man; make
|
||||
cd man; make man
|
||||
|
||||
install:
|
||||
for n in 1 5 7; do install -D -t ${PREFIX}/share/man/man$$n man/*.$$n; done
|
||||
install -D -t ${PREFIX}/.local/bin oddmu
|
||||
install -D -t ${PREFIX}/bin oddmu
|
||||
|
||||
# More could be added, of course!
|
||||
dist: oddmu-linux-amd64.tar.gz
|
||||
clean:
|
||||
rm --force oddmu oddmu.exe oddmu-{linux,darwin,windows}-{amd64,arm64}{,.tar.gz}
|
||||
cd man && make clean
|
||||
|
||||
dist: oddmu-linux-amd64.tar.gz oddmu-linux-arm64 oddmu-darwin-amd64.tar.gz oddmu-windows-amd64.tar.gz
|
||||
|
||||
oddmu-linux-amd64: *.go
|
||||
GOOS=linux GOARCH=amd64 go build -o $@
|
||||
|
||||
oddmu-linux-arm64: *.go
|
||||
env GOOS=linux GOARCH=arm64 GOARM=5 go build -o $@
|
||||
|
||||
oddmu-darwin-amd64: *.go
|
||||
GOOS=darwin GOARCH=arm64 go build -o $@
|
||||
|
||||
oddmu.exe: *.go
|
||||
GOOS=windows GOARCH=amd64 go build -o $@
|
||||
|
||||
oddmu-windows-amd64.tar.gz: oddmu.exe
|
||||
cd man && make html
|
||||
tar --create --file $@ --transform='s/^/oddmu\//' --exclude='*~' \
|
||||
$< *.md man/*.[157].{html,md} themes/
|
||||
|
||||
%.tar.gz: %
|
||||
tar czf $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
|
||||
tar --create --file $@ --transform='s/^$</oddmu/' --transform='s/^/oddmu\//' --exclude='*~' \
|
||||
$< Makefile *.socket *.service *.md man/Makefile man/*.1 man/*.5 man/*.7 themes/
|
||||
|
||||
priv:
|
||||
sudo setcap 'cap_net_bind_service=+ep' oddmu
|
||||
|
||||
133
README.md
133
README.md
@@ -31,7 +31,7 @@ If your pages don't provide their own title (`# title`), the file name
|
||||
necessary.
|
||||
|
||||
Other files can be uploaded and images (ending in `.jpg`, `.jpeg`,
|
||||
`.png`, `.heic` or `webp`) can be resized when they are uploaded
|
||||
`.png`, `.heic` or `.webp`) can be resized when they are uploaded
|
||||
(resulting in `.jpg` or `.png` files).
|
||||
|
||||
## Documentation
|
||||
@@ -40,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:
|
||||
@@ -301,7 +328,7 @@ in turn can be used by browsers to get hyphenation right. Apache-2.0.
|
||||
is used to sniff the MIME type of files with unknown filename
|
||||
extensions. MIT.
|
||||
|
||||
[github.com/bashdrew/goheif](https://github.com/bashdrew/goheif) is
|
||||
[github.com/gen2brain/heic](https://github.com/gen2brain/heic) is
|
||||
used to decode HEIC files (the new default file format for photos on
|
||||
iPhones). LGPL-3.0-only.
|
||||
|
||||
|
||||
112
export_cmd.go
Normal file
112
export_cmd.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/google/subcommands"
|
||||
htmlTemplate "html/template"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
textTemplate "text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type exportCmd struct {
|
||||
templateName string
|
||||
}
|
||||
|
||||
func (cmd *exportCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&cmd.templateName, "template", "feed.html", "template filename")
|
||||
}
|
||||
|
||||
func (*exportCmd) Name() string { return "export" }
|
||||
func (*exportCmd) Synopsis() string { return "export the whole site as one big RSS feed" }
|
||||
func (*exportCmd) Usage() string {
|
||||
return `export:
|
||||
Export the entire site as one big RSS feed. This may allow you to
|
||||
import the whole site into a different content management system.
|
||||
The feed contains every page, in HTML format, so the Markdown files
|
||||
are part of the feed, but none of the other files.
|
||||
|
||||
The RSS feed is printed to stdout so you probably want to redirect
|
||||
it:
|
||||
|
||||
oddmu export > /tmp/export.rss
|
||||
|
||||
Options:
|
||||
|
||||
-template "filename" specifies the template to use (default: feed.html)
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *exportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
index.load()
|
||||
return exportCli(os.Stdout, cmd.templateName, &index)
|
||||
}
|
||||
|
||||
// exportCli runs the export command on the command line. In order to make testing easier, it takes a Writer and an
|
||||
// indexStore. The Writer is important so that test code can provide a buffer instead of os.Stdout; the indexStore is
|
||||
// important so that test code can ensure no other test running in parallel can interfere with the list of known pages
|
||||
// (by adding or deleting pages).
|
||||
func exportCli(w io.Writer, templateName string, idx *indexStore) subcommands.ExitStatus {
|
||||
loadLanguages()
|
||||
feed := new(Feed)
|
||||
items := []Item{}
|
||||
// feed.Name remains unset
|
||||
feed.Date = time.Now().Format(time.RFC3339)
|
||||
for name, title := range idx.titles {
|
||||
if name == "index" {
|
||||
feed.Title = title
|
||||
}
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
p.handleTitle(false)
|
||||
p.renderHtml()
|
||||
fi, err := os.Stat(name + ".md")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Stat %s: %s\n", name, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
it := Item{Date: fi.ModTime().Format(time.RFC3339)}
|
||||
it.Title = p.Title
|
||||
it.Name = p.Name
|
||||
it.Body = p.Body
|
||||
it.Html = htmlTemplate.HTML(htmlTemplate.HTMLEscaper(p.Html))
|
||||
it.Hashtags = p.Hashtags
|
||||
items = append(items, it)
|
||||
}
|
||||
feed.Items = items
|
||||
// No effort is made to work with the templates var.
|
||||
if strings.HasSuffix(templateName, ".html") ||
|
||||
strings.HasSuffix(templateName, ".xml") ||
|
||||
strings.HasSuffix(templateName, ".rss") {
|
||||
w.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"))
|
||||
t, err := htmlTemplate.ParseFiles(templateName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Parsing %s: %s\n", templateName, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
err = t.Execute(w, feed)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Writing feed: %s\n", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
} else {
|
||||
t, err := textTemplate.ParseFiles(templateName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Parsing %s: %s\n", templateName, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
err = t.Execute(w, feed)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Writing feed: %s\n", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
55
export_cmd_test.go
Normal file
55
export_cmd_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExportCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := exportCli(b, "feed.html", minimalIndex(t))
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
assert.Contains(t, b.String(), "<title>Oddµ: A minimal wiki</title>")
|
||||
assert.Contains(t, b.String(), "<title>Welcome to Oddµ</title>")
|
||||
}
|
||||
|
||||
func TestExportCmdLanguage(t *testing.T) {
|
||||
os.Setenv("ODDMU_LANGUAGES", "de,en")
|
||||
loadLanguages()
|
||||
p := Page{Body: []byte("This is an English text. All right then!")}
|
||||
it := Item{Page: p}
|
||||
assert.Equal(t, "en", it.Language())
|
||||
}
|
||||
|
||||
func TestExportCmdJsonFeed(t *testing.T) {
|
||||
cleanup(t, "testdata/json")
|
||||
os.Mkdir("testdata/json", 0755)
|
||||
assert.NoError(t, os.WriteFile("testdata/json/template.json", []byte(`{
|
||||
"version": "https://jsonfeed.org/version/1.1",
|
||||
"title": "{{.Title}}",
|
||||
"home_page_url": "https://alexschroeder.ch",
|
||||
"others": [],
|
||||
"items": [{{range .Items}}
|
||||
{
|
||||
"id": "{{.Name}}",
|
||||
"url": "https://alexschroeder.ch/view/{{.Name}}",
|
||||
"title": "{{.Title}}",
|
||||
"language": "{{.Language}}"
|
||||
"date_modified": "{{.Date}}",
|
||||
"content_html": "{{.Html}}",
|
||||
"tags": [{{range .Hashtags}}"{{.}}",{{end}}""],
|
||||
},{{end}}
|
||||
{}
|
||||
]
|
||||
}
|
||||
`), 0644))
|
||||
b := new(bytes.Buffer)
|
||||
s := exportCli(b, "testdata/json/template.json", minimalIndex(t))
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
assert.Contains(t, b.String(), `"title": "Oddµ: A minimal wiki"`)
|
||||
assert.Regexp(t, regexp.MustCompile("<h1.*>Welcome to Oddµ</h1>"), b.String()) // skip id
|
||||
}
|
||||
12
go.mod
12
go.mod
@@ -1,14 +1,15 @@
|
||||
module alexschroeder.ch/cgit/oddmu
|
||||
|
||||
go 1.21.0
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.3
|
||||
|
||||
require (
|
||||
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
|
||||
github.com/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
|
||||
@@ -22,15 +23,18 @@ require (
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.7.1 // indirect
|
||||
github.com/gen2brain/heic v0.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/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
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,20 +1,24 @@
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd h1:SxkQeH4jjXT0zMgiRgkiIQjIvWfe9vXuTAmE3cfcQrU=
|
||||
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd/go.mod h1:p1sbxRy+MY71fEWHcfRmerC8WUYXDFCExF9A7aXwp98=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
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=
|
||||
@@ -53,6 +57,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
||||
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -62,6 +68,8 @@ golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
|
||||
59
hashtags_cmd.go
Normal file
59
hashtags_cmd.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type hashtagsCmd struct {
|
||||
}
|
||||
|
||||
func (cmd *hashtagsCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (*hashtagsCmd) Name() string { return "hashtags" }
|
||||
func (*hashtagsCmd) Synopsis() string { return "hashtag overview" }
|
||||
func (*hashtagsCmd) Usage() string {
|
||||
return `hashtags:
|
||||
Count the use of all hashtags and list them, separated by a tabulator.
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *hashtagsCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return hashtagsCli(os.Stdout)
|
||||
}
|
||||
|
||||
// hashtagsCli runs the hashtags command on the command line. It is used
|
||||
// here with an io.Writer for easy testing.
|
||||
func hashtagsCli(w io.Writer) subcommands.ExitStatus {
|
||||
index.load()
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
|
||||
type hashtag struct {
|
||||
label string
|
||||
count int
|
||||
}
|
||||
|
||||
hashtags := []hashtag{}
|
||||
|
||||
for token, docids := range index.token {
|
||||
hashtags = append(hashtags, hashtag{label: token, count: len(docids)})
|
||||
}
|
||||
|
||||
sort.Slice(hashtags, func(i, j int) bool {
|
||||
return hashtags[i].count > hashtags[j].count
|
||||
})
|
||||
|
||||
fmt.Fprintln(w, "Rank\tHashtag\tCount")
|
||||
for i, hashtag := range hashtags {
|
||||
fmt.Fprintf(w, "%d\t%s\t%d\n", i+1, hashtag.label, hashtag.count)
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
16
hashtags_cmd_test.go
Normal file
16
hashtags_cmd_test.go
Normal 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")
|
||||
}
|
||||
46
html_cmd.go
46
html_cmd.go
@@ -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
|
||||
}
|
||||
|
||||
4
index.go
4
index.go
@@ -6,13 +6,13 @@ package main
|
||||
|
||||
import (
|
||||
"golang.org/x/exp/constraints"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type docid uint
|
||||
@@ -23,7 +23,7 @@ type docid uint
|
||||
// It depends on the fact that Title is always plain text.
|
||||
type ImageData struct {
|
||||
Title, Name string
|
||||
Html template.HTML
|
||||
Html template.HTML
|
||||
}
|
||||
|
||||
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
|
||||
|
||||
56
links_cmd.go
Normal file
56
links_cmd.go
Normal 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
16
links_cmd_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinksCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := linksCli(b, []string{"README"})
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
x := b.String()
|
||||
assert.Contains(t, x, "https://alexschroeder.ch/view/oddmu/oddmu.1\n")
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
38
man/Makefile
38
man/Makefile
@@ -3,6 +3,20 @@ MAN=$(patsubst %.txt,%,${TEXT})
|
||||
HTML=$(patsubst %.txt,%.html,${TEXT})
|
||||
MD=$(patsubst %.txt,%.md,${TEXT})
|
||||
|
||||
help:
|
||||
@echo Help for Oddmu Documentation
|
||||
@echo ============================
|
||||
@echo make man
|
||||
@echo " regenerate man pages"
|
||||
@echo make html
|
||||
@echo " generate HTML pages"
|
||||
@echo make md
|
||||
@echo " generate Markdown pages"
|
||||
@echo make clean
|
||||
@echo " delete HTML and Markdown pages"
|
||||
@echo make realclean
|
||||
@echo " delete HTML, Markdown and man pages"
|
||||
|
||||
man: ${MAN}
|
||||
|
||||
%: %.txt
|
||||
@@ -11,24 +25,20 @@ man: ${MAN}
|
||||
html: ${HTML}
|
||||
|
||||
%.html: %.md
|
||||
echo '<!DOCTYPE html>' > $@
|
||||
oddmu html $(basename $<) | sed --regexp-extended \
|
||||
@echo Making $@
|
||||
@echo '<!DOCTYPE html>' > $@
|
||||
@oddmu html $(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/#([^ #])/\\#\1/' \
|
||||
-e 's/^([A-Z.-]*\([1-9]\))( ".*")?$$/# \1/' \
|
||||
< $< > $@
|
||||
@echo Making $@
|
||||
@perl scdoc-to-markdown < $< > $@
|
||||
|
||||
README.md: ../README.md
|
||||
sed --regexp-extended \
|
||||
@echo Making $@
|
||||
@sed --regexp-extended \
|
||||
-e 's/\]\(.*\/(.*)\.txt\)/](\1)/' \
|
||||
< $< > $@
|
||||
|
||||
@@ -37,7 +47,9 @@ upload: ${MD} README.md
|
||||
make clean
|
||||
|
||||
clean:
|
||||
rm --force ${HTML} ${MD} README.md
|
||||
@echo Removing HTML and Markdown files
|
||||
@rm --force ${HTML} ${MD} README.md
|
||||
|
||||
realclean: clean
|
||||
rm --force ${MAN}
|
||||
@echo Removing man pages
|
||||
@rm --force ${MAN}
|
||||
|
||||
79
man/oddmu-export.1
Normal file
79
man/oddmu-export.1
Normal file
@@ -0,0 +1,79 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-EXPORT" "1" "2024-08-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
68
man/oddmu-export.1.txt
Normal file
@@ -0,0 +1,68 @@
|
||||
ODDMU-EXPORT(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-export - export all pages into one file
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu export* [*-template* _filename_]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "export" subcommand prints a RSS file containing all the pages to stdout.
|
||||
You probably want to redirect this into a file so that you can upload and import
|
||||
it somewhere.
|
||||
|
||||
Note that this only handles pages (Markdown files). All other files (images,
|
||||
PDFs, whatever else you uploaded) are not part of the feed and has to be
|
||||
uploaded to the new platform in some other way.
|
||||
|
||||
The *-template* option specifies the template to use. If the template filename
|
||||
ends in _.xml_, _.html_ or _.rss_, it is assumed to contain XML and the optional
|
||||
XML preamble is printed and appropriate escaping rules are used.
|
||||
|
||||
# FILES
|
||||
|
||||
By default, the export uses the **feed.html** template in the current directory.
|
||||
|
||||
# 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
39
man/oddmu-hashtags.1
Normal file
@@ -0,0 +1,39 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-HASHTAGS" "1" "2024-08-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
30
man/oddmu-hashtags.1.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
ODDMU-HASHTAGS(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-hashtags - count the hashtags used
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu hashtags*
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "hashtags" subcommand counts all the hashtags used and lists them, separated
|
||||
by a TAB character.
|
||||
|
||||
# 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>.
|
||||
@@ -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.\&
|
||||
|
||||
@@ -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
29
man/oddmu-links.1
Normal file
@@ -0,0 +1,29 @@
|
||||
.\" Generated by scdoc 1.11.3
|
||||
.\" Complete documentation for this program is not available as a GNU info page
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-LINKS" "1" "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
22
man/oddmu-links.1.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
ODDMU-LINKS(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-links - list outgoing links for pages
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu links* _page names..._
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "links" subcommand lists outgoing links for one or more 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>.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ ODDMU-MISSING(1)
|
||||
|
||||
# NAME
|
||||
|
||||
oddmu-missing - list missing pages from the command-line
|
||||
oddmu-missing - list missing pages
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
|
||||
@@ -5,16 +5,52 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-RELEASES" "7" "2024-07-21"
|
||||
.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
|
||||
|
||||
@@ -2,12 +2,46 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
33
man/oddmu-toc.1
Normal file
33
man/oddmu-toc.1
Normal 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
26
man/oddmu-toc.1.txt
Normal 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>.
|
||||
32
man/oddmu.1
32
man/oddmu.1
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "1" "2024-05-11"
|
||||
.TH "ODDMU" "1" "2024-08-16"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -92,7 +92,7 @@ curl --form body="Did you bring a towel?"
|
||||
.RE
|
||||
.PP
|
||||
When calling the \fIdrop\fR action, the query parameters used are \fIname\fR for the
|
||||
target filename, \fIfile\fR for the file to upload.\& If the query parameter
|
||||
target filename and \fIfile\fR for the file to upload.\& If the query parameter
|
||||
\fImaxwidth\fR is set, an attempt is made to decode and resize the image.\& JPG, PNG,
|
||||
WEBP and HEIC files can be decoded.\& Only JPG and PNG files can be encoded,
|
||||
however.\& If the target name ends in \fI.\&jpg\fR, the \fIquality\fR query parameter is
|
||||
@@ -367,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>.\&
|
||||
|
||||
@@ -66,7 +66,7 @@ curl --form body="Did you bring a towel?" \
|
||||
```
|
||||
|
||||
When calling the _drop_ action, the query parameters used are _name_ for the
|
||||
target filename, _file_ for the file to upload. If the query parameter
|
||||
target filename and _file_ for the file to upload. If the query parameter
|
||||
_maxwidth_ is set, an attempt is made to decode and resize the image. JPG, PNG,
|
||||
WEBP and HEIC files can be decoded. Only JPG and PNG files can be encoded,
|
||||
however. If the target name ends in _.jpg_, the _quality_ query parameter is
|
||||
@@ -305,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>.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU.SERVICE" "5" "2024-04-21"
|
||||
.TH "ODDMU.SERVICE" "5" "2024-08-23"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -49,8 +49,7 @@ Install the service file and enable it:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ln -s /home/oddmu/oddmu\&.service /etc/systemd/system/
|
||||
systemctl enable --now oddmu
|
||||
sudo systemctl enable --now \&./oddmu\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
@@ -112,10 +111,8 @@ To install, enable and start both units:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
|
||||
ln -s /home/oddmu/oddmu-unix-domain\&.service /etc/systemd/system
|
||||
systemctl enable --now oddmu-unix-domain\&.socket
|
||||
systemctl enable --now oddmu-unix-domain\&.service
|
||||
sudo systemctl enable --now \&./oddmu-unix-domain\&.socket
|
||||
sudo systemctl enable --now \&./oddmu-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
@@ -131,10 +128,87 @@ echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
|
||||
Now you need to set up your web browser to use the Unix domain socket.\& See
|
||||
\fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example configurations.\&
|
||||
.PP
|
||||
.SS A personal wiki
|
||||
.PP
|
||||
On a single user machine, it might be useful to have a single wiki for the main
|
||||
user available.\& In order to do this, setup a "user" unit using systemd and save
|
||||
the following as "user-unix-domain.\&service":
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
[Unit]
|
||||
Description=Oddmu
|
||||
After=network\&.target
|
||||
[Install]
|
||||
WantedBy=default\&.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
ExecStart=/home/alex/src/oddmu/oddmu
|
||||
WorkingDirectory=/home/alex/wiki
|
||||
Environment="ODDMU_LANGUAGES=de,en"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Since this is a user service, the same user can edit the files using their
|
||||
favourite text editor.\&
|
||||
.PP
|
||||
Install it:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
systemctl --user enable --now \&./user-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
To examine the log:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
journalctl --user --unit user-unix-domain\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Note that no sudo is required!\&
|
||||
.PP
|
||||
.SS Using the priviledged port 80
|
||||
.PP
|
||||
When running a personal wiki, you can have the oddmu binary listen on port 80,
|
||||
the standard HTTP port.\& It is not really worth the effort: It means that you can
|
||||
visit "http://localhost/" instead of "http://localhost:8080".\& Nevertheless, if
|
||||
you'\&re interested in giving it a try, here'\&s how to do it.\&
|
||||
.PP
|
||||
The service definition must specify the new port:
|
||||
.PP
|
||||
Environment="ODDMU_PORT=80"
|
||||
.PP
|
||||
Since this is a privileged port, the binary needs an extra capability for an
|
||||
ordinary user to do this.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
sudo setcap \&'cap_net_bind_service=+ep\&' oddmu
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Note that as soon as you recompile, the capability is gone again and the above
|
||||
must be repeated.\&
|
||||
.PP
|
||||
.SH SECURITY
|
||||
.PP
|
||||
Only allow direct access to Oddmu on systems and networks where you'\&re OK with
|
||||
every user editing the pages.\& On the open web, this is not true.\& If your server
|
||||
is on the open web, always run Oddmu behind a regular web server acting as a
|
||||
reverse proxy, limiting regular visitors to read-only access.\& This means that
|
||||
the regular web server listens on the regular privileged ports (80 for HTTP,
|
||||
443 for HTTPS) and passes requests to Oddmu on some other port.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
|
||||
\fIsystemd.\&socket(5), \fRcapabilities_(7)
|
||||
\fIsystemd.\&socket\fR(5), \fIcapabilities\fR(7)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
|
||||
@@ -37,8 +37,7 @@ Environment="ODDMU_WEBFINGER=1"
|
||||
Install the service file and enable it:
|
||||
|
||||
```
|
||||
ln -s /home/oddmu/oddmu.service /etc/systemd/system/
|
||||
systemctl enable --now oddmu
|
||||
sudo systemctl enable --now ./oddmu.service
|
||||
```
|
||||
|
||||
You should be able to visit the wiki at http://localhost:8080/.
|
||||
@@ -90,10 +89,8 @@ Environment="ODDMU_WEBFINGER=1"
|
||||
To install, enable and start both units:
|
||||
|
||||
```
|
||||
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
|
||||
ln -s /home/oddmu/oddmu-unix-domain.service /etc/systemd/system
|
||||
systemctl enable --now oddmu-unix-domain.socket
|
||||
systemctl enable --now oddmu-unix-domain.service
|
||||
sudo systemctl enable --now ./oddmu-unix-domain.socket
|
||||
sudo systemctl enable --now ./oddmu-unix-domain.service
|
||||
```
|
||||
|
||||
To test just the unix domain socket, use _ncat(1)_:
|
||||
@@ -106,10 +103,79 @@ echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
|
||||
Now you need to set up your web browser to use the Unix domain socket. See
|
||||
_oddmu-apache_(5) or _oddmu-nginx_(5) for example configurations.
|
||||
|
||||
## A personal wiki
|
||||
|
||||
On a single user machine, it might be useful to have a single wiki for the main
|
||||
user available. In order to do this, setup a "user" unit using systemd and save
|
||||
the following as "user-unix-domain.service":
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Oddmu
|
||||
After=network.target
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
ExecStart=/home/alex/src/oddmu/oddmu
|
||||
WorkingDirectory=/home/alex/wiki
|
||||
Environment="ODDMU_LANGUAGES=de,en"
|
||||
```
|
||||
|
||||
Since this is a user service, the same user can edit the files using their
|
||||
favourite text editor.
|
||||
|
||||
Install it:
|
||||
|
||||
```
|
||||
systemctl --user enable --now ./user-unix-domain.service
|
||||
```
|
||||
|
||||
To examine the log:
|
||||
|
||||
```
|
||||
journalctl --user --unit user-unix-domain.service
|
||||
```
|
||||
|
||||
Note that no sudo is required!
|
||||
|
||||
## Using the priviledged port 80
|
||||
|
||||
When running a personal wiki, you can have the oddmu binary listen on port 80,
|
||||
the standard HTTP port. It is not really worth the effort: It means that you can
|
||||
visit "http://localhost/" instead of "http://localhost:8080". Nevertheless, if
|
||||
you're interested in giving it a try, here's how to do it.
|
||||
|
||||
The service definition must specify the new port:
|
||||
|
||||
Environment="ODDMU_PORT=80"
|
||||
|
||||
Since this is a privileged port, the binary needs an extra capability for an
|
||||
ordinary user to do this.
|
||||
|
||||
```
|
||||
sudo setcap 'cap_net_bind_service=+ep' oddmu
|
||||
```
|
||||
|
||||
Note that as soon as you recompile, the capability is gone again and the above
|
||||
must be repeated.
|
||||
|
||||
# SECURITY
|
||||
|
||||
Only allow direct access to Oddmu on systems and networks where you're OK with
|
||||
every user editing the pages. On the open web, this is not true. If your server
|
||||
is on the open web, always run Oddmu behind a regular web server acting as a
|
||||
reverse proxy, limiting regular visitors to read-only access. This means that
|
||||
the regular web server listens on the regular privileged ports (80 for HTTP,
|
||||
443 for HTTPS) and passes requests to Oddmu on some other port.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
|
||||
_systemd.socket(5), _capabilities_(7)
|
||||
_systemd.socket_(5), _capabilities_(7)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
|
||||
27
man/scdoc-to-markdown
Executable file
27
man/scdoc-to-markdown
Executable 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;
|
||||
}
|
||||
@@ -8,11 +8,9 @@ import (
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -34,23 +32,19 @@ func (cmd *missingCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (cmd *missingCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return missingCli(os.Stdout)
|
||||
return missingCli(os.Stdout, &index)
|
||||
}
|
||||
|
||||
func missingCli(w io.Writer) subcommands.ExitStatus {
|
||||
names, err := existingPages()
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
// missingCli implements the finding of links to missing pages. In order to make testing easier, it takes a Writer and
|
||||
// an indexStore. The Writer is important so that test code can provide a buffer instead of os.Stdout; the indexStore is
|
||||
// important so that test code can ensure no other test running in parallel can interfere with the list of known pages
|
||||
// (by adding or deleting pages).
|
||||
func missingCli(w io.Writer, idx *indexStore) subcommands.ExitStatus {
|
||||
found := false
|
||||
for name, isPage := range names {
|
||||
if !isPage {
|
||||
continue
|
||||
}
|
||||
for name := range idx.titles {
|
||||
p, err := loadPage(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", p.Name, err)
|
||||
fmt.Fprintf(os.Stderr, "Loading %s: %s\n", name, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
for _, link := range p.links() {
|
||||
@@ -66,13 +60,17 @@ func missingCli(w io.Writer) subcommands.ExitStatus {
|
||||
u.Path = strings.TrimSuffix(u.Path, ".md")
|
||||
// pages containing a colon need the ./ prefix
|
||||
u.Path = strings.TrimPrefix(u.Path, "./")
|
||||
// check whether the destinatino is a known page
|
||||
// check whether the destination is a known page
|
||||
destination, err := url.PathUnescape(u.Path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot decode %s: %s\n", link, err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
_, ok := names[destination]
|
||||
_, ok := idx.titles[destination]
|
||||
// links to directories can work
|
||||
if !ok {
|
||||
_, ok = idx.titles[path.Join(destination, "index")]
|
||||
}
|
||||
if !ok {
|
||||
if !found {
|
||||
fmt.Fprintln(w, "Page\tMissing")
|
||||
@@ -89,31 +87,6 @@ func missingCli(w io.Writer) subcommands.ExitStatus {
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
func existingPages() (map[string]bool, error) {
|
||||
names := make(map[string]bool)
|
||||
err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip hidden directories and files
|
||||
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(path, ".md") {
|
||||
name := filepath.ToSlash(strings.TrimSuffix(path, ".md"))
|
||||
names[name] = true
|
||||
} else {
|
||||
names[path] = false
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return names, err
|
||||
}
|
||||
|
||||
// links parses the page content and returns an array of link destinations.
|
||||
func (p *Page) links() []string {
|
||||
var links []string
|
||||
@@ -124,8 +97,17 @@ func (p *Page) links() []string {
|
||||
switch v := node.(type) {
|
||||
case *ast.Link:
|
||||
link := string(v.Destination)
|
||||
dir := p.Dir()
|
||||
links = append(links, path.Join(dir, link))
|
||||
url, err := url.Parse(link)
|
||||
if err != nil {
|
||||
// no error reporting
|
||||
return ast.GoToNext
|
||||
}
|
||||
if url.IsAbs() {
|
||||
links = append(links, link)
|
||||
} else {
|
||||
dir := p.Dir()
|
||||
links = append(links, path.Join(dir, link))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.GoToNext
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func TestMissingCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := missingCli(b)
|
||||
s := missingCli(b, minimalIndex(t))
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
r := `Page Missing
|
||||
index test
|
||||
|
||||
8
page.go
8
page.go
@@ -28,8 +28,8 @@ type Page struct {
|
||||
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
|
||||
// the Name "foo").
|
||||
type Link struct {
|
||||
Title string
|
||||
Url string
|
||||
Title string
|
||||
Url string
|
||||
}
|
||||
|
||||
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
|
||||
@@ -179,13 +179,13 @@ func (p *Page) Parents() []*Link {
|
||||
return links
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < len(elems) - 1; i++ {
|
||||
for i := 0; i < len(elems)-1; i++ {
|
||||
name := s + "index"
|
||||
title, ok := index.titles[name]
|
||||
if !ok {
|
||||
title = "…"
|
||||
}
|
||||
link := &Link{ Title: title, Url: strings.Repeat("../", len(elems) - i - 1) + "index" }
|
||||
link := &Link{Title: title, Url: strings.Repeat("../", len(elems)-i-1) + "index"}
|
||||
links = append(links, link)
|
||||
s += elems[i] + "/"
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func wikiRenderer() *html.Renderer {
|
||||
return renderer
|
||||
}
|
||||
|
||||
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Language, Page.Hashtags, and escapes Page.Name.
|
||||
// renderHtml renders the Page.Body to HTML and sets Page.Html, Page.Hashtags, and escapes Page.Name.
|
||||
func (p *Page) renderHtml() {
|
||||
parser, hashtags := wikiParser()
|
||||
renderer := wikiRenderer()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// well the page matched for a search query. Images are the images whose description match the query.
|
||||
type Result struct {
|
||||
Page
|
||||
Score int
|
||||
Score int
|
||||
Images []ImageData
|
||||
}
|
||||
|
||||
@@ -131,8 +131,8 @@ func search(q, dir, filter string, page int, all bool) ([]*Result, bool) {
|
||||
}
|
||||
if len(terms) > 0 {
|
||||
index.RLock()
|
||||
res := make([]ImageData, 0)
|
||||
for _, r := range results {
|
||||
res := make([]ImageData, 0)
|
||||
ImageLoop:
|
||||
for _, img := range index.images[r.Name] {
|
||||
title := strings.ToLower(img.Title)
|
||||
|
||||
@@ -42,7 +42,7 @@ button { background-color: #eee; color: inherit; border-radius: 4px; border-widt
|
||||
<span class="score">{{.Score}}</span></p>
|
||||
<blockquote>{{.Html}}</blockquote>
|
||||
{{range .Images}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img class="last" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
<p class="image"><a href="/view/{{.Name}}"><img loading="lazy" src="/view/{{.Name}}"></a><br/>{{.Html}}
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -28,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
|
||||
@@ -84,8 +84,8 @@ func searchCli(w io.Writer, dir string, n int, all, extract bool, quiet bool, ar
|
||||
|
||||
// searchExtract prints the search extracts to stdout with highlighting for a terminal.
|
||||
func searchExtract(w io.Writer, items []*Result) {
|
||||
heading := func (s string) string { return "\x1b[1;4m" + s + "\x1b[0m" } // bold + underline
|
||||
match := func (s string) string { return "\x1b[1m" + s + "\x1b[0m" } // bold
|
||||
heading := func(s string) string { return "\x1b[1;4m" + s + "\x1b[0m" } // bold + underline
|
||||
match := func(s string) string { return "\x1b[1m" + s + "\x1b[0m" } // bold
|
||||
re := regexp.MustCompile(`<b>(.*?)</b>`)
|
||||
for _, p := range items {
|
||||
s := re.ReplaceAllString(string(p.Html), match(`$1`))
|
||||
@@ -101,7 +101,7 @@ func searchExtract(w io.Writer, items []*Result) {
|
||||
if err != nil {
|
||||
name = img.Name
|
||||
}
|
||||
fmt.Fprintln(w, " - ", name);
|
||||
fmt.Fprintln(w, " - ", name)
|
||||
for _, s := range strings.Split(wordwrap.String(img.Title, 70), "\n") {
|
||||
fmt.Fprintln(w, " ", s)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -230,9 +234,9 @@ A quick sip too quick
|
||||
func TestImageSearch(t *testing.T) {
|
||||
cleanup(t, "testdata/images")
|
||||
|
||||
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# Pictures
|
||||
p := &Page{Name: "testdata/images/2024-07-21", Body: []byte(`# 2024-07-21 Pictures
|
||||
|
||||

|
||||

|
||||
|
||||
Pictures in the box
|
||||
Tiny windows to our past
|
||||
@@ -241,12 +245,26 @@ Where are you, my love?
|
||||
`)}
|
||||
p.save()
|
||||
|
||||
items, _ := search("phone", "testdata/images", "", 1, false)
|
||||
assert.Equal(t, 1, len(items), "one page found")
|
||||
assert.Equal(t, "Pictures", items[0].Title)
|
||||
assert.Equal(t, "phone", items[0].Images[0].Title)
|
||||
assert.Equal(t, "<b>phone</b>", string(items[0].Images[0].Html))
|
||||
q := &Page{Name: "testdata/images/2024-07-22", Body: []byte(`# 2024-07-22 The Moon
|
||||
|
||||
When the night is light
|
||||
Behind clouds the moon is bright
|
||||
Please call me, my love.
|
||||
`)}
|
||||
q.save()
|
||||
|
||||
items, _ := search("call", "testdata/images", "", 1, false)
|
||||
assert.Equal(t, 2, len(items), "two pages found")
|
||||
|
||||
assert.Equal(t, "2024-07-21 Pictures", items[0].Title)
|
||||
assert.Equal(t, "2024-07-22 The Moon", items[1].Title)
|
||||
|
||||
assert.NotEmpty(t, items[0].Images)
|
||||
assert.Equal(t, "phone call", items[0].Images[0].Title)
|
||||
assert.Equal(t, "phone <b>call</b>", string(items[0].Images[0].Html))
|
||||
assert.Equal(t, "testdata/images/2024-07-21.jpg", items[0].Images[0].Name)
|
||||
|
||||
assert.Empty(t, items[1].Images)
|
||||
}
|
||||
|
||||
func TestSearchQuestionmark(t *testing.T) {
|
||||
|
||||
@@ -42,17 +42,17 @@ func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
dir := filepath.Clean(args[0])
|
||||
return staticCli(dir, cmd.jobs, false)
|
||||
return staticCli(".", dir, cmd.jobs, false)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
source, target string
|
||||
info fs.FileInfo
|
||||
info fs.FileInfo
|
||||
}
|
||||
|
||||
// staticCli generates a static site in the designated directory. The quiet flag is used to suppress output when running
|
||||
// tests.
|
||||
func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
// tests. The source directory cannot be set from the command-line. The current directory (".") is assumed.
|
||||
func staticCli(source, target string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
index.load()
|
||||
index.RLock()
|
||||
defer index.RUnlock()
|
||||
@@ -65,7 +65,7 @@ func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
for i := 0; i < jobs; i++ {
|
||||
go staticWorker(tasks, results, done)
|
||||
}
|
||||
go staticWalk(dir, tasks, stop)
|
||||
go staticWalk(source, target, tasks, stop)
|
||||
go staticWatch(jobs, results, done)
|
||||
n, err := staticProgressIndicator(results, stop, quiet)
|
||||
if !quiet {
|
||||
@@ -78,18 +78,18 @@ func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// staticWalk walks the directory tree. Any directory it finds, it recreates in the destination directory. Any file it
|
||||
// staticWalk walks the source directory tree. Any directory it finds, it recreates in the target directory. Any file it
|
||||
// finds, it puts into the tasks channel for the staticWorker. When the directory walk is finished, the tasks channel is
|
||||
// closed. If there's an error on the stop channel, the walk returns that error.
|
||||
func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
func staticWalk(source, target string, tasks chan (args), stop chan (error)) {
|
||||
// The error returned here is what's in the stop channel but at the very end, a worker might return an error
|
||||
// even though the walk is already done. This is why we cannot rely on the return value of the walk.
|
||||
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
filepath.Walk(source, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case err := <- stop:
|
||||
case err := <-stop:
|
||||
return err
|
||||
default:
|
||||
base := filepath.Base(path)
|
||||
@@ -102,18 +102,28 @@ func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
}
|
||||
}
|
||||
// skip backup files, avoid recursion
|
||||
if strings.HasSuffix(path, "~") || strings.HasPrefix(path, dir) {
|
||||
if strings.HasSuffix(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
|
||||
target := filepath.Join(dir, path)
|
||||
if info.IsDir() {
|
||||
return os.Mkdir(target, 0755)
|
||||
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(target)
|
||||
other, err := os.Stat(actual_target)
|
||||
if err != nil || info.ModTime().After(other.ModTime()) {
|
||||
tasks <- args{ source: path, target: target, info: info }
|
||||
tasks <- args{source: path, target: actual_target, info: info}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -123,27 +133,27 @@ func staticWalk (dir string, tasks chan(args), stop chan(error)) {
|
||||
|
||||
// staticWatch counts the values coming out of the done channel. When the count matches the number of jobs started, we
|
||||
// know that all the tasks have been processed and the results channel is closed.
|
||||
func staticWatch(jobs int, results chan(error), done chan(bool)) {
|
||||
func staticWatch(jobs int, results chan (error), done chan (bool)) {
|
||||
for i := 0; i < jobs; i++ {
|
||||
<- done
|
||||
<-done
|
||||
}
|
||||
close(results)
|
||||
}
|
||||
|
||||
// staticWorker takes arguments off the tasks channel (the file to process) and put results in the results channel (any
|
||||
// errors encountered); when they're done they send true on the done channel.
|
||||
func staticWorker(tasks chan(args), results chan(error), done chan(bool)) {
|
||||
task, ok := <- tasks
|
||||
func staticWorker(tasks chan (args), results chan (error), done chan (bool)) {
|
||||
task, ok := <-tasks
|
||||
for ok {
|
||||
results <- staticFile(task.source, task.target, task.info)
|
||||
task, ok = <- tasks
|
||||
task, ok = <-tasks
|
||||
}
|
||||
done <- true
|
||||
}
|
||||
|
||||
// staticProgressIndicator watches the results channel and does a countdown. If the result channel reports an error,
|
||||
// that is put into the stop channel so that staticWalk stops adding to the tasks channel.
|
||||
func staticProgressIndicator(results chan(error), stop chan(error), quiet bool) (int, error) {
|
||||
func staticProgressIndicator(results chan (error), stop chan (error), quiet bool) (int, error) {
|
||||
n := 0
|
||||
t := time.Now()
|
||||
var err error
|
||||
@@ -154,7 +164,7 @@ func staticProgressIndicator(results chan(error), stop chan(error), quiet bool)
|
||||
stop <- err
|
||||
} else {
|
||||
n++
|
||||
if !quiet && n % 13 == 0 {
|
||||
if !quiet && n%13 == 0 {
|
||||
if time.Since(t) > time.Second {
|
||||
fmt.Printf("\r%d", n)
|
||||
t = time.Now()
|
||||
@@ -170,11 +180,11 @@ func staticProgressIndicator(results chan(error), stop chan(error), quiet bool)
|
||||
func staticFile(source, target string, info fs.FileInfo) error {
|
||||
// render pages
|
||||
if strings.HasSuffix(source, ".md") {
|
||||
p, err := staticPage(source[:len(source)-3], target[:len(target)-3] + ".html")
|
||||
p, err := staticPage(source[:len(source)-3], target[:len(target)-3]+".html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return staticFeed(source[:len(source)-3], target[:len(target)-3] + ".rss", p, info.ModTime())
|
||||
return staticFeed(source[:len(source)-3], target[:len(target)-3]+".rss", p, info.ModTime())
|
||||
}
|
||||
// remaining files are linked unless this is a template
|
||||
if slices.Contains(templateFiles, filepath.Base(source)) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func TestStaticCmd(t *testing.T) {
|
||||
cleanup(t, "testdata/static")
|
||||
s := staticCli("testdata/static", 2, true)
|
||||
s := staticCli(".", "testdata/static", 2, true)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
// pages
|
||||
assert.FileExists(t, "testdata/static/index.html")
|
||||
@@ -34,12 +34,8 @@ And the cars so loud
|
||||
`)}
|
||||
h.save()
|
||||
h.notify()
|
||||
wd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.Chdir("testdata/static-feed"))
|
||||
s := staticCli("../static-feed-out/", 2, true)
|
||||
s := staticCli("testdata/static-feed", "testdata/static-feed-out", 2, true)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
assert.NoError(t, os.Chdir(wd))
|
||||
assert.FileExists(t, "testdata/static-feed-out/2024-03-07-poem.html")
|
||||
assert.FileExists(t, "testdata/static-feed-out/Haiku.html")
|
||||
b, err := os.ReadFile("testdata/static-feed-out/Haiku.rss")
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
html { max-width: 80ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe }
|
||||
body { hyphens: auto }
|
||||
html { max-width: 80ch; padding: 1ch; margin: auto }
|
||||
body { hyphens: auto; color: #111; background-color: #ffe }
|
||||
header a { margin-right: 1ch }
|
||||
h1 { text-wrap: balance }
|
||||
footer { border-top: 1px solid #888 }
|
||||
form, textarea { width: 97%; font-size: inherit }
|
||||
pre { background-color: #fff; padding: 1ex 2ex; overflow-x: scroll }
|
||||
pre { background-color: #fff; padding: 1ex 2ex; overflow-x: auto }
|
||||
.diff { font-size: inherit; white-space: normal; overflow-wrap: break-word; background-color: white; border: 1px solid #333; padding: 1ch }
|
||||
img { max-width: 100%; max-height: 90vh }
|
||||
img, video { max-width: 100%; max-height: 90vh; width: auto; height: auto }
|
||||
.right img { float: right; margin-left: 2em; margin-bottom: 1em; border: 1px solid #111 }
|
||||
.left img { float: left; margin-right: 2em; margin-bottom: 1em; border: 1px solid #111 }
|
||||
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px }
|
||||
del { background-color: #fab }
|
||||
ins { background-color: #af8 }
|
||||
#search, #id { width: 30ch }
|
||||
#search, #id { max-width: 30ch; width: calc(100% - 23ch) }
|
||||
.result { font-size: larger }
|
||||
.score { font-size: smaller; opacity: 0.8 }
|
||||
.image { font-size: small; display: inline-block; margin-right: 1em; max-width: calc(20% - 1em); }
|
||||
#view form { margin-top: 2px }
|
||||
#view button { width: 6ch }
|
||||
#view label { display: inline-block; width: 10ch }
|
||||
@@ -25,7 +27,7 @@ th { font-weight: normal }
|
||||
th + th, td + td { padding-left: 1em }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html { color: #eeeee8; background-color: #333 }
|
||||
body { color: #eeeee8; background-color: #333 }
|
||||
footer { border-top: 1px solid #666 }
|
||||
.diff { background-color: inherit; border: 1px solid #666 }
|
||||
.right img { border: 1px solid #111 }
|
||||
|
||||
108
toc_cmd.go
Normal file
108
toc_cmd.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/google/subcommands"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type tocCmd struct {
|
||||
}
|
||||
|
||||
func (cmd *tocCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (*tocCmd) Name() string { return "toc" }
|
||||
func (*tocCmd) Synopsis() string { return "print the table of contents (toc) for a page" }
|
||||
func (*tocCmd) Usage() string {
|
||||
return `toc <page name> ...:
|
||||
Print the table of contents (toc) for a page.
|
||||
Use a single - to read Markdown from stdin.
|
||||
If only a single level one heading is appears
|
||||
in the page, it is dropped from the table of
|
||||
contents.
|
||||
`
|
||||
}
|
||||
|
||||
func (cmd *tocCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
return tocCli(os.Stdout, f.Args())
|
||||
}
|
||||
|
||||
// tocCli runs the toc command on the command line. It is used
|
||||
// here with an io.Writer for easy testing.
|
||||
func tocCli(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}
|
||||
p.toc().print(w)
|
||||
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
|
||||
}
|
||||
p.toc().print(w)
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// Toc represents an array of headings
|
||||
type Toc []*ast.Heading
|
||||
|
||||
// toc parses the page content and returns a Toc.
|
||||
func (p *Page) toc() Toc {
|
||||
var headings Toc
|
||||
parser, _ := wikiParser()
|
||||
doc := markdown.Parse(p.Body, parser)
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
if !entering {
|
||||
switch v := node.(type) {
|
||||
case *ast.Heading:
|
||||
headings = append(headings, v)
|
||||
}
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
return headings
|
||||
}
|
||||
|
||||
// print prints the Toc to the io.Writer. If the table of contents first heading is a level one heading and there are no
|
||||
// other level one headings, this is a "regular" table of contents. For a regular table of contents, the first entry is
|
||||
// skipped.
|
||||
func (toc Toc) print(w io.Writer) {
|
||||
minLevel := 0
|
||||
levelOneCount := 0
|
||||
for _, h := range toc {
|
||||
if h.Level == 1 {
|
||||
levelOneCount++
|
||||
}
|
||||
if h.Level < minLevel || minLevel == 0 {
|
||||
minLevel = h.Level
|
||||
}
|
||||
}
|
||||
for i, h := range toc {
|
||||
if i == 0 && h.Level == 1 && levelOneCount == 1 {
|
||||
minLevel++
|
||||
continue
|
||||
}
|
||||
for j := minLevel; j < h.Level; j++ {
|
||||
fmt.Fprint(w, " ")
|
||||
}
|
||||
fmt.Fprint(w, "* [")
|
||||
for _, c := range h.GetChildren() {
|
||||
fmt.Fprint(w, string(c.AsLeaf().Literal))
|
||||
}
|
||||
fmt.Fprintf(w, "](#%s)\n", h.HeadingID)
|
||||
}
|
||||
}
|
||||
45
toc_cmd_test.go
Normal file
45
toc_cmd_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ## is promoted to level 1 because there is just one instance of level 1
|
||||
func TestTocCmd(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
s := tocCli(b, []string{"README"})
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
x := b.String()
|
||||
assert.Contains(t, x, "\n* [Bugs](#bugs)\n")
|
||||
}
|
||||
|
||||
// ## is promoted to level 1 because there is no instance of level 1
|
||||
func TestTocNoH1(t *testing.T) {
|
||||
p := &Page{
|
||||
Body: []byte(`## Venti
|
||||
Es drückt der Sommer
|
||||
Weit weg hör' ich ein Flugzeug
|
||||
Ventilator hilf!`)}
|
||||
b := new(bytes.Buffer)
|
||||
p.toc().print(b)
|
||||
assert.Equal(t, "* [Venti](#venti)\n", b.String())
|
||||
}
|
||||
|
||||
// # is dropped because it's just one level 1 heading
|
||||
func TestTocDropH1(t *testing.T) {
|
||||
p := &Page{Body: []byte("# One\n## Two\n### Three\n")}
|
||||
b := new(bytes.Buffer)
|
||||
p.toc().print(b)
|
||||
assert.Equal(t, "* [Two](#two)\n * [Three](#three)\n", b.String())
|
||||
}
|
||||
|
||||
// # is kept because there is more than one level 1 heading
|
||||
func TestTocMultipleH1(t *testing.T) {
|
||||
p := &Page{Body: []byte("# One\n# Two\n## Three\n")}
|
||||
b := new(bytes.Buffer)
|
||||
p.toc().print(b)
|
||||
assert.Equal(t, "* [One](#one)\n* [Two](#two)\n * [Three](#three)\n", b.String())
|
||||
}
|
||||
98
tokenizer.go
98
tokenizer.go
@@ -16,16 +16,84 @@ func lowercaseFilter(tokens []string) []string {
|
||||
return r
|
||||
}
|
||||
|
||||
// tokenizeWithPredicates returns a slice of tokens for the given
|
||||
// text, including punctuation. Use this to begin tokenizing the query
|
||||
// string.
|
||||
func tokenizeOnWhitespace(q string) []string {
|
||||
return strings.Fields(q)
|
||||
// IsQuote reports whether the rune has the Quotation Mark property.
|
||||
func IsQuote(r rune) bool {
|
||||
// This property isn't the same as Z; special-case it.
|
||||
return unicode.Is(unicode.Quotation_Mark, r)
|
||||
}
|
||||
|
||||
// predicateFilter returns two slices of tokens: the first with
|
||||
// predicates, the other without predicates. Use this for query
|
||||
// string tokens.
|
||||
// tokenizeWithQuotes returns a slice of tokens for the given text, including punctuation. Use this to begin tokenizing
|
||||
// the query string. Note that quotation marks need a matching rune to end: 'foo' "foo" ‘foo’ ‚foo‘ ’foo’ “foo” „foo“
|
||||
// ”foo” «foo» »foo« ‹foo› ›foo‹ 「foo」 「foo」 『foo』 – read and despair:
|
||||
// https://en.wikipedia.org/wiki/Quotation_mark
|
||||
//
|
||||
// Also note that 〈foo〉 and 《foo》 are not considered to be quotation marks by Unicode.
|
||||
func tokenizeWithQuotes(s string) []string {
|
||||
type span struct {
|
||||
start int
|
||||
end int
|
||||
}
|
||||
|
||||
waitFor := rune(0)
|
||||
matchingRunes := [][]rune{{'\'', '\''}, {'"', '"'}, {'‘', '’'}, {'‚', '‘'}, {'’', '’'}, {'“', '”'}, {'„', '“'}, {'”', '”'},
|
||||
{'«', '»'}, {'»', '«'}, {'‹', '›'}, {'›', '‹'}, {'「', '」'}, {'「', '」'}, {'『', '』'}}
|
||||
|
||||
spans := make([]span, 0, 32)
|
||||
|
||||
// The comments in FieldsFunc say that doing this in a separate pass is faster.
|
||||
start := -1 // valid span start if >= 0
|
||||
RUNE:
|
||||
for end, rune := range s {
|
||||
if waitFor > 0 {
|
||||
if rune == waitFor {
|
||||
if start >= 0 {
|
||||
// skip "" and the like
|
||||
spans = append(spans, span{start, end})
|
||||
}
|
||||
// The comments in FieldsFunc say that doing this instead of using -1 is faster.
|
||||
start = ^start
|
||||
waitFor = 0
|
||||
} else if start < 0 {
|
||||
start = end
|
||||
}
|
||||
} else if unicode.IsSpace(rune) {
|
||||
if start >= 0 {
|
||||
spans = append(spans, span{start, end})
|
||||
start = ^start
|
||||
}
|
||||
} else {
|
||||
if start < 0 {
|
||||
// Only check for starting quote at the beginning of a token
|
||||
if IsQuote(rune) {
|
||||
waitFor = rune
|
||||
for _, match := range matchingRunes {
|
||||
if rune == match[0] {
|
||||
waitFor = match[1]
|
||||
continue RUNE
|
||||
}
|
||||
}
|
||||
}
|
||||
start = end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last field might end at EOF.
|
||||
if start >= 0 {
|
||||
spans = append(spans, span{start, len(s)})
|
||||
}
|
||||
|
||||
// Create strings from recorded field indices.
|
||||
a := make([]string, len(spans))
|
||||
for i, span := range spans {
|
||||
a[i] = s[span.start:span.end]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// predicateFilter returns two slices of tokens: the first with predicates, the other without predicates. Use this for
|
||||
// query string tokens.
|
||||
func predicateFilter(tokens []string) ([]string, []string) {
|
||||
with := make([]string, 0)
|
||||
without := make([]string, 0)
|
||||
@@ -39,18 +107,16 @@ func predicateFilter(tokens []string) ([]string, []string) {
|
||||
return with, without
|
||||
}
|
||||
|
||||
// predicatesAndTokens returns two slices of tokens: the first with
|
||||
// predicates, the other without predicates, all of them lower case.
|
||||
// Use this for query strings.
|
||||
// predicatesAndTokens returns two slices of tokens: the first with predicates, the other without predicates, all of
|
||||
// them lower case. Use this for query strings.
|
||||
func predicatesAndTokens(q string) ([]string, []string) {
|
||||
tokens := tokenizeOnWhitespace(q)
|
||||
tokens := tokenizeWithQuotes(q)
|
||||
tokens = lowercaseFilter(tokens)
|
||||
return predicateFilter(tokens)
|
||||
}
|
||||
|
||||
// noPredicateFilter returns a slice of tokens: the predicates without
|
||||
// the predicate, and all the others. That is: "foo:bar baz" is turned
|
||||
// into ["bar", "baz"] and the predicate "foo:" is dropped.
|
||||
// noPredicateFilter returns a slice of tokens: the predicates without the predicate, and all the others. That is:
|
||||
// "foo:bar baz" is turned into ["bar", "baz"] and the predicate "foo:" is dropped.
|
||||
func noPredicateFilter(tokens []string) []string {
|
||||
r := make([]string, 0)
|
||||
for _, token := range tokens {
|
||||
@@ -63,7 +129,7 @@ func noPredicateFilter(tokens []string) []string {
|
||||
// highlightTokens returns the tokens to highlight, including title
|
||||
// predicates.
|
||||
func highlightTokens(q string) []string {
|
||||
tokens := tokenizeOnWhitespace(q)
|
||||
tokens := tokenizeWithQuotes(q)
|
||||
tokens = lowercaseFilter(tokens)
|
||||
return noPredicateFilter(tokens)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
@@ -22,3 +23,35 @@ func TestTokensAndPredicates(t *testing.T) {
|
||||
assert.EqualValues(t, []string{"foo"}, terms)
|
||||
assert.EqualValues(t, []string{"title:bar"}, predicates)
|
||||
}
|
||||
|
||||
func TestQuoteRunes(t *testing.T) {
|
||||
s := `'"‘’‘‚“”„«»«‹›‹「」「」『』`
|
||||
for _, rune := range s {
|
||||
assert.True(t, IsQuote(rune), fmt.Sprintf("%c is a quote", rune))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotes(t *testing.T) {
|
||||
s := `'foo' "foo" ‘foo’ ‚foo‘ ’foo’ “foo” „foo“ ”foo” «foo» »foo« ‹foo› ›foo‹ 「foo」
|
||||
「foo」 『foo』`
|
||||
tokens := tokenizeWithQuotes(s)
|
||||
assert.EqualValues(t, []string{
|
||||
"foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo",
|
||||
"foo", "foo"}, tokens)
|
||||
}
|
||||
|
||||
func TestPhrases(t *testing.T) {
|
||||
s := `look for 'foo bar'`
|
||||
tokens := tokenizeWithQuotes(s)
|
||||
assert.EqualValues(t, []string{"look", "for", "foo bar"}, tokens)
|
||||
}
|
||||
|
||||
func TestKlingon(t *testing.T) {
|
||||
s := `quSDaq ba’lu’’a’`
|
||||
tokens := tokenizeWithQuotes(s)
|
||||
assert.EqualValues(t, []string{"quSDaq", "ba’lu’’a’"}, tokens)
|
||||
// quotes at the beginning of a word are not handled correctly
|
||||
s = `nuqDaq ‘oH tach’e’`
|
||||
tokens = tokenizeWithQuotes(s)
|
||||
assert.EqualValues(t, []string{"nuqDaq", "oH tach", "e’"}, tokens) // this is wrong 🤷
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/bashdrew/goheif"
|
||||
_ "github.com/gen2brain/heic"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/edwvee/exiffix"
|
||||
"io"
|
||||
@@ -82,7 +82,7 @@ func next(dir, fn string, i int) (string, error) {
|
||||
}
|
||||
ext := filepath.Ext(fn)
|
||||
// faking it
|
||||
m = []string{"", fn[:len(fn)-len(ext)]+"-", "0", ext}
|
||||
m = []string{"", fn[:len(fn)-len(ext)] + "-", "0", ext}
|
||||
}
|
||||
n, err := strconv.Atoi(m[2])
|
||||
if err == nil {
|
||||
@@ -239,6 +239,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
data.Set("last", filename) // has no slashes
|
||||
http.Redirect(w, r, "/upload/"+dir+"?"+data.Encode(), http.StatusFound)
|
||||
}
|
||||
|
||||
// Base returns a page name matching the first uploaded file: no extension and no appended number. If the name
|
||||
// refers to a directory, returns "index". This is used to create the form target in "upload.html", for example.
|
||||
func (u *upload) Base() string {
|
||||
|
||||
@@ -268,6 +268,6 @@ func TestUploadNext(t *testing.T) {
|
||||
s = append(s, nm)
|
||||
os.Create("testdata/next/" + nm)
|
||||
}
|
||||
r := []string{ "test-1.jpg", "test-2.jpg", "test-3.jpg", "test-4.jpg", "test-5.jpg", "test-6.jpg", "test-7.jpg", "test-8.jpg", "test-9.jpg", "test-10.jpg", "test-11.jpg", "test-12.jpg", "test-13.jpg", "test-14.jpg", "test-15.jpg", "test-16.jpg", "test-17.jpg", "test-18.jpg", "test-19.jpg", "test-20.jpg", "test-21.jpg", "test-22.jpg", "test-23.jpg", "test-24.jpg", "test-25.jpg" }
|
||||
r := []string{"test-1.jpg", "test-2.jpg", "test-3.jpg", "test-4.jpg", "test-5.jpg", "test-6.jpg", "test-7.jpg", "test-8.jpg", "test-9.jpg", "test-10.jpg", "test-11.jpg", "test-12.jpg", "test-13.jpg", "test-14.jpg", "test-15.jpg", "test-16.jpg", "test-17.jpg", "test-18.jpg", "test-19.jpg", "test-20.jpg", "test-21.jpg", "test-22.jpg", "test-23.jpg", "test-24.jpg", "test-25.jpg"}
|
||||
assert.Equal(t, r, s)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestWatchedPageUpdate(t *testing.T) {
|
||||
index.load()
|
||||
watches.install()
|
||||
assert.NoError(t, os.MkdirAll(dir, 0755))
|
||||
time.Sleep(time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
assert.Contains(t, watches.watcher.WatchList(), dir)
|
||||
|
||||
haiku := []byte(`# Pine cones
|
||||
@@ -24,7 +24,7 @@ Up and up in single file
|
||||
Who ate half a cone?`)
|
||||
assert.NoError(t, os.WriteFile(path, haiku, 0644))
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
watches.RLock()
|
||||
assert.Contains(t, watches.files, path)
|
||||
@@ -50,7 +50,7 @@ func TestWatchedTemplateUpdate(t *testing.T) {
|
||||
watches.install()
|
||||
assert.NoError(t, os.MkdirAll(dir, 0755))
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
assert.Contains(t, watches.watcher.WatchList(), dir)
|
||||
|
||||
@@ -71,7 +71,7 @@ the smell is everywhere
|
||||
[]byte("<body><h1>{{.Title}}</h1>{{.Html}}"),
|
||||
0644))
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
watches.RLock()
|
||||
assert.Contains(t, watches.files, path)
|
||||
|
||||
4
wiki.go
4
wiki.go
@@ -200,13 +200,17 @@ func commands() {
|
||||
subcommands.Register(subcommands.HelpCommand(), "")
|
||||
subcommands.Register(subcommands.FlagsCommand(), "")
|
||||
subcommands.Register(subcommands.CommandsCommand(), "")
|
||||
subcommands.Register(&exportCmd{}, "")
|
||||
subcommands.Register(&hashtagsCmd{}, "")
|
||||
subcommands.Register(&htmlCmd{}, "")
|
||||
subcommands.Register(&listCmd{}, "")
|
||||
subcommands.Register(&linksCmd{}, "")
|
||||
subcommands.Register(&missingCmd{}, "")
|
||||
subcommands.Register(¬ifyCmd{}, "")
|
||||
subcommands.Register(&replaceCmd{}, "")
|
||||
subcommands.Register(&searchCmd{}, "")
|
||||
subcommands.Register(&staticCmd{}, "")
|
||||
subcommands.Register(&tocCmd{}, "")
|
||||
subcommands.Register(&versionCmd{}, "")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
17
wiki_test.go
17
wiki_test.go
@@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -114,3 +115,19 @@ func cleanup(t *testing.T, dir string) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// minimalIndex creates a new indexStore containing just the known Markdown files without any additional Markdown files
|
||||
// from testdata.
|
||||
func minimalIndex(t *testing.T) *indexStore {
|
||||
idx := &indexStore{}
|
||||
idx.reset()
|
||||
names := []string{"index", "README"}
|
||||
for _, name := range names {
|
||||
p, err := loadPage(name)
|
||||
assert.NoError(t, err)
|
||||
idx.addPage(p)
|
||||
}
|
||||
err := filepath.Walk("themes", idx.walk)
|
||||
assert.NoError(t, err)
|
||||
return idx
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user