implement macros & includes

This commit is contained in:
Stanislav Seletskiy 2019-08-02 22:58:08 +03:00
parent e77e589494
commit 07a8e3f9d7
No known key found for this signature in database
GPG Key ID: E6B40F71C367E6B5
15 changed files with 870 additions and 149 deletions

124
README.md
View File

@ -30,10 +30,130 @@ File in extended format should follow specification
<page contents> <page contents>
``` ```
There can be any number of 'X-Parent' headers, if mark can't find specified There can be any number of 'Parent' headers, if mark can't find specified
parent by title, it will be created. parent by title, it will be created.
## Usage: Also, optional following headers are supported:
```markdown
<!-- Layout: (article|plain) -->
```
* (default) article: content will be put in narrow column for ease of
reading;
* plain: content will fill all page;
Mark supports Go templates, which can be includes into article by using path
to the template relative to current working dir, e.g.:
```markdown
<!-- Include: <path> -->
```
Templates may accept configuration data in YAML format which immediately
follows include tag:
```markdown
<!-- Include: <path>
<yaml-data> -->
```
Mark also supports macro definitions, which are defined as regexps which will
be replaced with specified template:
```markdown
<!-- Macro: <regexp>
Template: <path>
<yaml-data> -->
```
Capture groups can be defined in the macro's `<regexp>` which can be later
referenced in the `<yaml-data>` using `${<number>}` syntax.
By default, mark provides several built-in templates and macros:
* template `ac:status` to include badge-like text, which accepts following
parameters:
- Title: text to display in the badge
- Color: color to use as background/border for badge
- Grey
- Red
- Yellow
- Green
- Blue
- Subtle: specify to fill badge with background or not
- true
- false
See: https://confluence.atlassian.com/conf59/status-macro-792499207.html
* macro `@{...}` to mention user by name specified in the braces.
## Template & Macros Usecases
### Insert Disclamer
**disclamer.md**
```markdown
**NOTE**: this document is generated, do not edit manually.
```
**article.md**
```markdown
<!-- Space: TEST -->
<!-- Title: My Article -->
<!-- Include: disclamer.md -->
This is my article.
```
### Insert Status Badge
**article.md**
```markdown
<!-- Space: TEST -->
<!-- Title: TODO List -->
<!-- Macro: :done:
Template: ac:status
Title: DONE
Color: Green -->
<!-- Macro: :todo:
Template: ac:status
Title: TODO
Color: Blue -->
* :done: Write Article
* :todo: Publish Article
```
## Insert Jira Ticket
**ticket.md**
```markdown
[{{ .Ticket }}](http://myjira.atlassian.net/browse/{{ .Ticket }})
```
**article.md**
```markdown
<!-- Space: TEST -->
<!-- Title: TODO List -->
<!-- Macro: MYJIRA-\d+
Template: ticket.md
Ticket: ${0} -->
See task MYJIRA-123.
```
## Usage
``` ```
mark [options] [-u <username>] [-p <password>] [-k] [-l <url>] -f <file> mark [options] [-u <username>] [-p <password>] [-k] [-l <url>] -f <file>
mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <file> mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <file>

View File

@ -1 +0,0 @@
package main

View File

@ -1,29 +0,0 @@
package main
import "fmt"
type MacroLayout struct {
layout string
columns [][]byte
}
func (layout MacroLayout) Render() string {
switch layout.layout {
case "plain":
return string(layout.columns[0])
case "article":
fallthrough
default:
return fmt.Sprintf(
`<ac:layout>`+
`<ac:layout-section ac:type="two_right_sidebar">`+
`<ac:layout-cell>%s</ac:layout-cell>`+
`<ac:layout-cell></ac:layout-cell>`+
`</ac:layout-section>`+
`</ac:layout>`,
string(layout.columns[0]),
)
}
}

184
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -8,10 +9,12 @@ import (
"strings" "strings"
"github.com/kovetskiy/godocs" "github.com/kovetskiy/godocs"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/log"
"github.com/kovetskiy/mark/pkg/mark" "github.com/kovetskiy/mark/pkg/mark"
"github.com/reconquest/cog" "github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
@ -48,12 +51,51 @@ parent by title, it will be created.
Also, optional following headers are supported: Also, optional following headers are supported:
* <!-- Layout: <article|plain> --> * <!-- Layout: (article|plain) -->
- (default) article: content will be put in narrow column for ease of - (default) article: content will be put in narrow column for ease of
reading; reading;
- plain: content will fill all page; - plain: content will fill all page;
Mark supports Go templates, which can be includes into article by using path
to the template relative to current working dir, e.g.:
<!-- Include: <path> -->
Templates may accept configuration data in YAML format which immediately
follows include tag:
<!-- Include: <path>
<yaml-data> -->
Mark also supports macro definitions, which are defined as regexps which will
be replaced with specified template:
<!-- Macro: <regexp>
Template: <path>
<yaml-data> -->
Capture groups can be defined in the macro's <regexp> which can be later
referenced in the <yaml-data> using ${<number>} syntax.
By default, mark provides several built-in templates and macros:
* template 'ac:status' to include badge-like text, which accepts following
parameters:
- Title: text to display in the badge
- Color: color to use as background/border for badge
- Grey
- Yellow
- Red
- Blue
- Subtle: specify to fill badge with background or not
- true
- false
See: https://confluence.atlassian.com/conf59/status-macro-792499207.html
* macro '@{...}' to mention user by name specified in the braces.
Usage: Usage:
mark [options] [-u <username>] [-p <token>] [-k] [-l <url>] -f <file> mark [options] [-u <username>] [-p <token>] [-k] [-l <url>] -f <file>
mark [options] [-u <username>] [-p <password>] [-k] [-b <url>] -f <file> mark [options] [-u <username>] [-p <password>] [-k] [-b <url>] -f <file>
@ -80,31 +122,6 @@ Options:
` `
) )
var (
log *cog.Logger
)
func initlog(debug, trace bool) {
stderr := lorg.NewLog()
stderr.SetIndentLines(true)
stderr.SetFormat(
lorg.NewFormat("${time} ${level:[%s]:right:short} ${prefix}%s"),
)
log = cog.NewLogger(stderr)
if debug {
log.SetLevel(lorg.LevelDebug)
}
if trace {
log.SetLevel(lorg.LevelTrace)
}
mark.SetLogger(log)
confluence.SetLogger(log)
}
func main() { func main() {
args, err := godocs.Parse(usage, "mark 1.0", godocs.UsePager) args, err := godocs.Parse(usage, "mark 1.0", godocs.UsePager)
if err != nil { if err != nil {
@ -117,28 +134,13 @@ func main() {
editLock = args["-k"].(bool) editLock = args["-k"].(bool)
) )
initlog(args["--debug"].(bool), args["--trace"].(bool)) log.Init(args["--debug"].(bool), args["--trace"].(bool))
config, err := LoadConfig(filepath.Join(os.Getenv("HOME"), ".config/mark")) config, err := LoadConfig(filepath.Join(os.Getenv("HOME"), ".config/mark"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
markdownData, err := ioutil.ReadFile(targetFile)
if err != nil {
log.Fatal(err)
}
meta, err := mark.ExtractMeta(markdownData)
if err != nil {
log.Fatal(err)
}
if dryRun {
fmt.Println(string(mark.CompileMarkdown(markdownData)))
os.Exit(0)
}
creds, err := GetCredentials(args, config) creds, err := GetCredentials(args, config)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -146,10 +148,61 @@ func main() {
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password) api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
markdown, err := ioutil.ReadFile(targetFile)
if err != nil {
log.Fatal(err)
}
meta, err := mark.ExtractMeta(markdown)
if err != nil {
log.Fatal(err)
}
stdlib, err := stdlib.New(api)
if err != nil {
log.Fatal(err)
}
templates := stdlib.Templates
var recurse bool
for {
templates, markdown, recurse, err = includes.ProcessIncludes(
markdown,
templates,
)
if err != nil {
log.Fatal(err)
}
if !recurse {
break
}
}
macros, markdown, err := macro.LoadMacros(markdown, templates)
if err != nil {
log.Fatal(err)
}
macros = append(macros, stdlib.Macros...)
for _, macro := range macros {
markdown, err = macro.Apply(markdown)
if err != nil {
log.Fatal(err)
}
}
if dryRun {
fmt.Println(mark.CompileMarkdown(markdown, stdlib))
os.Exit(0)
}
if creds.PageID != "" && meta != nil { if creds.PageID != "" && meta != nil {
log.Warningf( log.Warning(
nil, `specified file contains metadata, ` +
`specified file contains metadata, `+
`but it will be ignored due specified command line URL`, `but it will be ignored due specified command line URL`,
) )
@ -157,10 +210,9 @@ func main() {
} }
if creds.PageID == "" && meta == nil { if creds.PageID == "" && meta == nil {
log.Fatalf( log.Fatal(
nil, `specified file doesn't contain metadata ` +
`specified file doesn't contain metadata `+ `and URL is not specified via command line ` +
`and URL is not specified via command line `+
`or doesn't contain pageId GET-parameter`, `or doesn't contain pageId GET-parameter`,
) )
} }
@ -195,14 +247,32 @@ func main() {
log.Fatalf(err, "unable to create/update attachments") log.Fatalf(err, "unable to create/update attachments")
} }
markdownData = mark.CompileAttachmentLinks(markdownData, attaches) markdown = mark.CompileAttachmentLinks(markdown, attaches)
htmlData := mark.CompileMarkdown(markdownData) html := mark.CompileMarkdown(markdown, stdlib)
err = api.UpdatePage( {
target, var buffer bytes.Buffer
MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(),
) err := stdlib.Templates.ExecuteTemplate(
&buffer,
"ac:layout",
struct {
Layout string
Body string
}{
Layout: meta.Layout,
Body: html,
},
)
if err != nil {
log.Fatal(err)
}
html = buffer.String()
}
err = api.UpdatePage(target, html)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -12,8 +12,6 @@ import (
"strings" "strings"
"github.com/bndr/gopencils" "github.com/bndr/gopencils"
"github.com/kovetskiy/lorg"
"github.com/reconquest/cog"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
@ -59,20 +57,6 @@ type AttachmentInfo struct {
} `json:"_links"` } `json:"_links"`
} }
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
}
type form struct { type form struct {
buffer io.Reader buffer io.Reader
writer *multipart.Writer writer *multipart.Writer
@ -471,6 +455,35 @@ func (api *API) UpdatePage(
return nil return nil
} }
func (api *API) GetUserByName(name string) (*User, error) {
var response struct {
Results []struct {
User User
}
}
_, err := api.rest.
Res("search").
Res("user", &response).
Get(map[string]string{
"cql": fmt.Sprintf("user.fullname~%q", name),
})
if err != nil {
return nil, err
}
if len(response.Results) == 0 {
return nil, karma.
Describe("name", name).
Reason(
"user with given name is not found",
)
}
return &response.Results[0].User, nil
}
func (api *API) GetCurrentUser() (*User, error) { func (api *API) GetCurrentUser() (*User, error) {
var user User var user User

100
pkg/log/log.go Normal file
View File

@ -0,0 +1,100 @@
package log
import (
"github.com/kovetskiy/lorg"
"github.com/reconquest/cog"
)
var (
log *cog.Logger
)
func Init(debug, trace bool) {
stderr := lorg.NewLog()
stderr.SetIndentLines(true)
stderr.SetFormat(
lorg.NewFormat("${time} ${level:[%s]:right:short} ${prefix}%s"),
)
log = cog.NewLogger(stderr)
if debug {
log.SetLevel(lorg.LevelDebug)
}
if trace {
log.SetLevel(lorg.LevelTrace)
}
}
func Fatalf(
reason interface{},
message string,
args ...interface{},
) {
log.Fatalf(reason, message, args...)
}
func Errorf(
reason interface{},
message string,
args ...interface{},
) {
log.Errorf(reason, message, args...)
}
func Warningf(
reason interface{},
message string,
args ...interface{},
) {
log.Warningf(reason, message, args...)
}
func Infof(
context interface{},
message string,
args ...interface{},
) {
log.Infof(context, message, args...)
}
func Debugf(
context interface{},
message string,
args ...interface{},
) {
log.Debugf(context, message, args...)
}
func Tracef(
context interface{},
message string,
args ...interface{},
) {
log.Tracef(context, message, args...)
}
func Fatal(values ...interface{}) {
log.Fatal(values...)
}
func Error(values ...interface{}) {
log.Error(values...)
}
func Warning(values ...interface{}) {
log.Warning(values...)
}
func Info(values ...interface{}) {
log.Info(values...)
}
func Debug(values ...interface{}) {
log.Debug(values...)
}
func Trace(values ...interface{}) {
log.Trace(values...)
}

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger" "github.com/kovetskiy/mark/pkg/log"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
@ -56,7 +56,7 @@ func EnsureAncestry(
return parent, nil return parent, nil
} }
logger.Debugf( log.Debugf(
"empty pages under %q to be created: %s", "empty pages under %q to be created: %s",
parent.Title, parent.Title,
strings.Join(rest, ` > `), strings.Join(rest, ` > `),

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/log"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )

View 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
View 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, &macro.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
}

View File

@ -4,7 +4,7 @@ import (
"strings" "strings"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/faces/logger" "github.com/kovetskiy/mark/pkg/log"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
) )
@ -47,7 +47,7 @@ func ResolvePage(
path := meta.Parents path := meta.Parents
path = append(path, meta.Title) path = append(path, meta.Title)
logger.Debugf( log.Debugf(
"resolving page path: ??? > %s", "resolving page path: ??? > %s",
strings.Join(path, ` > `), strings.Join(path, ` > `),
) )
@ -74,7 +74,7 @@ func ResolvePage(
titles = append(titles, parent.Title) titles = append(titles, parent.Title)
log.Infof( log.Infof(
nil, nil,
"page will be stored under path: %s > %s", "page will be stored under path: %s > %s",
strings.Join(titles, ` > `), strings.Join(titles, ` > `),
meta.Title, meta.Title,

View File

@ -2,14 +2,17 @@ package mark
import ( import (
"bytes" "bytes"
"fmt"
"regexp" "regexp"
"github.com/kovetskiy/mark/pkg/log"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/russross/blackfriday" "github.com/russross/blackfriday"
) )
type ConfluenceRenderer struct { type ConfluenceRenderer struct {
blackfriday.Renderer blackfriday.Renderer
Stdlib *stdlib.Lib
} }
func (renderer ConfluenceRenderer) BlockCode( func (renderer ConfluenceRenderer) BlockCode(
@ -17,22 +20,16 @@ func (renderer ConfluenceRenderer) BlockCode(
text []byte, text []byte,
lang string, lang string,
) { ) {
out.WriteString(MacroCode{lang, text}.Render()) renderer.Stdlib.Templates.ExecuteTemplate(
} out,
"ac:code",
type MacroCode struct { struct {
lang string Language string
code []byte Text string
} }{
lang,
func (code MacroCode) Render() string { string(text),
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,
) )
} }
@ -41,12 +38,13 @@ func (code MacroCode) Render() string {
// <a href="ac:rich-text-body">ac:rich-text-body</a> for whatever reason. // <a href="ac:rich-text-body">ac:rich-text-body</a> for whatever reason.
func CompileMarkdown( func CompileMarkdown(
markdown []byte, markdown []byte,
) []byte { stdlib *stdlib.Lib,
) string {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown)) log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
colon := regexp.MustCompile(`---BLACKFRIDAY-COLON---`) colon := regexp.MustCompile(`---BLACKFRIDAY-COLON---`)
tags := regexp.MustCompile(`<(/?\S+):(\S+)>`) tags := regexp.MustCompile(`<(/?\S+?):(\S+?)>`)
markdown = tags.ReplaceAll( markdown = tags.ReplaceAll(
markdown, markdown,
@ -54,7 +52,7 @@ func CompileMarkdown(
) )
renderer := ConfluenceRenderer{ renderer := ConfluenceRenderer{
blackfriday.HtmlRenderer( Renderer: blackfriday.HtmlRenderer(
blackfriday.HTML_USE_XHTML| blackfriday.HTML_USE_XHTML|
blackfriday.HTML_USE_SMARTYPANTS| blackfriday.HTML_USE_SMARTYPANTS|
blackfriday.HTML_SMARTYPANTS_FRACTIONS| blackfriday.HTML_SMARTYPANTS_FRACTIONS|
@ -62,6 +60,8 @@ func CompileMarkdown(
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES, blackfriday.HTML_SMARTYPANTS_LATEX_DASHES,
"", "", "", "",
), ),
Stdlib: stdlib,
} }
html := blackfriday.MarkdownOptions( html := blackfriday.MarkdownOptions(
@ -88,5 +88,5 @@ func CompileMarkdown(
log.Tracef(nil, "rendered markdown to html:\n%s", string(html)) log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
return html return string(html)
} }

View File

@ -4,28 +4,12 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"regexp" "regexp"
"strings" "strings"
"github.com/kovetskiy/lorg" "github.com/kovetskiy/mark/pkg/log"
"github.com/reconquest/cog"
) )
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 ( const (
HeaderParent = `Parent` HeaderParent = `Parent`
HeaderSpace = `Space` HeaderSpace = `Space`

147
pkg/mark/stdlib/stdlib.go Normal file
View 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
}

View File

@ -1 +0,0 @@
package main