32 Commits

Author SHA1 Message Date
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
103 changed files with 2561 additions and 1503 deletions

7
.gitignore vendored
View File

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

12
.gitmodules vendored
View File

@@ -67,3 +67,15 @@
[submodule "vendor/golang.org/x/image"] [submodule "vendor/golang.org/x/image"]
path = vendor/golang.org/x/image path = vendor/golang.org/x/image
url = https://go.googlesource.com/image url = https://go.googlesource.com/image
[submodule "vendor/github.com/smartystreets/goconvey"]
path = vendor/github.com/smartystreets/goconvey
url = https://github.com/smartystreets/goconvey
[submodule "vendor/github.com/jtolds/gls"]
path = vendor/github.com/jtolds/gls
url = https://github.com/jtolds/gls
[submodule "vendor/github.com/smartystreets/assertions"]
path = vendor/github.com/smartystreets/assertions
url = https://github.com/smartystreets/assertions
[submodule "vendor/github.com/itchio/go-brotli"]
path = vendor/github.com/itchio/go-brotli
url = https://github.com/itchio/go-brotli

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 # 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. mark2web ist ein Website-Generator, der als Eingabe Markdown-Dateien, Templates und Konfigurations-Dateien nutzt.
Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwaltung (z.B. git) und anschließende CI/CD-Pipeline, welche den Generator aufruft und die fertige Website publiziert. Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwaltung (z.B. git) und anschließende CI/CD-Pipeline, welche den Generator aufruft und die fertige Website publiziert.
@@ -8,4 +10,8 @@ Die vorgesehene Arbeitsweise ist die Pflege der Inhalte über eine Versionsverwa
Die Dokumentation ist auf der [mark2web-Website](https://www.mark2web.de/) zu finden. Außerdem ist die Dokumentation im Verzeichnis `website/content` dieses Repositories, da dies das Ausgangsmaterial der Projekt-Website ist. Die Dokumentation ist auf der [mark2web-Website](https://www.mark2web.de/) zu finden. Außerdem ist die Dokumentation im Verzeichnis `website/content` dieses Repositories, da dies das Ausgangsmaterial der Projekt-Website ist.
Die öffentliche Website ist mit **mark2web** generiert. Die öffentliche Website ist mit **mark2web** generiert.
## Lizenz
Das Projekt **mark2web** unterliegt der Lizenz "Simplified BSD License" (siehe [LICENSE](LICENSE)).

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

View File

@@ -1,4 +1,4 @@
FROM alpine FROM alpine
RUN apk update && apk add ca-certificates openssh-client sshpass rsync gzip && rm -r /var/cache/ RUN apk update && apk add ca-certificates openssh-client sshpass rsync gzip libc6-compat && rm -r /var/cache/
ADD mark2web / ADD mark2web /
CMD ["/mark2web"] CMD ["/mark2web"]

View File

@@ -6,8 +6,9 @@ import (
"os" "os"
"path" "path"
"gitbase.de/apairon/mark2web/config" "gitbase.de/apairon/mark2web/pkg/filter"
"gitbase.de/apairon/mark2web/helper" "gitbase.de/apairon/mark2web/pkg/helper"
"gitbase.de/apairon/mark2web/pkg/mark2web"
) )
var ( var (
@@ -19,10 +20,6 @@ var (
BuildTime = "UNKNOWN" BuildTime = "UNKNOWN"
) )
var log = helper.Log
var contentConfig = new(config.PathConfigTree)
func main() { func main() {
inDir := flag.String("in", "./", "input directory") inDir := flag.String("in", "./", "input directory")
outDir := flag.String("out", "html", "output directory") outDir := flag.String("out", "html", "output directory")
@@ -47,53 +44,53 @@ func main() {
helper.ConfigureLogger(level) helper.ConfigureLogger(level)
if inDir == nil || *inDir == "" { if inDir == nil || *inDir == "" {
log.Panic("input directory not specified") helper.Log.Panic("input directory not specified")
} }
iDir := path.Clean(*inDir) iDir := path.Clean(*inDir)
inDir = &iDir inDir = &iDir
log.Infof("input directory: %s", *inDir) helper.Log.Infof("input directory: %s", *inDir)
if outDir == nil || *outDir == "" { if outDir == nil || *outDir == "" {
log.Panic("output directory not specified") helper.Log.Panic("output directory not specified")
} }
oDir := path.Clean(*outDir) oDir := path.Clean(*outDir)
outDir = &oDir outDir = &oDir
log.Infof("output directory: %s", *outDir) helper.Log.Infof("output directory: %s", *outDir)
if createOutDir != nil && *createOutDir { if createOutDir != nil && *createOutDir {
if _, err := os.Stat(*outDir); os.IsNotExist(err) { if _, err := os.Stat(*outDir); os.IsNotExist(err) {
log.Debugf("output directory '%s' does not exist", *outDir) helper.Log.Debugf("output directory '%s' does not exist", *outDir)
log.Debugf("trying to create output directory: %s", *outDir) helper.Log.Debugf("trying to create output directory: %s", *outDir)
err := os.MkdirAll(*outDir, 0755) err := os.MkdirAll(*outDir, 0755)
if err != nil { if err != nil {
log.Panic(err) helper.Log.Panic(err)
} }
log.Noticef("created output directory: %s", *outDir) helper.Log.Noticef("created output directory: %s", *outDir)
} else { } else {
log.Noticef("output directory '%s' already exists", *outDir) helper.Log.Noticef("output directory '%s' already exists", *outDir)
} }
} }
if fD, err := os.Stat(*outDir); os.IsNotExist(err) { if fD, err := os.Stat(*outDir); os.IsNotExist(err) {
log.Panicf("output directory '%s' does not exist, try -create parameter or create manually", *outDir) helper.Log.Panicf("output directory '%s' does not exist, try -create parameter or create manually", *outDir)
} else { } else {
if fD == nil { if fD == nil {
log.Panicf("something went wrong, could not get file handle for output dir %s", *outDir) helper.Log.Panicf("something went wrong, could not get file handle for output dir %s", *outDir)
} else if !fD.IsDir() { } else if !fD.IsDir() {
log.Panicf("output directory '%s' is not a directory", *outDir) helper.Log.Panicf("output directory '%s' is not a directory", *outDir)
} }
} }
log.Debug("reading global config...") helper.Log.Debug("reading global config...")
configFilename := *inDir + "/config.yml" configFilename := *inDir + "/config.yml"
err := config.ReadGlobalConfig(configFilename) err := mark2web.Config.ReadFromFile(configFilename)
if err != nil { if err != nil {
log.Panicf("could not read file '%s': %s", configFilename, err) helper.Log.Panicf("could not read file '%s': %s", configFilename, err)
} }
config.Config.Directories.Input = *inDir mark2web.Config.Directories.Input = *inDir
config.Config.Directories.Output = *outDir mark2web.Config.Directories.Output = *outDir
log.Debugf("reading input directory %s", *inDir) helper.Log.Debugf("reading input directory %s", *inDir)
defaultTemplate := "base.html" defaultTemplate := "base.html"
defaultInputFile := "README.md" defaultInputFile := "README.md"
@@ -104,40 +101,33 @@ func main() {
defaultFilenameIgnore := "^_" defaultFilenameIgnore := "^_"
defaultFilenameOutputExtension := "html" defaultFilenameOutputExtension := "html"
defaultPathConfig := new(config.PathConfig) defaultPathConfig := new(mark2web.PathConfig)
defaultPathConfig.Template = &defaultTemplate defaultPathConfig.Template = &defaultTemplate
defaultPathConfig.Index = &config.IndexConfig{ defaultPathConfig.Index = &mark2web.IndexConfig{
InputFile: &defaultInputFile, InputFile: &defaultInputFile,
OutputFile: &defaultOutputFile, OutputFile: &defaultOutputFile,
} }
defaultPathConfig.Path = &config.DirnameConfig{ defaultPathConfig.Path = &mark2web.DirnameConfig{
Strip: &defaultPathStrip, Strip: &defaultPathStrip,
IgnoreForNav: &defaultPathIgnoreForNav, IgnoreForNav: &defaultPathIgnoreForNav,
} }
defaultPathConfig.Filename = &config.FilenameConfig{ defaultPathConfig.Filename = &mark2web.FilenameConfig{
Strip: &defaultFilenameStrip, Strip: &defaultFilenameStrip,
Ignore: &defaultFilenameIgnore, Ignore: &defaultFilenameIgnore,
OutputExtension: &defaultFilenameOutputExtension, OutputExtension: &defaultFilenameOutputExtension,
} }
defaultPathConfig.Imaging = &config.ImagingConfig{ defaultPathConfig.Imaging = &mark2web.ImagingConfig{
Width: 1920, Width: 1920,
Height: 1920, Height: 1920,
Process: "fit", Process: "fit",
Quality: 75, Quality: 75,
} }
helper.ReadContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig) filtersDir := *inDir + "/templates/filters"
//spew.Dump(contentConfig)
//spew.Dump(navMap)
templatesDir := *inDir + "/templates"
helper.SetTemplateDir(templatesDir)
filtersDir := templatesDir + "/filters"
if _, err := os.Stat(filtersDir); !os.IsNotExist(err) { if _, err := os.Stat(filtersDir); !os.IsNotExist(err) {
helper.RegisterFilters(filtersDir) filter.RegisterFilters(filtersDir)
} }
helper.ProcessContent(contentConfig, contentConfig)
helper.ProcessAssets() mark2web.Run(*inDir, *outDir, defaultPathConfig)
} }

View File

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

View File

@@ -1,2 +1,2 @@
This: 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,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)
}

73
pkg/filter/custom.go Normal file
View File

@@ -0,0 +1,73 @@
package filter
import (
"io/ioutil"
"path"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
"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)
if err != nil {
helper.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()
helper.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 {
helper.Log.Panicf("error in javascript vm for '%s': %s", jsFile, err)
}
if !fn.IsFunction() {
helper.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 mark2web.CurrentContext != nil {
thisObj.Set("context", *mark2web.CurrentContext)
}
if err != nil {
helper.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 {
helper.Log.Panicf("error in javascript file '%s' while calling returned function: %s", jsFile, err)
}
retGo, err := ret.Export()
if err != nil {
helper.Log.Panicf("export error for '%s': %s", jsFile, err)
}
return pongo2.AsValue(retGo), nil
},
)
if err != nil {
helper.Log.Panicf("could not register filter from '%s': %s", jsFile, err)
}
}
}
}
}

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

230
pkg/filter/image_process.go Normal file
View File

@@ -0,0 +1,230 @@
package filter
import (
"crypto/md5"
"errors"
"fmt"
"image"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
"gitbase.de/apairon/mark2web/pkg/mark2web"
"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
}
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 = 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) {
helper.Log.Infof("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() {
helper.Log.Noticef("skipped processing image from %s to %s, file already exists", imgSource, imgTarget)
} else {
mark2web.ThreadStart(func() {
helper.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 {
helper.Log.Panicf("filter:image_resize, 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:
helper.Log.Panicf("filter:image_resize, unknown anchor a=%s definition", p.Anchor)
}
img = imaging.Fill(img, p.Width, p.Height, anchor, imaging.Lanczos)
default:
helper.Log.Panicf("filter:image_resize, 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 {
helper.Log.Panicf("filter:image_resize, could save image '%s': %s", imgTarget, err)
}
helper.Log.Noticef("finished image: %s", imgTarget)
})
}
return pongo2.AsValue(mark2web.CurrentTreeNode.ResolveNavPath(p.Filename)), nil
}

View File

@@ -0,0 +1,54 @@
package filter
import (
"os"
"testing"
"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
mark2web.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")
*/
})
})
}

View File

@@ -1,6 +1,8 @@
package helper package helper
import "os" import (
"os"
)
// CreateDirectory creates direcory with all missing parents and panic if error // CreateDirectory creates direcory with all missing parents and panic if error
func CreateDirectory(dir string) { func CreateDirectory(dir string) {

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
}

53
pkg/helper/webrequest.go Normal file
View File

@@ -0,0 +1,53 @@
package helper
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
)
// JSONWebRequest will GET a json object/array from a given URL
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
}

56
pkg/mark2web/assets.go Normal file
View File

@@ -0,0 +1,56 @@
package mark2web
import (
"log"
"path"
"regexp"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
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
}
helper.Log.Noticef("copying assets from '%s' to '%s'", from, to)
err := cpy.Copy(from, to)
if err != nil {
helper.Log.Panicf("could not copy assets from '%s' to '%s': %s", from, to, err)
}
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 != "" {
helper.Log.Debugf("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)
if err != nil {
log.Panicf("could not compile regexp '%s' for assets path: %s", find, err)
}
repl = bToRoot + toPath + "/" + repl
repl = path.Clean(repl) + "/"
helper.Log.Debugf("new assets paths: %s", repl)
return regex.ReplaceAllString(str, repl)
}
return str
}

48
pkg/mark2web/brotli.go Normal file
View File

@@ -0,0 +1,48 @@
// +build cgo
package mark2web
import (
"io"
"os"
"gitbase.de/apairon/mark2web/pkg/helper"
"github.com/itchio/go-brotli/enc"
)
var brotliSupported = true
func handleBrotliCompression(filename string, content []byte) {
brFilename := filename + ".br"
helper.Log.Infof("writing to compressed output file: %s", brFilename)
f, err := os.Create(brFilename)
if err != nil {
helper.Log.Panicf("could not create file '%s': %s", brFilename, err)
}
defer f.Close()
bw := enc.NewBrotliWriter(f, nil)
defer bw.Close()
if content != nil {
// content given
_, err = bw.Write(content)
if err != nil {
helper.Log.Panicf("could not write brotli content for '%s': %s", filename, err)
}
} else {
// read file
r, err := os.Open(filename)
if err != nil {
helper.Log.Panicf("could not open file '%s': %s", filename, err)
}
defer r.Close()
_, err = io.Copy(bw, r)
if err != nil {
helper.Log.Panicf("could not write brotli file for '%s': %s", filename, err)
}
}
}

213
pkg/mark2web/collection.go Normal file
View File

@@ -0,0 +1,213 @@
package mark2web
import (
"io/ioutil"
"path"
"regexp"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
"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 == "" {
helper.Log.Panicf("missing Name in collection config in '%s'", node.InputPath)
}
if (colConfig.URL == nil || *colConfig.URL == "") &&
(colConfig.Directory == nil) {
helper.Log.Panicf("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)
if err != nil {
helper.Log.Panicf("invalid template string for Collection Element.URL in '%s': %s", node.InputPath, err)
}
errSrcText = "URL " + url
cacheKey = url
if cacheEntry, ok := colCache[url]; ok {
colData = cacheEntry.data
cacheEntry.hit++
} else {
helper.Log.Noticef("reading collection from: %s", errSrcText)
colData = helper.JSONWebRequest(url)
colCache[url] = &colCacheEntry{
data: colData,
navnames: make([]string, 0),
}
}
} else {
path := node.ResolveInputPath(colConfig.Directory.Path)
errSrcText = "DIR " + path
helper.Log.Noticef("reading collection from: %s", errSrcText)
d, err := ioutil.ReadDir(path)
if err != nil {
helper.Log.Panicf("could not read directory '%s': %s", path, err)
}
mStr := "."
if colConfig.Directory.MatchFilename != "" {
mStr = colConfig.Directory.MatchFilename
}
matcher, err := regexp.Compile(mStr)
if err != nil {
helper.Log.Panicf("could not compile regex for MatchFilename '%s' in '%s': %s", mStr, path, err)
}
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)
if err != nil {
helper.Log.Panicf("could not read file '%s': %s", inFile, err)
}
_, 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 {
helper.Log.Debug(spew.Sdump(colDataMap))
helper.Log.Panicf("invalid json data in [%s] from '%s' for entries", navT.EntriesAttribute, errSrcText)
}
}
} else {
entries, ok = colData.([]interface{})
}
if !ok {
helper.Log.Debug(spew.Sdump(colData))
helper.Log.Panicf("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)
if err != nil {
helper.Log.Panicf("could not merge context in '%s': %s", node.InputPath, err)
}
var jsonCtx map[string]interface{}
if jsonCtx, ok = colEl.(map[string]interface{}); !ok {
helper.Log.Debug(spew.Sdump(colEl))
helper.Log.Panicf("no json object for entry index %d from '%s'", idx, errSrcText)
}
err = helper.Merge(&ctxE, pongo2.Context(jsonCtx))
if err != nil {
helper.Log.Panicf("could not merge context in '%s': %s", node.InputPath, err)
}
tpl := ""
if navT.Template != "" {
tpl, err = pongo2.RenderTemplateString(navT.Template, ctxE)
if err != nil {
helper.Log.Panicf("invalid template string for NavTemplate.Template in '%s': %s", node.InputPath, err)
}
}
if tpl == "" {
tpl = *node.Config.Template
}
dataKey := ""
if navT.DataKey != "" {
dataKey, err = pongo2.RenderTemplateString(navT.DataKey, ctxE)
if err != nil {
helper.Log.Panicf("invalid template string for NavTemplate.DataKey in '%s': %s", node.InputPath, err)
}
}
goTo, err := pongo2.RenderTemplateString(navT.GoTo, ctxE)
if err != nil {
helper.Log.Panicf("invalid template string for NavTemplate.GoTo in '%s': %s", node.InputPath, err)
}
goTo = strings.Trim(goTo, "/")
goTo = path.Clean(goTo)
if strings.Contains(goTo, "..") {
helper.Log.Panicf("going back via .. in NavTemplate.GoTo forbidden in collection config in '%s': %s", node.InputPath, goTo)
}
if goTo == "." {
helper.Log.Panicf("invalid config '.' for NavTemplate.GoTo in collection config in '%s'", node.InputPath)
}
if goTo == "" {
helper.Log.Panicf("missing NavTemplate.GoTo in collection config in '%s'", node.InputPath)
}
navname := ""
if navT.Navname != "" {
navname, err = pongo2.RenderTemplateString(navT.Navname, ctxE)
if err != nil {
helper.Log.Panicf("invalid template string for NavTemplate.Navname in '%s': %s", node.InputPath, err)
}
}
body := ""
if navT.Body != "" {
body, err = pongo2.RenderTemplateString(navT.Body, ctxE)
if err != nil {
helper.Log.Panicf("invalid template string for NavTemplate.Body in '%s': %s", node.InputPath, err)
}
}
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
helper.Log.Panicf("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)
}
}
}
}

84
pkg/mark2web/compress.go Normal file
View File

@@ -0,0 +1,84 @@
package mark2web
import (
"compress/gzip"
"io"
"io/ioutil"
"os"
"path"
"gitbase.de/apairon/mark2web/pkg/helper"
)
func handleCompression(filename string, content []byte) {
ThreadStart(func() {
if _, ok := Config.Compress.Extensions[path.Ext(filename)]; ok {
if Config.Compress.Brotli {
handleBrotliCompression(filename, content)
}
if Config.Compress.GZIP {
gzFilename := filename + ".gz"
helper.Log.Infof("writing to compressed output file: %s", gzFilename)
f, err := os.Create(gzFilename)
if err != nil {
helper.Log.Panicf("could not create file '%s': %s", gzFilename, err)
}
defer f.Close()
zw, err := gzip.NewWriterLevel(f, gzip.BestCompression)
if err != nil {
helper.Log.Panicf("could not initialize gzip writer for '%s': %s", filename, err)
}
defer zw.Close()
if content != nil {
// content given
_, err = zw.Write(content)
if err != nil {
helper.Log.Panicf("could not write gziped content for '%s': %s", filename, err)
}
} else {
// read file
r, err := os.Open(filename)
if err != nil {
helper.Log.Panicf("could not open file '%s': %s", filename, err)
}
defer r.Close()
_, err = io.Copy(zw, r)
if err != nil {
helper.Log.Panicf("could not gzip file '%s': %s", filename, err)
}
}
}
}
})
}
func compressFilesInDir(dir string) {
helper.Log.Noticef("compressing configured files in: %s", dir)
var _processDir func(string)
_processDir = func(d string) {
entries, err := ioutil.ReadDir(d)
if err != nil {
helper.Log.Panicf("could not read dir '%s': %s", d, err)
}
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 ( import (
"io/ioutil" "io/ioutil"
@@ -13,6 +13,7 @@ type GlobalConfig struct {
} `yaml:"Webserver"` } `yaml:"Webserver"`
Assets struct { Assets struct {
Compress bool `yaml:"Compress"`
FromPath string `yaml:"FromPath"` FromPath string `yaml:"FromPath"`
ToPath string `yaml:"ToPath"` ToPath string `yaml:"ToPath"`
Action string `yaml:"Action"` Action string `yaml:"Action"`
@@ -26,20 +27,28 @@ type GlobalConfig struct {
Action string `yaml:"Action"` Action string `yaml:"Action"`
} `yaml:"OtherFiles"` } `yaml:"OtherFiles"`
Compress struct {
Brotli bool `yaml:"Brotli"`
GZIP bool `yaml:"GZIP"`
Extensions map[string]string `yaml:"Extensions"`
} `yaml:"Compress"`
Directories struct { Directories struct {
Input string Input string
Output string Output string
} }
} }
// Config is global config
var Config = new(GlobalConfig) var Config = new(GlobalConfig)
func ReadGlobalConfig(filename string) error { // ReadFromFile reads yaml config from file
func (c *GlobalConfig) ReadFromFile(filename string) error {
data, err := ioutil.ReadFile(filename) data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return err return err
} }
err = yaml.Unmarshal(data, Config) err = yaml.Unmarshal(data, c)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,60 +1,19 @@
package config package mark2web
import ( import "gitbase.de/apairon/mark2web/pkg/helper"
"fmt"
"reflect"
"github.com/imdario/mergo" // CollectionDirectoryConfig specifies how to handle a directory of markdown files as a collection
) type CollectionDirectoryConfig struct {
Path string `yaml:"Path"`
// MapString is a map[string]interface{} which always unmarsahls yaml to map[string]interface{} MatchFilename string `yaml:"MatchFilename"`
type MapString map[string]interface{} ReverseOrder bool `yaml:"ReverseOrder"`
// UnmarshalYAML handles all maps as map[string]interface{} for later JSON
// see https://github.com/elastic/beats/blob/6435194af9f42cbf778ca0a1a92276caf41a0da8/libbeat/common/mapstr.go
func (ms *MapString) UnmarshalYAML(unmarshal func(interface{}) error) error {
var result map[interface{}]interface{}
err := unmarshal(&result)
if err != nil {
return err
}
*ms = cleanUpInterfaceMap(result)
return nil
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) MapString {
result := make(MapString)
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
case string, bool, int, int8, int16, int32, int64, float32, float64:
return v
default:
return fmt.Sprintf("%v", v)
}
} }
// CollectionConfig describes a collection // CollectionConfig describes a collection
type CollectionConfig struct { type CollectionConfig struct {
Name *string `yaml:"Name"` Name *string `yaml:"Name"`
URL *string `yaml:"URL"` URL *string `yaml:"URL"`
Directory *CollectionDirectoryConfig `yaml:"Directory"`
NavTemplate *struct { NavTemplate *struct {
EntriesAttribute string `yaml:"EntriesAttribute"` EntriesAttribute string `yaml:"EntriesAttribute"`
GoTo string `yaml:"GoTo"` GoTo string `yaml:"GoTo"`
@@ -72,7 +31,7 @@ type ThisPathConfig struct {
Navname *string `yaml:"Navname"` Navname *string `yaml:"Navname"`
GoTo *string `yaml:"GoTo"` GoTo *string `yaml:"GoTo"`
Collections []*CollectionConfig `yaml:"Collections"` Collections []*CollectionConfig `yaml:"Collections"`
Data MapString `yaml:"Data"` Data helper.MapString `yaml:"Data"`
} }
// IndexConfig describes index input and output file // IndexConfig describes index input and output file
@@ -116,8 +75,9 @@ type ImagingConfig struct {
Anchor string `yaml:"Anchor"` Anchor string `yaml:"Anchor"`
Quality int `yaml:"Quality"` Quality int `yaml:"Quality"`
Filename string `yaml:"-"` TargetDir string `yaml:"-"`
Format string `yaml:"-"` Filename string `yaml:"-"`
Format string `yaml:"-"`
} }
// PathConfig of subdir // PathConfig of subdir
@@ -131,41 +91,8 @@ type PathConfig struct {
Markdown *MarkdownConfig `yaml:"Markdown"` Markdown *MarkdownConfig `yaml:"Markdown"`
Imaging *ImagingConfig `yaml:"Imaging"` Imaging *ImagingConfig `yaml:"Imaging"`
Data MapString `yaml:"Data"` Data helper.MapString `yaml:"Data"`
}
// Collections here are recursive if saved as nav, so request should be filtered
// PathConfigTree is complete config tree of content dir Collections []*CollectionConfig `yaml:"Collections"`
type PathConfigTree struct {
InputPath string
OutputPath string
Hidden bool // for collections which are not part of the navigation
ColMap MapString
InputFiles []string
OtherFiles []string
Config *PathConfig
Sub []*PathConfigTree
}
type ptrTransformer struct{}
func (t ptrTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ.Kind() == reflect.Ptr {
return func(dst, src reflect.Value) error {
if dst.CanSet() {
if dst.IsNil() {
dst.Set(src)
}
}
return nil
}
}
return nil
}
// Merge merges 2 objects or maps
func Merge(dst, src interface{}) error {
return mergo.Merge(dst, src, mergo.WithTransformers(ptrTransformer{}))
} }

301
pkg/mark2web/content.go Normal file
View File

@@ -0,0 +1,301 @@
package mark2web
import (
"bytes"
"io/ioutil"
"path"
"regexp"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
"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) {
if node.root == nil {
// first node is root
node.root = node
}
node.fillConfig(inBase, outBase, dir, conf)
files, err := ioutil.ReadDir(node.InputPath)
if err != nil {
helper.Log.Panic(err)
}
// 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":
helper.Log.Debugf(".MD %s", p)
if node.InputFiles == nil {
node.InputFiles = make([]string, 0)
}
node.InputFiles = append(node.InputFiles, f.Name())
break
default:
helper.Log.Debugf("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() {
helper.Log.Debugf("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)
helper.Log.Debugf("found yaml header in '%s', merging config", errorRef)
err := yaml.Unmarshal(yamlData, newConfig)
if err != nil {
helper.Log.Panicf("could not parse YAML header from '%s': %s", errorRef, err)
}
helper.Log.Debug("merging config with upper config")
oldThis := newConfig.This
helper.Merge(newConfig, node.Config)
newConfig.This = oldThis
helper.Log.Debug(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() {
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)
if err != nil {
helper.Log.Panicf("could not compile filename.ignore regexp '%s' for file '%s': %s", *ignoreRegex, inFile, err)
}
ignoreFile = regex.MatchString(file)
}
}
if ignoreFile {
helper.Log.Infof("ignoring file '%s', because of filename.ignore", inFile)
} else {
var input []byte
if file != "" {
helper.Log.Debugf("reading file: %s", inFile)
var err error
input, err = ioutil.ReadFile(inFile)
if err != nil {
helper.Log.Panicf("could not read '%s':%s", inFile, err)
}
helper.Log.Infof("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 {
helper.Log.Debugf("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)
if err != nil {
helper.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 := node.OutputPath + "/" + outputFilename
helper.Log.Debugf("using '%s' as output file", outFile)
helper.Log.Debugf("rendering template '%s' for '%s'", *newConfig.Template, outFile)
templateFilename := *newConfig.Template
result, err := renderTemplate(*newConfig.Template, node, newConfig, ctx)
if err != nil {
helper.Log.Panicf("could not execute template '%s' for input file '%s': %s", templateFilename, inFile, err)
}
result = node.fixAssetsPath(result)
helper.Log.Noticef("writing to output file: %s", outFile)
err = ioutil.WriteFile(outFile, []byte(result), 0644)
if err != nil {
helper.Log.Panicf("could not write to output file '%s': %s", outFile, err)
}
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
helper.Log.Noticef("copying file from '%s' to '%s'", from, to)
err := cpy.Copy(from, to)
if err != nil {
helper.Log.Panicf("could not copy file from '%s' to '%s': %s", from, to, err)
}
handleCompression(to, nil)
}
}
i := 0
// sub can dynamically increase, so no for range
for i < len(node.Sub) {
node.Sub[i].ProcessContent()
i++
}
}

170
pkg/mark2web/context.go Normal file
View File

@@ -0,0 +1,170 @@
package mark2web
import (
"io/ioutil"
"os"
"path"
"regexp"
"strings"
"time"
"gitbase.de/apairon/mark2web/pkg/helper"
"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
}
helper.Log.Infof("reading input directory: %s", inPath)
node.InputPath = inPath
// read config
newConfig := new(PathConfig)
helper.Log.Debug("looking for config.yml ...")
configFile := inPath + "/config.yml"
if _, err := os.Stat(configFile); os.IsNotExist(err) {
helper.Log.Debug("no config.yml found in this directory, using upper configs")
helper.Merge(newConfig, conf)
// remove this
newConfig.This = ThisPathConfig{}
} else {
helper.Log.Debug("reading config...")
data, err := ioutil.ReadFile(configFile)
if err != nil {
helper.Log.Panicf("could not read file '%s': %s", configFile, err)
}
err = yaml.Unmarshal(data, newConfig)
if err != nil {
helper.Log.Panicf("could not parse YAML file '%s': %s", configFile, err)
}
helper.Log.Debug("merging config with upper config")
oldThis := newConfig.This
helper.Merge(newConfig, conf)
newConfig.This = oldThis
helper.Log.Debug(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 {
helper.Log.Panicf("error compiling path.strip regex '%s' from '%s': %s", *regexStr, inBase+"/"+subDir, 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)
helper.Log.Infof("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) {
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)
if err != nil {
helper.Log.Panicf("merge of path config failed: %s", err)
}
// dont merge Data[DataKey]
if dataMapKey != "" {
mergedConfig.Data[dataMapKey] = nil
} else {
mergedConfig.Data = make(helper.MapString)
}
err = helper.Merge(mergedConfig, newPathConfig)
if err != nil {
helper.Log.Panicf("merge of path config failed: %s", err)
}
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/helper"
"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(helper.JSONWebRequest(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)
}

100
pkg/mark2web/htaccess.go Normal file
View File

@@ -0,0 +1,100 @@
package mark2web
import (
"io/ioutil"
"regexp"
"strings"
"gitbase.de/apairon/mark2web/pkg/helper"
)
func htaccessRedirect(outDir, goTo string) {
switch Config.Webserver.Type {
case "apache":
htaccessFile := outDir + "/.htaccess"
helper.Log.Noticef("writing '%s' with redirect to: %s", htaccessFile, goTo)
err := ioutil.WriteFile(htaccessFile, []byte(`RewriteEngine on
RewriteRule ^$ %{REQUEST_URI}`+goTo+`/ [R,L]
`), 0644)
if err != nil {
helper.Log.Panicf("could not write '%s': %s", htaccessFile, err)
}
}
}
// 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"
helper.Log.Noticef("writing webserver config to: %s", htaccessFile)
err := ioutil.WriteFile(htaccessFile, []byte(configStr), 0644)
if err != nil {
helper.Log.Panicf("could not write '%s': %s", htaccessFile, err)
}
}
}
}

View File

@@ -1,11 +1,11 @@
package helper package mark2web
import ( import (
"path" "path"
"regexp" "regexp"
"strings" "strings"
"gitbase.de/apairon/mark2web/config" "gitbase.de/apairon/mark2web/pkg/helper"
) )
// NavElement is one element with ist attributes and subs // NavElement is one element with ist attributes and subs
@@ -14,19 +14,23 @@ type NavElement struct {
GoTo string GoTo string
Active bool Active bool
ColMap config.MapString ColMap helper.MapString
Data interface{} Data interface{}
This config.ThisPathConfig This ThisPathConfig
SubMap *map[string]*NavElement SubMap *map[string]*NavElement
SubSlice *[]*NavElement SubSlice *[]*NavElement
} }
// BuildNavigation builds the navigation trees for use in templates // buildNavigation builds the navigation trees for use in templates
func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string) { func (node *TreeNode) buildNavigation(curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement) {
for _, el := range conf.Sub { buildNavigationRecursive(node.root, curNavMap, curNavSlice, navActive, node.CurrentNavPath(), node.BackToRootPath())
}
func buildNavigationRecursive(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string, backToRoot string) {
for _, el := range tree.Sub {
if el.Hidden { if el.Hidden {
continue // ignore hidden nav points from collections continue // ignore hidden nav points from collections
} }
@@ -38,15 +42,15 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
if ignNav != nil && *ignNav != "" { if ignNav != nil && *ignNav != "" {
regex, err := regexp.Compile(*ignNav) regex, err := regexp.Compile(*ignNav)
if err != nil { if err != nil {
Log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err) helper.Log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err)
} }
if regex.MatchString(path.Base(el.InputPath)) { if regex.MatchString(path.Base(el.InputPath)) {
Log.Debugf("ignoring input directory '%s' in navigation", el.InputPath) helper.Log.Debugf("ignoring input directory '%s' in navigation", el.InputPath)
continue continue
} }
} }
elPath := strings.TrimPrefix(el.OutputPath, config.Config.Directories.Output+"/") elPath := strings.TrimPrefix(el.OutputPath, Config.Directories.Output+"/")
subMap := make(map[string]*NavElement) subMap := make(map[string]*NavElement)
subSlice := make([]*NavElement, 0) subSlice := make([]*NavElement, 0)
@@ -88,8 +92,7 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
if activeNav != "" && activeNav != "/" { if activeNav != "" && activeNav != "/" {
// calculate relative path // calculate relative path
bToRoot := BackToRoot(activeNav) navEl.GoTo = backToRoot + navEl.GoTo
navEl.GoTo = bToRoot + navEl.GoTo
navEl.GoTo = path.Clean(navEl.GoTo) navEl.GoTo = path.Clean(navEl.GoTo)
} }
@@ -98,6 +101,6 @@ func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElem
*curNavSlice = append(*curNavSlice, &navEl) *curNavSlice = append(*curNavSlice, &navEl)
} }
BuildNavigation(el, &subMap, &subSlice, navActive, activeNav) buildNavigationRecursive(el, &subMap, &subSlice, navActive, activeNav, backToRoot)
} }
} }

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

16
pkg/mark2web/run.go Normal file
View File

@@ -0,0 +1,16 @@
package mark2web
// 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()
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
}

60
pkg/mark2web/wait.go Normal file
View File

@@ -0,0 +1,60 @@
package mark2web
import (
"runtime"
"sync"
"gitbase.de/apairon/mark2web/pkg/helper"
)
var wg sync.WaitGroup
var numCPU = runtime.NumCPU()
var curNumThreads = 1 // main thread is 1
func init() {
helper.Log.Infof("number of CPU core: %d", numCPU)
}
// Wait will wait for all our internal go threads
func Wait() {
wg.Wait()
}
// ThreadSetup adds 1 to wait group
func ThreadSetup() {
curNumThreads++
wg.Add(1)
}
// ThreadDone removes 1 from wait group
func ThreadDone() {
curNumThreads--
wg.Done()
}
// ThreadStart will start a thread an manages the wait group
func ThreadStart(f func(), forceNewThread ...bool) {
force := false
if len(forceNewThread) > 0 && forceNewThread[0] {
force = true
}
if numCPU > curNumThreads || force {
// only new thread if empty CPU core available or forced
threadF := func() {
f()
ThreadDone()
}
ThreadSetup()
go threadF()
} else {
helper.Log.Debugf("no more CPU core (%d used), staying in main thread", curNumThreads)
f()
}
}
// SetNumCPU is for testing package without threading
func SetNumCPU(i int) {
numCPU = i
}

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 GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog
?sort[date]=-1 ?sort[date]=-1
&limit=101 &limit=101
&token=89ff216524093123bf7a0a10f7b273 &token=985cee34099f4d3b08f18fc22f6296
&filter[link][$exists]=0
### ###

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

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

View File

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

View File

@@ -10,23 +10,16 @@ Data:
--- ---
# Installation # Installation
Damit die korrekten Versionsinformationen dynamisch in das finale mark2web-Binary eingefügt wurde, ist eine manuelle Installation aus dem Git-Repository sinnvoll.
Da die benötigten Pakete über die Go "vendor"-Funktionalität eingebunden sind ist ein `git submodule --init --recursive` nötig, wie im folgenden Abschnitt zu sehen ist:
```sh ```sh
mkdir -p $GOPATH/src/gitbase.de/apairon go get -v gitbase.de/apairon/mark2web/cmd/mark2web
git clone https://gitbase.de/apairon/mark2web.git $GOPATH/src/gitbase.de/apairon/mark2web
cd $GOPATH/src/gitbase.de/apairon/mark2web # setze Versioninformationen ins Binary
git submodule update --init --recursive pkg=$GOPATH/src/gitbase.de/apairon/mark2web
go install -v -ldflags "-X main.Version=`cat $pkg/build/VERSION` -X main.GitHash=`git --git-dir $pkg/.git rev-parse HEAD` -X main.BuildTime=`date -u '+%Y-%m-%d_%I:%M:%S%p'`" gitbase.de/apairon/mark2web/cmd/mark2web
./build.sh
``` ```
--- ---
Eine Installation über `go install gitbase.de/apairon/mark2web` wird derzeit noch nicht unterstützt, da dabei die Informationen für `mark2web -version` nicht generiert werden.
## Releases ## Releases
Vorkompilierte Binaries finden Sie auf der [Releases-Seite auf gitbase.de](https://gitbase.de/apairon/mark2web/releases). Vorkompilierte Binaries finden Sie auf der [Releases-Seite auf gitbase.de](https://gitbase.de/apairon/mark2web/releases).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

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

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

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
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.

View File

@@ -0,0 +1,8 @@
---
Data:
Version: "ab v1.0"
---
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).

View File

@@ -0,0 +1,17 @@
---
Data:
Version: "ab v1.0"
---
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.

View File

@@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
`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

@@ -0,0 +1,9 @@
This:
Data:
teaser: Globale Konfiguration und individuelle Content-Einstellungen
Collections:
- Name: doccoll
Directory:
Path: "."
MatchFilename: "^_\\d+(?P<lowdash>_*)(?P<title>.+)\\.md"
ReverseOrder: False

View File

@@ -1,23 +1,23 @@
This: #This:
Collections: Collections:
- Name: blog1st - Name: blog1st
URL: 'https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token={{ Data.token }}&filter[published]=true&sort[date]=-1&skip=0&limit=1' URL: 'https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token={{ Data.token }}&filter[published]=true&sort[date]=-1&skip=0&limit=1{% if Data.details._id %}&filter[link._id]={{ Data.details._id }}{% else %}&filter[link][$exists]=0{% endif %}'
NavTemplate: NavTemplate:
EntriesAttribute: entries EntriesAttribute: entries
GoTo: '{{ date }}-{{ title }}' GoTo: '{{ date }}-{{ title }}'
Navname: '{{ title }}' Navname: '{{ title }}'
Body: '{{ body }}' Body: '{{ body|safe }}'
Template: base_blog_details.html Template: base_blog_details.html
DataKey: details DataKey: details
Hidden: true # hide from nav, but use this feature for rendering detail sites Hidden: true # hide from nav, but use this feature for rendering detail sites
- Name: blog1skip - Name: blog1skip
URL: 'https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token={{ Data.token }}&filter[published]=true&sort[date]=-1&skip=1&limit=100' URL: 'https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token={{ Data.token }}&filter[published]=true&sort[date]=-1&skip=1&limit=100{% if Data.details._id %}&filter[link._id]={{ Data.details._id }}{% else %}&filter[link][$exists]=0{% endif %}'
NavTemplate: NavTemplate:
EntriesAttribute: entries EntriesAttribute: entries
GoTo: '{{ date }}-{{ title }}' GoTo: '{{ date }}-{{ title }}'
Navname: '{{ title }}' Navname: '{{ title }}'
Body: '{{ body }}' Body: '{{ body|safe }}'
Template: base_blog_details.html Template: base_blog_details.html
DataKey: details DataKey: details
Hidden: true Hidden: true

View File

@@ -0,0 +1,34 @@
# mark2web Lizenz
**mark2web** unterliegt der Lizenz "Simplified BSD License".
---
## Simplified BSD License
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

@@ -169,8 +169,8 @@ label {font-weight:600;}
#header { #header {
position:absolute; position:absolute;
top:50px; top:50px;
left:150px; left:0px;
right:150px; right:0px;
height:auto; height:auto;
z-index:300; z-index:300;
background:none; background:none;
@@ -272,6 +272,7 @@ pre {
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
margin-bottom: 25px; margin-bottom: 25px;
white-space: pre-wrap;
} }
code.language-mermaid { code.language-mermaid {
@@ -291,4 +292,14 @@ code.language-mermaid svg {
border-radius: 5px; border-radius: 5px;
display:inline-block; display:inline-block;
color: #fff; color: #fff;
}
.versionBadge {
text-align: right;
color: #444;
font-size: 10px;
padding: 3px;
background: #ddd;
border: #444 solid 1px;
border-radius: 5px;
} }

View File

@@ -1,6 +1,6 @@
$('#preloader').fadeIn('fast'); $('#preloader').fadeIn('fast');
$(window).on('load', function() { $(window).on('load', function() {
$('.spinner').fadeOut(); $('.spinner').fadeOut();
$('#preloader').delay(350).fadeOut('slow'); $('#preloader').delay(100).fadeOut('slow');
$('body').delay(350).css({'overflow':'visible'}); $('body').delay(100).css({'overflow':'visible'});
}); });

View File

@@ -56,10 +56,10 @@
{% for sl in Data.slider %} {% for sl in Data.slider %}
<div class="slide"> <div class="slide">
<img <img
src="{{ sl.img|image_process:"p=fill,w=1440,h=600,q=60" }}" src="{{ sl.img|image_process:"p=fill,w=1440,h=600,q=60,t=/img" }}"
srcset="{{ sl.img|image_process:"p=fill,w=768,h=384,q=60" }} 768w, srcset="{{ sl.img|image_process:"p=fill,w=768,h=384,q=60,t=/img" }} 768w,
{{ sl.img|image_process:"p=fill,w=1440,h=600,q=60" }} 1440w, {{ sl.img|image_process:"p=fill,w=1440,h=600,q=60,t=/img" }} 1440w,
{{ sl.img|image_process:"p=fill,w=1920,h=800,q=60" }} 1920w" {{ sl.img|image_process:"p=fill,w=1920,h=800,q=60,t=/img" }} 1920w"
alt="{{ sl.alt }}" style="opacity:{{ sl.opacity|default:1 }}"> alt="{{ sl.alt }}" style="opacity:{{ sl.opacity|default:1 }}">
</div> </div>
{% endfor %} {% endfor %}
@@ -103,10 +103,10 @@
<div class="maincontent"> <div class="maincontent">
{% if Data.background %} {% if Data.background %}
<img <img
src="{{ Data.background|image_process:"p=fill,w=1440,h=810,q=30" }}" src="{{ Data.background|image_process:"p=fill,w=1440,h=810,q=30,t=/img" }}"
srcset="{{ Data.background|image_process:"p=fill,w=768,h=768,q=30" }} 768w, srcset="{{ Data.background|image_process:"p=fill,w=768,h=768,q=30,t=/img" }} 768w,
{{ Data.background|image_process:"p=fill,w=1440,h=810,q=30" }} 1440w, {{ Data.background|image_process:"p=fill,w=1440,h=810,q=30,t=/img" }} 1440w,
{{ Data.background|image_process:"p=fill,w=1920,h=1020,q=30" }} 1920w" {{ Data.background|image_process:"p=fill,w=1920,h=1020,q=30,t=/img" }} 1920w"
alt="{{ Meta.Title }}" class="img2bg"> alt="{{ Meta.Title }}" class="img2bg">
{% endif %} {% endif %}
<div class="white_section section_padding"> <div class="white_section section_padding">

View File

@@ -7,7 +7,7 @@
{{ e.title }} {{ e.title }}
<div class="datum">{{ e.date|datum }}</div> <div class="datum">{{ e.date|datum }}</div>
</h2> </h2>
{{ e.teaser|markdown }} {{ e.teaser|markdown:"s=monokai" }}
{% if e.body %} {% if e.body %}
<a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a> <a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a>
{% endif %} {% endif %}
@@ -23,7 +23,7 @@
{{ e.title }} {{ e.title }}
<div class="datum">{{ e.date|datum }}</div> <div class="datum">{{ e.date|datum }}</div>
</h2> </h2>
{{ e.teaser|markdown }} {{ e.teaser|markdown:"s=monokai" }}
{% if e.body %} {% if e.body %}
<a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a> <a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a>
{% endif %} {% endif %}

Some files were not shown because too many files have changed in this diff Show More