Merge pull request #277 from mrueg/support-ac-include

feat: Support page include, excerpt and excerpt-include macro
This commit is contained in:
Manuel Rüger 2023-04-03 10:23:00 +02:00 committed by GitHub
commit a29feb1e96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 14 deletions

View File

@ -371,6 +371,19 @@ By default, mark provides several built-in templates and macros:
See: https://confluence.atlassian.com/doc/blog-posts-macro-139470.html See: https://confluence.atlassian.com/doc/blog-posts-macro-139470.html
* template: `ac:include` to include a page
- Page: the page to be included
- Space: the space the page is in (optional, otherwise same space)
* template: `ac:excerpt-include` to include the excerpt from another page
- Page: the page the excerpt should be included from
- NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
* template: `ac:excerpt` to create an excerpt and include it in the page
- Excerpt: The text you want to include
- OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
- Hidden: Hide the excerpt content (optional, default: false)
* macro `@{...}` to mention user by name specified in the braces. * macro `@{...}` to mention user by name specified in the braces.
## Template & Macros Usecases ## Template & Macros Usecases

View File

@ -6,6 +6,7 @@ import (
"regexp" "regexp"
"strings" "strings"
cparser "github.com/kovetskiy/mark/pkg/mark/parser"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log" "github.com/reconquest/pkg/log"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
@ -450,7 +451,7 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string {
converter.Parser().AddOptions(parser.WithInlineParsers( converter.Parser().AddOptions(parser.WithInlineParsers(
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse // Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
// the <ac:*/> tags. // the <ac:*/> tags.
util.Prioritized(NewACTagParser(), 199), util.Prioritized(cparser.NewConfluenceTagParser(), 199),
)) ))
converter.Renderer().AddOptions(renderer.WithNodeRenderers( converter.Renderer().AddOptions(renderer.WithNodeRenderers(

View File

@ -1,4 +1,4 @@
package mark package parser
import ( import (
"bytes" "bytes"
@ -9,25 +9,25 @@ import (
"regexp" "regexp"
) )
// NewACTagParser returns an inline parser that parses <ac:* /> tags to ensure that Confluence specific tags are parsed // NewConfluenceTagParser returns an inline parser that parses <ac:* /> and <ri:* /> tags to ensure that Confluence specific tags are parsed
// as ast.KindRawHtml so they are not escaped at render time. The parser must be registered with a higher priority // as ast.KindRawHtml so they are not escaped at render time. The parser must be registered with a higher priority
// than goldmark's linkParser. Otherwise, the linkParser would parse the <ac:* /> tags. // than goldmark's linkParser. Otherwise, the linkParser would parse the <ac:* /> tags.
func NewACTagParser() parser.InlineParser { func NewConfluenceTagParser() parser.InlineParser {
return &acTagParser{} return &confluenceTagParser{}
} }
var _ parser.InlineParser = (*acTagParser)(nil) var _ parser.InlineParser = (*confluenceTagParser)(nil)
// acTagParser is a stripped down version of goldmark's rawHTMLParser. // confluenceTagParser is a stripped down version of goldmark's rawHTMLParser.
// See: https://github.com/yuin/goldmark/blob/master/parser/raw_html.go // See: https://github.com/yuin/goldmark/blob/master/parser/raw_html.go
type acTagParser struct { type confluenceTagParser struct {
} }
func (s *acTagParser) Trigger() []byte { func (s *confluenceTagParser) Trigger() []byte {
return []byte{'<'} return []byte{'<'}
} }
func (s *acTagParser) Parse(_ ast.Node, block text.Reader, pc parser.Context) ast.Node { func (s *confluenceTagParser) Parse(_ ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine() line, _ := block.PeekLine()
if len(line) > 1 && util.IsAlphaNumeric(line[1]) { if len(line) > 1 && util.IsAlphaNumeric(line[1]) {
return s.parseMultiLineRegexp(openTagRegexp, block, pc) return s.parseMultiLineRegexp(openTagRegexp, block, pc)
@ -48,15 +48,15 @@ var tagnamePattern = `([A-Za-z][A-Za-z0-9-]*)`
var attributePattern = `(?:[\r\n \t]+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:[\r\n \t]*=[\r\n \t]*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)` var attributePattern = `(?:[\r\n \t]+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:[\r\n \t]*=[\r\n \t]*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)`
// Only match <ac:*/> tags // Only match <ac:*/> and <ri:*/> tags
var openTagRegexp = regexp.MustCompile("^<ac:" + tagnamePattern + attributePattern + `*[ \t]*/?>`) var openTagRegexp = regexp.MustCompile("^<(ac|ri):" + tagnamePattern + attributePattern + `*[ \t]*/?>`)
var closeTagRegexp = regexp.MustCompile("^</ac:" + tagnamePattern + `\s*>`) var closeTagRegexp = regexp.MustCompile("^</ac:" + tagnamePattern + `\s*>`)
var openCDATA = []byte("<![CDATA[") var openCDATA = []byte("<![CDATA[")
var closeCDATA = []byte("]]>") var closeCDATA = []byte("]]>")
var closeDecl = []byte(">") var closeDecl = []byte(">")
func (s *acTagParser) parseUntil(block text.Reader, closer []byte, _ parser.Context) ast.Node { func (s *confluenceTagParser) parseUntil(block text.Reader, closer []byte, _ parser.Context) ast.Node {
savedLine, savedSegment := block.Position() savedLine, savedSegment := block.Position()
node := ast.NewRawHTML() node := ast.NewRawHTML()
for { for {
@ -77,7 +77,7 @@ func (s *acTagParser) parseUntil(block text.Reader, closer []byte, _ parser.Cont
return nil return nil
} }
func (s *acTagParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, _ parser.Context) ast.Node { func (s *confluenceTagParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, _ parser.Context) ast.Node {
sline, ssegment := block.Position() sline, ssegment := block.Position()
if block.Match(reg) { if block.Match(reg) {
node := ast.NewRawHTML() node := ast.NewRawHTML()

View File

@ -261,6 +261,39 @@ func templates(api *confluence.API) (*template.Template, error) {
`</ac:structured-macro>{{printf "\n"}}`, `</ac:structured-macro>{{printf "\n"}}`,
), ),
/* https://confluence.atlassian.com/conf59/include-page-macro-792499125.html */
`ac:include`: text(
`<ac:structured-macro ac:name="include">{{printf "\n"}}`,
`<ac:parameter ac:name="">{{printf "\n"}}`,
`<ac:link>{{printf "\n"}}`,
`<ri:page ri:content-title="{{ .Page }}" {{if .Space }}ri:space-key="{{ .Space }}"{{end}}/>{{printf "\n"}}`,
`</ac:link>{{printf "\n"}}`,
`</ac:parameter>{{printf "\n"}}`,
`</ac:structured-macro>{{printf "\n"}}`,
),
/* https://confluence.atlassian.com/conf59/excerpt-include-macro-792499101.html */
`ac:excerpt-include`: text(
`<ac:macro ac:name="excerpt-include">{{printf "\n"}}`,
`<ac:parameter ac:name="nopanel">{{ if .NoPanel }}{{ .NoPanel }}{{ else }}false{{ end }}</ac:parameter>{{printf "\n"}}`,
`<ac:default-parameter>{{ .Page }}</ac:default-parameter>{{printf "\n"}}`,
`</ac:macro>{{printf "\n"}}`,
),
/* https://confluence.atlassian.com/conf59/excerpt-macro-792499102.html */
`ac:excerpt`: text(
`<ac:structured-macro ac:name="excerpt">{{printf "\n"}}`,
`<ac:parameter ac:name="hidden">{{ if .Hidden }}{{ .Hidden }}{{ else }}false{{ end }}</ac:parameter>{{printf "\n"}}`,
`<ac:parameter ac:name="atlassian-macro-output-type">{{ if .OutputType }}{{ .OutputType }}{{ else }}BLOCK{{ end }}</ac:parameter>{{printf "\n"}}`,
`<ac:rich-text-body>{{printf "\n"}}`,
`{{ .Excerpt }}{{printf "\n"}}`,
`</ac:rich-text-body>{{printf "\n"}}`,
`</ac:structured-macro>{{printf "\n"}}`,
),
// TODO(seletskiy): more templates here // TODO(seletskiy): more templates here
} { } {
templates, err = templates.New(name).Parse(body) templates, err = templates.New(name).Parse(body)