diff options
author | EuAndreh <eu@euandre.org> | 2025-05-11 07:58:03 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2025-05-11 07:58:03 -0300 |
commit | 04c91c40d548bb70062fcadeeeea37fd4319e066 (patch) | |
tree | e6c9c2d04de7544e5d3b0f2d48076c1da97a8975 | |
parent | src/gistatic.go: Add missing trailing newline to usage() output (diff) | |
download | gistatic-04c91c40d548bb70062fcadeeeea37fd4319e066.tar.gz gistatic-04c91c40d548bb70062fcadeeeea37fd4319e066.tar.xz |
Finish branches.html and setup i18n of manpages and HTML strings
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | Makefile | 43 | ||||
-rw-r--r-- | deps.mk | 7 | ||||
-rw-r--r-- | doc/gistatic.en.0.adoc | 7 | ||||
-rw-r--r-- | doc/gistatic.pt.0.adoc | 7 | ||||
-rwxr-xr-x | mkdeps.sh | 10 | ||||
-rw-r--r-- | po/doc/doc.pot | 39 | ||||
-rw-r--r-- | po/doc/note.txt | 5 | ||||
-rw-r--r-- | po/doc/po4a.cfg | 5 | ||||
-rw-r--r-- | po/doc/pt.po | 40 | ||||
-rw-r--r-- | po/gistatic/gistatic.pot | 86 | ||||
-rw-r--r-- | po/gistatic/po4a.cfg | 3 | ||||
-rw-r--r-- | po/gistatic/pt.po | 87 | ||||
-rw-r--r-- | src/gistatic.go | 1064 | ||||
-rw-r--r-- | tests/gistatic.go | 102 |
15 files changed, 1481 insertions, 30 deletions
@@ -1,6 +1,8 @@ -/src/version.go +!/doc/*.adoc +/doc/* +/po/*/*.mo +/src/meta.go /*.bin -/*.db /src/*.a /src/*.bin /src/*cgo* @@ -24,16 +24,22 @@ GOLDFLAGS = -L $(GOLIBDIR) .SUFFIXES: -.SUFFIXES: .go .a .bin .bin-check +.SUFFIXES: .go .a .bin .bin-check .adoc .po .mo .go.a: go tool compile -I $(@D) $(GOCFLAGS) -o $@ -p $(*F) \ `find $< $$(if [ $(*F) != main ]; then \ - echo src/$(NAME).go src/version.go; fi) | uniq` + echo src/$(NAME).go src/meta.go; fi) | uniq` .a.bin: go tool link -L $(@D) $(GOLDFLAGS) -o $@ --extldflags '$(LDLIBS)' $< +.adoc: + asciidoctor -b manpage -o $@ $< + +.po.mo: + msgfmt -c -o $@ $< + all: @@ -46,19 +52,23 @@ mains.bin = $(mains.go:.go=.bin) functional-tests/lib.a = $(functional-tests/lib.go:.go=.a) fuzz-targets/lib.a = $(fuzz-targets/lib.go:.go=.a) benchmarks/lib.a = $(benchmarks/lib.go:.go=.a) +manpages.N = $(manpages.N.adoc:.adoc=) +sources.mo = $(sources.po:.po=.mo) sources = \ src/$(NAME).go \ - src/version.go \ + src/meta.go \ src/main.go \ derived-assets = \ - src/version.go \ + src/meta.go \ $(libs.a) \ $(mains.a) \ $(mains.bin) \ $(NAME).bin \ + $(manpages.N) \ + $(sources.mo) \ side-assets = \ tests/fuzz/corpus/ \ @@ -72,15 +82,18 @@ all: $(derived-assets) $(libs.a): Makefile deps.mk -$(libs.a): src/$(NAME).go src/version.go +$(libs.a): src/$(NAME).go src/meta.go $(fuzz-targets/lib.a): go tool compile $(GOCFLAGS) -o $@ -p $(NAME) -d=libfuzzer \ - $*.go src/$(NAME).go src/version.go + $*.go src/$(NAME).go src/meta.go -src/version.go: Makefile - echo 'package $(NAME); const Version = "$(VERSION)"' > $@ +src/meta.go: Makefile + echo 'package $(NAME)' > $@ + echo 'const Version = "$(VERSION)"' >> $@ + echo 'const Name = "$(NAME)"' >> $@ + echo 'const LOCALEDIR = "$(LOCALEDIR)"' >> $@ $(NAME).bin: src/main.bin ln -fs src/main.bin $@ @@ -137,6 +150,17 @@ bench: $(benchmarks/main.bin-check) +i18n-doc: + po4a po/doc/po4a.cfg + +i18n-code: + gotext src/$(NAME).go > po/$(NAME)/$(NAME).pot + po4a po/$(NAME)/po4a.cfg + +i18n: i18n-doc i18n-code + + + ## Remove *all* derived artifacts produced during the build. ## A dedicated test asserts that this is always true. clean: @@ -154,6 +178,7 @@ install: all cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME) cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)' cp $(sources) '$(DESTDIR)$(SRCDIR)' + mandir install '$(DESTDIR)$(MANDIR)' $(manpages.N) ## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror ## of the "install" target, and removes *all* that was installed. @@ -164,6 +189,8 @@ uninstall: '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \ '$(DESTDIR)$(SRCDIR)' \ + mandir uninstall '$(DESTDIR)$(MANDIR)' $(manpages.N) + ALWAYS: @@ -12,6 +12,13 @@ mains.go = \ tests/fuzz/internal-queue/main.go \ tests/main.go \ +manpages.N.adoc = \ + doc/gistatic.en.0.adoc \ + doc/gistatic.pt.0.adoc \ + +sources.po = \ + po/gistatic/pt.po \ + functional-tests/lib.go = \ tests/functional/empty-repository/gistatic.go \ diff --git a/doc/gistatic.en.0.adoc b/doc/gistatic.en.0.adoc new file mode 100644 index 0000000..85f48ad --- /dev/null +++ b/doc/gistatic.en.0.adoc @@ -0,0 +1,7 @@ += gistatic(0) + +== NAME + +gistatic - . + +Something, something. diff --git a/doc/gistatic.pt.0.adoc b/doc/gistatic.pt.0.adoc new file mode 100644 index 0000000..85f48ad --- /dev/null +++ b/doc/gistatic.pt.0.adoc @@ -0,0 +1,7 @@ += gistatic(0) + +== NAME + +gistatic - . + +Something, something. @@ -13,8 +13,18 @@ mains() { find src tests -name '*.go' | grep '/main\.go$' } +docs() { + find doc/*.adoc +} + +pos() { + find po/ -name '*.po' | grep -v '^po/doc/' +} + libs | varlist 'libs.go' mains | varlist 'mains.go' +docs | varlist 'manpages.N.adoc' +pos | varlist 'sources.po' find tests/functional/*/*.go -not -name main.go | varlist 'functional-tests/lib.go' find tests/functional/*/main.go | varlist 'functional-tests/main.go' diff --git a/po/doc/doc.pot b/po/doc/doc.pot new file mode 100644 index 0000000..7a24056 --- /dev/null +++ b/po/doc/doc.pot @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE +# Copyright (C) YEAR Free Software Foundation, Inc. +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2025-05-10 17:17-0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. type: Title = +#: doc/gistatic.en.0.adoc:1 +#, no-wrap +msgid "gistatic(0)" +msgstr "" + +#. type: Title == +#: doc/gistatic.en.0.adoc:3 +#, no-wrap +msgid "NAME" +msgstr "" + +#. type: Plain text +#: doc/gistatic.en.0.adoc:6 +msgid "gistatic - ." +msgstr "" + +#. type: Plain text +#: doc/gistatic.en.0.adoc:7 +msgid "Something, something." +msgstr "" diff --git a/po/doc/note.txt b/po/doc/note.txt new file mode 100644 index 0000000..45279a4 --- /dev/null +++ b/po/doc/note.txt @@ -0,0 +1,5 @@ +PO4A-HEADER: mode=eof + + + +// Generated from po4a(1). diff --git a/po/doc/po4a.cfg b/po/doc/po4a.cfg new file mode 100644 index 0000000..f47447c --- /dev/null +++ b/po/doc/po4a.cfg @@ -0,0 +1,5 @@ +[options] --keep 0 --master-charset UTF-8 --localized-charset UTF-8 --addendum-charset UTF-8 + +[po_directory] po/doc/ + +[type: asciidoc] doc/gistatic.en.0.adoc $lang:doc/gistatic.$lang.0.adoc diff --git a/po/doc/pt.po b/po/doc/pt.po new file mode 100644 index 0000000..4f18856 --- /dev/null +++ b/po/doc/pt.po @@ -0,0 +1,40 @@ +# Portuguese translations for package. +# Copyright (C) 2025 THE 'S COPYRIGHT HOLDER +# This file is distributed under the same license as the package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: EMAIL\n" +"POT-Creation-Date: 2025-05-10 17:17-0300\n" +"PO-Revision-Date: 2025-05-10 17:09-0300\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. type: Title = +#: doc/gistatic.en.0.adoc:1 +#, no-wrap +msgid "gistatic(0)" +msgstr "" + +#. type: Title == +#: doc/gistatic.en.0.adoc:3 +#, no-wrap +msgid "NAME" +msgstr "" + +#. type: Plain text +#: doc/gistatic.en.0.adoc:6 +msgid "gistatic - ." +msgstr "" + +#. type: Plain text +#: doc/gistatic.en.0.adoc:7 +msgid "Something, something." +msgstr "" diff --git a/po/gistatic/gistatic.pot b/po/gistatic/gistatic.pot new file mode 100644 index 0000000..4bb37b7 --- /dev/null +++ b/po/gistatic/gistatic.pot @@ -0,0 +1,86 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: \n" + "Report-Msgid-Bugs-To: EMAIL\n" + "POT-Creation-Date: 2025-05-11 07:56-0300\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" + "Language-Team: LANGUAGE <LL@li.org>\n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" + +#: src/gistatic.go:917 src/gistatic.go:997 +msgid "Outlined icon of 3 melting ice cubes" +msgstr "" + +#: src/gistatic.go:920 src/gistatic.go:1000 +msgid "en" +msgstr "" + +#: src/gistatic.go:929 +msgid "Date" +msgstr "" + +#: src/gistatic.go:1002 +msgid "Repositories" +msgstr "" + +#: src/gistatic.go:1004 +msgid "Description" +msgstr "" + +#: src/gistatic.go:922 +msgid "tags" +msgstr "" + +#: src/gistatic.go:924 +msgid "commit" +msgstr "" + +#: src/gistatic.go:1003 +msgid "Name" +msgstr "" + +#: src/gistatic.go:1005 +msgid "Last commit" +msgstr "" + +#: src/gistatic.go:921 +msgid "branches" +msgstr "" + +#: src/gistatic.go:926 +msgid "Tag" +msgstr "" + +#: src/gistatic.go:927 +msgid "Commit message" +msgstr "" + +#: src/gistatic.go:928 +msgid "Author" +msgstr "" + +#: src/gistatic.go:1001 +msgid "Listing of repositories" +msgstr "" + +#: src/gistatic.go:923 +msgid "log" +msgstr "" + +#: src/gistatic.go:925 +msgid "Branch" +msgstr "" + +#: src/gistatic.go:1006 +msgid "Generated with" +msgstr "" + diff --git a/po/gistatic/po4a.cfg b/po/gistatic/po4a.cfg new file mode 100644 index 0000000..5c2eb8f --- /dev/null +++ b/po/gistatic/po4a.cfg @@ -0,0 +1,3 @@ +[options] --keep 0 --master-charset UTF-8 --localized-charset UTF-8 --addendum-charset UTF-8 + +[po_directory] po/gistatic/ diff --git a/po/gistatic/pt.po b/po/gistatic/pt.po new file mode 100644 index 0000000..5a3e054 --- /dev/null +++ b/po/gistatic/pt.po @@ -0,0 +1,87 @@ +# Portuguese translations for package. +# Copyright (C) 2025 THE 'S COPYRIGHT HOLDER +# This file is distributed under the same license as the package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: EMAIL\n" +"POT-Creation-Date: 2025-05-11 07:56-0300\n" +"PO-Revision-Date: 2025-05-10 17:50-0300\n" +"Last-Translator: EuAndreh <eu@euandre.org>\n" +"Language-Team: none\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.5\n" + +#: src/gistatic.go:917 src/gistatic.go:997 +msgid "Outlined icon of 3 melting ice cubes" +msgstr "" + +#: src/gistatic.go:920 src/gistatic.go:1000 +msgid "en" +msgstr "pt" + +#: src/gistatic.go:929 +msgid "Date" +msgstr "" + +#: src/gistatic.go:1002 +msgid "Repositories" +msgstr "Repositórios" + +#: src/gistatic.go:1004 +msgid "Description" +msgstr "" + +#: src/gistatic.go:922 +msgid "tags" +msgstr "" + +#: src/gistatic.go:924 +msgid "commit" +msgstr "" + +#: src/gistatic.go:1003 +msgid "Name" +msgstr "" + +#: src/gistatic.go:1005 +msgid "Last commit" +msgstr "" + +#: src/gistatic.go:921 +msgid "branches" +msgstr "" + +#: src/gistatic.go:926 +msgid "Tag" +msgstr "" + +#: src/gistatic.go:927 +msgid "Commit message" +msgstr "" + +#: src/gistatic.go:928 +msgid "Author" +msgstr "" + +#: src/gistatic.go:1001 +msgid "Listing of repositories" +msgstr "Lista de repositórios" + +#: src/gistatic.go:923 +msgid "log" +msgstr "" + +#: src/gistatic.go:925 +msgid "Branch" +msgstr "" + +#: src/gistatic.go:1006 +msgid "Generated with" +msgstr "" diff --git a/src/gistatic.go b/src/gistatic.go index fdbd2c4..3a56db2 100644 --- a/src/gistatic.go +++ b/src/gistatic.go @@ -3,25 +3,1055 @@ package gistatic import ( "flag" "fmt" + "html/template" "io" "os" + "os/exec" + "path" + "slices" + "strings" g "gobang" + gt "gotext" ) +const ( + dateFmt = "--date=format:%Y-%m-%dT%H:%M" +) + +var ( + fn_wd = os.Getwd + fn_mkdirp = func(path string) error { + return os.MkdirAll(path, 0777) + } + fn_writeFile = func(path string, content string) error { + return os.WriteFile(path, []byte(content), 0644) + } + + logoSVGUncolored = g.Heredoc(` + <?xml version="1.0" encoding="UTF-8" standalone="no"?> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"> + <path fill="$color" d="M 0 8 L 1 8 L 1 9 L 0 9 L 0 8 Z" /> + <path fill="$color" d="M 0 13 L 1 13 L 1 14 L 0 14 L 0 13 Z" /> + <path fill="$color" d="M 1 8 L 2 8 L 2 9 L 1 9 L 1 8 Z" /> + <path fill="$color" d="M 1 13 L 2 13 L 2 14 L 1 14 L 1 13 Z" /> + <path fill="$color" d="M 2 8 L 3 8 L 3 9 L 2 9 L 2 8 Z" /> + <path fill="$color" d="M 2 13 L 3 13 L 3 14 L 2 14 L 2 13 Z" /> + <path fill="$color" d="M 3 8 L 4 8 L 4 9 L 3 9 L 3 8 Z" /> + <path fill="$color" d="M 3 13 L 4 13 L 4 14 L 3 14 L 3 13 Z" /> + <path fill="$color" d="M 4 7 L 5 7 L 5 8 L 4 8 L 4 7 Z" /> + <path fill="$color" d="M 4 8 L 5 8 L 5 9 L 4 9 L 4 8 Z" /> + <path fill="$color" d="M 4 13 L 5 13 L 5 14 L 4 14 L 4 13 Z" /> + <path fill="$color" d="M 5 6 L 6 6 L 6 7 L 5 7 L 5 6 Z" /> + <path fill="$color" d="M 5 7 L 6 7 L 6 8 L 5 8 L 5 7 Z" /> + <path fill="$color" d="M 5 13 L 6 13 L 6 14 L 5 14 L 5 13 Z" /> + <path fill="$color" d="M 6 5 L 7 5 L 7 6 L 6 6 L 6 5 Z" /> + <path fill="$color" d="M 6 6 L 7 6 L 7 7 L 6 7 L 6 6 Z" /> + <path fill="$color" d="M 6 14 L 7 14 L 7 15 L 6 15 L 6 14 Z" /> + <path fill="$color" d="M 7 1 L 8 1 L 8 2 L 7 2 L 7 1 Z" /> + <path fill="$color" d="M 7 14 L 8 14 L 8 15 L 7 15 L 7 14 Z" /> + <path fill="$color" d="M 7 15 L 8 15 L 8 16 L 7 16 L 7 15 Z" /> + <path fill="$color" d="M 7 2 L 8 2 L 8 3 L 7 3 L 7 2 Z" /> + <path fill="$color" d="M 7 3 L 8 3 L 8 4 L 7 4 L 7 3 Z" /> + <path fill="$color" d="M 7 4 L 8 4 L 8 5 L 7 5 L 7 4 Z" /> + <path fill="$color" d="M 7 5 L 8 5 L 8 6 L 7 6 L 7 5 Z" /> + <path fill="$color" d="M 8 1 L 9 1 L 9 2 L 8 2 L 8 1 Z" /> + <path fill="$color" d="M 8 15 L 9 15 L 9 16 L 8 16 L 8 15 Z" /> + <path fill="$color" d="M 9 1 L 10 1 L 10 2 L 9 2 L 9 1 Z" /> + <path fill="$color" d="M 9 2 L 10 2 L 10 3 L 9 3 L 9 2 Z" /> + <path fill="$color" d="M 9 6 L 10 6 L 10 7 L 9 7 L 9 6 Z" /> + <path fill="$color" d="M 9 15 L 10 15 L 10 16 L 9 16 L 9 15 Z" /> + <path fill="$color" d="M 10 2 L 11 2 L 11 3 L 10 3 L 10 2 Z" /> + <path fill="$color" d="M 10 3 L 11 3 L 11 4 L 10 4 L 10 3 Z" /> + <path fill="$color" d="M 10 4 L 11 4 L 11 5 L 10 5 L 10 4 Z" /> + <path fill="$color" d="M 10 5 L 11 5 L 11 6 L 10 6 L 10 5 Z" /> + <path fill="$color" d="M 10 6 L 11 6 L 11 7 L 10 7 L 10 6 Z" /> + <path fill="$color" d="M 11 6 L 12 6 L 12 7 L 11 7 L 11 6 Z" /> + <path fill="$color" d="M 11 8 L 12 8 L 12 9 L 11 9 L 11 8 Z" /> + <path fill="$color" d="M 10 15 L 11 15 L 11 16 L 10 16 L 10 15 Z" /> + <path fill="$color" d="M 11 10 L 12 10 L 12 11 L 11 11 L 11 10 Z" /> + <path fill="$color" d="M 11 12 L 12 12 L 12 13 L 11 13 L 11 12 Z" /> + <path fill="$color" d="M 11 14 L 12 14 L 12 15 L 11 15 L 11 14 Z" /> + <path fill="$color" d="M 11 15 L 12 15 L 12 16 L 11 16 L 11 15 Z" /> + <path fill="$color" d="M 12 6 L 13 6 L 13 7 L 12 7 L 12 6 Z" /> + <path fill="$color" d="M 12 8 L 13 8 L 13 9 L 12 9 L 12 8 Z" /> + <path fill="$color" d="M 12 10 L 13 10 L 13 11 L 12 11 L 12 10 Z" /> + <path fill="$color" d="M 12 12 L 13 12 L 13 13 L 12 13 L 12 12 Z" /> + <path fill="$color" d="M 12 14 L 13 14 L 13 15 L 12 15 L 12 14 Z" /> + <path fill="$color" d="M 13 6 L 14 6 L 14 7 L 13 7 L 13 6 Z" /> + <path fill="$color" d="M 13 8 L 14 8 L 14 9 L 13 9 L 13 8 Z" /> + <path fill="$color" d="M 13 10 L 14 10 L 14 11 L 13 11 L 13 10 Z" /> + <path fill="$color" d="M 13 12 L 14 12 L 14 13 L 13 13 L 13 12 Z" /> + <path fill="$color" d="M 13 13 L 14 13 L 14 14 L 13 14 L 13 13 Z" /> + <path fill="$color" d="M 13 14 L 14 14 L 14 15 L 13 15 L 13 14 Z" /> + <path fill="$color" d="M 14 7 L 15 7 L 15 8 L 14 8 L 14 7 Z" /> + <path fill="$color" d="M 14 8 L 15 8 L 15 9 L 14 9 L 14 8 Z" /> + <path fill="$color" d="M 14 9 L 15 9 L 15 10 L 14 10 L 14 9 Z" /> + <path fill="$color" d="M 14 10 L 15 10 L 15 11 L 14 11 L 14 10 Z" /> + <path fill="$color" d="M 14 11 L 15 11 L 15 12 L 14 12 L 14 11 Z" /> + <path fill="$color" d="M 14 12 L 15 12 L 15 13 L 14 13 L 14 12 Z" /> + </svg> + `) + + styleCSS = g.Heredoc(` + :root { + --color: black; + --background-color: white; + --background-contrast-color: hsl(0, 0%, 98%); + --hover-color: hsl(0, 0%, 93%); + --nav-color: hsl(0, 0%, 87%); + --selected-color: hsl(0, 0%, 80%); + --diff-added-color: hsl(120, 100%, 23%); + --diff-removed-color: hsl(0, 100%, 47%); + } + + @media(prefers-color-scheme: dark) { + :root { + --color: white; + --background-color: black; + --background-contrast-color: hsl(0, 0%, 2%); + --hover-color: hsl(0, 0%, 7%); + --nav-color: hsl(0, 0%, 13%); + --selected-color: hsl(0, 0%, 20%); + } + + body { + color: var(--color); + background-color: var(--background-color); + } + + a { + color: hsl(211, 100%, 60%); + } + + a:visited { + color: hsl(242, 100%, 80%); + } + } + + body { + font-family: monospace; + max-width: 1100px; + margin: 0 auto 0 auto; + } + + .logo { + height: 6em; + width: 6em; + } + + .header-horizontal-grouping { + display: flex; + align-items: center; + margin-top: 1em; + margin-bottom: 1em; + } + + .header-description { + margin-left: 2em; + } + + nav { + margin-top: 2em; + } + + nav ul { + display: flex; + list-style-type: none; + margin-bottom: 0; + } + + nav li { + margin-left: 10px; + } + + nav a, nav a:visited { + padding: 2px 8px 0px 8px; + color: var(--color); + } + + .selected-nav-item { + background-color: var(--nav-color); + } + + hr { + margin-top: 0; + border: 0; + border-top: 3px solid var(--nav-color); + } + + table { + margin: 2em auto; + } + + th { + padding-bottom: 1em; + } + + tbody tr:hover { + background-color: var(--hover-color); + } + + td { + padding-left: 1em; + padding-right: 1em; + } + + + /* commit page */ + + .diff-added, .diff-removed { + text-decoration: none; + } + + .diff-added:target, .diff-removed:target { + background-color: var(--selected-color); + } + + .diff-added, .diff-added:visited { + color: var(--diff-added-color); + } + + .diff-removed, .diff-removed:visited { + color: var(--diff-removed-color); + } + + + /* log page */ + + .log-commit-box { + padding: 1em; + margin: 1em; + background-color: var(--background-contrast-color); + } + + .log-commit-tag { + padding: 2px; + border: 1px solid; + color: var(--color); + } + + .log-head-highlight { + background-color: #ff8888; /* FIXME: hsl + dark-mode */ + } + + .log-branch-highlight { + background-color: #88ff88; /* FIXME: hsl + dark-mode */ + } + + .log-tag-highlight { + background-color: #ffff88; /* FIXME: hsl + dark-mode */ + } + + .pre-wrapping { + overflow: auto; + margin: 1em; + } + + .log-notes { + /* FIXME: yellow box goes until the end of the screen */ + padding: 1em; + background-color: #ffd; /* FIXME: hsl + dark-mode */ + } + + .log-pagination { + text-align: center; + margin: 2em; + } + + + footer { + text-align: center; + } + `) + + listingHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + repoindexHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .Repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + branchesHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.metadata.description}}" /> + <link rel="icon" type="image/svg+xml" href="../img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="../style.css" /> + <title>{{.branches}} · {{.metadata.name}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <a href="../"> + <picture> + <source srcset="../img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="../img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + </a> + <div class="header-description"> + <h1> + <a href="./"> + {{.metadata.name}} + </a> + </h1> + <h2> + {{.metadata.description}} + </h2>{{if .cloneURL}} + <code> + git clone {{.cloneURL}}/{{.metadata.name}}/ + </code>{{end}} + </div> + </div> + <nav> + <ul> + <li class="selected-nav-item"> + <a href="branches.html"> + {{.branches}} + </a> + </li> + <li> + <a href="tags.html"> + {{.tags}} + </a> + </li> + <li> + <a href="{{.log}}/"> + {{.log}} + </a> + </li> + </ul> + </nav> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.branch}} + </th> + <th> + {{.commitMessage}} + </th> + <th> + {{.author}} + </th> + <th> + {{.date}} + </th> + </tr> + </thead> + <tbody>{{range .data.branches}} + <tr> + <td> + <a href="{{$.log}}/{{.name}}.html"> + {{.name}} + </a> + </td> + <td> + <a href="{{$.commit}}/{{.sha}}.html"> + {{.messageSummary}} + </a> + </td> + <td> + {{.author}} + </td> + <td> + <time datetime="{{.date}}">{{.date}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + tagsHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .Repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + logHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .Repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + commitHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .Repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + + sourceHTML = g.Must(template.New("").Parse(g.Heredoc(` + <!DOCTYPE html> + <html lang="{{.language}}"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="description" content="{{.pageDescription}}" /> + <link rel="icon" type="image/svg+xml" href="img/favicon.svg" /> + <link rel="stylesheet" type="text/css" href="style.css" /> + <title>{{.title}}</title> + </head> + <body> + <header> + <div class="header-horizontal-grouping"> + <picture> + <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" /> + <img src="img/logo/light.svg" alt="{{.logoAlt}}" /> + </picture> + <h1 class="header-description"> + {{.title}} + </h1> + </div> + <hr /> + </header> + <main> + <table> + <thead> + <tr> + <th> + {{.name}} + </th> + <th> + {{.description}} + </th> + <th> + {{.lastCommit}} + </th> + </tr> + </thead> + <tbody>{{range .repositories}} + <tr> + <td> + <a href="{{.name}}"> + {{.name}} + </a> + </td> + <td> + {{.description}} + </td> + <td> + <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time> + </td> + </tr>{{end}} + </tbody> + </table> + </main> + <footer> + <hr /> + <p> + {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a> + </p> + </footer> + </body> + </html> + `))) + +) + + + +type repositoryTemplateT struct{ + template *template.Template + path string + keys map[string]interface{} +} + type argsT struct{ - outputPath string - cloneUrl string - allArgs []string + allArgs []string + subArgs []string + cloneURL string + outdir string } + +func logoSVG(color string) string { + return strings.ReplaceAll(logoSVGUncolored, "$color", color) +} + +func generateAssets(outdir string) error { + err := fn_mkdirp(outdir + "/img/logo") + if err != nil { + return err + } + + err = fn_writeFile(outdir + "/img/favicon.svg", logoSVG("black")) + if err != nil { + return err + } + + err = fn_writeFile(outdir + "/img/logo/light.svg", logoSVG("black")) + if err != nil { + return err + } + + err = fn_writeFile(outdir + "/img/logo/dark.svg", logoSVG("white")) + if err != nil { + return err + } + + err = fn_writeFile(outdir + "/style.css", styleCSS) + if err != nil { + return err + } + + return nil +} + +func writePage( + dir string, + t *template.Template, + filename string, + keys map[string]interface{}, +) { + fullpath := dir + "/" + filename + s := strings.Builder{} + g.PanicIf(fn_mkdirp(path.Dir(fullpath))) + g.PanicIf(t.Execute(&s, keys)) + g.PanicIf(fn_writeFile(fullpath, s.String())) +} + +func cmd(stderr io.Writer, name string, args ...string) (string, error) { + command := exec.Command(name, args...) + command.Stderr = stderr + outBytes, err := command.Output() + if err != nil { + return "", err + } + + return strings.TrimSpace(string(outBytes)), nil +} + +func branchesData( + gitcmd func(...string) (string, error), +) ([]map[string]interface{}, error) { + branches := []map[string]interface{}{} + names, err := gitcmd("branch", "--format", "%(refname:lstrip=2)") + if err != nil { + return nil, err + } + + for _, name := range strings.Split(names, "\n") { + sha, err := gitcmd("rev-parse", name) + if err != nil { + return nil, err + } + + messageSummary, err := gitcmd("log", "-1", "--format=%s", sha) + if err != nil { + return nil, err + } + + author, err := gitcmd("log", "-1", "--format=%an", sha) + if err != nil { + return nil, err + } + + date, err := gitcmd( + "log", "-1", "--format=%cd", dateFmt, sha, + ) + if err != nil { + return nil, err + } + + branches = append(branches, map[string]interface{}{ + "name": name, + "sha": sha, + "messageSummary": messageSummary, + "author": author, + "date": date, + }) + } + + return branches, nil +} + +func generateRepository( + args argsT, + path string, + stderr io.Writer, + metadata map[string]interface{}, +) error { + gitcmd := func(args ...string) (string, error) { + return cmd( + stderr, + "git", + slices.Concat( + []string{"-C", path}, + args, + )..., + ) + } + + branches, err := branchesData(gitcmd) + if err != nil { + return err + } + + dir := args.outdir + "/" + (metadata["name"].(string)) + keys := map[string]interface{}{ + "metadata": metadata, + "cloneURL": args.cloneURL, + "logoAlt": gt.Gettext( + "Outlined icon of 3 melting ice cubes", + ), + "language": gt.Gettext("en"), + "branches": gt.Gettext("branches"), + "tags": gt.Gettext("tags"), + "log": gt.Gettext("log"), + "commit": gt.Gettext("commit"), + "branch": gt.Gettext("Branch"), + "tag": gt.Gettext("Tag"), + "commitMessage": gt.Gettext("Commit message"), + "author": gt.Gettext("Author"), + "date": gt.Gettext("Date"), + "data": map[string]interface{}{ + "branches": branches, + }, + } + + writePage(dir, repoindexHTML, "index.html", keys) + writePage(dir, branchesHTML, "branches.html", keys) + // commits := allCommits() + + return nil +} + +func basicRepositoryMetadata(path string) (map[string]interface{}, error) { + gitdirBytes, err := exec.Command( + "git", "-C", path, "rev-parse", "--git-dir", + ).Output() + if err != nil { + return nil, err + } + gitdir := strings.TrimSpace(string(gitdirBytes)) + + descriptionPath := path + "/" + gitdir + "/" + "description" + descriptionBytes, err := os.ReadFile(descriptionPath) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + description := strings.TrimSpace(string(descriptionBytes)) + + lastCommitBytes, err := exec.Command( + "git", "-C", path, "log", "-1", "--format=%cd", dateFmt, + ).Output() + if err != nil { + return nil, err + } + lastCommit := strings.TrimSpace(string(lastCommitBytes)) + + return map[string]interface{}{ + "name": path, + "description": description, + "lastCommitDate": lastCommit, + }, nil +} + +func generateRepositories( + args argsT, + stderr io.Writer, +) ([]map[string]interface{}, error) { + allMetadata := []map[string]interface{}{} + for _, path := range args.subArgs { + metadata, err := basicRepositoryMetadata(path) + if err != nil { + return nil, err + } + + err = generateRepository(args, path, stderr, metadata) + if err != nil { + return nil, err + } + + allMetadata = append(allMetadata, metadata) + } + + return allMetadata, nil +} + +func generateListing(args argsT, allMetadata []map[string]interface{}) error { + keys := map[string]interface{}{ + "logoAlt": gt.Gettext( + "Outlined icon of 3 melting ice cubes", + ), + "language": gt.Gettext("en"), + "pageDescription": gt.Gettext("Listing of repositories"), + "title": gt.Gettext("Repositories"), + "name": gt.Gettext("Name"), + "description": gt.Gettext("Description"), + "lastCommit": gt.Gettext("Last commit"), + "generatedWith": gt.Gettext("Generated with"), + "repositories": allMetadata, + } + s := strings.Builder{} + err := listingHTML.Execute(&s, keys) + if err != nil { + return err + } + + err = fn_writeFile(args.outdir + "/index.html", s.String()) + if err != nil { + return err + } + + return nil +} + +func run(args argsT, _ io.Reader, _ io.Writer, stderr io.Writer) int { + err := fn_mkdirp(args.outdir) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + + err = generateAssets(args.outdir) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + + allMetadata, err := generateRepositories(args, stderr) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + + err = generateListing(args, allMetadata) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + + return 0 +} + func usage(argv0 string, w io.Writer) { fmt.Fprintf( w, - "Usage: %s [-o DIRECTORY] [-u CLONE_URL] REPOSITORY\n", + "Usage: %s [-o DIRECTORY] [-u CLONE_URL] REPOSITORY...\n", argv0, ) } @@ -32,12 +1062,13 @@ func getopt(allArgs []string, w io.Writer) (argsT, int) { fs := flag.NewFlagSet("", flag.ContinueOnError) fs.Usage = func() {} fs.SetOutput(w) - outputPath := fs.String( + + outdir := fs.String( "o", - g.Must(os.Getwd()), - "The directory where to write the generated files", + g.Must(fn_wd()), + "Where to store the generated files", ) - cloneUrl := fs.String( + cloneURL := fs.String( "u", "", "The prefix of the online cloning addresss", @@ -48,27 +1079,20 @@ func getopt(allArgs []string, w io.Writer) (argsT, int) { } subArgs := fs.Args() - if len(subArgs) == 0 { - fmt.Fprintf(w, "Missing DIRECTORY.\n") - usage(argv0, w) - return argsT{}, 2 - } return argsT{ - outputPath: *outputPath, - cloneUrl: *cloneUrl, - allArgs: allArgs, + allArgs: allArgs, + subArgs: subArgs, + cloneURL: *cloneURL, + outdir: *outdir, }, 0 } -func run(args argsT, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { - return 0 -} - func Main() { g.Init() + gt.Init(Name, LOCALEDIR) args, rc := getopt(os.Args, os.Stderr) g.ExitIf(rc) os.Exit(run(args, os.Stdin, os.Stdout, os.Stderr)) diff --git a/tests/gistatic.go b/tests/gistatic.go index 9f09a22..5798a0c 100644 --- a/tests/gistatic.go +++ b/tests/gistatic.go @@ -1,4 +1,106 @@ package gistatic +import ( + "strings" + + g "gobang" +) + + + +func test_usage() { + g.TestStart("usage()") + + g.Testing("it writes the usage string to the io.Writer", func() { + w := strings.Builder{} + usage("xxx", &w) + + expected := g.Heredoc(` + Usage: xxx [-o DIRECTORY] [-u CLONE_URL] REPOSITORY... + `) + g.TAssertEqual(w.String(), expected) + }) +} + +func test_getopt() { + g.TestStart("getopt()") + + usage := g.Heredoc(` + Usage: $0 [-o DIRECTORY] [-u CLONE_URL] REPOSITORY... + `) + + g.Testing("we supress the default error message", func() { + w := strings.Builder{} + argv := []string{"$0", "-h"} + _, rc := getopt(argv, &w) + + g.TAssertEqual(w.String(), usage) + g.TAssertEqual(rc, 2) + }) + + g.Testing("we get unsupported flag error", func() { + w := strings.Builder{} + argv := []string{"$0", "-A"} + _, rc := getopt(argv, &w) + + const message = "flag provided but not defined: -A\n" + g.TAssertEqual(w.String(), message + usage) + g.TAssertEqual(rc, 2) + }) + + g.Testing("we get incorrect use of flag error", func() { + w1 := strings.Builder{} + argv1 := []string{"$0", "-u"} + _, rc1 := getopt(argv1, &w1) + + const message1 = "flag needs an argument: -u\n" + g.TAssertEqual(w1.String(), message1 + usage) + g.TAssertEqual(rc1, 2) + + w2 := strings.Builder{} + argv2 := []string{"$0", "-o"} + _, rc2 := getopt(argv2, &w2) + + const message2 = "flag needs an argument: -o\n" + g.TAssertEqual(w2.String(), message2 + usage) + g.TAssertEqual(rc2, 2) + }) + + g.Testing("the argsT has the flag URL value", func() { + fn_wdSaved := fn_wd + fn_wd = func() (string, error) { + return "virtual working directory", nil + } + + w := strings.Builder{} + argv := []string{"$0", "-u", "proto://URL", "a-path"} + expected := argsT{ + cloneURL: "proto://URL", + allArgs: []string{"$0", "-u", "proto://URL", "a-path"}, + subArgs: []string{"a-path"}, + outdir: "virtual working directory", + } + + args, rc := getopt(argv, &w) + + fn_wd = fn_wdSaved + + g.TAssertEqual(w.String(), "") + g.TAssertEqual(rc, 0) + g.TAssertEqual(args, expected) + }) +} + +func test_run() { + // FIXME +} + + + func MainTest() { + g.Init() + + test_usage() + test_getopt() + test_run() } |