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> | ||||
| ``` | ||||
| 
 | ||||
| There can be any number of 'X-Parent' headers, if mark can't find specified | ||||
| There can be any number of 'Parent' headers, if mark can't find specified | ||||
| parent by title, it will be created. | ||||
| 
 | ||||
| ## Usage: | ||||
| Also, optional following headers are supported: | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Layout: (article|plain) --> | ||||
| ``` | ||||
| 
 | ||||
| * (default) article: content will be put in narrow column for ease of | ||||
|   reading; | ||||
| * plain: content will fill all page; | ||||
| 
 | ||||
| Mark supports Go templates, which can be included into article by using path | ||||
| to the template relative to current working dir, e.g.: | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Include: <path> --> | ||||
| ``` | ||||
| 
 | ||||
| Templates may accept configuration data in YAML format which immediately | ||||
| follows include tag: | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Include: <path> | ||||
|      <yaml-data> --> | ||||
| ``` | ||||
| 
 | ||||
| Mark also supports macro definitions, which are defined as regexps which will | ||||
| be replaced with specified template: | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Macro: <regexp> | ||||
|      Template: <path> | ||||
|      <yaml-data> --> | ||||
| ``` | ||||
| 
 | ||||
| Capture groups can be defined in the macro's <regexp> which can be later | ||||
| referenced in the `<yaml-data>` using `${<number>}` syntax, where `<number>` is | ||||
| number of a capture group in regexp (`${0}` is used for entire regexp match), | ||||
| for example: | ||||
| 
 | ||||
| ```markdown | ||||
|   <!-- Macro: MYJIRA-\d+ | ||||
|        Template: ac:jira:ticket | ||||
|        Ticket: ${0} --> | ||||
| ``` | ||||
| 
 | ||||
| By default, mark provides several built-in templates and macros: | ||||
| 
 | ||||
| * template `ac:status` to include badge-like text, which accepts following | ||||
|   parameters: | ||||
|   - Title: text to display in the badge | ||||
|   - Color: color to use as background/border for badge | ||||
|     - Grey | ||||
|     - Red | ||||
|     - Yellow | ||||
|     - Green | ||||
|     - Blue | ||||
|   - Subtle: specify to fill badge with background or not | ||||
|     - true | ||||
|     - false | ||||
| 
 | ||||
| * template `ac:jira:ticket` to include JIRA ticket link. Parameters: | ||||
|   - Ticket: Jira ticket number like BUGS-123. | ||||
| 
 | ||||
|   See: https://confluence.atlassian.com/conf59/status-macro-792499207.html | ||||
| 
 | ||||
| * macro `@{...}` to mention user by name specified in the braces. | ||||
| 
 | ||||
| ## Template & Macros Usecases | ||||
| 
 | ||||
| ### Insert Disclamer | ||||
| 
 | ||||
| **disclamer.md** | ||||
| 
 | ||||
| ```markdown | ||||
| **NOTE**: this document is generated, do not edit manually. | ||||
| ``` | ||||
| 
 | ||||
| **article.md** | ||||
| ```markdown | ||||
| <!-- Space: TEST --> | ||||
| <!-- Title: My Article --> | ||||
| 
 | ||||
| <!-- Include: disclamer.md --> | ||||
| 
 | ||||
| This is my article. | ||||
| ``` | ||||
| 
 | ||||
| ### Insert Status Badge | ||||
| 
 | ||||
| **article.md** | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Space: TEST --> | ||||
| <!-- Title: TODO List --> | ||||
| 
 | ||||
| <!-- Macro: :done: | ||||
|      Template: ac:status | ||||
|      Title: DONE | ||||
|      Color: Green --> | ||||
| 
 | ||||
| <!-- Macro: :todo: | ||||
|      Template: ac:status | ||||
|      Title: TODO | ||||
|      Color: Blue --> | ||||
| 
 | ||||
| * :done: Write Article | ||||
| * :todo: Publish Article | ||||
| ``` | ||||
| 
 | ||||
| ## Insert Jira Ticket | ||||
| 
 | ||||
| **article.md** | ||||
| 
 | ||||
| ```markdown | ||||
| <!-- Space: TEST --> | ||||
| <!-- Title: TODO List --> | ||||
| 
 | ||||
| <!-- Macro: MYJIRA-\d+ | ||||
|      Template: ac:jira:ticket | ||||
|      Ticket: ${0} --> | ||||
| 
 | ||||
| See task MYJIRA-123. | ||||
| ``` | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ``` | ||||
| mark [options] [-u <username>] [-p <password>] [-k] [-l <url>] -f <file> | ||||
| mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <file> | ||||
|  | ||||
| @ -1 +0,0 @@ | ||||
| package main | ||||
| @ -1,29 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import "fmt" | ||||
| 
 | ||||
| type MacroLayout struct { | ||||
| 	layout  string | ||||
| 	columns [][]byte | ||||
| } | ||||
| 
 | ||||
| func (layout MacroLayout) Render() string { | ||||
| 	switch layout.layout { | ||||
| 	case "plain": | ||||
| 		return string(layout.columns[0]) | ||||
| 
 | ||||
| 	case "article": | ||||
| 		fallthrough | ||||
| 
 | ||||
| 	default: | ||||
| 		return fmt.Sprintf( | ||||
| 			`<ac:layout>`+ | ||||
| 				`<ac:layout-section ac:type="two_right_sidebar">`+ | ||||
| 				`<ac:layout-cell>%s</ac:layout-cell>`+ | ||||
| 				`<ac:layout-cell></ac:layout-cell>`+ | ||||
| 				`</ac:layout-section>`+ | ||||
| 				`</ac:layout>`, | ||||
| 			string(layout.columns[0]), | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										191
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								main.go
									
									
									
									
									
								
							| @ -1,6 +1,7 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| @ -8,10 +9,12 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/godocs" | ||||
| 	"github.com/kovetskiy/lorg" | ||||
| 	"github.com/kovetskiy/mark/pkg/confluence" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark" | ||||
| 	"github.com/reconquest/cog" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/includes" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/macro" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/stdlib" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| @ -48,12 +51,60 @@ parent by title, it will be created. | ||||
| 
 | ||||
| Also, optional following headers are supported: | ||||
| 
 | ||||
|   * <!-- Layout: <article|plain> --> | ||||
|   * <!-- Layout: (article|plain) --> | ||||
| 
 | ||||
|     - (default) article: content will be put in narrow column for ease of | ||||
|       reading; | ||||
|     - plain: content will fill all page; | ||||
| 
 | ||||
| Mark supports Go templates, which can be included into article by using path | ||||
| to the template relative to current working dir, e.g.: | ||||
| 
 | ||||
|   <!-- Include: <path> --> | ||||
| 
 | ||||
| Templates may accept configuration data in YAML format which immediately | ||||
| follows include tag: | ||||
| 
 | ||||
|   <!-- Include: <path> | ||||
|        <yaml-data> --> | ||||
| 
 | ||||
| Mark also supports macro definitions, which are defined as regexps which will | ||||
| be replaced with specified template: | ||||
| 
 | ||||
|   <!-- Macro: <regexp> | ||||
|        Template: <path> | ||||
|        <yaml-data> --> | ||||
| 
 | ||||
| Capture groups can be defined in the macro's <regexp> which can be later | ||||
| referenced in the <yaml-data> using ${<number>} syntax, where <number> is | ||||
| number of a capture group in regexp (${0} is used for entire regexp match), for | ||||
| example: | ||||
| 
 | ||||
|   <!-- Macro: MYJIRA-\d+ | ||||
|        Template: ac:jira:ticket | ||||
|        Ticket: ${0} --> | ||||
| 
 | ||||
| By default, mark provides several built-in templates and macros: | ||||
| 
 | ||||
| * template 'ac:status' to include badge-like text, which accepts following | ||||
|   parameters: | ||||
|   - Title: text to display in the badge | ||||
|   - Color: color to use as background/border for badge | ||||
|     - Grey | ||||
|     - Yellow | ||||
|     - Red | ||||
|     - Blue | ||||
|   - Subtle: specify to fill badge with background or not | ||||
|     - true | ||||
|     - false | ||||
| 
 | ||||
|   See: https://confluence.atlassian.com/conf59/status-macro-792499207.html | ||||
| 
 | ||||
| * template 'ac:jira:ticket' to include JIRA ticket link. Parameters: | ||||
|   - Ticket: Jira ticket number like BUGS-123. | ||||
| 
 | ||||
| * macro '@{...}' to mention user by name specified in the braces. | ||||
| 
 | ||||
| Usage: | ||||
|   mark [options] [-u <username>] [-p <token>] [-k] [-l <url>] -f <file> | ||||
|   mark [options] [-u <username>] [-p <password>] [-k] [-b <url>] -f <file> | ||||
| @ -80,31 +131,6 @@ Options: | ||||
| ` | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	log *cog.Logger | ||||
| ) | ||||
| 
 | ||||
| func initlog(debug, trace bool) { | ||||
| 	stderr := lorg.NewLog() | ||||
| 	stderr.SetIndentLines(true) | ||||
| 	stderr.SetFormat( | ||||
| 		lorg.NewFormat("${time} ${level:[%s]:right:short} ${prefix}%s"), | ||||
| 	) | ||||
| 
 | ||||
| 	log = cog.NewLogger(stderr) | ||||
| 
 | ||||
| 	if debug { | ||||
| 		log.SetLevel(lorg.LevelDebug) | ||||
| 	} | ||||
| 
 | ||||
| 	if trace { | ||||
| 		log.SetLevel(lorg.LevelTrace) | ||||
| 	} | ||||
| 
 | ||||
| 	mark.SetLogger(log) | ||||
| 	confluence.SetLogger(log) | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	args, err := godocs.Parse(usage, "mark 1.0", godocs.UsePager) | ||||
| 	if err != nil { | ||||
| @ -117,28 +143,13 @@ func main() { | ||||
| 		editLock      = args["-k"].(bool) | ||||
| 	) | ||||
| 
 | ||||
| 	initlog(args["--debug"].(bool), args["--trace"].(bool)) | ||||
| 	log.Init(args["--debug"].(bool), args["--trace"].(bool)) | ||||
| 
 | ||||
| 	config, err := LoadConfig(filepath.Join(os.Getenv("HOME"), ".config/mark")) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	markdownData, err := ioutil.ReadFile(targetFile) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	meta, err := mark.ExtractMeta(markdownData) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if dryRun { | ||||
| 		fmt.Println(string(mark.CompileMarkdown(markdownData))) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 
 | ||||
| 	creds, err := GetCredentials(args, config) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| @ -146,10 +157,61 @@ func main() { | ||||
| 
 | ||||
| 	api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password) | ||||
| 
 | ||||
| 	markdown, err := ioutil.ReadFile(targetFile) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	meta, markdown, err := mark.ExtractMeta(markdown) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	stdlib, err := stdlib.New(api) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	templates := stdlib.Templates | ||||
| 
 | ||||
| 	var recurse bool | ||||
| 
 | ||||
| 	for { | ||||
| 		templates, markdown, recurse, err = includes.ProcessIncludes( | ||||
| 			markdown, | ||||
| 			templates, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 
 | ||||
| 		if !recurse { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	macros, markdown, err := macro.ExtractMacros(markdown, templates) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	macros = append(macros, stdlib.Macros...) | ||||
| 
 | ||||
| 	for _, macro := range macros { | ||||
| 		markdown, err = macro.Apply(markdown) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if dryRun { | ||||
| 		fmt.Println(mark.CompileMarkdown(markdown, stdlib)) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 
 | ||||
| 	if creds.PageID != "" && meta != nil { | ||||
| 		log.Warningf( | ||||
| 			nil, | ||||
| 			`specified file contains metadata, `+ | ||||
| 		log.Warning( | ||||
| 			`specified file contains metadata, ` + | ||||
| 				`but it will be ignored due specified command line URL`, | ||||
| 		) | ||||
| 
 | ||||
| @ -157,10 +219,9 @@ func main() { | ||||
| 	} | ||||
| 
 | ||||
| 	if creds.PageID == "" && meta == nil { | ||||
| 		log.Fatalf( | ||||
| 			nil, | ||||
| 			`specified file doesn't contain metadata `+ | ||||
| 				`and URL is not specified via command line `+ | ||||
| 		log.Fatal( | ||||
| 			`specified file doesn't contain metadata ` + | ||||
| 				`and URL is not specified via command line ` + | ||||
| 				`or doesn't contain pageId GET-parameter`, | ||||
| 		) | ||||
| 	} | ||||
| @ -195,18 +256,36 @@ func main() { | ||||
| 		log.Fatalf(err, "unable to create/update attachments") | ||||
| 	} | ||||
| 
 | ||||
| 	markdownData = mark.CompileAttachmentLinks(markdownData, attaches) | ||||
| 	markdown = mark.CompileAttachmentLinks(markdown, attaches) | ||||
| 
 | ||||
| 	htmlData := mark.CompileMarkdown(markdownData) | ||||
| 	html := mark.CompileMarkdown(markdown, stdlib) | ||||
| 
 | ||||
| 	err = api.UpdatePage( | ||||
| 		target, | ||||
| 		MacroLayout{meta.Layout, [][]byte{htmlData}}.Render(), | ||||
| 	{ | ||||
| 		var buffer bytes.Buffer | ||||
| 
 | ||||
| 		err := stdlib.Templates.ExecuteTemplate( | ||||
| 			&buffer, | ||||
| 			"ac:layout", | ||||
| 			struct { | ||||
| 				Layout string | ||||
| 				Body   string | ||||
| 			}{ | ||||
| 				Layout: meta.Layout, | ||||
| 				Body:   html, | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 
 | ||||
| 		html = buffer.String() | ||||
| 	} | ||||
| 
 | ||||
| 	err = api.UpdatePage(target, html) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if editLock { | ||||
| 		log.Infof( | ||||
| 			nil, | ||||
|  | ||||
| @ -12,8 +12,6 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/bndr/gopencils" | ||||
| 	"github.com/kovetskiy/lorg" | ||||
| 	"github.com/reconquest/cog" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| @ -59,20 +57,6 @@ type AttachmentInfo struct { | ||||
| 	} `json:"_links"` | ||||
| } | ||||
| 
 | ||||
| func discarder() *lorg.Log { | ||||
| 	stderr := lorg.NewLog() | ||||
| 	stderr.SetOutput(ioutil.Discard) | ||||
| 	return stderr | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	log = cog.NewLogger(discarder()) | ||||
| ) | ||||
| 
 | ||||
| func SetLogger(logger *cog.Logger) { | ||||
| 	log = logger | ||||
| } | ||||
| 
 | ||||
| type form struct { | ||||
| 	buffer io.Reader | ||||
| 	writer *multipart.Writer | ||||
| @ -471,6 +455,35 @@ func (api *API) UpdatePage( | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (api *API) GetUserByName(name string) (*User, error) { | ||||
| 	var response struct { | ||||
| 		Results []struct { | ||||
| 			User User | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	_, err := api.rest. | ||||
| 		Res("search"). | ||||
| 		Res("user", &response). | ||||
| 		Get(map[string]string{ | ||||
| 			"cql": fmt.Sprintf("user.fullname~%q", name), | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if len(response.Results) == 0 { | ||||
| 		return nil, karma. | ||||
| 			Describe("name", name). | ||||
| 			Reason( | ||||
| 				"user with given name is not found", | ||||
| 			) | ||||
| 	} | ||||
| 
 | ||||
| 	return &response.Results[0].User, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func (api *API) GetCurrentUser() (*User, error) { | ||||
| 	var user User | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										100
									
								
								pkg/log/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								pkg/log/log.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| package log | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/kovetskiy/lorg" | ||||
| 	"github.com/reconquest/cog" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	log *cog.Logger | ||||
| ) | ||||
| 
 | ||||
| func Init(debug, trace bool) { | ||||
| 	stderr := lorg.NewLog() | ||||
| 	stderr.SetIndentLines(true) | ||||
| 	stderr.SetFormat( | ||||
| 		lorg.NewFormat("${time} ${level:[%s]:right:short} ${prefix}%s"), | ||||
| 	) | ||||
| 
 | ||||
| 	log = cog.NewLogger(stderr) | ||||
| 
 | ||||
| 	if debug { | ||||
| 		log.SetLevel(lorg.LevelDebug) | ||||
| 	} | ||||
| 
 | ||||
| 	if trace { | ||||
| 		log.SetLevel(lorg.LevelTrace) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Fatalf( | ||||
| 	reason interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Fatalf(reason, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Errorf( | ||||
| 	reason interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Errorf(reason, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Warningf( | ||||
| 	reason interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Warningf(reason, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Infof( | ||||
| 	context interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Infof(context, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Debugf( | ||||
| 	context interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Debugf(context, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Tracef( | ||||
| 	context interface{}, | ||||
| 	message string, | ||||
| 	args ...interface{}, | ||||
| ) { | ||||
| 	log.Tracef(context, message, args...) | ||||
| } | ||||
| 
 | ||||
| func Fatal(values ...interface{}) { | ||||
| 	log.Fatal(values...) | ||||
| } | ||||
| 
 | ||||
| func Error(values ...interface{}) { | ||||
| 	log.Error(values...) | ||||
| } | ||||
| 
 | ||||
| func Warning(values ...interface{}) { | ||||
| 	log.Warning(values...) | ||||
| } | ||||
| 
 | ||||
| func Info(values ...interface{}) { | ||||
| 	log.Info(values...) | ||||
| } | ||||
| 
 | ||||
| func Debug(values ...interface{}) { | ||||
| 	log.Debug(values...) | ||||
| } | ||||
| 
 | ||||
| func Trace(values ...interface{}) { | ||||
| 	log.Trace(values...) | ||||
| } | ||||
| @ -5,7 +5,7 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/confluence" | ||||
| 	"github.com/reconquest/faces/logger" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| @ -56,7 +56,7 @@ func EnsureAncestry( | ||||
| 		return parent, nil | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Debugf( | ||||
| 	log.Debugf( | ||||
| 		"empty pages under %q to be created: %s", | ||||
| 		parent.Title, | ||||
| 		strings.Join(rest, ` > `), | ||||
|  | ||||
| @ -13,6 +13,7 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/confluence" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										151
									
								
								pkg/mark/includes/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								pkg/mark/includes/templates.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | ||||
| package includes | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 
 | ||||
| 	"gopkg.in/yaml.v2" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	reIncludeDirective = regexp.MustCompile( | ||||
| 		// <!-- Include: <template path> | ||||
| 		//      <optional yaml data> --> | ||||
| 
 | ||||
| 		`(?s)` + // dot capture newlines | ||||
| 			/**/ `<!--\s*Include:\s*(?P<template>\S+)\s*` + | ||||
| 			/*   */ `(\n(?P<config>.*?))?-->`, | ||||
| 	) | ||||
| ) | ||||
| 
 | ||||
| func LoadTemplate( | ||||
| 	path string, | ||||
| 	templates *template.Template, | ||||
| ) (*template.Template, error) { | ||||
| 	var ( | ||||
| 		name  = strings.TrimSuffix(path, filepath.Ext(path)) | ||||
| 		facts = karma.Describe("name", name) | ||||
| 	) | ||||
| 
 | ||||
| 	if template := templates.Lookup(name); template != nil { | ||||
| 		return template, nil | ||||
| 	} | ||||
| 
 | ||||
| 	var body []byte | ||||
| 
 | ||||
| 	body, err := ioutil.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		err = facts.Format( | ||||
| 			err, | ||||
| 			"unable to read template file", | ||||
| 		) | ||||
| 
 | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	templates, err = templates.New(name).Parse(string(body)) | ||||
| 	if err != nil { | ||||
| 		err = facts.Format( | ||||
| 			err, | ||||
| 			"unable to parse template", | ||||
| 		) | ||||
| 
 | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return templates, nil | ||||
| } | ||||
| 
 | ||||
| func ProcessIncludes( | ||||
| 	contents []byte, | ||||
| 	templates *template.Template, | ||||
| ) (*template.Template, []byte, bool, error) { | ||||
| 	vardump := func( | ||||
| 		facts *karma.Context, | ||||
| 		data map[string]interface{}, | ||||
| 	) *karma.Context { | ||||
| 		for key, value := range data { | ||||
| 			key = "var " + key | ||||
| 			facts = facts.Describe( | ||||
| 				key, | ||||
| 				strings.ReplaceAll( | ||||
| 					fmt.Sprint(value), | ||||
| 					"\n", | ||||
| 					"\n"+strings.Repeat(" ", len(key)+2), | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 
 | ||||
| 		return facts | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
| 		recurse bool | ||||
| 		err     error | ||||
| 	) | ||||
| 
 | ||||
| 	contents = reIncludeDirective.ReplaceAllFunc( | ||||
| 		contents, | ||||
| 		func(spec []byte) []byte { | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			groups := reIncludeDirective.FindSubmatch(spec) | ||||
| 
 | ||||
| 			var ( | ||||
| 				path, config = string(groups[1]), groups[2] | ||||
| 				data         = map[string]interface{}{} | ||||
| 
 | ||||
| 				facts = karma.Describe("path", path) | ||||
| 			) | ||||
| 
 | ||||
| 			err = yaml.Unmarshal(config, &data) | ||||
| 			if err != nil { | ||||
| 				err = facts. | ||||
| 					Describe("config", string(config)). | ||||
| 					Format( | ||||
| 						err, | ||||
| 						"unable to unmarshal template data config", | ||||
| 					) | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			log.Tracef(vardump(facts, data), "including template %q", path) | ||||
| 
 | ||||
| 			templates, err = LoadTemplate(path, templates) | ||||
| 			if err != nil { | ||||
| 				err = facts.Format(err, "unable to load template") | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			var buffer bytes.Buffer | ||||
| 
 | ||||
| 			err = templates.Execute(&buffer, data) | ||||
| 			if err != nil { | ||||
| 				err = vardump(facts, data).Format( | ||||
| 					err, | ||||
| 					"unable to execute template", | ||||
| 				) | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			recurse = true | ||||
| 
 | ||||
| 			return buffer.Bytes() | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return templates, contents, recurse, err | ||||
| } | ||||
							
								
								
									
										172
									
								
								pkg/mark/macro/macro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								pkg/mark/macro/macro.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | ||||
| package macro | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/includes" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| 	"github.com/reconquest/regexputil-go" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| ) | ||||
| 
 | ||||
| var reMacroDirective = regexp.MustCompile( | ||||
| 	// <!-- Macro: <regexp> | ||||
| 	//      Template: <template path> | ||||
| 	//      <optional yaml data> --> | ||||
| 
 | ||||
| 	`(?s)` + // dot capture newlines | ||||
| 		/**/ `<!--\s*Macro:\s*(?P<expr>[^\n]+)\n` + | ||||
| 		/*    */ `\s*Template:\s*(?P<template>\S+)\s*` + | ||||
| 		/*   */ `(\n(?P<config>.*?))?-->`, | ||||
| ) | ||||
| 
 | ||||
| type Macro struct { | ||||
| 	Regexp   *regexp.Regexp | ||||
| 	Template *template.Template | ||||
| 	Config   map[string]interface{} | ||||
| } | ||||
| 
 | ||||
| func (macro *Macro) Apply( | ||||
| 	content []byte, | ||||
| ) ([]byte, error) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	content = macro.Regexp.ReplaceAllFunc( | ||||
| 		content, | ||||
| 		func(match []byte) []byte { | ||||
| 			config := macro.configure( | ||||
| 				macro.Config, | ||||
| 				macro.Regexp.FindSubmatch(match), | ||||
| 			) | ||||
| 
 | ||||
| 			var buffer bytes.Buffer | ||||
| 
 | ||||
| 			err = macro.Template.Execute(&buffer, config) | ||||
| 			if err != nil { | ||||
| 				err = karma.Format( | ||||
| 					err, | ||||
| 					"unable to execute macros template", | ||||
| 				) | ||||
| 			} | ||||
| 
 | ||||
| 			return buffer.Bytes() | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return content, err | ||||
| } | ||||
| 
 | ||||
| func (macro *Macro) configure(node interface{}, groups [][]byte) interface{} { | ||||
| 	switch node := node.(type) { | ||||
| 	case map[interface{}]interface{}: | ||||
| 		for key, value := range node { | ||||
| 			node[key] = macro.configure(value, groups) | ||||
| 		} | ||||
| 
 | ||||
| 		return node | ||||
| 	case map[string]interface{}: | ||||
| 		for key, value := range node { | ||||
| 			node[key] = macro.configure(value, groups) | ||||
| 		} | ||||
| 
 | ||||
| 		return node | ||||
| 	case []interface{}: | ||||
| 		for key, value := range node { | ||||
| 			node[key] = macro.configure(value, groups) | ||||
| 		} | ||||
| 
 | ||||
| 		return node | ||||
| 	case string: | ||||
| 		for i, group := range groups { | ||||
| 			node = strings.ReplaceAll( | ||||
| 				node, | ||||
| 				fmt.Sprintf("${%d}", i), | ||||
| 				string(group), | ||||
| 			) | ||||
| 		} | ||||
| 
 | ||||
| 		return node | ||||
| 	} | ||||
| 
 | ||||
| 	return node | ||||
| } | ||||
| 
 | ||||
| func ExtractMacros( | ||||
| 	contents []byte, | ||||
| 	templates *template.Template, | ||||
| ) ([]Macro, []byte, error) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	var macros []Macro | ||||
| 
 | ||||
| 	contents = reMacroDirective.ReplaceAllFunc( | ||||
| 		contents, | ||||
| 		func(spec []byte) []byte { | ||||
| 			if err != nil { | ||||
| 				return spec | ||||
| 			} | ||||
| 
 | ||||
| 			groups := reMacroDirective.FindStringSubmatch(string(spec)) | ||||
| 
 | ||||
| 			var ( | ||||
| 				expr     = regexputil.Subexp(reMacroDirective, groups, "expr") | ||||
| 				template = regexputil.Subexp(reMacroDirective, groups, "template") | ||||
| 				config   = regexputil.Subexp(reMacroDirective, groups, "config") | ||||
| 
 | ||||
| 				macro Macro | ||||
| 			) | ||||
| 
 | ||||
| 			macro.Template, err = includes.LoadTemplate(template, templates) | ||||
| 
 | ||||
| 			if err != nil { | ||||
| 				err = karma.Format(err, "unable to load template") | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			facts := karma. | ||||
| 				Describe("template", template). | ||||
| 				Describe("expr", expr) | ||||
| 
 | ||||
| 			macro.Regexp, err = regexp.Compile(expr) | ||||
| 			if err != nil { | ||||
| 				err = facts. | ||||
| 					Format( | ||||
| 						err, | ||||
| 						"unable to compile macros regexp", | ||||
| 					) | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			err = yaml.Unmarshal([]byte(config), ¯o.Config) | ||||
| 			if err != nil { | ||||
| 				err = facts. | ||||
| 					Describe("config", string(config)). | ||||
| 					Format( | ||||
| 						err, | ||||
| 						"unable to unmarshal template data config", | ||||
| 					) | ||||
| 
 | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			log.Tracef( | ||||
| 				facts.Describe("config", macro.Config), | ||||
| 				"loaded macro %q", | ||||
| 				expr, | ||||
| 			) | ||||
| 
 | ||||
| 			macros = append(macros, macro) | ||||
| 
 | ||||
| 			return []byte{} | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return macros, contents, err | ||||
| } | ||||
| @ -4,7 +4,7 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/confluence" | ||||
| 	"github.com/reconquest/faces/logger" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| @ -47,7 +47,7 @@ func ResolvePage( | ||||
| 		path := meta.Parents | ||||
| 		path = append(path, meta.Title) | ||||
| 
 | ||||
| 		logger.Debugf( | ||||
| 		log.Debugf( | ||||
| 			"resolving page path: ??? > %s", | ||||
| 			strings.Join(path, ` > `), | ||||
| 		) | ||||
|  | ||||
| @ -2,14 +2,17 @@ package mark | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/stdlib" | ||||
| 	"github.com/russross/blackfriday" | ||||
| ) | ||||
| 
 | ||||
| type ConfluenceRenderer struct { | ||||
| 	blackfriday.Renderer | ||||
| 
 | ||||
| 	Stdlib *stdlib.Lib | ||||
| } | ||||
| 
 | ||||
| func (renderer ConfluenceRenderer) BlockCode( | ||||
| @ -17,22 +20,16 @@ func (renderer ConfluenceRenderer) BlockCode( | ||||
| 	text []byte, | ||||
| 	lang string, | ||||
| ) { | ||||
| 	out.WriteString(MacroCode{lang, text}.Render()) | ||||
| } | ||||
| 
 | ||||
| type MacroCode struct { | ||||
| 	lang string | ||||
| 	code []byte | ||||
| } | ||||
| 
 | ||||
| func (code MacroCode) Render() string { | ||||
| 	return fmt.Sprintf( | ||||
| 		`<ac:structured-macro ac:name="code">`+ | ||||
| 			`<ac:parameter ac:name="language">%s</ac:parameter>`+ | ||||
| 			`<ac:parameter ac:name="collapse">false</ac:parameter>`+ | ||||
| 			`<ac:plain-text-body><![CDATA[%s]]></ac:plain-text-body>`+ | ||||
| 			`</ac:structured-macro>`, | ||||
| 		code.lang, code.code, | ||||
| 	renderer.Stdlib.Templates.ExecuteTemplate( | ||||
| 		out, | ||||
| 		"ac:code", | ||||
| 		struct { | ||||
| 			Language string | ||||
| 			Text     string | ||||
| 		}{ | ||||
| 			lang, | ||||
| 			string(text), | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| @ -41,12 +38,13 @@ func (code MacroCode) Render() string { | ||||
| // <a href="ac:rich-text-body">ac:rich-text-body</a> for whatever reason. | ||||
| func CompileMarkdown( | ||||
| 	markdown []byte, | ||||
| ) []byte { | ||||
| 	stdlib *stdlib.Lib, | ||||
| ) string { | ||||
| 	log.Tracef(nil, "rendering markdown:\n%s", string(markdown)) | ||||
| 
 | ||||
| 	colon := regexp.MustCompile(`---BLACKFRIDAY-COLON---`) | ||||
| 
 | ||||
| 	tags := regexp.MustCompile(`<(/?\S+):(\S+)>`) | ||||
| 	tags := regexp.MustCompile(`<(/?\S+?):(\S+?)>`) | ||||
| 
 | ||||
| 	markdown = tags.ReplaceAll( | ||||
| 		markdown, | ||||
| @ -54,7 +52,7 @@ func CompileMarkdown( | ||||
| 	) | ||||
| 
 | ||||
| 	renderer := ConfluenceRenderer{ | ||||
| 		blackfriday.HtmlRenderer( | ||||
| 		Renderer: blackfriday.HtmlRenderer( | ||||
| 			blackfriday.HTML_USE_XHTML| | ||||
| 				blackfriday.HTML_USE_SMARTYPANTS| | ||||
| 				blackfriday.HTML_SMARTYPANTS_FRACTIONS| | ||||
| @ -62,6 +60,8 @@ func CompileMarkdown( | ||||
| 				blackfriday.HTML_SMARTYPANTS_LATEX_DASHES, | ||||
| 			"", "", | ||||
| 		), | ||||
| 
 | ||||
| 		Stdlib: stdlib, | ||||
| 	} | ||||
| 
 | ||||
| 	html := blackfriday.MarkdownOptions( | ||||
| @ -88,5 +88,5 @@ func CompileMarkdown( | ||||
| 
 | ||||
| 	log.Tracef(nil, "rendered markdown to html:\n%s", string(html)) | ||||
| 
 | ||||
| 	return html | ||||
| 	return string(html) | ||||
| } | ||||
|  | ||||
| @ -4,28 +4,12 @@ import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/lorg" | ||||
| 	"github.com/reconquest/cog" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| ) | ||||
| 
 | ||||
| func discarder() *lorg.Log { | ||||
| 	stderr := lorg.NewLog() | ||||
| 	stderr.SetOutput(ioutil.Discard) | ||||
| 	return stderr | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	log = cog.NewLogger(discarder()) | ||||
| ) | ||||
| 
 | ||||
| func SetLogger(logger *cog.Logger) { | ||||
| 	log = logger | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	HeaderParent     = `Parent` | ||||
| 	HeaderSpace      = `Space` | ||||
| @ -42,25 +26,30 @@ type Meta struct { | ||||
| 	Attachments []string | ||||
| } | ||||
| 
 | ||||
| func ExtractMeta(data []byte) (*Meta, error) { | ||||
| 	var ( | ||||
| 		headerPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`) | ||||
| 		headerPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`) | ||||
| 	) | ||||
| var ( | ||||
| 	reHeaderPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`) | ||||
| 	reHeaderPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`) | ||||
| ) | ||||
| 
 | ||||
| 	var meta *Meta | ||||
| func ExtractMeta(data []byte) (*Meta, []byte, error) { | ||||
| 	var ( | ||||
| 		meta   *Meta | ||||
| 		offset int | ||||
| 	) | ||||
| 
 | ||||
| 	scanner := bufio.NewScanner(bytes.NewBuffer(data)) | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
| 
 | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		matches := headerPatternV2.FindStringSubmatch(line) | ||||
| 		offset += len(line) | ||||
| 
 | ||||
| 		matches := reHeaderPatternV2.FindStringSubmatch(line) | ||||
| 		if matches == nil { | ||||
| 			matches = headerPatternV1.FindStringSubmatch(line) | ||||
| 			matches = reHeaderPatternV1.FindStringSubmatch(line) | ||||
| 			if matches == nil { | ||||
| 				break | ||||
| 			} | ||||
| @ -113,22 +102,22 @@ func ExtractMeta(data []byte) (*Meta, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	if meta == nil { | ||||
| 		return nil, nil | ||||
| 		return nil, data, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if meta.Space == "" { | ||||
| 		return nil, fmt.Errorf( | ||||
| 		return nil, nil, fmt.Errorf( | ||||
| 			"space key is not set (%s header is not set)", | ||||
| 			HeaderSpace, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	if meta.Title == "" { | ||||
| 		return nil, fmt.Errorf( | ||||
| 		return nil, nil, fmt.Errorf( | ||||
| 			"page title is not set (%s header is not set)", | ||||
| 			HeaderTitle, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return meta, nil | ||||
| 	return nil, data[offset+1:], nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										153
									
								
								pkg/mark/stdlib/stdlib.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								pkg/mark/stdlib/stdlib.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | ||||
| package stdlib | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 
 | ||||
| 	"github.com/kovetskiy/mark/pkg/confluence" | ||||
| 	"github.com/kovetskiy/mark/pkg/log" | ||||
| 	"github.com/kovetskiy/mark/pkg/mark/macro" | ||||
| 
 | ||||
| 	"github.com/reconquest/karma-go" | ||||
| ) | ||||
| 
 | ||||
| type Lib struct { | ||||
| 	Macros    []macro.Macro | ||||
| 	Templates *template.Template | ||||
| } | ||||
| 
 | ||||
| func New(api *confluence.API) (*Lib, error) { | ||||
| 	var ( | ||||
| 		lib Lib | ||||
| 		err error | ||||
| 	) | ||||
| 
 | ||||
| 	lib.Templates, err = templates(api) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	lib.Macros, err = macros(lib.Templates) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &lib, nil | ||||
| } | ||||
| 
 | ||||
| func macros(templates *template.Template) ([]macro.Macro, error) { | ||||
| 	text := func(line ...string) []byte { | ||||
| 		return []byte(strings.Join(line, "\n")) | ||||
| 	} | ||||
| 
 | ||||
| 	macros, _, err := macro.ExtractMacros( | ||||
| 		[]byte(text( | ||||
| 			`<!-- Macro: @\{([^}]+)\}`, | ||||
| 			`     Template: ac:link:user`, | ||||
| 			`     Name: ${1} -->`, | ||||
| 
 | ||||
| 			// TODO(seletskiy): more macros here | ||||
| 		)), | ||||
| 
 | ||||
| 		templates, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return macros, nil | ||||
| } | ||||
| 
 | ||||
| func templates(api *confluence.API) (*template.Template, error) { | ||||
| 	text := func(line ...string) string { | ||||
| 		return strings.Join(line, ``) | ||||
| 	} | ||||
| 
 | ||||
| 	templates := template.New(`stdlib`).Funcs( | ||||
| 		template.FuncMap{ | ||||
| 			"user": func(name string) *confluence.User { | ||||
| 				user, err := api.GetUserByName(name) | ||||
| 				if err != nil { | ||||
| 					log.Error(err) | ||||
| 				} | ||||
| 
 | ||||
| 				return user | ||||
| 			}, | ||||
| 
 | ||||
| 			// The only way to escape CDATA end marker ']]>' is to split it | ||||
| 			// into two CDATA sections. | ||||
| 			"cdata": func(data string) string { | ||||
| 				return strings.ReplaceAll( | ||||
| 					data, | ||||
| 					"]]>", | ||||
| 					"]]><![CDATA[]]]]><![CDATA[>", | ||||
| 				) | ||||
| 			}, | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	var err error | ||||
| 
 | ||||
| 	for name, body := range map[string]string{ | ||||
| 		// This template is used to select whole article layout | ||||
| 		`ac:layout`: text( | ||||
| 			`{{ if eq .Layout "article" }}`, | ||||
| 			/**/ `<ac:layout>`, | ||||
| 			/**/ `<ac:layout-section ac:type="two_right_sidebar">`, | ||||
| 			/**/ `<ac:layout-cell>{{ .Body }}</ac:layout-cell>`, | ||||
| 			/**/ `<ac:layout-cell></ac:layout-cell>`, | ||||
| 			/**/ `</ac:layout-section>`, | ||||
| 			/**/ `</ac:layout>`, | ||||
| 			`{{ else }}`, | ||||
| 			/**/ `{{ .Body }}`, | ||||
| 			`{{ end }}`, | ||||
| 		), | ||||
| 
 | ||||
| 		// This template is used for rendering code in ``` | ||||
| 		`ac:code`: text( | ||||
| 			`<ac:structured-macro ac:name="code">`, | ||||
| 			`<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>`, | ||||
| 			`<ac:parameter ac:name="collapse">false</ac:parameter>`, | ||||
| 			`<ac:plain-text-body><![CDATA[{{ .Text | cdata }}]]></ac:plain-text-body>`, | ||||
| 			`</ac:structured-macro>`, | ||||
| 		), | ||||
| 
 | ||||
| 		`ac:status`: text( | ||||
| 			`<ac:structured-macro ac:name="status">`, | ||||
| 			`<ac:parameter ac:name="colour">{{ or .Color "Grey" }}</ac:parameter>`, | ||||
| 			`<ac:parameter ac:name="title">{{ or .Title .Color }}</ac:parameter>`, | ||||
| 			`<ac:parameter ac:name="subtle">{{ or .Subtle false }}</ac:parameter>`, | ||||
| 			`</ac:structured-macro>`, | ||||
| 		), | ||||
| 
 | ||||
| 		`ac:link:user`: text( | ||||
| 			`{{ with .Name | user }}`, | ||||
| 			/**/ `<ac:link>`, | ||||
| 			/**/ `<ri:user ri:account-id="{{ .AccountID }}"/>`, | ||||
| 			/**/ `</ac:link>`, | ||||
| 			`{{ else }}`, | ||||
| 			/**/ `{{ .Name }}`, | ||||
| 			`{{ end }}`, | ||||
| 		), | ||||
| 
 | ||||
| 		`ac:jira:ticket`: text( | ||||
| 			`<ac:structured-macro ac:name="jira">`, | ||||
| 			`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`, | ||||
| 			`</ac:structured-macro>`, | ||||
| 		), | ||||
| 
 | ||||
| 		// TODO(seletskiy): more templates here | ||||
| 	} { | ||||
| 		templates, err = templates.New(name).Parse(body) | ||||
| 		if err != nil { | ||||
| 			return nil, karma. | ||||
| 				Describe("template", body). | ||||
| 				Format( | ||||
| 					err, | ||||
| 					"unable to parse template", | ||||
| 				) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return templates, nil | ||||
| } | ||||
| @ -1 +0,0 @@ | ||||
| package main | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Egor Kovetskiy
						Egor Kovetskiy