48 Commits

Author SHA1 Message Date
Sebastian Frank
9f45010228 prepared release 1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-12 13:44:48 +01:00
Sebastian Frank
5f8e267bbf Merge branch 'master' of ssh://gitbase.de:2222/apairon/mark2web
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-12 13:25:34 +01:00
Sebastian Frank
6ad0bb6ed9 fixes #8, -out ./foobar 2019-03-12 13:25:17 +01:00
Sebastian Frank
7bc7d50c0c fixes #9, -out ./foobar
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-12 13:24:38 +01:00
Sebastian Frank
de7931acda closes #9, Timestamp variable in template context
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-12 13:16:37 +01:00
Sebastian Frank
4041868a9f collection URL instead of EntriesJSON vi fnRequest
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-11 18:56:29 +01:00
Sebastian Frank
6c3f985d1f slider for blog details 2019-03-11 16:38:44 +01:00
Sebastian Frank
2f2454ee54 json and dump filter 2019-03-11 15:29:05 +01:00
Sebastian Frank
a2eaa3f4b4 meta in blog details
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-10 16:41:56 +01:00
Sebastian Frank
9c0d959181 dont read local image file if target exists 2019-03-10 16:36:20 +01:00
Sebastian Frank
567cd1646e Merge branch 'collections' of ssh://gitbase.de:2222/apairon/mark2web into collections
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-10 16:13:56 +01:00
Sebastian Frank
38820e6baf collections as one navigation level 2019-03-10 16:02:53 +01:00
Sebastian Frank
e3dfbcb591 collections via config 2019-03-10 13:26:26 +01:00
Sebastian Frank
7f9910244b webhook test 2019-03-10 11:02:51 +01:00
Sebastian Frank
7fa0c67f6f top img in website
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-08 15:20:10 +01:00
Sebastian Frank
a8ad0f0b59 no debug output on deploy website
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-06 15:50:45 +01:00
Sebastian Frank
09c6176ea7 .drone.yml empty target for push
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-06 12:45:15 +01:00
Sebastian Frank
3f4ab3b9c8 target in .drone.ci
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-06 12:41:21 +01:00
Sebastian Frank
6c59e6684f rest test
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-04 17:15:37 +01:00
Sebastian Frank
aa6ade5657 drone promote test
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-04 16:43:46 +01:00
Sebastian Frank
c4e6a2f409 finished image_process filter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2019-03-04 16:05:29 +01:00
Sebastian Frank
3a467134b3 image_resize filter
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-03 13:25:00 +01:00
Sebastian Frank
15af8e487c changed cms url
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-02 17:50:26 +01:00
Sebastian Frank
cc5870dcec webhook test
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-02 15:07:13 +01:00
Sebastian Frank
6e3688d713 motto and otto for nodejs module syntax in custom filters
Some checks failed
continuous-integration/drone/push Build is failing
2019-03-02 14:45:08 +01:00
Sebastian Frank
8e84901465 fixed run, if templates/filters is missing
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 18:41:22 +01:00
Sebastian Frank
079534ab71 fixed #7, javascript filter for pongo2 with context in this variable
Some checks failed
continuous-integration/drone/push Build is failing
2019-02-28 18:36:36 +01:00
Sebastian Frank
9e2f16dde9 website css modified
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 16:25:50 +01:00
Sebastian Frank
3726be5b58 added ca-certificates to docker image
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 15:45:11 +01:00
Sebastian Frank
cb55dcd42b cleaned spew output
Some checks failed
continuous-integration/drone/push Build is failing
2019-02-28 15:40:06 +01:00
Sebastian Frank
5acc4083aa added body to fnRender 2019-02-28 15:13:59 +01:00
Sebastian Frank
e943271561 render paths via fnRender correct
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 14:14:31 +01:00
Sebastian Frank
d78ecf4682 reorganizes processing code
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 12:13:28 +01:00
Sebastian Frank
39f1932cc3 generate details sites from fnRender
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-28 10:43:30 +01:00
Sebastian Frank
650bdc2fd6 reorganized code
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-27 17:33:26 +01:00
Sebastian Frank
8f1345d4aa fnRequest, pongo2-addons added
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-27 15:58:10 +01:00
Sebastian Frank
4c2a13d6b5 added matomo opt-out on website
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-24 19:50:54 +01:00
Sebastian Frank
4e12a6e6e3 added website preloader
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-24 19:40:00 +01:00
Sebastian Frank
e99b4326a8 fixed #8, removed CR from input markdown
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-24 19:12:31 +01:00
Sebastian Frank
9f499ea1de matomo siteId in config.yml
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-21 18:29:01 +01:00
Sebastian Frank
0aefb5c758 matomo tracking on website
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-21 18:10:05 +01:00
Sebastian Frank
bd3d04b061 center svg on website
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 15:29:14 +01:00
Sebastian Frank
a36b0e21c9 hide mermaid code on website
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 15:10:43 +01:00
Sebastian Frank
1f71e0467f Blog on website
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 13:38:29 +01:00
Sebastian Frank
9d815a2a9b fixed website template https
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 13:17:22 +01:00
Sebastian Frank
8d0f8b40bf rsync website in deploy step
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 13:15:25 +01:00
Sebastian Frank
232902ea63 rsync website in deploy step
Some checks failed
continuous-integration/drone/push Build is failing
2019-02-20 13:10:09 +01:00
Sebastian Frank
e79a527ac6 vscode run on save setting
All checks were successful
continuous-integration/drone/push Build is passing
2019-02-20 12:57:40 +01:00
66 changed files with 2030 additions and 705 deletions

View File

@@ -16,12 +16,16 @@ steps:
- git submodule update --init --recursive - git submodule update --init --recursive
- git fetch --tags - git fetch --tags
- ./build.sh - ./build.sh
when:
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 - ./dist/mark2web-`cat VERSION`-linux-amd64 -version
- ./dist/mark2web-`cat VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug - ./dist/mark2web-`cat VERSION`-linux-amd64 -in example -out example_out -create -logLevel debug
when:
event: [ push, tag ]
- name: build for freebsd - name: build for freebsd
image: golang:latest image: golang:latest
@@ -72,12 +76,22 @@ steps:
- name: deploy website - name: deploy website
image: apairon/mark2web:latest image: apairon/mark2web:latest
pull: never pull: never
environment:
RSYNC_PASS:
from_secret: rsync_pass
commands: commands:
- /mark2web -version - /mark2web -version
- /mark2web -in website -out html -create -logLevel debug - /mark2web -in website -out html -create -logLevel info
- '
rsync -rlcgD -i -u -v --stats
--delete
-e "sshpass -p $${RSYNC_PASS} ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 22222"
html/
basiskonfiguration_mark2web_rsync@deploy.bc1.basehosts.de:./'
when: when:
branch: [ master ] branch: [ master ]
event: [ push ] event: [ promote, push ]
target: [ "", website ]
- name: prepare release - name: prepare release
image: apairon/mark2web:latest image: apairon/mark2web:latest

39
.gitmodules vendored
View File

@@ -25,9 +25,6 @@
[submodule "vendor/github.com/davecgh/go-spew"] [submodule "vendor/github.com/davecgh/go-spew"]
path = vendor/github.com/davecgh/go-spew path = vendor/github.com/davecgh/go-spew
url = https://github.com/davecgh/go-spew url = https://github.com/davecgh/go-spew
[submodule "vendor/github.com/aymerick/raymond"]
path = vendor/github.com/aymerick/raymond
url = https://github.com/aymerick/raymond
[submodule "vendor/github.com/imdario/mergo"] [submodule "vendor/github.com/imdario/mergo"]
path = vendor/github.com/imdario/mergo path = vendor/github.com/imdario/mergo
url = https://github.com/imdario/mergo url = https://github.com/imdario/mergo
@@ -40,9 +37,33 @@
[submodule "vendor/github.com/juju/errors"] [submodule "vendor/github.com/juju/errors"]
path = vendor/github.com/juju/errors path = vendor/github.com/juju/errors
url = https://github.com/juju/errors url = https://github.com/juju/errors
[submodule "vendor/github.com/gosimple/slug"] [submodule "vendor/github.com/flosch/pongo2-addons"]
path = vendor/github.com/gosimple/slug path = vendor/github.com/flosch/pongo2-addons
url = https://github.com/gosimple/slug url = https://github.com/flosch/pongo2-addons
[submodule "vendor/github.com/rainycape/unidecode"] [submodule "vendor/github.com/extemporalgenome/slug"]
path = vendor/github.com/rainycape/unidecode path = vendor/github.com/extemporalgenome/slug
url = https://github.com/rainycape/unidecode url = https://github.com/extemporalgenome/slug
[submodule "vendor/golang.org/x/text"]
path = vendor/golang.org/x/text
url = https://go.googlesource.com/text
[submodule "vendor/github.com/flosch/go-humanize"]
path = vendor/github.com/flosch/go-humanize
url = https://github.com/flosch/go-humanize
[submodule "vendor/github.com/russross/blackfriday"]
path = vendor/github.com/russross/blackfriday
url = https://github.com/russross/blackfriday
[submodule "vendor/github.com/robertkrimen/otto"]
path = vendor/github.com/robertkrimen/otto
url = https://github.com/robertkrimen/otto
[submodule "vendor/gopkg.in/sourcemap.v1"]
path = vendor/gopkg.in/sourcemap.v1
url = https://gopkg.in/sourcemap.v1
[submodule "vendor/github.com/ddliu/motto"]
path = vendor/github.com/ddliu/motto
url = https://github.com/ddliu/motto
[submodule "vendor/github.com/disintegration/imaging"]
path = vendor/github.com/disintegration/imaging
url = https://github.com/disintegration/imaging
[submodule "vendor/golang.org/x/image"]
path = vendor/golang.org/x/image
url = https://go.googlesource.com/image

View File

@@ -1,5 +1,14 @@
{ {
"files.associations": { "files.associations": {
"**/templates/*.html": "django-html" "**/templates/*.html": "django-html"
},
"saveAndRun": {
"commands": [
{
"match": "website/.*",
"cmd": "time mark2web -in ${workspaceRoot}/website -out ${workspaceRoot}/html -create",
"silent": false
}
]
} }
} }

View File

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

View File

@@ -1,8 +1,16 @@
Dies ist das erste Release von *mark2web*. NEUERUNGEN:
In diesem Release werden folgende Features unterstützt:
- Verabeitung von Markdown-Dateien für Website-Inhalte - pongo2-addons standardmäßig eingebunden
- Umwandlung der Ordnerstruktur des `content`-Verzeichnis in Navigationsbäume - fnRequest Template-Funktion für Web-Requests
- Verarbeitung von Pongo2-Templates mit dem Inhalt zur finalen Website - fnRender Template-Funktion um Unterseiten zu rendern
- Kopieren der Assets ins Zielverzeichnis - eigene Filter via Javascript
- Anpassung des Asset-Pfads in den HTML-Dateien - 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 +1 @@
1.0.0 1.1.0

47
config/global.go Normal file
View File

@@ -0,0 +1,47 @@
package config
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
// GlobalConfig is config which is used only once in root dir
type GlobalConfig struct {
Webserver struct {
Type string `yaml:"Type"`
} `yaml:"Webserver"`
Assets struct {
FromPath string `yaml:"FromPath"`
ToPath string `yaml:"ToPath"`
Action string `yaml:"Action"`
FixTemplate struct {
Find string `yaml:"Find"`
Replace string `yaml:"Replace"`
} `yaml:"FixTemplate"`
} `yaml:"Assets"`
OtherFiles struct {
Action string `yaml:"Action"`
} `yaml:"OtherFiles"`
Directories struct {
Input string
Output string
}
}
var Config = new(GlobalConfig)
func ReadGlobalConfig(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
err = yaml.Unmarshal(data, Config)
if err != nil {
return err
}
return nil
}

171
config/tree.go Normal file
View File

@@ -0,0 +1,171 @@
package config
import (
"fmt"
"reflect"
"github.com/imdario/mergo"
)
// MapString is a map[string]interface{} which always unmarsahls yaml to map[string]interface{}
type MapString map[string]interface{}
// UnmarshalYAML handles all maps as map[string]interface{} for later JSON
// see https://github.com/elastic/beats/blob/6435194af9f42cbf778ca0a1a92276caf41a0da8/libbeat/common/mapstr.go
func (ms *MapString) UnmarshalYAML(unmarshal func(interface{}) error) error {
var result map[interface{}]interface{}
err := unmarshal(&result)
if err != nil {
return err
}
*ms = cleanUpInterfaceMap(result)
return nil
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) MapString {
result := make(MapString)
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
case string, bool, int, int8, int16, int32, int64, float32, float64:
return v
default:
return fmt.Sprintf("%v", v)
}
}
// CollectionConfig describes a collection
type CollectionConfig struct {
Name *string `yaml:"Name"`
URL *string `yaml:"URL"`
NavTemplate *struct {
EntriesAttribute string `yaml:"EntriesAttribute"`
GoTo string `yaml:"GoTo"`
Navname string `yaml:"Navname"`
Body string `yaml:"Body"`
DataKey string `yaml:"DataKey"`
Hidden bool `yaml:"Hidden"`
Template string `yaml:"Template"`
//Recursive bool `yaml:"Recursive"`
} `yaml:"NavTemplate"`
}
// ThisPathConfig is struct for This in paths yaml
type ThisPathConfig struct {
Navname *string `yaml:"Navname"`
GoTo *string `yaml:"GoTo"`
Collections []*CollectionConfig `yaml:"Collections"`
Data MapString `yaml:"Data"`
}
// IndexConfig describes index input and output file
type IndexConfig struct {
InputFile *string `yaml:"InputFile"`
InputString *string `yaml:"InputString"`
OutputFile *string `yaml:"OutputFile"`
}
// MetaData describes meta data for current site/tree node
type MetaData struct {
Title *string `yaml:"Title"`
Description *string `yaml:"Description"`
Keywords *string `yaml:"Keywords"`
}
// DirnameConfig describes how to handle directory names
type DirnameConfig struct {
Strip *string `yaml:"Strip"`
IgnoreForNav *string `yaml:"IgnoreForNav"`
}
// FilenameConfig describes how to handle filenames
type FilenameConfig struct {
Strip *string `yaml:"Strip"`
Ignore *string `yaml:"Ignore"`
OutputExtension *string `yaml:"OutputExtension"`
}
// MarkdownConfig describes markdown handling
type MarkdownConfig struct {
ChromaRenderer *bool `yaml:"ChromaRenderer"`
ChromaStyle *string `yaml:"ChromaStyle"`
}
// ImagingConfig defines parameter for imaging processing
type ImagingConfig struct {
Width int `yaml:"Width"`
Height int `yaml:"Height"`
Process string `yaml:"Process"`
Anchor string `yaml:"Anchor"`
Quality int `yaml:"Quality"`
Filename string `yaml:"-"`
Format string `yaml:"-"`
}
// PathConfig of subdir
type PathConfig struct {
This ThisPathConfig `yaml:"This"`
Template *string `yaml:"Template"`
Index *IndexConfig `yaml:"Index"`
Meta *MetaData `yaml:"Meta"`
Path *DirnameConfig `yaml:"Path"`
Filename *FilenameConfig `yaml:"Filename"`
Markdown *MarkdownConfig `yaml:"Markdown"`
Imaging *ImagingConfig `yaml:"Imaging"`
Data MapString `yaml:"Data"`
}
// PathConfigTree is complete config tree of content dir
type PathConfigTree struct {
InputPath string
OutputPath string
Hidden bool // for collections which are not part of the navigation
ColMap MapString
InputFiles []string
OtherFiles []string
Config *PathConfig
Sub []*PathConfigTree
}
type ptrTransformer struct{}
func (t ptrTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ.Kind() == reflect.Ptr {
return func(dst, src reflect.Value) error {
if dst.CanSet() {
if dst.IsNil() {
dst.Set(src)
}
}
return nil
}
}
return nil
}
// Merge merges 2 objects or maps
func Merge(dst, src interface{}) error {
return mergo.Merge(dst, src, mergo.WithTransformers(ptrTransformer{}))
}

28
helper/assets.go Normal file
View File

@@ -0,0 +1,28 @@
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)
}
}
}

514
helper/content.go Normal file
View File

@@ -0,0 +1,514 @@
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...)
}

24
helper/dir.go Normal file
View File

@@ -0,0 +1,24 @@
package helper
import "os"
// CreateDirectory creates direcory with all missing parents and panic if error
func CreateDirectory(dir string) {
Log.Debugf("trying to create output directory: %s", dir)
if dirH, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, 0755)
if err != nil {
Log.Panicf("could not create output directory '%s': %s", dir, err)
}
Log.Noticef("created output directory: %s", dir)
} else if dirH != nil {
if dirH.IsDir() {
Log.Noticef("output directory '%s' already exists", dir)
} else {
Log.Panicf("output directory '%s' is no directory", dir)
}
} else {
Log.Panicf("unknown error for output directory '%s': %s", dir, err)
}
}

52
helper/logger.go Normal file
View File

@@ -0,0 +1,52 @@
package helper
import (
"os"
"github.com/davecgh/go-spew/spew"
"github.com/op/go-logging"
)
// Log is global logger
var Log = logging.MustGetLogger("myLogger")
// ConfigureLogger sets logger backend and level
func ConfigureLogger(level string) {
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
logBackendFormatter := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
))
logBackendLeveled := logging.AddModuleLevel(logBackendFormatter)
logBackendLevel := logging.INFO
switch level {
case "debug":
logBackendLevel = logging.DEBUG
break
case "info":
logBackendLevel = logging.INFO
break
case "notice":
logBackendLevel = logging.NOTICE
break
case "warning":
logBackendLevel = logging.WARNING
break
case "error":
logBackendLevel = logging.ERROR
break
}
logBackendLeveled.SetLevel(logBackendLevel, "")
logging.SetBackend(logBackendLeveled)
}
func init() {
spew.Config.DisablePointerAddresses = true
spew.Config.DisableCapacities = true
spew.Config.DisableMethods = true
spew.Config.DisablePointerMethods = true
}

103
helper/navigation.go Normal file
View File

@@ -0,0 +1,103 @@
package helper
import (
"path"
"regexp"
"strings"
"gitbase.de/apairon/mark2web/config"
)
// NavElement is one element with ist attributes and subs
type NavElement struct {
Navname string
GoTo string
Active bool
ColMap config.MapString
Data interface{}
This config.ThisPathConfig
SubMap *map[string]*NavElement
SubSlice *[]*NavElement
}
// BuildNavigation builds the navigation trees for use in templates
func BuildNavigation(conf *config.PathConfigTree, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string) {
for _, el := range conf.Sub {
if el.Hidden {
continue // ignore hidden nav points from collections
}
var ignNav *string
if p := el.Config.Path; p != nil {
ignNav = p.IgnoreForNav
}
if ignNav != nil && *ignNav != "" {
regex, err := regexp.Compile(*ignNav)
if err != nil {
Log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err)
}
if regex.MatchString(path.Base(el.InputPath)) {
Log.Debugf("ignoring input directory '%s' in navigation", el.InputPath)
continue
}
}
elPath := strings.TrimPrefix(el.OutputPath, config.Config.Directories.Output+"/")
subMap := make(map[string]*NavElement)
subSlice := make([]*NavElement, 0)
navEl := NavElement{
Active: strings.HasPrefix(activeNav, elPath),
Data: el.Config.Data,
ColMap: el.ColMap,
SubMap: &subMap,
SubSlice: &subSlice,
}
navEl.This = el.Config.This
if navEl.Active {
// add to navActive level navigation
currentLevel := strings.Count(activeNav, "/")
if len(*navActive) <= currentLevel {
// not registered
*navActive = append(*navActive, &navEl)
}
}
n := el.Config.This.Navname
if n != nil {
navEl.Navname = *n
}
g := el.Config.This.GoTo
if g != nil {
if strings.HasPrefix(*g, "/") {
// abslute
navEl.GoTo = *g
} else {
// relative
navEl.GoTo = elPath + "/" + *g
}
} else {
navEl.GoTo = elPath + "/"
}
if activeNav != "" && activeNav != "/" {
// calculate relative path
bToRoot := BackToRoot(activeNav)
navEl.GoTo = bToRoot + navEl.GoTo
navEl.GoTo = path.Clean(navEl.GoTo)
}
(*curNavMap)[path.Base(el.OutputPath)] = &navEl
if curNavSlice != nil {
*curNavSlice = append(*curNavSlice, &navEl)
}
BuildNavigation(el, &subMap, &subSlice, navActive, activeNav)
}
}

103
helper/render.go Normal file
View File

@@ -0,0 +1,103 @@
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
}

353
helper/template_filters.go Normal file
View File

@@ -0,0 +1,353 @@
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

@@ -0,0 +1,125 @@
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)
}

676
main.go
View File

@@ -3,23 +3,11 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"reflect"
"regexp"
"strings"
"github.com/imdario/mergo" "gitbase.de/apairon/mark2web/config"
"gitbase.de/apairon/mark2web/helper"
"github.com/Depado/bfchroma"
"github.com/davecgh/go-spew/spew"
"github.com/flosch/pongo2"
"github.com/gosimple/slug"
"github.com/op/go-logging"
cpy "github.com/otiai10/copy"
"gopkg.in/russross/blackfriday.v2"
"gopkg.in/yaml.v2"
) )
var ( var (
@@ -31,587 +19,13 @@ var (
BuildTime = "UNKNOWN" BuildTime = "UNKNOWN"
) )
var log = logging.MustGetLogger("myLogger") var log = helper.Log
var inDir *string var contentConfig = new(config.PathConfigTree)
var outDir *string
var templateCache = make(map[string]*pongo2.Template)
// GlobalConfig is config which is used only once in root dir
type GlobalConfig struct {
Webserver struct {
Type string `yaml:"Type"`
} `yaml:"Webserver"`
Assets struct {
FromPath string `yaml:"FromPath"`
ToPath string `yaml:"ToPath"`
Action string `yaml:"Action"`
FixTemplate struct {
Find string `yaml:"Find"`
Replace string `yaml:"Replace"`
} `yaml:"FixTemplate"`
} `yaml:"Assets"`
OtherFiles struct {
Action string `yaml:"Action"`
} `yaml:"OtherFiles"`
}
var globalConfig = new(GlobalConfig)
// ThisPathConfig is struct for This in paths yaml
type ThisPathConfig struct {
Navname *string `yaml:"Navname"`
GoTo *string `yaml:"GoTo"`
Data interface{} `yaml:"Data"`
}
type indexStruct struct {
InputFile *string `yaml:"InputFile"`
OutputFile *string `yaml:"OutputFile"`
}
type metaStruct struct {
Title *string `yaml:"Title"`
Description *string `yaml:"Description"`
Keywords *string `yaml:"Keywords"`
}
type pathStruct struct {
Strip *string `yaml:"Strip"`
IgnoreForNav *string `yaml:"IgnoreForNav"`
}
type filenameStruct struct {
Strip *string `yaml:"Strip"`
Ignore *string `yaml:"Ignore"`
OutputExtension *string `yaml:"OutputExtension"`
}
type markdownStruct struct {
ChromaRenderer *bool `yaml:"ChromaRenderer"`
ChromaStyle *string `yaml:"ChromaStyle"`
}
// PathConfig of subdir
type PathConfig struct {
This ThisPathConfig `yaml:"This"`
Template *string `yaml:"Template"`
Index *indexStruct `yaml:"Index"`
Meta *metaStruct `yaml:"Meta"`
Path *pathStruct `yaml:"Path"`
Filename *filenameStruct `yaml:"Filename"`
Markdown *markdownStruct `yaml:"Markdown"`
Data interface{} `yaml:"Data"`
}
// PathConfigTree is complete config tree of content dir
type PathConfigTree struct {
InputPath string
OutputPath string
InputFiles []string
OtherFiles []string
Config *PathConfig
Sub []*PathConfigTree
}
var contentConfig = new(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
}
func merge(dst, src interface{}) error {
return mergo.Merge(dst, src, mergo.WithTransformers(ptrTransformer{}))
}
func backToRoot(curNavPath string) string {
tmpPath := ""
if curNavPath != "" {
for i := strings.Count(curNavPath, "/") + 1; i > 0; i-- {
tmpPath += "../"
}
}
return tmpPath
}
func readContentDir(inBase string, outBase string, dir string, conf *PathConfig, tree *PathConfigTree) {
inPath := inBase
if dir != "" {
inPath += "/" + dir
}
log.Infof("reading input directory: %s", inPath)
files, err := ioutil.ReadDir(inPath)
if err != nil {
log.Panic(err)
}
tree.InputPath = inPath
// read config
newConfig := new(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")
merge(newConfig, conf)
// remove this
newConfig.This = 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
merge(newConfig, conf)
newConfig.This = oldThis
log.Debug(spew.Sdump(newConfig))
}
tree.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 tree.Config.This.Navname == nil {
navname := strings.Replace(stripedDir, "_", " ", -1)
tree.Config.This.Navname = &navname
}
stripedDir = slug.Make(stripedDir)
outPath := outBase + "/" + stripedDir
outPath = path.Clean(outPath)
log.Infof("calculated output directory: %s", outPath)
tree.OutputPath = outPath
// first only files
for _, f := range files {
p := inPath + "/" + 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 := inPath + "/" + f.Name()
if f.IsDir() {
log.Debugf("DIR %s", p)
newTree := new(PathConfigTree)
if tree.Sub == nil {
tree.Sub = make([]*PathConfigTree, 0)
}
tree.Sub = append(tree.Sub, newTree)
readContentDir(inPath, outPath, f.Name(), newConfig, newTree)
}
}
}
type navElement struct {
Navname string
GoTo string
Active bool
Data interface{}
This ThisPathConfig
SubMap *map[string]*navElement
SubSlice *[]*navElement
}
func buildNavigation(conf *PathConfigTree, curNavMap *map[string]*navElement, curNavSlice *[]*navElement, navActive *[]*navElement, activeNav string) {
for _, el := range conf.Sub {
var ignNav *string
if p := el.Config.Path; p != nil {
ignNav = p.IgnoreForNav
}
if ignNav != nil && *ignNav != "" {
regex, err := regexp.Compile(*ignNav)
if err != nil {
log.Panicf("could not compile IngoreForNav regexp '%s' in '%s': %s", *ignNav, el.InputPath, err)
}
if regex.MatchString(path.Base(el.InputPath)) {
log.Debugf("ignoring input directory '%s' in navigation", el.InputPath)
continue
}
}
elPath := strings.TrimPrefix(el.OutputPath, *outDir+"/")
subMap := make(map[string]*navElement)
subSlice := make([]*navElement, 0)
navEl := navElement{
Active: strings.HasPrefix(activeNav, elPath),
Data: el.Config.Data,
SubMap: &subMap,
SubSlice: &subSlice,
}
navEl.This = el.Config.This
if navEl.Active {
// add to navActive level navigation
currentLevel := strings.Count(activeNav, "/")
if len(*navActive) <= currentLevel {
// not registered
*navActive = append(*navActive, &navEl)
}
}
n := el.Config.This.Navname
if n != nil {
navEl.Navname = *n
}
g := el.Config.This.GoTo
if g != nil {
if strings.HasPrefix(*g, "/") {
// abslute
navEl.GoTo = *g
} else {
// relative
navEl.GoTo = elPath + "/" + *g
}
} else {
navEl.GoTo = elPath + "/"
}
if activeNav != "" && activeNav != "/" {
// calculate relative path
bToRoot := backToRoot(activeNav)
navEl.GoTo = bToRoot + navEl.GoTo
navEl.GoTo = path.Clean(navEl.GoTo)
}
(*curNavMap)[navEl.Navname] = &navEl
if curNavSlice != nil {
*curNavSlice = append(*curNavSlice, &navEl)
}
buildNavigation(el, &subMap, &subSlice, navActive, activeNav)
}
}
func processContent(conf *PathConfigTree) {
log.Debugf("trying to create output directory: %s", conf.OutputPath)
if dirH, err := os.Stat(conf.OutputPath); os.IsNotExist(err) {
err := os.MkdirAll(conf.OutputPath, 0755)
if err != nil {
log.Panicf("could not create output directory '%s': %s", conf.OutputPath, err)
}
log.Noticef("created output directory: %s", conf.OutputPath)
} else if dirH != nil {
if dirH.IsDir() {
log.Noticef("output directory '%s' already exists", conf.OutputPath)
} else {
log.Panicf("output directory '%s' is no directory", conf.OutputPath)
}
} else {
log.Panicf("unknown error for output directory '%s': %s", conf.OutputPath, err)
}
curNavPath := strings.TrimPrefix(conf.OutputPath, *outDir)
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 globalConfig.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 {
inFile := conf.InputPath + "/" + file
log.Debugf("reading file: %s", inFile)
input, err := ioutil.ReadFile(inFile)
if err != nil {
log.Panicf("could not read '%s':%s", inFile, err)
}
log.Infof("processing input file '%s'", inFile)
newConfig := new(PathConfig)
regex := regexp.MustCompile("(?s)^---(.*?)\\r?\\n\\r?---\\r?\\n\\r?")
yamlData := regex.Find(input)
if string(yamlData) != "" {
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
merge(newConfig, conf.Config)
newConfig.This = oldThis
log.Debug(spew.Sdump(newConfig))
input = regex.ReplaceAll(input, []byte(""))
} else {
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)
var options []blackfriday.Option
var chromaRenderer *bool
var chromaStyle *string
if m := newConfig.Markdown; 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),
),
),
}
}
html := blackfriday.Run(input, options...)
// 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(blackfriday.Run([]byte(iPart), options...))))
}
log.Debugf("rendering template '%s' for '%s'", *newConfig.Template, outFile)
templateFile := *inDir + "/templates/" + *newConfig.Template
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
}
}
// build navigation
navMap := make(map[string]*navElement)
navSlice := make([]*navElement, 0)
navActive := make([]*navElement, 0)
buildNavigation(contentConfig, &navMap, &navSlice, &navActive, curNavPath)
// read yaml header as data for template
ctx := make(pongo2.Context)
ctx["This"] = newConfig.This
ctx["Meta"] = newConfig.Meta
ctx["Data"] = newConfig.Data
ctx["NavMap"] = navMap
ctx["NavSlice"] = navSlice
ctx["NavActive"] = navActive
ctx["Body"] = pongo2.AsSafeValue(string(html))
ctx["BodyParts"] = htmlParts
result, err := template.Execute(ctx)
if err != nil {
log.Panicf("could not execute template '%s' for input file '%s': %s", templateFile, inFile, err)
}
if find := globalConfig.Assets.FixTemplate.Find; find != "" {
log.Debugf("fixing assets paths in '%s' for '%s'", templateFile, inFile)
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 := globalConfig.Assets.FixTemplate.Replace
repl = bToRoot + globalConfig.Assets.ToPath + "/" + repl
repl = path.Clean(repl) + "/"
log.Debugf("new assets paths: %s", repl)
result = regex.ReplaceAllString(result, repl)
}
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 globalConfig.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)
}
}
}
for _, el := range conf.Sub {
processContent(el)
}
}
func processAssets() {
switch globalConfig.Assets.Action {
case "copy":
from := globalConfig.Assets.FromPath
to := globalConfig.Assets.ToPath
if !strings.HasPrefix(from, "/") {
from = *inDir + "/" + from
}
if !strings.HasPrefix(to, "/") {
to = *outDir + "/" + 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)
}
}
}
func main() { func main() {
spew.Config.DisablePointerAddresses = true inDir := flag.String("in", "./", "input directory")
spew.Config.DisableCapacities = true outDir := flag.String("out", "html", "output directory")
spew.Config.DisableMethods = true
spew.Config.DisablePointerMethods = true
inDir = flag.String("in", "./", "input directory")
outDir = flag.String("out", "html", "output directory")
createOutDir := flag.Bool("create", false, "create output directory if not existing") createOutDir := flag.Bool("create", false, "create output directory if not existing")
//clearOutDir := flag.Bool("clear", false, "clear output directory before generating website") //clearOutDir := flag.Bool("clear", false, "clear output directory before generating website")
logLevel := flag.String("logLevel", "info", "log level: debug, info, warning, error") logLevel := flag.String("logLevel", "info", "log level: debug, info, warning, error")
@@ -626,47 +40,24 @@ func main() {
os.Exit(0) os.Exit(0)
} }
logBackend := logging.NewLogBackend(os.Stderr, "", 0) level := "info"
logBackendFormatter := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
))
logBackendLeveled := logging.AddModuleLevel(logBackendFormatter)
logBackendLevel := logging.INFO
if logLevel != nil { if logLevel != nil {
switch *logLevel { level = *logLevel
case "debug":
logBackendLevel = logging.DEBUG
break
case "info":
logBackendLevel = logging.INFO
break
case "notice":
logBackendLevel = logging.NOTICE
break
case "warning":
logBackendLevel = logging.WARNING
break
case "error":
logBackendLevel = logging.ERROR
break
}
} }
logBackendLeveled.SetLevel(logBackendLevel, "") helper.ConfigureLogger(level)
logging.SetBackend(logBackendLeveled)
if inDir == nil || *inDir == "" { if inDir == nil || *inDir == "" {
log.Panic("input directory not specified") log.Panic("input directory not specified")
} }
iDir := path.Clean(*inDir)
inDir = &iDir
log.Infof("input directory: %s", *inDir) log.Infof("input directory: %s", *inDir)
if outDir == nil || *outDir == "" { if outDir == nil || *outDir == "" {
log.Panic("output directory not specified") log.Panic("output directory not specified")
} }
oDir := path.Clean(*outDir)
outDir = &oDir
log.Infof("output directory: %s", *outDir) log.Infof("output directory: %s", *outDir)
if createOutDir != nil && *createOutDir { if createOutDir != nil && *createOutDir {
@@ -694,17 +85,13 @@ func main() {
} }
log.Debug("reading global config...") log.Debug("reading global config...")
p := *inDir + "/config.yml" configFilename := *inDir + "/config.yml"
data, err := ioutil.ReadFile(p) err := config.ReadGlobalConfig(configFilename)
if err != nil { if err != nil {
log.Panicf("could not read file '%s': %s", p, err) log.Panicf("could not read file '%s': %s", configFilename, err)
} }
err = yaml.Unmarshal(data, globalConfig) config.Config.Directories.Input = *inDir
if err != nil { config.Config.Directories.Output = *outDir
log.Panicf("could not parse YAML file '%s': %s", p, err)
}
log.Debug(spew.Sdump(globalConfig))
log.Debugf("reading input directory %s", *inDir) log.Debugf("reading input directory %s", *inDir)
@@ -717,29 +104,40 @@ func main() {
defaultFilenameIgnore := "^_" defaultFilenameIgnore := "^_"
defaultFilenameOutputExtension := "html" defaultFilenameOutputExtension := "html"
defaultPathConfig := new(PathConfig) defaultPathConfig := new(config.PathConfig)
defaultPathConfig.Template = &defaultTemplate defaultPathConfig.Template = &defaultTemplate
defaultPathConfig.Index = &indexStruct{ defaultPathConfig.Index = &config.IndexConfig{
InputFile: &defaultInputFile, InputFile: &defaultInputFile,
OutputFile: &defaultOutputFile, OutputFile: &defaultOutputFile,
} }
defaultPathConfig.Path = &pathStruct{ defaultPathConfig.Path = &config.DirnameConfig{
Strip: &defaultPathStrip, Strip: &defaultPathStrip,
IgnoreForNav: &defaultPathIgnoreForNav, IgnoreForNav: &defaultPathIgnoreForNav,
} }
defaultPathConfig.Filename = &filenameStruct{ defaultPathConfig.Filename = &config.FilenameConfig{
Strip: &defaultFilenameStrip, Strip: &defaultFilenameStrip,
Ignore: &defaultFilenameIgnore, Ignore: &defaultFilenameIgnore,
OutputExtension: &defaultFilenameOutputExtension, OutputExtension: &defaultFilenameOutputExtension,
} }
defaultPathConfig.Imaging = &config.ImagingConfig{
Width: 1920,
Height: 1920,
Process: "fit",
Quality: 75,
}
readContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig) helper.ReadContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig)
//spew.Dump(contentConfig) //spew.Dump(contentConfig)
//spew.Dump(navMap) //spew.Dump(navMap)
processContent(contentConfig) templatesDir := *inDir + "/templates"
helper.SetTemplateDir(templatesDir)
processAssets() filtersDir := templatesDir + "/filters"
if _, err := os.Stat(filtersDir); !os.IsNotExist(err) {
helper.RegisterFilters(filtersDir)
}
helper.ProcessContent(contentConfig, contentConfig)
helper.ProcessAssets()
} }

32
test.rest Normal file
View File

@@ -0,0 +1,32 @@
GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog
?sort[date]=-1
&limit=101
&token=89ff216524093123bf7a0a10f7b273
###
&filter[link._id]=5c76a0f4643334fe0400039c
&filter[published]=true
###
GET https://mark2web.basiscms.de/api/imagestyles/style/klein
?src=/vhosts/mark2web.basiscms.de/uploads/2019/02/27/5c767a7f3dec9computer-3368242_1920.jpg
&output=1
&token=89ff216524093123bf7a0a10f7b273
###
GET https://mark2web.basiscms.de/api/imagestyles/style
###
GET https://ci.basehosts.de/api/repos/apairon/mark2web/builds?page=1
Authorization: Bearer { ci_token }
###
POST https://ci.basehosts.de/api/repos/apairon/mark2web/builds/63/promote?target=website
Authorization: Bearer { ci_token }
###

0
testfile Normal file
View File

1
vendor/github.com/ddliu/motto generated vendored Submodule

1
vendor/github.com/gosimple/slug generated vendored

1
vendor/github.com/robertkrimen/otto generated vendored Submodule

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

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

1
vendor/gopkg.in/sourcemap.v1 generated vendored Submodule

View File

@@ -9,3 +9,8 @@ Meta:
Markdown: Markdown:
ChromaRenderer: True ChromaRenderer: True
ChromaStyle: monokai ChromaStyle: monokai
Data:
debug: False
matomoSiteId: 89
token: 985cee34099f4d3b08f18fc22f6296 # cockpit api token

View File

@@ -2,11 +2,19 @@
Markdown: Markdown:
ChromaRenderer: False ChromaRenderer: False
Data:
background: /img/coffee.jpg
slider:
- img: /img/coffee.jpg
alt:
opacity: 1
--- ---
# mark2web # mark2web
mark2web ist ein Generator, der aus Markdown- und Konfig-Dateien in einer Ordnerstruktur eine statische Website unter Zuhilfenahme von Templates generiert. mark2web ist ein Generator, der aus Markdown- und Konfig-Dateien in einer Ordnerstruktur eine statische Website unter Zuhilfenahme von Templates generiert.
```mermaid ```mermaid
graph TD graph TD
@@ -28,25 +36,35 @@ graph TD
classDef out stroke-width:5px,stroke:#b5c50f,fill:#ccc classDef out stroke-width:5px,stroke:#b5c50f,fill:#ccc
class M,C,D,A in class M,C,D,A in
class W out class W out
click C "../benutzung/konfiguration" "Doku: Benutzung/Konfiguration"
click M "../benutzung/inhalte" "Doku: Benutzung/Inhalte"
click A "../benutzung/inhalte" "Doku: Benutzung/Inhalte"
click D "../benutzung/templates" "Doku: Benutzung/Templates"
``` ```
<script defer> <script defer>
window.onload = function() { window.onload = function() {
mermaid.init(undefined,$("code.language-mermaid")); mermaid.init(undefined,$("code.language-mermaid"));
$("code.language-mermaid").css("visibility", "visible");
}; };
</script> </script>
--- ---
Der Generator selbst wurde in [Go](https://golang.org/) geschrieben. Es wurden dabei eine Vielzahl existierender Packages verwendet. Unter Anderem: Der Generator selbst wurde in [Go](https://golang.org/) geschrieben. Es wurden dabei eine Vielzahl existierender Packages verwendet.
Unter Anderem:
- der Markdown-Parser [blackfriday](https://github.com/russross/blackfriday) - der Markdown-Parser [blackfriday](https://github.com/russross/blackfriday)
- die Template-Sprache "Django Template Language" über das Paket [pongo2](github.com/flosch/pongo2) - die Template-Sprache "Django Template Language" über das Paket [pongo2](https://github.com/flosch/pongo2)
- das Logging-Paket [go-logging](github.com/op/go-logging) - das Logging-Paket [go-logging](https://github.com/op/go-logging)
- der YAML-Parser [go-yaml](https://github.com/go-yaml/yaml) - der YAML-Parser [go-yaml](https://github.com/go-yaml/yaml)
- die Imaging Bibliothek [disintegration/imaging](github.com/disintegration/imaging)
- der Javascript-Interpreter [otto](github.com/robertkrimen/otto) mit der Erweiterung [motto](github.com/ddliu/motto)
Weitere Pakete, die verwendet wurden finden Sie in den Quellen. Weitere Pakete, die verwendet wurden finden Sie in den Quellen.
Diese Website wurde selbst mit mark2web generiert. Der entsprechende Quellcode, sowie die Quellen zu mark2web finden Sie unter: Diese Website wurde selbst mit mark2web generiert. Der entsprechende Quellcode, sowie die Quellen zu mark2web finden Sie unter:
**https://gitbase.de/apairon/mark2web** **[https://gitbase.de/apairon/mark2web](5c76a0f4643334fe0400039c)**

View File

@@ -1,3 +1,13 @@
---
Data:
background: /img/laptop.jpg
slider:
- img: /img/laptop.jpg
alt:
opacity: 1
---
# Installation # Installation
Damit die korrekten Versionsinformationen dynamisch in das finale mark2web-Binary eingefügt wurde, ist eine manuelle Installation aus dem Git-Repository sinnvoll. Damit die korrekten Versionsinformationen dynamisch in das finale mark2web-Binary eingefügt wurde, ist eine manuelle Installation aus dem Git-Repository sinnvoll.
@@ -17,4 +27,6 @@ git submodule update --init --recursive
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. 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.
Später folgen vorkomplierte Releases über die Repository-Website. ## Releases
Vorkompilierte Binaries finden Sie auf der [Releases-Seite auf gitbase.de](https://gitbase.de/apairon/mark2web/releases).

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

View File

@@ -1,3 +1,14 @@
---
Data:
background: /img/folder.jpg
slider:
- img: /img/folder.jpg
alt:
opacity: 1
---
# Benutzung # Benutzung
## Ordnerstruktur ## Ordnerstruktur
@@ -38,6 +49,8 @@ DIR assets (kann auch abweichend benannt werden)
DIR css DIR css
DIR templates DIR templates
DIR filters
FILE myfilter.js
FIL base.html FIL base.html
FIL base_sub.html FIL base_sub.html

View File

@@ -1,3 +1,14 @@
---
Data:
background: /img/wire.jpg
slider:
- img: /img/wire.jpg
alt:
opacity: 1
---
# Konfiguration # Konfiguration
Die Konfigurationsdatein sind im YAML-Format gehalten (siehe: [Wikipedia](https://de.wikipedia.org/wiki/YAML)). Die Konfigurationsdatein sind im YAML-Format gehalten (siehe: [Wikipedia](https://de.wikipedia.org/wiki/YAML)).

View File

@@ -1,3 +1,14 @@
---
Data:
background: /img/write.jpg
slider:
- img: /img/write.jpg
alt:
opacity: 1
---
# Inhalte # Inhalte
Die Inhalte der Website werden im Ordner `content` und dessen Unterordnern gespeichert. Die Inhalte der Website werden im Ordner `content` und dessen Unterordnern gespeichert.

View File

@@ -1,3 +1,14 @@
---
Data:
background: /img/design.jpg
slider:
- img: /img/design.jpg
alt:
opacity: 1
---
# Templates # Templates
Templates werden über das pongo2-Paket gerendert. Dieses nutzt die Template-Sprache **Django-Template**. Templates werden über das pongo2-Paket gerendert. Dieses nutzt die Template-Sprache **Django-Template**.

View File

@@ -1,2 +1,9 @@
This: This:
GoTo: ordnerstruktur GoTo: ordnerstruktur
Data:
slider:
- img: /img/gears.jpg
alt:
opacity: 1

View File

@@ -0,0 +1,14 @@
---
Template: base_blog.html
Data:
background: /img/blog.jpg
slider:
- img: /img/blog.jpg
alt:
opacity: 1
---
# Blog

View File

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

View File

@@ -41,6 +41,8 @@ Ihr Besuch dieser Webseite wird aktuell von der Matomo Webanalyse erfasst. Klick
Sie können sich hier entscheiden, ob in Ihrem Browser ein eindeutiger Webanalyse-Cookie abgelegt werden darf, um dem Betreiber der Website die Erfassung und Analyse verschiedener statistischer Daten zu ermöglichen. Sie können sich hier entscheiden, ob in Ihrem Browser ein eindeutiger Webanalyse-Cookie abgelegt werden darf, um dem Betreiber der Website die Erfassung und Analyse verschiedener statistischer Daten zu ermöglichen.
Wenn Sie sich dagegen entscheiden möchten, klicken Sie den folgenden Link, um den Matomo-Deaktivierungs-Cookie in Ihrem Browser abzulegen. Wenn Sie sich dagegen entscheiden möchten, klicken Sie den folgenden Link, um den Matomo-Deaktivierungs-Cookie in Ihrem Browser abzulegen.
<iframe style="border: 0; height: 200px; width: 100%;" src="https://www.basisstats.de/index.php?module=CoreAdminHome&action=optOut&language=de&backgroundColor=ffffff&fontColor=4e4e4e&fontSize=17px&fontFamily='Open%20Sans'%2C%20sans-serif"></iframe>
## 4. Google Maps ## 4. Google Maps
Wir verwenden auf unserer Internetseite Google Maps zur Auffindung unseres Firmensitzes. Dieser Dienst wird von der Firma Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043 USA, nachfolgend „Google“ angeboten. Bei jedem Aufruf von Google Maps wird von Google ein Cookie gesetzt, um bei der Anzeige der Seite, auf der Google Maps integriert ist, Nutzereinstellungen und -daten zu verarbeiten. Dieser Cookie von Google wird im Regelfall nicht durch das Schließen des Browsers gelöscht, sondern läuft nach einer bestimmten Zeit ab, soweit er nicht von Ihnen zuvor manuell gelöscht wurde. Wenn Sie mit dieser Verarbeitung Ihrer Daten nicht einverstanden sind, besteht die Möglichkeit, den Service von Google Maps zu deaktivieren und auf diesem Weg die Übertragung von Daten an Google zu verhindern. Hierfür gibt es folgende Möglichkeiten: Sie können geeignete Browsererweiterungen installieren (wie z.B. uMatrix oder Disconnect) und diese anweisen, die Übertragung an Google zu unterbinden. Sie können auch die Java-Script-Funktion in Ihrem Browser deaktivieren. Wir weisen Sie darauf hin, dass in diesem Fall die Nutzung von Google Maps und ggf. anderer Teile unserer Internetseite nicht oder nur noch eingeschränkt möglich ist. Die Nutzung von Google Maps und der hierdurch erlangten Informationen erfolgt gemäß den Google-Nutzungsbedingungen http://www.google.de/intl/de/policies/terms/regional.html sowie der zusätzlichen Geschäftsbedingungen für Google Maps https://www.google.com/intl/de_de/help/terms_maps.html. Wir verwenden auf unserer Internetseite Google Maps zur Auffindung unseres Firmensitzes. Dieser Dienst wird von der Firma Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043 USA, nachfolgend „Google“ angeboten. Bei jedem Aufruf von Google Maps wird von Google ein Cookie gesetzt, um bei der Anzeige der Seite, auf der Google Maps integriert ist, Nutzereinstellungen und -daten zu verarbeiten. Dieser Cookie von Google wird im Regelfall nicht durch das Schließen des Browsers gelöscht, sondern läuft nach einer bestimmten Zeit ab, soweit er nicht von Ihnen zuvor manuell gelöscht wurde. Wenn Sie mit dieser Verarbeitung Ihrer Daten nicht einverstanden sind, besteht die Möglichkeit, den Service von Google Maps zu deaktivieren und auf diesem Weg die Übertragung von Daten an Google zu verhindern. Hierfür gibt es folgende Möglichkeiten: Sie können geeignete Browsererweiterungen installieren (wie z.B. uMatrix oder Disconnect) und diese anweisen, die Übertragung an Google zu unterbinden. Sie können auch die Java-Script-Funktion in Ihrem Browser deaktivieren. Wir weisen Sie darauf hin, dass in diesem Fall die Nutzung von Google Maps und ggf. anderer Teile unserer Internetseite nicht oder nur noch eingeschränkt möglich ist. Die Nutzung von Google Maps und der hierdurch erlangten Informationen erfolgt gemäß den Google-Nutzungsbedingungen http://www.google.de/intl/de/policies/terms/regional.html sowie der zusätzlichen Geschäftsbedingungen für Google Maps https://www.google.com/intl/de_de/help/terms_maps.html.

BIN
website/img/blog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

BIN
website/img/coffee.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

BIN
website/img/design.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

BIN
website/img/desk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

BIN
website/img/folder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

BIN
website/img/gears.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

BIN
website/img/html.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

BIN
website/img/keyboard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

BIN
website/img/laptop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

BIN
website/img/laptop2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

BIN
website/img/wire.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
website/img/write.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

View File

@@ -24,7 +24,10 @@ hr { display:block; height:1px; border:0; border:none; padding:0;}
input, select {vertical-align:middle;} input, select {vertical-align:middle;}
input, select, textarea {-webkit-appearance: none; border-radius: 0;} input, select, textarea {-webkit-appearance: none; border-radius: 0;}
.clear {clear:both; margin: 0; padding:0;} .clear {clear:both; margin: 0; padding:0;}
img {line-height:0;} img {
line-height:0;
max-width: 100%;
}
/*-------------------------------------------------------------- /*--------------------------------------------------------------
# General Settings # General Settings
@@ -65,13 +68,14 @@ hr {height:1px; background:none; border-bottom:dotted 1px #666; margin-bottom:20
.btn { .btn {
padding:8px 15px; padding:8px 15px;
background:#464645; background:#464645;
border: 1px solid #fff;
color:#FFF; color:#FFF;
transition:all 0.3s; transition:all 0.3s;
text-transform:none; text-transform:none;
display:inline-block; display:inline-block;
margin-top:20px; margin-top:20px;
} }
.btn:hover {background:#b5c50f; color:#FFF;} .btn:hover {background:#b5c50f; color:#464645;}
.center_btn { width:100%; text-align:center; } .center_btn { width:100%; text-align:center; }
/* White Color */ /* White Color */
@@ -150,7 +154,7 @@ label {font-weight:600;}
# Header # Header
--------------------------------------------------------------*/ --------------------------------------------------------------*/
#header {height:80px;} #header {height:80px;}
#header .logo {height:70%; max-width:500px;} #header .logo {height:70%; max-width:400px;}
#header .logo img {height:100%;} #header .logo img {height:100%;}
#header .container { #header .container {
@@ -171,6 +175,7 @@ label {font-weight:600;}
z-index:300; z-index:300;
background:none; background:none;
} }
#header .logo {height:70%; max-width:100px;}
#header .logo img {height:auto; width:100%;} #header .logo img {height:auto; width:100%;}
} }
@@ -192,7 +197,6 @@ label {font-weight:600;}
} }
.boxen {}
.boxen .col-sm-6 {padding:15px;} .boxen .col-sm-6 {padding:15px;}
.box { .box {
height:100%; height:100%;
@@ -268,4 +272,23 @@ pre {
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
margin-bottom: 25px; margin-bottom: 25px;
}
code.language-mermaid {
visibility: hidden;
}
code.language-mermaid svg {
margin-left: auto;
margin-right: auto;
display: block;
}
.datum {
background-color: #2e2e2e;
padding: 5px;
font-size: 0.5em;
border-radius: 5px;
display:inline-block;
color: #fff;
} }

View File

@@ -66,7 +66,7 @@ a#pull span:nth-of-type(3) {margin-bottom:0px;}
#mainmenu ul li a { #mainmenu ul li a {
padding:0px 15px; padding:0px 15px;
height:50px; height:50px;
color:#FFF; color: #464645;
font-size:17px; font-size:17px;
display:flex; display:flex;
display:-webkit-flex; display:-webkit-flex;
@@ -76,6 +76,9 @@ a#pull span:nth-of-type(3) {margin-bottom:0px;}
flex-direction:column; flex-direction:column;
} }
#mainmenu ul li.active a {
color: #fff;
}
#mainmenu ul li .trigger { #mainmenu ul li .trigger {
width:50px; width:50px;
height:50px; height:50px;
@@ -99,6 +102,7 @@ a#pull span:nth-of-type(3) {margin-bottom:0px;}
#mainmenu ul li.active { #mainmenu ul li.active {
background:#464645; background:#464645;
color: #fff;
} }
#mainmenu .sub-menu { #mainmenu .sub-menu {
@@ -153,6 +157,12 @@ a#pull span:nth-of-type(3) {margin-bottom:0px;}
background:#b5c50f; background:#b5c50f;
/*border-top:solid 5px #781c45;*/ /*border-top:solid 5px #781c45;*/
} }
#mainmenu ul.sub-menu li a{
color: #464645;
}
#mainmenu ul.sub-menu li.active a, #mainmenu ul.sub-menu li:hover a {
color: #fff;
}
#mainmenu>ul>li>a {padding:0px 20px;} #mainmenu>ul>li>a {padding:0px 20px;}
#mainmenu>ul>li>a, #mainmenu>ul>li>ul>li>a {transition:background 0.3s;} #mainmenu>ul>li>a, #mainmenu>ul>li>ul>li>a {transition:background 0.3s;}
#mainmenu>ul>li>ul>li:hover>a {background:#464645;} #mainmenu>ul>li>ul>li:hover>a {background:#464645;}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -3,27 +3,42 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" href="project-files/img/favicon.ico" /> <link rel="icon" href="project-files/img/favicon.ico?{{ Timestamp }}" />
<!-- Meta --> <!-- Meta -->
{% block meta %}
<title>{{ Meta.Title }}</title> <title>{{ Meta.Title }}</title>
<meta name="description" content="{{ Meta.Description }}" /> <meta name="description" content="{{ Meta.Description }}" />
<meta name="keywords" content="{{ Meta.Keywords }}" /> <meta name="keywords" content="{{ Meta.Keywords }}" />
{% endblock meta %}
<link rel="stylesheet" type="text/css" href="project-files/css/preloader.css">
<link rel="stylesheet" type="text/css" href="project-files/css/preloader.css?{{ Timestamp }}">
{% include "partials/matomo.html" with siteId=Data.matomoSiteId %}
</head> </head>
<body> <body>
<div id="preloader">
<div class="spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
</div>
<header id="header"> <header id="header">
<div class="container"> <div class="container">
<div class="logo"><img src="project-files/img/logo.png" alt=""></div> <div class="logo" {% if not Data.slider and not Data.details.slider %}style="max-width: 400px;"{% endif %}>
<a href="{{ "/"|relative_path }}">
<picture>
<source media="(max-width: 768px)" srcset="project-files/img/logo_text.png">
<img src="project-files/img/logo{% if not Data.slider and not Data.details.slider %}_text{% endif %}.png" alt="mark2web Logo">
</picture>
</a>
</div>
<a id="pull"> <a id="pull">
<span class="pull_button"> <span class="pull_button">
<span></span> <span></span>
@@ -33,13 +48,23 @@
<span class="text">Menu</span> <span class="text">Menu</span>
</a> </a>
</div> </div>
</header> </header>
<!-- ========== Slider ========== --> <!-- ========== Slider ========== -->
<section class="slider white_arrows" data-dots="true" data-arrows="true"> <section class="slider white_arrows" data-dots="true" data-arrows="true">
{% block slider %}
{% for sl in Data.slider %}
<div class="slide">
<img
src="{{ sl.img|image_process:"p=fill,w=1440,h=600,q=60" }}"
srcset="{{ sl.img|image_process:"p=fill,w=768,h=384,q=60" }} 768w,
{{ sl.img|image_process:"p=fill,w=1440,h=600,q=60" }} 1440w,
{{ sl.img|image_process:"p=fill,w=1920,h=800,q=60" }} 1920w"
alt="{{ sl.alt }}" style="opacity:{{ sl.opacity|default:1 }}">
</div>
{% endfor %}
{% endblock slider %}
</section> </section>
<!-- ========== Navigation ========== --> <!-- ========== Navigation ========== -->
<div class="navholder" id="nav"> <div class="navholder" id="nav">
@@ -60,7 +85,6 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</nav> </nav>
</div> </div>
</div> </div>
@@ -69,18 +93,46 @@
<!-- ========== Content First ========== --> <!-- ========== Content First ========== -->
<div class="contentfirst section_padding white_color "> <div class="contentfirst section_padding white_color ">
<div class="container"> <div class="container">
{% block part0 %}
{{ BodyParts.0 }} {{ BodyParts.0 }}
{% endblock part0 %}
</div> </div>
</div> </div>
<!-- ========== Main Content ========== --> <!-- ========== Main Content ========== -->
<div class="maincontent"> <div class="maincontent">
{% if Data.background %}
<img
src="{{ Data.background|image_process:"p=fill,w=1440,h=810,q=30" }}"
srcset="{{ Data.background|image_process:"p=fill,w=768,h=768,q=30" }} 768w,
{{ Data.background|image_process:"p=fill,w=1440,h=810,q=30" }} 1440w,
{{ Data.background|image_process:"p=fill,w=1920,h=1020,q=30" }} 1920w"
alt="{{ Meta.Title }}" class="img2bg">
{% endif %}
<div class="white_section section_padding"> <div class="white_section section_padding">
<div class="container"> <div class="container">
{% block part1 %}
{{ BodyParts.1 }} {{ BodyParts.1 }}
</div> {% endblock part1 %}
{% if Data.debug %}
<hr>
<h2>DEBUG</h2>
<h3>This</h3>
<pre>{{ This|json:"pretty" }}</pre>
<h3>Data</h3>
<pre>{{ Data|json:"pretty" }}</pre>
<h3>NavMap</h3>
<pre>{{ NavMap|json:"pretty" }}</pre>
{% endif %}
</div>
</div> </div>
{% if NavActive.2.SubSlice %} {% if NavActive.2.SubSlice %}
<div class="section_padding" style="border-top: 1px solid #888;"> <div class="section_padding" style="border-top: 1px solid #888;">
<div class="container"> <div class="container">
@@ -94,20 +146,19 @@
<div class="content"> <div class="content">
<p>{{ nav.This.Data.teaser }} <p>{{ nav.This.Data.teaser }}
</p> </p>
<a href="{{ nav.GoTo }}" class="btn">mehr lesen »</a> <a href="{{ nav.GoTo }}" class="btn">mehr lesen &raquo;</a>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- ========== Footer ========== --> <!-- ========== Footer ========== -->
<footer id="footer"> <footer id="footer">
<div class="container"> <div class="container">
@@ -141,7 +192,6 @@
</div> </div>
</section> </section>
<!-- ========== SCROLL TOP ========== --> <!-- ========== SCROLL TOP ========== -->
<div class="scrolltop"> <div class="scrolltop">
@@ -151,23 +201,23 @@
<!-- Style --> <!-- Style -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="project-files/css/grid.css"> <link rel="stylesheet" type="text/css" href="project-files/css/grid.css?{{ Timestamp }}">
<link rel="stylesheet" type="text/css" href="project-files/css/main.css"> <link rel="stylesheet" type="text/css" href="project-files/css/main.css?{{ Timestamp }}">
<link rel="stylesheet" type="text/css" href="project-files/css/menu.css"> <link rel="stylesheet" type="text/css" href="project-files/css/menu.css?{{ Timestamp }}">
<link rel="stylesheet" type="text/css" href="project-files/css/slider.css"> <link rel="stylesheet" type="text/css" href="project-files/css/slider.css?{{ Timestamp }}">
<link rel="stylesheet" type="text/css" href="project-files/fonts/font-awesome/font-awesome.css"> <link rel="stylesheet" type="text/css" href="project-files/fonts/font-awesome/font-awesome.css?{{ Timestamp }}">
<link rel="stylesheet" type="text/css" href="project-files/js/slick/slick.css"> <link rel="stylesheet" type="text/css" href="project-files/js/slick/slick.css?{{ Timestamp }}">
<!-- Javascript --> <!-- Javascript -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="project-files/js/slick/slick.min.js"></script> <script src="project-files/js/slick/slick.min.js?{{ Timestamp }}"></script>
<script src="project-files/js/slick/slick-custom.js"></script> <script src="project-files/js/slick/slick-custom.js?{{ Timestamp }}"></script>
<script src="project-files/js/preloader.js"></script> <script src="project-files/js/preloader.js?{{ Timestamp }}"></script>
<script src="project-files/js/functions.js"></script> <script src="project-files/js/functions.js?{{ Timestamp }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.0.0/mermaid.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.0.0/mermaid.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block part0 %}
{{ Body }}
{% for e in NavElement.ColMap.blog1st.entries %}
<h2>
{{ e.title }}
<div class="datum">{{ e.date|datum }}</div>
</h2>
{{ e.teaser|markdown }}
{% if e.body %}
<a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a>
{% endif %}
{% endfor %}
{% endblock part0 %}
{% block part1 %}
{% comment %}
limit wird für skip in Query gebraucht
{% endcomment %}
{% for e in NavElement.ColMap.blog1skip.entries %}
<h2>
{{ e.title }}
<div class="datum">{{ e.date|datum }}</div>
</h2>
{{ e.teaser|markdown }}
{% if e.body %}
<a href="{{ e.date|add:"-"|add:e.title|slugify }}" class="btn">mehr lesen &raquo;</a>
{% endif %}
{% endfor %}
{% endblock part1 %}

View File

@@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% block meta %}
<title>{{ Data.details.date|datum }} {{ Data.details.title }}</title>
<meta name="description" content="{{ Data.details.teaser }}" />
<meta name="keywords" content="{{ Meta.Keywords }}" />
{% endblock meta %}
{% if Data.details.slider %}
{% block slider %}
{% for sl in Data.details.slider %}
<div class="slide">
<img
src="{{ "https://mark2web.basiscms.de"|add:sl.path|image_process:"p=fill,w=1440,h=600,q=60" }}"
srcset="{{ "https://mark2web.basiscms.de"|add:sl.path|image_process:"p=fill,w=768,h=384,q=60" }} 768w,
{{ "https://mark2web.basiscms.de"|add:sl.path|image_process:"p=fill,w=1440,h=600,q=60" }} 1440w,
{{ "https://mark2web.basiscms.de"|add:sl.path|image_process:"p=fill,w=1920,h=800,q=60" }} 1920w"
alt="{{ sl.meta.title }}">
</div>
{% endfor %}
{% endblock slider %}
{% endif %}
{% block part0 %}
<h1>
{{ Data.details.title }}
<div class="datum">{{ Data.details.date|datum }}</div>
</h1>
{{ Data.details.teaser|markdown }}
{% endblock part0 %}
{% block part1 %}
{{ Body }}
<br>
<a href="../" class="btn">&laquo; zurück</a>
{% endblock part1 %}

View File

@@ -0,0 +1,16 @@
var strings = require('./lib/strings');
function datum(str, param) {
if (!param) {
param = "$3.$2.$1";
}
/*
this.context.Meta.Title ist hier z.B. möglich
*/
return strings.replace(str, /^([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+).*/, param)
// return str.replace(/^([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+).*/, param)
}
module.exports = datum;

View File

@@ -0,0 +1,5 @@
module.exports = {
replace: function (str, regex, replace) {
return str.replace(regex, replace);
}
}

View File

@@ -0,0 +1,17 @@
<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setCookieDomain", "*.mark2web.de"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://www.basisstats.de/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '{{ siteId|default:0 }}']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="https://www.basisstats.de/matomo.php?idsite=89&amp;rec=1" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->