mirror of
https://github.com/kovetskiy/mark.git
synced 2025-04-24 05:42:40 +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>
|
<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 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] [-l <url>] -f <file>
|
||||||
mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <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
|
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,60 @@ 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 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:
|
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 +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() {
|
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 +143,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 +157,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, 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 {
|
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 +219,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 +256,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)
|
||||||
}
|
}
|
||||||
|
@ -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
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"
|
"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, ` > `),
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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"
|
"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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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`
|
||||||
@ -42,25 +26,30 @@ type Meta struct {
|
|||||||
Attachments []string
|
Attachments []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractMeta(data []byte) (*Meta, error) {
|
var (
|
||||||
var (
|
reHeaderPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
|
||||||
headerPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
|
reHeaderPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`)
|
||||||
headerPatternV2 = 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))
|
scanner := bufio.NewScanner(bytes.NewBuffer(data))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
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 {
|
if matches == nil {
|
||||||
matches = headerPatternV1.FindStringSubmatch(line)
|
matches = reHeaderPatternV1.FindStringSubmatch(line)
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -113,22 +102,22 @@ func ExtractMeta(data []byte) (*Meta, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if meta == nil {
|
if meta == nil {
|
||||||
return nil, nil
|
return nil, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.Space == "" {
|
if meta.Space == "" {
|
||||||
return nil, fmt.Errorf(
|
return nil, nil, fmt.Errorf(
|
||||||
"space key is not set (%s header is not set)",
|
"space key is not set (%s header is not set)",
|
||||||
HeaderSpace,
|
HeaderSpace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.Title == "" {
|
if meta.Title == "" {
|
||||||
return nil, fmt.Errorf(
|
return nil, nil, fmt.Errorf(
|
||||||
"page title is not set (%s header is not set)",
|
"page title is not set (%s header is not set)",
|
||||||
HeaderTitle,
|
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