mirror of
https://github.com/kovetskiy/mark.git
synced 2026-05-02 05:12:35 +00:00
implement macros & includes
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/reconquest/faces/logger"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
@@ -56,7 +56,7 @@ func EnsureAncestry(
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
logger.Debugf(
|
||||
log.Debugf(
|
||||
"empty pages under %q to be created: %s",
|
||||
parent.Title,
|
||||
strings.Join(rest, ` > `),
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
|
||||
155
pkg/mark/includes/templates.go
Normal file
155
pkg/mark/includes/templates.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package includes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
var (
|
||||
reIncludeDirective = regexp.MustCompile(`(?s)<!-- Include: (\S+)(.*?)-->`)
|
||||
)
|
||||
|
||||
func LoadTemplate(
|
||||
path string,
|
||||
templates *template.Template,
|
||||
) (string, *template.Template, error) {
|
||||
var (
|
||||
name = strings.TrimSuffix(path, filepath.Ext(path))
|
||||
facts = karma.Describe("name", name)
|
||||
)
|
||||
|
||||
if template := templates.Lookup(name); template != nil {
|
||||
return name, template, nil
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
body, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
err = facts.Format(
|
||||
err,
|
||||
"unable to read template file",
|
||||
)
|
||||
|
||||
return name, nil, err
|
||||
}
|
||||
|
||||
templates, err = templates.New(name).Parse(string(body))
|
||||
if err != nil {
|
||||
err = facts.Format(
|
||||
err,
|
||||
"unable to parse template",
|
||||
)
|
||||
|
||||
return name, nil, err
|
||||
}
|
||||
|
||||
return name, templates, nil
|
||||
}
|
||||
|
||||
func ProcessIncludes(
|
||||
contents []byte,
|
||||
templates *template.Template,
|
||||
) (*template.Template, []byte, bool, error) {
|
||||
vardump := func(
|
||||
facts *karma.Context,
|
||||
data map[string]interface{},
|
||||
) *karma.Context {
|
||||
for key, value := range data {
|
||||
key = "var " + key
|
||||
facts = facts.Describe(
|
||||
key,
|
||||
strings.ReplaceAll(
|
||||
fmt.Sprint(value),
|
||||
"\n",
|
||||
"\n"+strings.Repeat(" ", len(key)+2),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
var (
|
||||
recurse bool
|
||||
err error
|
||||
)
|
||||
|
||||
contents = reIncludeDirective.ReplaceAllFunc(
|
||||
contents,
|
||||
func(spec []byte) []byte {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
groups := reIncludeDirective.FindSubmatch(spec)
|
||||
|
||||
var (
|
||||
path, config = string(groups[1]), groups[2]
|
||||
data = map[string]interface{}{}
|
||||
|
||||
facts = karma.Describe("path", path)
|
||||
)
|
||||
|
||||
err = yaml.Unmarshal(config, &data)
|
||||
if err != nil {
|
||||
err = facts.
|
||||
Describe("config", string(config)).
|
||||
Format(
|
||||
err,
|
||||
"unable to unmarshal template data config",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Tracef(vardump(facts, data), "including template %q", path)
|
||||
|
||||
var name string
|
||||
|
||||
name, templates, err = LoadTemplate(path, templates)
|
||||
if err != nil {
|
||||
err = facts.Format(err, "unable to load template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
facts = facts.Describe("name", name)
|
||||
|
||||
template := templates.Lookup(string(name))
|
||||
if template == nil {
|
||||
err = facts.Reason("template not found")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err = template.Execute(&buffer, data)
|
||||
if err != nil {
|
||||
err = vardump(facts, data).Format(
|
||||
err,
|
||||
"unable to execute template",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
recurse = true
|
||||
|
||||
return buffer.Bytes()
|
||||
},
|
||||
)
|
||||
|
||||
return templates, contents, recurse, err
|
||||
}
|
||||
162
pkg/mark/macro/macro.go
Normal file
162
pkg/mark/macro/macro.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package macro
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/kovetskiy/mark/pkg/mark/includes"
|
||||
"github.com/reconquest/karma-go"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var reMacroDirective = regexp.MustCompile(
|
||||
`(?s)<!-- Macro: ([^\n]+)\n\s*Template: (\S+)\n(.*?)-->`,
|
||||
)
|
||||
|
||||
type Macro struct {
|
||||
Regexp *regexp.Regexp
|
||||
Template *template.Template
|
||||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
func (macro *Macro) Apply(
|
||||
content []byte,
|
||||
) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
content = macro.Regexp.ReplaceAllFunc(
|
||||
content,
|
||||
func(match []byte) []byte {
|
||||
config := macro.configure(
|
||||
macro.Config,
|
||||
macro.Regexp.FindSubmatch(match),
|
||||
)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err = macro.Template.Execute(&buffer, config)
|
||||
if err != nil {
|
||||
err = karma.Format(
|
||||
err,
|
||||
"unable to execute macros template",
|
||||
)
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
},
|
||||
)
|
||||
|
||||
return content, err
|
||||
}
|
||||
|
||||
func (macro *Macro) configure(node interface{}, groups [][]byte) interface{} {
|
||||
switch node := node.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
for key, value := range node {
|
||||
node[key] = macro.configure(value, groups)
|
||||
}
|
||||
|
||||
return node
|
||||
case map[string]interface{}:
|
||||
for key, value := range node {
|
||||
node[key] = macro.configure(value, groups)
|
||||
}
|
||||
|
||||
return node
|
||||
case []interface{}:
|
||||
for key, value := range node {
|
||||
node[key] = macro.configure(value, groups)
|
||||
}
|
||||
|
||||
return node
|
||||
case string:
|
||||
for i, group := range groups {
|
||||
node = strings.ReplaceAll(
|
||||
node,
|
||||
fmt.Sprintf("${%d}", i),
|
||||
string(group),
|
||||
)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func LoadMacros(
|
||||
contents []byte,
|
||||
templates *template.Template,
|
||||
) ([]Macro, []byte, error) {
|
||||
var err error
|
||||
|
||||
var macros []Macro
|
||||
|
||||
contents = reMacroDirective.ReplaceAllFunc(
|
||||
contents,
|
||||
func(spec []byte) []byte {
|
||||
if err != nil {
|
||||
return spec
|
||||
}
|
||||
|
||||
groups := reMacroDirective.FindSubmatch(spec)
|
||||
|
||||
var (
|
||||
expr, path, config = groups[1], string(groups[2]), groups[3]
|
||||
|
||||
macro Macro
|
||||
)
|
||||
|
||||
_, macro.Template, err = includes.LoadTemplate(path, templates)
|
||||
|
||||
if err != nil {
|
||||
err = karma.Format(err, "unable to load template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
facts := karma.
|
||||
Describe("template", path).
|
||||
Describe("expr", string(expr))
|
||||
|
||||
macro.Regexp, err = regexp.Compile(string(expr))
|
||||
if err != nil {
|
||||
err = facts.
|
||||
Format(
|
||||
err,
|
||||
"unable to compile macros regexp",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(config, ¯o.Config)
|
||||
if err != nil {
|
||||
err = facts.
|
||||
Describe("config", string(config)).
|
||||
Format(
|
||||
err,
|
||||
"unable to unmarshal template data config",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Tracef(
|
||||
facts.Describe("config", macro.Config),
|
||||
"loaded macro %q",
|
||||
expr,
|
||||
)
|
||||
|
||||
macros = append(macros, macro)
|
||||
|
||||
return []byte{}
|
||||
},
|
||||
)
|
||||
|
||||
return macros, contents, err
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/reconquest/faces/logger"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ func ResolvePage(
|
||||
path := meta.Parents
|
||||
path = append(path, meta.Title)
|
||||
|
||||
logger.Debugf(
|
||||
log.Debugf(
|
||||
"resolving page path: ??? > %s",
|
||||
strings.Join(path, ` > `),
|
||||
)
|
||||
@@ -74,7 +74,7 @@ func ResolvePage(
|
||||
titles = append(titles, parent.Title)
|
||||
|
||||
log.Infof(
|
||||
nil,
|
||||
nil,
|
||||
"page will be stored under path: %s > %s",
|
||||
strings.Join(titles, ` > `),
|
||||
meta.Title,
|
||||
|
||||
@@ -2,14 +2,17 @@ package mark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
type ConfluenceRenderer struct {
|
||||
blackfriday.Renderer
|
||||
|
||||
Stdlib *stdlib.Lib
|
||||
}
|
||||
|
||||
func (renderer ConfluenceRenderer) BlockCode(
|
||||
@@ -17,22 +20,16 @@ func (renderer ConfluenceRenderer) BlockCode(
|
||||
text []byte,
|
||||
lang string,
|
||||
) {
|
||||
out.WriteString(MacroCode{lang, text}.Render())
|
||||
}
|
||||
|
||||
type MacroCode struct {
|
||||
lang string
|
||||
code []byte
|
||||
}
|
||||
|
||||
func (code MacroCode) Render() string {
|
||||
return fmt.Sprintf(
|
||||
`<ac:structured-macro ac:name="code">`+
|
||||
`<ac:parameter ac:name="language">%s</ac:parameter>`+
|
||||
`<ac:parameter ac:name="collapse">false</ac:parameter>`+
|
||||
`<ac:plain-text-body><![CDATA[%s]]></ac:plain-text-body>`+
|
||||
`</ac:structured-macro>`,
|
||||
code.lang, code.code,
|
||||
renderer.Stdlib.Templates.ExecuteTemplate(
|
||||
out,
|
||||
"ac:code",
|
||||
struct {
|
||||
Language string
|
||||
Text string
|
||||
}{
|
||||
lang,
|
||||
string(text),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,12 +38,13 @@ func (code MacroCode) Render() string {
|
||||
// <a href="ac:rich-text-body">ac:rich-text-body</a> for whatever reason.
|
||||
func CompileMarkdown(
|
||||
markdown []byte,
|
||||
) []byte {
|
||||
stdlib *stdlib.Lib,
|
||||
) string {
|
||||
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
|
||||
|
||||
colon := regexp.MustCompile(`---BLACKFRIDAY-COLON---`)
|
||||
|
||||
tags := regexp.MustCompile(`<(/?\S+):(\S+)>`)
|
||||
tags := regexp.MustCompile(`<(/?\S+?):(\S+?)>`)
|
||||
|
||||
markdown = tags.ReplaceAll(
|
||||
markdown,
|
||||
@@ -54,7 +52,7 @@ func CompileMarkdown(
|
||||
)
|
||||
|
||||
renderer := ConfluenceRenderer{
|
||||
blackfriday.HtmlRenderer(
|
||||
Renderer: blackfriday.HtmlRenderer(
|
||||
blackfriday.HTML_USE_XHTML|
|
||||
blackfriday.HTML_USE_SMARTYPANTS|
|
||||
blackfriday.HTML_SMARTYPANTS_FRACTIONS|
|
||||
@@ -62,6 +60,8 @@ func CompileMarkdown(
|
||||
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES,
|
||||
"", "",
|
||||
),
|
||||
|
||||
Stdlib: stdlib,
|
||||
}
|
||||
|
||||
html := blackfriday.MarkdownOptions(
|
||||
@@ -88,5 +88,5 @@ func CompileMarkdown(
|
||||
|
||||
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
|
||||
|
||||
return html
|
||||
return string(html)
|
||||
}
|
||||
|
||||
@@ -4,28 +4,12 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/lorg"
|
||||
"github.com/reconquest/cog"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
)
|
||||
|
||||
func discarder() *lorg.Log {
|
||||
stderr := lorg.NewLog()
|
||||
stderr.SetOutput(ioutil.Discard)
|
||||
return stderr
|
||||
}
|
||||
|
||||
var (
|
||||
log = cog.NewLogger(discarder())
|
||||
)
|
||||
|
||||
func SetLogger(logger *cog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
|
||||
const (
|
||||
HeaderParent = `Parent`
|
||||
HeaderSpace = `Space`
|
||||
|
||||
147
pkg/mark/stdlib/stdlib.go
Normal file
147
pkg/mark/stdlib/stdlib.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"github.com/kovetskiy/mark/pkg/mark/macro"
|
||||
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
type Lib struct {
|
||||
Macros []macro.Macro
|
||||
Templates *template.Template
|
||||
}
|
||||
|
||||
func New(api *confluence.API) (*Lib, error) {
|
||||
var (
|
||||
lib Lib
|
||||
err error
|
||||
)
|
||||
|
||||
lib.Templates, err = templates(api)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lib.Macros, err = macros(lib.Templates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lib, nil
|
||||
}
|
||||
|
||||
func macros(templates *template.Template) ([]macro.Macro, error) {
|
||||
text := func(line ...string) []byte {
|
||||
return []byte(strings.Join(line, "\n"))
|
||||
}
|
||||
|
||||
macros, _, err := macro.LoadMacros(
|
||||
[]byte(text(
|
||||
`<!-- Macro: @\{([^}]+)\}`,
|
||||
` Template: ac:link:user`,
|
||||
` Name: ${1} -->`,
|
||||
|
||||
// TODO(seletskiy): more macros here
|
||||
)),
|
||||
|
||||
templates,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return macros, nil
|
||||
}
|
||||
|
||||
func templates(api *confluence.API) (*template.Template, error) {
|
||||
text := func(line ...string) string {
|
||||
return strings.Join(line, ``)
|
||||
}
|
||||
|
||||
templates := template.New(`stdlib`).Funcs(
|
||||
template.FuncMap{
|
||||
"user": func(name string) *confluence.User {
|
||||
user, err := api.GetUserByName(name)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return user
|
||||
},
|
||||
|
||||
// The only way to escape CDATA end marker ']]>' is to split it
|
||||
// into two CDATA sections.
|
||||
"cdata": func(data string) string {
|
||||
return strings.ReplaceAll(
|
||||
data,
|
||||
"]]>",
|
||||
"]]><![CDATA[]]]]><![CDATA[>",
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
for name, body := range map[string]string{
|
||||
// This template is used to select whole article layout
|
||||
`ac:layout`: text(
|
||||
`{{ if eq .Layout "article" }}`,
|
||||
/**/ `<ac:layout>`,
|
||||
/**/ `<ac:layout-section ac:type="two_right_sidebar">`,
|
||||
/**/ `<ac:layout-cell>{{ .Body }}</ac:layout-cell>`,
|
||||
/**/ `<ac:layout-cell></ac:layout-cell>`,
|
||||
/**/ `</ac:layout-section>`,
|
||||
/**/ `</ac:layout>`,
|
||||
`{{ else }}`,
|
||||
/**/ `{{ .Body }}`,
|
||||
`{{ end }}`,
|
||||
),
|
||||
|
||||
// This template is used for rendering code in ```
|
||||
`ac:code`: text(
|
||||
`<ac:structured-macro ac:name="code">`,
|
||||
`<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>`,
|
||||
`<ac:parameter ac:name="collapse">false</ac:parameter>`,
|
||||
`<ac:plain-text-body><![CDATA[{{ .Text | cdata }}]]></ac:plain-text-body>`,
|
||||
`</ac:structured-macro>`,
|
||||
),
|
||||
|
||||
`ac:status`: text(
|
||||
`<ac:structured-macro ac:name="status">`,
|
||||
`<ac:parameter ac:name="colour">{{ or .Color "Grey" }}</ac:parameter>`,
|
||||
`<ac:parameter ac:name="title">{{ or .Title .Color }}</ac:parameter>`,
|
||||
`<ac:parameter ac:name="subtle">{{ or .Subtle false }}</ac:parameter>`,
|
||||
`</ac:structured-macro>`,
|
||||
),
|
||||
|
||||
`ac:link:user`: text(
|
||||
`{{ with .Name | user }}`,
|
||||
/**/ `<ac:link>`,
|
||||
/**/ `<ri:user ri:account-id="{{ .AccountID }}"/>`,
|
||||
/**/ `</ac:link>`,
|
||||
`{{ else }}`,
|
||||
/**/ `{{ .Name }}`,
|
||||
`{{ end }}`,
|
||||
),
|
||||
|
||||
// TODO(seletskiy): more templates here
|
||||
} {
|
||||
templates, err = templates.New(name).Parse(body)
|
||||
if err != nil {
|
||||
return nil, karma.
|
||||
Describe("template", body).
|
||||
Format(
|
||||
err,
|
||||
"unable to parse template",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
Reference in New Issue
Block a user