Support inline images

This commit is contained in:
Manuel Rüger 2023-04-26 08:02:35 +02:00
parent 2b756daf37
commit fd97ee70f9
9 changed files with 125 additions and 18 deletions

View File

@ -300,8 +300,8 @@ By default, mark provides several built-in templates and macros:
* template: `ac:youtube` to include YouTube Widget. Parameters: * template: `ac:youtube` to include YouTube Widget. Parameters:
- URL: YouTube video endpoint - URL: YouTube video endpoint
- Width: Width in px. Defualts to "640px" - Width: Width in px. Defaults to "640px"
- Height: Height in px. Defualts to "360px" - Height: Height in px. Defaults to "360px"
See: https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube 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>) 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 ### Add width for an image
Use the following macro: 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). 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 ### Render Mermaid Diagram

View File

@ -364,7 +364,7 @@ func processFile(
markdown = mark.DropDocumentLeadingH1(markdown) 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) fmt.Println(html)
os.Exit(0) os.Exit(0)
} }
@ -441,7 +441,7 @@ func processFile(
markdown = mark.DropDocumentLeadingH1(markdown) 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 // Resolve attachements detected from markdown
_, err = mark.ResolveAttachments( _, err = mark.ResolveAttachments(

View File

@ -166,7 +166,8 @@ func SubstituteLinks(markdown []byte, links []LinkSubstitution) []byte {
} }
func parseLinks(markdown string) []markdownLink { func parseLinks(markdown string) []markdownLink {
re := regexp.MustCompile(`\[[^\]]+\]\((([^\)#]+)?#?([^\)]+)?)\)`) // Matches links but not inline images
re := regexp.MustCompile(`[^\!]\[[^\]]+\]\((([^\)#]+)?#?([^\)]+)?)\)`)
matches := re.FindAllStringSubmatch(markdown, -1) matches := re.FindAllStringSubmatch(markdown, -1)
links := make([]markdownLink, len(matches)) links := make([]markdownLink, len(matches))

View File

@ -3,11 +3,13 @@ package mark
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
cparser "github.com/kovetskiy/mark/pkg/mark/parser" cparser "github.com/kovetskiy/mark/pkg/mark/parser"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/pkg/log" "github.com/reconquest/pkg/log"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
@ -49,16 +51,18 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int {
type ConfluenceRenderer struct { type ConfluenceRenderer struct {
html.Config html.Config
Stdlib *stdlib.Lib Stdlib *stdlib.Lib
Path string
MermaidProvider string MermaidProvider string
LevelMap BlockQuoteLevelMap LevelMap BlockQuoteLevelMap
Attachments []Attachment Attachments []Attachment
} }
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer // 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{ return &ConfluenceRenderer{
Config: html.NewConfig(), Config: html.NewConfig(),
Stdlib: stdlib, Stdlib: stdlib,
Path: path,
MermaidProvider: mermaidProvider, MermaidProvider: mermaidProvider,
LevelMap: nil, LevelMap: nil,
Attachments: []Attachment{}, Attachments: []Attachment{},
@ -84,7 +88,7 @@ func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegister
// reg.Register(ast.KindAutoLink, r.renderNode) // reg.Register(ast.KindAutoLink, r.renderNode)
// reg.Register(ast.KindCodeSpan, r.renderNode) // reg.Register(ast.KindCodeSpan, r.renderNode)
// reg.Register(ast.KindEmphasis, 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.KindLink, r.renderLink)
// reg.Register(ast.KindRawHTML, r.renderNode) // reg.Register(ast.KindRawHTML, r.renderNode)
// reg.Register(ast.KindText, r.renderNode) // reg.Register(ast.KindText, r.renderNode)
@ -372,11 +376,13 @@ func (r *ConfluenceRenderer) renderFencedCodeBlock(writer util.BufWriter, source
Width string Width string
Height string Height string
Title string Title string
Alt string
Attachment string Attachment string
}{ }{
attachment.Width, attachment.Width,
attachment.Height, attachment.Height,
attachment.Name, attachment.Name,
"",
attachment.Filename, attachment.Filename,
}, },
) )
@ -463,10 +469,72 @@ func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byt
return ast.WalkContinue, nil 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)) log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
confluenceRenderer := NewConfluenceRenderer(stdlib, mermaidProvider) confluenceRenderer := NewConfluenceRenderer(stdlib, path, mermaidProvider)
converter := goldmark.New( converter := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
@ -530,3 +598,18 @@ func ExtractDocumentLeadingH1(markdown []byte) string {
return string(groups[1]) 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()
}

View File

@ -36,7 +36,7 @@ func TestCompileMarkdown(t *testing.T) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
actual, _ := CompileMarkdown(markdown, lib, "") actual, _ := CompileMarkdown(markdown, lib, filename, "")
test.EqualValues(string(html), actual, filename+" vs "+htmlname) test.EqualValues(string(html), actual, filename+" vs "+htmlname)
} }
} }

View File

@ -201,7 +201,7 @@ func templates(api *confluence.API) (*template.Template, error) {
`{{ if .Page}}`, `{{ if .Page}}`,
/**/ `<ac:parameter ac:name="page">`, /**/ `<ac:parameter ac:name="page">`,
/**/ `<ac:link>`, /**/ `<ac:link>`,
/**/ `<ri:page ri:content-title="{{ .Page}}"/>`, /**/ `<ri:page ri:content-title="{{ .Page }}"/>`,
/**/ `</ac:link>`, /**/ `</ac:link>`,
/**/ `</ac:parameter>`, /**/ `</ac:parameter>`,
`{{printf "\n"}}{{end}}`, `{{printf "\n"}}{{end}}`,
@ -217,10 +217,16 @@ func templates(api *confluence.API) (*template.Template, error) {
`ac:emoticon`: text( `ac:emoticon`: text(
`<ac:emoticon ac:name="{{ .Name }}"/>`, `<ac:emoticon ac:name="{{ .Name }}"/>`,
), ),
`ac:image`: text( `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"}}`, `<ac:image`,
`<ri:attachment ri:filename="{{ .Attachment | convertAttachment }}"/>{{printf "\n"}}`, `{{ if .Width }} ac:width="{{ .Width }}"{{end}}`,
`</ac:image>{{printf "\n"}}`, `{{ 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 */ /* 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:iframe`: text(
`<ac:structured-macro ac:name="iframe">{{printf "\n"}}`, `<ac:structured-macro ac:name="iframe">{{printf "\n"}}`,
`<ac:parameter ac:name="src"><ri:url ri:value="{{ .URL }}" /></ac:parameter>{{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 .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 .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 .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="width">{{ or .Width "640px" }}</ac:parameter>{{printf "\n"}}`,
`<ac:parameter ac:name="height">{{ or .Height "360px" }}</ac:parameter>{{printf "\n"}}`, `<ac:parameter ac:name="height">{{ or .Height "360px" }}</ac:parameter>{{printf "\n"}}`,
`</ac:structured-macro>{{printf "\n"}}`, `</ac:structured-macro>{{printf "\n"}}`,

View File

@ -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="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="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>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> <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"> <div class="footnotes" role="doc-endnotes">
<hr /> <hr />

View File

@ -10,5 +10,10 @@ Use [Another Page](ac:)
Use [page link with spaces](<ac:Page With Space>) 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] Use footnotes link [^1]
[^1]: a footnote link [^1]: a footnote link

BIN
pkg/mark/testdata/test.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB