1
0

utilize wand

This commit is contained in:
Arpad Ryszka 2026-01-21 22:00:31 +01:00
parent 40bd187975
commit 20bb6895bf
14 changed files with 942 additions and 706 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
.bin
.build
testdocs_test.go
.cover

View File

@ -1,5 +1,8 @@
sources = $(shell find . -name "*.go" | grep -v testdocs_test.go)
prefix ?= ~/bin
sources = $(shell find . -name "*.go" | grep -v testdocs_test.go | grep -v cmd/docreflect/docs.gen.go)
PREFIX ?= /usr/local
prefix ?= $(PREFIX)
release_date = $(shell git show -s --format=%cs HEAD)
version = $(release_date)-$(shell git rev-parse --short HEAD)
default: build
@ -9,16 +12,33 @@ libdocreflect: $(sources)
libgenerate: $(sources)
go build ./generate
.bin:
mkdir -p .bin
.build:
mkdir -p .build
.bin/docreflect: $(sources) .bin
go build -o .bin/docreflect ./cmd/docreflect
.build/docreflect: $(sources) .build cmd/docreflect/docs.gen.go
go build -o .build/docreflect -ldflags "-X main.version=$(version)" ./cmd/docreflect
build: libdocreflect libgenerate .bin/docreflect
.build/docreflect.1: $(sources) .build cmd/docreflect/docs.gen.go
go run scripts/man.go $(version) $(release_date) ./cmd/docreflect > .build/docreflect.1
testdocs_test.go: $(sources) .bin/docreflect
.bin/docreflect generate docreflect_test code.squareroundforest.org/arpio/docreflect/internal/tests/src/testpackage > testdocs_test.go
cmd/docreflect/readme.md: $(sources) cmd/docreflect/docs.gen.go
go run scripts/markdown.go ./cmd/docreflect > cmd/docreflect/readme.md
build: libdocreflect libgenerate .build/docreflect cmd/docreflect/readme.md
testdocs_test.go: $(sources) .build
go run ./cmd/docreflect \
docreflect_test \
code.squareroundforest.org/arpio/docreflect/internal/tests/src/testpackage \
> .build/testdocs_test.go \
&& mv .build/testdocs_test.go testdocs_test.go
cmd/docreflect/docs.gen.go: $(sources)
go run ./cmd/docreflect \
main \
code.squareroundforest.org/arpio/docreflect/generate \
> .build/docs.gen.go \
&& mv .build/docs.gen.go cmd/docreflect/docs.gen.go
.cover: $(sources) testdocs_test.go
go test -count 1 -coverprofile .cover . ./generate
@ -32,12 +52,17 @@ showcover: .cover
go tool cover -html .cover
fmt: $(sources)
go fmt . ./generate ./cmd/docreflect
go fmt . ./generate ./cmd/docreflect ./scripts
install: .bin/docreflect
cp .bin/docreflect $(prefix)
$(prefix)/bin/docreflect: .build/docreflect
cp .build/docreflect $(prefix)/bin/docreflect
$(prefix)/share/man/man1/docreflect.1: .build/docreflect.1
cp .build/docreflect.1 $(prefix)/share/man/man1/docreflect.1
install: $(prefix)/bin/docreflect $(prefix)/share/man/man1/docreflect.1
clean:
go clean ./...
rm -rf .bin
rm -rf .build
rm -f testdocs_test.go

View File

@ -0,0 +1,55 @@
/*
Generated with https://code.squareroundforest.org/arpio/docreflect
*/
package main
import "code.squareroundforest.org/arpio/docreflect"
func init() {
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate", "Package generate produces Go source code that registers documentation for use with the docreflect package.\n")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.GenerateRegistry", "GenerateRegistry generates a Go source file containing an init function that registers the documentation for\nthe declarations specified by their fully qualified Go path.\n\nThe paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package\npaths is recommended over specific symbols in most cases.\n\nLimitations:\n\n- Type references (such as type aliases or definitions based on named types) are not resolved.\n\n- Import paths are not followed.\n\nfunc(o, w, outputPackageName, path)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.Options", "Options contains options for the generator.\n")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.Options.Main", "Main indicates that the docs for the symbols will be lookded up as part of the main package of an\nexecutable. This is necessary, because the fully qualified Go path of symbols in a main package differs\nfrom the path pointing to that package in a module.\n")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.cleanPaths", "\nfunc(gopath)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.collectGoDirs", "\nfunc(o)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findFieldDocs", "\nfunc(str, fieldPath)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findGoMod", "\nfunc(dir)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.fixDocPackage", "\nfunc(p)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.format", "\nfunc(w, pname, docs)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcDocs", "\nfunc(f)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcParams", "\nfunc(f)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.generate", "\nfunc(o, gopaths)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.getGoroot", "\nfunc()")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.header", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.importPackages", "\nfunc(o, godirs, paths)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.initOptions", "\nfunc()")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.merge", "\nfunc(m)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.methodDocs", "\nfunc(importPath, t)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.modCache", "\nfunc()")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.goroot", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.isMain", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.modules", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.wd", "")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageDocs", "\nfunc(pkg)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageFuncDocs", "\nfunc(importPath, funcs)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packagePaths", "\nfunc(p)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parsePackages", "\nfunc(pkgs)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parserInclude", "\nfunc(pkg)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.readGomod", "\nfunc(wd)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.replacePath", "\nfunc(p, prefix, replace)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.replacePaths", "\nfunc(m, prefix, replace)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.set", "\nfunc(m, key, value)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.splitGopath", "\nfunc(p)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldDocs", "\nfunc(t, fieldPath)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldsDocs", "\nfunc(importPath, t)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolDocs", "\nfunc(pkg, gopath)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolPath", "\nfunc(packagePath, name)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeDocs", "\nfunc(o, pkgs, gopaths)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeFieldDocs", "\nfunc(packagePath, prefix, f)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeDocs", "\nfunc(importPath, types)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeMethodDocs", "\nfunc(t, name)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.unpack", "\nfunc(e)")
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.valueDocs", "\nfunc(packagePath, v)")
}

View File

@ -2,18 +2,16 @@ package main
import (
"code.squareroundforest.org/arpio/docreflect/generate"
"log"
"os"
"code.squareroundforest.org/arpio/wand"
)
func main() {
args := os.Args[1:]
if args[0] == "generate" {
args = args[1:]
}
var version = "dev"
packageName, args := args[0], args[1:]
if err := generate.GenerateRegistry(generate.Options{}, os.Stdout, packageName, args...); err != nil {
log.Fatalln(err)
}
func main() {
cmd := wand.Command("generate-registry", generate.GenerateRegistry)
cmd = wand.Default(cmd)
cmd = wand.ShortForm(cmd, "m", "main")
cmd = wand.Group("docreflect", cmd)
cmd = wand.Version(cmd, version)
wand.Exec(cmd)
}

90
cmd/docreflect/readme.md Normal file
View File

@ -0,0 +1,90 @@
# docreflect
## Synopsis:
```
docreflect [options]... [--] <outputPackageName string> [path string]...
docreflect <subcommand>
```
## Description:
generates a Go source file containing an init function that registers the documentation for the declarations
specified by their fully qualified Go path.
The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package paths
is recommended over specific symbols in most cases.
Limitations:
\- Type references (such as type aliases or definitions based on named types) are not resolved.
\- Import paths are not followed.
## Options:
- --main, -m bool: indicates that the docs for the symbols will be lookded up as part of the main package of an
executable. This is necessary, because the fully qualified Go path of symbols in a main package differs from
the path pointing to that package in a module.
- --help: Show help.
Hints:
- Bool options can be used with implicit true values.
- Option values can be set both via = or just separated by space.
## Subcommands:
Show help for each subcommand by calling \<command\> help or \<command\> --help.
### docreflect generate-registry (default)
#### Synopsis:
```
docreflect generate-registry [options]... [--] <outputPackageName string> [path string]...
docreflect generate-registry <subcommand>
```
#### Description:
generates a Go source file containing an init function that registers the documentation for the declarations
specified by their fully qualified Go path.
The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package paths
is recommended over specific symbols in most cases.
Limitations:
\- Type references (such as type aliases or definitions based on named types) are not resolved.
\- Import paths are not followed.
#### Options:
- --main, -m bool: indicates that the docs for the symbols will be lookded up as part of the main package of an
executable. This is necessary, because the fully qualified Go path of symbols in a main package differs from
the path pointing to that package in a module.
- --help: Show help.
### docreflect version
Show version.
## Environment variables:
Every command line option's value can also be provided as an environment variable. Environment variable names
need to use snake casing like myapp\_foo\_bar\_baz or MYAPP\_FOO\_BAR\_BAZ, or other casing that doesn't include the
'-' dash character, and they need to be prefixed with the name of the application, as in the base name of the
command.
When both the environment variable and the command line option is defined, the command line option overrides the
environment variable. Multiple values for the same environment variable can be defined by concatenating the
values with the ':' separator character. When overriding multiple values with command line options, all the
environment values of the same field are dropped.
### Example environment variable:
```
DOCREFLECT_MAIN=true
```

651
generate/generate.go Normal file
View File

@ -0,0 +1,651 @@
package generate
import (
"fmt"
"go/ast"
"go/build"
"go/doc"
"go/parser"
"go/token"
"golang.org/x/mod/modfile"
"io"
"io/fs"
"os"
"path"
"runtime"
"slices"
"sort"
"strconv"
"strings"
)
const header = `/*
Generated with https://code.squareroundforest.org/arpio/docreflect
*/
`
type options struct {
wd string
goroot string
modules map[string]string
isMain bool
}
func getGoroot() string {
gr := os.Getenv("GOROOT")
if gr != "" {
return gr
}
return runtime.GOROOT()
}
func modCache() string {
mc := os.Getenv("GOMODCACHE")
if mc != "" {
return mc
}
gp := os.Getenv("GOPATH")
if gp == "" {
gp = path.Join(os.Getenv("HOME"), "go")
}
mc = path.Join(gp, "pkg/mod")
return mc
}
func findGoMod(dir string) (string, bool) {
p := path.Join(dir, "go.mod")
f, err := os.Stat(p)
if err == nil && !f.IsDir() {
return p, true
}
if dir == "/" {
return "", false
}
return findGoMod(path.Dir(dir))
}
func readGomod(wd string) map[string]string {
p, ok := findGoMod(wd)
if !ok {
return nil
}
d, err := os.ReadFile(p)
if err != nil {
return nil
}
m, err := modfile.Parse(p, d, nil)
if err != nil {
return nil
}
var dirs map[string]string
mc := modCache()
for _, dep := range m.Require {
p := dep.Mod.String()
dirs = set(dirs, p, path.Join(mc, p))
}
name := m.Module.Mod.String()
dirs[name] = path.Dir(p)
return dirs
}
func initOptions() options {
wd, _ := os.Getwd()
gr := getGoroot()
modules := readGomod(wd)
return options{
wd: wd,
goroot: gr,
modules: modules,
}
}
func cleanPaths(gopath []string) []string {
var c []string
for _, pi := range gopath {
pi = path.Clean(pi)
c = append(c, pi)
}
return c
}
func splitGopath(p string) (string, string) {
parts := strings.Split(p, "/")
last := len(parts) - 1
parts, lastPart := parts[:last], parts[last]
symbolParts := strings.Split(lastPart, ".")
pkg := symbolParts[0]
return strings.Join(append(parts, pkg), "/"), strings.Join(symbolParts[1:], ".")
}
func packagePaths(p []string) []string {
var (
pp []string
m map[string]bool
)
for _, pi := range p {
ppi, _ := splitGopath(pi)
if m[ppi] {
continue
}
pp = append(pp, ppi)
m = set(m, ppi, true)
}
return pp
}
func collectGoDirs(o options) []string {
var dirs []string
if o.goroot != "" {
dirs = append(dirs, path.Join(o.goroot, "src"))
dirs = append(dirs, path.Join(o.goroot, "src", "cmd"))
}
return dirs
}
func importPackages(o options, godirs, paths []string) ([]*build.Package, error) {
var pkgs []*build.Package
for _, p := range paths {
var found bool
for mod, modDir := range o.modules {
if !strings.HasPrefix(p, strings.Split(mod, "@")[0]) {
continue
}
pkg, err := build.Import(p, modDir, build.ImportComment)
if err != nil || pkg == nil {
continue
}
pkgs = append(pkgs, pkg)
found = true
break
}
if found {
continue
}
for _, d := range godirs {
pkg, err := build.Import(p, d, build.ImportComment)
if err != nil || pkg == nil {
continue
}
pkgs = append(pkgs, pkg)
found = true
break
}
if !found {
return nil, fmt.Errorf("failed to import package for %s", p)
}
}
return pkgs, nil
}
func parserInclude(pkg *build.Package) func(fs.FileInfo) bool {
return func(file fs.FileInfo) bool {
for _, fn := range pkg.GoFiles {
if fn == file.Name() {
return true
}
}
return false
}
}
func parsePackages(pkgs []*build.Package) (map[string][]*ast.Package, error) {
var ppkgs map[string][]*ast.Package
for _, p := range pkgs {
fset := token.NewFileSet()
pm, err := parser.ParseDir(fset, p.Dir, parserInclude(p), parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse package %s: %w", p.Name, err)
}
for _, pp := range pm {
if pp == nil {
continue
}
ppkgs = set(ppkgs, p.ImportPath, append(ppkgs[p.ImportPath], pp))
}
}
return ppkgs, nil
}
func fixDocPackage(p doc.Package) doc.Package {
for _, t := range p.Types {
p.Consts = append(p.Consts, t.Consts...)
p.Vars = append(p.Vars, t.Vars...)
p.Funcs = append(p.Funcs, t.Funcs...)
}
return p
}
func set[K comparable, V any](m map[K]V, key K, value V) map[K]V {
if m == nil {
m = make(map[K]V)
}
m[key] = value
return m
}
func merge[K comparable, V any](m ...map[K]V) map[K]V {
if len(m) == 1 {
return m[0]
}
var mm map[K]V
for key, value := range m[0] {
mm = set(mm, key, value)
}
mr := merge(m[1:]...)
for key, value := range mr {
mm = set(mm, key, value)
}
return mm
}
func unpack(e ast.Expr) ast.Expr {
p, ok := e.(*ast.StarExpr)
if !ok {
return e
}
return unpack(p.X)
}
func funcParams(f *doc.Func) string {
if f.Decl == nil || f.Decl.Type == nil || f.Decl.Type.Params == nil {
return "func()"
}
var paramNames []string
for _, p := range f.Decl.Type.Params.List {
for _, n := range p.Names {
if n == nil {
continue
}
paramNames = append(paramNames, n.Name)
}
}
return fmt.Sprintf("func(%s)", strings.Join(paramNames, ", "))
}
func funcDocs(f *doc.Func) string {
return fmt.Sprintf("%s\n%s", f.Doc, funcParams(f))
}
func findFieldDocs(str *ast.StructType, fieldPath []string) (string, bool) {
if str.Fields == nil {
return "", false
}
for _, f := range str.Fields.List {
var found bool
for _, fn := range f.Names {
if fn != nil && fn.Name == fieldPath[0] {
found = true
break
}
}
if !found {
continue
}
if len(fieldPath) == 1 {
if f.Doc == nil {
return "", true
}
return f.Doc.Text(), true
}
te := unpack(f.Type)
fstr, ok := te.(*ast.StructType)
if !ok {
return "", false
}
return findFieldDocs(fstr, fieldPath[1:])
}
return "", false
}
func structFieldDocs(t *doc.Type, fieldPath []string) (string, bool) {
if t.Decl == nil || len(t.Decl.Specs) != 1 {
return "", false
}
ts, ok := t.Decl.Specs[0].(*ast.TypeSpec)
if !ok {
return "", false
}
te := unpack(ts.Type)
str, ok := te.(*ast.StructType)
if !ok {
return "", false
}
return findFieldDocs(str, fieldPath)
}
func typeMethodDocs(t *doc.Type, name string) (string, bool) {
for _, m := range t.Methods {
if m.Name != name {
continue
}
return funcDocs(m), true
}
return "", false
}
func symbolDocs(pkg *doc.Package, gopath string) (string, bool) {
_, s := splitGopath(gopath)
symbol := strings.Split(s, ".")
if len(symbol) == 1 {
for _, c := range pkg.Consts {
if c == nil || !slices.Contains(c.Names, symbol[0]) {
continue
}
return c.Doc, true
}
for _, v := range pkg.Vars {
if v == nil || !slices.Contains(v.Names, symbol[0]) {
continue
}
return v.Doc, true
}
for _, f := range pkg.Funcs {
if f == nil || f.Name != symbol[0] {
continue
}
return funcDocs(f), true
}
}
for _, t := range pkg.Types {
if t == nil || t.Name != symbol[0] {
continue
}
if len(symbol) == 1 {
return t.Doc, true
}
if d, ok := structFieldDocs(t, symbol[1:]); ok {
return d, true
}
if len(symbol) != 2 {
return "", false
}
if d, ok := typeMethodDocs(t, symbol[1]); ok {
return d, ok
}
}
return "", false
}
func symbolPath(packagePath string, name ...string) string {
return fmt.Sprintf("%s.%s", packagePath, strings.Join(name, "."))
}
func valueDocs(packagePath string, v []*doc.Value) map[string]string {
var d map[string]string
for _, vi := range v {
if vi != nil {
for _, n := range vi.Names {
d = set(d, symbolPath(packagePath, n), vi.Doc)
}
}
}
return d
}
func takeFieldDocs(packagePath string, prefix []string, f *ast.Field) map[string]string {
var docs map[string]string
for _, fn := range f.Names {
if fn == nil {
continue
}
docs = set(docs, symbolPath(packagePath, append(prefix, fn.Name)...), f.Doc.Text())
te := unpack(f.Type)
fst, ok := te.(*ast.StructType)
if !ok || fst.Fields == nil {
continue
}
for _, fi := range fst.Fields.List {
if fi == nil {
continue
}
docs = merge(docs, takeFieldDocs(packagePath, append(prefix, fn.Name), fi))
}
}
return docs
}
func structFieldsDocs(importPath string, t *doc.Type) map[string]string {
if t.Decl == nil || len(t.Decl.Specs) != 1 {
return nil
}
ts, ok := t.Decl.Specs[0].(*ast.TypeSpec)
if !ok {
return nil
}
te := unpack(ts.Type)
str, ok := te.(*ast.StructType)
if !ok || str.Fields == nil {
return nil
}
var d map[string]string
for _, f := range str.Fields.List {
if f == nil {
continue
}
d = merge(d, takeFieldDocs(importPath, []string{t.Name}, f))
}
return d
}
func methodDocs(importPath string, t *doc.Type) map[string]string {
var d map[string]string
for _, m := range t.Methods {
if m != nil {
d = set(d, symbolPath(importPath, t.Name, m.Name), funcDocs(m))
}
}
return d
}
func typeDocs(importPath string, types []*doc.Type) map[string]string {
var d map[string]string
for _, t := range types {
if t == nil {
continue
}
d = set(d, symbolPath(importPath, t.Name), t.Doc)
d = merge(d, structFieldsDocs(importPath, t))
d = merge(d, methodDocs(importPath, t))
}
return d
}
func packageFuncDocs(importPath string, funcs []*doc.Func) map[string]string {
var d map[string]string
for _, f := range funcs {
if f != nil {
d = set(d, symbolPath(importPath, f.Name), funcDocs(f))
}
}
return d
}
func packageDocs(pkg *doc.Package) map[string]string {
return merge(
map[string]string{pkg.ImportPath: pkg.Doc},
valueDocs(pkg.ImportPath, pkg.Consts),
valueDocs(pkg.ImportPath, pkg.Vars),
typeDocs(pkg.ImportPath, pkg.Types),
packageFuncDocs(pkg.ImportPath, pkg.Funcs),
)
}
func replacePath(p, prefix, replace string) string {
if !strings.HasPrefix(p, prefix) {
return p
}
return fmt.Sprintf("%s%s", replace, p[len(prefix):])
}
func replacePaths(m map[string]string, prefix, replace string) map[string]string {
var mm map[string]string
for p, d := range m {
mm = set(mm, replacePath(p, prefix, replace), d)
}
return mm
}
func takeDocs(o options, pkgs map[string][]*ast.Package, gopaths []string) (map[string]string, error) {
var dm map[string]string
for _, gp := range gopaths {
pp, _ := splitGopath(gp)
isPackage := pp == gp
for _, pkg := range pkgs[pp] {
dpkg := doc.New(pkg, pp, doc.AllDecls|doc.PreserveAST)
*dpkg = fixDocPackage(*dpkg)
if isPackage {
pd := packageDocs(dpkg)
if o.isMain {
pd = replacePaths(pd, pp, "main")
}
dm = merge(dm, pd)
continue
}
sd, ok := symbolDocs(dpkg, gp)
if !ok {
return nil, fmt.Errorf("symbol not found: %s", gp)
}
if o.isMain {
gp = replacePath(gp, pp, "main")
}
dm = set(dm, gp, sd)
}
}
return dm, nil
}
func generate(o options, gopaths ...string) (map[string]string, error) {
gopaths = cleanPaths(gopaths)
ppaths := packagePaths(gopaths)
dirs := collectGoDirs(o)
pkgs, err := importPackages(o, dirs, ppaths)
if err != nil {
return nil, err
}
ppkgs, err := parsePackages(pkgs)
if err != nil {
return nil, err
}
return takeDocs(o, ppkgs, gopaths)
}
func format(w io.Writer, pname string, docs map[string]string) error {
var err error
printf := func(f string, a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintf(w, f, a...)
}
println := func(a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintln(w, a...)
}
println(header)
printf("package %s\n", pname)
println("import \"code.squareroundforest.org/arpio/docreflect\"")
println("func init() {")
var paths []string
for path := range docs {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
doc := docs[path]
path, doc = strconv.Quote(path), strconv.Quote(doc)
printf("docreflect.Register(%s, %s)\n", path, doc)
}
printf("}")
return err
}

View File

@ -1,677 +1,32 @@
// Package generate provides a generator to generate go code from go docs that registers doc entries
// for use with the docreflect package.
// Package generate produces Go source code that registers documentation for use with the docreflect package.
package generate
import (
"fmt"
"go/ast"
"go/build"
"go/doc"
"go/parser"
"go/token"
"golang.org/x/mod/modfile"
"io"
"io/fs"
"os"
"path"
"runtime"
"slices"
"sort"
"strconv"
"strings"
)
const header = `/*
Generated with https://code.squareroundforest.org/arpio/docreflect
*/
`
import "io"
// Options contains options for the generator.
type Options struct {
// Main indicates that the docs for the symbols will be lookded up as part of the main package of an
// executable.
// executable. This is necessary, because the fully qualified Go path of symbols in a main package differs
// from the path pointing to that package in a module.
Main bool
}
type options struct {
wd string
goroot string
modules map[string]string
isMain bool
}
func getGoroot() string {
gr := os.Getenv("GOROOT")
if gr != "" {
return gr
}
return runtime.GOROOT()
}
func modCache() string {
mc := os.Getenv("GOMODCACHE")
if mc != "" {
return mc
}
gp := os.Getenv("GOPATH")
if gp == "" {
gp = path.Join(os.Getenv("HOME"), "go")
}
mc = path.Join(gp, "pkg/mod")
return mc
}
func findGoMod(dir string) (string, bool) {
p := path.Join(dir, "go.mod")
f, err := os.Stat(p)
if err == nil && !f.IsDir() {
return p, true
}
if dir == "/" {
return "", false
}
return findGoMod(path.Dir(dir))
}
func readGomod(wd string) map[string]string {
p, ok := findGoMod(wd)
if !ok {
return nil
}
d, err := os.ReadFile(p)
if err != nil {
return nil
}
m, err := modfile.Parse(p, d, nil)
if err != nil {
return nil
}
var dirs map[string]string
mc := modCache()
for _, dep := range m.Require {
p := dep.Mod.String()
dirs = set(dirs, p, path.Join(mc, p))
}
name := m.Module.Mod.String()
dirs[name] = path.Dir(p)
return dirs
}
func initOptions() options {
wd, _ := os.Getwd()
gr := getGoroot()
modules := readGomod(wd)
return options{
wd: wd,
goroot: gr,
modules: modules,
}
}
func cleanPaths(gopath []string) []string {
var c []string
for _, pi := range gopath {
pi = path.Clean(pi)
c = append(c, pi)
}
return c
}
func splitGopath(p string) (string, string) {
parts := strings.Split(p, "/")
last := len(parts) - 1
parts, lastPart := parts[:last], parts[last]
symbolParts := strings.Split(lastPart, ".")
pkg := symbolParts[0]
return strings.Join(append(parts, pkg), "/"), strings.Join(symbolParts[1:], ".")
}
func packagePaths(p []string) []string {
var (
pp []string
m map[string]bool
)
for _, pi := range p {
ppi, _ := splitGopath(pi)
if m[ppi] {
continue
}
pp = append(pp, ppi)
m = set(m, ppi, true)
}
return pp
}
func collectGoDirs(o options) []string {
var dirs []string
if o.goroot != "" {
dirs = append(dirs, path.Join(o.goroot, "src"))
dirs = append(dirs, path.Join(o.goroot, "src", "cmd"))
}
return dirs
}
func importPackages(o options, godirs, paths []string) ([]*build.Package, error) {
var pkgs []*build.Package
for _, p := range paths {
var found bool
for mod, modDir := range o.modules {
if !strings.HasPrefix(p, strings.Split(mod, "@")[0]) {
continue
}
pkg, err := build.Import(p, modDir, build.ImportComment)
if err != nil || pkg == nil {
continue
}
pkgs = append(pkgs, pkg)
found = true
break
}
if found {
continue
}
for _, d := range godirs {
pkg, err := build.Import(p, d, build.ImportComment)
if err != nil || pkg == nil {
continue
}
pkgs = append(pkgs, pkg)
found = true
break
}
if !found {
return nil, fmt.Errorf("failed to import package for %s", p)
}
}
return pkgs, nil
}
func parserInclude(pkg *build.Package) func(fs.FileInfo) bool {
return func(file fs.FileInfo) bool {
for _, fn := range pkg.GoFiles {
if fn == file.Name() {
return true
}
}
return false
}
}
func parsePackages(pkgs []*build.Package) (map[string][]*ast.Package, error) {
var ppkgs map[string][]*ast.Package
for _, p := range pkgs {
fset := token.NewFileSet()
pm, err := parser.ParseDir(fset, p.Dir, parserInclude(p), parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse package %s: %w", p.Name, err)
}
for _, pp := range pm {
if pp == nil {
continue
}
ppkgs = set(ppkgs, p.ImportPath, append(ppkgs[p.ImportPath], pp))
}
}
return ppkgs, nil
}
func fixDocPackage(p doc.Package) doc.Package {
for _, t := range p.Types {
p.Consts = append(p.Consts, t.Consts...)
p.Vars = append(p.Vars, t.Vars...)
p.Funcs = append(p.Funcs, t.Funcs...)
}
return p
}
func set[K comparable, V any](m map[K]V, key K, value V) map[K]V {
if m == nil {
m = make(map[K]V)
}
m[key] = value
return m
}
func merge[K comparable, V any](m ...map[K]V) map[K]V {
if len(m) == 1 {
return m[0]
}
var mm map[K]V
for key, value := range m[0] {
mm = set(mm, key, value)
}
mr := merge(m[1:]...)
for key, value := range mr {
mm = set(mm, key, value)
}
return mm
}
func unpack(e ast.Expr) ast.Expr {
p, ok := e.(*ast.StarExpr)
if !ok {
return e
}
return unpack(p.X)
}
func funcParams(f *doc.Func) string {
if f.Decl == nil || f.Decl.Type == nil || f.Decl.Type.Params == nil {
return "func()"
}
var paramNames []string
for _, p := range f.Decl.Type.Params.List {
for _, n := range p.Names {
if n == nil {
continue
}
paramNames = append(paramNames, n.Name)
}
}
return fmt.Sprintf("func(%s)", strings.Join(paramNames, ", "))
}
func funcDocs(f *doc.Func) string {
return fmt.Sprintf("%s\n%s", f.Doc, funcParams(f))
}
func findFieldDocs(str *ast.StructType, fieldPath []string) (string, bool) {
if str.Fields == nil {
return "", false
}
for _, f := range str.Fields.List {
var found bool
for _, fn := range f.Names {
if fn != nil && fn.Name == fieldPath[0] {
found = true
break
}
}
if !found {
continue
}
if len(fieldPath) == 1 {
if f.Doc == nil {
return "", true
}
return f.Doc.Text(), true
}
te := unpack(f.Type)
fstr, ok := te.(*ast.StructType)
if !ok {
return "", false
}
return findFieldDocs(fstr, fieldPath[1:])
}
return "", false
}
func structFieldDocs(t *doc.Type, fieldPath []string) (string, bool) {
if t.Decl == nil || len(t.Decl.Specs) != 1 {
return "", false
}
ts, ok := t.Decl.Specs[0].(*ast.TypeSpec)
if !ok {
return "", false
}
te := unpack(ts.Type)
str, ok := te.(*ast.StructType)
if !ok {
return "", false
}
return findFieldDocs(str, fieldPath)
}
func typeMethodDocs(t *doc.Type, name string) (string, bool) {
for _, m := range t.Methods {
if m.Name != name {
continue
}
return funcDocs(m), true
}
return "", false
}
func symbolDocs(pkg *doc.Package, gopath string) (string, bool) {
_, s := splitGopath(gopath)
symbol := strings.Split(s, ".")
if len(symbol) == 1 {
for _, c := range pkg.Consts {
if c == nil || !slices.Contains(c.Names, symbol[0]) {
continue
}
return c.Doc, true
}
for _, v := range pkg.Vars {
if v == nil || !slices.Contains(v.Names, symbol[0]) {
continue
}
return v.Doc, true
}
for _, f := range pkg.Funcs {
if f == nil || f.Name != symbol[0] {
continue
}
return funcDocs(f), true
}
}
for _, t := range pkg.Types {
if t == nil || t.Name != symbol[0] {
continue
}
if len(symbol) == 1 {
return t.Doc, true
}
if d, ok := structFieldDocs(t, symbol[1:]); ok {
return d, true
}
if len(symbol) != 2 {
return "", false
}
if d, ok := typeMethodDocs(t, symbol[1]); ok {
return d, ok
}
}
return "", false
}
func symbolPath(packagePath string, name ...string) string {
return fmt.Sprintf("%s.%s", packagePath, strings.Join(name, "."))
}
func valueDocs(packagePath string, v []*doc.Value) map[string]string {
var d map[string]string
for _, vi := range v {
if vi != nil {
for _, n := range vi.Names {
d = set(d, symbolPath(packagePath, n), vi.Doc)
}
}
}
return d
}
func takeFieldDocs(packagePath string, prefix []string, f *ast.Field) map[string]string {
var docs map[string]string
for _, fn := range f.Names {
if fn == nil {
continue
}
docs = set(docs, symbolPath(packagePath, append(prefix, fn.Name)...), f.Doc.Text())
te := unpack(f.Type)
fst, ok := te.(*ast.StructType)
if !ok || fst.Fields == nil {
continue
}
for _, fi := range fst.Fields.List {
if fi == nil {
continue
}
docs = merge(docs, takeFieldDocs(packagePath, append(prefix, fn.Name), fi))
}
}
return docs
}
func structFieldsDocs(importPath string, t *doc.Type) map[string]string {
if t.Decl == nil || len(t.Decl.Specs) != 1 {
return nil
}
ts, ok := t.Decl.Specs[0].(*ast.TypeSpec)
if !ok {
return nil
}
te := unpack(ts.Type)
str, ok := te.(*ast.StructType)
if !ok || str.Fields == nil {
return nil
}
var d map[string]string
for _, f := range str.Fields.List {
if f == nil {
continue
}
d = merge(d, takeFieldDocs(importPath, []string{t.Name}, f))
}
return d
}
func methodDocs(importPath string, t *doc.Type) map[string]string {
var d map[string]string
for _, m := range t.Methods {
if m != nil {
d = set(d, symbolPath(importPath, t.Name, m.Name), funcDocs(m))
}
}
return d
}
func typeDocs(importPath string, types []*doc.Type) map[string]string {
var d map[string]string
for _, t := range types {
if t == nil {
continue
}
d = set(d, symbolPath(importPath, t.Name), t.Doc)
d = merge(d, structFieldsDocs(importPath, t))
d = merge(d, methodDocs(importPath, t))
}
return d
}
func packageFuncDocs(importPath string, funcs []*doc.Func) map[string]string {
var d map[string]string
for _, f := range funcs {
if f != nil {
d = set(d, symbolPath(importPath, f.Name), funcDocs(f))
}
}
return d
}
func packageDocs(pkg *doc.Package) map[string]string {
return merge(
map[string]string{pkg.ImportPath: pkg.Doc},
valueDocs(pkg.ImportPath, pkg.Consts),
valueDocs(pkg.ImportPath, pkg.Vars),
typeDocs(pkg.ImportPath, pkg.Types),
packageFuncDocs(pkg.ImportPath, pkg.Funcs),
)
}
func replacePath(p, prefix, replace string) string {
if !strings.HasPrefix(p, prefix) {
return p
}
return fmt.Sprintf("%s%s", replace, p[len(prefix):])
}
func replacePaths(m map[string]string, prefix, replace string) map[string]string {
var mm map[string]string
for p, d := range m {
mm = set(mm, replacePath(p, prefix, replace), d)
}
return mm
}
func takeDocs(o options, pkgs map[string][]*ast.Package, gopaths []string) (map[string]string, error) {
var dm map[string]string
for _, gp := range gopaths {
pp, _ := splitGopath(gp)
isPackage := pp == gp
for _, pkg := range pkgs[pp] {
dpkg := doc.New(pkg, pp, doc.AllDecls|doc.PreserveAST)
*dpkg = fixDocPackage(*dpkg)
if isPackage {
pd := packageDocs(dpkg)
if o.isMain {
pd = replacePaths(pd, pp, "main")
}
dm = merge(dm, pd)
continue
}
sd, ok := symbolDocs(dpkg, gp)
if !ok {
return nil, fmt.Errorf("symbol not found: %s", gp)
}
if o.isMain {
gp = replacePath(gp, pp, "main")
}
dm = set(dm, gp, sd)
}
}
return dm, nil
}
func generate(o options, gopaths ...string) (map[string]string, error) {
gopaths = cleanPaths(gopaths)
ppaths := packagePaths(gopaths)
dirs := collectGoDirs(o)
pkgs, err := importPackages(o, dirs, ppaths)
if err != nil {
return nil, err
}
ppkgs, err := parsePackages(pkgs)
if err != nil {
return nil, err
}
return takeDocs(o, ppkgs, gopaths)
}
func format(w io.Writer, pname string, docs map[string]string) error {
var err error
printf := func(f string, a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintf(w, f, a...)
}
println := func(a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintln(w, a...)
}
println(header)
printf("package %s\n", pname)
println("import \"code.squareroundforest.org/arpio/docreflect\"")
println("func init() {")
var paths []string
for path := range docs {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
doc := docs[path]
path, doc = strconv.Quote(path), strconv.Quote(doc)
printf("docreflect.Register(%s, %s)\n", path, doc)
}
printf("}")
return err
}
// GenerateRegistry generates a Go code file to the output, including a package init function that
// will register the documentation of the declarations specified by their gopath.
// GenerateRegistry generates a Go source file containing an init function that registers the documentation for
// the declarations specified by their fully qualified Go path.
//
// The gopath argument accepts any number of package, package level symbol, or struct field paths.
// It is recommended to use package paths unless special circumstances.
// The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package
// paths is recommended over specific symbols in most cases.
//
// Some important gotchas to keep in mind, GenerateRegistry does not resolve type references like
// type aliases, or type definitions based on named types, and it doesn't follow import paths.
func GenerateRegistry(o Options, w io.Writer, outputPackageName string, gopath ...string) error {
// Limitations:
//
// - Type references (such as type aliases or definitions based on named types) are not resolved.
//
// - Import paths are not followed.
func GenerateRegistry(o Options, w io.Writer, outputPackageName string, path ...string) error {
oo := initOptions()
oo.isMain = o.Main
d, err := generate(oo, gopath...)
d, err := generate(oo, path...)
if err != nil {
return err
}

15
go.mod
View File

@ -1,10 +1,19 @@
module code.squareroundforest.org/arpio/docreflect
go 1.24.3
go 1.25.3
require (
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239
code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4
golang.org/x/mod v0.27.0
)
require github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 // indirect
require (
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 // indirect
code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 // indirect
code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f // indirect
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
)

22
go.sum
View File

@ -1,6 +1,20 @@
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU=
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 h1:JzqT9RArcw2sD4QPAyTss/sHaCZvCv+91DDJPZOrShw=
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4/go.mod h1:myJFmFAZ/75y5xdA1jjpc4ItNJwdRqaL+TQhIvDU8Vk=
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 h1:SIgLIawD6Vv7rAvUobpVshLshdwFEJ0NOUrWpheS088=
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI=
code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 h1:b7voJlwe0jKH568X+O7b/JTAUrHLTSKNSSL+hhV2Q/Q=
code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9/go.mod h1:hq+2CENEd4bVSZnOdq38FUFOJJnF3OTQRv78qMGkNlE=
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 h1:JvLVMuvF2laxXkIZbHC1/0xtKyKndAwIHbIIWkHqTzc=
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI=
code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs=
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 h1:2aa62CYm9ld5SNoFxWzE2wUN0xjVWQ+xieoeFantdg4=
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18/go.mod h1:+0G3gufMAP8SCEIrDT1D/DaVOSfjS8EwPTBs5vfxqQg=
code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4 h1:+CjwwD1mWGHFpcIgP9F0uQ8JEepDBht7yojLum6n6QA=
code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4/go.mod h1:fPxs3LeGPxRMWUIXgBcdszk3a8d1TRqSHSVs5VL28Rc=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=

11
lib.go
View File

@ -1,8 +1,8 @@
// Package docreflect returns the Go documentation for packages, types and functions.
//
// The Go documentation of packages is not available during runtime by default, so in order to use docreflect, the documentation
// for the selected packages and symbols needs to be generated during build time. To generate the documentation, see the
// docreflect/generate package or the docreflect generate command.
// The Go documentation of packages is not available during runtime by default, so in order to use docreflect,
// the documentation for the selected packages and symbols needs to be generated during build time. To generate
// the documentation, see the docreflect/generate package or the docreflect generate command.
package docreflect
import (
@ -95,8 +95,9 @@ func functionParams(d string) []string {
return pp
}
// Register is used by the code generated by the docreflect/generate package or the docreflect generate command to register
// the selected documentation during startup. Register is not meant to be used directly by importing packages.
// Register is used by the code generated by the docreflect/generate package or the docreflect generate command
// to register the selected documentation during startup. Register is not meant to be used directly by importing
// packages.
func Register(gopath, docs string) {
registry[gopath] = docs
}

View File

@ -2,20 +2,23 @@
Library and command to help accessing go doc comments during runtime.
Go doc comments are not accessible during runtime via reflection. To make them avaiable during runtime, we need to capture them
during build time. The docreflect command, or the docreflect/generate library package, fetches the go doc comments of the specified
declarations, and generates Go code that registers the docs for every declaration. Code that includes the generated initialization
code, will have the docs accessible via the top level docreflect package methods.
Go doc comments are not accessible during runtime via reflection. To make them avaiable during runtime, we need
to capture them during build time. The docreflect command, or the docreflect/generate library package, fetches
the go doc comments of the specified declarations, and generates Go code that registers the docs for every
declaration. Code that includes the generated initialization code, will have the docs accessible via the top
level docreflect package methods.
**Declarations:**
- the declarations must be absolute Go paths
- when passing in the import path of a package, all the top level symbols of the package, plus the struct fields and methods of the
top level types will be included
- when passing in the import path of a package, all the top level symbols of the package, plus the struct fields
and methods of the top level types will be included
- the package documentation can be fetched using `docreflect.Docs("absolute/import/path/of/package")`
- when passing in the import path of only the selected symbols, the rest of the package level symbols will be ignroed
- when passing in the import path of only the selected symbols, the rest of the package level symbols will be
ignroed
**Gotchas:**
- type aliases and type definitions based on named types are not resolved
- package imports are not resolved, necessary packages need to be included in the generate arguments
@ -30,7 +33,10 @@ make install
Usage of the docreflect command:
```
docreflect generate --package-name mypackage coderepos.org/jdoe/mypackage coderepos.org/jdoe/otherpackage > docreflect.go
docreflect generate \
--package-name mypackage \
coderepos.org/jdoe/mypackage coderepos.org/jdoe/otherpackage \
> docreflect.go
```
*Made in Berlin, DE*
*Made in Berlin, DE*

18
scripts/man.go Normal file
View File

@ -0,0 +1,18 @@
package main
import (
"code.squareroundforest.org/arpio/wand/tools"
"log"
"os"
)
func main() {
o := tools.ManOptions{
Version: os.Args[1],
DateString: os.Args[2],
}
if err := tools.Man(os.Stdout, o, os.Args[3]); err != nil {
log.Fatalln(err)
}
}

14
scripts/markdown.go Normal file
View File

@ -0,0 +1,14 @@
package main
import (
"code.squareroundforest.org/arpio/wand/tools"
"log"
"os"
)
func main() {
var o tools.MarkdownOptions
if err := tools.Markdown(os.Stdout, o, os.Args[1]); err != nil {
log.Fatalln(err)
}
}