chore: init first alpha
All checks were successful
ci/woodpecker/push/commit Pipeline was successful

This commit is contained in:
kekskurse 2025-07-03 22:14:19 +02:00
commit 45d9761be1
19 changed files with 3382 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
public/
docker-compose.yml
run.sh
layout/node_modules
# Added by goreleaser init:
dist/

36
.goreleaser.yaml Normal file
View file

@ -0,0 +1,36 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
release:
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

16
.woodpecker/commit.yml Normal file
View file

@ -0,0 +1,16 @@
when:
- event: push
branch: main
steps:
- name: test
image: golang:1.24
commands:
- go test ./...
- name: build tailwind css
image: codeberg.org/woodpecker-plugins/node-pm
commands:
- cd layout
- npm install
- npx @tailwindcss/cli -i style.css -o output.css

0
Readme.md Normal file
View file

37
go.mod Normal file
View file

@ -0,0 +1,37 @@
module git.keks.cloud/kekskurse/wikipress
go 1.24.1
require (
github.com/go-git/go-git/v5 v5.16.2
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v3 v3.3.8
github.com/yassinebenaid/godump v0.11.1
github.com/yuin/goldmark v1.7.12
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

110
go.sum Normal file
View file

@ -0,0 +1,110 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

1008
layout/output.css Normal file

File diff suppressed because it is too large Load diff

1295
layout/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

7
layout/package.json Normal file
View file

@ -0,0 +1,7 @@
{
"devDependencies": {
"@tailwindcss/cli": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"tailwindcss": "^4.1.11"
}
}

143
layout/src/default.html Normal file
View file

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mein Wiki</title>
<link rel="stylesheet" href="/output.css" />
<script>
function toggleSubtree(id) {
const el = document.getElementById(id);
const icon = document.getElementById('icon-' + id);
if (el.classList.contains('hidden')) {
el.classList.remove('hidden');
icon.textContent = '▼';
} else {
el.classList.add('hidden');
icon.textContent = '▶';
}
}
</script>
</head>
<body class="bg-gray-100 text-gray-900 font-sans flex flex-col min-h-screen">
<header class="md:hidden flex items-center justify-between bg-white border-b p-4">
<h1 class="text-lg font-bold">Wiki</h1>
<button id="menu-btn" class="focus:outline-none">
<!-- einfaches SVG-Icon -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</header>
<div class="flex flex-1">
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-40 w-64 bg-gray-200 border-r border-gray-400 p-4 overflow-y-auto
transform -translate-x-full transition-transform duration-200 ease-in-out
md:relative md:translate-x-0 md:transform-none">
<!-- Dein Menü hier kopieren -->
<div class="flex justify-between items-center mb-4">
<h1 class="text-xl font-bold">Wiki-Menü</h1>
</div>
<nav class="space-y-2 text-sm">
<ul class="space-y-1">
{{- $global := . -}}
{{define "node"}}
{{- $ctx := . -}}
{{range $ctx.Menu.Children }}
{{if .Page}}
<li>
<a href="{{ .Link }}" class="block px-2 py-1 rounded hover:bg-gray-300">{{ .Name }}</a>
</li>
{{ else }}
<li>
<button onclick="toggleSubtree('{{ .ID }}')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-300 flex items-center">
<span id="icon-{{ .ID }}" class="mr-2">{{if ne (index $ctx.Folder .Level) .Name}}▶{{else}}▼{{end}}</span>{{ .Text }}
</button>
<ul id="{{ .ID }}" class="ml-4 mt-1 {{if ne (index $ctx.Folder .Level) .Name}}hidden{{end}} space-y-1">
{{template "node" (dict "Menu" . "Folder" $ctx.Folder) }}
</ul>
</li>
{{ end }}
{{ end }}
{{ end }}
{{template "node" (dict "Menu" .Menu "Folder" .Folders) }}
</ul>
</nav>
</aside>
<div id="overlay"
class="fixed inset-0 bg-black bg-opacity-50 z-30 opacity-0 pointer-events-none
transition-opacity duration-200 ease-in-out
md:hidden"></div>
<!-- Main Content -->
<main class="flex-1 p-8 overflow-y-auto">
<div class="max-w-screen-xl mx-auto">
<h1 class="text-4xl font-bold mb-6 float-left">{{ .Title }}</h1><h5 class="float-right">from {{ .Version.Author.Name}} at {{ .Version.Date }}</h5>
<div class="clear-both"></div>
<div>
<div class="border-b border-gray-300 mb-4">
<nav class="flex space-x-4" id="tabs">
<button class="tab px-4 py-2 text-sm font-medium border-b-2 border-gray-900" onclick="showTab('inhalt')">Inhalt</button>
<button class="tab px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800" onclick="showTab('history')">Historie</button>
</nav>
</div>
<div id="inhalt">
<div class="prose max-w-none">
{{ .HTMLContent }}
</div>
</div>
<div id="history" class="hidden">
<h3 class="text-2xl font-serif font-semibold mb-4">Versionshistorie</h3>
<ul class="list-disc list-inside text-gray-800">
{{ range .File.Versions }}
<li><strong>{{ .Message }}</strong> from {{ .Author.Name}} at {{ .Date }}</li>
{{ end }}
</ul>
</div>
</div>
</div>
</main>
<script>
function showTab(tabId) {
document.getElementById('inhalt').classList.add('hidden');
document.getElementById('history').classList.add('hidden');
document.getElementById(tabId).classList.remove('hidden');
document.querySelectorAll('#tabs .tab').forEach(btn => btn.classList.remove('border-gray-900'));
event.target.classList.add('border-gray-900');
}
</script>
<script>
const btn = document.getElementById('menu-btn');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
function toggleMenu() {
sidebar.classList.toggle('-translate-x-full');
overlay.classList.toggle('opacity-0');
overlay.classList.toggle('pointer-events-none');
}
btn.addEventListener('click', toggleMenu);
overlay.addEventListener('click', toggleMenu);
</script>
</div>
<!-- Footer -->
<!--<footer class="bg-gray-300 text-center py-4 border-t border-gray-400">
<div class="flex justify-center space-x-8 text-sm">
<a href="#" class="hover:underline">Impressum</a>
<a href="#" class="hover:underline">Wikipress</a>
<a href="#" class="hover:underline">Contact</a>
</div>
</footer>-->
</body>
</html>

2
layout/style.css Normal file
View file

@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";

87
main.go Normal file
View file

@ -0,0 +1,87 @@
package main
import (
"context"
"fmt"
"log"
"os"
"git.keks.cloud/kekskurse/wikipress/pkg/wikipress"
"github.com/urfave/cli/v3"
)
func main() {
cmd := &cli.Command{
Name: "generate",
Usage: "generate html from git folder",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
Value: "content",
Usage: "Path to the content git repro",
},
&cli.StringFlag{
Name: "out",
Value: "./public",
Usage: "Path to the content git repro",
},
&cli.BoolFlag{
Name: "no-git",
Value: false,
Usage: "dont use git, just current state",
},
},
Action: action,
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}
func action(ctx context.Context, cmd *cli.Command) error {
os.MkdirAll(cmd.String("path"), 0755)
var files []wikipress.File
var err error
if cmd.Bool("no-git") {
files, err = wikipress.ListFilesFromHDD(cmd.String("path"))
if err != nil {
return err
}
} else {
files, err = wikipress.ListFilesFromGitRepro(cmd.String("path"))
if err != nil {
return err
}
}
menu := wikipress.GenerateMenu(files)
for _, file := range files {
// Copy not mark down files
if file.GetExtension() != "md" {
err = wikipress.CreateFilePageAndCopyContent(file, cmd.String("out"))
if err != nil {
return err
}
err = os.Symlink(fmt.Sprintf("./%v_%v.%v", file.GetName(), file.GetLastVersion().Hash, file.GetExtension()), fmt.Sprintf("%v/%v/%v.%v", cmd.String("out"), file.GetFolder(), file.GetName(), file.GetExtension()))
if err != nil {
return err
}
// TODO: Copy and create media page
continue
}
err = wikipress.GenerateHTMLPageForMarkedown(file, menu, cmd.String("out"))
if err != nil {
return err
}
err = os.Symlink(fmt.Sprintf("./%v_%v.html", file.GetName(), file.GetLastVersion().Hash), fmt.Sprintf("%v/%v/%v.html", cmd.String("out"), file.GetFolder(), file.GetName()))
if err != nil {
return err
}
}
return nil
}

9
pkg/wikipress/error.go Normal file
View file

@ -0,0 +1,9 @@
package wikipress
import "errors"
var (
ErrCantReadGitRepro = errors.New("Cant read git repro")
ErrCantRenderNonMarktdownFile = errors.New("Cant render non markdown files")
ErrCantParseMarkdownToHTML = errors.New("Cant render markdown to html")
)

55
pkg/wikipress/file.go Normal file
View file

@ -0,0 +1,55 @@
package wikipress
import "strings"
type File struct {
Name string
Deleted bool
Versions []Version
}
func (f File) GetExtension() string {
lastDot := strings.LastIndex(f.Name, ".")
if lastDot == -1 {
return ""
}
return f.Name[lastDot+1:]
}
func (f File) GetFolder() string {
lastSlash := strings.LastIndex(f.Name, "/")
if lastSlash == -1 {
return ""
}
return f.Name[0:lastSlash]
}
func (f File) GetName() string {
lastSlash := strings.LastIndex(f.Name, "/")
lastDot := strings.LastIndex(f.Name, ".")
return f.Name[lastSlash+1 : lastDot]
}
func (f File) GetFolders() []string {
folder := f.GetFolder()
res := strings.Split(folder, "/")
return res
}
func (f File) GetTitle() string {
title := f.GetLastVersion().GetHeader("title", f.GetName())
return title
}
func (f File) GetLastVersion() Version {
return f.Versions[len(f.Versions)-1]
}
func (f File) GetMenuPosition() int {
if len(f.Versions) == 0 {
return 1000
}
return f.GetLastVersion().GetHeaderInt("order", 1500)
}

View file

@ -0,0 +1,51 @@
package wikipress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetFileExtension(t *testing.T) {
tts := []struct {
name string
file File
expExtension string
}{
{
name: "markedown file",
file: File{
Name: "home.md",
},
expExtension: "md",
},
{
name: "multible dot",
file: File{
Name: "foo.br.jpg",
},
expExtension: "jpg",
},
{
name: "with folder",
file: File{
Name: "foo/bar/test.txt",
},
expExtension: "txt",
},
{
name: "no dot",
file: File{
Name: "something",
},
expExtension: "",
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
extension := tt.file.GetExtension()
assert.Equal(t, tt.expExtension, extension, "should be right extension")
})
}
}

193
pkg/wikipress/generator.go Normal file
View file

@ -0,0 +1,193 @@
package wikipress
import (
"fmt"
"html/template"
"os"
"sort"
"strings"
"github.com/google/uuid"
)
func CreateFilePageAndCopyContent(file File, outfolder string) error {
if file.GetExtension() == "md" {
return nil
}
for versionNumber := range file.Versions {
folder := fmt.Sprintf("%v/%v", outfolder, file.GetFolder())
path := fmt.Sprintf("%v/%v_%v.%v", folder, file.GetName(), file.Versions[versionNumber].Hash, file.GetExtension())
err := os.WriteFile(path, file.Versions[versionNumber].contentbinary, 0o644)
if err != nil {
return err
}
}
return nil
}
func GenerateHTMLPageForMarkedown(file File, menu MenuItem, outfolder string) error {
os.MkdirAll(outfolder, 0755)
if file.GetExtension() != "md" {
return ErrCantRenderNonMarktdownFile
}
for versionnumber := range file.Versions {
err := renderHTML(file, menu, versionnumber, outfolder)
if err != nil {
return err
}
}
return nil
}
type MenuItem struct {
ID string
Name string
Text string
Link string
File File
Level int
Children []*MenuItem
Page bool
}
func GenerateMenu(files []File) MenuItem {
menu := MenuItem{}
for _, file := range files {
if file.Deleted {
continue
}
if file.GetExtension() != "md" {
continue
}
folders := file.GetFolders()
folders = append(folders, "")
currentMenu := &menu
for level, folderLevel := range folders {
if folderLevel == "" {
newMenuItem := MenuItem{}
newMenuItem.Name = file.GetTitle()
newMenuItem.Page = true
newMenuItem.File = file
newMenuItem.Level = level
if file.GetFolder() != "" {
newMenuItem.Link = "/" + file.GetFolder()
}
newMenuItem.Link = newMenuItem.Link + "/" + file.GetName() + ".html"
currentMenu.Children = append(currentMenu.Children, &newMenuItem)
break
// Create new Item
} else {
found := false
for _, m := range currentMenu.Children {
if m.Name == folderLevel {
currentMenu = m
found = true
}
}
if !found {
newMenuItem := MenuItem{}
newMenuItem.ID = uuid.NewString()
newMenuItem.Name = folderLevel
newMenuItem.Level = level
newMenuItem.Text = strings.Title(folderLevel)
currentMenu.Children = append(currentMenu.Children, &newMenuItem)
currentMenu = &newMenuItem
}
// Get children or create a new one if needed
}
}
}
menu.Children = sortMenu(menu.Children)
return menu
}
func sortMenu(input []*MenuItem) []*MenuItem {
sort.Slice(input, func(i, j int) bool {
return strings.ToLower(input[i].Name) < strings.ToLower(input[j].Name)
})
sort.Slice(input, func(i, j int) bool {
if input[i].File.GetMenuPosition() < input[j].File.GetMenuPosition() {
return true
}
return false
})
for _, i := range input {
i.Children = sortMenu(i.Children)
}
return input
}
type htmlTemplateData struct {
HTMLContent template.HTML
Title string
File File
Menu MenuItem
Folders []string
Version Version
}
func renderHTML(file File, menu MenuItem, versionNumber int, outfolder string) error {
folder := fmt.Sprintf("%v/%v", outfolder, file.GetFolder())
htmlPath := fmt.Sprintf("%v/%v_%v.html", folder, file.GetName(), file.Versions[versionNumber].Hash)
fmt.Println(htmlPath)
os.MkdirAll(folder, 0755)
var err error
h := htmlTemplateData{}
h.Menu = menu
h.Title = file.Versions[versionNumber].GetHeader("title", file.GetName())
h.File = file
h.Folders = file.GetFolders()
h.Version = file.Versions[versionNumber]
content, err := file.Versions[versionNumber].GetHTMLContent()
if err != nil {
return err
}
h.HTMLContent = template.HTML(content)
tmpl := template.New("default.html")
tmpl.Funcs(template.FuncMap{
"dict": dict,
"index": index,
})
tmpl, err = tmpl.ParseFiles("layout/src/default.html")
if err != nil {
return err
}
outFile, err := os.Create(htmlPath)
if err != nil {
return err
}
err = tmpl.Execute(outFile, h)
if err != nil {
return err
}
return nil
}
func dict(values ...any) map[string]any {
m := make(map[string]any)
for i := 0; i < len(values); i += 2 {
m[values[i].(string)] = values[i+1]
}
return m
}
func index(slice []string, i int) string {
if i < 0 || i >= len(slice) {
return ""
}
return slice[i]
}

150
pkg/wikipress/git.go Normal file
View file

@ -0,0 +1,150 @@
package wikipress
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func ListFilesFromHDD(opath string) ([]File, error) {
opath = opath + "/"
FileList := []File{}
err := filepath.Walk(opath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err // z.B. fehlende Leserechte
}
if !info.IsDir() {
v := Version{}
data, err := os.ReadFile(path)
if err != nil {
return err
}
v.content = string(data)
v.contentbinary = data
v.Author.Name = "HDD"
v.Date = time.Now()
v.Hash = "abc"
v.Message = "test"
f := File{}
f.Name = strings.TrimPrefix(path, opath)
f.Versions = append(f.Versions, v)
FileList = append(FileList, f)
}
return nil
})
return FileList, err
}
func ListFilesFromGitRepro(path string) ([]File, error) {
repo, err := git.PlainOpen(path)
if err != nil {
return []File{}, fmt.Errorf("%w: %s", ErrCantReadGitRepro, err)
}
ref, err := repo.Head()
if err != nil {
return []File{}, fmt.Errorf("%w: %s", ErrCantReadGitRepro, err)
}
commitIter, err := repo.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
return []File{}, fmt.Errorf("%w: %s", ErrCantReadGitRepro, err)
}
var commits []*object.Commit
err = commitIter.ForEach(func(c *object.Commit) error {
commits = append(commits, c)
return nil
})
if err != nil {
return []File{}, fmt.Errorf("%w: %s", ErrCantReadGitRepro, err)
}
for i, j := 0, len(commits)-1; i < j; i, j = i+1, j-1 {
commits[i], commits[j] = commits[j], commits[i]
}
lastContent := make(map[string]string)
fileList := make(map[string]File)
currentFileList := make(map[string]bool)
for _, c := range commits {
tree, err := c.Tree()
if err != nil {
return []File{}, err
}
currentFileList = make(map[string]bool)
err = tree.Files().ForEach(func(f *object.File) error {
path := filepath.ToSlash(f.Name)
currentFileList[f.Name] = true
content, err := f.Contents()
if err != nil {
return err
}
if lastContent[path] == content {
return nil
}
byteContentReader, err := f.Reader()
if err != nil {
return err
}
byteContent, err := io.ReadAll(byteContentReader)
if err != nil {
return err
}
lastContent[path] = content
version := Version{}
version.Author.Name = c.Author.Name
version.Message = c.Message
version.Date = c.Author.When
version.content = content
version.contentbinary = byteContent
version.Hash = c.Hash.String()
file, ok := fileList[path]
if !ok {
file.Name = f.Name
}
file.Versions = append(file.Versions, version)
fileList[path] = file
return nil
})
if err != nil {
return []File{}, err
}
}
if err != nil {
return []File{}, fmt.Errorf("%w: %s", ErrCantReadGitRepro, err)
}
fileListReturn := []File{}
for _, f := range fileList {
_, ok := currentFileList[f.Name]
if !ok {
f.Deleted = true
}
fileListReturn = append(fileListReturn, f)
}
return fileListReturn, nil
}

99
pkg/wikipress/version.go Normal file
View file

@ -0,0 +1,99 @@
package wikipress
import (
"bytes"
"fmt"
"strconv"
"strings"
"time"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
)
type Version struct {
Author struct {
Name string
}
Message string
Date time.Time
content string
contentbinary []byte
Hash string
}
func (v Version) GetBody() string {
details := strings.SplitN(v.content, "---", 2)
if len(details) == 2 {
return details[1]
}
return ""
}
func (v Version) GetHeaders() map[string]string {
details := strings.SplitN(v.content, "---", 2)
if len(details) < 2 {
return make(map[string]string)
}
lines := strings.Split(details[0], "\n")
headers := make(map[string]string)
for _, line := range lines {
lineContent := strings.SplitN(line, ":", 2)
if len(lineContent) != 2 {
continue
}
headers[lineContent[0]] = strings.TrimSpace(lineContent[1])
}
return headers
}
func (v Version) GetHeader(name, defaultValue string) string {
headers := v.GetHeaders()
val, ok := headers[name]
if ok {
return val
}
return defaultValue
}
func (v Version) GetHeaderInt(name string, defaultValue int) int {
headers := v.GetHeaders()
val, ok := headers[name]
if ok {
vali, err := strconv.Atoi(val)
if err != nil {
return defaultValue
}
return vali
}
return defaultValue
}
func (v Version) GetHTMLContent() (string, error) {
mdParser := goldmark.New(
goldmark.WithExtensions(
extension.GFM, // GitHub-Flavored Markdown (inkl. Tabellen, Tasklists)
extension.Footnote, // Fußnoten
extension.Table, // Tabellen (redundant, aber explizit)
extension.Strikethrough, // Durchgestrichener Text
extension.DefinitionList,
extension.TaskList,
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
html.WithUnsafe(), // erlaubt raw HTML (vorsichtig!)
),
)
var buf bytes.Buffer
err := mdParser.Convert([]byte(v.GetBody()), &buf)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrCantParseMarkdownToHTML, err)
}
return buf.String(), nil
}

View file

@ -0,0 +1,78 @@
package wikipress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetody(t *testing.T) {
tts := []struct {
name string
version Version
expBody string
}{
{
name: "simple content",
version: Version{content: "foobar\r\n---\r\nBlaBla"},
expBody: "\r\nBlaBla",
},
{
name: "file without body",
version: Version{content: "blabla"},
expBody: "",
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
body := tt.version.GetBody()
assert.Equal(t, tt.expBody, body, "should return currect body")
})
}
}
func TestGetHeaders(t *testing.T) {
expData1 := make(map[string]string)
expData1["foo"] = "bar"
tts := []struct {
name string
version Version
expHeader map[string]string
}{
{
name: "simple test",
version: Version{content: "foo: bar\r\n---\r\ncontent"},
expHeader: expData1,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
headers := tt.version.GetHeaders()
assert.Equal(t, tt.expHeader, headers, "headers should match")
})
}
}
func TestGetHTMLContent(t *testing.T) {
tts := []struct {
name string
version Version
exptHTML string
}{
{
name: "simeple",
version: Version{content: "foo:bar\r\n---\r\n# Simple\r\nTest"},
exptHTML: "<h1>Simple</h1>\n<p>Test</p>\n",
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
html, err := tt.version.GetHTMLContent()
assert.Nil(t, err, "should return no error")
assert.Equal(t, tt.exptHTML, html, "should return expected html")
})
}
}