mirror of
https://github.com/kovetskiy/mark.git
synced 2025-04-23 21:32:41 +08:00
Merge pull request #8 from seletskiy/master
implement macros & includes
This commit is contained in:
commit
2369954b08
129
README.md
129
README.md
@ -30,10 +30,135 @@ File in extended format should follow specification
|
||||
<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.
|
||||
|
||||
## 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 included 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, where `<number>` is
|
||||
number of a capture group in regexp (`${0}` is used for entire regexp match),
|
||||
for example:
|
||||
|
||||
```markdown
|
||||
<!-- Macro: MYJIRA-\d+
|
||||
Template: ac:jira:ticket
|
||||
Ticket: ${0} -->
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
* template `ac:jira:ticket` to include JIRA ticket link. Parameters:
|
||||
- Ticket: Jira ticket number like BUGS-123.
|
||||
|
||||
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
|
||||
|
||||
**article.md**
|
||||
|
||||
```markdown
|
||||
<!-- Space: TEST -->
|
||||
<!-- Title: TODO List -->
|
||||
|
||||
<!-- Macro: MYJIRA-\d+
|
||||
Template: ac:jira:ticket
|
||||
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] [-n] -c <file>
|
||||
|
@ -1 +0,0 @@
|
||||
package main
|
@ -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]),
|
||||
)
|
||||
}
|
||||
}
|
193
main.go
193
main.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -8,10 +9,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kovetskiy/godocs"
|
||||
"github.com/kovetskiy/lorg"
|
||||
"github.com/kovetskiy/mark/pkg/confluence"
|
||||
"github.com/kovetskiy/mark/pkg/log"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -48,12 +51,60 @@ parent by title, it will be created.
|
||||
|
||||
Also, optional following headers are supported:
|
||||
|
||||
* <!-- Layout: <article|plain> -->
|
||||
* <!-- 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 included 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, where <number> is
|
||||
number of a capture group in regexp (${0} is used for entire regexp match), for
|
||||
example:
|
||||
|
||||
<!-- Macro: MYJIRA-\d+
|
||||
Template: ac:jira:ticket
|
||||
Ticket: ${0} -->
|
||||
|
||||
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
|
||||
|
||||
* template 'ac:jira:ticket' to include JIRA ticket link. Parameters:
|
||||
- Ticket: Jira ticket number like BUGS-123.
|
||||
|
||||
* macro '@{...}' to mention user by name specified in the braces.
|
||||
|
||||
Usage:
|
||||
mark [options] [-u <username>] [-p <token>] [-k] [-l <url>] -f <file>
|
||||
mark [options] [-u <username>] [-p <password>] [-k] [-b <url>] -f <file>
|
||||
@ -80,31 +131,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() {
|
||||
args, err := godocs.Parse(usage, "mark 1.0", godocs.UsePager)
|
||||
if err != nil {
|
||||
@ -117,28 +143,13 @@ func main() {
|
||||
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"))
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -146,10 +157,61 @@ func main() {
|
||||
|
||||
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
|
||||
|
||||
markdown, err := ioutil.ReadFile(targetFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
meta, markdown, 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.ExtractMacros(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 {
|
||||
log.Warningf(
|
||||
nil,
|
||||
`specified file contains metadata, `+
|
||||
log.Warning(
|
||||
`specified file contains metadata, ` +
|
||||
`but it will be ignored due specified command line URL`,
|
||||
)
|
||||
|
||||
@ -157,10 +219,9 @@ func main() {
|
||||
}
|
||||
|
||||
if creds.PageID == "" && meta == nil {
|
||||
log.Fatalf(
|
||||
nil,
|
||||
`specified file doesn't contain metadata `+
|
||||
`and URL is not specified via command line `+
|
||||
log.Fatal(
|
||||
`specified file doesn't contain metadata ` +
|
||||
`and URL is not specified via command line ` +
|
||||
`or doesn't contain pageId GET-parameter`,
|
||||
)
|
||||
}
|
||||
@ -195,14 +256,32 @@ func main() {
|
||||
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,
|
||||
MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(),
|
||||
)
|
||||
{
|
||||
var buffer bytes.Buffer
|
||||
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -12,8 +12,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bndr/gopencils"
|
||||
"github.com/kovetskiy/lorg"
|
||||
"github.com/reconquest/cog"
|
||||
"github.com/reconquest/karma-go"
|
||||
)
|
||||
|
||||
@ -59,20 +57,6 @@ type AttachmentInfo struct {
|
||||
} `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 {
|
||||
buffer io.Reader
|
||||
writer *multipart.Writer
|
||||
@ -471,6 +455,35 @@ func (api *API) UpdatePage(
|
||||
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) {
|
||||
var user User
|
||||
|
||||
|
100
pkg/log/log.go
Normal file
100
pkg/log/log.go
Normal 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...)
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
||||
|
151
pkg/mark/includes/templates.go
Normal file
151
pkg/mark/includes/templates.go
Normal file
@ -0,0 +1,151 @@
|
||||
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(
|
||||
// <!-- Include: <template path>
|
||||
// <optional yaml data> -->
|
||||
|
||||
`(?s)` + // dot capture newlines
|
||||
/**/ `<!--\s*Include:\s*(?P<template>\S+)\s*` +
|
||||
/* */ `(\n(?P<config>.*?))?-->`,
|
||||
)
|
||||
)
|
||||
|
||||
func LoadTemplate(
|
||||
path string,
|
||||
templates *template.Template,
|
||||
) (*template.Template, error) {
|
||||
var (
|
||||
name = strings.TrimSuffix(path, filepath.Ext(path))
|
||||
facts = karma.Describe("name", name)
|
||||
)
|
||||
|
||||
if template := templates.Lookup(name); template != nil {
|
||||
return template, nil
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
body, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
err = facts.Format(
|
||||
err,
|
||||
"unable to read template file",
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templates, err = templates.New(name).Parse(string(body))
|
||||
if err != nil {
|
||||
err = facts.Format(
|
||||
err,
|
||||
"unable to parse template",
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return 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)
|
||||
|
||||
templates, err = LoadTemplate(path, templates)
|
||||
if err != nil {
|
||||
err = facts.Format(err, "unable to load template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err = templates.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
|
||||
}
|
172
pkg/mark/macro/macro.go
Normal file
172
pkg/mark/macro/macro.go
Normal file
@ -0,0 +1,172 @@
|
||||
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"
|
||||
"github.com/reconquest/regexputil-go"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var reMacroDirective = regexp.MustCompile(
|
||||
// <!-- Macro: <regexp>
|
||||
// Template: <template path>
|
||||
// <optional yaml data> -->
|
||||
|
||||
`(?s)` + // dot capture newlines
|
||||
/**/ `<!--\s*Macro:\s*(?P<expr>[^\n]+)\n` +
|
||||
/* */ `\s*Template:\s*(?P<template>\S+)\s*` +
|
||||
/* */ `(\n(?P<config>.*?))?-->`,
|
||||
)
|
||||
|
||||
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 ExtractMacros(
|
||||
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.FindStringSubmatch(string(spec))
|
||||
|
||||
var (
|
||||
expr = regexputil.Subexp(reMacroDirective, groups, "expr")
|
||||
template = regexputil.Subexp(reMacroDirective, groups, "template")
|
||||
config = regexputil.Subexp(reMacroDirective, groups, "config")
|
||||
|
||||
macro Macro
|
||||
)
|
||||
|
||||
macro.Template, err = includes.LoadTemplate(template, templates)
|
||||
|
||||
if err != nil {
|
||||
err = karma.Format(err, "unable to load template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
facts := karma.
|
||||
Describe("template", template).
|
||||
Describe("expr", expr)
|
||||
|
||||
macro.Regexp, err = regexp.Compile(expr)
|
||||
if err != nil {
|
||||
err = facts.
|
||||
Format(
|
||||
err,
|
||||
"unable to compile macros regexp",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal([]byte(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`
|
||||
@ -42,25 +26,30 @@ type Meta struct {
|
||||
Attachments []string
|
||||
}
|
||||
|
||||
func ExtractMeta(data []byte) (*Meta, error) {
|
||||
var (
|
||||
headerPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
|
||||
headerPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`)
|
||||
)
|
||||
var (
|
||||
reHeaderPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
|
||||
reHeaderPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`)
|
||||
)
|
||||
|
||||
var meta *Meta
|
||||
func ExtractMeta(data []byte) (*Meta, []byte, error) {
|
||||
var (
|
||||
meta *Meta
|
||||
offset int
|
||||
)
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewBuffer(data))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
matches := headerPatternV2.FindStringSubmatch(line)
|
||||
offset += len(line)
|
||||
|
||||
matches := reHeaderPatternV2.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
matches = headerPatternV1.FindStringSubmatch(line)
|
||||
matches = reHeaderPatternV1.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
break
|
||||
}
|
||||
@ -113,22 +102,22 @@ func ExtractMeta(data []byte) (*Meta, error) {
|
||||
}
|
||||
|
||||
if meta == nil {
|
||||
return nil, nil
|
||||
return nil, data, nil
|
||||
}
|
||||
|
||||
if meta.Space == "" {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"space key is not set (%s header is not set)",
|
||||
HeaderSpace,
|
||||
)
|
||||
}
|
||||
|
||||
if meta.Title == "" {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"page title is not set (%s header is not set)",
|
||||
HeaderTitle,
|
||||
)
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
return nil, data[offset+1:], nil
|
||||
}
|
||||
|
153
pkg/mark/stdlib/stdlib.go
Normal file
153
pkg/mark/stdlib/stdlib.go
Normal file
@ -0,0 +1,153 @@
|
||||
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.ExtractMacros(
|
||||
[]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 }}`,
|
||||
),
|
||||
|
||||
`ac:jira:ticket`: text(
|
||||
`<ac:structured-macro ac:name="jira">`,
|
||||
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
|
||||
`</ac:structured-macro>`,
|
||||
),
|
||||
|
||||
// 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
|
||||
}
|
@ -1 +0,0 @@
|
||||
package main
|
Loading…
x
Reference in New Issue
Block a user