collections via markdown files
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Sebastian Frank 2019-03-22 17:22:03 +01:00
parent 4a9a3eec06
commit ff1da084af
Signed by: apairon
GPG Key ID: 7270D06DDA7FE8C3
46 changed files with 547 additions and 362 deletions

View File

@ -2,6 +2,7 @@ NEUERUNGEN:
- Cached Collection Webrequests - Cached Collection Webrequests
- recursive Collections - recursive Collections
- Datei basierte Collections
- markdown-Filter `s=SYNTAX_HIGHLIGHT_SHEMA` Parameter - markdown-Filter `s=SYNTAX_HIGHLIGHT_SHEMA` Parameter
- image_process nutzt alle CPU-Kerne - image_process nutzt alle CPU-Kerne
- GZIP/Brotli Vor-Komprimierung der Inhalte und Assets - GZIP/Brotli Vor-Komprimierung der Inhalte und Assets

View File

@ -130,7 +130,7 @@ func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *
} }
} else { } else {
// local file // local file
imgSource = mark2web.ResolveInputPath(imgSource) imgSource = mark2web.CurrentTreeNode.ResolveInputPath(imgSource)
if p.Filename == "" { if p.Filename == "" {
p.Filename = fmt.Sprintf( p.Filename = fmt.Sprintf(
"%s_%s", "%s_%s",
@ -142,7 +142,7 @@ func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *
var imgTarget string var imgTarget string
if p.TargetDir != "" { if p.TargetDir != "" {
imgTarget = mark2web.ResolveOutputPath( imgTarget = mark2web.CurrentTreeNode.ResolveOutputPath(
path.Clean(p.TargetDir) + "/" + path.Clean(p.TargetDir) + "/" +
p.Filename, p.Filename,
) )
@ -158,10 +158,10 @@ func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *
} }
} }
p.Filename = mark2web.ResolveNavPath(p.TargetDir + "/" + p.Filename) p.Filename = mark2web.CurrentTreeNode.ResolveNavPath(p.TargetDir + "/" + p.Filename)
} else { } else {
imgTarget = mark2web.ResolveOutputPath(p.Filename) imgTarget = mark2web.CurrentTreeNode.ResolveOutputPath(p.Filename)
} }
if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() { if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() {
@ -226,5 +226,5 @@ func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *
helper.Log.Noticef("finished image: %s", imgTarget) helper.Log.Noticef("finished image: %s", imgTarget)
}) })
} }
return pongo2.AsValue(mark2web.ResolveNavPath(p.Filename)), nil return pongo2.AsValue(mark2web.CurrentTreeNode.ResolveNavPath(p.Filename)), nil
} }

View File

@ -36,9 +36,6 @@ func TestImageProcessFilter(t *testing.T) {
}, },
}, },
} }
mark2web.CurrentContext = &pongo2.Context{
"CurrentPath": "",
}
os.Remove("../../test/out/fit_300x300_q060_test.jpg") os.Remove("../../test/out/fit_300x300_q060_test.jpg")

View File

@ -8,7 +8,7 @@ import (
// RelativePathFilter returns the relative path to navpoint based on current nav // RelativePathFilter returns the relative path to navpoint based on current nav
func RelativePathFilter(in, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { func RelativePathFilter(in, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
return pongo2.AsValue( return pongo2.AsValue(
mark2web.ResolveNavPath( mark2web.CurrentTreeNode.ResolveNavPath(
in.String(), in.String(),
), ),
), nil ), nil

View File

@ -17,10 +17,23 @@ func TestRelativePathFilter(t *testing.T) {
"testabs": "/abs", "testabs": "/abs",
"testsub": "../sub/rel", "testsub": "../sub/rel",
} }
mark2web.Config.Directories.Output = "../../test/out"
mark2web.CurrentTreeNode = &mark2web.TreeNode{
InputPath: "../../test/in/content",
OutputPath: "../../test/out/sub",
Config: &mark2web.PathConfig{
Imaging: &mark2web.ImagingConfig{
Quality: 60,
Height: 300,
Width: 300,
Process: "fit",
},
},
}
Convey("parse template", func() { Convey("parse template", func() {
mark2web.CurrentContext = &pongo2.Context{
"CurrentPath": "sub",
}
output, err := pongo2.RenderTemplateString("{{ testrel|relative_path }}", ctx) output, err := pongo2.RenderTemplateString("{{ testrel|relative_path }}", ctx)
So(err, ShouldBeNil) So(err, ShouldBeNil)

View File

@ -2,7 +2,6 @@ package helper
import ( import (
"os" "os"
"strings"
) )
// CreateDirectory creates direcory with all missing parents and panic if error // CreateDirectory creates direcory with all missing parents and panic if error
@ -25,14 +24,3 @@ func CreateDirectory(dir string) {
Log.Panicf("unknown error for output directory '%s': %s", dir, err) Log.Panicf("unknown error for output directory '%s': %s", dir, err)
} }
} }
// BackToRoot builds ../../ string
func BackToRoot(curNavPath string) string {
tmpPath := ""
if curNavPath != "" {
for i := strings.Count(curNavPath, "/") + 1; i > 0; i-- {
tmpPath += "../"
}
}
return tmpPath
}

18
pkg/helper/regexp.go Normal file
View File

@ -0,0 +1,18 @@
package helper
import "regexp"
// GetRegexpParams gets a map of named regexp group matches
// use pe. (?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2}) as regexp
func GetRegexpParams(regEx *regexp.Regexp, str string) (paramsMap map[string]string) {
match := regEx.FindStringSubmatch(str)
paramsMap = make(map[string]string)
for i, name := range regEx.SubexpNames() {
if i > 0 && i <= len(match) {
paramsMap[name] = match[i]
}
}
return
}

View File

@ -35,13 +35,13 @@ func ProcessAssets() {
} }
// fixAssetsPath replaces assets path based on current path // fixAssetsPath replaces assets path based on current path
func fixAssetsPath(str, curNavPath string) string { func (node *TreeNode) fixAssetsPath(str string) string {
if find := Config.Assets.FixTemplate.Find; find != "" { if find := Config.Assets.FixTemplate.Find; find != "" {
helper.Log.Debugf("fixing assets paths for path '%s'", curNavPath) helper.Log.Debugf("fixing assets paths for path '%s'", node.CurrentNavPath())
repl := Config.Assets.FixTemplate.Replace repl := Config.Assets.FixTemplate.Replace
toPath := Config.Assets.ToPath toPath := Config.Assets.ToPath
bToRoot := helper.BackToRoot(curNavPath) bToRoot := node.BackToRootPath()
regex, err := regexp.Compile(find) regex, err := regexp.Compile(find)
if err != nil { if err != nil {
log.Panicf("could not compile regexp '%s' for assets path: %s", find, err) log.Panicf("could not compile regexp '%s' for assets path: %s", find, err)

View File

@ -1,7 +1,9 @@
package mark2web package mark2web
import ( import (
"io/ioutil"
"path" "path"
"regexp"
"strings" "strings"
"gitbase.de/apairon/mark2web/pkg/helper" "gitbase.de/apairon/mark2web/pkg/helper"
@ -20,13 +22,12 @@ var colCache = make(map[string]*colCacheEntry)
func (node *TreeNode) handleCollections() { func (node *TreeNode) handleCollections() {
collections := append(node.Config.Collections, node.Config.This.Collections...) collections := append(node.Config.Collections, node.Config.This.Collections...)
for _, colConfig := range collections { for _, colConfig := range collections {
if colConfig != nil { if colConfig.Name == nil || *colConfig.Name == "" {
if colConfig.Name == nil || *colConfig.Name == "" { helper.Log.Panicf("missing Name in collection config in '%s'", node.InputPath)
helper.Log.Panicf("missing Name in collection config in '%s'", node.InputPath) }
} if (colConfig.URL == nil || *colConfig.URL == "") &&
if colConfig.URL == nil || *colConfig.URL == "" { (colConfig.Directory == nil) {
helper.Log.Panicf("missing EntriesJSON in collection config in '%s'", node.InputPath) helper.Log.Panicf("missing URL and Directory in collection config in '%s'", node.InputPath)
}
} }
if node.ColMap == nil { if node.ColMap == nil {
@ -36,21 +37,73 @@ func (node *TreeNode) handleCollections() {
ctx["This"] = node.Config.This ctx["This"] = node.Config.This
ctx["Data"] = node.Config.Data ctx["Data"] = node.Config.Data
url, err := pongo2.RenderTemplateString(*colConfig.URL, ctx)
if err != nil {
helper.Log.Panicf("invalid template string for Collection Element.URL in '%s': %s", node.InputPath, err)
}
var colData interface{} var colData interface{}
if cacheEntry, ok := colCache[url]; ok {
colData = cacheEntry.data errSrcText := ""
cacheEntry.hit++ cacheKey := ""
} else {
colData = helper.JSONWebRequest(url) if colConfig.URL != nil {
colCache[url] = &colCacheEntry{ url, err := pongo2.RenderTemplateString(*colConfig.URL, ctx)
data: colData, if err != nil {
navnames: make([]string, 0), helper.Log.Panicf("invalid template string for Collection Element.URL in '%s': %s", node.InputPath, err)
} }
errSrcText = "URL " + url
cacheKey = url
if cacheEntry, ok := colCache[url]; ok {
colData = cacheEntry.data
cacheEntry.hit++
} else {
helper.Log.Noticef("reading collection from: %s", errSrcText)
colData = helper.JSONWebRequest(url)
colCache[url] = &colCacheEntry{
data: colData,
navnames: make([]string, 0),
}
}
} else {
path := node.ResolveInputPath(colConfig.Directory.Path)
errSrcText = "DIR " + path
helper.Log.Noticef("reading collection from: %s", errSrcText)
d, err := ioutil.ReadDir(path)
if err != nil {
helper.Log.Panicf("could not read directory '%s': %s", path, err)
}
mStr := "."
if colConfig.Directory.MatchFilename != "" {
mStr = colConfig.Directory.MatchFilename
}
matcher, err := regexp.Compile(mStr)
if err != nil {
helper.Log.Panicf("could not compile regex for MatchFilename '%s' in '%s': %s", mStr, path, err)
}
if colConfig.Directory.ReverseOrder {
for i := len(d)/2 - 1; i >= 0; i-- {
opp := len(d) - 1 - i
d[i], d[opp] = d[opp], d[i]
}
}
fcolData := make([]pongo2.Context, 0)
for _, fh := range d {
if !fh.IsDir() && matcher.MatchString(fh.Name()) {
inFile := path + "/" + fh.Name()
md, err := ioutil.ReadFile(inFile)
if err != nil {
helper.Log.Panicf("could not read file '%s': %s", inFile, err)
}
_, ctx := node.processMarkdownWithHeader(md, inFile)
(*ctx)["FilenameMatch"] = helper.GetRegexpParams(matcher, fh.Name())
fcolData = append(fcolData, *ctx)
}
}
colData = fcolData
} }
node.ColMap[*colConfig.Name] = colData node.ColMap[*colConfig.Name] = colData
@ -64,7 +117,7 @@ func (node *TreeNode) handleCollections() {
entries, ok = colDataMap[navT.EntriesAttribute].([]interface{}) entries, ok = colDataMap[navT.EntriesAttribute].([]interface{})
if !ok { if !ok {
helper.Log.Debug(spew.Sdump(colDataMap)) helper.Log.Debug(spew.Sdump(colDataMap))
helper.Log.Panicf("invalid json data in [%s] from url '%s' for entries", navT.EntriesAttribute, url) helper.Log.Panicf("invalid json data in [%s] from '%s' for entries", navT.EntriesAttribute, errSrcText)
} }
} }
} else { } else {
@ -72,7 +125,7 @@ func (node *TreeNode) handleCollections() {
} }
if !ok { if !ok {
helper.Log.Debug(spew.Sdump(colData)) helper.Log.Debug(spew.Sdump(colData))
helper.Log.Panicf("invalid json data from url '%s', need array of objects for entries or object with configured NavTemplate.EntriesAttribute", url) helper.Log.Panicf("invalid json data from '%s', need array of objects for entries or object with configured NavTemplate.EntriesAttribute", errSrcText)
} }
// build navigation with detail sites // build navigation with detail sites
@ -85,7 +138,7 @@ func (node *TreeNode) handleCollections() {
var jsonCtx map[string]interface{} var jsonCtx map[string]interface{}
if jsonCtx, ok = colEl.(map[string]interface{}); !ok { if jsonCtx, ok = colEl.(map[string]interface{}); !ok {
helper.Log.Debug(spew.Sdump(colEl)) helper.Log.Debug(spew.Sdump(colEl))
helper.Log.Panicf("no json object for entry index %d from url '%s'", idx, url) helper.Log.Panicf("no json object for entry index %d from '%s'", idx, errSrcText)
} }
err = helper.Merge(&ctxE, pongo2.Context(jsonCtx)) err = helper.Merge(&ctxE, pongo2.Context(jsonCtx))
if err != nil { if err != nil {
@ -143,14 +196,14 @@ func (node *TreeNode) handleCollections() {
} }
} }
if l := len(colCache[url].navnames); colCache[url].hit > 1 && if l := len(colCache[cacheKey].navnames); colCache[cacheKey].hit > 1 &&
l > 0 && l > 0 &&
navname == colCache[url].navnames[l-1] { navname == colCache[cacheKey].navnames[l-1] {
// navname before used same url, so recursion loop // navname before used same url, so recursion loop
helper.Log.Panicf("collection request loop detected for in '%s' for url: %s", node.InputPath, url) helper.Log.Panicf("collection request loop detected for in '%s' for : %s", node.InputPath, errSrcText)
} }
colCache[url].navnames = append(colCache[url].navnames, navname) colCache[cacheKey].navnames = append(colCache[cacheKey].navnames, navname)
node.addSubNode(tpl, goTo, navname, colEl, dataKey, body, navT.Hidden) node.addSubNode(tpl, goTo, navname, colEl, dataKey, body, navT.Hidden)
} }

View File

@ -2,10 +2,18 @@ package mark2web
import "gitbase.de/apairon/mark2web/pkg/helper" import "gitbase.de/apairon/mark2web/pkg/helper"
// CollectionDirectoryConfig specifies how to handle a directory of markdown files as a collection
type CollectionDirectoryConfig struct {
Path string `yaml:"Path"`
MatchFilename string `yaml:"MatchFilename"`
ReverseOrder bool `yaml:"ReverseOrder"`
}
// CollectionConfig describes a collection // CollectionConfig describes a collection
type CollectionConfig struct { type CollectionConfig struct {
Name *string `yaml:"Name"` Name *string `yaml:"Name"`
URL *string `yaml:"URL"` URL *string `yaml:"URL"`
Directory *CollectionDirectoryConfig `yaml:"Directory"`
NavTemplate *struct { NavTemplate *struct {
EntriesAttribute string `yaml:"EntriesAttribute"` EntriesAttribute string `yaml:"EntriesAttribute"`
GoTo string `yaml:"GoTo"` GoTo string `yaml:"GoTo"`

View File

@ -65,24 +65,104 @@ func (node *TreeNode) ReadContentDir(inBase string, outBase string, dir string,
} }
} }
func (node *TreeNode) processMarkdownWithHeader(md []byte, errorRef string) (*PathConfig, *pongo2.Context) {
newConfig := new(PathConfig)
headerRegex := regexp.MustCompile("(?s)^---(.*?)\\r?\\n\\r?---\\r?\\n\\r?")
yamlData := headerRegex.Find(md)
if string(yamlData) != "" {
// replace tabs
yamlData = bytes.Replace(yamlData, []byte("\t"), []byte(" "), -1)
helper.Log.Debugf("found yaml header in '%s', merging config", errorRef)
err := yaml.Unmarshal(yamlData, newConfig)
if err != nil {
helper.Log.Panicf("could not parse YAML header from '%s': %s", errorRef, err)
}
helper.Log.Debug("merging config with upper config")
oldThis := newConfig.This
helper.Merge(newConfig, node.Config)
newConfig.This = oldThis
helper.Log.Debug(spew.Sdump(newConfig))
md = headerRegex.ReplaceAll(md, []byte(""))
} else {
helper.Merge(newConfig, node.Config)
}
// use --- for splitting document in markdown parts
regex := regexp.MustCompile("\\r?\\n\\r?---\\r?\\n\\r?")
inputParts := regex.Split(string(md), -1)
htmlParts := make([]*pongo2.Value, 0)
chromaRenderer := false
chromaStyle := "monokai"
if m := newConfig.Markdown; m != nil {
if m.ChromaRenderer != nil && *m.ChromaRenderer {
chromaRenderer = true
}
if m.ChromaStyle != nil && *m.ChromaStyle != "" {
chromaStyle = *m.ChromaStyle
}
}
for _, iPart := range inputParts {
htmlParts = append(htmlParts,
pongo2.AsSafeValue(
string(helper.RenderMarkdown([]byte(iPart), chromaRenderer, chromaStyle))))
}
// build navigation
navMap := make(map[string]*NavElement)
navSlice := make([]*NavElement, 0)
navActive := make([]*NavElement, 0)
node.buildNavigation(&navMap, &navSlice, &navActive)
// read yaml header as data for template
ctx := NewContext()
ctx["This"] = newConfig.This
ctx["Meta"] = newConfig.Meta
ctx["Markdown"] = newConfig.Markdown
ctx["Data"] = newConfig.Data
ctx["ColMap"] = node.root.ColMap // root as NavMap and NavSlice, for sub go to NavElement.ColMap
ctx["NavMap"] = navMap
ctx["NavSlice"] = navSlice
ctx["NavActive"] = navActive
ctx["Body"] = pongo2.AsSafeValue(string(helper.RenderMarkdown(md, chromaRenderer, chromaStyle)))
ctx["BodyParts"] = htmlParts
ctx["CurrentPath"] = node.CurrentNavPath()
// set active nav element
if len(navActive) > 0 {
ctx["NavElement"] = navActive[len(navActive)-1]
} else {
// if no active path to content, we are in root dir
ctx["NavElement"] = &NavElement{
GoTo: node.BackToRootPath(),
Active: true,
ColMap: node.ColMap,
Data: node.Config.Data,
This: node.Config.This,
SubMap: &navMap,
SubSlice: &navSlice,
}
}
return newConfig, &ctx
}
// ProcessContent walks recursivly through the input paths and processes all files for output // ProcessContent walks recursivly through the input paths and processes all files for output
func (node *TreeNode) ProcessContent() { func (node *TreeNode) ProcessContent() {
helper.CreateDirectory(node.OutputPath) helper.CreateDirectory(node.OutputPath)
curNavPath := strings.TrimPrefix(node.OutputPath, Config.Directories.Output)
curNavPath = strings.TrimPrefix(curNavPath, "/")
curNavPath = path.Clean(curNavPath)
if curNavPath == "." {
curNavPath = ""
}
if node.root != node { if node.root != node {
// write htaccess for rewrites, root will be written in WriteWebserverConfig() // write htaccess for rewrites, root will be written in WriteWebserverConfig()
goTo := node.Config.This.GoTo goTo := node.Config.This.GoTo
if goTo != nil && *goTo != "" { if goTo != nil && *goTo != "" {
goToFixed := *goTo goToFixed := *goTo
if strings.HasPrefix(goToFixed, "/") { if strings.HasPrefix(goToFixed, "/") {
goToFixed = helper.BackToRoot(curNavPath) + goToFixed goToFixed = node.BackToRootPath() + goToFixed
} }
goToFixed = path.Clean(goToFixed) goToFixed = path.Clean(goToFixed)
@ -91,82 +171,63 @@ func (node *TreeNode) ProcessContent() {
} }
for _, file := range node.InputFiles { for _, file := range node.InputFiles {
var input []byte
inFile := "InputString" inFile := "InputString"
if file != "" {
inFile = node.InputPath + "/" + file
helper.Log.Debugf("reading file: %s", inFile)
var err error
input, err = ioutil.ReadFile(inFile)
if err != nil {
helper.Log.Panicf("could not read '%s':%s", inFile, err)
}
helper.Log.Infof("processing input file '%s'", inFile)
} else {
// use input string if available and input filename == ""
var inputString *string
if i := node.Config.Index; i != nil {
inputString = i.InputString
}
if inputString != nil {
helper.Log.Debugf("using input string instead of file")
input = []byte(*inputString)
}
}
newConfig := new(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)
helper.Log.Debugf("found yaml header in '%s', merging config", inFile)
err := yaml.Unmarshal(yamlData, newConfig)
if err != nil {
helper.Log.Panicf("could not parse YAML header from '%s': %s", inFile, err)
}
helper.Log.Debug("merging config with upper config")
oldThis := newConfig.This
helper.Merge(newConfig, node.Config)
newConfig.This = oldThis
helper.Log.Debug(spew.Sdump(newConfig))
input = regex.ReplaceAll(input, []byte(""))
} else {
helper.Merge(newConfig, node.Config)
}
// ignore ??? // ignore ???
ignoreFile := false ignoreFile := false
var ignoreRegex *string if file != "" {
var stripRegex *string inFile = node.InputPath + "/" + file
var outputExt *string var ignoreRegex *string
if f := newConfig.Filename; f != nil { if f := node.Config.Filename; f != nil {
ignoreRegex = f.Ignore ignoreRegex = f.Ignore
stripRegex = f.Strip }
outputExt = f.OutputExtension if ignoreRegex != nil && *ignoreRegex != "" {
} regex, err := regexp.Compile(*ignoreRegex)
if ignoreRegex != nil && *ignoreRegex != "" { if err != nil {
regex, err := regexp.Compile(*ignoreRegex) helper.Log.Panicf("could not compile filename.ignore regexp '%s' for file '%s': %s", *ignoreRegex, inFile, err)
if err != nil { }
helper.Log.Panicf("could not compile filename.ignore regexp '%s' for file '%s': %s", *ignoreRegex, inFile, err) ignoreFile = regex.MatchString(file)
} }
ignoreFile = regex.MatchString(file)
} }
if ignoreFile { if ignoreFile {
helper.Log.Infof("ignoring file '%s', because of filename.ignore", inFile) helper.Log.Infof("ignoring file '%s', because of filename.ignore", inFile)
} else { } else {
var input []byte
if file != "" {
helper.Log.Debugf("reading file: %s", inFile)
var err error
input, err = ioutil.ReadFile(inFile)
if err != nil {
helper.Log.Panicf("could not read '%s':%s", inFile, err)
}
helper.Log.Infof("processing input file '%s'", inFile)
} else {
// use input string if available and input filename == ""
var inputString *string
if i := node.Config.Index; i != nil {
inputString = i.InputString
}
if inputString != nil {
helper.Log.Debugf("using input string instead of file")
input = []byte(*inputString)
}
}
newConfig, ctx := node.processMarkdownWithHeader(input, inFile)
// build output filename // build output filename
outputFilename := file outputFilename := file
var stripRegex *string
var outputExt *string
if f := newConfig.Filename; f != nil {
stripRegex = f.Strip
outputExt = f.OutputExtension
}
var indexInputFile *string var indexInputFile *string
var indexOutputFile *string var indexOutputFile *string
if i := newConfig.Index; i != nil { if i := newConfig.Index; i != nil {
@ -194,71 +255,14 @@ func (node *TreeNode) ProcessContent() {
outFile := node.OutputPath + "/" + outputFilename outFile := node.OutputPath + "/" + outputFilename
helper.Log.Debugf("using '%s' as output file", outFile) helper.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)
chromaRenderer := false
chromaStyle := "monokai"
if m := newConfig.Markdown; m != nil {
if m.ChromaRenderer != nil && *m.ChromaRenderer {
chromaRenderer = true
}
if m.ChromaStyle != nil && *m.ChromaStyle != "" {
chromaStyle = *m.ChromaStyle
}
}
for _, iPart := range inputParts {
htmlParts = append(htmlParts,
pongo2.AsSafeValue(
string(helper.RenderMarkdown([]byte(iPart), chromaRenderer, chromaStyle))))
}
// build navigation
navMap := make(map[string]*NavElement)
navSlice := make([]*NavElement, 0)
navActive := make([]*NavElement, 0)
buildNavigation(node.root, &navMap, &navSlice, &navActive, curNavPath)
// read yaml header as data for template
ctx := NewContext()
ctx["This"] = newConfig.This
ctx["Meta"] = newConfig.Meta
ctx["Markdown"] = newConfig.Markdown
ctx["Data"] = newConfig.Data
ctx["ColMap"] = node.root.ColMap // root as NavMap and NavSlice, for sub go to NavElement.ColMap
ctx["NavMap"] = navMap
ctx["NavSlice"] = navSlice
ctx["NavActive"] = navActive
ctx["Body"] = pongo2.AsSafeValue(string(helper.RenderMarkdown(input, chromaRenderer, chromaStyle)))
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: helper.BackToRoot(curNavPath),
Active: true,
ColMap: node.ColMap,
Data: node.Config.Data,
This: node.Config.This,
SubMap: &navMap,
SubSlice: &navSlice,
}
}
helper.Log.Debugf("rendering template '%s' for '%s'", *newConfig.Template, outFile) helper.Log.Debugf("rendering template '%s' for '%s'", *newConfig.Template, outFile)
templateFilename := *newConfig.Template templateFilename := *newConfig.Template
result, err := renderTemplate(*newConfig.Template, node, newConfig, &ctx) result, err := renderTemplate(*newConfig.Template, node, newConfig, ctx)
if err != nil { if err != nil {
helper.Log.Panicf("could not execute template '%s' for input file '%s': %s", templateFilename, inFile, err) helper.Log.Panicf("could not execute template '%s' for input file '%s': %s", templateFilename, inFile, err)
} }
result = fixAssetsPath(result, curNavPath) result = node.fixAssetsPath(result)
helper.Log.Noticef("writing to output file: %s", outFile) helper.Log.Noticef("writing to output file: %s", outFile)
err = ioutil.WriteFile(outFile, []byte(result), 0644) err = ioutil.WriteFile(outFile, []byte(result), 0644)

View File

@ -25,7 +25,11 @@ type NavElement struct {
} }
// buildNavigation builds the navigation trees for use in templates // buildNavigation builds the navigation trees for use in templates
func buildNavigation(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string) { func (node *TreeNode) buildNavigation(curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement) {
buildNavigationRecursive(node.root, curNavMap, curNavSlice, navActive, node.CurrentNavPath(), node.BackToRootPath())
}
func buildNavigationRecursive(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSlice *[]*NavElement, navActive *[]*NavElement, activeNav string, backToRoot string) {
for _, el := range tree.Sub { for _, el := range tree.Sub {
if el.Hidden { if el.Hidden {
continue // ignore hidden nav points from collections continue // ignore hidden nav points from collections
@ -88,8 +92,7 @@ func buildNavigation(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSl
if activeNav != "" && activeNav != "/" { if activeNav != "" && activeNav != "/" {
// calculate relative path // calculate relative path
bToRoot := helper.BackToRoot(activeNav) navEl.GoTo = backToRoot + navEl.GoTo
navEl.GoTo = bToRoot + navEl.GoTo
navEl.GoTo = path.Clean(navEl.GoTo) navEl.GoTo = path.Clean(navEl.GoTo)
} }
@ -98,6 +101,6 @@ func buildNavigation(tree *TreeNode, curNavMap *map[string]*NavElement, curNavSl
*curNavSlice = append(*curNavSlice, &navEl) *curNavSlice = append(*curNavSlice, &navEl)
} }
buildNavigation(el, &subMap, &subSlice, navActive, activeNav) buildNavigationRecursive(el, &subMap, &subSlice, navActive, activeNav, backToRoot)
} }
} }

View File

@ -3,36 +3,58 @@ package mark2web
import ( import (
"path" "path"
"strings" "strings"
"gitbase.de/apairon/mark2web/pkg/helper"
) )
// ResolveNavPath fixes nav target relative to current navigation path // ResolveNavPath fixes nav target relative to current navigation path
func ResolveNavPath(target string) string { func (node *TreeNode) ResolveNavPath(target string) string {
curNavPath := (*CurrentContext)["CurrentPath"].(string)
if strings.HasPrefix(target, "/") { if strings.HasPrefix(target, "/") {
target = helper.BackToRoot(curNavPath) + target target = node.BackToRootPath() + target
} }
target = path.Clean(target) target = path.Clean(target)
return target return target
} }
// ResolveOutputPath fixes output directory relative to current navigation path // ResolveOutputPath fixes output directory relative to current navigation path
func ResolveOutputPath(target string) string { func (node *TreeNode) ResolveOutputPath(target string) string {
if strings.HasPrefix(target, "/") { if strings.HasPrefix(target, "/") {
target = Config.Directories.Output + "/" + target target = Config.Directories.Output + "/" + target
} else { } else {
target = CurrentTreeNode.OutputPath + "/" + target target = node.OutputPath + "/" + target
} }
return path.Clean(target) return path.Clean(target)
} }
// ResolveInputPath fixes input directory relative to current navigation path // ResolveInputPath fixes input directory relative to current navigation path
func ResolveInputPath(target string) string { func (node *TreeNode) ResolveInputPath(target string) string {
if strings.HasPrefix(target, "/") { if strings.HasPrefix(target, "/") {
target = Config.Directories.Input + "/" + target target = Config.Directories.Input + "/" + target
} else { } else {
target = CurrentTreeNode.InputPath + "/" + target target = node.InputPath + "/" + target
} }
return path.Clean(target) return path.Clean(target)
} }
// CurrentNavPath is current navigation path for this node
func (node *TreeNode) CurrentNavPath() string {
curNavPath := strings.TrimPrefix(node.OutputPath, Config.Directories.Output)
curNavPath = strings.TrimPrefix(curNavPath, "/")
curNavPath = path.Clean(curNavPath)
if curNavPath == "." {
curNavPath = ""
}
return curNavPath
}
// BackToRootPath builds ../../ string
func (node *TreeNode) BackToRootPath() string {
curNavPath := node.CurrentNavPath()
tmpPath := ""
if curNavPath != "" {
for i := strings.Count(curNavPath, "/") + 1; i > 0; i-- {
tmpPath += "../"
}
}
return tmpPath
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

View File

@ -1,150 +0,0 @@
---
Data:
background: /img/wire.jpg
slider:
- img: /img/wire.jpg
alt:
opacity: 1
---
# Konfiguration
Die Konfigurationsdatein sind im YAML-Format gehalten (siehe: [Wikipedia](https://de.wikipedia.org/wiki/YAML)).
## globale Einstellungen
Die obersten Verzeichnis sich befindende Datei `config.yml` kann z.B. folgenden Inhalt haben:
```yaml
Webserver:
Type: "apache"
Assets:
FromPath: "assets"
ToPath: "assets"
Action: "copy"
FixTemplate:
Find: "\\.\\./assets/"
Replace: ""
OtherFiles:
Action: "copy"
```
### Sektion `Webserver:`
#### `Type:`
Derzeit wird hier nur der Wert `apache` unterstützt. Ist dieser Wert gesetzt werden automatische .htaccess-Dateien mit mod_rewrite-Anweisungen generiert, die eine saubere Weiterleitung bei entsprechenden Konfig-Anweisungen im `content`-Verzeichnis ermöglichen.
### Sektion `Assets:`
`Assets:` steuert, wie mit Bild/JS/CSS Dateien umgegangen werden soll.
#### `FromPath:`
Lage des Asset-Verzeichnis unterhalb des `content`-Verzeichnis
#### `ToPath:`
Zielverzeichnis im Ausgabe-Verzeichnis der fertig generierten Website
#### `Action:`
Derzeit nur `copy`, also das Kopieren der Dateien und Unterordner ins Zielverzeichnis
#### `FixTemplate:`
Wenn hier `Find:` (regulärer Ausdruck) und `Replace:` (Ersetzung) angeben sind, werden die gefundenden Pfadangaben in der generierten HTML-Dateien durch den korrekten relativen Pfad zum Asset-Verzeichnis ersetzt.
### Sektion `OtherFiles:`
`OtherFiles:` definiert, wie mit anderen Dateien innerhalb des `content`-Verzeichnis umgegangen werden soll.
#### `Action:`
Derzeit nur `copy`, also das Kopieren der Dateien in das entsprechende Unterverzeichnis im Ausgabe-Verzeichnis
---
## Konfiguration im `content`-Verzeichnis
Im `content`-Verzeichnis, sowie in jedem Unterverzeichnis unterhalb von `content` kann sich eine `config.yml`-Datei befinden, wie aus folgendem Beispiel:
```yaml
This:
GoTo: "/de/service/impressum/"
Navname: "Impressumsverweis"
```
oder
```yaml
This:
Navname: "FAQ's"
Data:
slogan: "Wer nicht fragt, bekommt keine Antwort."
Template: "base.html"
Index:
InputFile: "README.md"
OutputFile: "index.html"
Meta:
Title: "Fragen und Antworten"
Description: "Dies ist die Fragen und Antworten Unterseite."
Keywords: "FAQ, Fragen, Antworten"
Data:
background: "bg.jpg"
slider:
- img: "assets/img/slider1.jpg"
alt: "Alternativtext 1"
- img: "assets/img/slider2.jpg"
alt: "Alternativtext 2"
- img: "assets/img/slider3.jpg"
alt: "Alternativtext 3"
```
### `This:` Sektion
Sämtlich Werte unterhalb dieser Sektion gelten nur für den Inhalt, bzw. Navigationspunkt in dessen Ordner sich die `config.yml` befindet. Die Werte werden nicht an Unterordner wertervererbt.
#### `GoTo:`
Falls der Navigationspunkt selbst keinen Inhalt darstellen soll, sondern nur weiterleiten soll, so wird hier das Weiterleitungsziel eingegeben.
Das Ziel ist der absolute (startend mit `/`) oder relative Pfad zum Zielnavigationspunkt.
Die Schreibweise des Pfades ist so zu verwenden, wie der Pfad nach Umschreibung und Säuberung des Pfades im Zielverzeichnis dargestellt wird.
Aus `de/mainnav/03_Fragen und Antworten` wird also z.B. `de/mainnav/fragen-und-antworten`.
#### `Navname:`
Dieser Wert überschreibt den aus dem Ordnernamen automatisch abgeleiteten Navigationspunkt-Namen. Dies ist zum Beispiel dann nützlich, wenn Sonderzeichen im Verzeichnisnamen nicht vorkommen sollen, aber im Namen des Navigationspunkts gebraucht werden.
#### `Data:`
Unterhalb von `Data:` können beliebige Datenstrukturen erfasst werden. Da diese Struktur unterhalb von `This:` angeordnet ist, werden auch die Daten nicht weiter an Unterordner vererbt.
Hier können z.B. Informationen zum Navigationspunkt abgelegt werden, die im Template Zusatzinformationen darstellen (z.B. ein Slogan zu einem Navigationspunkt).
### `Meta:` Sektion
Unter `Title:`, `Description:` und `Keywords:` werden die typischen Metaangaben abgelegt, die im
```html
<head>
...
</head>
```
übllicherweise Verwendung finden. Die entsprechenden Platzhalter stehen im Template zur Verfügung.
`Meta:` vererbt seine individuellen Informationen an die Unterordner weiter, sofern diese dort nicht selbst in einer `config.yml` oder im Kopf der Markdown-Datei definiert sind.
### `Data:` Sektion
`Data:` an dieser Stelle kann, wie auch `Data:` unterhalb von `This:`, beliebige Daten aufnehmen. Die Daten hier allerdings werden an Unterordner weitervererbt, sofern diese nicht dort oder in der Markdown-Datei selbst festegelegt überschrieben wurden.

View File

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

View File

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

View File

@ -0,0 +1,17 @@
Die obersten Verzeichnis sich befindende Datei `config.yml` kann z.B. folgenden Inhalt haben:
```yaml
Webserver:
Type: "apache"
Assets:
FromPath: "assets"
ToPath: "assets"
Action: "copy"
FixTemplate:
Find: "\\.\\./assets/"
Replace: ""
OtherFiles:
Action: "copy"
```

View File

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

View File

@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Derzeit wird hier nur der Wert `apache` unterstützt. Ist dieser Wert gesetzt werden automatische .htaccess-Dateien mit mod_rewrite-Anweisungen generiert, die eine saubere Weiterleitung bei entsprechenden Konfig-Anweisungen im `content`-Verzeichnis ermöglichen.

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Wenn hier `Find:` (regulärer Ausdruck) und `Replace:` (Ersetzung) angeben sind, werden die gefundenden Pfadangaben in der generierten HTML-Dateien durch den korrekten relativen Pfad zum Asset-Verzeichnis ersetzt.

View File

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

View File

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

View File

@ -0,0 +1,45 @@
---
Data:
Version: "ab v1.0"
---
Im `content`-Verzeichnis, sowie in jedem Unterverzeichnis unterhalb von `content` kann sich eine `config.yml`-Datei befinden, wie aus folgendem Beispiel:
```yaml
This:
GoTo: "/de/service/impressum/"
Navname: "Impressumsverweis"
```
oder
```yaml
This:
Navname: "FAQ's"
Data:
slogan: "Wer nicht fragt, bekommt keine Antwort."
Template: "base.html"
Index:
InputFile: "README.md"
OutputFile: "index.html"
Meta:
Title: "Fragen und Antworten"
Description: "Dies ist die Fragen und Antworten Unterseite."
Keywords: "FAQ, Fragen, Antworten"
Data:
background: "bg.jpg"
slider:
- img: "assets/img/slider1.jpg"
alt: "Alternativtext 1"
- img: "assets/img/slider2.jpg"
alt: "Alternativtext 2"
- img: "assets/img/slider3.jpg"
alt: "Alternativtext 3"
```

View File

@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Sämtlich Werte unterhalb dieser Sektion gelten nur für den Inhalt, bzw. Navigationspunkt in dessen Ordner sich die `config.yml` befindet. Die Werte werden nicht an Unterordner wertervererbt.

View File

@ -0,0 +1,10 @@
---
Data:
Version: "ab v1.0"
---
Falls der Navigationspunkt selbst keinen Inhalt darstellen soll, sondern nur weiterleiten soll, so wird hier das Weiterleitungsziel eingegeben.
Das Ziel ist der absolute (startend mit `/`) oder relative Pfad zum Zielnavigationspunkt.
Die Schreibweise des Pfades ist so zu verwenden, wie der Pfad nach Umschreibung und Säuberung des Pfades im Zielverzeichnis dargestellt wird.
Aus `de/mainnav/03_Fragen und Antworten` wird also z.B. `de/mainnav/fragen-und-antworten`.

View File

@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
Dieser Wert überschreibt den aus dem Ordnernamen automatisch abgeleiteten Navigationspunkt-Namen. Dies ist zum Beispiel dann nützlich, wenn Sonderzeichen im Verzeichnisnamen nicht vorkommen sollen, aber im Namen des Navigationspunkts gebraucht werden.

View File

@ -0,0 +1,8 @@
---
Data:
Version: "ab v1.0"
---
Unterhalb von `Data:` können beliebige Datenstrukturen erfasst werden. Da diese Struktur unterhalb von `This:` angeordnet ist, werden auch die Daten nicht weiter an Unterordner vererbt.
Hier können z.B. Informationen zum Navigationspunkt abgelegt werden, die im Template Zusatzinformationen darstellen (z.B. ein Slogan zu einem Navigationspunkt).

View File

@ -0,0 +1,17 @@
---
Data:
Version: "ab v1.0"
---
Unter `Title:`, `Description:` und `Keywords:` werden die typischen Metaangaben abgelegt, die im
```html
<head>
...
</head>
```
übllicherweise Verwendung finden. Die entsprechenden Platzhalter stehen im Template zur Verfügung.
`Meta:` vererbt seine individuellen Informationen an die Unterordner weiter, sofern diese dort nicht selbst in einer `config.yml` oder im Kopf der Markdown-Datei definiert sind.

View File

@ -0,0 +1,7 @@
---
Data:
Version: "ab v1.0"
---
`Data:` an dieser Stelle kann, wie auch `Data:` unterhalb von `This:`, beliebige Daten aufnehmen. Die Daten hier allerdings werden an Unterordner weitervererbt, sofern diese nicht dort oder in der Markdown-Datei selbst festegelegt überschrieben wurden.

View File

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

View File

@ -293,3 +293,13 @@ code.language-mermaid svg {
display:inline-block; display:inline-block;
color: #fff; color: #fff;
} }
.versionBadge {
text-align: right;
color: #444;
font-size: 10px;
padding: 3px;
background: #ddd;
border: #444 solid 1px;
border-radius: 5px;
}

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block part0 %}
{{ Body }}
{% endblock part0 %}
{% block part1 %}
{% for e in NavElement.ColMap.doccoll %}
{% with e.FilenameMatch.lowdash|count + 1 as h %}
<h{{ h }}>
{{ e.FilenameMatch.title }}
{% if e.Data.Version %}
<span class="versionBadge">{{ e.Data.Version }}</span>
{% endif %}
</h{{ h }}>
{% endwith %}
{{ e.Body }}
{% endfor %}
{% endblock part1 %}

View File

@ -0,0 +1,5 @@
function count(el, param) {
return el.length
}
module.exports = count;