Move mention macro into a goldmark-parser/renderer

This commit is contained in:
Manuel Rüger 2026-02-08 00:28:36 +01:00
parent b85e40402c
commit e294a7317e
14 changed files with 160 additions and 46 deletions

View File

@ -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

View File

@ -10,7 +10,6 @@ import (
"github.com/urfave/cli/v3"
)
var (
version = "dev"
commit = "none"

View File

@ -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 <ac:*/> tags.

View File

@ -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)

60
parser/mention.go Normal file
View File

@ -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)
}

42
renderer/mention.go Normal file
View File

@ -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
}

View File

@ -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(
`<!-- 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, ``)
@ -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)

3
testdata/mention-stripnewlines.html vendored Normal file
View File

@ -0,0 +1,3 @@
<p>Hello username! Unclosed @{mention Not a mention @{} Another one name Multiple one and two In a link <a href="http://example.com">mention me</a> In a code <code>@{username}</code> In a block:</p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[@{username}]]></ac:plain-text-body></ac:structured-macro><p>Indented:</p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[@{username}]]></ac:plain-text-body></ac:structured-macro>

10
testdata/mention.html vendored Normal file
View File

@ -0,0 +1,10 @@
<p>Hello username!
Unclosed @{mention
Not a mention @{}
Another one name
Multiple one and two
In a link <a href="http://example.com">mention me</a>
In a code <code>@{username}</code>
In a block:</p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[@{username}]]></ac:plain-text-body></ac:structured-macro><p>Indented:</p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[@{username}]]></ac:plain-text-body></ac:structured-macro>

15
testdata/mention.md vendored Normal file
View File

@ -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}

View File

@ -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
}

View File

@ -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 {

View File

@ -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{