From 07a8e3f9d776ac1a2812fc80e77b1ce8eb367704 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 2 Aug 2019 22:58:08 +0300 Subject: [PATCH 1/6] implement macros & includes --- README.md | 124 +++++++++++++++++++++++- macro_code.go | 1 - macro_layout.go | 29 ------ main.go | 168 +++++++++++++++++++++++---------- pkg/confluence/api.go | 45 +++++---- pkg/log/log.go | 100 ++++++++++++++++++++ pkg/mark/ancestry.go | 4 +- pkg/mark/attachment.go | 1 + pkg/mark/includes/templates.go | 155 ++++++++++++++++++++++++++++++ pkg/mark/macro/macro.go | 162 +++++++++++++++++++++++++++++++ pkg/mark/mark.go | 6 +- pkg/mark/markdown.go | 42 ++++----- pkg/mark/meta.go | 18 +--- pkg/mark/stdlib/stdlib.go | 147 +++++++++++++++++++++++++++++ renderer.go | 1 - 15 files changed, 862 insertions(+), 141 deletions(-) delete mode 100644 macro_code.go delete mode 100644 macro_layout.go create mode 100644 pkg/log/log.go create mode 100644 pkg/mark/includes/templates.go create mode 100644 pkg/mark/macro/macro.go create mode 100644 pkg/mark/stdlib/stdlib.go delete mode 100644 renderer.go diff --git a/README.md b/README.md index 41c67caa..0731fb48 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,130 @@ File in extended format should follow specification ``` -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 + +``` + +* (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 + +``` + +Templates may accept configuration data in YAML format which immediately +follows include tag: + +```markdown + +``` + +Mark also supports macro definitions, which are defined as regexps which will +be replaced with specified template: + +```markdown + +``` + +Capture groups can be defined in the macro's `` which can be later +referenced in the `` using `${}` 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 + + + + + +This is my article. +``` + +### Insert Status Badge + +**article.md** + +```markdown + + + + + + + +* :done: Write Article +* :todo: Publish Article +``` + +## Insert Jira Ticket + +**ticket.md** + +```markdown +[{{ .Ticket }}](http://myjira.atlassian.net/browse/{{ .Ticket }}) +``` + +**article.md** + +```markdown + + + + + +See task MYJIRA-123. +``` + +## Usage + ``` mark [options] [-u ] [-p ] [-k] [-l ] -f mark [options] [-u ] [-p ] [-k] [-n] -c diff --git a/macro_code.go b/macro_code.go deleted file mode 100644 index 06ab7d0f..00000000 --- a/macro_code.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/macro_layout.go b/macro_layout.go deleted file mode 100644 index 524a594a..00000000 --- a/macro_layout.go +++ /dev/null @@ -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( - ``+ - ``+ - `%s`+ - ``+ - ``+ - ``, - string(layout.columns[0]), - ) - } -} diff --git a/main.go b/main.go index 56919096..ac4a6ac8 100644 --- a/main.go +++ b/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,51 @@ parent by title, it will be created. Also, optional following headers are supported: - * + * - (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.: + + + +Templates may accept configuration data in YAML format which immediately +follows include tag: + + + +Mark also supports macro definitions, which are defined as regexps which will +be replaced with specified template: + + + +Capture groups can be defined in the macro's which can be later +referenced in the using ${} 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: mark [options] [-u ] [-p ] [-k] [-l ] -f mark [options] [-u ] [-p ] [-k] [-b ] -f @@ -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() { args, err := godocs.Parse(usage, "mark 1.0", godocs.UsePager) if err != nil { @@ -117,39 +134,75 @@ 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) + creds, err := GetCredentials(args, config) if err != nil { log.Fatal(err) } - meta, err := mark.ExtractMeta(markdownData) + api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password) + + markdown, err := ioutil.ReadFile(targetFile) if err != nil { log.Fatal(err) } - if dryRun { - fmt.Println(string(mark.CompileMarkdown(markdownData))) - os.Exit(0) + meta, err := mark.ExtractMeta(markdown) + if err != nil { + log.Fatal(err) } - creds, err := GetCredentials(args, config) + stdlib, err := stdlib.New(api) if err != nil { log.Fatal(err) } - api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password) + 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 { - 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 +210,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 +247,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) } diff --git a/pkg/confluence/api.go b/pkg/confluence/api.go index 98f298c1..428d4810 100644 --- a/pkg/confluence/api.go +++ b/pkg/confluence/api.go @@ -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 diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 00000000..ca6173ca --- /dev/null +++ b/pkg/log/log.go @@ -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...) +} diff --git a/pkg/mark/ancestry.go b/pkg/mark/ancestry.go index e27e6658..8165614a 100644 --- a/pkg/mark/ancestry.go +++ b/pkg/mark/ancestry.go @@ -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, ` > `), diff --git a/pkg/mark/attachment.go b/pkg/mark/attachment.go index 79d8b7de..6ae83d3c 100644 --- a/pkg/mark/attachment.go +++ b/pkg/mark/attachment.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/kovetskiy/mark/pkg/confluence" + "github.com/kovetskiy/mark/pkg/log" "github.com/reconquest/karma-go" ) diff --git a/pkg/mark/includes/templates.go b/pkg/mark/includes/templates.go new file mode 100644 index 00000000..9391bdea --- /dev/null +++ b/pkg/mark/includes/templates.go @@ -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)`) +) + +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 +} diff --git a/pkg/mark/macro/macro.go b/pkg/mark/macro/macro.go new file mode 100644 index 00000000..6e67c4be --- /dev/null +++ b/pkg/mark/macro/macro.go @@ -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)`, +) + +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 +} diff --git a/pkg/mark/mark.go b/pkg/mark/mark.go index becbcf22..c438a180 100644 --- a/pkg/mark/mark.go +++ b/pkg/mark/mark.go @@ -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, diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go index 0e793504..9603618b 100644 --- a/pkg/mark/markdown.go +++ b/pkg/mark/markdown.go @@ -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( - ``+ - `%s`+ - `false`+ - ``+ - ``, - 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 { // ac:rich-text-body 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) } diff --git a/pkg/mark/meta.go b/pkg/mark/meta.go index 8c025f20..928653bb 100644 --- a/pkg/mark/meta.go +++ b/pkg/mark/meta.go @@ -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` diff --git a/pkg/mark/stdlib/stdlib.go b/pkg/mark/stdlib/stdlib.go new file mode 100644 index 00000000..a3beed1a --- /dev/null +++ b/pkg/mark/stdlib/stdlib.go @@ -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( + ``, + + // 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, + "]]>", + "]]>", + ) + }, + }, + ) + + 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" }}`, + /**/ ``, + /**/ ``, + /**/ `{{ .Body }}`, + /**/ ``, + /**/ ``, + /**/ ``, + `{{ else }}`, + /**/ `{{ .Body }}`, + `{{ end }}`, + ), + + // This template is used for rendering code in ``` + `ac:code`: text( + ``, + `{{ .Language }}`, + `false`, + ``, + ``, + ), + + `ac:status`: text( + ``, + `{{ or .Color "Grey" }}`, + `{{ or .Title .Color }}`, + `{{ or .Subtle false }}`, + ``, + ), + + `ac:link:user`: text( + `{{ with .Name | user }}`, + /**/ ``, + /**/ ``, + /**/ ``, + `{{ 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 +} diff --git a/renderer.go b/renderer.go deleted file mode 100644 index 06ab7d0f..00000000 --- a/renderer.go +++ /dev/null @@ -1 +0,0 @@ -package main From 58f4a5510016d26423167711c7c74aff867d44bb Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 6 Aug 2019 18:07:40 +0300 Subject: [PATCH 2/6] add jira ticket template --- README.md | 11 ++++------- main.go | 3 +++ pkg/mark/stdlib/stdlib.go | 6 ++++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0731fb48..7de857c9 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ By default, mark provides several built-in templates and macros: - 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. @@ -133,12 +136,6 @@ This is my article. ## Insert Jira Ticket -**ticket.md** - -```markdown -[{{ .Ticket }}](http://myjira.atlassian.net/browse/{{ .Ticket }}) -``` - **article.md** ```markdown @@ -146,7 +143,7 @@ This is my article. See task MYJIRA-123. diff --git a/main.go b/main.go index ac4a6ac8..45996f22 100644 --- a/main.go +++ b/main.go @@ -94,6 +94,9 @@ By default, mark provides several built-in templates and macros: 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: diff --git a/pkg/mark/stdlib/stdlib.go b/pkg/mark/stdlib/stdlib.go index a3beed1a..d9dd0e97 100644 --- a/pkg/mark/stdlib/stdlib.go +++ b/pkg/mark/stdlib/stdlib.go @@ -130,6 +130,12 @@ func templates(api *confluence.API) (*template.Template, error) { `{{ end }}`, ), + `ac:jira:ticket`: text( + ``, + `{{ .Ticket }}`, + ``, + ), + // TODO(seletskiy): more templates here } { templates, err = templates.New(name).Parse(body) From de2c8766329071b1e6acd13f27de3c5c767eabc9 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 8 Aug 2019 15:17:44 +0300 Subject: [PATCH 3/6] fix documentation problems --- README.md | 14 +++++++++++--- main.go | 10 ++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7de857c9..8a3fbd74 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Also, optional following headers are supported: reading; * plain: content will fill all page; -Mark supports Go templates, which can be includes into article by using path +Mark supports Go templates, which can be included into article by using path to the template relative to current working dir, e.g.: ```markdown @@ -67,8 +67,16 @@ be replaced with specified template: --> ``` -Capture groups can be defined in the macro's `` which can be later -referenced in the `` using `${}` syntax. +Capture groups can be defined in the macro's which can be later +referenced in the `` using `${}` syntax, where `` is +number of a capture group in regexp (`${0}` is used for entire regexp match), +for example: + +```markdown + +``` By default, mark provides several built-in templates and macros: diff --git a/main.go b/main.go index 45996f22..9f83ebcd 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,7 @@ Also, optional following headers are supported: reading; - plain: content will fill all page; -Mark supports Go templates, which can be includes into article by using path +Mark supports Go templates, which can be included into article by using path to the template relative to current working dir, e.g.: @@ -76,7 +76,13 @@ be replaced with specified template: --> Capture groups can be defined in the macro's which can be later -referenced in the using ${} syntax. +referenced in the using ${} syntax, where is +number of a capture group in regexp (${0} is used for entire regexp match), for +example: + + By default, mark provides several built-in templates and macros: From 4cfda3afc1406600ebca1d94f87170439b2a48a9 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 8 Aug 2019 15:32:34 +0300 Subject: [PATCH 4/6] add optional spaces in regexps --- pkg/mark/includes/templates.go | 4 +++- pkg/mark/macro/macro.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/mark/includes/templates.go b/pkg/mark/includes/templates.go index 9391bdea..5626335a 100644 --- a/pkg/mark/includes/templates.go +++ b/pkg/mark/includes/templates.go @@ -16,7 +16,9 @@ import ( ) var ( - reIncludeDirective = regexp.MustCompile(`(?s)`) + reIncludeDirective = regexp.MustCompile( + `(?s)`, + ) ) func LoadTemplate( diff --git a/pkg/mark/macro/macro.go b/pkg/mark/macro/macro.go index 6e67c4be..093ec151 100644 --- a/pkg/mark/macro/macro.go +++ b/pkg/mark/macro/macro.go @@ -14,7 +14,7 @@ import ( ) var reMacroDirective = regexp.MustCompile( - `(?s)`, + `(?s)`, ) type Macro struct { From 559b91390030cc78be5f1af1f010573831a04a19 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 8 Aug 2019 15:54:03 +0300 Subject: [PATCH 5/6] LoadTemplates: remove excessive return value --- pkg/mark/includes/templates.go | 25 +++++++------------------ pkg/mark/macro/macro.go | 4 ++-- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/pkg/mark/includes/templates.go b/pkg/mark/includes/templates.go index 5626335a..ec220fbc 100644 --- a/pkg/mark/includes/templates.go +++ b/pkg/mark/includes/templates.go @@ -24,14 +24,14 @@ var ( func LoadTemplate( path string, templates *template.Template, -) (string, *template.Template, error) { +) (*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 + return template, nil } var body []byte @@ -43,7 +43,7 @@ func LoadTemplate( "unable to read template file", ) - return name, nil, err + return nil, err } templates, err = templates.New(name).Parse(string(body)) @@ -53,10 +53,10 @@ func LoadTemplate( "unable to parse template", ) - return name, nil, err + return nil, err } - return name, templates, nil + return templates, nil } func ProcessIncludes( @@ -117,27 +117,16 @@ func ProcessIncludes( log.Tracef(vardump(facts, data), "including template %q", path) - var name string - - name, templates, err = LoadTemplate(path, templates) + 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) + err = templates.Execute(&buffer, data) if err != nil { err = vardump(facts, data).Format( err, diff --git a/pkg/mark/macro/macro.go b/pkg/mark/macro/macro.go index 093ec151..abd1a96d 100644 --- a/pkg/mark/macro/macro.go +++ b/pkg/mark/macro/macro.go @@ -14,7 +14,7 @@ import ( ) var reMacroDirective = regexp.MustCompile( - `(?s)`, + `(?s)`, ) type Macro struct { @@ -111,7 +111,7 @@ func LoadMacros( macro Macro ) - _, macro.Template, err = includes.LoadTemplate(path, templates) + macro.Template, err = includes.LoadTemplate(path, templates) if err != nil { err = karma.Format(err, "unable to load template") From 6b83d140d54482e903fbd775ba035c349781e71d Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 8 Aug 2019 23:41:26 +0300 Subject: [PATCH 6/6] use regexputil --- main.go | 4 ++-- pkg/mark/includes/templates.go | 7 ++++++- pkg/mark/macro/macro.go | 28 +++++++++++++++++++--------- pkg/mark/meta.go | 29 +++++++++++++++++------------ pkg/mark/stdlib/stdlib.go | 2 +- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index 9f83ebcd..c343ab9e 100644 --- a/main.go +++ b/main.go @@ -162,7 +162,7 @@ func main() { log.Fatal(err) } - meta, err := mark.ExtractMeta(markdown) + meta, markdown, err := mark.ExtractMeta(markdown) if err != nil { log.Fatal(err) } @@ -190,7 +190,7 @@ func main() { } } - macros, markdown, err := macro.LoadMacros(markdown, templates) + macros, markdown, err := macro.ExtractMacros(markdown, templates) if err != nil { log.Fatal(err) } diff --git a/pkg/mark/includes/templates.go b/pkg/mark/includes/templates.go index ec220fbc..1e10df1f 100644 --- a/pkg/mark/includes/templates.go +++ b/pkg/mark/includes/templates.go @@ -17,7 +17,12 @@ import ( var ( reIncludeDirective = regexp.MustCompile( - `(?s)`, + // + + `(?s)` + // dot capture newlines + /**/ ``, ) ) diff --git a/pkg/mark/macro/macro.go b/pkg/mark/macro/macro.go index abd1a96d..cc525507 100644 --- a/pkg/mark/macro/macro.go +++ b/pkg/mark/macro/macro.go @@ -10,11 +10,19 @@ import ( "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( - `(?s)`, + // + + `(?s)` + // dot capture newlines + /**/ ``, ) type Macro struct { @@ -88,7 +96,7 @@ func (macro *Macro) configure(node interface{}, groups [][]byte) interface{} { return node } -func LoadMacros( +func ExtractMacros( contents []byte, templates *template.Template, ) ([]Macro, []byte, error) { @@ -103,15 +111,17 @@ func LoadMacros( return spec } - groups := reMacroDirective.FindSubmatch(spec) + groups := reMacroDirective.FindStringSubmatch(string(spec)) var ( - expr, path, config = groups[1], string(groups[2]), groups[3] + 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(path, templates) + macro.Template, err = includes.LoadTemplate(template, templates) if err != nil { err = karma.Format(err, "unable to load template") @@ -120,10 +130,10 @@ func LoadMacros( } facts := karma. - Describe("template", path). - Describe("expr", string(expr)) + Describe("template", template). + Describe("expr", expr) - macro.Regexp, err = regexp.Compile(string(expr)) + macro.Regexp, err = regexp.Compile(expr) if err != nil { err = facts. Format( @@ -134,7 +144,7 @@ func LoadMacros( return nil } - err = yaml.Unmarshal(config, ¯o.Config) + err = yaml.Unmarshal([]byte(config), ¯o.Config) if err != nil { err = facts. Describe("config", string(config)). diff --git a/pkg/mark/meta.go b/pkg/mark/meta.go index 928653bb..da4337c4 100644 --- a/pkg/mark/meta.go +++ b/pkg/mark/meta.go @@ -26,25 +26,30 @@ type Meta struct { Attachments []string } -func ExtractMeta(data []byte) (*Meta, error) { +var ( + reHeaderPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`) + reHeaderPatternV2 = regexp.MustCompile(``) +) + +func ExtractMeta(data []byte) (*Meta, []byte, error) { var ( - headerPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`) - headerPatternV2 = regexp.MustCompile(``) + meta *Meta + offset int ) - var meta *Meta - 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 } @@ -97,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 } diff --git a/pkg/mark/stdlib/stdlib.go b/pkg/mark/stdlib/stdlib.go index d9dd0e97..0cad0b3a 100644 --- a/pkg/mark/stdlib/stdlib.go +++ b/pkg/mark/stdlib/stdlib.go @@ -40,7 +40,7 @@ func macros(templates *template.Template) ([]macro.Macro, error) { return []byte(strings.Join(line, "\n")) } - macros, _, err := macro.LoadMacros( + macros, _, err := macro.ExtractMacros( []byte(text( `