Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daed37587e
|
||
|
|
7695f42e20
|
||
|
|
5d6d03702e
|
||
|
|
4cb09fb81f
|
||
|
|
234137f22f
|
||
|
|
267d1010bb
|
||
|
|
740fb94556
|
||
|
|
a17926f54b
|
||
|
|
b9c4553577
|
||
|
|
745c886cec
|
||
|
|
ff1da084af
|
||
|
|
4a9a3eec06
|
||
|
|
3573e23212
|
||
|
|
1312dcecb5
|
||
|
|
f8f40b2134
|
||
|
|
58681bd7df
|
||
|
|
7df4a03171
|
||
|
|
5624c7af87
|
||
|
|
23fd5fe1d4
|
||
|
|
50139c6f51
|
||
|
|
c5fd151060
|
||
|
|
90a39e3027
|
||
|
|
9d855f586d
|
||
|
|
946f586ccb
|
||
|
|
cd8c7fa657
|
||
|
|
3e3d1f05a0
|
||
|
|
5cc4b9d001
|
||
|
|
3c87da15e1
|
||
|
|
baa38b668e
|
||
|
|
d652afd633
|
||
|
|
ada333a0e1
|
||
|
|
0dfe0f8142
|
||
|
|
70d7497eda
|
||
|
|
dfc932b7b0
|
||
|
|
938e597f3f
|
||
|
|
29f01a2618
|
||
|
|
66a9ebe452
|
||
|
|
b2e0d78a2c
|
||
|
|
6b34509d9a
|
||
|
|
8210e16305
|
||
|
|
2f114885ac
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
test.html
|
/html/
|
||||||
html/
|
/build/dist
|
||||||
mark2web
|
/coverage.out
|
||||||
|
/test/out
|
||||||
24
.gitmodules
vendored
24
.gitmodules
vendored
@@ -67,3 +67,27 @@
|
|||||||
[submodule "vendor/golang.org/x/image"]
|
[submodule "vendor/golang.org/x/image"]
|
||||||
path = vendor/golang.org/x/image
|
path = vendor/golang.org/x/image
|
||||||
url = https://go.googlesource.com/image
|
url = https://go.googlesource.com/image
|
||||||
|
[submodule "vendor/github.com/smartystreets/goconvey"]
|
||||||
|
path = vendor/github.com/smartystreets/goconvey
|
||||||
|
url = https://github.com/smartystreets/goconvey
|
||||||
|
[submodule "vendor/github.com/jtolds/gls"]
|
||||||
|
path = vendor/github.com/jtolds/gls
|
||||||
|
url = https://github.com/jtolds/gls
|
||||||
|
[submodule "vendor/github.com/smartystreets/assertions"]
|
||||||
|
path = vendor/github.com/smartystreets/assertions
|
||||||
|
url = https://github.com/smartystreets/assertions
|
||||||
|
[submodule "vendor/github.com/itchio/go-brotli"]
|
||||||
|
path = vendor/github.com/itchio/go-brotli
|
||||||
|
url = https://github.com/itchio/go-brotli
|
||||||
|
[submodule "vendor/github.com/gosuri/uiprogress"]
|
||||||
|
path = vendor/github.com/gosuri/uiprogress
|
||||||
|
url = https://github.com/gosuri/uiprogress
|
||||||
|
[submodule "vendor/github.com/gosuri/uilive"]
|
||||||
|
path = vendor/github.com/gosuri/uilive
|
||||||
|
url = https://github.com/gosuri/uilive
|
||||||
|
[submodule "vendor/github.com/mattn/go-tty"]
|
||||||
|
path = vendor/github.com/mattn/go-tty
|
||||||
|
url = https://github.com/mattn/go-tty
|
||||||
|
[submodule "vendor/golang.org/x/sys"]
|
||||||
|
path = vendor/golang.org/x/sys
|
||||||
|
url = https://go.googlesource.com/sys
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"match": "website/.*",
|
"match": "website/.*",
|
||||||
"cmd": "time mark2web -in ${workspaceRoot}/website -out ${workspaceRoot}/html -create",
|
"cmd": "time mark2web -in ${workspaceRoot}/website -out ${workspaceRoot}/html -create -logLevel warning -progress",
|
||||||
"silent": false
|
"silent": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
26
LICENSE
Normal file
26
LICENSE
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Copyright (c) 2019, Sebastian Frank
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
The views and conclusions contained in the software and documentation are those
|
||||||
|
of the authors and should not be interpreted as representing official policies,
|
||||||
|
either expressed or implied, of the mark2web project.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# mark2web
|
# mark2web
|
||||||
|
|
||||||
|
[](https://ci.basehosts.de/apairon/mark2web)
|
||||||
|
|
||||||
mark2web ist ein Website-Generator, der als Eingabe Markdown-Dateien, Templates und Konfigurations-Dateien nutzt.
|
mark2web ist ein Website-Generator, der als Eingabe Markdown-Dateien, Templates und Konfigurations-Dateien nutzt.
|
||||||
|
|
||||||
Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwaltung (z.B. git) und anschließende CI/CD-Pipeline, welche den Generator aufruft und die fertige Website publiziert.
|
Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwaltung (z.B. git) und anschließende CI/CD-Pipeline, welche den Generator aufruft und die fertige Website publiziert.
|
||||||
@@ -9,3 +11,7 @@ Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwa
|
|||||||
Die Dokumentation ist auf der [mark2web-Website](https://www.mark2web.de/) zu finden. Außerdem ist die Dokumentation im Verzeichnis `website/content` dieses Repositories, da dies das Ausgangsmaterial der Projekt-Website ist.
|
Die Dokumentation ist auf der [mark2web-Website](https://www.mark2web.de/) zu finden. Außerdem ist die Dokumentation im Verzeichnis `website/content` dieses Repositories, da dies das Ausgangsmaterial der Projekt-Website ist.
|
||||||
|
|
||||||
Die öffentliche Website ist mit **mark2web** generiert.
|
Die öffentliche Website ist mit **mark2web** generiert.
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
Das Projekt **mark2web** unterliegt der Lizenz "Simplified BSD License" (siehe [LICENSE](LICENSE)).
|
||||||
16
RELEASE.md
16
RELEASE.md
@@ -1,16 +0,0 @@
|
|||||||
NEUERUNGEN:
|
|
||||||
|
|
||||||
- pongo2-addons standardmäßig eingebunden
|
|
||||||
- fnRequest Template-Funktion für Web-Requests
|
|
||||||
- fnRender Template-Funktion um Unterseiten zu rendern
|
|
||||||
- eigene Filter via Javascript
|
|
||||||
- image_process Template-Filter um Bilder zu skalieren (resize, fit, fill)
|
|
||||||
- json Template-Filter um Variablen als JSON auszugeben
|
|
||||||
- dump Template-Filter um Variablen über spew.Dump auszugeben
|
|
||||||
- Collections via Web-Request mit optionaler Unternavigation
|
|
||||||
- Timestamp Variable
|
|
||||||
|
|
||||||
BUG FIXES:
|
|
||||||
|
|
||||||
- CR in Markdown entfernt, blackfriday-Bug
|
|
||||||
- Pfadangaben gesäubert, da sonst bei `-out ./html` der Assets-Pfad falsch ermittelt wurde
|
|
||||||
4
build.sh
4
build.sh
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
mkdir -p dist
|
|
||||||
go build -v -ldflags "-X main.Version=`git describe --tags --long` -X main.GitHash=`git rev-parse HEAD` -X main.BuildTime=`date -u '+%Y-%m-%d_%I:%M:%S%p'`" -o dist/mark2web-`cat VERSION`-${GOOS}-${GOARCH}${FILEEXT}
|
|
||||||
9
build/RELEASE.md
Normal file
9
build/RELEASE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
NEUERUNGEN:
|
||||||
|
|
||||||
|
- Cached Collection Webrequests
|
||||||
|
- recursive Collections
|
||||||
|
- Datei basierte Collections
|
||||||
|
- markdown-Filter `s=SYNTAX_HIGHLIGHT_SHEMA` Parameter
|
||||||
|
- image_process nutzt alle CPU-Kerne
|
||||||
|
- GZIP/Brotli Vor-Komprimierung der Inhalte und Assets
|
||||||
|
- Code neu organisiert
|
||||||
1
build/VERSION
Normal file
1
build/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.2.0
|
||||||
@@ -6,24 +6,43 @@ workspace:
|
|||||||
path: src/gitbase.de/apairon/mark2web
|
path: src/gitbase.de/apairon/mark2web
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build for linux
|
- name: init submodules
|
||||||
image: golang:latest
|
image: docker:git
|
||||||
environment:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: amd64
|
|
||||||
commands:
|
commands:
|
||||||
- git submodule update --init --recursive
|
- git submodule update --init --recursive
|
||||||
- git fetch --tags
|
- git fetch --tags
|
||||||
- ./build.sh
|
when:
|
||||||
|
event: [ push, tag ]
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
image: golang:latest
|
||||||
|
environment:
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
commands:
|
||||||
|
# fake term for goconvey color output
|
||||||
|
- env TERM=xterm-color256 go test -v -coverprofile coverage.out ./pkg/*
|
||||||
|
when:
|
||||||
|
event: [ push, tag ]
|
||||||
|
|
||||||
|
- name: build for linux
|
||||||
|
image: golang:latest
|
||||||
|
environment:
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
commands:
|
||||||
|
- scripts/build.sh
|
||||||
when:
|
when:
|
||||||
event: [ push, tag ]
|
event: [ push, tag ]
|
||||||
|
|
||||||
- name: test with example content
|
- name: test with example content
|
||||||
image: alpine
|
image: alpine
|
||||||
commands:
|
commands:
|
||||||
- ./dist/mark2web-`cat VERSION`-linux-amd64 -version
|
- apk add --no-cache libc6-compat
|
||||||
- ./dist/mark2web-`cat VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug
|
- dist/mark2web-`cat build/VERSION`-linux-amd64 -version
|
||||||
|
- dist/mark2web-`cat build/VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug
|
||||||
when:
|
when:
|
||||||
event: [ push, tag ]
|
event: [ push, tag ]
|
||||||
|
|
||||||
@@ -34,7 +53,7 @@ steps:
|
|||||||
GOOS: freebsd
|
GOOS: freebsd
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
commands:
|
commands:
|
||||||
- ./build.sh
|
- scripts/build.sh
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
|
|
||||||
@@ -45,7 +64,7 @@ steps:
|
|||||||
GOOS: darwin
|
GOOS: darwin
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
commands:
|
commands:
|
||||||
- ./build.sh
|
- scripts/build.sh
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
|
|
||||||
@@ -57,15 +76,15 @@ steps:
|
|||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
FILEEXT: .exe
|
FILEEXT: .exe
|
||||||
commands:
|
commands:
|
||||||
- ./build.sh
|
- scripts/build.sh
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
|
|
||||||
- name: build docker image
|
- name: build docker image
|
||||||
image: docker
|
image: docker
|
||||||
commands:
|
commands:
|
||||||
- cp dist/mark2web-`cat VERSION`-linux-amd64 mark2web
|
- cp dist/mark2web-`cat build/VERSION`-linux-amd64 build/package/mark2web
|
||||||
- docker build -t apairon/mark2web .
|
- docker build -t apairon/mark2web build/package
|
||||||
volumes:
|
volumes:
|
||||||
- name: docker
|
- name: docker
|
||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
@@ -73,6 +92,17 @@ steps:
|
|||||||
branch: [ master ]
|
branch: [ master ]
|
||||||
event: [ push ]
|
event: [ push ]
|
||||||
|
|
||||||
|
- name: build website
|
||||||
|
image: apairon/mark2web:latest
|
||||||
|
pull: never
|
||||||
|
commands:
|
||||||
|
- /mark2web -version
|
||||||
|
- /mark2web -in website -out html -create -logLevel info
|
||||||
|
when:
|
||||||
|
branch: [ master ]
|
||||||
|
event: [ promote, push ]
|
||||||
|
target: [ "", website ]
|
||||||
|
|
||||||
- name: deploy website
|
- name: deploy website
|
||||||
image: apairon/mark2web:latest
|
image: apairon/mark2web:latest
|
||||||
pull: never
|
pull: never
|
||||||
@@ -80,8 +110,6 @@ steps:
|
|||||||
RSYNC_PASS:
|
RSYNC_PASS:
|
||||||
from_secret: rsync_pass
|
from_secret: rsync_pass
|
||||||
commands:
|
commands:
|
||||||
- /mark2web -version
|
|
||||||
- /mark2web -in website -out html -create -logLevel info
|
|
||||||
- '
|
- '
|
||||||
rsync -rlcgD -i -u -v --stats
|
rsync -rlcgD -i -u -v --stats
|
||||||
--delete
|
--delete
|
||||||
@@ -110,8 +138,8 @@ steps:
|
|||||||
base_url: https://gitbase.de
|
base_url: https://gitbase.de
|
||||||
files:
|
files:
|
||||||
- dist/*
|
- dist/*
|
||||||
title: VERSION
|
title: build/VERSION
|
||||||
note: RELEASE.md
|
note: build/RELEASE.md
|
||||||
checksum:
|
checksum:
|
||||||
- md5
|
- md5
|
||||||
- sha256
|
- sha256
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine
|
FROM alpine
|
||||||
RUN apk update && apk add ca-certificates openssh-client sshpass rsync gzip && rm -r /var/cache/
|
RUN apk update && apk add ca-certificates openssh-client sshpass rsync gzip libc6-compat && rm -r /var/cache/
|
||||||
ADD mark2web /
|
ADD mark2web /
|
||||||
CMD ["/mark2web"]
|
CMD ["/mark2web"]
|
||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
"gitbase.de/apairon/mark2web/pkg/filter"
|
||||||
"gitbase.de/apairon/mark2web/helper"
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,16 +21,13 @@ var (
|
|||||||
BuildTime = "UNKNOWN"
|
BuildTime = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = helper.Log
|
|
||||||
|
|
||||||
var contentConfig = new(config.PathConfigTree)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
inDir := flag.String("in", "./", "input directory")
|
inDir := flag.String("in", "./", "input directory")
|
||||||
outDir := flag.String("out", "html", "output directory")
|
outDir := flag.String("out", "html", "output directory")
|
||||||
createOutDir := flag.Bool("create", false, "create output directory if not existing")
|
createOutDir := flag.Bool("create", false, "create output directory if not existing")
|
||||||
//clearOutDir := flag.Bool("clear", false, "clear output directory before generating website")
|
//clearOutDir := flag.Bool("clear", false, "clear output directory before generating website")
|
||||||
logLevel := flag.String("logLevel", "info", "log level: debug, info, warning, error")
|
logLevel := flag.String("logLevel", "", "log level: debug, info, notice, warning, error")
|
||||||
|
progressBars := flag.Bool("progress", false, "show progress bars for jobs")
|
||||||
version := flag.Bool("version", false, "print version of this executable")
|
version := flag.Bool("version", false, "print version of this executable")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -40,60 +39,66 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
level := "info"
|
level := "notice"
|
||||||
if logLevel != nil {
|
|
||||||
|
if progressBars != nil && *progressBars {
|
||||||
|
progress.Start()
|
||||||
|
level = "warning" // disable log for progressbars
|
||||||
|
}
|
||||||
|
|
||||||
|
if logLevel != nil && *logLevel != "" {
|
||||||
level = *logLevel
|
level = *logLevel
|
||||||
}
|
}
|
||||||
helper.ConfigureLogger(level)
|
logger.SetLogLevel(level)
|
||||||
|
|
||||||
if inDir == nil || *inDir == "" {
|
if inDir == nil || *inDir == "" {
|
||||||
log.Panic("input directory not specified")
|
logger.Exit("input directory not specified")
|
||||||
}
|
}
|
||||||
iDir := path.Clean(*inDir)
|
iDir := path.Clean(*inDir)
|
||||||
inDir = &iDir
|
inDir = &iDir
|
||||||
log.Infof("input directory: %s", *inDir)
|
logger.I("input directory: %s", *inDir)
|
||||||
|
|
||||||
if outDir == nil || *outDir == "" {
|
if outDir == nil || *outDir == "" {
|
||||||
log.Panic("output directory not specified")
|
logger.Exit("output directory not specified")
|
||||||
}
|
}
|
||||||
oDir := path.Clean(*outDir)
|
oDir := path.Clean(*outDir)
|
||||||
outDir = &oDir
|
outDir = &oDir
|
||||||
log.Infof("output directory: %s", *outDir)
|
logger.I("output directory: %s", *outDir)
|
||||||
|
|
||||||
if createOutDir != nil && *createOutDir {
|
if createOutDir != nil && *createOutDir {
|
||||||
if _, err := os.Stat(*outDir); os.IsNotExist(err) {
|
if _, err := os.Stat(*outDir); os.IsNotExist(err) {
|
||||||
log.Debugf("output directory '%s' does not exist", *outDir)
|
logger.D("output directory '%s' does not exist", *outDir)
|
||||||
log.Debugf("trying to create output directory: %s", *outDir)
|
logger.D("trying to create output directory: %s", *outDir)
|
||||||
err := os.MkdirAll(*outDir, 0755)
|
err := os.MkdirAll(*outDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
logger.Log.Panic(err)
|
||||||
}
|
}
|
||||||
log.Noticef("created output directory: %s", *outDir)
|
logger.I("created output directory: %s", *outDir)
|
||||||
} else {
|
} else {
|
||||||
log.Noticef("output directory '%s' already exists", *outDir)
|
logger.I("output directory '%s' already exists", *outDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fD, err := os.Stat(*outDir); os.IsNotExist(err) {
|
if fD, err := os.Stat(*outDir); os.IsNotExist(err) {
|
||||||
log.Panicf("output directory '%s' does not exist, try -create parameter or create manually", *outDir)
|
logger.Eexit(err, "output directory '%s' does not exist, try -create parameter or create manually", *outDir)
|
||||||
} else {
|
} else {
|
||||||
if fD == nil {
|
if fD == nil {
|
||||||
log.Panicf("something went wrong, could not get file handle for output dir %s", *outDir)
|
logger.P("something went wrong, could not get file handle for output dir %s", *outDir)
|
||||||
} else if !fD.IsDir() {
|
} else if !fD.IsDir() {
|
||||||
log.Panicf("output directory '%s' is not a directory", *outDir)
|
logger.Exit("output directory '%s' is not a directory", *outDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("reading global config...")
|
logger.D("reading global config...")
|
||||||
configFilename := *inDir + "/config.yml"
|
configFilename := *inDir + "/config.yml"
|
||||||
err := config.ReadGlobalConfig(configFilename)
|
err := mark2web.Config.ReadFromFile(configFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("could not read file '%s': %s", configFilename, err)
|
logger.Eexit(err, "could not read file '%s'", configFilename)
|
||||||
}
|
}
|
||||||
config.Config.Directories.Input = *inDir
|
mark2web.Config.Directories.Input = *inDir
|
||||||
config.Config.Directories.Output = *outDir
|
mark2web.Config.Directories.Output = *outDir
|
||||||
|
|
||||||
log.Debugf("reading input directory %s", *inDir)
|
logger.D("reading input directory %s", *inDir)
|
||||||
|
|
||||||
defaultTemplate := "base.html"
|
defaultTemplate := "base.html"
|
||||||
defaultInputFile := "README.md"
|
defaultInputFile := "README.md"
|
||||||
@@ -104,40 +109,35 @@ func main() {
|
|||||||
defaultFilenameIgnore := "^_"
|
defaultFilenameIgnore := "^_"
|
||||||
defaultFilenameOutputExtension := "html"
|
defaultFilenameOutputExtension := "html"
|
||||||
|
|
||||||
defaultPathConfig := new(config.PathConfig)
|
defaultPathConfig := new(mark2web.PathConfig)
|
||||||
defaultPathConfig.Template = &defaultTemplate
|
defaultPathConfig.Template = &defaultTemplate
|
||||||
defaultPathConfig.Index = &config.IndexConfig{
|
defaultPathConfig.Index = &mark2web.IndexConfig{
|
||||||
InputFile: &defaultInputFile,
|
InputFile: &defaultInputFile,
|
||||||
OutputFile: &defaultOutputFile,
|
OutputFile: &defaultOutputFile,
|
||||||
}
|
}
|
||||||
defaultPathConfig.Path = &config.DirnameConfig{
|
defaultPathConfig.Path = &mark2web.DirnameConfig{
|
||||||
Strip: &defaultPathStrip,
|
Strip: &defaultPathStrip,
|
||||||
IgnoreForNav: &defaultPathIgnoreForNav,
|
IgnoreForNav: &defaultPathIgnoreForNav,
|
||||||
}
|
}
|
||||||
defaultPathConfig.Filename = &config.FilenameConfig{
|
defaultPathConfig.Filename = &mark2web.FilenameConfig{
|
||||||
Strip: &defaultFilenameStrip,
|
Strip: &defaultFilenameStrip,
|
||||||
Ignore: &defaultFilenameIgnore,
|
Ignore: &defaultFilenameIgnore,
|
||||||
OutputExtension: &defaultFilenameOutputExtension,
|
OutputExtension: &defaultFilenameOutputExtension,
|
||||||
}
|
}
|
||||||
defaultPathConfig.Imaging = &config.ImagingConfig{
|
defaultPathConfig.Imaging = &mark2web.ImagingConfig{
|
||||||
Width: 1920,
|
Width: 1920,
|
||||||
Height: 1920,
|
Height: 1920,
|
||||||
Process: "fit",
|
Process: "fit",
|
||||||
Quality: 75,
|
Quality: 75,
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.ReadContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig)
|
filtersDir := *inDir + "/templates/filters"
|
||||||
//spew.Dump(contentConfig)
|
|
||||||
|
|
||||||
//spew.Dump(navMap)
|
|
||||||
|
|
||||||
templatesDir := *inDir + "/templates"
|
|
||||||
helper.SetTemplateDir(templatesDir)
|
|
||||||
filtersDir := templatesDir + "/filters"
|
|
||||||
if _, err := os.Stat(filtersDir); !os.IsNotExist(err) {
|
if _, err := os.Stat(filtersDir); !os.IsNotExist(err) {
|
||||||
helper.RegisterFilters(filtersDir)
|
filter.RegisterFilters(filtersDir)
|
||||||
}
|
}
|
||||||
helper.ProcessContent(contentConfig, contentConfig)
|
|
||||||
|
|
||||||
helper.ProcessAssets()
|
mark2web.Run(*inDir, *outDir, defaultPathConfig)
|
||||||
|
|
||||||
|
logger.N("done")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
This:
|
This:
|
||||||
GoTo: main/home
|
GoTo: main/home/
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
This:
|
This:
|
||||||
GoTo: main/home
|
GoTo: main/home/
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
|
||||||
cpy "github.com/otiai10/copy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProcessAssets copies the assets from input to output dir
|
|
||||||
func ProcessAssets() {
|
|
||||||
switch config.Config.Assets.Action {
|
|
||||||
case "copy":
|
|
||||||
from := config.Config.Assets.FromPath
|
|
||||||
to := config.Config.Assets.ToPath
|
|
||||||
if !strings.HasPrefix(from, "/") {
|
|
||||||
from = config.Config.Directories.Input + "/" + from
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(to, "/") {
|
|
||||||
to = config.Config.Directories.Output + "/" + to
|
|
||||||
}
|
|
||||||
Log.Noticef("copying assets from '%s' to '%s'", from, to)
|
|
||||||
err := cpy.Copy(from, to)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not copy assets from '%s' to '%s': %s", from, to, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
|
||||||
"github.com/Depado/bfchroma"
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/extemporalgenome/slug"
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
cpy "github.com/otiai10/copy"
|
|
||||||
"gopkg.in/russross/blackfriday.v2"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newContext() pongo2.Context {
|
|
||||||
return pongo2.Context{
|
|
||||||
"fnRequest": RequestFn,
|
|
||||||
"fnRender": RenderFn,
|
|
||||||
|
|
||||||
"AssetsPath": config.Config.Assets.ToPath,
|
|
||||||
|
|
||||||
"Timestamp": time.Now().Unix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillNodeConfig(node *config.PathConfigTree, inBase, outBase, dir string, conf *config.PathConfig) {
|
|
||||||
inPath := inBase
|
|
||||||
if dir != "" {
|
|
||||||
inPath += "/" + dir
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Infof("reading input directory: %s", inPath)
|
|
||||||
|
|
||||||
node.InputPath = inPath
|
|
||||||
|
|
||||||
// read config
|
|
||||||
newConfig := new(config.PathConfig)
|
|
||||||
Log.Debug("looking for config.yml ...")
|
|
||||||
configFile := inPath + "/config.yml"
|
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
|
||||||
Log.Debug("no config.yml found in this directory, using upper configs")
|
|
||||||
config.Merge(newConfig, conf)
|
|
||||||
// remove this
|
|
||||||
newConfig.This = config.ThisPathConfig{}
|
|
||||||
} else {
|
|
||||||
Log.Debug("reading config...")
|
|
||||||
data, err := ioutil.ReadFile(configFile)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not read file '%s': %s", configFile, err)
|
|
||||||
}
|
|
||||||
err = yaml.Unmarshal(data, newConfig)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not parse YAML file '%s': %s", configFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debug("merging config with upper config")
|
|
||||||
oldThis := newConfig.This
|
|
||||||
config.Merge(newConfig, conf)
|
|
||||||
newConfig.This = oldThis
|
|
||||||
|
|
||||||
Log.Debug(spew.Sdump(newConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Config = newConfig
|
|
||||||
|
|
||||||
// calc outDir
|
|
||||||
stripedDir := dir
|
|
||||||
var regexStr *string
|
|
||||||
if newConfig.Path != nil {
|
|
||||||
regexStr = newConfig.Path.Strip
|
|
||||||
}
|
|
||||||
if regexStr != nil && *regexStr != "" {
|
|
||||||
if regex, err := regexp.Compile(*regexStr); err != nil {
|
|
||||||
Log.Panicf("error compiling path.strip regex '%s' from '%s': %s", *regexStr, inBase+"/"+dir, err)
|
|
||||||
} else {
|
|
||||||
stripedDir = regex.ReplaceAllString(stripedDir, "$1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Config.This.Navname == nil {
|
|
||||||
navname := strings.Replace(stripedDir, "_", " ", -1)
|
|
||||||
node.Config.This.Navname = &navname
|
|
||||||
}
|
|
||||||
|
|
||||||
stripedDir = slug.Slug(stripedDir)
|
|
||||||
outPath := outBase + "/" + stripedDir
|
|
||||||
outPath = path.Clean(outPath)
|
|
||||||
|
|
||||||
Log.Infof("calculated output directory: %s", outPath)
|
|
||||||
node.OutputPath = outPath
|
|
||||||
|
|
||||||
// handle collections
|
|
||||||
for _, colConfig := range newConfig.This.Collections {
|
|
||||||
if colConfig != nil {
|
|
||||||
if colConfig.Name == nil || *colConfig.Name == "" {
|
|
||||||
Log.Panicf("missing Name in collection config in '%s'", inPath)
|
|
||||||
}
|
|
||||||
if colConfig.URL == nil || *colConfig.URL == "" {
|
|
||||||
Log.Panicf("missing EntriesJSON in collection config in '%s'", inPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.ColMap == nil {
|
|
||||||
node.ColMap = make(config.MapString)
|
|
||||||
}
|
|
||||||
ctx := newContext()
|
|
||||||
ctx["This"] = node.Config.This
|
|
||||||
ctx["Data"] = node.Config.Data
|
|
||||||
|
|
||||||
url, err := pongo2.RenderTemplateString(*colConfig.URL, ctx)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for Collection Element.URL in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
colData := jsonWebRequest(url)
|
|
||||||
node.ColMap[*colConfig.Name] = colData
|
|
||||||
|
|
||||||
if navT := colConfig.NavTemplate; navT != nil {
|
|
||||||
var entries []interface{}
|
|
||||||
var ok bool
|
|
||||||
if navT.EntriesAttribute != "" {
|
|
||||||
var colDataMap map[string]interface{}
|
|
||||||
if colDataMap, ok = colData.(map[string]interface{}); ok {
|
|
||||||
entries, ok = colDataMap[navT.EntriesAttribute].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
Log.Debug(spew.Sdump(colDataMap))
|
|
||||||
Log.Panicf("invalid json data in [%s] from url '%s' for entries", navT.EntriesAttribute, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entries, ok = colData.([]interface{})
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
Log.Debug(spew.Sdump(colData))
|
|
||||||
Log.Panicf("invalid json data from url '%s', need array of objects for entries or object with configured NavTemplate.EntriesAttribute", url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build navigation with detail sites
|
|
||||||
for idx, colEl := range entries {
|
|
||||||
ctxE := make(pongo2.Context)
|
|
||||||
err := config.Merge(&ctxE, ctx)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not merge context in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
var jsonCtx map[string]interface{}
|
|
||||||
if jsonCtx, ok = colEl.(map[string]interface{}); !ok {
|
|
||||||
Log.Debug(spew.Sdump(colEl))
|
|
||||||
Log.Panicf("no json object for entry index %d from url '%s'", idx, url)
|
|
||||||
}
|
|
||||||
err = config.Merge(&ctxE, pongo2.Context(jsonCtx))
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not merge context in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := ""
|
|
||||||
if navT.Template != "" {
|
|
||||||
tpl, err = pongo2.RenderTemplateString(navT.Template, ctxE)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for NavTemplate.Template in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tpl == "" {
|
|
||||||
tpl = *newConfig.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
dataKey := ""
|
|
||||||
if navT.DataKey != "" {
|
|
||||||
dataKey, err = pongo2.RenderTemplateString(navT.DataKey, ctxE)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for NavTemplate.DataKey in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
goTo, err := pongo2.RenderTemplateString(navT.GoTo, ctxE)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for NavTemplate.GoTo in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
goTo = strings.Trim(goTo, "/")
|
|
||||||
goTo = path.Clean(goTo)
|
|
||||||
|
|
||||||
if strings.Contains(goTo, "..") {
|
|
||||||
Log.Panicf("going back via .. in NavTemplate.GoTo forbidden in collection config in '%s': %s", inPath, goTo)
|
|
||||||
}
|
|
||||||
if goTo == "." {
|
|
||||||
Log.Panicf("invalid config '.' for NavTemplate.GoTo in collection config in '%s'", inPath)
|
|
||||||
}
|
|
||||||
if goTo == "" {
|
|
||||||
Log.Panicf("missing NavTemplate.GoTo in collection config in '%s'", inPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
navname := ""
|
|
||||||
if navT.Navname != "" {
|
|
||||||
navname, err = pongo2.RenderTemplateString(navT.Navname, ctxE)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for NavTemplate.Navname in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body := ""
|
|
||||||
if navT.Body != "" {
|
|
||||||
body, err = pongo2.RenderTemplateString(navT.Body, ctxE)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("invalid template string for NavTemplate.Body in '%s': %s", inPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add2Nav(node, node.Config, tpl, goTo, navname, colEl, dataKey, body, navT.Hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadContentDir walks through content directory and builds the tree of configurations
|
|
||||||
func ReadContentDir(inBase string, outBase string, dir string, conf *config.PathConfig, tree *config.PathConfigTree) {
|
|
||||||
fillNodeConfig(tree, inBase, outBase, dir, conf)
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(tree.InputPath)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// first only files
|
|
||||||
for _, f := range files {
|
|
||||||
p := tree.InputPath + "/" + f.Name()
|
|
||||||
if !f.IsDir() && f.Name() != "config.yml" {
|
|
||||||
switch path.Ext(f.Name()) {
|
|
||||||
case ".md":
|
|
||||||
Log.Debugf(".MD %s", p)
|
|
||||||
if tree.InputFiles == nil {
|
|
||||||
tree.InputFiles = make([]string, 0)
|
|
||||||
}
|
|
||||||
tree.InputFiles = append(tree.InputFiles, f.Name())
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
Log.Debugf("FIL %s", p)
|
|
||||||
if tree.OtherFiles == nil {
|
|
||||||
tree.OtherFiles = make([]string, 0)
|
|
||||||
}
|
|
||||||
tree.OtherFiles = append(tree.OtherFiles, f.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only directorys, needed config before
|
|
||||||
for _, f := range files {
|
|
||||||
p := tree.InputPath + "/" + f.Name()
|
|
||||||
if f.IsDir() {
|
|
||||||
Log.Debugf("DIR %s", p)
|
|
||||||
newTree := new(config.PathConfigTree)
|
|
||||||
if tree.Sub == nil {
|
|
||||||
tree.Sub = make([]*config.PathConfigTree, 0)
|
|
||||||
}
|
|
||||||
tree.Sub = append(tree.Sub, newTree)
|
|
||||||
ReadContentDir(tree.InputPath, tree.OutputPath, f.Name(), tree.Config, newTree)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessContent walks recursivly through the input paths and processes all files for output
|
|
||||||
func ProcessContent(rootConf, conf *config.PathConfigTree) {
|
|
||||||
CreateDirectory(conf.OutputPath)
|
|
||||||
|
|
||||||
curNavPath := strings.TrimPrefix(conf.OutputPath, config.Config.Directories.Output)
|
|
||||||
curNavPath = strings.TrimPrefix(curNavPath, "/")
|
|
||||||
curNavPath = path.Clean(curNavPath)
|
|
||||||
if curNavPath == "." {
|
|
||||||
curNavPath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
goTo := conf.Config.This.GoTo
|
|
||||||
if goTo != nil && *goTo != "" {
|
|
||||||
goToFixed := *goTo
|
|
||||||
if strings.HasPrefix(goToFixed, "/") {
|
|
||||||
goToFixed = BackToRoot(curNavPath) + goToFixed
|
|
||||||
}
|
|
||||||
goToFixed = path.Clean(goToFixed)
|
|
||||||
|
|
||||||
switch config.Config.Webserver.Type {
|
|
||||||
case "apache":
|
|
||||||
htaccessFile := conf.OutputPath + "/.htaccess"
|
|
||||||
Log.Noticef("writing '%s' with redirect to: %s", htaccessFile, goToFixed)
|
|
||||||
err := ioutil.WriteFile(htaccessFile, []byte(`RewriteEngine on
|
|
||||||
RewriteRule ^$ %{REQUEST_URI}`+goToFixed+`/ [R,L]
|
|
||||||
`), 0644)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not write '%s': %s", htaccessFile, err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range conf.InputFiles {
|
|
||||||
var input []byte
|
|
||||||
inFile := "InputString"
|
|
||||||
|
|
||||||
if file != "" {
|
|
||||||
inFile = conf.InputPath + "/" + file
|
|
||||||
Log.Debugf("reading file: %s", inFile)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
input, err = ioutil.ReadFile(inFile)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not read '%s':%s", inFile, err)
|
|
||||||
}
|
|
||||||
Log.Infof("processing input file '%s'", inFile)
|
|
||||||
} else {
|
|
||||||
// use input string if available and input filename == ""
|
|
||||||
var inputString *string
|
|
||||||
if i := conf.Config.Index; i != nil {
|
|
||||||
inputString = i.InputString
|
|
||||||
}
|
|
||||||
if inputString != nil {
|
|
||||||
Log.Debugf("using input string instead of file")
|
|
||||||
input = []byte(*inputString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig := new(config.PathConfig)
|
|
||||||
|
|
||||||
regex := regexp.MustCompile("(?s)^---(.*?)\\r?\\n\\r?---\\r?\\n\\r?")
|
|
||||||
yamlData := regex.Find(input)
|
|
||||||
if string(yamlData) != "" {
|
|
||||||
// replace tabs
|
|
||||||
yamlData = bytes.Replace(yamlData, []byte("\t"), []byte(" "), -1)
|
|
||||||
|
|
||||||
Log.Debugf("found yaml header in '%s', merging config", inFile)
|
|
||||||
err := yaml.Unmarshal(yamlData, newConfig)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not parse YAML header from '%s': %s", inFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debug("merging config with upper config")
|
|
||||||
oldThis := newConfig.This
|
|
||||||
config.Merge(newConfig, conf.Config)
|
|
||||||
newConfig.This = oldThis
|
|
||||||
|
|
||||||
Log.Debug(spew.Sdump(newConfig))
|
|
||||||
|
|
||||||
input = regex.ReplaceAll(input, []byte(""))
|
|
||||||
} else {
|
|
||||||
config.Merge(newConfig, conf.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore ???
|
|
||||||
ignoreFile := false
|
|
||||||
var ignoreRegex *string
|
|
||||||
var stripRegex *string
|
|
||||||
var outputExt *string
|
|
||||||
if f := newConfig.Filename; f != nil {
|
|
||||||
ignoreRegex = f.Ignore
|
|
||||||
stripRegex = f.Strip
|
|
||||||
outputExt = f.OutputExtension
|
|
||||||
}
|
|
||||||
if ignoreRegex != nil && *ignoreRegex != "" {
|
|
||||||
regex, err := regexp.Compile(*ignoreRegex)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not compile filename.ignore regexp '%s' for file '%s': %s", *ignoreRegex, inFile, err)
|
|
||||||
}
|
|
||||||
ignoreFile = regex.MatchString(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ignoreFile {
|
|
||||||
Log.Infof("ignoring file '%s', because of filename.ignore", inFile)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// build output filename
|
|
||||||
outputFilename := file
|
|
||||||
|
|
||||||
var indexInputFile *string
|
|
||||||
var indexOutputFile *string
|
|
||||||
if i := newConfig.Index; i != nil {
|
|
||||||
indexInputFile = i.InputFile
|
|
||||||
indexOutputFile = i.OutputFile
|
|
||||||
}
|
|
||||||
|
|
||||||
if indexInputFile != nil &&
|
|
||||||
*indexInputFile == file &&
|
|
||||||
indexOutputFile != nil &&
|
|
||||||
*indexOutputFile != "" {
|
|
||||||
outputFilename = *indexOutputFile
|
|
||||||
} else {
|
|
||||||
if stripRegex != nil && *stripRegex != "" {
|
|
||||||
regex, err := regexp.Compile(*stripRegex)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not compile filename.strip regexp '%s' for file '%s': %s", *stripRegex, inFile, err)
|
|
||||||
}
|
|
||||||
outputFilename = regex.ReplaceAllString(outputFilename, "$1")
|
|
||||||
}
|
|
||||||
if outputExt != nil && *outputExt != "" {
|
|
||||||
outputFilename += "." + *outputExt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := conf.OutputPath + "/" + outputFilename
|
|
||||||
Log.Debugf("using '%s' as output file", outFile)
|
|
||||||
|
|
||||||
// use --- for splitting document in markdown parts
|
|
||||||
regex := regexp.MustCompile("\\r?\\n\\r?---\\r?\\n\\r?")
|
|
||||||
inputParts := regex.Split(string(input), -1)
|
|
||||||
htmlParts := make([]*pongo2.Value, 0)
|
|
||||||
for _, iPart := range inputParts {
|
|
||||||
htmlParts = append(htmlParts,
|
|
||||||
pongo2.AsSafeValue(
|
|
||||||
string(renderMarkdown([]byte(iPart), newConfig.Markdown))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// build navigation
|
|
||||||
navMap := make(map[string]*NavElement)
|
|
||||||
navSlice := make([]*NavElement, 0)
|
|
||||||
navActive := make([]*NavElement, 0)
|
|
||||||
BuildNavigation(rootConf, &navMap, &navSlice, &navActive, curNavPath)
|
|
||||||
|
|
||||||
// read yaml header as data for template
|
|
||||||
ctx := newContext()
|
|
||||||
ctx["This"] = newConfig.This
|
|
||||||
ctx["Meta"] = newConfig.Meta
|
|
||||||
ctx["Data"] = newConfig.Data
|
|
||||||
ctx["ColMap"] = rootConf.ColMap // root as NavMap and NavSlice, for sub go to NavElement.ColMap
|
|
||||||
ctx["NavMap"] = navMap
|
|
||||||
ctx["NavSlice"] = navSlice
|
|
||||||
ctx["NavActive"] = navActive
|
|
||||||
ctx["Body"] = pongo2.AsSafeValue(string(renderMarkdown(input, newConfig.Markdown)))
|
|
||||||
ctx["BodyParts"] = htmlParts
|
|
||||||
ctx["CurrentPath"] = curNavPath
|
|
||||||
// set active nav element
|
|
||||||
if len(navActive) > 0 {
|
|
||||||
ctx["NavElement"] = navActive[len(navActive)-1]
|
|
||||||
} else {
|
|
||||||
// if no active path to content, we are in root dir
|
|
||||||
ctx["NavElement"] = &NavElement{
|
|
||||||
GoTo: BackToRoot(curNavPath),
|
|
||||||
Active: true,
|
|
||||||
ColMap: rootConf.ColMap,
|
|
||||||
Data: rootConf.Config.Data,
|
|
||||||
This: rootConf.Config.This,
|
|
||||||
SubMap: &navMap,
|
|
||||||
SubSlice: &navSlice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debugf("rendering template '%s' for '%s'", *newConfig.Template, outFile)
|
|
||||||
templateFilename := *newConfig.Template
|
|
||||||
result, err := RenderTemplate(*newConfig.Template, conf, newConfig, &ctx)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not execute template '%s' for input file '%s': %s", templateFilename, inFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = FixAssetsPath(result, curNavPath)
|
|
||||||
|
|
||||||
Log.Noticef("writing to output file: %s", outFile)
|
|
||||||
err = ioutil.WriteFile(outFile, []byte(result), 0644)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not write to output file '%s': %s", outFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Println(string(html))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process other files, copy...
|
|
||||||
for _, file := range conf.OtherFiles {
|
|
||||||
switch config.Config.OtherFiles.Action {
|
|
||||||
case "copy":
|
|
||||||
from := conf.InputPath + "/" + file
|
|
||||||
to := conf.OutputPath + "/" + file
|
|
||||||
Log.Noticef("copying file from '%s' to '%s'", from, to)
|
|
||||||
err := cpy.Copy(from, to)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not copy file from '%s' to '%s': %s", from, to, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for i < len(conf.Sub) {
|
|
||||||
ProcessContent(rootConf, conf.Sub[i])
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderMarkdown(input []byte, markdownConf *config.MarkdownConfig) []byte {
|
|
||||||
var options []blackfriday.Option
|
|
||||||
|
|
||||||
var chromaRenderer *bool
|
|
||||||
var chromaStyle *string
|
|
||||||
if m := markdownConf; m != nil {
|
|
||||||
chromaRenderer = m.ChromaRenderer
|
|
||||||
chromaStyle = m.ChromaStyle
|
|
||||||
}
|
|
||||||
if chromaStyle == nil {
|
|
||||||
style := "monokai"
|
|
||||||
chromaStyle = &style
|
|
||||||
}
|
|
||||||
if chromaRenderer != nil && *chromaRenderer {
|
|
||||||
options = []blackfriday.Option{
|
|
||||||
blackfriday.WithRenderer(
|
|
||||||
bfchroma.NewRenderer(
|
|
||||||
bfchroma.Style(*chromaStyle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix \r from markdown for blackfriday
|
|
||||||
input = bytes.Replace(input, []byte("\r"), []byte(""), -1)
|
|
||||||
return blackfriday.Run(input, options...)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import "os"
|
|
||||||
|
|
||||||
// CreateDirectory creates direcory with all missing parents and panic if error
|
|
||||||
func CreateDirectory(dir string) {
|
|
||||||
Log.Debugf("trying to create output directory: %s", dir)
|
|
||||||
|
|
||||||
if dirH, err := os.Stat(dir); os.IsNotExist(err) {
|
|
||||||
err := os.MkdirAll(dir, 0755)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not create output directory '%s': %s", dir, err)
|
|
||||||
}
|
|
||||||
Log.Noticef("created output directory: %s", dir)
|
|
||||||
} else if dirH != nil {
|
|
||||||
if dirH.IsDir() {
|
|
||||||
Log.Noticef("output directory '%s' already exists", dir)
|
|
||||||
} else {
|
|
||||||
Log.Panicf("output directory '%s' is no directory", dir)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.Panicf("unknown error for output directory '%s': %s", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/op/go-logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Log is global logger
|
|
||||||
var Log = logging.MustGetLogger("myLogger")
|
|
||||||
|
|
||||||
// ConfigureLogger sets logger backend and level
|
|
||||||
func ConfigureLogger(level string) {
|
|
||||||
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
|
||||||
logBackendFormatter := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(
|
|
||||||
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
|
||||||
))
|
|
||||||
logBackendLeveled := logging.AddModuleLevel(logBackendFormatter)
|
|
||||||
logBackendLevel := logging.INFO
|
|
||||||
switch level {
|
|
||||||
case "debug":
|
|
||||||
logBackendLevel = logging.DEBUG
|
|
||||||
break
|
|
||||||
|
|
||||||
case "info":
|
|
||||||
logBackendLevel = logging.INFO
|
|
||||||
break
|
|
||||||
|
|
||||||
case "notice":
|
|
||||||
logBackendLevel = logging.NOTICE
|
|
||||||
break
|
|
||||||
|
|
||||||
case "warning":
|
|
||||||
logBackendLevel = logging.WARNING
|
|
||||||
break
|
|
||||||
|
|
||||||
case "error":
|
|
||||||
logBackendLevel = logging.ERROR
|
|
||||||
break
|
|
||||||
|
|
||||||
}
|
|
||||||
logBackendLeveled.SetLevel(logBackendLevel, "")
|
|
||||||
logging.SetBackend(logBackendLeveled)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
spew.Config.DisablePointerAddresses = true
|
|
||||||
spew.Config.DisableCapacities = true
|
|
||||||
spew.Config.DisableMethods = true
|
|
||||||
spew.Config.DisablePointerMethods = true
|
|
||||||
}
|
|
||||||
103
helper/render.go
103
helper/render.go
@@ -1,103 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var templateCache = make(map[string]*pongo2.Template)
|
|
||||||
var currentContext *pongo2.Context
|
|
||||||
var currentTreeNodeConfig *config.PathConfigTree
|
|
||||||
var currentPathConfig *config.PathConfig
|
|
||||||
var templateDir string
|
|
||||||
|
|
||||||
// BackToRoot builds ../../ string
|
|
||||||
func BackToRoot(curNavPath string) string {
|
|
||||||
tmpPath := ""
|
|
||||||
if curNavPath != "" {
|
|
||||||
for i := strings.Count(curNavPath, "/") + 1; i > 0; i-- {
|
|
||||||
tmpPath += "../"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmpPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveNavPath fixes nav target relative to current navigation path
|
|
||||||
func ResolveNavPath(target string) string {
|
|
||||||
curNavPath := (*currentContext)["CurrentPath"].(string)
|
|
||||||
if strings.HasPrefix(target, "/") {
|
|
||||||
target = BackToRoot(curNavPath) + target
|
|
||||||
}
|
|
||||||
target = path.Clean(target)
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveOutputPath fixes output directory relative to current navigation path
|
|
||||||
func ResolveOutputPath(target string) string {
|
|
||||||
if strings.HasPrefix(target, "/") {
|
|
||||||
target = config.Config.Directories.Output + "/" + target
|
|
||||||
} else {
|
|
||||||
target = currentTreeNodeConfig.OutputPath + "/" + target
|
|
||||||
}
|
|
||||||
return path.Clean(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveInputPath fixes input directory relative to current navigation path
|
|
||||||
func ResolveInputPath(target string) string {
|
|
||||||
if strings.HasPrefix(target, "/") {
|
|
||||||
target = config.Config.Directories.Input + "/" + target
|
|
||||||
} else {
|
|
||||||
target = currentTreeNodeConfig.InputPath + "/" + target
|
|
||||||
}
|
|
||||||
return path.Clean(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTemplateDir sets base directory for searching template files
|
|
||||||
func SetTemplateDir(dir string) {
|
|
||||||
templateDir = dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderTemplate renders a pongo2 template with context
|
|
||||||
func RenderTemplate(filename string, treeNodeConfig *config.PathConfigTree, pathConfig *config.PathConfig, ctx *pongo2.Context) (string, error) {
|
|
||||||
currentContext = ctx
|
|
||||||
currentTreeNodeConfig = treeNodeConfig
|
|
||||||
currentPathConfig = pathConfig
|
|
||||||
templateFile := templateDir + "/" + filename
|
|
||||||
template := templateCache[templateFile]
|
|
||||||
if template == nil {
|
|
||||||
var err error
|
|
||||||
if template, err = pongo2.FromFile(templateFile); err != nil {
|
|
||||||
log.Panicf("could not parse template '%s': %s", templateFile, err)
|
|
||||||
} else {
|
|
||||||
templateCache[templateFile] = template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return template.Execute(*ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FixAssetsPath replaces assets path based on current path
|
|
||||||
func FixAssetsPath(str, curNavPath string) string {
|
|
||||||
if find := config.Config.Assets.FixTemplate.Find; find != "" {
|
|
||||||
Log.Debugf("fixing assets paths for path '%s'", curNavPath)
|
|
||||||
repl := config.Config.Assets.FixTemplate.Replace
|
|
||||||
toPath := config.Config.Assets.ToPath
|
|
||||||
|
|
||||||
bToRoot := BackToRoot(curNavPath)
|
|
||||||
regex, err := regexp.Compile(find)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("could not compile regexp '%s' for assets path: %s", find, err)
|
|
||||||
}
|
|
||||||
repl = bToRoot + toPath + "/" + repl
|
|
||||||
repl = path.Clean(repl) + "/"
|
|
||||||
Log.Debugf("new assets paths: %s", repl)
|
|
||||||
return regex.ReplaceAllString(str, repl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ddliu/motto"
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
_ "github.com/flosch/pongo2-addons"
|
|
||||||
_ "github.com/robertkrimen/otto/underscore"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
err := pongo2.ReplaceFilter("markdown", MarkdownFilter)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newFilters := map[string]pongo2.FilterFunction{
|
|
||||||
"image_process": ImageProcessFilter,
|
|
||||||
"relative_path": RelativePathFilter,
|
|
||||||
"json": JSONFilter,
|
|
||||||
"dump": DumpFilter,
|
|
||||||
}
|
|
||||||
for name, fn := range newFilters {
|
|
||||||
err := pongo2.RegisterFilter(name, fn)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DumpFilter is a pongo2 filter, which returns a spew.Dump of the input
|
|
||||||
func DumpFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
dumpString := spew.Sdump(in.Interface())
|
|
||||||
return pongo2.AsValue(string(dumpString)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONFilter is a pongo2 filter, which returns a json string of the input
|
|
||||||
func JSONFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
pretty := false
|
|
||||||
for _, s := range strings.Split(param.String(), ",") {
|
|
||||||
switch s {
|
|
||||||
case "pretty":
|
|
||||||
pretty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
var jsonBytes []byte
|
|
||||||
if pretty {
|
|
||||||
jsonBytes, err = json.MarshalIndent(in.Interface(), "", " ")
|
|
||||||
|
|
||||||
} else {
|
|
||||||
jsonBytes, err = json.Marshal(in.Interface())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:json",
|
|
||||||
OrigError: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pongo2.AsSafeValue(string(jsonBytes)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownFilter is a pongo2 filter, which converts markdown to html
|
|
||||||
func MarkdownFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
return pongo2.AsSafeValue(
|
|
||||||
string(
|
|
||||||
renderMarkdown(
|
|
||||||
[]byte(in.String()),
|
|
||||||
currentPathConfig.Markdown,
|
|
||||||
))),
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseImageParams(str string) (*config.ImagingConfig, error) {
|
|
||||||
p := config.ImagingConfig{}
|
|
||||||
if str == "" {
|
|
||||||
config.Merge(&p, currentPathConfig.Imaging)
|
|
||||||
// Filename and Format are only valid for current image
|
|
||||||
p.Filename = ""
|
|
||||||
p.Format = ""
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
for _, s := range strings.Split(str, ",") {
|
|
||||||
e := strings.Split(s, "=")
|
|
||||||
if len(e) < 2 {
|
|
||||||
return nil, fmt.Errorf("invalid image parameter: %s", s)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
switch e[0] {
|
|
||||||
case "w":
|
|
||||||
p.Width, err = strconv.Atoi(e[1])
|
|
||||||
case "h":
|
|
||||||
p.Height, err = strconv.Atoi(e[1])
|
|
||||||
case "f":
|
|
||||||
p.Filename = e[1]
|
|
||||||
case "p":
|
|
||||||
p.Process = e[1]
|
|
||||||
case "a":
|
|
||||||
p.Anchor = e[1]
|
|
||||||
case "q":
|
|
||||||
p.Quality, err = strconv.Atoi(e[1])
|
|
||||||
if p.Quality < 0 || p.Quality > 100 {
|
|
||||||
err = errors.New("q= must be between 1 and 100")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid image parameter: %s", s)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not convert image parameter to correct value type for '%s': %s", s, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImageFromURL(url string) (image.Image, string, error) {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("could not get url '%s': %s", url, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
img, format, err := image.Decode(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("could read body from url '%s': %s", url, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return img, format, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageProcessFilter read the image url and process parameters and saves the resized/processed image
|
|
||||||
// param: w=WITDH,h=HEIGHT
|
|
||||||
func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
imgSource := in.String()
|
|
||||||
p, err := parseImageParams(param.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: errors.New("no imaging config defined"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var img image.Image
|
|
||||||
if p.Process == "" {
|
|
||||||
p.Process = "resize"
|
|
||||||
}
|
|
||||||
filePrefix := fmt.Sprintf(
|
|
||||||
"%s_%dx%d_q%03d",
|
|
||||||
p.Process,
|
|
||||||
p.Width,
|
|
||||||
p.Height,
|
|
||||||
p.Quality,
|
|
||||||
)
|
|
||||||
if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") {
|
|
||||||
// remote file
|
|
||||||
img, p.Format, err = getImageFromURL(imgSource)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: fmt.Errorf("could not open image '%s': %s", imgSource, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// build filename
|
|
||||||
if p.Filename == "" {
|
|
||||||
var fBase string
|
|
||||||
if u, _ := url.Parse(imgSource); u != nil {
|
|
||||||
fBase = strings.Split(path.Base(u.Path), ".")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Filename = fmt.Sprintf(
|
|
||||||
"%s_%x_%s.%s",
|
|
||||||
filePrefix,
|
|
||||||
md5.Sum([]byte(imgSource)),
|
|
||||||
fBase,
|
|
||||||
p.Format,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// local file
|
|
||||||
imgSource = ResolveInputPath(imgSource)
|
|
||||||
if p.Filename == "" {
|
|
||||||
p.Filename = fmt.Sprintf(
|
|
||||||
"%s_%s",
|
|
||||||
filePrefix,
|
|
||||||
path.Base(imgSource),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imgTarget := ResolveOutputPath(p.Filename)
|
|
||||||
|
|
||||||
if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() {
|
|
||||||
Log.Noticef("skipped processing image from %s to %s, file already exists", imgSource, imgTarget)
|
|
||||||
} else {
|
|
||||||
Log.Noticef("processing image from %s to %s", imgSource, imgTarget)
|
|
||||||
if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") {
|
|
||||||
// webrequest before finding target filename, because of file format in filename
|
|
||||||
} else {
|
|
||||||
img, err = imaging.Open(imgSource, imaging.AutoOrientation(true))
|
|
||||||
if err != nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: fmt.Errorf("could not open image '%s': %s", imgSource, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.Process {
|
|
||||||
case "resize":
|
|
||||||
img = imaging.Resize(img, p.Width, p.Height, imaging.Lanczos)
|
|
||||||
case "fit":
|
|
||||||
img = imaging.Fit(img, p.Width, p.Height, imaging.Lanczos)
|
|
||||||
case "fill":
|
|
||||||
var anchor imaging.Anchor
|
|
||||||
switch strings.ToLower(p.Anchor) {
|
|
||||||
case "":
|
|
||||||
fallthrough
|
|
||||||
case "center":
|
|
||||||
anchor = imaging.Center
|
|
||||||
case "topleft":
|
|
||||||
anchor = imaging.TopLeft
|
|
||||||
case "top":
|
|
||||||
anchor = imaging.Top
|
|
||||||
case "topright":
|
|
||||||
anchor = imaging.TopRight
|
|
||||||
case "left":
|
|
||||||
anchor = imaging.Left
|
|
||||||
case "right":
|
|
||||||
anchor = imaging.Right
|
|
||||||
case "bottomleft":
|
|
||||||
anchor = imaging.BottomLeft
|
|
||||||
case "bottom":
|
|
||||||
anchor = imaging.Bottom
|
|
||||||
case "bottomright":
|
|
||||||
anchor = imaging.BottomRight
|
|
||||||
default:
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: fmt.Errorf("unknown anchor a=%s definition", p.Anchor),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
img = imaging.Fill(img, p.Width, p.Height, anchor, imaging.Lanczos)
|
|
||||||
default:
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: fmt.Errorf("invalid p parameter '%s'", p.Process),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encodeOptions = make([]imaging.EncodeOption, 0)
|
|
||||||
if p.Quality > 0 {
|
|
||||||
encodeOptions = append(encodeOptions, imaging.JPEGQuality(p.Quality))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = imaging.Save(img, imgTarget, encodeOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &pongo2.Error{
|
|
||||||
Sender: "filter:image_resize",
|
|
||||||
OrigError: fmt.Errorf("could save image '%s': %s", imgTarget, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pongo2.AsValue(ResolveNavPath(p.Filename)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelativePathFilter returns the relative path to navpoint based on current nav
|
|
||||||
func RelativePathFilter(in, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
return pongo2.AsValue(
|
|
||||||
ResolveNavPath(
|
|
||||||
in.String(),
|
|
||||||
),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterFilters reads a directory and register filters from files within it
|
|
||||||
func RegisterFilters(dir string) {
|
|
||||||
files, err := ioutil.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not read from template filters dir '%s': %s", dir, err)
|
|
||||||
}
|
|
||||||
for _, f := range files {
|
|
||||||
if !f.IsDir() {
|
|
||||||
switch path.Ext(f.Name()) {
|
|
||||||
case ".js":
|
|
||||||
fileBase := strings.TrimSuffix(f.Name(), ".js")
|
|
||||||
jsFile := dir + "/" + f.Name()
|
|
||||||
Log.Debugf("trying to register filter from: %s", jsFile)
|
|
||||||
/*
|
|
||||||
jsStr, err := ioutil.ReadFile(jsFile)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not read '%s': %s", jsFile, err)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
vm := motto.New()
|
|
||||||
fn, err := vm.Run(jsFile)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("error in javascript vm for '%s': %s", jsFile, err)
|
|
||||||
}
|
|
||||||
if !fn.IsFunction() {
|
|
||||||
Log.Panicf("%s does not contain a function code", jsFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pongo2.RegisterFilter(
|
|
||||||
fileBase,
|
|
||||||
func(in, param *pongo2.Value) (out *pongo2.Value, erro *pongo2.Error) {
|
|
||||||
thisObj, _ := vm.Object("({})")
|
|
||||||
if currentContext != nil {
|
|
||||||
thisObj.Set("context", *currentContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not set context as in '%s': %s", jsFile, err)
|
|
||||||
}
|
|
||||||
ret, err := fn.Call(thisObj.Value(), in.Interface(), param.Interface())
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("error in javascript file '%s' while calling returned function: %s", jsFile, err)
|
|
||||||
}
|
|
||||||
retGo, err := ret.Export()
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("export error for '%s': %s", jsFile, err)
|
|
||||||
}
|
|
||||||
return pongo2.AsValue(retGo), nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not register filter from '%s': %s", jsFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func jsonWebRequest(url string) interface{} {
|
|
||||||
Log.Noticef("requesting url via GET %s", url)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not get url '%s': %s", url, err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
Log.Panicf("could not read body from url '%s': %s", url, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debugf("output from url '%s':\n%s", url, string(body))
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
Log.Panicf("bad status '%d - %s' from url '%s'", resp.StatusCode, resp.Status, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
|
||||||
|
|
||||||
if strings.Contains(contentType, "json") {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Log.Panicf("is not json '%s' from url '%s'", contentType, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMap := make(map[string]interface{})
|
|
||||||
err = json.Unmarshal(body, &jsonMap)
|
|
||||||
if err == nil {
|
|
||||||
return jsonMap
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonArrayMap := make([]map[string]interface{}, 0)
|
|
||||||
err = json.Unmarshal(body, &jsonArrayMap)
|
|
||||||
if err == nil {
|
|
||||||
return jsonArrayMap
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Panicf("could not read json from '%s': invalid type", url)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestFn will make a web request and returns map[string]interface form pongo2
|
|
||||||
func RequestFn(url *pongo2.Value, args ...*pongo2.Value) *pongo2.Value {
|
|
||||||
u := url.String()
|
|
||||||
return pongo2.AsValue(jsonWebRequest(u))
|
|
||||||
}
|
|
||||||
|
|
||||||
func add2Nav(currentNode *config.PathConfigTree, pathConfig *config.PathConfig, tplFilename, outDir string, navname string, ctx interface{}, dataMapKey string, body string, hidden bool) {
|
|
||||||
newNodeConfig := new(config.PathConfigTree)
|
|
||||||
fillNodeConfig(
|
|
||||||
newNodeConfig,
|
|
||||||
currentNode.InputPath,
|
|
||||||
currentNode.OutputPath,
|
|
||||||
outDir,
|
|
||||||
pathConfig,
|
|
||||||
)
|
|
||||||
if navname != "" {
|
|
||||||
newNodeConfig.Config.This = config.ThisPathConfig{
|
|
||||||
Navname: &navname,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dataMapKey != "" {
|
|
||||||
if newNodeConfig.Config.Data == nil {
|
|
||||||
newNodeConfig.Config.Data = make(config.MapString)
|
|
||||||
}
|
|
||||||
// as submap in Data
|
|
||||||
newNodeConfig.Config.Data[dataMapKey] = ctx
|
|
||||||
} else if m, ok := ctx.(map[string]interface{}); ok {
|
|
||||||
// direct set data
|
|
||||||
newNodeConfig.Config.Data = m
|
|
||||||
}
|
|
||||||
|
|
||||||
// fake via normal file behavior
|
|
||||||
newNodeConfig.Config.Template = &tplFilename
|
|
||||||
newNodeConfig.InputFiles = []string{""} // empty file is special for use InputString
|
|
||||||
indexInFile := ""
|
|
||||||
indexOutFile := "index.html"
|
|
||||||
if idx := newNodeConfig.Config.Index; idx != nil {
|
|
||||||
if idx.OutputFile != nil && *idx.OutputFile != "" {
|
|
||||||
indexOutFile = *idx.OutputFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newNodeConfig.Config.Index = &config.IndexConfig{
|
|
||||||
InputFile: &indexInFile,
|
|
||||||
OutputFile: &indexOutFile,
|
|
||||||
InputString: &body,
|
|
||||||
}
|
|
||||||
newNodeConfig.Hidden = hidden
|
|
||||||
|
|
||||||
currentNode.Sub = append(currentNode.Sub, newNodeConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderFn renders a pongo2 template with additional context
|
|
||||||
func RenderFn(templateFilename, outDir, ctx *pongo2.Value, param ...*pongo2.Value) *pongo2.Value {
|
|
||||||
dataMapKey := ""
|
|
||||||
body := ""
|
|
||||||
|
|
||||||
for i, p := range param {
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
dataMapKey = p.String()
|
|
||||||
case 1:
|
|
||||||
body = p.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add2Nav(currentTreeNodeConfig, currentPathConfig, templateFilename.String(), outDir.String(), "", ctx.Interface(), dataMapKey, body, true)
|
|
||||||
|
|
||||||
return pongo2.AsValue(nil)
|
|
||||||
}
|
|
||||||
63
pkg/filter/custom.go
Normal file
63
pkg/filter/custom.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
"github.com/ddliu/motto"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
_ "github.com/robertkrimen/otto/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterFilters reads a directory and register filters from files within it
|
||||||
|
func RegisterFilters(dir string) {
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
logger.Eexit(err, "could not read from template filters dir '%s'", dir)
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() {
|
||||||
|
switch path.Ext(f.Name()) {
|
||||||
|
case ".js":
|
||||||
|
fileBase := strings.TrimSuffix(f.Name(), ".js")
|
||||||
|
jsFile := dir + "/" + f.Name()
|
||||||
|
logger.D("trying to register filter from: %s", jsFile)
|
||||||
|
/*
|
||||||
|
jsStr, err := ioutil.ReadFile(jsFile)
|
||||||
|
if err != nil {
|
||||||
|
Log.Panicf("could not read '%s': %s", jsFile, err)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
vm := motto.New()
|
||||||
|
fn, err := vm.Run(jsFile)
|
||||||
|
logger.Eexit(err, "error in javascript vm for '%s'", jsFile)
|
||||||
|
if !fn.IsFunction() {
|
||||||
|
logger.Exit("%s does not contain a function code", jsFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pongo2.RegisterFilter(
|
||||||
|
fileBase,
|
||||||
|
func(in, param *pongo2.Value) (out *pongo2.Value, erro *pongo2.Error) {
|
||||||
|
thisObj, _ := vm.Object("({})")
|
||||||
|
var err error
|
||||||
|
if mark2web.CurrentContext != nil {
|
||||||
|
err = thisObj.Set("context", *mark2web.CurrentContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Perr(err, "could not set context in '%s': %s", jsFile)
|
||||||
|
ret, err := fn.Call(thisObj.Value(), in.Interface(), param.Interface())
|
||||||
|
logger.Eexit(err, "error in javascript file '%s' while calling returned function", jsFile)
|
||||||
|
|
||||||
|
retGo, err := ret.Export()
|
||||||
|
logger.Perr(err, "export error for '%s'", jsFile)
|
||||||
|
return pongo2.AsValue(retGo), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logger.Perr(err, "could not register filter from '%s'", jsFile)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
pkg/filter/dump.go
Normal file
12
pkg/filter/dump.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DumpFilter is a pongo2 filter, which returns a spew.Dump of the input
|
||||||
|
func DumpFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
dumpString := spew.Sdump(in.Interface())
|
||||||
|
return pongo2.AsValue(string(dumpString)), nil
|
||||||
|
}
|
||||||
24
pkg/filter/dump_test.go
Normal file
24
pkg/filter/dump_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDumpFilter(t *testing.T) {
|
||||||
|
Convey("set context", t, func() {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"testvar": "test",
|
||||||
|
}
|
||||||
|
Convey("parse template", func() {
|
||||||
|
output, err := pongo2.RenderTemplateString("{{ testvar|safe|dump }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, spew.Sdump("test"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
243
pkg/filter/image_process.go
Normal file
243
pkg/filter/image_process.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/jobm"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/webrequest"
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseImageParams(str string) (*mark2web.ImagingConfig, error) {
|
||||||
|
p := mark2web.ImagingConfig{}
|
||||||
|
if str == "" {
|
||||||
|
helper.Merge(&p, mark2web.CurrentTreeNode.Config.Imaging)
|
||||||
|
// Filename and Format are only valid for current image
|
||||||
|
p.Filename = ""
|
||||||
|
p.Format = ""
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
for _, s := range strings.Split(str, ",") {
|
||||||
|
e := strings.Split(s, "=")
|
||||||
|
if len(e) < 2 {
|
||||||
|
return nil, fmt.Errorf("invalid image parameter: %s", s)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch e[0] {
|
||||||
|
case "w":
|
||||||
|
p.Width, err = strconv.Atoi(e[1])
|
||||||
|
case "h":
|
||||||
|
p.Height, err = strconv.Atoi(e[1])
|
||||||
|
case "f":
|
||||||
|
p.Filename = e[1]
|
||||||
|
case "t":
|
||||||
|
p.TargetDir = e[1]
|
||||||
|
case "p":
|
||||||
|
p.Process = e[1]
|
||||||
|
case "a":
|
||||||
|
p.Anchor = e[1]
|
||||||
|
case "q":
|
||||||
|
p.Quality, err = strconv.Atoi(e[1])
|
||||||
|
if p.Quality < 0 || p.Quality > 100 {
|
||||||
|
err = errors.New("q= must be between 1 and 100")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid image parameter: %s", s)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not convert image parameter to correct value type for '%s': %s", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageProcessFilter read the image url and process parameters and saves the resized/processed image
|
||||||
|
// param: w=WITDH,h=HEIGHT
|
||||||
|
func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
imgSource := in.String()
|
||||||
|
p, err := parseImageParams(param.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:image_resize",
|
||||||
|
OrigError: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:image_resize",
|
||||||
|
OrigError: errors.New("no imaging config defined"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var img image.Image
|
||||||
|
if p.Process == "" {
|
||||||
|
p.Process = "resize"
|
||||||
|
}
|
||||||
|
filePrefix := fmt.Sprintf(
|
||||||
|
"%s_%dx%d_q%03d",
|
||||||
|
p.Process,
|
||||||
|
p.Width,
|
||||||
|
p.Height,
|
||||||
|
p.Quality,
|
||||||
|
)
|
||||||
|
if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") {
|
||||||
|
// build filename
|
||||||
|
if p.Filename == "" {
|
||||||
|
var fBase string
|
||||||
|
if u, _ := url.Parse(imgSource); u != nil {
|
||||||
|
fBase = path.Base(u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Filename = fmt.Sprintf(
|
||||||
|
"%s_%x_%s",
|
||||||
|
filePrefix,
|
||||||
|
md5.Sum([]byte(imgSource)),
|
||||||
|
fBase,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// local file
|
||||||
|
imgSource = mark2web.CurrentTreeNode.ResolveInputPath(imgSource)
|
||||||
|
if p.Filename == "" {
|
||||||
|
p.Filename = fmt.Sprintf(
|
||||||
|
"%s_%s",
|
||||||
|
filePrefix,
|
||||||
|
path.Base(imgSource),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgTarget string
|
||||||
|
if p.TargetDir != "" {
|
||||||
|
imgTarget = mark2web.CurrentTreeNode.ResolveOutputPath(
|
||||||
|
path.Clean(p.TargetDir) + "/" +
|
||||||
|
p.Filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
pt := path.Dir(imgTarget)
|
||||||
|
if _, err := os.Stat(pt); os.IsNotExist(err) {
|
||||||
|
logger.I("create image target dir: %s", pt)
|
||||||
|
if err := os.MkdirAll(pt, 0755); err != nil {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:image_resize",
|
||||||
|
OrigError: fmt.Errorf("could not create image target dir '%s': %s", pt, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Filename = mark2web.CurrentTreeNode.ResolveNavPath(p.TargetDir + "/" + p.Filename)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
imgTarget = mark2web.CurrentTreeNode.ResolveOutputPath(p.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() {
|
||||||
|
logger.N("skipped processing image from %s to %s, file already exists", imgSource, imgTarget)
|
||||||
|
} else {
|
||||||
|
jobm.Enqueue(jobm.Job{
|
||||||
|
Function: func() {
|
||||||
|
logger.N("processing image from %s to %s", imgSource, imgTarget)
|
||||||
|
if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") {
|
||||||
|
// remote file
|
||||||
|
img, p.Format, err = webrequest.GetImage(imgSource)
|
||||||
|
} else {
|
||||||
|
img, err = imaging.Open(imgSource, imaging.AutoOrientation(true))
|
||||||
|
}
|
||||||
|
logger.Eexit(err, "filter:image_resize, could not open image '%s'", imgSource)
|
||||||
|
|
||||||
|
switch p.Process {
|
||||||
|
case "resize":
|
||||||
|
img = imaging.Resize(img, p.Width, p.Height, imaging.Lanczos)
|
||||||
|
case "fit":
|
||||||
|
img = imaging.Fit(img, p.Width, p.Height, imaging.Lanczos)
|
||||||
|
case "fill":
|
||||||
|
var anchor imaging.Anchor
|
||||||
|
switch strings.ToLower(p.Anchor) {
|
||||||
|
case "":
|
||||||
|
fallthrough
|
||||||
|
case "center":
|
||||||
|
anchor = imaging.Center
|
||||||
|
case "topleft":
|
||||||
|
anchor = imaging.TopLeft
|
||||||
|
case "top":
|
||||||
|
anchor = imaging.Top
|
||||||
|
case "topright":
|
||||||
|
anchor = imaging.TopRight
|
||||||
|
case "left":
|
||||||
|
anchor = imaging.Left
|
||||||
|
case "right":
|
||||||
|
anchor = imaging.Right
|
||||||
|
case "bottomleft":
|
||||||
|
anchor = imaging.BottomLeft
|
||||||
|
case "bottom":
|
||||||
|
anchor = imaging.Bottom
|
||||||
|
case "bottomright":
|
||||||
|
anchor = imaging.BottomRight
|
||||||
|
default:
|
||||||
|
logger.Exit("filter:image_resize, unknown anchor a=%s definition", p.Anchor)
|
||||||
|
}
|
||||||
|
img = imaging.Fill(img, p.Width, p.Height, anchor, imaging.Lanczos)
|
||||||
|
default:
|
||||||
|
logger.Exit("filter:image_resize, invalid p parameter '%s'", p.Process)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Format == "" {
|
||||||
|
switch strings.ToLower(path.Ext(imgTarget)) {
|
||||||
|
case ".jpg", ".jpeg", ".gif", ".png":
|
||||||
|
var encodeOptions = make([]imaging.EncodeOption, 0)
|
||||||
|
if p.Quality > 0 {
|
||||||
|
encodeOptions = append(encodeOptions, imaging.JPEGQuality(p.Quality))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imaging.Save(img, imgTarget, encodeOptions...)
|
||||||
|
logger.Eerr(err, "filter:image_resize, could save image '%s'", imgTarget)
|
||||||
|
default:
|
||||||
|
logger.Exit("filter:image_resize, invalid filename extension for image: %s", imgTarget)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out, err := os.Create(imgTarget)
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
logger.Eexit(err, "filter:image_resize, could not create image file '%s'", imgTarget)
|
||||||
|
switch p.Format {
|
||||||
|
case "jpeg", "jpg":
|
||||||
|
var jpegOpt *jpeg.Options
|
||||||
|
if p.Quality > 0 {
|
||||||
|
jpegOpt = &jpeg.Options{
|
||||||
|
Quality: p.Quality,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = jpeg.Encode(out, img, jpegOpt)
|
||||||
|
case "png":
|
||||||
|
err = png.Encode(out, img)
|
||||||
|
case "gif":
|
||||||
|
err = gif.Encode(out, img, nil)
|
||||||
|
default:
|
||||||
|
logger.Exit("filter:image_resize, unknown format '%s' for '%s'", p.Format, imgSource)
|
||||||
|
}
|
||||||
|
logger.Eexit(err, "filter:image_resize, could not encode image file '%s'", imgTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.N("finished image: %s", imgTarget)
|
||||||
|
},
|
||||||
|
Description: imgSource,
|
||||||
|
Category: "image process",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return pongo2.AsValue(mark2web.CurrentTreeNode.ResolveNavPath(p.Filename)), nil
|
||||||
|
}
|
||||||
55
pkg/filter/image_process_test.go
Normal file
55
pkg/filter/image_process_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/jobm"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImageProcessFilter(t *testing.T) {
|
||||||
|
Convey("set context", t, func() {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"testlocal": "/img/test.jpg",
|
||||||
|
"testurl": "http://url",
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to check files after function calls, so no multithreading
|
||||||
|
jobm.SetNumCPU(1)
|
||||||
|
|
||||||
|
mark2web.Config.Directories.Input = "../../test/in"
|
||||||
|
mark2web.Config.Directories.Output = "../../test/out"
|
||||||
|
|
||||||
|
mark2web.CurrentTreeNode = &mark2web.TreeNode{
|
||||||
|
InputPath: "../../test/in/content",
|
||||||
|
OutputPath: "../../test/out",
|
||||||
|
Config: &mark2web.PathConfig{
|
||||||
|
Imaging: &mark2web.ImagingConfig{
|
||||||
|
Quality: 60,
|
||||||
|
Height: 300,
|
||||||
|
Width: 300,
|
||||||
|
Process: "fit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove("../../test/out/fit_300x300_q060_test.jpg")
|
||||||
|
|
||||||
|
Convey("local image with defaults", func() {
|
||||||
|
output, err := pongo2.RenderTemplateString("{{ testlocal|image_process }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "fit_300x300_q060_test.jpg")
|
||||||
|
|
||||||
|
Convey("local image with fit", func() {
|
||||||
|
output, err := pongo2.RenderTemplateString(`{{ testlocal|image_process:"p=fit,w=300,h=300,q=60,t=/" }}`, ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "/fit_300x300_q060_test.jpg")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
26
pkg/filter/init.go
Normal file
26
pkg/filter/init.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
_ "github.com/flosch/pongo2-addons"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := pongo2.ReplaceFilter("markdown", MarkdownFilter)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newFilters := map[string]pongo2.FilterFunction{
|
||||||
|
"image_process": ImageProcessFilter,
|
||||||
|
"relative_path": RelativePathFilter,
|
||||||
|
"json": JSONFilter,
|
||||||
|
"dump": DumpFilter,
|
||||||
|
}
|
||||||
|
for name, fn := range newFilters {
|
||||||
|
err := pongo2.RegisterFilter(name, fn)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pkg/filter/json.go
Normal file
35
pkg/filter/json.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONFilter is a pongo2 filter, which returns a json string of the input
|
||||||
|
func JSONFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
pretty := false
|
||||||
|
for _, s := range strings.Split(param.String(), ",") {
|
||||||
|
switch s {
|
||||||
|
case "pretty":
|
||||||
|
pretty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var jsonBytes []byte
|
||||||
|
if pretty {
|
||||||
|
jsonBytes, err = json.MarshalIndent(in.Interface(), "", " ")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
jsonBytes, err = json.Marshal(in.Interface())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:json",
|
||||||
|
OrigError: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pongo2.AsSafeValue(string(jsonBytes)), nil
|
||||||
|
}
|
||||||
50
pkg/filter/json_test.go
Normal file
50
pkg/filter/json_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJSONFilter(t *testing.T) {
|
||||||
|
Convey("set context", t, func() {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"teststr": "test",
|
||||||
|
"testmap": map[string]interface{}{
|
||||||
|
"float": 1.23,
|
||||||
|
"int": 5,
|
||||||
|
"str": "test",
|
||||||
|
},
|
||||||
|
"testerr": math.Inf(1),
|
||||||
|
}
|
||||||
|
Convey("parse template", func() {
|
||||||
|
output, err := pongo2.RenderTemplateString("{{ teststr|safe|json }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `"test"`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testmap|safe|json }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `{"float":1.23,"int":5,"str":"test"}`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString(`{{ testmap|safe|json:"pretty" }}`, ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `{
|
||||||
|
"float": 1.23,
|
||||||
|
"int": 5,
|
||||||
|
"str": "test"
|
||||||
|
}`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testnil|safe|json }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `null`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testerr|safe|json }}", ctx)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(output, ShouldEqual, ``)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
51
pkg/filter/markdown.go
Normal file
51
pkg/filter/markdown.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarkdownFilter is a pongo2 filter, which converts markdown to html
|
||||||
|
func MarkdownFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
chromaRenderer := false
|
||||||
|
chromaStyle := ""
|
||||||
|
if pStr := param.String(); pStr != "" {
|
||||||
|
for _, s := range strings.Split(pStr, ",") {
|
||||||
|
e := strings.Split(s, "=")
|
||||||
|
if len(e) < 2 {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:markdown",
|
||||||
|
OrigError: fmt.Errorf("invalid parameter: %s", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch e[0] {
|
||||||
|
case "s":
|
||||||
|
if e[1] == "" {
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:markdown",
|
||||||
|
OrigError: fmt.Errorf("need a syntax sheme name for parameter '%s='", e[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chromaRenderer = true
|
||||||
|
chromaStyle = e[1]
|
||||||
|
default:
|
||||||
|
return nil, &pongo2.Error{
|
||||||
|
Sender: "filter:markdown",
|
||||||
|
OrigError: fmt.Errorf("unknown parameter '%s='", e[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pongo2.AsSafeValue(
|
||||||
|
string(
|
||||||
|
helper.RenderMarkdown(
|
||||||
|
[]byte(in.String()),
|
||||||
|
chromaRenderer,
|
||||||
|
chromaStyle,
|
||||||
|
))),
|
||||||
|
nil
|
||||||
|
}
|
||||||
49
pkg/filter/markdown_test.go
Normal file
49
pkg/filter/markdown_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarkdownFilter(t *testing.T) {
|
||||||
|
Convey("set context", t, func() {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"testvar": "# test",
|
||||||
|
"testcode": "```sh\ntest=test\n```",
|
||||||
|
}
|
||||||
|
Convey("parse template", func() {
|
||||||
|
output, err := pongo2.RenderTemplateString("{{ testvar|markdown }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "<h1>test</h1>\n")
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testcode|markdown }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `<pre><code class="language-sh">test=test
|
||||||
|
</code></pre>
|
||||||
|
`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString(`{{ testcode|markdown:"s=monokai" }}`, ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, `<pre style="color:#f8f8f2;background-color:#272822">test<span style="color:#f92672">=</span>test
|
||||||
|
</pre>`)
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString(`{{ testcode|markdown:"s=" }}`, ctx)
|
||||||
|
So(output, ShouldBeEmpty)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldContainSubstring, "need a syntax sheme name for parameter")
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString(`{{ testcode|markdown:"test=test" }}`, ctx)
|
||||||
|
So(output, ShouldBeEmpty)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldContainSubstring, "unknown parameter")
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString(`{{ testcode|markdown:"s=monokai,test" }}`, ctx)
|
||||||
|
So(output, ShouldBeEmpty)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldContainSubstring, "invalid parameter: test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
15
pkg/filter/relative_path.go
Normal file
15
pkg/filter/relative_path.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RelativePathFilter returns the relative path to navpoint based on current nav
|
||||||
|
func RelativePathFilter(in, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
return pongo2.AsValue(
|
||||||
|
mark2web.CurrentTreeNode.ResolveNavPath(
|
||||||
|
in.String(),
|
||||||
|
),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
53
pkg/filter/relative_path_test.go
Normal file
53
pkg/filter/relative_path_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/mark2web"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRelativePathFilter(t *testing.T) {
|
||||||
|
Convey("set context", t, func() {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"testrel": "rel",
|
||||||
|
"testabs": "/abs",
|
||||||
|
"testsub": "../sub/rel",
|
||||||
|
}
|
||||||
|
|
||||||
|
mark2web.Config.Directories.Output = "../../test/out"
|
||||||
|
|
||||||
|
mark2web.CurrentTreeNode = &mark2web.TreeNode{
|
||||||
|
InputPath: "../../test/in/content",
|
||||||
|
OutputPath: "../../test/out/sub",
|
||||||
|
Config: &mark2web.PathConfig{
|
||||||
|
Imaging: &mark2web.ImagingConfig{
|
||||||
|
Quality: 60,
|
||||||
|
Height: 300,
|
||||||
|
Width: 300,
|
||||||
|
Process: "fit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("parse template", func() {
|
||||||
|
|
||||||
|
output, err := pongo2.RenderTemplateString("{{ testrel|relative_path }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "rel")
|
||||||
|
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testabs|relative_path }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "../abs")
|
||||||
|
|
||||||
|
/* TODO
|
||||||
|
output, err = pongo2.RenderTemplateString("{{ testsub|relative_path }}", ctx)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(output, ShouldEqual, "rel")
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
27
pkg/helper/dir.go
Normal file
27
pkg/helper/dir.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateDirectory creates direcory with all missing parents and panic if error
|
||||||
|
func CreateDirectory(dir string) {
|
||||||
|
logger.D("trying to create output directory: %s", dir)
|
||||||
|
|
||||||
|
if dirH, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
err := os.MkdirAll(dir, 0755)
|
||||||
|
logger.Eexit(err, "could not create output directory '%s'", dir)
|
||||||
|
|
||||||
|
logger.I("created output directory: %s", dir)
|
||||||
|
} else if dirH != nil {
|
||||||
|
if dirH.IsDir() {
|
||||||
|
logger.I("output directory '%s' already exists", dir)
|
||||||
|
} else {
|
||||||
|
logger.Exit("output directory '%s' is no directory", dir)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Perr(err, "unknown error for output directory '%s'", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
pkg/helper/map_string.go
Normal file
47
pkg/helper/map_string.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MapString is a map[string]interface{} which always unmarsahls yaml to map[string]interface{}
|
||||||
|
type MapString map[string]interface{}
|
||||||
|
|
||||||
|
// UnmarshalYAML handles all maps as map[string]interface{} for later JSON
|
||||||
|
// see https://github.com/elastic/beats/blob/6435194af9f42cbf778ca0a1a92276caf41a0da8/libbeat/common/mapstr.go
|
||||||
|
func (ms *MapString) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var result map[interface{}]interface{}
|
||||||
|
err := unmarshal(&result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*ms = cleanUpInterfaceMap(result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpInterfaceArray(in []interface{}) []interface{} {
|
||||||
|
result := make([]interface{}, len(in))
|
||||||
|
for i, v := range in {
|
||||||
|
result[i] = cleanUpMapValue(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpInterfaceMap(in map[interface{}]interface{}) MapString {
|
||||||
|
result := make(MapString)
|
||||||
|
for k, v := range in {
|
||||||
|
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpMapValue(v interface{}) interface{} {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return cleanUpInterfaceArray(v)
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
return cleanUpInterfaceMap(v)
|
||||||
|
case string, bool, int, int8, int16, int32, int64, float32, float64:
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
pkg/helper/markdown.go
Normal file
30
pkg/helper/markdown.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/Depado/bfchroma"
|
||||||
|
"gopkg.in/russross/blackfriday.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderMarkdown renders input to html with chroma syntax highlighting if wanted
|
||||||
|
func RenderMarkdown(input []byte, chromaRenderer bool, chromaStyle string) []byte {
|
||||||
|
var options []blackfriday.Option
|
||||||
|
|
||||||
|
if chromaStyle == "" {
|
||||||
|
chromaStyle = "monokai"
|
||||||
|
}
|
||||||
|
if chromaRenderer {
|
||||||
|
options = []blackfriday.Option{
|
||||||
|
blackfriday.WithRenderer(
|
||||||
|
bfchroma.NewRenderer(
|
||||||
|
bfchroma.Style(chromaStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix \r from markdown for blackfriday
|
||||||
|
input = bytes.Replace(input, []byte("\r"), []byte(""), -1)
|
||||||
|
return blackfriday.Run(input, options...)
|
||||||
|
}
|
||||||
28
pkg/helper/merge.go
Normal file
28
pkg/helper/merge.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ptrTransformer struct{}
|
||||||
|
|
||||||
|
func (t ptrTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
|
||||||
|
if typ.Kind() == reflect.Ptr {
|
||||||
|
return func(dst, src reflect.Value) error {
|
||||||
|
if dst.CanSet() {
|
||||||
|
if dst.IsNil() {
|
||||||
|
dst.Set(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges 2 objects or maps
|
||||||
|
func Merge(dst, src interface{}) error {
|
||||||
|
return mergo.Merge(dst, src, mergo.WithTransformers(ptrTransformer{}))
|
||||||
|
}
|
||||||
18
pkg/helper/regexp.go
Normal file
18
pkg/helper/regexp.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// GetRegexpParams gets a map of named regexp group matches
|
||||||
|
// use pe. (?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2}) as regexp
|
||||||
|
func GetRegexpParams(regEx *regexp.Regexp, str string) (paramsMap map[string]string) {
|
||||||
|
|
||||||
|
match := regEx.FindStringSubmatch(str)
|
||||||
|
|
||||||
|
paramsMap = make(map[string]string)
|
||||||
|
for i, name := range regEx.SubexpNames() {
|
||||||
|
if i > 0 && i <= len(match) {
|
||||||
|
paramsMap[name] = match[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
16
pkg/helper/string.go
Normal file
16
pkg/helper/string.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
// ShortenStringLeft shortens a string
|
||||||
|
func ShortenStringLeft(str string, num int) string {
|
||||||
|
if num <= 4 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
tstr := str
|
||||||
|
if len(str) > num {
|
||||||
|
if num > 3 {
|
||||||
|
num -= 3
|
||||||
|
}
|
||||||
|
tstr = "..." + str[len(str)-num:len(str)]
|
||||||
|
}
|
||||||
|
return tstr
|
||||||
|
}
|
||||||
61
pkg/jobm/jobmanager.go
Normal file
61
pkg/jobm/jobmanager.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package jobm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var numCPU = runtime.NumCPU()
|
||||||
|
|
||||||
|
// Job is a wrapper to descripe a Job function
|
||||||
|
type Job struct {
|
||||||
|
Function func()
|
||||||
|
Description string
|
||||||
|
Category string
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobChan = make(chan []Job)
|
||||||
|
|
||||||
|
func worker(jobChan <-chan []Job) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for jobs := range jobChan {
|
||||||
|
for _, job := range jobs {
|
||||||
|
progress.DescribeCurrent(job.Category, job.Description)
|
||||||
|
job.Function()
|
||||||
|
progress.IncrDone(job.Category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
//logger.I("number of CPU core: %d", numCPU)
|
||||||
|
// one core for main thread
|
||||||
|
for i := 0; i < numCPU; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go worker(jobChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue enqueues a job to the job queue
|
||||||
|
func Enqueue(jobs ...Job) {
|
||||||
|
for _, job := range jobs {
|
||||||
|
progress.IncrTotal(job.Category)
|
||||||
|
}
|
||||||
|
jobChan <- jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait will wait for all jobs to finish
|
||||||
|
func Wait() {
|
||||||
|
close(jobChan)
|
||||||
|
progress.Stop()
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNumCPU is for testing package without threading
|
||||||
|
func SetNumCPU(i int) {
|
||||||
|
numCPU = i
|
||||||
|
}
|
||||||
138
pkg/logger/logger.go
Normal file
138
pkg/logger/logger.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/op/go-logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log is global logger
|
||||||
|
var Log = logging.MustGetLogger("myLogger")
|
||||||
|
var logBackendLeveled logging.LeveledBackend
|
||||||
|
|
||||||
|
var Prefix = ""
|
||||||
|
|
||||||
|
// SetLogLevel sets log level for global logger (debug, info, notice, warning, error)
|
||||||
|
func SetLogLevel(level string) {
|
||||||
|
logBackendLevel := logging.INFO
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
logBackendLevel = logging.DEBUG
|
||||||
|
break
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
logBackendLevel = logging.INFO
|
||||||
|
break
|
||||||
|
|
||||||
|
case "notice":
|
||||||
|
logBackendLevel = logging.NOTICE
|
||||||
|
break
|
||||||
|
|
||||||
|
case "warning":
|
||||||
|
logBackendLevel = logging.WARNING
|
||||||
|
break
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
logBackendLevel = logging.ERROR
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
logBackendLeveled.SetLevel(logBackendLevel, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureLogger sets logger backend and level
|
||||||
|
func configureLogger() {
|
||||||
|
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||||
|
logBackendFormatter := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(
|
||||||
|
`%{color}%{time:2006-01-02 15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||||
|
))
|
||||||
|
logBackendLeveled = logging.AddModuleLevel(logBackendFormatter)
|
||||||
|
logBackendLeveled.SetLevel(logging.NOTICE, "")
|
||||||
|
logging.SetBackend(logBackendLeveled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
spew.Config.DisablePointerAddresses = true
|
||||||
|
spew.Config.DisableCapacities = true
|
||||||
|
spew.Config.DisableMethods = true
|
||||||
|
spew.Config.DisablePointerMethods = true
|
||||||
|
configureLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefix() string {
|
||||||
|
return Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// D is shorthand for Debugf
|
||||||
|
func D(format string, args ...interface{}) {
|
||||||
|
Log.Debugf(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I is shorthand for Infof
|
||||||
|
func I(format string, args ...interface{}) {
|
||||||
|
Log.Infof(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// N is shorthand for Noticef
|
||||||
|
func N(format string, args ...interface{}) {
|
||||||
|
Log.Noticef(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// W is shorthand for Warningf
|
||||||
|
func W(format string, args ...interface{}) {
|
||||||
|
Log.Warningf(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// E is shorthand for Errorf
|
||||||
|
func E(format string, args ...interface{}) {
|
||||||
|
Log.Errorf(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// P is shorthand for Panicf
|
||||||
|
func P(format string, args ...interface{}) {
|
||||||
|
Log.Panicf(prefix()+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eerr is shorthand for
|
||||||
|
// if err != nil {
|
||||||
|
// Log.Errorf(...)
|
||||||
|
// }
|
||||||
|
func Eerr(err error, format string, args ...interface{}) {
|
||||||
|
if err != nil {
|
||||||
|
args = append(args, err)
|
||||||
|
Log.Errorf(prefix()+format+" (Error: %s)", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eexit is shorthand for
|
||||||
|
// if err != nil {
|
||||||
|
// Log.Errorf(...)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
func Eexit(err error, format string, args ...interface{}) {
|
||||||
|
Eerr(err, format, args...)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit is shorthand for
|
||||||
|
// Log.Errorf(...)
|
||||||
|
// os.Exit(1)
|
||||||
|
func Exit(format string, args ...interface{}) {
|
||||||
|
E(format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perr is shorthand for
|
||||||
|
// if err != nil {
|
||||||
|
// Log.Panicf(...)
|
||||||
|
// }
|
||||||
|
func Perr(err error, format string, args ...interface{}) {
|
||||||
|
if err != nil {
|
||||||
|
args = append(args, err)
|
||||||
|
Log.Panicf(prefix()+format+" (Error: %s)", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
pkg/mark2web/assets.go
Normal file
53
pkg/mark2web/assets.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
cpy "github.com/otiai10/copy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessAssets copies the assets from input to output dir
|
||||||
|
func ProcessAssets() {
|
||||||
|
switch Config.Assets.Action {
|
||||||
|
case "copy":
|
||||||
|
from := Config.Assets.FromPath
|
||||||
|
to := Config.Assets.ToPath
|
||||||
|
if !strings.HasPrefix(from, "/") {
|
||||||
|
from = Config.Directories.Input + "/" + from
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(to, "/") {
|
||||||
|
to = Config.Directories.Output + "/" + to
|
||||||
|
}
|
||||||
|
logger.N("copying assets from '%s' to '%s'", from, to)
|
||||||
|
err := cpy.Copy(from, to)
|
||||||
|
|
||||||
|
logger.Perr(err, "could not copy assets from '%s' to '%s'", from, to)
|
||||||
|
|
||||||
|
if Config.Assets.Compress {
|
||||||
|
compressFilesInDir(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixAssetsPath replaces assets path based on current path
|
||||||
|
func (node *TreeNode) fixAssetsPath(str string) string {
|
||||||
|
if find := Config.Assets.FixTemplate.Find; find != "" {
|
||||||
|
logger.D("fixing assets paths for path '%s'", node.CurrentNavPath())
|
||||||
|
repl := Config.Assets.FixTemplate.Replace
|
||||||
|
toPath := Config.Assets.ToPath
|
||||||
|
|
||||||
|
bToRoot := node.BackToRootPath()
|
||||||
|
regex, err := regexp.Compile(find)
|
||||||
|
logger.Eexit(err, "could not compile regexp '%s' for assets path", find)
|
||||||
|
|
||||||
|
repl = bToRoot + toPath + "/" + repl
|
||||||
|
repl = path.Clean(repl) + "/"
|
||||||
|
logger.D("new assets paths: %s", repl)
|
||||||
|
return regex.ReplaceAllString(str, repl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
41
pkg/mark2web/brotli.go
Normal file
41
pkg/mark2web/brotli.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// +build cgo
|
||||||
|
|
||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"github.com/itchio/go-brotli/enc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var brotliSupported = true
|
||||||
|
|
||||||
|
func handleBrotliCompression(filename string, content []byte) {
|
||||||
|
brFilename := filename + ".br"
|
||||||
|
|
||||||
|
logger.I("writing to compressed output file: %s", brFilename)
|
||||||
|
|
||||||
|
f, err := os.Create(brFilename)
|
||||||
|
logger.Eexit(err, "could not create file '%s'", brFilename)
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
bw := enc.NewBrotliWriter(f, nil)
|
||||||
|
defer bw.Close()
|
||||||
|
|
||||||
|
if content != nil {
|
||||||
|
// content given
|
||||||
|
_, err = bw.Write(content)
|
||||||
|
logger.Eexit(err, "could not write brotli content for '%s'", filename)
|
||||||
|
} else {
|
||||||
|
// read file
|
||||||
|
r, err := os.Open(filename)
|
||||||
|
logger.Eexit(err, "could not open file '%s'", filename)
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(bw, r)
|
||||||
|
logger.Eexit(err, "could not write brotli file for '%s'", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
196
pkg/mark2web/collection.go
Normal file
196
pkg/mark2web/collection.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/webrequest"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type colCacheEntry struct {
|
||||||
|
data interface{}
|
||||||
|
hit int
|
||||||
|
navnames []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var colCache = make(map[string]*colCacheEntry)
|
||||||
|
|
||||||
|
func (node *TreeNode) handleCollections() {
|
||||||
|
collections := append(node.Config.Collections, node.Config.This.Collections...)
|
||||||
|
for _, colConfig := range collections {
|
||||||
|
if colConfig.Name == nil || *colConfig.Name == "" {
|
||||||
|
logger.Exit("missing Name in collection config in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
if (colConfig.URL == nil || *colConfig.URL == "") &&
|
||||||
|
(colConfig.Directory == nil) {
|
||||||
|
logger.Exit("missing URL and Directory in collection config in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.ColMap == nil {
|
||||||
|
node.ColMap = make(helper.MapString)
|
||||||
|
}
|
||||||
|
ctx := NewContext()
|
||||||
|
ctx["This"] = node.Config.This
|
||||||
|
ctx["Data"] = node.Config.Data
|
||||||
|
|
||||||
|
var colData interface{}
|
||||||
|
|
||||||
|
errSrcText := ""
|
||||||
|
cacheKey := ""
|
||||||
|
|
||||||
|
if colConfig.URL != nil {
|
||||||
|
url, err := pongo2.RenderTemplateString(*colConfig.URL, ctx)
|
||||||
|
logger.Eexit(err, "invalid template string for Collection Element.URL in '%s'", node.InputPath)
|
||||||
|
|
||||||
|
errSrcText = "URL " + url
|
||||||
|
cacheKey = url
|
||||||
|
|
||||||
|
if cacheEntry, ok := colCache[url]; ok {
|
||||||
|
colData = cacheEntry.data
|
||||||
|
cacheEntry.hit++
|
||||||
|
} else {
|
||||||
|
logger.N("reading collection from: %s", errSrcText)
|
||||||
|
colData = webrequest.GetJSON(url)
|
||||||
|
colCache[url] = &colCacheEntry{
|
||||||
|
data: colData,
|
||||||
|
navnames: make([]string, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
path := node.ResolveInputPath(colConfig.Directory.Path)
|
||||||
|
errSrcText = "DIR " + path
|
||||||
|
|
||||||
|
logger.N("reading collection from: %s", errSrcText)
|
||||||
|
d, err := ioutil.ReadDir(path)
|
||||||
|
logger.Eexit(err, "could not read directory '%s'", path)
|
||||||
|
|
||||||
|
mStr := "."
|
||||||
|
if colConfig.Directory.MatchFilename != "" {
|
||||||
|
mStr = colConfig.Directory.MatchFilename
|
||||||
|
}
|
||||||
|
matcher, err := regexp.Compile(mStr)
|
||||||
|
logger.Eexit(err, "could not compile regex for MatchFilename '%s' in '%s'", mStr, path)
|
||||||
|
|
||||||
|
if colConfig.Directory.ReverseOrder {
|
||||||
|
for i := len(d)/2 - 1; i >= 0; i-- {
|
||||||
|
opp := len(d) - 1 - i
|
||||||
|
d[i], d[opp] = d[opp], d[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fcolData := make([]pongo2.Context, 0)
|
||||||
|
for _, fh := range d {
|
||||||
|
if !fh.IsDir() && matcher.MatchString(fh.Name()) {
|
||||||
|
inFile := path + "/" + fh.Name()
|
||||||
|
md, err := ioutil.ReadFile(inFile)
|
||||||
|
logger.Eexit(err, "could not read file '%s'", inFile)
|
||||||
|
|
||||||
|
_, ctx := node.processMarkdownWithHeader(md, inFile)
|
||||||
|
(*ctx)["FilenameMatch"] = helper.GetRegexpParams(matcher, fh.Name())
|
||||||
|
fcolData = append(fcolData, *ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colData = fcolData
|
||||||
|
}
|
||||||
|
|
||||||
|
node.ColMap[*colConfig.Name] = colData
|
||||||
|
|
||||||
|
if navT := colConfig.NavTemplate; navT != nil {
|
||||||
|
var entries []interface{}
|
||||||
|
var ok bool
|
||||||
|
if navT.EntriesAttribute != "" {
|
||||||
|
var colDataMap map[string]interface{}
|
||||||
|
if colDataMap, ok = colData.(map[string]interface{}); ok {
|
||||||
|
entries, ok = colDataMap[navT.EntriesAttribute].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
logger.D(spew.Sdump(colDataMap))
|
||||||
|
logger.Exit("invalid json data in [%s] from '%s' for entries", navT.EntriesAttribute, errSrcText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries, ok = colData.([]interface{})
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
logger.D(spew.Sdump(colData))
|
||||||
|
logger.Exit("invalid json data from '%s', need array of objects for entries or object with configured NavTemplate.EntriesAttribute", errSrcText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build navigation with detail sites
|
||||||
|
for idx, colEl := range entries {
|
||||||
|
ctxE := make(pongo2.Context)
|
||||||
|
err := helper.Merge(&ctxE, ctx)
|
||||||
|
logger.Eexit(err, "could not merge context in '%s'", node.InputPath)
|
||||||
|
|
||||||
|
var jsonCtx map[string]interface{}
|
||||||
|
if jsonCtx, ok = colEl.(map[string]interface{}); !ok {
|
||||||
|
logger.D(spew.Sdump(colEl))
|
||||||
|
logger.Exit("no json object for entry index %d from '%s'", idx, errSrcText)
|
||||||
|
}
|
||||||
|
err = helper.Merge(&ctxE, pongo2.Context(jsonCtx))
|
||||||
|
logger.Eexit(err, "could not merge context in '%s'", node.InputPath)
|
||||||
|
|
||||||
|
tpl := ""
|
||||||
|
if navT.Template != "" {
|
||||||
|
tpl, err = pongo2.RenderTemplateString(navT.Template, ctxE)
|
||||||
|
logger.Eexit(err, "invalid template string for NavTemplate.Template in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
if tpl == "" {
|
||||||
|
tpl = *node.Config.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
dataKey := ""
|
||||||
|
if navT.DataKey != "" {
|
||||||
|
dataKey, err = pongo2.RenderTemplateString(navT.DataKey, ctxE)
|
||||||
|
logger.Eexit(err, "invalid template string for NavTemplate.DataKey in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
goTo, err := pongo2.RenderTemplateString(navT.GoTo, ctxE)
|
||||||
|
logger.Eexit(err, "invalid template string for NavTemplate.GoTo in '%s'", node.InputPath)
|
||||||
|
|
||||||
|
goTo = strings.Trim(goTo, "/")
|
||||||
|
goTo = path.Clean(goTo)
|
||||||
|
|
||||||
|
if strings.Contains(goTo, "..") {
|
||||||
|
logger.Exit("going back via .. in NavTemplate.GoTo forbidden in collection config in '%s': %s", node.InputPath, goTo)
|
||||||
|
}
|
||||||
|
if goTo == "." {
|
||||||
|
logger.Exit("invalid config '.' for NavTemplate.GoTo in collection config in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
if goTo == "" {
|
||||||
|
logger.Exit("missing NavTemplate.GoTo in collection config in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
navname := ""
|
||||||
|
if navT.Navname != "" {
|
||||||
|
navname, err = pongo2.RenderTemplateString(navT.Navname, ctxE)
|
||||||
|
logger.Eexit(err, "invalid template string for NavTemplate.Navname in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
body := ""
|
||||||
|
if navT.Body != "" {
|
||||||
|
body, err = pongo2.RenderTemplateString(navT.Body, ctxE)
|
||||||
|
logger.Eexit(err, "invalid template string for NavTemplate.Body in '%s'", node.InputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(colCache[cacheKey].navnames); colCache[cacheKey].hit > 1 &&
|
||||||
|
l > 0 &&
|
||||||
|
navname == colCache[cacheKey].navnames[l-1] {
|
||||||
|
// navname before used same url, so recursion loop
|
||||||
|
logger.Exit("collection request loop detected for in '%s' for : %s", node.InputPath, errSrcText)
|
||||||
|
}
|
||||||
|
|
||||||
|
colCache[cacheKey].navnames = append(colCache[cacheKey].navnames, navname)
|
||||||
|
|
||||||
|
node.addSubNode(tpl, goTo, navname, colEl, dataKey, body, navT.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
78
pkg/mark2web/compress.go
Normal file
78
pkg/mark2web/compress.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/jobm"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleCompression(filename string, content []byte) {
|
||||||
|
jobm.Enqueue(jobm.Job{
|
||||||
|
Function: func() {
|
||||||
|
if _, ok := Config.Compress.Extensions[path.Ext(filename)]; ok {
|
||||||
|
|
||||||
|
if Config.Compress.Brotli {
|
||||||
|
handleBrotliCompression(filename, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Config.Compress.GZIP {
|
||||||
|
gzFilename := filename + ".gz"
|
||||||
|
|
||||||
|
logger.I("writing to compressed output file: %s", gzFilename)
|
||||||
|
|
||||||
|
f, err := os.Create(gzFilename)
|
||||||
|
logger.Eexit(err, "could not create file '%s'", gzFilename)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
zw, err := gzip.NewWriterLevel(f, gzip.BestCompression)
|
||||||
|
logger.Eexit(err, "could not initialize gzip writer for '%s'", filename)
|
||||||
|
defer zw.Close()
|
||||||
|
|
||||||
|
if content != nil {
|
||||||
|
// content given
|
||||||
|
_, err = zw.Write(content)
|
||||||
|
logger.Eexit(err, "could not write gziped content for '%s'", filename)
|
||||||
|
} else {
|
||||||
|
// read file
|
||||||
|
r, err := os.Open(filename)
|
||||||
|
logger.Eexit(err, "could not open file '%s'", filename)
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(zw, r)
|
||||||
|
logger.Eexit(err, "could not gzip file '%s'", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Description: filename,
|
||||||
|
Category: "compress",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressFilesInDir(dir string) {
|
||||||
|
logger.N("compressing configured files in: %s", dir)
|
||||||
|
|
||||||
|
var _processDir func(string)
|
||||||
|
_processDir = func(d string) {
|
||||||
|
entries, err := ioutil.ReadDir(d)
|
||||||
|
logger.Eexit(err, "could not read dir '%s'", d)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
_processDir(d + "/" + entry.Name())
|
||||||
|
} else {
|
||||||
|
handleCompression(d+"/"+entry.Name(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_processDir(dir)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package mark2web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -13,6 +13,7 @@ type GlobalConfig struct {
|
|||||||
} `yaml:"Webserver"`
|
} `yaml:"Webserver"`
|
||||||
|
|
||||||
Assets struct {
|
Assets struct {
|
||||||
|
Compress bool `yaml:"Compress"`
|
||||||
FromPath string `yaml:"FromPath"`
|
FromPath string `yaml:"FromPath"`
|
||||||
ToPath string `yaml:"ToPath"`
|
ToPath string `yaml:"ToPath"`
|
||||||
Action string `yaml:"Action"`
|
Action string `yaml:"Action"`
|
||||||
@@ -26,20 +27,28 @@ type GlobalConfig struct {
|
|||||||
Action string `yaml:"Action"`
|
Action string `yaml:"Action"`
|
||||||
} `yaml:"OtherFiles"`
|
} `yaml:"OtherFiles"`
|
||||||
|
|
||||||
|
Compress struct {
|
||||||
|
Brotli bool `yaml:"Brotli"`
|
||||||
|
GZIP bool `yaml:"GZIP"`
|
||||||
|
Extensions map[string]string `yaml:"Extensions"`
|
||||||
|
} `yaml:"Compress"`
|
||||||
|
|
||||||
Directories struct {
|
Directories struct {
|
||||||
Input string
|
Input string
|
||||||
Output string
|
Output string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config is global config
|
||||||
var Config = new(GlobalConfig)
|
var Config = new(GlobalConfig)
|
||||||
|
|
||||||
func ReadGlobalConfig(filename string) error {
|
// ReadFromFile reads yaml config from file
|
||||||
|
func (c *GlobalConfig) ReadFromFile(filename string) error {
|
||||||
data, err := ioutil.ReadFile(filename)
|
data, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = yaml.Unmarshal(data, Config)
|
err = yaml.Unmarshal(data, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1,60 +1,19 @@
|
|||||||
package config
|
package mark2web
|
||||||
|
|
||||||
import (
|
import "gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/imdario/mergo"
|
// CollectionDirectoryConfig specifies how to handle a directory of markdown files as a collection
|
||||||
)
|
type CollectionDirectoryConfig struct {
|
||||||
|
Path string `yaml:"Path"`
|
||||||
// MapString is a map[string]interface{} which always unmarsahls yaml to map[string]interface{}
|
MatchFilename string `yaml:"MatchFilename"`
|
||||||
type MapString map[string]interface{}
|
ReverseOrder bool `yaml:"ReverseOrder"`
|
||||||
|
|
||||||
// UnmarshalYAML handles all maps as map[string]interface{} for later JSON
|
|
||||||
// see https://github.com/elastic/beats/blob/6435194af9f42cbf778ca0a1a92276caf41a0da8/libbeat/common/mapstr.go
|
|
||||||
func (ms *MapString) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var result map[interface{}]interface{}
|
|
||||||
err := unmarshal(&result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*ms = cleanUpInterfaceMap(result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpInterfaceArray(in []interface{}) []interface{} {
|
|
||||||
result := make([]interface{}, len(in))
|
|
||||||
for i, v := range in {
|
|
||||||
result[i] = cleanUpMapValue(v)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpInterfaceMap(in map[interface{}]interface{}) MapString {
|
|
||||||
result := make(MapString)
|
|
||||||
for k, v := range in {
|
|
||||||
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpMapValue(v interface{}) interface{} {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
return cleanUpInterfaceArray(v)
|
|
||||||
case map[interface{}]interface{}:
|
|
||||||
return cleanUpInterfaceMap(v)
|
|
||||||
case string, bool, int, int8, int16, int32, int64, float32, float64:
|
|
||||||
return v
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectionConfig describes a collection
|
// CollectionConfig describes a collection
|
||||||
type CollectionConfig struct {
|
type CollectionConfig struct {
|
||||||
Name *string `yaml:"Name"`
|
Name *string `yaml:"Name"`
|
||||||
URL *string `yaml:"URL"`
|
URL *string `yaml:"URL"`
|
||||||
|
Directory *CollectionDirectoryConfig `yaml:"Directory"`
|
||||||
NavTemplate *struct {
|
NavTemplate *struct {
|
||||||
EntriesAttribute string `yaml:"EntriesAttribute"`
|
EntriesAttribute string `yaml:"EntriesAttribute"`
|
||||||
GoTo string `yaml:"GoTo"`
|
GoTo string `yaml:"GoTo"`
|
||||||
@@ -72,7 +31,7 @@ type ThisPathConfig struct {
|
|||||||
Navname *string `yaml:"Navname"`
|
Navname *string `yaml:"Navname"`
|
||||||
GoTo *string `yaml:"GoTo"`
|
GoTo *string `yaml:"GoTo"`
|
||||||
Collections []*CollectionConfig `yaml:"Collections"`
|
Collections []*CollectionConfig `yaml:"Collections"`
|
||||||
Data MapString `yaml:"Data"`
|
Data helper.MapString `yaml:"Data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexConfig describes index input and output file
|
// IndexConfig describes index input and output file
|
||||||
@@ -116,6 +75,7 @@ type ImagingConfig struct {
|
|||||||
Anchor string `yaml:"Anchor"`
|
Anchor string `yaml:"Anchor"`
|
||||||
Quality int `yaml:"Quality"`
|
Quality int `yaml:"Quality"`
|
||||||
|
|
||||||
|
TargetDir string `yaml:"-"`
|
||||||
Filename string `yaml:"-"`
|
Filename string `yaml:"-"`
|
||||||
Format string `yaml:"-"`
|
Format string `yaml:"-"`
|
||||||
}
|
}
|
||||||
@@ -131,41 +91,8 @@ type PathConfig struct {
|
|||||||
Markdown *MarkdownConfig `yaml:"Markdown"`
|
Markdown *MarkdownConfig `yaml:"Markdown"`
|
||||||
Imaging *ImagingConfig `yaml:"Imaging"`
|
Imaging *ImagingConfig `yaml:"Imaging"`
|
||||||
|
|
||||||
Data MapString `yaml:"Data"`
|
Data helper.MapString `yaml:"Data"`
|
||||||
}
|
|
||||||
|
// Collections here are recursive if saved as nav, so request should be filtered
|
||||||
// PathConfigTree is complete config tree of content dir
|
Collections []*CollectionConfig `yaml:"Collections"`
|
||||||
type PathConfigTree struct {
|
|
||||||
InputPath string
|
|
||||||
OutputPath string
|
|
||||||
Hidden bool // for collections which are not part of the navigation
|
|
||||||
|
|
||||||
ColMap MapString
|
|
||||||
|
|
||||||
InputFiles []string
|
|
||||||
OtherFiles []string
|
|
||||||
|
|
||||||
Config *PathConfig
|
|
||||||
Sub []*PathConfigTree
|
|
||||||
}
|
|
||||||
|
|
||||||
type ptrTransformer struct{}
|
|
||||||
|
|
||||||
func (t ptrTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
|
|
||||||
if typ.Kind() == reflect.Ptr {
|
|
||||||
return func(dst, src reflect.Value) error {
|
|
||||||
if dst.CanSet() {
|
|
||||||
if dst.IsNil() {
|
|
||||||
dst.Set(src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges 2 objects or maps
|
|
||||||
func Merge(dst, src interface{}) error {
|
|
||||||
return mergo.Merge(dst, src, mergo.WithTransformers(ptrTransformer{}))
|
|
||||||
}
|
}
|
||||||
297
pkg/mark2web/content.go
Normal file
297
pkg/mark2web/content.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/progress"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
cpy "github.com/otiai10/copy"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadContentDir walks through content directory and builds the tree of configurations
|
||||||
|
func (node *TreeNode) ReadContentDir(inBase string, outBase string, dir string, conf *PathConfig) {
|
||||||
|
progress.IncrTotal("content dir")
|
||||||
|
progress.DescribeCurrent("content dir", "found "+inBase)
|
||||||
|
|
||||||
|
if node.root == nil {
|
||||||
|
// first node is root
|
||||||
|
node.root = node
|
||||||
|
}
|
||||||
|
node.fillConfig(inBase, outBase, dir, conf)
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(node.InputPath)
|
||||||
|
logger.Eexit(err, "could not read dir '%s'", node.InputPath)
|
||||||
|
|
||||||
|
// first only files
|
||||||
|
for _, f := range files {
|
||||||
|
p := node.InputPath + "/" + f.Name()
|
||||||
|
if !f.IsDir() && f.Name() != "config.yml" {
|
||||||
|
switch path.Ext(f.Name()) {
|
||||||
|
case ".md":
|
||||||
|
logger.D(".MD %s", p)
|
||||||
|
if node.InputFiles == nil {
|
||||||
|
node.InputFiles = make([]string, 0)
|
||||||
|
}
|
||||||
|
node.InputFiles = append(node.InputFiles, f.Name())
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logger.D("FIL %s", p)
|
||||||
|
if node.OtherFiles == nil {
|
||||||
|
node.OtherFiles = make([]string, 0)
|
||||||
|
}
|
||||||
|
node.OtherFiles = append(node.OtherFiles, f.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only directorys, needed config before
|
||||||
|
for _, f := range files {
|
||||||
|
p := node.InputPath + "/" + f.Name()
|
||||||
|
if f.IsDir() {
|
||||||
|
logger.D("DIR %s", p)
|
||||||
|
newTree := new(TreeNode)
|
||||||
|
newTree.root = node.root
|
||||||
|
if node.Sub == nil {
|
||||||
|
node.Sub = make([]*TreeNode, 0)
|
||||||
|
}
|
||||||
|
node.Sub = append(node.Sub, newTree)
|
||||||
|
newTree.ReadContentDir(node.InputPath, node.OutputPath, f.Name(), node.Config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *TreeNode) processMarkdownWithHeader(md []byte, errorRef string) (*PathConfig, *pongo2.Context) {
|
||||||
|
|
||||||
|
newConfig := new(PathConfig)
|
||||||
|
|
||||||
|
headerRegex := regexp.MustCompile("(?s)^---(.*?)\\r?\\n\\r?---\\r?\\n\\r?")
|
||||||
|
yamlData := headerRegex.Find(md)
|
||||||
|
if string(yamlData) != "" {
|
||||||
|
// replace tabs
|
||||||
|
yamlData = bytes.Replace(yamlData, []byte("\t"), []byte(" "), -1)
|
||||||
|
|
||||||
|
logger.D("found yaml header in '%s', merging config", errorRef)
|
||||||
|
err := yaml.Unmarshal(yamlData, newConfig)
|
||||||
|
logger.Eexit(err, "could not parse YAML header from '%s'", errorRef)
|
||||||
|
|
||||||
|
logger.D("merging config with upper config")
|
||||||
|
oldThis := newConfig.This
|
||||||
|
helper.Merge(newConfig, node.Config)
|
||||||
|
newConfig.This = oldThis
|
||||||
|
|
||||||
|
logger.D(spew.Sdump(newConfig))
|
||||||
|
|
||||||
|
md = headerRegex.ReplaceAll(md, []byte(""))
|
||||||
|
} else {
|
||||||
|
helper.Merge(newConfig, node.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use --- for splitting document in markdown parts
|
||||||
|
regex := regexp.MustCompile("\\r?\\n\\r?---\\r?\\n\\r?")
|
||||||
|
inputParts := regex.Split(string(md), -1)
|
||||||
|
htmlParts := make([]*pongo2.Value, 0)
|
||||||
|
|
||||||
|
chromaRenderer := false
|
||||||
|
chromaStyle := "monokai"
|
||||||
|
if m := newConfig.Markdown; m != nil {
|
||||||
|
if m.ChromaRenderer != nil && *m.ChromaRenderer {
|
||||||
|
chromaRenderer = true
|
||||||
|
}
|
||||||
|
if m.ChromaStyle != nil && *m.ChromaStyle != "" {
|
||||||
|
chromaStyle = *m.ChromaStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, iPart := range inputParts {
|
||||||
|
htmlParts = append(htmlParts,
|
||||||
|
pongo2.AsSafeValue(
|
||||||
|
string(helper.RenderMarkdown([]byte(iPart), chromaRenderer, chromaStyle))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// build navigation
|
||||||
|
navMap := make(map[string]*NavElement)
|
||||||
|
navSlice := make([]*NavElement, 0)
|
||||||
|
navActive := make([]*NavElement, 0)
|
||||||
|
node.buildNavigation(&navMap, &navSlice, &navActive)
|
||||||
|
|
||||||
|
// read yaml header as data for template
|
||||||
|
ctx := NewContext()
|
||||||
|
ctx["This"] = newConfig.This
|
||||||
|
ctx["Meta"] = newConfig.Meta
|
||||||
|
ctx["Markdown"] = newConfig.Markdown
|
||||||
|
ctx["Data"] = newConfig.Data
|
||||||
|
ctx["ColMap"] = node.root.ColMap // root as NavMap and NavSlice, for sub go to NavElement.ColMap
|
||||||
|
ctx["NavMap"] = navMap
|
||||||
|
ctx["NavSlice"] = navSlice
|
||||||
|
ctx["NavActive"] = navActive
|
||||||
|
ctx["Body"] = pongo2.AsSafeValue(string(helper.RenderMarkdown(md, chromaRenderer, chromaStyle)))
|
||||||
|
ctx["BodyParts"] = htmlParts
|
||||||
|
ctx["CurrentPath"] = node.CurrentNavPath()
|
||||||
|
// set active nav element
|
||||||
|
if len(navActive) > 0 {
|
||||||
|
ctx["NavElement"] = navActive[len(navActive)-1]
|
||||||
|
} else {
|
||||||
|
// if no active path to content, we are in root dir
|
||||||
|
ctx["NavElement"] = &NavElement{
|
||||||
|
GoTo: node.BackToRootPath(),
|
||||||
|
Active: true,
|
||||||
|
ColMap: node.ColMap,
|
||||||
|
Data: node.Config.Data,
|
||||||
|
This: node.Config.This,
|
||||||
|
SubMap: &navMap,
|
||||||
|
SubSlice: &navSlice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfig, &ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessContent walks recursivly through the input paths and processes all files for output
|
||||||
|
func (node *TreeNode) ProcessContent() {
|
||||||
|
progress.DescribeCurrent("content dir", "processing "+node.InputPath)
|
||||||
|
|
||||||
|
helper.CreateDirectory(node.OutputPath)
|
||||||
|
|
||||||
|
if node.root != node {
|
||||||
|
// write htaccess for rewrites, root will be written in WriteWebserverConfig()
|
||||||
|
goTo := node.Config.This.GoTo
|
||||||
|
if goTo != nil && *goTo != "" {
|
||||||
|
goToFixed := *goTo
|
||||||
|
if strings.HasPrefix(goToFixed, "/") {
|
||||||
|
goToFixed = node.BackToRootPath() + goToFixed
|
||||||
|
}
|
||||||
|
goToFixed = path.Clean(goToFixed)
|
||||||
|
|
||||||
|
htaccessRedirect(node.OutputPath, goToFixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range node.InputFiles {
|
||||||
|
inFile := "InputString"
|
||||||
|
|
||||||
|
// ignore ???
|
||||||
|
ignoreFile := false
|
||||||
|
if file != "" {
|
||||||
|
inFile = node.InputPath + "/" + file
|
||||||
|
var ignoreRegex *string
|
||||||
|
if f := node.Config.Filename; f != nil {
|
||||||
|
ignoreRegex = f.Ignore
|
||||||
|
}
|
||||||
|
if ignoreRegex != nil && *ignoreRegex != "" {
|
||||||
|
regex, err := regexp.Compile(*ignoreRegex)
|
||||||
|
logger.Eexit(err, "could not compile filename.ignore regexp '%s' for file '%s'", *ignoreRegex, inFile)
|
||||||
|
ignoreFile = regex.MatchString(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignoreFile {
|
||||||
|
logger.I("ignoring file '%s', because of filename.ignore", inFile)
|
||||||
|
} else {
|
||||||
|
var input []byte
|
||||||
|
|
||||||
|
if file != "" {
|
||||||
|
logger.D("reading file: %s", inFile)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
input, err = ioutil.ReadFile(inFile)
|
||||||
|
logger.Eexit(err, "could not read '%s'", inFile)
|
||||||
|
|
||||||
|
logger.I("processing input file '%s'", inFile)
|
||||||
|
} else {
|
||||||
|
// use input string if available and input filename == ""
|
||||||
|
var inputString *string
|
||||||
|
if i := node.Config.Index; i != nil {
|
||||||
|
inputString = i.InputString
|
||||||
|
}
|
||||||
|
if inputString != nil {
|
||||||
|
logger.D("using input string instead of file")
|
||||||
|
input = []byte(*inputString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig, ctx := node.processMarkdownWithHeader(input, inFile)
|
||||||
|
|
||||||
|
// build output filename
|
||||||
|
outputFilename := file
|
||||||
|
|
||||||
|
var stripRegex *string
|
||||||
|
var outputExt *string
|
||||||
|
if f := newConfig.Filename; f != nil {
|
||||||
|
stripRegex = f.Strip
|
||||||
|
outputExt = f.OutputExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexInputFile *string
|
||||||
|
var indexOutputFile *string
|
||||||
|
if i := newConfig.Index; i != nil {
|
||||||
|
indexInputFile = i.InputFile
|
||||||
|
indexOutputFile = i.OutputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if indexInputFile != nil &&
|
||||||
|
*indexInputFile == file &&
|
||||||
|
indexOutputFile != nil &&
|
||||||
|
*indexOutputFile != "" {
|
||||||
|
outputFilename = *indexOutputFile
|
||||||
|
} else {
|
||||||
|
if stripRegex != nil && *stripRegex != "" {
|
||||||
|
regex, err := regexp.Compile(*stripRegex)
|
||||||
|
logger.Eexit(err, "could not compile filename.strip regexp '%s' for file '%s'", *stripRegex, inFile)
|
||||||
|
|
||||||
|
outputFilename = regex.ReplaceAllString(outputFilename, "$1")
|
||||||
|
}
|
||||||
|
if outputExt != nil && *outputExt != "" {
|
||||||
|
outputFilename += "." + *outputExt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := node.OutputPath + "/" + outputFilename
|
||||||
|
logger.D("using '%s' as output file", outFile)
|
||||||
|
logger.D("rendering template '%s' for '%s'", *newConfig.Template, outFile)
|
||||||
|
templateFilename := *newConfig.Template
|
||||||
|
result, err := renderTemplate(*newConfig.Template, node, newConfig, ctx)
|
||||||
|
logger.Eexit(err, "could not execute template '%s' for input file '%s': %s", templateFilename, inFile)
|
||||||
|
|
||||||
|
result = node.fixAssetsPath(result)
|
||||||
|
|
||||||
|
logger.N("writing to output file: %s", outFile)
|
||||||
|
err = ioutil.WriteFile(outFile, []byte(result), 0644)
|
||||||
|
logger.Eexit(err, "could not write to output file '%s'", outFile)
|
||||||
|
|
||||||
|
handleCompression(outFile, []byte(result))
|
||||||
|
|
||||||
|
//fmt.Println(string(html))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process other files, copy...
|
||||||
|
for _, file := range node.OtherFiles {
|
||||||
|
switch Config.OtherFiles.Action {
|
||||||
|
case "copy":
|
||||||
|
from := node.InputPath + "/" + file
|
||||||
|
to := node.OutputPath + "/" + file
|
||||||
|
logger.N("copying file from '%s' to '%s'", from, to)
|
||||||
|
err := cpy.Copy(from, to)
|
||||||
|
logger.Eexit(err, "could not copy file from '%s' to '%s': %s", from, to)
|
||||||
|
|
||||||
|
handleCompression(to, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.IncrDone("content dir")
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
// sub can dynamically increase, so no for range
|
||||||
|
for i < len(node.Sub) {
|
||||||
|
node.Sub[i].ProcessContent()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
169
pkg/mark2web/context.go
Normal file
169
pkg/mark2web/context.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/progress"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/extemporalgenome/slug"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurrentContext is current pongo2 template context
|
||||||
|
var CurrentContext *pongo2.Context
|
||||||
|
|
||||||
|
// CurrentTreeNode is current node we are on while processing template
|
||||||
|
var CurrentTreeNode *TreeNode
|
||||||
|
|
||||||
|
// NewContext returns prefilled context with some functions and variables
|
||||||
|
func NewContext() pongo2.Context {
|
||||||
|
ctx := pongo2.Context{
|
||||||
|
"fnRequest": RequestFn,
|
||||||
|
"fnRender": RenderFn,
|
||||||
|
|
||||||
|
"AssetsPath": Config.Assets.ToPath,
|
||||||
|
"Timestamp": time.Now().Unix,
|
||||||
|
}
|
||||||
|
CurrentContext = &ctx
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *TreeNode) fillConfig(inBase, outBase, subDir string, conf *PathConfig) {
|
||||||
|
inPath := inBase
|
||||||
|
if subDir != "" {
|
||||||
|
inPath += "/" + subDir
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.I("reading input directory: %s", inPath)
|
||||||
|
|
||||||
|
node.InputPath = inPath
|
||||||
|
|
||||||
|
// read config
|
||||||
|
newConfig := new(PathConfig)
|
||||||
|
logger.D("looking for config.yml ...")
|
||||||
|
configFile := inPath + "/config.yml"
|
||||||
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
|
logger.D("no config.yml found in this directory, using upper configs")
|
||||||
|
helper.Merge(newConfig, conf)
|
||||||
|
// remove this
|
||||||
|
newConfig.This = ThisPathConfig{}
|
||||||
|
} else {
|
||||||
|
logger.D("reading config...")
|
||||||
|
data, err := ioutil.ReadFile(configFile)
|
||||||
|
logger.Eexit(err, "could not read file '%s'", configFile)
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(data, newConfig)
|
||||||
|
logger.Eexit(err, "could not parse YAML file '%s'", configFile)
|
||||||
|
|
||||||
|
logger.D("merging config with upper config")
|
||||||
|
oldThis := newConfig.This
|
||||||
|
helper.Merge(newConfig, conf)
|
||||||
|
newConfig.This = oldThis
|
||||||
|
|
||||||
|
logger.D(spew.Sdump(newConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Config = newConfig
|
||||||
|
|
||||||
|
// calc outDir
|
||||||
|
stripedDir := subDir
|
||||||
|
var regexStr *string
|
||||||
|
if newConfig.Path != nil {
|
||||||
|
regexStr = newConfig.Path.Strip
|
||||||
|
}
|
||||||
|
if regexStr != nil && *regexStr != "" {
|
||||||
|
if regex, err := regexp.Compile(*regexStr); err != nil {
|
||||||
|
logger.Eexit(err, "error compiling path.strip regex '%s' from '%s'", *regexStr, inBase+"/"+subDir)
|
||||||
|
} else {
|
||||||
|
stripedDir = regex.ReplaceAllString(stripedDir, "$1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Config.This.Navname == nil {
|
||||||
|
navname := strings.Replace(stripedDir, "_", " ", -1)
|
||||||
|
node.Config.This.Navname = &navname
|
||||||
|
}
|
||||||
|
|
||||||
|
stripedDir = slug.Slug(stripedDir)
|
||||||
|
outPath := outBase + "/" + stripedDir
|
||||||
|
outPath = path.Clean(outPath)
|
||||||
|
|
||||||
|
logger.I("calculated output directory: %s", outPath)
|
||||||
|
node.OutputPath = outPath
|
||||||
|
|
||||||
|
// handle collections
|
||||||
|
node.handleCollections()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *TreeNode) addSubNode(tplFilename, subDir string, navname string, ctx interface{}, dataMapKey string, body string, hideInNav bool) {
|
||||||
|
progress.IncrTotal("content dir")
|
||||||
|
progress.DescribeCurrent("content dir", "subdir "+node.InputPath+"/"+subDir)
|
||||||
|
|
||||||
|
newNode := new(TreeNode)
|
||||||
|
newNode.root = node.root
|
||||||
|
|
||||||
|
newPathConfig := new(PathConfig)
|
||||||
|
if navname != "" {
|
||||||
|
newPathConfig.This = ThisPathConfig{
|
||||||
|
Navname: &navname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dataMapKey != "" {
|
||||||
|
if newPathConfig.Data == nil {
|
||||||
|
newPathConfig.Data = make(helper.MapString)
|
||||||
|
}
|
||||||
|
// as submap in Data
|
||||||
|
newPathConfig.Data[dataMapKey] = ctx
|
||||||
|
} else if m, ok := ctx.(map[string]interface{}); ok {
|
||||||
|
// direct set data
|
||||||
|
newPathConfig.Data = m
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedConfig := new(PathConfig)
|
||||||
|
err := helper.Merge(mergedConfig, node.Config)
|
||||||
|
logger.Eexit(err, "merge of path config failed")
|
||||||
|
|
||||||
|
// dont merge Data[DataKey]
|
||||||
|
if dataMapKey != "" {
|
||||||
|
mergedConfig.Data[dataMapKey] = nil
|
||||||
|
} else {
|
||||||
|
mergedConfig.Data = make(helper.MapString)
|
||||||
|
}
|
||||||
|
err = helper.Merge(mergedConfig, newPathConfig)
|
||||||
|
logger.Eexit(err, "merge of path config failed")
|
||||||
|
|
||||||
|
newNode.fillConfig(
|
||||||
|
node.InputPath,
|
||||||
|
node.OutputPath,
|
||||||
|
subDir,
|
||||||
|
mergedConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
// fake via normal file behavior
|
||||||
|
newNode.Config.Template = &tplFilename
|
||||||
|
newNode.InputFiles = []string{""} // empty file is special for use InputString
|
||||||
|
indexInFile := ""
|
||||||
|
indexOutFile := "index.html"
|
||||||
|
if idx := newNode.Config.Index; idx != nil {
|
||||||
|
if idx.OutputFile != nil && *idx.OutputFile != "" {
|
||||||
|
indexOutFile = *idx.OutputFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newNode.Config.Index = &IndexConfig{
|
||||||
|
InputFile: &indexInFile,
|
||||||
|
OutputFile: &indexOutFile,
|
||||||
|
InputString: &body,
|
||||||
|
}
|
||||||
|
newNode.Hidden = hideInNav
|
||||||
|
|
||||||
|
node.Sub = append(node.Sub, newNode)
|
||||||
|
}
|
||||||
31
pkg/mark2web/context_fn.go
Normal file
31
pkg/mark2web/context_fn.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/webrequest"
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestFn will make a web request and returns map[string]interface form pongo2
|
||||||
|
func RequestFn(url *pongo2.Value, args ...*pongo2.Value) *pongo2.Value {
|
||||||
|
u := url.String()
|
||||||
|
return pongo2.AsValue(webrequest.GetJSON(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFn renders a pongo2 template with additional context
|
||||||
|
func RenderFn(templateFilename, subDir, ctx *pongo2.Value, param ...*pongo2.Value) *pongo2.Value {
|
||||||
|
dataMapKey := ""
|
||||||
|
body := ""
|
||||||
|
|
||||||
|
for i, p := range param {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
dataMapKey = p.String()
|
||||||
|
case 1:
|
||||||
|
body = p.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentTreeNode.addSubNode(templateFilename.String(), subDir.String(), "", ctx.Interface(), dataMapKey, body, true)
|
||||||
|
|
||||||
|
return pongo2.AsValue(nil)
|
||||||
|
}
|
||||||
96
pkg/mark2web/htaccess.go
Normal file
96
pkg/mark2web/htaccess.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func htaccessRedirect(outDir, goTo string) {
|
||||||
|
switch Config.Webserver.Type {
|
||||||
|
case "apache":
|
||||||
|
htaccessFile := outDir + "/.htaccess"
|
||||||
|
logger.N("writing '%s' with redirect to: %s", htaccessFile, goTo)
|
||||||
|
err := ioutil.WriteFile(htaccessFile, []byte(`RewriteEngine on
|
||||||
|
RewriteRule ^$ %{REQUEST_URI}`+goTo+`/ [R,L]
|
||||||
|
`), 0644)
|
||||||
|
logger.Eexit(err, "could not write '%s'", htaccessFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteWebserverConfig build the config for pre compression and more
|
||||||
|
func (tree *TreeNode) WriteWebserverConfig() {
|
||||||
|
goTo := ""
|
||||||
|
if g := tree.Config.This.GoTo; g != nil && *g != "" {
|
||||||
|
goTo = strings.TrimPrefix(*g, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch Config.Webserver.Type {
|
||||||
|
case "apache":
|
||||||
|
configStr := `
|
||||||
|
RewriteEngine on
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
if goTo != "" {
|
||||||
|
configStr += `
|
||||||
|
RewriteRule ^$ %{REQUEST_URI}` + goTo + `/ [R,L]
|
||||||
|
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
configStr += `
|
||||||
|
AddCharset UTF-8 .html
|
||||||
|
AddCharset UTF-8 .json
|
||||||
|
AddCharset UTF-8 .js
|
||||||
|
AddCharset UTF-8 .css
|
||||||
|
|
||||||
|
RemoveLanguage .br
|
||||||
|
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
`
|
||||||
|
|
||||||
|
rewriteMacro := func(e, c, x, xx string) string {
|
||||||
|
return `
|
||||||
|
|
||||||
|
######` + e + `.` + x + `
|
||||||
|
|
||||||
|
RewriteCond "%{HTTP:Accept-encoding}" "` + xx + `"
|
||||||
|
RewriteCond "%{REQUEST_FILENAME}\.` + x + `" -s
|
||||||
|
RewriteRule "^(.*)` + e + `" "$1` + e + `\.` + x + `" [QSA]
|
||||||
|
|
||||||
|
RewriteRule "` + e + `\.` + x + `$" "-" [E=no-gzip:1,E=no-brotli]
|
||||||
|
|
||||||
|
<FilesMatch "` + e + `\.` + x + `$">
|
||||||
|
ForceType '` + c + `; charset=UTF-8'
|
||||||
|
Header append Content-Encoding ` + xx + `
|
||||||
|
Header append Vary Accept-Encoding
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
for ext, contentType := range Config.Compress.Extensions {
|
||||||
|
rExt := regexp.QuoteMeta(ext)
|
||||||
|
if brotliSupported && Config.Compress.Brotli {
|
||||||
|
configStr += rewriteMacro(rExt, contentType, "br", "br")
|
||||||
|
}
|
||||||
|
if Config.Compress.GZIP {
|
||||||
|
configStr += rewriteMacro(rExt, contentType, "gz", "gzip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configStr += `
|
||||||
|
</IfModule>
|
||||||
|
`
|
||||||
|
|
||||||
|
if configStr != "" {
|
||||||
|
htaccessFile := Config.Directories.Output + "/.htaccess"
|
||||||
|
logger.N("writing webserver config to: %s", htaccessFile)
|
||||||
|
err := ioutil.WriteFile(htaccessFile, []byte(configStr), 0644)
|
||||||
|
logger.Eexit(err, "could not write '%s'", htaccessFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package helper
|
package mark2web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitbase.de/apairon/mark2web/config"
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NavElement is one element with ist attributes and subs
|
// NavElement is one element with ist attributes and subs
|
||||||
@@ -14,19 +15,23 @@ type NavElement struct {
|
|||||||
GoTo string
|
GoTo string
|
||||||
Active bool
|
Active bool
|
||||||
|
|
||||||
ColMap config.MapString
|
ColMap helper.MapString
|
||||||
|
|
||||||
Data interface{}
|
Data interface{}
|
||||||
|
|
||||||
This config.ThisPathConfig
|
This ThisPathConfig
|
||||||
|
|
||||||
SubMap *map[string]*NavElement
|
SubMap *map[string]*NavElement
|
||||||
SubSlice *[]*NavElement
|
SubSlice *[]*NavElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildNavigation builds the navigation trees for use in templates
|
// buildNavigation builds the navigation trees for use in templates
|
||||||
func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string) {
|
func (node *TreeNode) buildNavigation(curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement) {
|
||||||
for _, el := range conf.Sub {
|
buildNavigationRecursive(node.root, curNavMap, curNavSlice, navActive, node.CurrentNavPath(), node.BackToRootPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNavigationRecursive(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string, backToRoot string) {
|
||||||
|
for _, el := range tree.Sub {
|
||||||
if el.Hidden {
|
if el.Hidden {
|
||||||
continue // ignore hidden nav points from collections
|
continue // ignore hidden nav points from collections
|
||||||
}
|
}
|
||||||
@@ -37,16 +42,15 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
|
|||||||
}
|
}
|
||||||
if ignNav != nil && *ignNav != "" {
|
if ignNav != nil && *ignNav != "" {
|
||||||
regex, err := regexp.Compile(*ignNav)
|
regex, err := regexp.Compile(*ignNav)
|
||||||
if err != nil {
|
logger.Eexit(err, "could not compile IngoreForNav regexp '%s' in '%s'", *ignNav, el.InputPath)
|
||||||
Log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err)
|
|
||||||
}
|
|
||||||
if regex.MatchString(path.Base(el.InputPath)) {
|
if regex.MatchString(path.Base(el.InputPath)) {
|
||||||
Log.Debugf("ignoring input directory '%s' in navigation", el.InputPath)
|
logger.D("ignoring input directory '%s' in navigation", el.InputPath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elPath := strings.TrimPrefix(el.OutputPath, config.Config.Directories.Output+"/")
|
elPath := strings.TrimPrefix(el.OutputPath, Config.Directories.Output+"/")
|
||||||
|
|
||||||
subMap := make(map[string]*NavElement)
|
subMap := make(map[string]*NavElement)
|
||||||
subSlice := make([]*NavElement, 0)
|
subSlice := make([]*NavElement, 0)
|
||||||
@@ -88,8 +92,7 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
|
|||||||
|
|
||||||
if activeNav != "" && activeNav != "/" {
|
if activeNav != "" && activeNav != "/" {
|
||||||
// calculate relative path
|
// calculate relative path
|
||||||
bToRoot := BackToRoot(activeNav)
|
navEl.GoTo = backToRoot + navEl.GoTo
|
||||||
navEl.GoTo = bToRoot + navEl.GoTo
|
|
||||||
navEl.GoTo = path.Clean(navEl.GoTo)
|
navEl.GoTo = path.Clean(navEl.GoTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +101,6 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
|
|||||||
*curNavSlice = append(*curNavSlice, &navEl)
|
*curNavSlice = append(*curNavSlice, &navEl)
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildNavigation(el, &subMap, &subSlice, navActive, activeNav)
|
buildNavigationRecursive(el, &subMap, &subSlice, navActive, activeNav, backToRoot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
pkg/mark2web/no_cgo.go
Normal file
14
pkg/mark2web/no_cgo.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// +build !cgo
|
||||||
|
|
||||||
|
package mark2web
|
||||||
|
|
||||||
|
import "gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
|
||||||
|
var brotliSupported = false
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
helper.Log.Warning("cgo is disabled, so brotli compression is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBrotliCompression(filename string, content []byte) {
|
||||||
|
}
|
||||||
60
pkg/mark2web/path.go
Normal file
60
pkg/mark2web/path.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveNavPath fixes nav target relative to current navigation path
|
||||||
|
func (node *TreeNode) ResolveNavPath(target string) string {
|
||||||
|
if strings.HasPrefix(target, "/") {
|
||||||
|
target = node.BackToRootPath() + target
|
||||||
|
}
|
||||||
|
target = path.Clean(target)
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveOutputPath fixes output directory relative to current navigation path
|
||||||
|
func (node *TreeNode) ResolveOutputPath(target string) string {
|
||||||
|
if strings.HasPrefix(target, "/") {
|
||||||
|
target = Config.Directories.Output + "/" + target
|
||||||
|
} else {
|
||||||
|
target = node.OutputPath + "/" + target
|
||||||
|
}
|
||||||
|
return path.Clean(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveInputPath fixes input directory relative to current navigation path
|
||||||
|
func (node *TreeNode) ResolveInputPath(target string) string {
|
||||||
|
if strings.HasPrefix(target, "/") {
|
||||||
|
target = Config.Directories.Input + "/" + target
|
||||||
|
} else {
|
||||||
|
target = node.InputPath + "/" + target
|
||||||
|
}
|
||||||
|
return path.Clean(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentNavPath is current navigation path for this node
|
||||||
|
func (node *TreeNode) CurrentNavPath() string {
|
||||||
|
curNavPath := strings.TrimPrefix(node.OutputPath, Config.Directories.Output)
|
||||||
|
curNavPath = strings.TrimPrefix(curNavPath, "/")
|
||||||
|
curNavPath = path.Clean(curNavPath)
|
||||||
|
if curNavPath == "." {
|
||||||
|
curNavPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return curNavPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackToRootPath builds ../../ string
|
||||||
|
func (node *TreeNode) BackToRootPath() string {
|
||||||
|
curNavPath := node.CurrentNavPath()
|
||||||
|
|
||||||
|
tmpPath := ""
|
||||||
|
if curNavPath != "" {
|
||||||
|
for i := strings.Count(curNavPath, "/") + 1; i > 0; i-- {
|
||||||
|
tmpPath += "../"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmpPath
|
||||||
|
}
|
||||||
33
pkg/mark2web/render.go
Normal file
33
pkg/mark2web/render.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/flosch/pongo2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var templateCache = make(map[string]*pongo2.Template)
|
||||||
|
var templateDir string
|
||||||
|
|
||||||
|
// SetTemplateDir sets base directory for searching template files
|
||||||
|
func SetTemplateDir(dir string) {
|
||||||
|
templateDir = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderTemplate renders a pongo2 template with context
|
||||||
|
func renderTemplate(filename string, node *TreeNode, pathConfig *PathConfig, ctx *pongo2.Context) (string, error) {
|
||||||
|
CurrentContext = ctx
|
||||||
|
CurrentTreeNode = node
|
||||||
|
templateFile := templateDir + "/" + filename
|
||||||
|
template := templateCache[templateFile]
|
||||||
|
if template == nil {
|
||||||
|
var err error
|
||||||
|
if template, err = pongo2.FromFile(templateFile); err != nil {
|
||||||
|
log.Panicf("could not parse template '%s': %s", templateFile, err)
|
||||||
|
} else {
|
||||||
|
templateCache[templateFile] = template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.Execute(*ctx)
|
||||||
|
}
|
||||||
18
pkg/mark2web/run.go
Normal file
18
pkg/mark2web/run.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import "gitbase.de/apairon/mark2web/pkg/jobm"
|
||||||
|
|
||||||
|
// Run will do a complete run of mark2web
|
||||||
|
func Run(inDir, outDir string, defaultPathConfig *PathConfig) {
|
||||||
|
SetTemplateDir(inDir + "/templates")
|
||||||
|
|
||||||
|
tree := new(TreeNode)
|
||||||
|
tree.ReadContentDir(inDir+"/content", outDir, "", defaultPathConfig)
|
||||||
|
tree.ProcessContent()
|
||||||
|
|
||||||
|
ProcessAssets()
|
||||||
|
|
||||||
|
tree.WriteWebserverConfig()
|
||||||
|
|
||||||
|
jobm.Wait()
|
||||||
|
}
|
||||||
21
pkg/mark2web/tree.go
Normal file
21
pkg/mark2web/tree.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package mark2web
|
||||||
|
|
||||||
|
import "gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
|
||||||
|
// TreeNode is complete config tree of content dir
|
||||||
|
type TreeNode struct {
|
||||||
|
InputPath string
|
||||||
|
OutputPath string
|
||||||
|
Hidden bool // for collections which are not part of the navigation
|
||||||
|
|
||||||
|
ColMap helper.MapString
|
||||||
|
|
||||||
|
InputFiles []string
|
||||||
|
OtherFiles []string
|
||||||
|
|
||||||
|
Config *PathConfig
|
||||||
|
Sub []*TreeNode
|
||||||
|
|
||||||
|
root *TreeNode // shows always to root of tree
|
||||||
|
parent *TreeNode
|
||||||
|
}
|
||||||
96
pkg/progress/bar.go
Normal file
96
pkg/progress/bar.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package progress
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/helper"
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
"github.com/gosuri/uiprogress"
|
||||||
|
"github.com/mattn/go-tty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bar struct {
|
||||||
|
Bar *uiprogress.Bar
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
var bars = make(map[string]*bar)
|
||||||
|
var initialized = false
|
||||||
|
var terminalWidth = 80
|
||||||
|
|
||||||
|
// OverallTotal is number of total jobs
|
||||||
|
var OverallTotal = 0
|
||||||
|
|
||||||
|
// OverallDone is number of done jobs
|
||||||
|
var OverallDone = 0
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
updateLoggerPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the bar drawing
|
||||||
|
func Start() {
|
||||||
|
if t, err := tty.Open(); err == nil && t != nil {
|
||||||
|
terminalWidth, _, _ = t.Size()
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
uiprogress.Start() // start rendering
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLoggerPrefix() {
|
||||||
|
logger.Prefix = fmt.Sprintf("%3d/%3d: ", OverallDone, OverallTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrTotal increases the total jobs for the bar
|
||||||
|
func IncrTotal(barname string) {
|
||||||
|
OverallTotal++
|
||||||
|
updateLoggerPrefix()
|
||||||
|
|
||||||
|
if initialized {
|
||||||
|
_bar := bars[barname]
|
||||||
|
if _bar == nil {
|
||||||
|
_bar = new(bar)
|
||||||
|
_bar.Bar = uiprogress.AddBar(1)
|
||||||
|
_bar.Bar.Width = 25
|
||||||
|
|
||||||
|
_bar.Bar.PrependFunc(func(b *uiprogress.Bar) string {
|
||||||
|
return fmt.Sprintf("%15s: %3d/%3d", helper.ShortenStringLeft(barname, 15), b.Current(), b.Total)
|
||||||
|
})
|
||||||
|
_bar.Bar.AppendFunc(func(b *uiprogress.Bar) string {
|
||||||
|
return fmt.Sprintf("%s", helper.ShortenStringLeft(_bar.Description, terminalWidth-80))
|
||||||
|
})
|
||||||
|
|
||||||
|
bars[barname] = _bar
|
||||||
|
} else {
|
||||||
|
_bar.Bar.Total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrDone increases to done jobs counter
|
||||||
|
func IncrDone(barname string) {
|
||||||
|
OverallDone++
|
||||||
|
updateLoggerPrefix()
|
||||||
|
|
||||||
|
if initialized {
|
||||||
|
bars[barname].Bar.Incr()
|
||||||
|
bars[barname].Description = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescribeCurrent describes the current job
|
||||||
|
func DescribeCurrent(barname, description string) {
|
||||||
|
if initialized {
|
||||||
|
bars[barname].Description = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the bar drawing
|
||||||
|
func Stop() {
|
||||||
|
if initialized {
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
uiprogress.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
109
pkg/webrequest/request.go
Normal file
109
pkg/webrequest/request.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package webrequest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/progress"
|
||||||
|
|
||||||
|
"gitbase.de/apairon/mark2web/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrImageEntry struct {
|
||||||
|
img image.Image
|
||||||
|
format string
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrJSONEntry struct {
|
||||||
|
data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrImageCache = make(map[string]*wrImageEntry)
|
||||||
|
var wrJSONCache = make(map[string]*wrJSONEntry)
|
||||||
|
|
||||||
|
// Get will fetch an url and returns reponse
|
||||||
|
func Get(url string, opts interface{}) (resp *http.Response, err error) {
|
||||||
|
logger.N("requesting url via GET %s", url)
|
||||||
|
|
||||||
|
progress.IncrTotal("web request")
|
||||||
|
progress.DescribeCurrent("web request", url)
|
||||||
|
resp, err = http.Get(url)
|
||||||
|
progress.IncrDone("web request")
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJSON will GET a json object/array from a given URL
|
||||||
|
func GetJSON(url string) interface{} {
|
||||||
|
cached := wrJSONCache[url]
|
||||||
|
if cached == nil {
|
||||||
|
resp, err := Get(url, nil)
|
||||||
|
logger.Eexit(err, "could not get url '%s'", url)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
logger.Eexit(err, "could not read body from url '%s'", url)
|
||||||
|
|
||||||
|
logger.D("output from url '%s':\n%s", url, string(body))
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
logger.Exit("bad status '%d - %s' from url '%s'", resp.StatusCode, resp.Status, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if strings.Contains(contentType, "json") {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.Exit("is not json '%s' from url '%s'", contentType, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
cached = new(wrJSONEntry)
|
||||||
|
|
||||||
|
jsonMap := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(body, &jsonMap)
|
||||||
|
if err == nil {
|
||||||
|
cached.data = jsonMap
|
||||||
|
} else {
|
||||||
|
jsonArrayMap := make([]map[string]interface{}, 0)
|
||||||
|
err = json.Unmarshal(body, &jsonArrayMap)
|
||||||
|
if err == nil {
|
||||||
|
cached.data = jsonArrayMap
|
||||||
|
} else {
|
||||||
|
logger.Exit("could not read json from '%s': invalid type", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrJSONCache[url] = cached
|
||||||
|
|
||||||
|
}
|
||||||
|
return cached.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImage gets an image from an url
|
||||||
|
func GetImage(url string) (image.Image, string, error) {
|
||||||
|
cached := wrImageCache[url]
|
||||||
|
if cached == nil {
|
||||||
|
resp, err := Get(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("could not get url '%s': %s", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, format, err := image.Decode(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("could read body from url '%s': %s", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cached = &wrImageEntry{
|
||||||
|
img: img,
|
||||||
|
format: format,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrImageCache[url] = cached
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.img, cached.format, nil
|
||||||
|
}
|
||||||
7
scripts/build.sh
Executable file
7
scripts/build.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
curdir=$(dirname $0)
|
||||||
|
distdir="$curdir/../dist"
|
||||||
|
|
||||||
|
mkdir -p "$distdir"
|
||||||
|
go build -v -ldflags "-X main.Version=`git describe --tags --long` -X main.GitHash=`git rev-parse HEAD` -X main.BuildTime=`date -u '+%Y-%m-%d_%I:%M:%S%p'`" -o "$distdir/mark2web-`cat $curdir/../build/VERSION`-${GOOS}-${GOARCH}${FILEEXT}" "$curdir/../cmd/mark2web/main.go"
|
||||||
BIN
test/in/img/test.jpg
Normal file
BIN
test/in/img/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
@@ -1,7 +1,8 @@
|
|||||||
GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog
|
GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog
|
||||||
?sort[date]=-1
|
?sort[date]=-1
|
||||||
&limit=101
|
&limit=101
|
||||||
&token=89ff216524093123bf7a0a10f7b273
|
&token=985cee34099f4d3b08f18fc22f6296
|
||||||
|
&filter[link][$exists]=0
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
1
vendor/github.com/gosuri/uilive
generated
vendored
Submodule
1
vendor/github.com/gosuri/uilive
generated
vendored
Submodule
Submodule vendor/github.com/gosuri/uilive added at ac356e6e42
1
vendor/github.com/gosuri/uiprogress
generated
vendored
Submodule
1
vendor/github.com/gosuri/uiprogress
generated
vendored
Submodule
Submodule vendor/github.com/gosuri/uiprogress added at d0567a9d84
1
vendor/github.com/itchio/go-brotli
generated
vendored
Submodule
1
vendor/github.com/itchio/go-brotli
generated
vendored
Submodule
Submodule vendor/github.com/itchio/go-brotli added at 29899a4470
1
vendor/github.com/jtolds/gls
generated
vendored
Submodule
1
vendor/github.com/jtolds/gls
generated
vendored
Submodule
Submodule vendor/github.com/jtolds/gls added at b4936e0604
1
vendor/github.com/mattn/go-tty
generated
vendored
Submodule
1
vendor/github.com/mattn/go-tty
generated
vendored
Submodule
Submodule vendor/github.com/mattn/go-tty added at 5518497423
1
vendor/github.com/smartystreets/assertions
generated
vendored
Submodule
1
vendor/github.com/smartystreets/assertions
generated
vendored
Submodule
Submodule vendor/github.com/smartystreets/assertions added at 980c5ac6f3
1
vendor/github.com/smartystreets/goconvey
generated
vendored
Submodule
1
vendor/github.com/smartystreets/goconvey
generated
vendored
Submodule
Submodule vendor/github.com/smartystreets/goconvey added at 200a235640
1
vendor/golang.org/x/sys
generated
vendored
Submodule
1
vendor/golang.org/x/sys
generated
vendored
Submodule
Submodule vendor/golang.org/x/sys added at f49334f85d
@@ -2,6 +2,7 @@ Webserver:
|
|||||||
Type: "apache" # generates .htaccess
|
Type: "apache" # generates .htaccess
|
||||||
|
|
||||||
Assets:
|
Assets:
|
||||||
|
Compress: True
|
||||||
FromPath: "project-files"
|
FromPath: "project-files"
|
||||||
ToPath: "project-files"
|
ToPath: "project-files"
|
||||||
Action: "copy" # symlink, copy or move
|
Action: "copy" # symlink, copy or move
|
||||||
@@ -12,3 +13,10 @@ Assets:
|
|||||||
OtherFiles:
|
OtherFiles:
|
||||||
Action: "copy"
|
Action: "copy"
|
||||||
|
|
||||||
|
Compress:
|
||||||
|
Brotli: True
|
||||||
|
GZIP: True
|
||||||
|
Extensions:
|
||||||
|
.html: text/html
|
||||||
|
.css: text/css
|
||||||
|
.js: text/javascript
|
||||||
@@ -10,23 +10,16 @@ Data:
|
|||||||
---
|
---
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Damit die korrekten Versionsinformationen dynamisch in das finale mark2web-Binary eingefügt wurde, ist eine manuelle Installation aus dem Git-Repository sinnvoll.
|
|
||||||
Da die benötigten Pakete über die Go "vendor"-Funktionalität eingebunden sind ist ein `git submodule --init --recursive` nötig, wie im folgenden Abschnitt zu sehen ist:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir -p $GOPATH/src/gitbase.de/apairon
|
go get -v gitbase.de/apairon/mark2web/cmd/mark2web
|
||||||
git clone https://gitbase.de/apairon/mark2web.git $GOPATH/src/gitbase.de/apairon/mark2web
|
|
||||||
|
|
||||||
cd $GOPATH/src/gitbase.de/apairon/mark2web
|
# setze Versioninformationen ins Binary
|
||||||
git submodule update --init --recursive
|
pkg=$GOPATH/src/gitbase.de/apairon/mark2web
|
||||||
|
go install -v -ldflags "-X main.Version=`cat $pkg/build/VERSION` -X main.GitHash=`git --git-dir $pkg/.git rev-parse HEAD` -X main.BuildTime=`date -u '+%Y-%m-%d_%I:%M:%S%p'`" gitbase.de/apairon/mark2web/cmd/mark2web
|
||||||
./build.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Eine Installation über `go install gitbase.de/apairon/mark2web` wird derzeit noch nicht unterstützt, da dabei die Informationen für `mark2web -version` nicht generiert werden.
|
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
Vorkompilierte Binaries finden Sie auf der [Releases-Seite auf gitbase.de](https://gitbase.de/apairon/mark2web/releases).
|
Vorkompilierte Binaries finden Sie auf der [Releases-Seite auf gitbase.de](https://gitbase.de/apairon/mark2web/releases).
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 790 KiB |
@@ -1,3 +0,0 @@
|
|||||||
This:
|
|
||||||
Data:
|
|
||||||
teaser: Wie werden die Inhalte und Templates organisiert?
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
---
|
|
||||||
Data:
|
|
||||||
background: /img/wire.jpg
|
|
||||||
|
|
||||||
slider:
|
|
||||||
- img: /img/wire.jpg
|
|
||||||
alt:
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Konfiguration
|
|
||||||
|
|
||||||
Die Konfigurationsdatein sind im YAML-Format gehalten (siehe: [Wikipedia](https://de.wikipedia.org/wiki/YAML)).
|
|
||||||
|
|
||||||
## globale Einstellungen
|
|
||||||
|
|
||||||
Die obersten Verzeichnis sich befindende Datei `config.yml` kann z.B. folgenden Inhalt haben:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Webserver:
|
|
||||||
Type: "apache"
|
|
||||||
|
|
||||||
Assets:
|
|
||||||
FromPath: "assets"
|
|
||||||
ToPath: "assets"
|
|
||||||
Action: "copy"
|
|
||||||
FixTemplate:
|
|
||||||
Find: "\\.\\./assets/"
|
|
||||||
Replace: ""
|
|
||||||
|
|
||||||
OtherFiles:
|
|
||||||
Action: "copy"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sektion `Webserver:`
|
|
||||||
|
|
||||||
#### `Type:`
|
|
||||||
|
|
||||||
Derzeit wird hier nur der Wert `apache` unterstützt. Ist dieser Wert gesetzt werden automatische .htaccess-Dateien mit mod_rewrite-Anweisungen generiert, die eine saubere Weiterleitung bei entsprechenden Konfig-Anweisungen im `content`-Verzeichnis ermöglichen.
|
|
||||||
|
|
||||||
### Sektion `Assets:`
|
|
||||||
|
|
||||||
`Assets:` steuert, wie mit Bild/JS/CSS Dateien umgegangen werden soll.
|
|
||||||
|
|
||||||
#### `FromPath:`
|
|
||||||
|
|
||||||
Lage des Asset-Verzeichnis unterhalb des `content`-Verzeichnis
|
|
||||||
|
|
||||||
#### `ToPath:`
|
|
||||||
|
|
||||||
Zielverzeichnis im Ausgabe-Verzeichnis der fertig generierten Website
|
|
||||||
|
|
||||||
#### `Action:`
|
|
||||||
|
|
||||||
Derzeit nur `copy`, also das Kopieren der Dateien und Unterordner ins Zielverzeichnis
|
|
||||||
|
|
||||||
#### `FixTemplate:`
|
|
||||||
|
|
||||||
Wenn hier `Find:` (regulärer Ausdruck) und `Replace:` (Ersetzung) angeben sind, werden die gefundenden Pfadangaben in der generierten HTML-Dateien durch den korrekten relativen Pfad zum Asset-Verzeichnis ersetzt.
|
|
||||||
|
|
||||||
### Sektion `OtherFiles:`
|
|
||||||
|
|
||||||
`OtherFiles:` definiert, wie mit anderen Dateien innerhalb des `content`-Verzeichnis umgegangen werden soll.
|
|
||||||
|
|
||||||
#### `Action:`
|
|
||||||
|
|
||||||
Derzeit nur `copy`, also das Kopieren der Dateien in das entsprechende Unterverzeichnis im Ausgabe-Verzeichnis
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Konfiguration im `content`-Verzeichnis
|
|
||||||
|
|
||||||
Im `content`-Verzeichnis, sowie in jedem Unterverzeichnis unterhalb von `content` kann sich eine `config.yml`-Datei befinden, wie aus folgendem Beispiel:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
This:
|
|
||||||
GoTo: "/de/service/impressum/"
|
|
||||||
Navname: "Impressumsverweis"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
oder
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
This:
|
|
||||||
Navname: "FAQ's"
|
|
||||||
Data:
|
|
||||||
slogan: "Wer nicht fragt, bekommt keine Antwort."
|
|
||||||
|
|
||||||
Template: "base.html"
|
|
||||||
|
|
||||||
Index:
|
|
||||||
InputFile: "README.md"
|
|
||||||
OutputFile: "index.html"
|
|
||||||
|
|
||||||
Meta:
|
|
||||||
Title: "Fragen und Antworten"
|
|
||||||
Description: "Dies ist die Fragen und Antworten Unterseite."
|
|
||||||
Keywords: "FAQ, Fragen, Antworten"
|
|
||||||
|
|
||||||
Data:
|
|
||||||
background: "bg.jpg"
|
|
||||||
slider:
|
|
||||||
- img: "assets/img/slider1.jpg"
|
|
||||||
alt: "Alternativtext 1"
|
|
||||||
- img: "assets/img/slider2.jpg"
|
|
||||||
alt: "Alternativtext 2"
|
|
||||||
- img: "assets/img/slider3.jpg"
|
|
||||||
alt: "Alternativtext 3"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### `This:` Sektion
|
|
||||||
|
|
||||||
Sämtlich Werte unterhalb dieser Sektion gelten nur für den Inhalt, bzw. Navigationspunkt in dessen Ordner sich die `config.yml` befindet. Die Werte werden nicht an Unterordner wertervererbt.
|
|
||||||
|
|
||||||
#### `GoTo:`
|
|
||||||
|
|
||||||
Falls der Navigationspunkt selbst keinen Inhalt darstellen soll, sondern nur weiterleiten soll, so wird hier das Weiterleitungsziel eingegeben.
|
|
||||||
Das Ziel ist der absolute (startend mit `/`) oder relative Pfad zum Zielnavigationspunkt.
|
|
||||||
Die Schreibweise des Pfades ist so zu verwenden, wie der Pfad nach Umschreibung und Säuberung des Pfades im Zielverzeichnis dargestellt wird.
|
|
||||||
Aus `de/mainnav/03_Fragen und Antworten` wird also z.B. `de/mainnav/fragen-und-antworten`.
|
|
||||||
|
|
||||||
#### `Navname:`
|
|
||||||
|
|
||||||
Dieser Wert überschreibt den aus dem Ordnernamen automatisch abgeleiteten Navigationspunkt-Namen. Dies ist zum Beispiel dann nützlich, wenn Sonderzeichen im Verzeichnisnamen nicht vorkommen sollen, aber im Namen des Navigationspunkts gebraucht werden.
|
|
||||||
|
|
||||||
#### `Data:`
|
|
||||||
|
|
||||||
Unterhalb von `Data:` können beliebige Datenstrukturen erfasst werden. Da diese Struktur unterhalb von `This:` angeordnet ist, werden auch die Daten nicht weiter an Unterordner vererbt.
|
|
||||||
Hier können z.B. Informationen zum Navigationspunkt abgelegt werden, die im Template Zusatzinformationen darstellen (z.B. ein Slogan zu einem Navigationspunkt).
|
|
||||||
|
|
||||||
### `Meta:` Sektion
|
|
||||||
|
|
||||||
Unter `Title:`, `Description:` und `Keywords:` werden die typischen Metaangaben abgelegt, die im
|
|
||||||
|
|
||||||
```html
|
|
||||||
<head>
|
|
||||||
...
|
|
||||||
</head>
|
|
||||||
```
|
|
||||||
|
|
||||||
übllicherweise Verwendung finden. Die entsprechenden Platzhalter stehen im Template zur Verfügung.
|
|
||||||
|
|
||||||
`Meta:` vererbt seine individuellen Informationen an die Unterordner weiter, sofern diese dort nicht selbst in einer `config.yml` oder im Kopf der Markdown-Datei definiert sind.
|
|
||||||
|
|
||||||
### `Data:` Sektion
|
|
||||||
|
|
||||||
`Data:` an dieser Stelle kann, wie auch `Data:` unterhalb von `This:`, beliebige Daten aufnehmen. Die Daten hier allerdings werden an Unterordner weitervererbt, sofern diese nicht dort oder in der Markdown-Datei selbst festegelegt überschrieben wurden.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
This:
|
|
||||||
Data:
|
|
||||||
teaser: Globale Konfiguration und individuelle Content-Einstellungen
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
---
|
|
||||||
Data:
|
|
||||||
background: /img/design.jpg
|
|
||||||
|
|
||||||
slider:
|
|
||||||
- img: /img/design.jpg
|
|
||||||
alt:
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Templates
|
|
||||||
|
|
||||||
Templates werden über das pongo2-Paket gerendert. Dieses nutzt die Template-Sprache **Django-Template**.
|
|
||||||
|
|
||||||
Sämtliche Template-Dateien sind im Ordner `templates` zu speichern.
|
|
||||||
Die Endung kann frei gewählt werden. Für diese Dokumentation und auch als Grundlage für Beispiele wurde `.html` gewählt, da somit auch das Syntax-Highlighting gegeben ist.
|
|
||||||
|
|
||||||
## grober Überblick
|
|
||||||
|
|
||||||
Nachfolgend ist ein Beispiel eines Templates:
|
|
||||||
|
|
||||||
```django
|
|
||||||
<html>
|
|
||||||
<meta>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
|
|
||||||
<title>{{ Meta.Title }}</title>
|
|
||||||
<meta name="description" content="{{ Meta.Description }}" />
|
|
||||||
<meta name="keywords" content="{{ Meta.Keywords }}" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="../assets/css/main.css">
|
|
||||||
</meta>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% block header %}
|
|
||||||
<header>
|
|
||||||
<div class="langSelect">
|
|
||||||
{% for nav in NavSlice %}
|
|
||||||
<a href="{{ nav.GoTo }}" {% if nav.Active %}class="active"{% endif %}>
|
|
||||||
<img src="../assets/img/{{ nav.Navname }}.png" alt="{{ nav.Navname }}" style="height: 20px;">
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div id="logoDiv"><img src="../assets/img/logo.png"></div>
|
|
||||||
<div id="mainNavDiv" class="nav">
|
|
||||||
<table border="0" style="width: 100%">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div><b>main Level 1 ...</b></div>
|
|
||||||
<ul>
|
|
||||||
{% for nav in NavActive.0.SubMap.main.SubSlice %}
|
|
||||||
<li {% if nav.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">
|
|
||||||
{{ nav.Navname }}
|
|
||||||
</a>
|
|
||||||
{% if nav.SubSlice %}
|
|
||||||
<ul>
|
|
||||||
{% for nav2 in nav.SubSlice %}
|
|
||||||
<li {% if nav2.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav2.GoTo }}" title="{{ nav2.This.Data.hoverText }}">
|
|
||||||
{{ nav2.Navname }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div><b>main/service Level 2</b></div>
|
|
||||||
<ul>
|
|
||||||
{% for nav in NavActive.2.SubSlice %}
|
|
||||||
<li {% if nav.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">
|
|
||||||
{{ nav.Navname }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block breadcrumb %}
|
|
||||||
<div id="breadcrumb">
|
|
||||||
{% for nav in NavActive %}
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">
|
|
||||||
{{ nav.Navname }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div id="content">
|
|
||||||
{{ Body }}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block footer %}
|
|
||||||
<footer class="nav">
|
|
||||||
<div><b>service Level 1</b></div>
|
|
||||||
<ul>
|
|
||||||
{% for nav in NavActive.0.SubMap.service.SubSlice %}
|
|
||||||
<li {% if nav.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">
|
|
||||||
{{ nav.Navname }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Wie im Beispiel zu sehen ist, werden einfache **Variables** über:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ Variable }}
|
|
||||||
```
|
|
||||||
|
|
||||||
eingebunden. Variablen können außerdem speziell weiterverarbeitet werden. Dies geschieht mit sogenannten Filtern oder Filterfunktionen. Die Syntax dafür ist folgendermaßen:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ Variable|Filter }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Blockanweisungen dagegen verwenden zum Beispiel folgende Platzhalter:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{% if Variable %}
|
|
||||||
...
|
|
||||||
{% endif %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Eine Liste der in Django möglichen Anweisungen finden Sie unter [Django builtins](https://docs.djangoproject.com/en/2.1/ref/templates/builtins/).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mark2web Variablen
|
|
||||||
|
|
||||||
Der mark2web-Generator liefert für die Template-Verarbeitung Variablen für die Navigation und den Inhalt.
|
|
||||||
|
|
||||||
### Website-Inhalt
|
|
||||||
|
|
||||||
Das rohe HTML, welches aus einer Markdown-Datei generiert wird steht über folgende Variablen zur Verfügung.
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ Body }} = komplettes HTML aus der Markdown-Datei
|
|
||||||
{{ BodyParts.0 }} = erster HTML-Block
|
|
||||||
{{ BodyParts.1 }} = zweiter HTML-Block
|
|
||||||
usw.
|
|
||||||
```
|
|
||||||
|
|
||||||
Ist die Markdown-Datei durch `---` auf einer Zeile (nach den Kopfdaten) geteilt, stehen die Einzelteile im Slice/Array `{{ BodyParts }}` zur Verfügung.
|
|
||||||
|
|
||||||
Aus folgender Markdown-Datei `README.md` in einem `content`-Unterverzeichnis:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Titel 1
|
|
||||||
|
|
||||||
Text 1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Titel 2
|
|
||||||
|
|
||||||
Text 2
|
|
||||||
```
|
|
||||||
|
|
||||||
wird für `{{ Body }}` folgendes HTML:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<h1>Titel 1</h1>
|
|
||||||
<p>Text 1</p>
|
|
||||||
<hr>
|
|
||||||
<h2>Titel 2</h2>
|
|
||||||
<p>Text 2</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
`BodyParts` erklärt sich an folgendem Template:
|
|
||||||
|
|
||||||
```django
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
{% for part in BodyParts %}
|
|
||||||
<td>
|
|
||||||
{{ part }}
|
|
||||||
</td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
```
|
|
||||||
|
|
||||||
Aus dem Template wird nach dem Rendern mit obiger Markdown-Datei also folgendes HTML:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h1>Titel 1</h1>
|
|
||||||
<p>Text 1</p>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<h2>Titel 2</h2>
|
|
||||||
<p>Text 2</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Einrückungen im HTML wurden für die bessere Lesbarkeit angepasst. Wie zu sehen ist, wird `---` in `{{ Body }}` laut Markdown-Syntax zu `<br>`. In `{{ BodyParts.N }}` ist es jedoch nicht enthalten, da es hier nur zur Trennung des Dokuments dient.
|
|
||||||
|
|
||||||
### Navigation
|
|
||||||
|
|
||||||
Jedes Navigationselement steht intern in folgender go-Struktur zur Verfügung:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type navElement struct {
|
|
||||||
Navname string
|
|
||||||
GoTo string
|
|
||||||
Active bool
|
|
||||||
|
|
||||||
Data interface{}
|
|
||||||
|
|
||||||
This ThisPathConfig
|
|
||||||
|
|
||||||
SubMap *map[string]*navElement
|
|
||||||
SubSlice *[]*navElement
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Diese erste Navigationsebene wird mit seinen Unternavigationspunkten zum einen auf die Variable `{{ NavMap }}` in Form einer Map (assoziatives Array) mit dem umgeformten Namen (wie im Zielverzeichnis) abgebildet.
|
|
||||||
Außerdem steht die erste Navigationsebene als Liste, bzw. Slice (Array) über die Variable `{{ NavSlice }}` zur verfügung.
|
|
||||||
|
|
||||||
Wird z.B. folgende Navigation als Zielverzeichnis-Struktur angenommen:
|
|
||||||
|
|
||||||
```plain
|
|
||||||
de
|
|
||||||
main
|
|
||||||
home
|
|
||||||
leistungen
|
|
||||||
referenzen
|
|
||||||
service
|
|
||||||
impressum
|
|
||||||
en
|
|
||||||
main
|
|
||||||
home
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Teasertext aus folgender `config.yml` im `content`-Verzeichnis `de/main/02_Leistungen`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
This:
|
|
||||||
Data:
|
|
||||||
teaser: Teasertext
|
|
||||||
```
|
|
||||||
|
|
||||||
welcher zum Navigationspunkt im Zielpfad *de/main/leistungen* gehört, ist über folgende Template-Variablen erreichbar:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ NavMap.de.SubMap.main.SubMap.leistungen.This.Data.teaser }}
|
|
||||||
|
|
||||||
oder
|
|
||||||
|
|
||||||
{{ NavSlice.0.SubSlice.0.SubSlice.1.This.Data.teaser}}
|
|
||||||
|
|
||||||
oder auch eine Kombination
|
|
||||||
|
|
||||||
{{ NavMap.de.SubMap.main.SubSlice.1.This.Data.teaser }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Natürlich wird diese Variable in der Form so nie verwendet, sondern soll nur den Aufbau der Struktur verdeutlichen. Üblicherweise werden Schleifenblöcke verwendet um die Navigationsbäume auszugeben, wie z.B. eine Liste als Sprachwähler, wenn man annimmt, dass die erste Navigationsebene die Website-Sprache ist:
|
|
||||||
|
|
||||||
```django
|
|
||||||
<ul>
|
|
||||||
{% for lang in NavMap %}
|
|
||||||
<li {% if lang.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ lang.GoTo }}">{{ lang.Navname }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
```
|
|
||||||
|
|
||||||
Wie im Beispiel zu sehen ist, wird das aktive Navigationselement mit `class="active"` über die Variable `Active` aus der Struktur markiert.
|
|
||||||
|
|
||||||
#### aktiver Navigationspfad
|
|
||||||
|
|
||||||
Der aktive Navigationspfad ist über eine weitere vorbelegte Variable zu erfahren:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ NavActive }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ähnlich wie `{{ NavSlice }}` oder `{{ ...SubSlice }}` ist dies ein Slice/Array welches als Elemnte Navigationselemente aus oben angegebener Struktur enthält.
|
|
||||||
Im Gegensatz zu `{{ NavSlice }}` besteht die Liste nicht aus Elementen einer Ebene, sonder aus allen aktiven Elemtenten in des aktuellen Pfads.
|
|
||||||
|
|
||||||
Geht man also wieder vom obigen Beispiel aus und der aktive Pfad ist *de/main/leistungen*, so würden folgendes zutreffen:
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ NavActive.0 }} ist das Navigationselement für "de"
|
|
||||||
{{ NavActive.1 }} ist das Navigationselement für "main"
|
|
||||||
{{ NavActive.2 }} ist das Navigationselement für "Leistungen"
|
|
||||||
```
|
|
||||||
|
|
||||||
Somit lassen sich leicht Pfade anzeigen, bzw. Breadcrumbs in die Website einbinden, wie im folgenden Beispiel:
|
|
||||||
|
|
||||||
```django
|
|
||||||
aktiver Pfad:
|
|
||||||
{% for nav in NavActive %}
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">{{ nav.Navname }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ebenso lässt sich bei mehrsprachigen Seite immer die richte Hauptnavigation zur aktuelle Sprache laden:
|
|
||||||
|
|
||||||
```django
|
|
||||||
<h3>Hauptnavigation</h3>
|
|
||||||
<ul>
|
|
||||||
{% for nav in NavActive.0.SubMap.main.SubSlice %}
|
|
||||||
<li {% if nav.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav.GoTo }}" title="{{ nav.This.Data.hoverText }}">
|
|
||||||
{{ nav.Navname }}
|
|
||||||
</a>
|
|
||||||
{% if nav.SubSlice %}
|
|
||||||
<ul>
|
|
||||||
{% for nav2 in nav.SubSlice %}
|
|
||||||
<li {% if nav2.Active %}class="active"{% endif %}>
|
|
||||||
<a href="{{ nav2.GoTo }}" title="{{ nav2.This.Data.hoverText }}">
|
|
||||||
{{ nav2.Navname }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Meta-Angaben
|
|
||||||
|
|
||||||
Über die Variablen
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ Meta.Title }}
|
|
||||||
{{ Meta.Description }}
|
|
||||||
{{ Meta.Keywords }}
|
|
||||||
```
|
|
||||||
|
|
||||||
stehen die üblichen Meta-Angaben für die Verwendung im `<head>` Tag zur Verfügung.
|
|
||||||
|
|
||||||
### weitere Daten
|
|
||||||
|
|
||||||
Die Variablen
|
|
||||||
|
|
||||||
```django
|
|
||||||
{{ This.Navname }}
|
|
||||||
{{ This.Data }}
|
|
||||||
{{ Data }}
|
|
||||||
```
|
|
||||||
|
|
||||||
stehen ebenfalls zur Verfügung und spiegeln die Daten aus den Konfig-Dateien `config.yml` und den Kopfdaten der Markdown-Datei wieder.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
This:
|
|
||||||
Data:
|
|
||||||
teaser: Aus Markdown wird HTML
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
Template: base_doc.html
|
||||||
|
|
||||||
Data:
|
Data:
|
||||||
background: /img/folder.jpg
|
background: /img/folder.jpg
|
||||||
|
|
||||||
@@ -9,9 +11,7 @@ Data:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Benutzung
|
# Ordnerstruktur
|
||||||
|
|
||||||
## Ordnerstruktur
|
|
||||||
|
|
||||||
Das Ausgangsmaterial für die zu generierende Website wird in folgender Ordnerstruktur organisiert:
|
Das Ausgangsmaterial für die zu generierende Website wird in folgender Ordnerstruktur organisiert:
|
||||||
|
|
||||||
@@ -58,25 +58,3 @@ FIL config.yml (globale Konfiguration, enthält andere Anweisungen als individue
|
|||||||
```
|
```
|
||||||
|
|
||||||
In der Minimal-Variante sind nur die Ordner `content` und `templates` nötig.
|
In der Minimal-Variante sind nur die Ordner `content` und `templates` nötig.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `content`
|
|
||||||
|
|
||||||
- enthält die Markdown-Dateien und Konfigurationen für die Navigationsstruktur und Einzelseiten
|
|
||||||
- voranestellte Nummer mit Unterstrich wie z.B. `01_` dienen nur der Sortierung und gehen nicht in den eigentlichen Navigationspfad mit ein
|
|
||||||
- zur Bildung des Navigationspfades werden die Verzeichnisnamen in Kleinschreibung konvertiert
|
|
||||||
- Navigationsnamen für die Website werden aus dem Pfad gebildet, wobei `_`(Unterstriche) in Leerzeichen umgewandelt werden
|
|
||||||
- Navigationsnamen können durch die `config.yml` überschrieben werden
|
|
||||||
|
|
||||||
#### Medien und Downloads
|
|
||||||
|
|
||||||
- Mediendateien werden neben den Inhalten in `content` abgelegt und müssen dementsprechend relativ verlinkt werden
|
|
||||||
|
|
||||||
### `assets`
|
|
||||||
|
|
||||||
- Bilder/CSS/JS die in Templates oder mehrfach in den Content-Seiten benötigt werden liegen in `assets`
|
|
||||||
|
|
||||||
### `templates`
|
|
||||||
|
|
||||||
- Template-Dateien für die Generierung der Website liegen hier
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- enthält die Markdown-Dateien und Konfigurationen für die Navigationsstruktur und Einzelseiten
|
||||||
|
- voranestellte Nummer mit Unterstrich wie z.B. `01_` dienen nur der Sortierung und gehen nicht in den eigentlichen Navigationspfad mit ein
|
||||||
|
- zur Bildung des Navigationspfades werden die Verzeichnisnamen in Kleinschreibung konvertiert
|
||||||
|
- Navigationsnamen für die Website werden aus dem Pfad gebildet, wobei `_`(Unterstriche) in Leerzeichen umgewandelt werden
|
||||||
|
- Navigationsnamen können durch die `config.yml` überschrieben werden
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Mediendateien werden neben den Inhalten in `content` abgelegt und müssen dementsprechend relativ verlinkt werden
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Bilder/CSS/JS die in Templates oder mehrfach in den Content-Seiten benötigt werden liegen in `assets`
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Template-Dateien für die Generierung der Website liegen hier
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
This:
|
||||||
|
Data:
|
||||||
|
teaser: Wie werden die Inhalte und Templates organisiert?
|
||||||
|
Collections:
|
||||||
|
- Name: doccoll
|
||||||
|
Directory:
|
||||||
|
Path: "."
|
||||||
|
MatchFilename: "^_\\d+(?P<lowdash>_*)(?P<title>.+)\\.md"
|
||||||
|
ReverseOrder: False
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
Template: base_doc.html
|
||||||
|
|
||||||
|
Data:
|
||||||
|
background: /img/wire.jpg
|
||||||
|
|
||||||
|
slider:
|
||||||
|
- img: /img/wire.jpg
|
||||||
|
alt:
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
|
||||||
|
Die Konfigurationsdatein sind im YAML-Format gehalten (siehe: [Wikipedia](https://de.wikipedia.org/wiki/YAML)).
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Die obersten Verzeichnis sich befindende Datei `config.yml` kann z.B. folgenden Inhalt haben:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Webserver:
|
||||||
|
Type: "apache"
|
||||||
|
|
||||||
|
Assets:
|
||||||
|
FromPath: "assets"
|
||||||
|
ToPath: "assets"
|
||||||
|
Action: "copy"
|
||||||
|
FixTemplate:
|
||||||
|
Find: "\\.\\./assets/"
|
||||||
|
Replace: ""
|
||||||
|
|
||||||
|
OtherFiles:
|
||||||
|
Action: "copy"
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Derzeit wird hier nur der Wert `apache` unterstützt. Ist dieser Wert gesetzt werden automatische .htaccess-Dateien mit mod_rewrite-Anweisungen generiert, die eine saubere Weiterleitung bei entsprechenden Konfig-Anweisungen im `content`-Verzeichnis ermöglichen.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`Assets:` steuert, wie mit Bild/JS/CSS Dateien umgegangen werden soll.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Lage des Asset-Verzeichnis unterhalb des `content`-Verzeichnis
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Zielverzeichnis im Ausgabe-Verzeichnis der fertig generierten Website
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Derzeit nur `copy`, also das Kopieren der Dateien und Unterordner ins Zielverzeichnis
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Wenn hier `Find:` (regulärer Ausdruck) und `Replace:` (Ersetzung) angeben sind, werden die gefundenden Pfadangaben in der generierten HTML-Dateien durch den korrekten relativen Pfad zum Asset-Verzeichnis ersetzt.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`OtherFiles:` definiert, wie mit anderen Dateien innerhalb des `content`-Verzeichnis umgegangen werden soll.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Derzeit nur `copy`, also das Kopieren der Dateien in das entsprechende Unterverzeichnis im Ausgabe-Verzeichnis
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Im `content`-Verzeichnis, sowie in jedem Unterverzeichnis unterhalb von `content` kann sich eine `config.yml`-Datei befinden, wie aus folgendem Beispiel:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
This:
|
||||||
|
GoTo: "/de/service/impressum/"
|
||||||
|
Navname: "Impressumsverweis"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
oder
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
This:
|
||||||
|
Navname: "FAQ's"
|
||||||
|
Data:
|
||||||
|
slogan: "Wer nicht fragt, bekommt keine Antwort."
|
||||||
|
|
||||||
|
Template: "base.html"
|
||||||
|
|
||||||
|
Index:
|
||||||
|
InputFile: "README.md"
|
||||||
|
OutputFile: "index.html"
|
||||||
|
|
||||||
|
Meta:
|
||||||
|
Title: "Fragen und Antworten"
|
||||||
|
Description: "Dies ist die Fragen und Antworten Unterseite."
|
||||||
|
Keywords: "FAQ, Fragen, Antworten"
|
||||||
|
|
||||||
|
Data:
|
||||||
|
background: "bg.jpg"
|
||||||
|
slider:
|
||||||
|
- img: "assets/img/slider1.jpg"
|
||||||
|
alt: "Alternativtext 1"
|
||||||
|
- img: "assets/img/slider2.jpg"
|
||||||
|
alt: "Alternativtext 2"
|
||||||
|
- img: "assets/img/slider3.jpg"
|
||||||
|
alt: "Alternativtext 3"
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Sämtlich Werte unterhalb dieser Sektion gelten nur für den Inhalt, bzw. Navigationspunkt in dessen Ordner sich die `config.yml` befindet. Die Werte werden nicht an Unterordner wertervererbt.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
Data:
|
||||||
|
Version: "ab v1.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Falls der Navigationspunkt selbst keinen Inhalt darstellen soll, sondern nur weiterleiten soll, so wird hier das Weiterleitungsziel eingegeben.
|
||||||
|
Das Ziel ist der absolute (startend mit `/`) oder relative Pfad zum Zielnavigationspunkt.
|
||||||
|
Die Schreibweise des Pfades ist so zu verwenden, wie der Pfad nach Umschreibung und Säuberung des Pfades im Zielverzeichnis dargestellt wird.
|
||||||
|
Aus `de/mainnav/03_Fragen und Antworten` wird also z.B. `de/mainnav/fragen-und-antworten`.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user