diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7ac080..af61d3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: - master env: - GO_VERSION: "~1.20.2" + GO_VERSION: "~1.20.3" jobs: # Runs Golangci-lint on the source code diff --git a/Dockerfile b/Dockerfile index 0184ce5..b8cea44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,15 @@ -FROM golang:1.20.2 as builder +FROM golang:1.20.3 as builder ENV GOPATH="/go" WORKDIR /go/src/github.com/kovetskiy/mark COPY / . RUN make get \ && make build -FROM alpine:3.17 -RUN apk --no-cache add ca-certificates bash sed git +FROM chromedp/headless-shell:latest +RUN apt-get update \ +&& apt-get install --no-install-recommends -qq ca-certificates bash sed git \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + COPY --from=builder /go/src/github.com/kovetskiy/mark/mark /bin/ WORKDIR /docs diff --git a/README.md b/README.md index 486a87d..3a57834 100644 --- a/README.md +++ b/README.md @@ -553,6 +553,21 @@ And attach any image with the following The width will be the commented html after the image (in this case 300px). +### Render Mermaid Diagram + +Confluence doesn't provide [mermaid.js](https://github.com/mermaid-js/mermaid) support natively. Mark provides a convenient way to enable the feature like [Github does](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/). +As long as you have a code block and are marked as "mermaid", the mark will automatically render it as a PNG image and insert into before the code block. + + ```mermaid title diagrams_example + graph TD; + A-->B; + ``` + +In order to properly render mermaid, you can choose between the following mermaid providers: + +* "mermaid-go" via [mermaid.go](https://github.com/dreampuf/mermaid.go) +* "cloudscript" via [cloudscript-io-mermaid-addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon) + ## Installation ### Homebrew diff --git a/go.mod b/go.mod index b33191a..a6701e8 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,37 @@ module github.com/kovetskiy/mark go 1.19 require ( + github.com/dreampuf/mermaid.go v0.0.4-0.20220314184516-44d4f4fc9d39 github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69 github.com/kovetskiy/lorg v1.2.0 - github.com/reconquest/karma-go v0.0.0-20220904173930-21741aa386a6 + github.com/reconquest/karma-go v0.0.0-20230425053540-765a8ab89f64 github.com/reconquest/pkg v1.3.0 github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 github.com/stretchr/testify v1.8.2 github.com/urfave/cli/v2 v2.25.1 github.com/yuin/goldmark v1.5.4 - golang.org/x/tools v0.7.0 + golang.org/x/tools v0.8.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect + github.com/chromedp/cdproto v0.0.0-20230329100754-6125fc8d7142 // indirect + github.com/chromedp/chromedp v0.9.1 // indirect + github.com/chromedp/sysutil v1.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.1.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3 // indirect + golang.org/x/sys v0.7.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index d73146b..51c9764 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,58 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20230329100754-6125fc8d7142 h1:7H1PudqT2SgX/U2ZwPOBOvj75Jnoks+eYVvEEFpi7h0= +github.com/chromedp/cdproto v0.0.0-20230329100754-6125fc8d7142/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA= +github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dreampuf/mermaid.go v0.0.4-0.20220314184516-44d4f4fc9d39 h1:oeOSCOh+XHblSoj/FsR0X/BOT6f3oj0OE58Y2tPoISM= +github.com/dreampuf/mermaid.go v0.0.4-0.20220314184516-44d4f4fc9d39/go.mod h1:e9BYes2y5OhMb70VsPioP2vksPofJQ40ywhFwtmQ45w= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= +github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69 h1:vn82v0gKhTTm67znr7nxYBNW4mJ8zfY7dywZivUy3tY= github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM= github.com/kovetskiy/lorg v0.0.0-20200107130803-9a7136a95634/go.mod h1:B8HeKAukXULNzWWsW5k/SQyDkiQZPn7lTBJDB46MZ9I= github.com/kovetskiy/lorg v1.2.0 h1:wNIUT/VOhcjKOmizDClZLvchbKFGW+dzf9fQXbSVS5E= github.com/kovetskiy/lorg v1.2.0/go.mod h1:rdiamaIRUCkX9HtFZd0D9dQqUbad21hipHk+sat7Z6s= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65 h1:IiAAeijD0sU3C6OO9vy/0WUUfRabZ1aH7hijCBteJC4= github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65/go.mod h1:iin2k2yhKESAy14B2fXK8gpf1nofl7dTXH5U+VdIlss= github.com/reconquest/karma-go v0.0.0-20200928103525-22da92476de6/go.mod h1:yuQiKpTdmXSX7E+h+3dD4jx09P/gHc67mRxN3eFLt7o= -github.com/reconquest/karma-go v0.0.0-20220904173930-21741aa386a6 h1:wSI9nn6ZDtuA4Coi6oWmhBqSHGLUK2XRhn9x/4QCCMY= -github.com/reconquest/karma-go v0.0.0-20220904173930-21741aa386a6/go.mod h1:qMQ8twYxBpCJ4IYrAnMDBtdNMj0ZTH+kcu94ZIY6vDU= +github.com/reconquest/karma-go v0.0.0-20230425053540-765a8ab89f64 h1:1ftdBojSr4bnBpe3UkSGG40fI70g5Av3xLQKn9Gkmx4= +github.com/reconquest/karma-go v0.0.0-20230425053540-765a8ab89f64/go.mod h1:52XRXXa2ec/VNrlCirwasdJfNmjI1O87q098gmqILh0= github.com/reconquest/pkg v1.3.0 h1:Yuoxiw92rP/srKXMo5qSML2InhJ+xAqHJIx3/y/2zh8= github.com/reconquest/pkg v1.3.0/go.mod h1:hUQ0SzzBlFRSbo6lFYG2tSpLMjqOuUqm2LtpjR/+1sg= github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 h1:bcDXaTFC09IIg13Z8gfQHk4gSu001ET7ssW/wKRvPzg= github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -45,8 +71,12 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3 h1:BhVaeQJc3xalHGONn215FylzuxdQBIT3d/aRjDg4nXQ= github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 7ac84aa..3e93fcc 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/kovetskiy/mark/pkg/mark/includes" "github.com/kovetskiy/mark/pkg/mark/macro" "github.com/kovetskiy/mark/pkg/mark/stdlib" + "github.com/kovetskiy/mark/pkg/mark/vfs" "github.com/reconquest/karma-go" "github.com/reconquest/pkg/log" "github.com/urfave/cli/v2" @@ -22,8 +23,7 @@ import ( const ( version = "9.1.4" usage = "A tool for updating Atlassian Confluence pages from markdown." - description = `Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark -` + description = `Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark` ) var flags = []cli.Flag{ @@ -139,6 +139,12 @@ var flags = []cli.Flag{ Usage: "use specified space key. If the space key is not specified, it must be set in the page metadata.", EnvVars: []string{"MARK_SPACE"}, }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "mermaid-provider", + Value: "cloudscript", + Usage: "defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go.", + EnvVars: []string{"MARK_MERMAID_PROVIDER"}, + }), } func main() { @@ -347,7 +353,8 @@ func processFile( markdown = mark.DropDocumentLeadingH1(markdown) } - fmt.Println(mark.CompileMarkdown(markdown, stdlib)) + html, _ := mark.CompileMarkdown(markdown, stdlib, cCtx.String("mermaid-provider")) + fmt.Println(html) os.Exit(0) } @@ -399,11 +406,16 @@ func processFile( target = page } + // Resolve attachments created from directive + localAttachments, err := mark.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments) + if err != nil { + log.Fatalf(err, "unable to locate attachments") + } + attaches, err := mark.ResolveAttachments( api, target, - filepath.Dir(file), - meta.Attachments, + localAttachments, ) if err != nil { log.Fatalf(err, "unable to create/update attachments") @@ -418,7 +430,17 @@ func processFile( markdown = mark.DropDocumentLeadingH1(markdown) } - html := mark.CompileMarkdown(markdown, stdlib) + html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, cCtx.String("mermaid-provider")) + + // Resolve attachements detected from markdown + _, err = mark.ResolveAttachments( + api, + target, + inlineAttachments, + ) + if err != nil { + log.Fatalf(err, "unable to create/update attachments") + } { var buffer bytes.Buffer diff --git a/pkg/confluence/api.go b/pkg/confluence/api.go index d4d7be4..5c2e5a4 100644 --- a/pkg/confluence/api.go +++ b/pkg/confluence/api.go @@ -8,7 +8,6 @@ import ( "io" "mime/multipart" "net/http" - "os" "strings" "github.com/kovetskiy/gopencils" @@ -52,7 +51,7 @@ type PageInfo struct { } `json:"version"` Ancestors []struct { - Id string `json:"id"` + ID string `json:"id"` Title string `json:"title"` } `json:"ancestors"` @@ -141,7 +140,7 @@ func (api *API) FindRootPage(space string) (*PageInfo, error) { } return &PageInfo{ - ID: page.Ancestors[0].Id, + ID: page.Ancestors[0].ID, Title: page.Ancestors[0].Title, }, nil } @@ -158,7 +157,7 @@ func (api *API) FindHomePage(space string) (*PageInfo, error) { return nil, err } - if request.Raw.StatusCode == 404 || request.Raw.StatusCode != 200 { + if request.Raw.StatusCode == http.StatusNotFound || request.Raw.StatusCode != http.StatusOK { return nil, newErrorStatusNotOK(request) } @@ -193,7 +192,7 @@ func (api *API) FindPage( // allow 404 because it's fine if page is not found, // the function will return nil, nil - if request.Raw.StatusCode != 404 && request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusNotFound && request.Raw.StatusCode != http.StatusOK { return nil, newErrorStatusNotOK(request) } @@ -208,11 +207,11 @@ func (api *API) CreateAttachment( pageID string, name string, comment string, - path string, + reader io.Reader, ) (AttachmentInfo, error) { var info AttachmentInfo - form, err := getAttachmentPayload(name, comment, path) + form, err := getAttachmentPayload(name, comment, reader) if err != nil { return AttachmentInfo{}, err } @@ -243,7 +242,7 @@ func (api *API) CreateAttachment( return info, err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return info, newErrorStatusNotOK(request) } @@ -276,11 +275,11 @@ func (api *API) UpdateAttachment( attachID string, name string, comment string, - path string, + reader io.Reader, ) (AttachmentInfo, error) { var info AttachmentInfo - form, err := getAttachmentPayload(name, comment, path) + form, err := getAttachmentPayload(name, comment, reader) if err != nil { return AttachmentInfo{}, err } @@ -313,7 +312,7 @@ func (api *API) UpdateAttachment( return info, err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return info, newErrorStatusNotOK(request) } @@ -353,23 +352,12 @@ func (api *API) UpdateAttachment( return shortResponse, nil } -func getAttachmentPayload(name, comment, path string) (*form, error) { +func getAttachmentPayload(name, comment string, reader io.Reader) (*form, error) { var ( payload = bytes.NewBuffer(nil) writer = multipart.NewWriter(payload) ) - file, err := os.Open(path) - if err != nil { - return nil, karma.Format( - err, - "unable to open file: %q", - path, - ) - } - - defer file.Close() - content, err := writer.CreateFormFile("file", name) if err != nil { return nil, karma.Format( @@ -378,7 +366,7 @@ func getAttachmentPayload(name, comment, path string) (*form, error) { ) } - _, err = io.Copy(content, file) + _, err = io.Copy(content, reader) if err != nil { return nil, karma.Format( err, @@ -436,7 +424,7 @@ func (api *API) GetAttachments(pageID string) ([]AttachmentInfo, error) { return nil, err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return nil, newErrorStatusNotOK(request) } @@ -459,7 +447,7 @@ func (api *API) GetPageByID(pageID string) (*PageInfo, error) { return nil, err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return nil, newErrorStatusNotOK(request) } @@ -507,7 +495,7 @@ func (api *API) CreatePage( return nil, err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return nil, newErrorStatusNotOK(request) } @@ -521,7 +509,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne if page.Type != "blogpost" && len(page.Ancestors) > 0 { // picking only the last one, which is required by confluence oldAncestors = []map[string]interface{}{ - {"id": page.Ancestors[len(page.Ancestors)-1].Id}, + {"id": page.Ancestors[len(page.Ancestors)-1].ID}, } } @@ -547,7 +535,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne "ancestors": oldAncestors, "body": map[string]interface{}{ "storage": map[string]interface{}{ - "value": string(newContent), + "value": newContent, "representation": "storage", }, }, @@ -573,7 +561,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne return err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return newErrorStatusNotOK(request) } @@ -654,7 +642,7 @@ func (api *API) RestrictPageUpdatesCloud( return err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return newErrorStatusNotOK(request) } @@ -685,7 +673,7 @@ func (api *API) RestrictPageUpdatesServer( return err } - if request.Raw.StatusCode != 200 { + if request.Raw.StatusCode != http.StatusOK { return newErrorStatusNotOK(request) } @@ -715,13 +703,13 @@ func (api *API) RestrictPageUpdates( } func newErrorStatusNotOK(request *gopencils.Resource) error { - if request.Raw.StatusCode == 401 { + if request.Raw.StatusCode == http.StatusUnauthorized { return errors.New( "Confluence API returned unexpected status: 401 (Unauthorized)", ) } - if request.Raw.StatusCode == 404 { + if request.Raw.StatusCode == http.StatusNotFound { return errors.New( "Confluence API returned unexpected status: 404 (Not Found)", ) diff --git a/pkg/mark/attachment.go b/pkg/mark/attachment.go index ef670d2..8f4ad7e 100644 --- a/pkg/mark/attachment.go +++ b/pkg/mark/attachment.go @@ -6,13 +6,13 @@ import ( "encoding/hex" "io" "net/url" - "os" "path" "path/filepath" "sort" "strings" "github.com/kovetskiy/mark/pkg/confluence" + "github.com/kovetskiy/mark/pkg/mark/vfs" "github.com/reconquest/karma-go" "github.com/reconquest/pkg/log" ) @@ -22,36 +22,32 @@ const ( ) type Attachment struct { - ID string - Name string - Filename string - Path string - Checksum string - Link string - Replace string + ID string + Name string + Filename string + FileBytes []byte + Checksum string + Link string + Width string + Height string + Replace string } func ResolveAttachments( api *confluence.API, page *confluence.PageInfo, - base string, - replacements []string, + attachments []Attachment, ) ([]Attachment, error) { - attaches, err := prepareAttachments(base, replacements) - if err != nil { - return nil, err - } - - for i := range attaches { - checksum, err := getChecksum(attaches[i].Path) + for i := range attachments { + checksum, err := GetChecksum(bytes.NewReader(attachments[i].FileBytes)) if err != nil { return nil, karma.Format( err, - "unable to get checksum for attachment: %q", attaches[i].Name, + "unable to get checksum for attachment: %q", attachments[i].Name, ) } - attaches[i].Checksum = checksum + attachments[i].Checksum = checksum } remotes, err := api.GetAttachments(page.ID) @@ -62,18 +58,18 @@ func ResolveAttachments( existing := []Attachment{} creating := []Attachment{} updating := []Attachment{} - for _, attach := range attaches { + for _, attachment := range attachments { var found bool var same bool for _, remote := range remotes { - if remote.Filename == attach.Filename { - same = attach.Checksum == strings.TrimPrefix( + if remote.Filename == attachment.Filename { + same = attachment.Checksum == strings.TrimPrefix( remote.Metadata.Comment, AttachmentChecksumPrefix, ) - attach.ID = remote.ID - attach.Link = path.Join( + attachment.ID = remote.ID + attachment.Link = path.Join( remote.Links.Context, remote.Links.Download, ) @@ -86,109 +82,143 @@ func ResolveAttachments( if found { if same { - existing = append(existing, attach) + existing = append(existing, attachment) } else { - updating = append(updating, attach) + updating = append(updating, attachment) } } else { - creating = append(creating, attach) + creating = append(creating, attachment) } } - for i, attach := range creating { - log.Infof(nil, "creating attachment: %q", attach.Name) + for i, attachment := range creating { + log.Infof(nil, "creating attachment: %q", attachment.Name) info, err := api.CreateAttachment( page.ID, - attach.Filename, - AttachmentChecksumPrefix+attach.Checksum, - attach.Path, + attachment.Filename, + AttachmentChecksumPrefix+attachment.Checksum, + bytes.NewReader(attachment.FileBytes), ) if err != nil { return nil, karma.Format( err, "unable to create attachment %q", - attach.Name, + attachment.Name, ) } - attach.ID = info.ID - attach.Link = path.Join( + attachment.ID = info.ID + attachment.Link = path.Join( info.Links.Context, info.Links.Download, ) - creating[i] = attach + creating[i] = attachment } - for i, attach := range updating { - log.Infof(nil, "updating attachment: %q", attach.Name) + for i, attachment := range updating { + log.Infof(nil, "updating attachment: %q", attachment.Name) info, err := api.UpdateAttachment( page.ID, - attach.ID, - attach.Filename, - AttachmentChecksumPrefix+attach.Checksum, - attach.Path, + attachment.ID, + attachment.Filename, + AttachmentChecksumPrefix+attachment.Checksum, + bytes.NewReader(attachment.FileBytes), ) if err != nil { return nil, karma.Format( err, "unable to update attachment %q", - attach.Name, + attachment.Name, ) } - attach.Link = path.Join( + attachment.Link = path.Join( info.Links.Context, info.Links.Download, ) - updating[i] = attach + updating[i] = attachment } for i := range existing { - log.Infof(nil, "keeping unmodified attachment: %q", attaches[i].Name) + log.Infof(nil, "keeping unmodified attachment: %q", attachments[i].Name) } - attaches = []Attachment{} - attaches = append(attaches, existing...) - attaches = append(attaches, creating...) - attaches = append(attaches, updating...) + attachments = []Attachment{} + attachments = append(attachments, existing...) + attachments = append(attachments, creating...) + attachments = append(attachments, updating...) - return attaches, nil + return attachments, nil } -func prepareAttachments(base string, replacements []string) ([]Attachment, error) { - attaches := []Attachment{} - for _, name := range replacements { - attach := Attachment{ - Name: name, - Filename: strings.ReplaceAll(name, "/", "_"), - Path: filepath.Join(base, name), - Replace: name, +func ResolveLocalAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) { + attachments, err := prepareAttachments(opener, base, replacements) + if err != nil { + return nil, err + } + + for _, attachment := range attachments { + checksum, err := GetChecksum(bytes.NewReader(attachment.FileBytes)) + if err != nil { + return nil, karma.Format( + err, + "unable to get checksum for attachment: %q", attachment.Name, + ) } - attaches = append(attaches, attach) + attachment.Checksum = checksum } - - return attaches, nil + return attachments, err } -func CompileAttachmentLinks(markdown []byte, attaches []Attachment) []byte { +// prepareAttachements creates an array of attachement objects based on an array of filepaths +func prepareAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) { + attachments := []Attachment{} + for _, name := range replacements { + attachment, err := prepareAttachment(opener, base, name) + if err != nil { + return nil, err + } + + attachments = append(attachments, attachment) + } + + return attachments, nil +} + +// prepareAttachement opens the file, reads its content and creates an attachement object +func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error) { + attachmentPath := filepath.Join(base, name) + file, err := opener.Open(attachmentPath) + if err != nil { + return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath) + } + defer file.Close() + + fileBytes, err := io.ReadAll(file) + if err != nil { + return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath) + } + + return Attachment{ + Name: name, + Filename: strings.ReplaceAll(name, "/", "_"), + FileBytes: fileBytes, + Replace: name, + }, nil +} + +func CompileAttachmentLinks(markdown []byte, attachments []Attachment) []byte { links := map[string]string{} replaces := []string{} - for _, attach := range attaches { - uri, err := url.ParseRequestURI(attach.Link) - if err != nil { - links[attach.Replace] = strings.ReplaceAll("&", "&", attach.Link) - } else { - links[attach.Replace] = uri.Path + - "?" + url.QueryEscape(uri.Query().Encode()) - } - - replaces = append(replaces, attach.Replace) + for _, attachment := range attachments { + links[attachment.Replace] = parseAttachmentLink(attachment.Link) + replaces = append(replaces, attachment.Replace) } // sort by length so first items will have bigger length @@ -240,20 +270,21 @@ func CompileAttachmentLinks(markdown []byte, attaches []Attachment) []byte { return markdown } -func getChecksum(filename string) (string, error) { - file, err := os.Open(filename) - if err != nil { - return "", karma.Format( - err, - "unable to open file", - ) - } - defer file.Close() - +func GetChecksum(reader io.Reader) (string, error) { hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { + if _, err := io.Copy(hash, reader); err != nil { return "", err } return hex.EncodeToString(hash.Sum(nil)), nil } + +func parseAttachmentLink(attachLink string) string { + uri, err := url.ParseRequestURI(attachLink) + if err != nil { + return strings.ReplaceAll("&", "&", attachLink) + } else { + return uri.Path + + "?" + url.QueryEscape(uri.Query().Encode()) + } +} diff --git a/pkg/mark/attachment_test.go b/pkg/mark/attachment_test.go index 8fe39fd..714a6a4 100644 --- a/pkg/mark/attachment_test.go +++ b/pkg/mark/attachment_test.go @@ -1,6 +1,9 @@ package mark import ( + "bytes" + "io" + "os" "testing" "github.com/stretchr/testify/assert" @@ -14,43 +17,74 @@ var ( } ) +type bufferCloser struct { + *bytes.Buffer +} + +func (bufferCloser) Close() error { return nil } + +type virtualOpener struct { + PathToBuf map[string]*bufferCloser +} + +func (o *virtualOpener) Open(name string) (io.ReadWriteCloser, error) { + if buf, ok := o.PathToBuf[name]; ok { + return buf, nil + } + return nil, os.ErrNotExist +} + func TestPrepareAttachmentsWithWorkDirBase(t *testing.T) { - attaches, err := prepareAttachments(".", replacements) + testingOpener := &virtualOpener{ + PathToBuf: map[string]*bufferCloser{ + "image1.jpg": {bytes.NewBuffer(nil)}, + "images/image2.jpg": {bytes.NewBuffer(nil)}, + "../image3.jpg": {bytes.NewBuffer(nil)}, + }, + } + + attaches, err := prepareAttachments(testingOpener, ".", replacements) + t.Logf("attatches: %s", err) if err != nil { println(err.Error()) + t.Fatal(err) } assert.Equal(t, "image1.jpg", attaches[0].Name) assert.Equal(t, "image1.jpg", attaches[0].Replace) - assert.Equal(t, "image1.jpg", attaches[0].Path) assert.Equal(t, "images/image2.jpg", attaches[1].Name) assert.Equal(t, "images/image2.jpg", attaches[1].Replace) - assert.Equal(t, "images/image2.jpg", attaches[1].Path) assert.Equal(t, "../image3.jpg", attaches[2].Name) assert.Equal(t, "../image3.jpg", attaches[2].Replace) - assert.Equal(t, "../image3.jpg", attaches[2].Path) assert.Equal(t, len(attaches), 3) } func TestPrepareAttachmentsWithSubDirBase(t *testing.T) { - attaches, _ := prepareAttachments("a/b", replacements) + testingOpener := &virtualOpener{ + PathToBuf: map[string]*bufferCloser{ + "a/b/image1.jpg": {bytes.NewBuffer(nil)}, + "a/b/images/image2.jpg": {bytes.NewBuffer(nil)}, + "a/image3.jpg": {bytes.NewBuffer(nil)}, + }, + } + attaches, err := prepareAttachments(testingOpener, "a/b", replacements) + if err != nil { + t.Fatal(err) + } assert.Equal(t, "image1.jpg", attaches[0].Name) assert.Equal(t, "image1.jpg", attaches[0].Replace) - assert.Equal(t, "a/b/image1.jpg", attaches[0].Path) assert.Equal(t, "images/image2.jpg", attaches[1].Name) assert.Equal(t, "images/image2.jpg", attaches[1].Replace) - assert.Equal(t, "a/b/images/image2.jpg", attaches[1].Path) assert.Equal(t, "../image3.jpg", attaches[2].Name) assert.Equal(t, "../image3.jpg", attaches[2].Replace) - assert.Equal(t, "a/image3.jpg", attaches[2].Path) assert.Equal(t, len(attaches), 3) } diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go index 5b12cd1..9325e5e 100644 --- a/pkg/mark/markdown.go +++ b/pkg/mark/markdown.go @@ -48,17 +48,20 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int { // Renderer renders anchor [Node]s. type ConfluenceRenderer struct { html.Config - Stdlib *stdlib.Lib - - LevelMap BlockQuoteLevelMap + Stdlib *stdlib.Lib + MermaidProvider string + LevelMap BlockQuoteLevelMap + Attachments []Attachment } // NewConfluenceRenderer creates a new instance of the ConfluenceRenderer -func NewConfluenceRenderer(stdlib *stdlib.Lib, opts ...html.Option) renderer.NodeRenderer { +func NewConfluenceRenderer(stdlib *stdlib.Lib, mermaidProvider string, opts ...html.Option) renderer.NodeRenderer { return &ConfluenceRenderer{ - Config: html.NewConfig(), - Stdlib: stdlib, - LevelMap: nil, + Config: html.NewConfig(), + Stdlib: stdlib, + MermaidProvider: mermaidProvider, + LevelMap: nil, + Attachments: []Attachment{}, } } @@ -355,29 +358,59 @@ func (r *ConfluenceRenderer) renderFencedCodeBlock(writer util.BufWriter, source line := node.Lines().At(i) lval = append(lval, line.Value(source)...) } - err := r.Stdlib.Templates.ExecuteTemplate( - writer, - "ac:code", - struct { - Language string - Collapse bool - Title string - Theme string - Linenumbers bool - Firstline int - Text string - }{ - lang, - collapse, - title, - theme, - linenumbers, - firstline, - strings.TrimSuffix(string(lval), "\n"), - }, - ) - if err != nil { - return ast.WalkStop, err + + if lang == "mermaid" && r.MermaidProvider == "mermaid-go" { + attachment, err := processMermaidLocally(title, lval) + if err != nil { + return ast.WalkStop, err + } + r.Attachments = append(r.Attachments, attachment) + err = r.Stdlib.Templates.ExecuteTemplate( + writer, + "ac:image", + struct { + Width string + Height string + Title string + Attachment string + }{ + attachment.Width, + attachment.Height, + attachment.Name, + attachment.Filename, + }, + ) + + if err != nil { + return ast.WalkStop, err + } + + } else { + err := r.Stdlib.Templates.ExecuteTemplate( + writer, + "ac:code", + struct { + Language string + Collapse bool + Title string + Theme string + Linenumbers bool + Firstline int + Text string + }{ + lang, + collapse, + title, + theme, + linenumbers, + firstline, + strings.TrimSuffix(string(lval), "\n"), + }, + ) + + if err != nil { + return ast.WalkStop, err + } } return ast.WalkContinue, nil @@ -430,9 +463,11 @@ func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byt return ast.WalkContinue, nil } -func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string { +func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, mermaidProvider string) (string, []Attachment) { log.Tracef(nil, "rendering markdown:\n%s", string(markdown)) + confluenceRenderer := NewConfluenceRenderer(stdlib, mermaidProvider) + converter := goldmark.New( goldmark.WithExtensions( extension.GFM, @@ -455,7 +490,7 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string { )) converter.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(NewConfluenceRenderer(stdlib), 100), + util.Prioritized(confluenceRenderer, 100), )) var buf bytes.Buffer @@ -469,7 +504,8 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string { log.Tracef(nil, "rendered markdown to html:\n%s", string(html)) - return string(html) + return string(html), confluenceRenderer.(*ConfluenceRenderer).Attachments + } // DropDocumentLeadingH1 will drop leading H1 headings to prevent diff --git a/pkg/mark/markdown_test.go b/pkg/mark/markdown_test.go index d48ebbc..a4c5053 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, "") test.EqualValues(string(html), actual, filename+" vs "+htmlname) } } diff --git a/pkg/mark/mermaid.go b/pkg/mark/mermaid.go new file mode 100644 index 0000000..73df4a5 --- /dev/null +++ b/pkg/mark/mermaid.go @@ -0,0 +1,50 @@ +package mark + +import ( + "bytes" + "context" + "strconv" + "time" + + mermaid "github.com/dreampuf/mermaid.go" +) + +var renderTimeout = 60 * time.Second + +func processMermaidLocally(title string, mermaidDiagram []byte) (attachement Attachment, err error) { + ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout) + defer cancel() + + renderer, err := mermaid.NewRenderEngine(ctx) + + if err != nil { + return Attachment{}, err + } + + pngBytes, boxModel, err := renderer.RenderAsPng(string(mermaidDiagram)) + if err != nil { + return Attachment{}, err + } + + checkSum, err := GetChecksum(bytes.NewReader(mermaidDiagram)) + + if err != nil { + return Attachment{}, err + } + if title == "" { + title = checkSum + } + + fileName := title + ".png" + + return Attachment{ + ID: "", + Name: title, + Filename: fileName, + FileBytes: pngBytes, + Checksum: checkSum, + Replace: title, + Width: strconv.FormatInt(boxModel.Width, 10), + Height: strconv.FormatInt(boxModel.Height, 10), + }, nil +} diff --git a/pkg/mark/mermaid_test.go b/pkg/mark/mermaid_test.go new file mode 100644 index 0000000..357db91 --- /dev/null +++ b/pkg/mark/mermaid_test.go @@ -0,0 +1,38 @@ +package mark + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractMermaidImage(t *testing.T) { + tests := []struct { + name string + markdown []byte + want Attachment + wantErr assert.ErrorAssertionFunc + }{ + {"example", []byte("graph TD;\n A-->B;"), Attachment{ + FileBytes: []byte{0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x2f, 0x0, 0x0, 0x0, 0x8e, 0x8, 0x6, 0x0, 0x0, 0x0, 0xa1, 0x67, 0x51, 0xe7, 0x0, 0x0, 0x0, 0x1, 0x73, 0x52, 0x47, 0x42, 0x0, 0xae, 0xce, 0x1c, 0xe9, 0x0, 0x0, 0x4, 0xc0, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0xed, 0x9c, 0x5f, 0x48, 0x5b, 0x57, 0x1c, 0xc7, 0xbf, 0xb7, 0xb6, 0x7a, 0xdb, 0xe8, 0x22, 0xce, 0xce, 0x18, 0xe6, 0xfc, 0xb7, 0x3f, 0x4a, 0xab, 0x2d, 0x31, 0xb5, 0x5, 0xd7, 0x3a, 0x25, 0x56, 0x5b, 0xc3, 0x64, 0x4f, 0x52, 0x28, 0x94, 0x3d, 0x58, 0xd7, 0x87, 0x15, 0x56, 0xda, 0x87, 0x8e, 0x62, 0xe9, 0xcb, 0x28, 0x4c, 0x46, 0xa1, 0x43, 0x29, 0x3e, 0xf8, 0xb0, 0x87, 0x31, 0x18, 0x48, 0xab, 0x73, 0x5d, 0xa2, 0xd5, 0xe0, 0x90, 0x4e, 0x92, 0x58, 0x86, 0xed, 0x64, 0xa2, 0xab, 0x22, 0xa9, 0xb3, 0xba, 0x6e, 0x6a, 0x52, 0x67, 0xaf, 0x66, 0x4f, 0x13, 0x52, 0x93, 0x5c, 0xcd, 0xce, 0xf1, 0xb7, 0xc0, 0xef, 0xf3, 0x94, 0x9b, 0x73, 0x72, 0xbe, 0x1f, 0x7e, 0x9c, 0x24, 0x37, 0xb9, 0xe7, 0x1e, 0x25, 0x18, 0xc, 0x6, 0x11, 0xa7, 0xec, 0xa0, 0x16, 0xf8, 0x2f, 0xb0, 0x3c, 0x15, 0x2c, 0x4f, 0x45, 0x5c, 0xcb, 0xef, 0x8c, 0xd6, 0x38, 0xf3, 0x78, 0x19, 0x53, 0xa3, 0x81, 0xed, 0x72, 0xd9, 0xc0, 0x1b, 0x5, 0x7b, 0x60, 0xca, 0x51, 0x23, 0xb6, 0x47, 0x95, 0x7f, 0xf2, 0xdb, 0x32, 0xc6, 0x47, 0x2, 0xc8, 0xcc, 0xdb, 0x23, 0x5c, 0x4c, 0x8f, 0x27, 0x13, 0x1, 0x24, 0xaa, 0x3b, 0x62, 0x97, 0x7, 0x80, 0xf4, 0xd7, 0x55, 0x14, 0x95, 0xa7, 0x9, 0x15, 0xdb, 0xc, 0x2f, 0x56, 0xd6, 0x74, 0xfb, 0xc4, 0xf5, 0x9c, 0x67, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0xf9, 0x70, 0x74, 0x77, 0x77, 0xa0, 0xba, 0xda, 0x8a, 0xa6, 0xa6, 0x4f, 0x64, 0x45, 0xc8, 0x93, 0x77, 0x3a, 0x3b, 0x91, 0x9b, 0xfb, 0x26, 0xdc, 0xee, 0x41, 0x3c, 0x7b, 0xf6, 0x87, 0x94, 0xc, 0x29, 0xf2, 0xd3, 0xd3, 0x53, 0x78, 0xf4, 0xe8, 0x67, 0x9c, 0x3b, 0x77, 0x11, 0xaa, 0xba, 0x1b, 0x7d, 0x7d, 0x77, 0x65, 0xc4, 0xc8, 0x91, 0x77, 0x38, 0xee, 0xc0, 0x64, 0x32, 0xa3, 0xb8, 0xb8, 0x4, 0x15, 0x15, 0x35, 0x70, 0x3a, 0x3b, 0x65, 0xc4, 0x88, 0x97, 0x5f, 0x5b, 0x5b, 0x43, 0x6f, 0x6f, 0x37, 0x6c, 0x36, 0x3b, 0x14, 0x45, 0x41, 0x55, 0x95, 0x1d, 0x13, 0x13, 0x63, 0x18, 0x1f, 0xff, 0x55, 0x74, 0x94, 0x78, 0xf9, 0xe1, 0xe1, 0x21, 0xcc, 0xcd, 0xcd, 0xa2, 0xaa, 0xca, 0xe, 0x0, 0x28, 0x28, 0xd8, 0x8f, 0xac, 0xac, 0x1c, 0xf4, 0xf6, 0x7e, 0x27, 0x3a, 0x4a, 0xbc, 0xbc, 0xc3, 0x71, 0x7, 0xc5, 0xc5, 0x16, 0xa4, 0xa7, 0xbf, 0x6, 0x4d, 0xd3, 0xa0, 0x69, 0x1a, 0x6c, 0xb6, 0x5a, 0xdc, 0xbb, 0xf7, 0x3d, 0x34, 0x4d, 0x13, 0x9a, 0xa5, 0xfb, 0x4b, 0x6a, 0x2b, 0xf8, 0xfd, 0x4b, 0x18, 0x1c, 0xec, 0xc7, 0xca, 0xca, 0xa, 0x6a, 0x6b, 0x8f, 0x6c, 0x68, 0xf7, 0x78, 0xee, 0xe3, 0xf0, 0xe1, 0x77, 0x85, 0xe5, 0x9, 0x95, 0xef, 0xef, 0xff, 0x1, 0x8a, 0xa2, 0xe0, 0xc6, 0x8d, 0x76, 0x24, 0x24, 0x24, 0x84, 0xb4, 0xb5, 0xb6, 0x36, 0xa3, 0xa7, 0xa7, 0xeb, 0xff, 0x2b, 0xef, 0x70, 0x74, 0xe2, 0xd0, 0xa1, 0x32, 0x14, 0x16, 0x16, 0x6d, 0x68, 0xab, 0xac, 0x3c, 0x81, 0x5b, 0xb7, 0xbe, 0xc0, 0xe2, 0xe2, 0x2, 0x52, 0x52, 0x5e, 0x11, 0x92, 0x27, 0x6c, 0xce, 0xfb, 0x7c, 0xd3, 0x18, 0x1d, 0x1d, 0xc1, 0xd1, 0xa3, 0xb6, 0xb0, 0xed, 0x65, 0x65, 0x95, 0xd0, 0x34, 0xd, 0x2e, 0x97, 0x43, 0x54, 0xa4, 0x38, 0xf9, 0x81, 0x81, 0x1e, 0x24, 0x25, 0x25, 0xa1, 0xb4, 0x34, 0xfc, 0xb4, 0x48, 0x4b, 0x7b, 0x15, 0x45, 0x45, 0x16, 0x38, 0x9d, 0x5d, 0xa2, 0x22, 0xa1, 0x44, 0xfb, 0x7f, 0x7e, 0xb8, 0xef, 0x4f, 0xcc, 0xfd, 0xae, 0xc1, 0x52, 0x95, 0x2e, 0x2c, 0x70, 0xb3, 0x78, 0x9d, 0x73, 0xd8, 0x6b, 0xda, 0x89, 0x83, 0xef, 0xa5, 0x46, 0xec, 0xc3, 0x67, 0x95, 0x54, 0xb0, 0x3c, 0x15, 0x2c, 0x4f, 0x5, 0xcb, 0x53, 0xc1, 0xf2, 0x54, 0xb0, 0x3c, 0x15, 0xba, 0xe7, 0xf3, 0x73, 0xd3, 0xcb, 0x18, 0x19, 0x90, 0xf3, 0xbf, 0x8b, 0x5e, 0xee, 0x5e, 0x53, 0x72, 0xd4, 0x3e, 0x51, 0xe5, 0x33, 0x73, 0x55, 0xbc, 0xf8, 0x5b, 0xff, 0xc2, 0x56, 0x38, 0xdc, 0x6e, 0x37, 0xbc, 0x5e, 0x2f, 0x2c, 0x16, 0xb, 0xac, 0x56, 0xeb, 0x96, 0x5f, 0x9f, 0xbf, 0x3f, 0xfa, 0x65, 0x4c, 0x40, 0x47, 0xde, 0x94, 0xa3, 0xea, 0xe, 0x10, 0x9, 0xef, 0xe3, 0x9, 0x3c, 0x98, 0xfa, 0x16, 0xd6, 0xe3, 0xa9, 0x28, 0xad, 0x39, 0x1e, 0xd3, 0x18, 0x7a, 0xc4, 0xf5, 0x9c, 0x67, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0x79, 0x2a, 0x58, 0x9e, 0x8a, 0xa8, 0xb, 0xe4, 0x36, 0x8b, 0xdb, 0xed, 0x46, 0x5b, 0x5b, 0x5b, 0xc8, 0x73, 0x3e, 0x9f, 0xf, 0x3e, 0x9f, 0xf, 0x66, 0xb3, 0x19, 0x66, 0xb3, 0x39, 0xa4, 0xad, 0xa1, 0xa1, 0x21, 0xa6, 0xcb, 0x9b, 0x2f, 0x23, 0x44, 0x1e, 0x0, 0xec, 0x76, 0x3b, 0x66, 0x66, 0x66, 0x74, 0xfb, 0x99, 0x4c, 0x26, 0x74, 0x75, 0x89, 0x59, 0x9e, 0x28, 0x6c, 0xda, 0x34, 0x36, 0x36, 0xa, 0xed, 0xb7, 0x19, 0x84, 0x55, 0x1e, 0xd0, 0xaf, 0xbe, 0xc8, 0xaa, 0x3, 0x82, 0xdf, 0xb0, 0x7a, 0x55, 0x15, 0x59, 0x75, 0x40, 0x70, 0xe5, 0x81, 0xc8, 0xd5, 0x17, 0x5d, 0x75, 0x40, 0xc2, 0x47, 0x65, 0xa4, 0xea, 0x8a, 0xae, 0x3a, 0x20, 0xa1, 0xf2, 0xc0, 0xc6, 0xea, 0xcb, 0xa8, 0x3a, 0x20, 0xe9, 0x4b, 0xea, 0xe5, 0x2a, 0xcb, 0xa8, 0x3a, 0x20, 0xa9, 0xf2, 0x0, 0x50, 0x5e, 0x5e, 0xe, 0xbf, 0xdf, 0xf, 0x83, 0xc1, 0x0, 0x97, 0xcb, 0x25, 0x23, 0x42, 0xde, 0xbe, 0x7, 0x36, 0xcb, 0x47, 0x98, 0x9c, 0x9c, 0x44, 0x76, 0x76, 0x36, 0x86, 0xee, 0xc6, 0xb6, 0x52, 0x4a, 0x6f, 0xdf, 0x3, 0xdd, 0xf5, 0xf3, 0xa3, 0x9e, 0x25, 0xb2, 0x7d, 0xf, 0xa, 0xad, 0xc9, 0x51, 0xd7, 0xcf, 0xf3, 0xbe, 0x7, 0x54, 0xb0, 0x3c, 0x15, 0x2c, 0x4f, 0x5, 0xcb, 0x53, 0xc1, 0xf2, 0x54, 0xc4, 0xb5, 0xbc, 0xd0, 0x5b, 0x4a, 0x6f, 0xdf, 0xfe, 0x6, 0x2d, 0x2d, 0x9f, 0xaf, 0x1f, 0x2b, 0x8a, 0x2, 0xa3, 0x31, 0x15, 0xf9, 0xf9, 0xef, 0xa0, 0xbe, 0xfe, 0x43, 0x1c, 0x38, 0x50, 0x22, 0x32, 0x4e, 0xac, 0xfc, 0xbf, 0x5c, 0xbd, 0xda, 0xc, 0x83, 0x21, 0x19, 0xc1, 0x60, 0x10, 0xf3, 0xf3, 0x4f, 0xd1, 0xd1, 0xf1, 0x35, 0xae, 0x5c, 0xf9, 0x18, 0x37, 0x6f, 0x7e, 0x85, 0x9c, 0x9c, 0x7c, 0x61, 0x39, 0x52, 0xe4, 0xf7, 0xed, 0x3b, 0x8, 0xa3, 0x31, 0x35, 0xe4, 0xf8, 0xcc, 0x99, 0xf7, 0x31, 0x34, 0xf4, 0xa3, 0x50, 0xf9, 0x6d, 0x99, 0xf3, 0xbb, 0x76, 0x25, 0x2, 0x0, 0x52, 0x52, 0x8c, 0x42, 0xc7, 0x95, 0x52, 0xf9, 0xd5, 0xd5, 0x55, 0x68, 0x9a, 0x86, 0x60, 0x30, 0x88, 0xd9, 0xd9, 0x19, 0xb4, 0xb7, 0x7f, 0x89, 0x8c, 0x8c, 0x4c, 0x1c, 0x3b, 0x16, 0xfe, 0x2e, 0xe5, 0x58, 0x91, 0x22, 0x7f, 0xea, 0x54, 0x75, 0xc8, 0x71, 0x46, 0x46, 0x26, 0x2e, 0x5f, 0xfe, 0xc, 0x6, 0x43, 0xf4, 0x7b, 0x40, 0xb6, 0x8a, 0x14, 0xf9, 0xeb, 0xd7, 0x5b, 0xd6, 0x45, 0x17, 0x17, 0x17, 0xe0, 0xf1, 0xdc, 0xc7, 0xa5, 0x4b, 0x67, 0x71, 0xe1, 0x42, 0x13, 0x2a, 0x2b, 0x4f, 0x8, 0xcb, 0x91, 0x22, 0x9f, 0x97, 0xf7, 0x76, 0xc8, 0x1b, 0xb6, 0xa4, 0xe4, 0x8, 0x2, 0x81, 0x25, 0xb4, 0xb6, 0x36, 0xa3, 0xa2, 0xa2, 0x6, 0x8a, 0xa2, 0x8, 0xc9, 0xd9, 0xb6, 0x2f, 0xa9, 0xdc, 0xdc, 0xb7, 0xb0, 0xb0, 0xf0, 0x17, 0xe6, 0xe7, 0x9f, 0xa, 0x1b, 0x73, 0xdb, 0xe4, 0xc7, 0xc6, 0x7e, 0x41, 0x62, 0x62, 0x22, 0x92, 0x93, 0xc5, 0xec, 0x79, 0x0, 0x48, 0x9a, 0x36, 0xf, 0x1f, 0x3e, 0x58, 0x9f, 0xf3, 0xcf, 0x9f, 0x7, 0xe0, 0xf5, 0xfe, 0x4, 0xa7, 0xb3, 0xb, 0xa7, 0x4f, 0x9f, 0x85, 0xaa, 0xc6, 0x76, 0xeb, 0x52, 0x38, 0xa4, 0xc8, 0x5f, 0xbb, 0x76, 0x71, 0xfd, 0xb1, 0xaa, 0xee, 0x46, 0x56, 0x56, 0x36, 0xce, 0x9f, 0xff, 0x14, 0x27, 0x4f, 0x7e, 0x20, 0x34, 0x47, 0xa8, 0x7c, 0x5d, 0x5d, 0x3d, 0xea, 0xea, 0xea, 0x45, 0xe, 0x19, 0x95, 0xb8, 0x3e, 0xab, 0x64, 0x79, 0x2a, 0x58, 0x9e, 0xa, 0x96, 0xa7, 0x82, 0xe5, 0xa9, 0x60, 0x79, 0x2a, 0xe2, 0x5a, 0x9e, 0xf7, 0x3d, 0x90, 0xc1, 0x66, 0xf6, 0x3d, 0x90, 0x76, 0x11, 0x79, 0x3b, 0x88, 0xeb, 0x39, 0xcf, 0xf2, 0x54, 0xb0, 0x3c, 0x15, 0x71, 0x2d, 0xff, 0xf, 0x4a, 0x95, 0x78, 0x8f, 0xd5, 0x30, 0xc5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}, + Filename: "example.png", + Name: "example", + Replace: "example", + Checksum: "1743a4f31ab66244591f06c8056e08053b8e0a554eb9a38709af6e9d145ac84f", + ID: "", + Width: "47", + Height: "142", + }, + assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := processMermaidLocally(tt.name, tt.markdown) + if !tt.wantErr(t, err, fmt.Sprintf("processMermaidLocally(%v, %v)", tt.name, tt.markdown)) { + return + } + assert.Equalf(t, tt.want, got, "processMermaidLocally(%v, %v)", tt.name, tt.markdown) + }) + } +} diff --git a/pkg/mark/parser/confluencetags.go b/pkg/mark/parser/confluencetags.go index 56bba8d..54e7ff0 100644 --- a/pkg/mark/parser/confluencetags.go +++ b/pkg/mark/parser/confluencetags.go @@ -2,11 +2,12 @@ package parser import ( "bytes" + "regexp" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" - "regexp" ) // NewConfluenceTagParser returns an inline parser that parses and tags to ensure that Confluence specific tags are parsed diff --git a/pkg/mark/stdlib/stdlib.go b/pkg/mark/stdlib/stdlib.go index 6df86d8..bf294b8 100644 --- a/pkg/mark/stdlib/stdlib.go +++ b/pkg/mark/stdlib/stdlib.go @@ -42,13 +42,13 @@ func macros(templates *template.Template) ([]macro.Macro, error) { macros, _, err := macro.ExtractMacros( "", - []byte(text( + text( ``, // TODO(seletskiy): more macros here - )), + ), templates, ) @@ -218,7 +218,9 @@ func templates(api *confluence.API) (*template.Template, error) { ``, ), `ac:image`: text( - ``, + `{{printf "\n"}}`, + `{{printf "\n"}}`, + `{{printf "\n"}}`, ), /* https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube */ diff --git a/pkg/mark/vfs/vfs.go b/pkg/mark/vfs/vfs.go new file mode 100644 index 0000000..b23d627 --- /dev/null +++ b/pkg/mark/vfs/vfs.go @@ -0,0 +1,19 @@ +package vfs + +import ( + "io" + "os" +) + +type Opener interface { + Open(name string) (io.ReadWriteCloser, error) +} + +type LocalOSOpener struct { +} + +func (o LocalOSOpener) Open(name string) (io.ReadWriteCloser, error) { + return os.Open(name) +} + +var LocalOS = LocalOSOpener{}