mirror of
				https://github.com/kovetskiy/mark.git
				synced 2025-10-31 19:57:36 +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
	 Egor Kovetskiy
						Egor Kovetskiy