Repository: kovetskiy/mark
Pull request #296 (2023-04-27 20:55:29)

Support inline images
This will allow automated inclusion of inline images
```markdown
![Example](../images/examples.png)
```
and uploading them as an attachment




Patch:
diff --git a/README.md b/README.md
index 3a57834..531a0ab 100644
--- a/README.md
+++ b/README.md
@@ -300,8 +300,8 @@ By default, mark provides several built-in templates and macros:
 
 * template: `ac:youtube` to include YouTube Widget. Parameters:
   - URL: YouTube video endpoint
-  - Width: Width in px. Defualts to "640px"
-  - Height: Height in px. Defualts to "360px"
+  - Width: Width in px. Defaults to "640px"
+  - Height: Height in px. Defaults to "360px"
 
   See: https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube
 
@@ -537,6 +537,15 @@ And this is how to link when the linktext is the same as the [Pagetitle](ac:)
 Link to a [page title with space](<ac:With Space>)
 ```
 
+### Upload and included inline images
+
+```markdown
+![Example](../images/examples.png)
+```
+will automatically upload the inlined image as an attachment and inline the image using the `ac:image` template.
+
+If the file is not found, it will inline the image using the `ac:image` template and link to the image.
+
 ### Add width for an image
 
 Use the following macro:
@@ -552,6 +561,7 @@ And attach any image with the following
 ```
 The width will be the commented html after the image (in this case 300px).
 
+Currently this is not compatible with the automated upload of inline images.
 
 ### Render Mermaid Diagram
 
diff --git a/main.go b/main.go
index 4a9b525..20a45fc 100644
--- a/main.go
+++ b/main.go
@@ -364,7 +364,7 @@ func processFile(
 			markdown = mark.DropDocumentLeadingH1(markdown)
 		}
 
-		html, _ := mark.CompileMarkdown(markdown, stdlib, cCtx.String("mermaid-provider"))
+		html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"))
 		fmt.Println(html)
 		os.Exit(0)
 	}
@@ -441,7 +441,7 @@ func processFile(
 		markdown = mark.DropDocumentLeadingH1(markdown)
 	}
 
-	html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, cCtx.String("mermaid-provider"))
+	html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"))
 
 	// Resolve attachements detected from markdown
 	_, err = mark.ResolveAttachments(
diff --git a/pkg/mark/link.go b/pkg/mark/link.go
index 17b654a..0215a8b 100644
--- a/pkg/mark/link.go
+++ b/pkg/mark/link.go
@@ -166,7 +166,8 @@ func SubstituteLinks(markdown []byte, links []LinkSubstitution) []byte {
 }
 
 func parseLinks(markdown string) []markdownLink {
-	re := regexp.MustCompile(`\[[^\]]+\]\((([^\)#]+)?#?([^\)]+)?)\)`)
+	// Matches links but not inline images
+	re := regexp.MustCompile(`[^\!]\[[^\]]+\]\((([^\)#]+)?#?([^\)]+)?)\)`)
 	matches := re.FindAllStringSubmatch(markdown, -1)
 
 	links := make([]markdownLink, len(matches))
diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go
index 9325e5e..3e84e41 100644
--- a/pkg/mark/markdown.go
+++ b/pkg/mark/markdown.go
@@ -3,11 +3,13 @@ package mark
 import (
 	"bytes"
 	"fmt"
+	"path/filepath"
 	"regexp"
 	"strings"
 
 	cparser "github.com/kovetskiy/mark/pkg/mark/parser"
 	"github.com/kovetskiy/mark/pkg/mark/stdlib"
+	"github.com/kovetskiy/mark/pkg/mark/vfs"
 	"github.com/reconquest/pkg/log"
 	"github.com/yuin/goldmark"
 
@@ -49,16 +51,18 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int {
 type ConfluenceRenderer struct {
 	html.Config
 	Stdlib          *stdlib.Lib
+	Path            string
 	MermaidProvider string
 	LevelMap        BlockQuoteLevelMap
 	Attachments     []Attachment
 }
 
 // NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
-func NewConfluenceRenderer(stdlib *stdlib.Lib, mermaidProvider string, opts ...html.Option) renderer.NodeRenderer {
+func NewConfluenceRenderer(stdlib *stdlib.Lib, path string, mermaidProvider string, opts ...html.Option) renderer.NodeRenderer {
 	return &ConfluenceRenderer{
 		Config:          html.NewConfig(),
 		Stdlib:          stdlib,
+		Path:            path,
 		MermaidProvider: mermaidProvider,
 		LevelMap:        nil,
 		Attachments:     []Attachment{},
@@ -84,7 +88,7 @@ func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegister
 	// reg.Register(ast.KindAutoLink, r.renderNode)
 	// reg.Register(ast.KindCodeSpan, r.renderNode)
 	// reg.Register(ast.KindEmphasis, r.renderNode)
-	// reg.Register(ast.KindImage, r.renderNode)
+	reg.Register(ast.KindImage, r.renderImage)
 	reg.Register(ast.KindLink, r.renderLink)
 	// reg.Register(ast.KindRawHTML, r.renderNode)
 	// reg.Register(ast.KindText, r.renderNode)
@@ -372,11 +376,13 @@ func (r *ConfluenceRenderer) renderFencedCodeBlock(writer util.BufWriter, source
 				Width      string
 				Height     string
 				Title      string
+				Alt        string
 				Attachment string
 			}{
 				attachment.Width,
 				attachment.Height,
 				attachment.Name,
+				"",
 				attachment.Filename,
 			},
 		)
@@ -463,10 +469,72 @@ func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byt
 	return ast.WalkContinue, nil
 }
 
-func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, mermaidProvider string) (string, []Attachment) {
+// renderImage renders an inline image
+func (r *ConfluenceRenderer) renderImage(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+	n := node.(*ast.Image)
+
+	attachments, err := ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(r.Path), []string{string(n.Destination)})
+
+	// We were unable to resolve it locally, treat as URL
+	if err != nil {
+		err = r.Stdlib.Templates.ExecuteTemplate(
+			writer,
+			"ac:image",
+			struct {
+				Width      string
+				Height     string
+				Title      string
+				Alt        string
+				Attachment string
+				Url        string
+			}{
+				"",
+				"",
+				string(n.Title),
+				string(nodeToHTMLText(n, source)),
+				"",
+				string(n.Destination),
+			},
+		)
+	} else {
+
+		r.Attachments = append(r.Attachments, attachments[0])
+
+		err = r.Stdlib.Templates.ExecuteTemplate(
+			writer,
+			"ac:image",
+			struct {
+				Width      string
+				Height     string
+				Title      string
+				Alt        string
+				Attachment string
+				Url        string
+			}{
+				"",
+				"",
+				string(n.Title),
+				string(nodeToHTMLText(n, source)),
+				attachments[0].Filename,
+				"",
+			},
+		)
+	}
+
+	if err != nil {
+		return ast.WalkStop, err
+	}
+
+	return ast.WalkSkipChildren, nil
+}
+
+func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string) (string, []Attachment) {
 	log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
 
-	confluenceRenderer := NewConfluenceRenderer(stdlib, mermaidProvider)
+	confluenceRenderer := NewConfluenceRenderer(stdlib, path, mermaidProvider)
 
 	converter := goldmark.New(
 		goldmark.WithExtensions(
@@ -530,3 +598,18 @@ func ExtractDocumentLeadingH1(markdown []byte) string {
 		return string(groups[1])
 	}
 }
+
+// https://github.com/yuin/goldmark/blob/c446c414ef3a41fb562da0ae5badd18f1502c42f/renderer/html/html.go
+func nodeToHTMLText(n ast.Node, source []byte) []byte {
+	var buf bytes.Buffer
+	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+		if s, ok := c.(*ast.String); ok && s.IsCode() {
+			buf.Write(s.Text(source))
+		} else if !c.HasChildren() {
+			buf.Write(util.EscapeHTML(c.Text(source)))
+		} else {
+			buf.Write(nodeToHTMLText(c, source))
+		}
+	}
+	return buf.Bytes()
+}
diff --git a/pkg/mark/stdlib/stdlib.go b/pkg/mark/stdlib/stdlib.go
index bf294b8..427f251 100644
--- a/pkg/mark/stdlib/stdlib.go
+++ b/pkg/mark/stdlib/stdlib.go
@@ -201,7 +201,7 @@ func templates(api *confluence.API) (*template.Template, error) {
 			`{{ if .Page}}`,
 			/**/ `<ac:parameter ac:name="page">`,
 			/**/ `<ac:link>`,
-			/**/ `<ri:page ri:content-title="{{ .Page}}"/>`,
+			/**/ `<ri:page ri:content-title="{{ .Page }}"/>`,
 			/**/ `</ac:link>`,
 			/**/ `</ac:parameter>`,
 			`{{printf "\n"}}{{end}}`,
@@ -217,10 +217,16 @@ func templates(api *confluence.API) (*template.Template, error) {
 		`ac:emoticon`: text(
 			`<ac:emoticon ac:name="{{ .Name }}"/>`,
 		),
+
 		`ac:image`: text(
-			`<ac:image{{ if .Width}} ac:width="{{ .Width }}"{{end}}{{ if .Height }} ac:height="{{ .Height }}"{{end}}{{ if .Title }} ac:title="{{ .Title }}"{{end}}>{{printf "\n"}}`,
-			`<ri:attachment ri:filename="{{ .Attachment | convertAttachment }}"/>{{printf "\n"}}`,
-			`</ac:image>{{printf "\n"}}`,
+			`<ac:image`,
+			`{{ if .Width }} ac:width="{{ .Width }}"{{end}}`,
+			`{{ if .Height }} ac:height="{{ .Height }}"{{end}}`,
+			`{{ if .Title }} ac:title="{{ .Title }}"{{end}}`,
+			`{{ if .Alt }} ac:alt="{{ .Alt }}"{{end}}>`,
+			`{{ if .Attachment }}<ri:attachment ri:filename="{{ .Attachment | convertAttachment }}"/>{{end}}`,
+			`{{ if .Url }}<ri:url ri:value="{{ .Url }}"/>{{end}}`,
+			`</ac:image>`,
 		),
 
 		/* https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube */
@@ -240,9 +246,9 @@ func templates(api *confluence.API) (*template.Template, error) {
 		`ac:iframe`: text(
 			`<ac:structured-macro ac:name="iframe">{{printf "\n"}}`,
 			`<ac:parameter ac:name="src"><ri:url ri:value="{{ .URL }}" /></ac:parameter>{{printf "\n"}}`,
-			`{{ if .Frameborder}}<ac:parameter ac:name="frameborder">{{ .Frameborder }}</ac:parameter>{{printf "\n"}}{{end}}`,
-			`{{ if .Scrolling}}<ac:parameter ac:name="id">{{ .Scrolling }}</ac:parameter>{{printf "\n"}}{{end}}`,
-			`{{ if .Align}}<ac:parameter ac:name="align">{{ .Align }}</ac:parameter>{{printf "\n"}}{{end}}`,
+			`{{ if .Frameborder }}<ac:parameter ac:name="frameborder">{{ .Frameborder }}</ac:parameter>{{printf "\n"}}{{end}}`,
+			`{{ if .Scrolling }}<ac:parameter ac:name="id">{{ .Scrolling }}</ac:parameter>{{printf "\n"}}{{end}}`,
+			`{{ if .Align }}<ac:parameter ac:name="align">{{ .Align }}</ac:parameter>{{printf "\n"}}{{end}}`,
 			`<ac:parameter ac:name="width">{{ or .Width "640px" }}</ac:parameter>{{printf "\n"}}`,
 			`<ac:parameter ac:name="height">{{ or .Height "360px" }}</ac:parameter>{{printf "\n"}}`,
 			`</ac:structured-macro>{{printf "\n"}}`,


Test patch:
diff --git a/pkg/mark/markdown_test.go b/pkg/mark/markdown_test.go
index a4c5053..269d27f 100644
--- a/pkg/mark/markdown_test.go
+++ b/pkg/mark/markdown_test.go
@@ -36,7 +36,7 @@ func TestCompileMarkdown(t *testing.T) {
 		if err != nil {
 			panic(err)
 		}
-		actual, _ := CompileMarkdown(markdown, lib, "")
+		actual, _ := CompileMarkdown(markdown, lib, filename, "")
 		test.EqualValues(string(html), actual, filename+" vs "+htmlname)
 	}
 }
diff --git a/pkg/mark/testdata/links.html b/pkg/mark/testdata/links.html
index 625f00e..743676d 100644
--- a/pkg/mark/testdata/links.html
+++ b/pkg/mark/testdata/links.html
@@ -4,6 +4,8 @@
 <p>Use <ac:link><ri:page ri:content-title="AnotherPage"/><ac:plain-text-link-body><![CDATA[AnotherPage]]></ac:plain-text-link-body></ac:link></p>
 <p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
 <p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
+<p><ac:image ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
+<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png"/></ac:image></p>
 <p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
 <div class="footnotes" role="doc-endnotes">
 <hr />
diff --git a/pkg/mark/testdata/links.md b/pkg/mark/testdata/links.md
index ce4a282..bee9e3e 100644
--- a/pkg/mark/testdata/links.md
+++ b/pkg/mark/testdata/links.md
@@ -10,5 +10,10 @@ Use [Another Page](ac:)
 
 Use [page link with spaces](<ac:Page With Space>)
 
+![My Image](test.png)
+
+![My External Image](http://confluence.atlassian.com/images/logo/confluence_48_trans.png)
+
 Use footnotes link [^1]  
-[^1]: a footnote link
\ No newline at end of file
+[^1]: a footnote link
+