diff --git a/README.md b/README.md index 785a1ed..e9ff468 100644 --- a/README.md +++ b/README.md @@ -809,7 +809,7 @@ USAGE: mark [global options] VERSION: - v15.1.0@b3a6f1efae97dfaa1400a3175cdd3377f8176e88 + v15.2.0@1c82927c11a2999a39e5052aae6d2c65a201260c DESCRIPTION: Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark @@ -833,7 +833,7 @@ GLOBAL OPTIONS: --password string, -p string use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication. [$MARK_PASSWORD] --target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL] --base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL] - --config string, -c string use the specified configuration file. (default: "$HOME/.config/mark.toml") [$MARK_CONFIG] + --config string, -c string use the specified configuration file. (default: "${HOME}/.config/mark.toml") [$MARK_CONFIG] --ci run on CI mode. It won't fail if files are not found. [$MARK_CI] --space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE] --parents string A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself. [$MARK_PARENTS] @@ -842,7 +842,7 @@ GLOBAL OPTIONS: --include-path string Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH] --changes-only Avoids re-uploading pages that haven't changed since the last run. [$MARK_CHANGES_ONLY] --d2-scale float defines the scaling factor for d2 renderings. (default: 1) [$MARK_D2_SCALE] - --features string [ --features string ] Enables optional features. Current features: d2, mermaid, mkdocsadmonitions (default: "mermaid") [$MARK_FEATURES] + --features string [ --features string ] Enables optional features. Current features: d2, mermaid, mention, mkdocsadmonitions (default: "mermaid", "mention") [$MARK_FEATURES] --insecure-skip-tls-verify skip TLS certificate verification (useful for self-signed certificates) [$MARK_INSECURE_SKIP_TLS_VERIFY] --help, -h show help --version, -v print the version diff --git a/main.go b/main.go index fd20301..5967b6c 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "github.com/urfave/cli/v3" ) - var ( version = "dev" commit = "none" diff --git a/markdown/markdown.go b/markdown/markdown.go index 2ae7edc..d412670 100644 --- a/markdown/markdown.go +++ b/markdown/markdown.go @@ -70,6 +70,18 @@ func (c *ConfluenceExtension) Extend(m goldmark.Markdown) { )) } + if slices.Contains(c.MarkConfig.Features, "mention") { + m.Parser().AddOptions( + parser.WithInlineParsers( + util.Prioritized(cparser.NewMentionParser(), 99), + ), + ) + + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(crenderer.NewConfluenceMentionRenderer(c.Stdlib), 100), + )) + } + m.Parser().AddOptions(parser.WithInlineParsers( // Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse // the tags. diff --git a/markdown/markdown_test.go b/markdown/markdown_test.go index 0c02c95..91fe041 100644 --- a/markdown/markdown_test.go +++ b/markdown/markdown_test.go @@ -64,7 +64,7 @@ func TestCompileMarkdown(t *testing.T) { D2Scale: 1.0, DropFirstH1: false, StripNewlines: false, - Features: []string{"mkdocsadmonitions"}, + Features: []string{"mkdocsadmonitions", "mention"}, } actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg) @@ -106,7 +106,7 @@ func TestCompileMarkdownDropH1(t *testing.T) { D2Scale: 1.0, DropFirstH1: true, StripNewlines: false, - Features: []string{"mkdocsadmonitions"}, + Features: []string{"mkdocsadmonitions", "mention"}, } actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg) @@ -137,7 +137,7 @@ func TestCompileMarkdownStripNewlines(t *testing.T) { } var variant string switch filename { - case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md", "testdata/admonitions.md": + case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md", "testdata/admonitions.md", "testdata/mention.md": variant = "-stripnewlines" default: variant = "" @@ -150,7 +150,7 @@ func TestCompileMarkdownStripNewlines(t *testing.T) { D2Scale: 1.0, DropFirstH1: false, StripNewlines: true, - Features: []string{"mkdocsadmonitions"}, + Features: []string{"mkdocsadmonitions", "mention"}, } actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg) diff --git a/parser/mention.go b/parser/mention.go new file mode 100644 index 0000000..7c90f75 --- /dev/null +++ b/parser/mention.go @@ -0,0 +1,60 @@ +package parser + +import ( + "bytes" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +type Mention struct { + ast.BaseInline + Name []byte +} + +func (m *Mention) Dump(source []byte, level int) { + ast.DumpHelper(m, source, level, map[string]string{ + "Name": string(m.Name), + }, nil) +} + +var KindMention = ast.NewNodeKind("Mention") + +func (m *Mention) Kind() ast.NodeKind { + return KindMention +} + +func NewMention(name []byte) *Mention { + return &Mention{ + Name: name, + } +} + +type mentionParser struct { +} + +func NewMentionParser() parser.InlineParser { + return &mentionParser{} +} + +func (s *mentionParser) Trigger() []byte { + return []byte{'@'} +} + +func (s *mentionParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + line, _ := block.PeekLine() + if len(line) < 3 || line[1] != '{' { + return nil + } + + index := bytes.IndexByte(line, '}') + if index == -1 || index <= 2 { + return nil + } + + name := line[2:index] + block.Advance(index + 1) + + return NewMention(name) +} diff --git a/renderer/mention.go b/renderer/mention.go new file mode 100644 index 0000000..8ad5328 --- /dev/null +++ b/renderer/mention.go @@ -0,0 +1,42 @@ +package renderer + +import ( + "github.com/kovetskiy/mark/parser" + "github.com/kovetskiy/mark/stdlib" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +type ConfluenceMentionRenderer struct { + Stdlib *stdlib.Lib +} + +func NewConfluenceMentionRenderer(stdlib *stdlib.Lib) renderer.NodeRenderer { + return &ConfluenceMentionRenderer{ + Stdlib: stdlib, + } +} + +func (r *ConfluenceMentionRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(parser.KindMention, r.renderMention) +} + +func (r *ConfluenceMentionRenderer) renderMention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + n := node.(*parser.Mention) + + err := r.Stdlib.Templates.ExecuteTemplate(w, "ac:link:user", struct { + Name string + }{ + Name: string(n.Name), + }) + if err != nil { + return ast.WalkStop, err + } + + return ast.WalkContinue, nil +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 28eeac8..5ef845d 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -5,14 +5,12 @@ import ( "text/template" "github.com/kovetskiy/mark/confluence" - "github.com/kovetskiy/mark/macro" "github.com/reconquest/pkg/log" "github.com/reconquest/karma-go" ) type Lib struct { - Macros []macro.Macro Templates *template.Template } @@ -27,7 +25,6 @@ func New(api *confluence.API) (*Lib, error) { return nil, err } - lib.Macros, err = macros(lib.Templates) if err != nil { return nil, err } @@ -35,31 +32,6 @@ func New(api *confluence.API) (*Lib, error) { 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( - "", - "", - text( - ``, - - // TODO(seletskiy): more macros here - ), - - templates, - ) - if err != nil { - return nil, err - } - - return macros, nil -} - func templates(api *confluence.API) (*template.Template, error) { text := func(line ...string) string { return strings.Join(line, ``) @@ -68,6 +40,9 @@ func templates(api *confluence.API) (*template.Template, error) { templates := template.New(`stdlib`).Funcs( template.FuncMap{ "user": func(name string) *confluence.User { + if api == nil { + return nil + } user, err := api.GetUserByName(name) if err != nil { log.Error(err) diff --git a/testdata/mention-stripnewlines.html b/testdata/mention-stripnewlines.html new file mode 100644 index 0000000..d099d67 --- /dev/null +++ b/testdata/mention-stripnewlines.html @@ -0,0 +1,3 @@ +

Hello username! Unclosed @{mention Not a mention @{} Another one name Multiple one and two In a link mention me In a code @{username} In a block:

+false

Indented:

+false \ No newline at end of file diff --git a/testdata/mention.html b/testdata/mention.html new file mode 100644 index 0000000..a26657e --- /dev/null +++ b/testdata/mention.html @@ -0,0 +1,10 @@ +

Hello username! +Unclosed @{mention +Not a mention @{} +Another one name +Multiple one and two +In a link mention me +In a code @{username} +In a block:

+false

Indented:

+false diff --git a/testdata/mention.md b/testdata/mention.md new file mode 100644 index 0000000..c36f19c --- /dev/null +++ b/testdata/mention.md @@ -0,0 +1,15 @@ +Hello @{username}! +Unclosed @{mention +Not a mention @{} +Another one @{name} +Multiple @{one} and @{two} +In a link [mention @{me}](http://example.com) +In a code `@{username}` +In a block: +``` +@{username} +``` + +Indented: + + @{username} \ No newline at end of file diff --git a/types/types.go b/types/types.go index 177aaaf..832daeb 100644 --- a/types/types.go +++ b/types/types.go @@ -1,9 +1,9 @@ package types type MarkConfig struct { - MermaidScale float64 - D2Scale float64 - DropFirstH1 bool - StripNewlines bool - Features []string + MermaidScale float64 + D2Scale float64 + DropFirstH1 bool + StripNewlines bool + Features []string } diff --git a/util/auth.go b/util/auth.go index 4efdddd..fddaf85 100644 --- a/util/auth.go +++ b/util/auth.go @@ -67,7 +67,7 @@ func GetCredentials( "flag or be stored in configuration file", ) } - + if baseURL == "" { baseURL = url.Scheme + "://" + url.Host } diff --git a/util/cli.go b/util/cli.go index 360b73c..55e1a86 100644 --- a/util/cli.go +++ b/util/cli.go @@ -186,8 +186,6 @@ func processFile( return nil } - macros = append(macros, stdlib.Macros...) - for _, macro := range macros { markdown, err = macro.Apply(markdown) if err != nil { diff --git a/util/flags.go b/util/flags.go index 297a5fa..c10a7cc 100644 --- a/util/flags.go +++ b/util/flags.go @@ -192,8 +192,8 @@ var Flags = []cli.Flag{ &cli.StringSliceFlag{ Name: "features", - Value: []string{"mermaid"}, - Usage: "Enables optional features. Current features: d2, mermaid, mkdocsadmonitions", + Value: []string{"mermaid", "mention"}, + Usage: "Enables optional features. Current features: d2, mermaid, mention, mkdocsadmonitions", Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_FEATURES"), altsrctoml.TOML("features", altsrc.NewStringPtrSourcer(&filename))), }, &cli.BoolFlag{