41 Commits
v1.1.0 ... jobm

Author SHA1 Message Date
Sebastian Frank
daed37587e improved logging
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-29 15:49:25 +01:00
Sebastian Frank
7695f42e20 started new logger output
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-27 13:52:22 +01:00
Sebastian Frank
5d6d03702e webrequest pkg, web request bar 2019-03-27 12:52:30 +01:00
Sebastian Frank
4cb09fb81f local collections for docu in website
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-25 15:33:53 +01:00
Sebastian Frank
234137f22f cached webrequests 2019-03-25 15:07:02 +01:00
Sebastian Frank
267d1010bb -progress cli param for bars 2019-03-25 14:01:28 +01:00
Sebastian Frank
740fb94556 job manager, jobm pkg 2019-03-25 10:16:33 +01:00
Sebastian Frank
a17926f54b logger pkg 2019-03-25 09:28:58 +01:00
Sebastian Frank
b9c4553577 fixed website blog
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-24 14:51:40 +01:00
Sebastian Frank
745c886cec bump to version 1.2.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-22 17:24:41 +01:00
Sebastian Frank
ff1da084af collections via markdown files
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-22 17:22:03 +01:00
Sebastian Frank
4a9a3eec06 fixed missing apache rewrite in root
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-22 11:29:08 +01:00
Sebastian Frank
3573e23212 build badge in README
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-21 16:42:26 +01:00
Sebastian Frank
1312dcecb5 build and deploy website in drone are now 2 steps
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-21 16:36:09 +01:00
Sebastian Frank
f8f40b2134 libc6-compat in alpine testing
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-21 16:30:48 +01:00
Sebastian Frank
58681bd7df CGO_ENABLED=0 for cross compile
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-21 16:27:32 +01:00
Sebastian Frank
7df4a03171 libc6-compat in alpine docker image
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-21 16:23:33 +01:00
Sebastian Frank
5624c7af87 disable brotli support without CGO
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-21 16:12:55 +01:00
Sebastian Frank
23fd5fe1d4 gzip pre compression
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-21 14:55:40 +01:00
Sebastian Frank
50139c6f51 markdown filter s parameter, more tests
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-20 14:53:38 +01:00
Sebastian Frank
c5fd151060 added license to website
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-20 13:13:54 +01:00
Sebastian Frank
90a39e3027 added license
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-20 13:07:17 +01:00
Sebastian Frank
9d855f586d goconvey color output
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 21:41:38 +01:00
Sebastian Frank
946f586ccb goconvey color output
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 21:34:46 +01:00
Sebastian Frank
cd8c7fa657 goconvey color output
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 21:32:19 +01:00
Sebastian Frank
3e3d1f05a0 added goconvey to vendor
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 21:29:16 +01:00
Sebastian Frank
5cc4b9d001 added tests
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-19 21:25:14 +01:00
Sebastian Frank
3c87da15e1 fixed Docker build
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 12:56:56 +01:00
Sebastian Frank
baa38b668e fixed Docker build
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-19 12:54:37 +01:00
Sebastian Frank
d652afd633 multithreaded image processing
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-19 12:46:32 +01:00
Sebastian Frank
ada333a0e1 optimized code 2019-03-19 11:46:21 +01:00
Sebastian Frank
0dfe0f8142 Run function 2019-03-19 11:34:35 +01:00
Sebastian Frank
70d7497eda better project layout
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 11:15:32 +01:00
Sebastian Frank
dfc932b7b0 fixed node.root
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-19 10:38:48 +01:00
Sebastian Frank
938e597f3f reorganized code
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-18 15:35:17 +01:00
Sebastian Frank
29f01a2618 reorganized code
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-18 15:14:41 +01:00
Sebastian Frank
66a9ebe452 fixed drone ci
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-18 13:37:22 +01:00
Sebastian Frank
b2e0d78a2c reorganized code
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-18 13:34:52 +01:00
Sebastian Frank
6b34509d9a target dir via t parameter in image_process filter
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-14 12:10:17 +01:00
Sebastian Frank
8210e16305 website pre wrap
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-12 14:59:50 +01:00
Sebastian Frank
2f114885ac fixed blog details body
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-12 14:56:00 +01:00
126 changed files with 3323 additions and 1991 deletions

7
.gitignore vendored
View File

@@ -1,3 +1,4 @@
test.html
html/
mark2web
/html/
/build/dist
/coverage.out
/test/out

24
.gitmodules vendored
View File

@@ -67,3 +67,27 @@
[submodule "vendor/golang.org/x/image"]
path = vendor/golang.org/x/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

View File

@@ -6,7 +6,7 @@
"commands": [
{
"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
}
]

26
LICENSE Normal file
View 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.

View File

@@ -1,5 +1,7 @@
# mark2web
[![Build Status](https://ci.basehosts.de/api/badges/apairon/mark2web/status.svg)](https://ci.basehosts.de/apairon/mark2web)
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.
@@ -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 öffentliche Website ist mit **mark2web** generiert.
## Lizenz
Das Projekt **mark2web** unterliegt der Lizenz "Simplified BSD License" (siehe [LICENSE](LICENSE)).

View File

@@ -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

View File

@@ -1 +0,0 @@
1.1.0

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
1.2.0

View File

@@ -6,24 +6,43 @@ workspace:
path: src/gitbase.de/apairon/mark2web
steps:
- name: build for linux
image: golang:latest
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
- name: init submodules
image: docker:git
commands:
- git submodule update --init --recursive
- 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:
event: [ push, tag ]
- name: test with example content
image: alpine
commands:
- ./dist/mark2web-`cat VERSION`-linux-amd64 -version
- ./dist/mark2web-`cat VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug
- apk add --no-cache libc6-compat
- dist/mark2web-`cat build/VERSION`-linux-amd64 -version
- dist/mark2web-`cat build/VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug
when:
event: [ push, tag ]
@@ -34,7 +53,7 @@ steps:
GOOS: freebsd
GOARCH: amd64
commands:
- ./build.sh
- scripts/build.sh
when:
event: [ tag ]
@@ -45,7 +64,7 @@ steps:
GOOS: darwin
GOARCH: amd64
commands:
- ./build.sh
- scripts/build.sh
when:
event: [ tag ]
@@ -57,15 +76,15 @@ steps:
GOARCH: amd64
FILEEXT: .exe
commands:
- ./build.sh
- scripts/build.sh
when:
event: [ tag ]
- name: build docker image
image: docker
commands:
- cp dist/mark2web-`cat VERSION`-linux-amd64 mark2web
- docker build -t apairon/mark2web .
- cp dist/mark2web-`cat build/VERSION`-linux-amd64 build/package/mark2web
- docker build -t apairon/mark2web build/package
volumes:
- name: docker
path: /var/run/docker.sock
@@ -73,6 +92,17 @@ steps:
branch: [ master ]
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
image: apairon/mark2web:latest
pull: never
@@ -80,8 +110,6 @@ steps:
RSYNC_PASS:
from_secret: rsync_pass
commands:
- /mark2web -version
- /mark2web -in website -out html -create -logLevel info
- '
rsync -rlcgD -i -u -v --stats
--delete
@@ -110,8 +138,8 @@ steps:
base_url: https://gitbase.de
files:
- dist/*
title: VERSION
note: RELEASE.md
title: build/VERSION
note: build/RELEASE.md
checksum:
- md5
- sha256

View File

@@ -1,4 +1,4 @@
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 /
CMD ["/mark2web"]

View File

@@ -6,8 +6,10 @@ import (
"os"
"path"
"gitbase.de/apairon/mark2web/config"
"gitbase.de/apairon/mark2web/helper"
"gitbase.de/apairon/mark2web/pkg/filter"
"gitbase.de/apairon/mark2web/pkg/logger"
"gitbase.de/apairon/mark2web/pkg/mark2web"
"gitbase.de/apairon/mark2web/pkg/progress"
)
var (
@@ -19,16 +21,13 @@ var (
BuildTime = "UNKNOWN"
)
var log = helper.Log
var contentConfig = new(config.PathConfigTree)
func main() {
inDir := flag.String("in", "./", "input directory")
outDir := flag.String("out", "html", "output directory")
createOutDir := flag.Bool("create", false, "create output directory if not existing")
//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")
flag.Parse()
@@ -40,60 +39,66 @@ func main() {
os.Exit(0)
}
level := "info"
if logLevel != nil {
level := "notice"
if progressBars != nil && *progressBars {
progress.Start()
level = "warning" // disable log for progressbars
}
if logLevel != nil && *logLevel != "" {
level = *logLevel
}
helper.ConfigureLogger(level)
logger.SetLogLevel(level)
if inDir == nil || *inDir == "" {
log.Panic("input directory not specified")
logger.Exit("input directory not specified")
}
iDir := path.Clean(*inDir)
inDir = &iDir
log.Infof("input directory: %s", *inDir)
logger.I("input directory: %s", *inDir)
if outDir == nil || *outDir == "" {
log.Panic("output directory not specified")
logger.Exit("output directory not specified")
}
oDir := path.Clean(*outDir)
outDir = &oDir
log.Infof("output directory: %s", *outDir)
logger.I("output directory: %s", *outDir)
if createOutDir != nil && *createOutDir {
if _, err := os.Stat(*outDir); os.IsNotExist(err) {
log.Debugf("output directory '%s' does not exist", *outDir)
log.Debugf("trying to create output directory: %s", *outDir)
logger.D("output directory '%s' does not exist", *outDir)
logger.D("trying to create output directory: %s", *outDir)
err := os.MkdirAll(*outDir, 0755)
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 {
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) {
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 {
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() {
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"
err := config.ReadGlobalConfig(configFilename)
err := mark2web.Config.ReadFromFile(configFilename)
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
config.Config.Directories.Output = *outDir
mark2web.Config.Directories.Input = *inDir
mark2web.Config.Directories.Output = *outDir
log.Debugf("reading input directory %s", *inDir)
logger.D("reading input directory %s", *inDir)
defaultTemplate := "base.html"
defaultInputFile := "README.md"
@@ -104,40 +109,35 @@ func main() {
defaultFilenameIgnore := "^_"
defaultFilenameOutputExtension := "html"
defaultPathConfig := new(config.PathConfig)
defaultPathConfig := new(mark2web.PathConfig)
defaultPathConfig.Template = &defaultTemplate
defaultPathConfig.Index = &config.IndexConfig{
defaultPathConfig.Index = &mark2web.IndexConfig{
InputFile: &defaultInputFile,
OutputFile: &defaultOutputFile,
}
defaultPathConfig.Path = &config.DirnameConfig{
defaultPathConfig.Path = &mark2web.DirnameConfig{
Strip: &defaultPathStrip,
IgnoreForNav: &defaultPathIgnoreForNav,
}
defaultPathConfig.Filename = &config.FilenameConfig{
defaultPathConfig.Filename = &mark2web.FilenameConfig{
Strip: &defaultFilenameStrip,
Ignore: &defaultFilenameIgnore,
OutputExtension: &defaultFilenameOutputExtension,
}
defaultPathConfig.Imaging = &config.ImagingConfig{
defaultPathConfig.Imaging = &mark2web.ImagingConfig{
Width: 1920,
Height: 1920,
Process: "fit",
Quality: 75,
}
helper.ReadContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig)
//spew.Dump(contentConfig)
//spew.Dump(navMap)
templatesDir := *inDir + "/templates"
helper.SetTemplateDir(templatesDir)
filtersDir := templatesDir + "/filters"
filtersDir := *inDir + "/templates/filters"
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")
}

View File

@@ -1,2 +1,2 @@
This:
GoTo: main/home
GoTo: main/home/

View File

@@ -1,2 +1,2 @@
This:
GoTo: main/home
GoTo: main/home/

View File

@@ -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)
}
}
}

View File

@@ -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...)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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")
})
})
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View File

@@ -1,4 +1,4 @@
package config
package mark2web
import (
"io/ioutil"
@@ -13,6 +13,7 @@ type GlobalConfig struct {
} `yaml:"Webserver"`
Assets struct {
Compress bool `yaml:"Compress"`
FromPath string `yaml:"FromPath"`
ToPath string `yaml:"ToPath"`
Action string `yaml:"Action"`
@@ -26,20 +27,28 @@ type GlobalConfig struct {
Action string `yaml:"Action"`
} `yaml:"OtherFiles"`
Compress struct {
Brotli bool `yaml:"Brotli"`
GZIP bool `yaml:"GZIP"`
Extensions map[string]string `yaml:"Extensions"`
} `yaml:"Compress"`
Directories struct {
Input string
Output string
}
}
// Config is global config
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)
if err != nil {
return err
}
err = yaml.Unmarshal(data, Config)
err = yaml.Unmarshal(data, c)
if err != nil {
return err
}

View File

@@ -1,60 +1,19 @@
package config
package mark2web
import (
"fmt"
"reflect"
import "gitbase.de/apairon/mark2web/pkg/helper"
"github.com/imdario/mergo"
)
// 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)
}
// CollectionDirectoryConfig specifies how to handle a directory of markdown files as a collection
type CollectionDirectoryConfig struct {
Path string `yaml:"Path"`
MatchFilename string `yaml:"MatchFilename"`
ReverseOrder bool `yaml:"ReverseOrder"`
}
// CollectionConfig describes a collection
type CollectionConfig struct {
Name *string `yaml:"Name"`
URL *string `yaml:"URL"`
Name *string `yaml:"Name"`
URL *string `yaml:"URL"`
Directory *CollectionDirectoryConfig `yaml:"Directory"`
NavTemplate *struct {
EntriesAttribute string `yaml:"EntriesAttribute"`
GoTo string `yaml:"GoTo"`
@@ -72,7 +31,7 @@ type ThisPathConfig struct {
Navname *string `yaml:"Navname"`
GoTo *string `yaml:"GoTo"`
Collections []*CollectionConfig `yaml:"Collections"`
Data MapString `yaml:"Data"`
Data helper.MapString `yaml:"Data"`
}
// IndexConfig describes index input and output file
@@ -116,8 +75,9 @@ type ImagingConfig struct {
Anchor string `yaml:"Anchor"`
Quality int `yaml:"Quality"`
Filename string `yaml:"-"`
Format string `yaml:"-"`
TargetDir string `yaml:"-"`
Filename string `yaml:"-"`
Format string `yaml:"-"`
}
// PathConfig of subdir
@@ -131,41 +91,8 @@ type PathConfig struct {
Markdown *MarkdownConfig `yaml:"Markdown"`
Imaging *ImagingConfig `yaml:"Imaging"`
Data MapString `yaml:"Data"`
}
// PathConfigTree is complete config tree of content dir
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{}))
Data helper.MapString `yaml:"Data"`
// Collections here are recursive if saved as nav, so request should be filtered
Collections []*CollectionConfig `yaml:"Collections"`
}

297
pkg/mark2web/content.go Normal file
View 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
View 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)
}

View 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
View 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)
}
}
}

View File

@@ -1,11 +1,12 @@
package helper
package mark2web
import (
"path"
"regexp"
"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
@@ -14,19 +15,23 @@ type NavElement struct {
GoTo string
Active bool
ColMap config.MapString
ColMap helper.MapString
Data interface{}
This config.ThisPathConfig
This ThisPathConfig
SubMap *map[string]*NavElement
SubSlice *[]*NavElement
}
// BuildNavigation builds the navigation trees for use in templates
func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string) {
for _, el := range conf.Sub {
// buildNavigation builds the navigation trees for use in templates
func (node *TreeNode) buildNavigation(curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement) {
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 {
continue // ignore hidden nav points from collections
}
@@ -37,16 +42,15 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
}
if ignNav != nil && *ignNav != "" {
regex, err := regexp.Compile(*ignNav)
if err != nil {
Log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err)
}
logger.Eexit(err, "could not compile IngoreForNav regexp '%s' in '%s'", *ignNav, 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
}
}
elPath := strings.TrimPrefix(el.OutputPath, config.Config.Directories.Output+"/")
elPath := strings.TrimPrefix(el.OutputPath, Config.Directories.Output+"/")
subMap := make(map[string]*NavElement)
subSlice := make([]*NavElement, 0)
@@ -88,8 +92,7 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
if activeNav != "" && activeNav != "/" {
// calculate relative path
bToRoot := BackToRoot(activeNav)
navEl.GoTo = bToRoot + navEl.GoTo
navEl.GoTo = backToRoot + 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)
}
BuildNavigation(el, &subMap, &subSlice, navActive, activeNav)
buildNavigationRecursive(el, &subMap, &subSlice, navActive, activeNav, backToRoot)
}
}

14
pkg/mark2web/no_cgo.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

View File

@@ -1,7 +1,8 @@
GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog
?sort[date]=-1
&limit=101
&token=89ff216524093123bf7a0a10f7b273
&token=985cee34099f4d3b08f18fc22f6296
&filter[link][$exists]=0
###

1
vendor/github.com/gosuri/uilive generated vendored Submodule

1
vendor/github.com/gosuri/uiprogress generated vendored Submodule

1
vendor/github.com/itchio/go-brotli generated vendored Submodule

1
vendor/github.com/jtolds/gls generated vendored Submodule

1
vendor/github.com/mattn/go-tty generated vendored Submodule

1
vendor/golang.org/x/sys generated vendored Submodule

Submodule vendor/golang.org/x/sys added at f49334f85d

View File

@@ -2,6 +2,7 @@ Webserver:
Type: "apache" # generates .htaccess
Assets:
Compress: True
FromPath: "project-files"
ToPath: "project-files"
Action: "copy" # symlink, copy or move
@@ -12,3 +13,10 @@ Assets:
OtherFiles:
Action: "copy"
Compress:
Brotli: True
GZIP: True
Extensions:
.html: text/html
.css: text/css
.js: text/javascript

View File

@@ -10,23 +10,16 @@ Data:
---
# 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
mkdir -p $GOPATH/src/gitbase.de/apairon
git clone https://gitbase.de/apairon/mark2web.git $GOPATH/src/gitbase.de/apairon/mark2web
go get -v gitbase.de/apairon/mark2web/cmd/mark2web
cd $GOPATH/src/gitbase.de/apairon/mark2web
git submodule update --init --recursive
./build.sh
# setze Versioninformationen ins Binary
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
```
---
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
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

View File

@@ -1,3 +0,0 @@
This:
Data:
teaser: Wie werden die Inhalte und Templates organisiert?

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
This:
Data:
teaser: Globale Konfiguration und individuelle Content-Einstellungen

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
This:
Data:
teaser: Aus Markdown wird HTML

View File

@@ -1,4 +1,6 @@
---
Template: base_doc.html
Data:
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:
@@ -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.
---
### `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

View File

@@ -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

View File

@@ -0,0 +1 @@
- Mediendateien werden neben den Inhalten in `content` abgelegt und müssen dementsprechend relativ verlinkt werden

View File

@@ -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`

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
- Template-Dateien für die Generierung der Website liegen hier

View File

@@ -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

View File

@@ -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)).

View File

@@ -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"
```

View File

@@ -0,0 +1,5 @@
---
Data:
Version: "ab v1.0"
---

View File

@@ -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.

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
`Assets:` steuert, wie mit Bild/JS/CSS Dateien umgegangen werden soll.

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Lage des Asset-Verzeichnis unterhalb des `content`-Verzeichnis

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Zielverzeichnis im Ausgabe-Verzeichnis der fertig generierten Website

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Derzeit nur `copy`, also das Kopieren der Dateien und Unterordner ins Zielverzeichnis

View File

@@ -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.

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
`OtherFiles:` definiert, wie mit anderen Dateien innerhalb des `content`-Verzeichnis umgegangen werden soll.

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Derzeit nur `copy`, also das Kopieren der Dateien in das entsprechende Unterverzeichnis im Ausgabe-Verzeichnis

View File

@@ -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"
```

View File

@@ -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.

View File

@@ -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