Add support for mermaid via library

Implementation is largely based on: https://github.com/kovetskiy/mark/pull/167

Co-Authored-By: Manuel Rueger <manuel@rueg.eu>
This commit is contained in:
Dreampuf 2023-04-12 13:43:34 +02:00 committed by Manuel Rüger
parent 88c070f524
commit d9d560eda0
16 changed files with 466 additions and 186 deletions

View File

@ -11,7 +11,7 @@ on:
- master - master
env: env:
GO_VERSION: "~1.20.2" GO_VERSION: "~1.20.3"
jobs: jobs:
# Runs Golangci-lint on the source code # Runs Golangci-lint on the source code

View File

@ -1,11 +1,15 @@
FROM golang:1.20.2 as builder FROM golang:1.20.3 as builder
ENV GOPATH="/go" ENV GOPATH="/go"
WORKDIR /go/src/github.com/kovetskiy/mark WORKDIR /go/src/github.com/kovetskiy/mark
COPY / . COPY / .
RUN make get \ RUN make get \
&& make build && make build
FROM alpine:3.17 FROM chromedp/headless-shell:latest
RUN apk --no-cache add ca-certificates bash sed git 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/ COPY --from=builder /go/src/github.com/kovetskiy/mark/mark /bin/
WORKDIR /docs WORKDIR /docs

View File

@ -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). 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 ## Installation
### Homebrew ### Homebrew

16
go.mod
View File

@ -3,27 +3,37 @@ module github.com/kovetskiy/mark
go 1.19 go 1.19
require ( require (
github.com/dreampuf/mermaid.go v0.0.4-0.20220314184516-44d4f4fc9d39
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69 github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69
github.com/kovetskiy/lorg v1.2.0 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/pkg v1.3.0
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
github.com/urfave/cli/v2 v2.25.1 github.com/urfave/cli/v2 v2.25.1
github.com/yuin/goldmark v1.5.4 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 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/BurntSushi/toml v1.2.1 // indirect 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/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65 // indirect github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3 // 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 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
) )

48
go.sum
View File

@ -1,32 +1,58 @@
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vn82v0gKhTTm67znr7nxYBNW4mJ8zfY7dywZivUy3tY=
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM= 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 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 h1:wNIUT/VOhcjKOmizDClZLvchbKFGW+dzf9fQXbSVS5E=
github.com/kovetskiy/lorg v1.2.0/go.mod h1:rdiamaIRUCkX9HtFZd0D9dQqUbad21hipHk+sat7Z6s= 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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:IiAAeijD0sU3C6OO9vy/0WUUfRabZ1aH7hijCBteJC4=
github.com/reconquest/cog v0.0.0-20210820140837-c5c4e8f49c65/go.mod h1:iin2k2yhKESAy14B2fXK8gpf1nofl7dTXH5U+VdIlss= 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-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-20230425053540-765a8ab89f64 h1:1ftdBojSr4bnBpe3UkSGG40fI70g5Av3xLQKn9Gkmx4=
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/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 h1:Yuoxiw92rP/srKXMo5qSML2InhJ+xAqHJIx3/y/2zh8=
github.com/reconquest/pkg v1.3.0/go.mod h1:hUQ0SzzBlFRSbo6lFYG2tSpLMjqOuUqm2LtpjR/+1sg= 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 h1:bcDXaTFC09IIg13Z8gfQHk4gSu001ET7ssW/wKRvPzg=
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= 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/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 h1:BhVaeQJc3xalHGONn215FylzuxdQBIT3d/aRjDg4nXQ=
github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess= 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/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 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 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

34
main.go
View File

@ -13,6 +13,7 @@ import (
"github.com/kovetskiy/mark/pkg/mark/includes" "github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/macro" "github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/kovetskiy/mark/pkg/mark/stdlib" "github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log" "github.com/reconquest/pkg/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -22,8 +23,7 @@ import (
const ( const (
version = "9.1.4" version = "9.1.4"
usage = "A tool for updating Atlassian Confluence pages from markdown." 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{ 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.", Usage: "use specified space key. If the space key is not specified, it must be set in the page metadata.",
EnvVars: []string{"MARK_SPACE"}, 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() { func main() {
@ -347,7 +353,8 @@ func processFile(
markdown = mark.DropDocumentLeadingH1(markdown) 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) os.Exit(0)
} }
@ -399,11 +406,16 @@ func processFile(
target = page target = page
} }
// Resolve attachments created from <!-- Attachment: --> 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( attaches, err := mark.ResolveAttachments(
api, api,
target, target,
filepath.Dir(file), localAttachments,
meta.Attachments,
) )
if err != nil { if err != nil {
log.Fatalf(err, "unable to create/update attachments") log.Fatalf(err, "unable to create/update attachments")
@ -418,7 +430,17 @@ func processFile(
markdown = mark.DropDocumentLeadingH1(markdown) 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 var buffer bytes.Buffer

View File

@ -8,7 +8,6 @@ import (
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/kovetskiy/gopencils" "github.com/kovetskiy/gopencils"
@ -52,7 +51,7 @@ type PageInfo struct {
} `json:"version"` } `json:"version"`
Ancestors []struct { Ancestors []struct {
Id string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
} `json:"ancestors"` } `json:"ancestors"`
@ -141,7 +140,7 @@ func (api *API) FindRootPage(space string) (*PageInfo, error) {
} }
return &PageInfo{ return &PageInfo{
ID: page.Ancestors[0].Id, ID: page.Ancestors[0].ID,
Title: page.Ancestors[0].Title, Title: page.Ancestors[0].Title,
}, nil }, nil
} }
@ -158,7 +157,7 @@ func (api *API) FindHomePage(space string) (*PageInfo, error) {
return nil, err 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) return nil, newErrorStatusNotOK(request)
} }
@ -193,7 +192,7 @@ func (api *API) FindPage(
// allow 404 because it's fine if page is not found, // allow 404 because it's fine if page is not found,
// the function will return nil, nil // 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) return nil, newErrorStatusNotOK(request)
} }
@ -208,11 +207,11 @@ func (api *API) CreateAttachment(
pageID string, pageID string,
name string, name string,
comment string, comment string,
path string, reader io.Reader,
) (AttachmentInfo, error) { ) (AttachmentInfo, error) {
var info AttachmentInfo var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path) form, err := getAttachmentPayload(name, comment, reader)
if err != nil { if err != nil {
return AttachmentInfo{}, err return AttachmentInfo{}, err
} }
@ -243,7 +242,7 @@ func (api *API) CreateAttachment(
return info, err return info, err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return info, newErrorStatusNotOK(request) return info, newErrorStatusNotOK(request)
} }
@ -276,11 +275,11 @@ func (api *API) UpdateAttachment(
attachID string, attachID string,
name string, name string,
comment string, comment string,
path string, reader io.Reader,
) (AttachmentInfo, error) { ) (AttachmentInfo, error) {
var info AttachmentInfo var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path) form, err := getAttachmentPayload(name, comment, reader)
if err != nil { if err != nil {
return AttachmentInfo{}, err return AttachmentInfo{}, err
} }
@ -313,7 +312,7 @@ func (api *API) UpdateAttachment(
return info, err return info, err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return info, newErrorStatusNotOK(request) return info, newErrorStatusNotOK(request)
} }
@ -353,23 +352,12 @@ func (api *API) UpdateAttachment(
return shortResponse, nil return shortResponse, nil
} }
func getAttachmentPayload(name, comment, path string) (*form, error) { func getAttachmentPayload(name, comment string, reader io.Reader) (*form, error) {
var ( var (
payload = bytes.NewBuffer(nil) payload = bytes.NewBuffer(nil)
writer = multipart.NewWriter(payload) 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) content, err := writer.CreateFormFile("file", name)
if err != nil { if err != nil {
return nil, karma.Format( 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 { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
@ -436,7 +424,7 @@ func (api *API) GetAttachments(pageID string) ([]AttachmentInfo, error) {
return nil, err return nil, err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request) return nil, newErrorStatusNotOK(request)
} }
@ -459,7 +447,7 @@ func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
return nil, err return nil, err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request) return nil, newErrorStatusNotOK(request)
} }
@ -507,7 +495,7 @@ func (api *API) CreatePage(
return nil, err return nil, err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request) 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 { if page.Type != "blogpost" && len(page.Ancestors) > 0 {
// picking only the last one, which is required by confluence // picking only the last one, which is required by confluence
oldAncestors = []map[string]interface{}{ 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, "ancestors": oldAncestors,
"body": map[string]interface{}{ "body": map[string]interface{}{
"storage": map[string]interface{}{ "storage": map[string]interface{}{
"value": string(newContent), "value": newContent,
"representation": "storage", "representation": "storage",
}, },
}, },
@ -573,7 +561,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ne
return err return err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request) return newErrorStatusNotOK(request)
} }
@ -654,7 +642,7 @@ func (api *API) RestrictPageUpdatesCloud(
return err return err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request) return newErrorStatusNotOK(request)
} }
@ -685,7 +673,7 @@ func (api *API) RestrictPageUpdatesServer(
return err return err
} }
if request.Raw.StatusCode != 200 { if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request) return newErrorStatusNotOK(request)
} }
@ -715,13 +703,13 @@ func (api *API) RestrictPageUpdates(
} }
func newErrorStatusNotOK(request *gopencils.Resource) error { func newErrorStatusNotOK(request *gopencils.Resource) error {
if request.Raw.StatusCode == 401 { if request.Raw.StatusCode == http.StatusUnauthorized {
return errors.New( return errors.New(
"Confluence API returned unexpected status: 401 (Unauthorized)", "Confluence API returned unexpected status: 401 (Unauthorized)",
) )
} }
if request.Raw.StatusCode == 404 { if request.Raw.StatusCode == http.StatusNotFound {
return errors.New( return errors.New(
"Confluence API returned unexpected status: 404 (Not Found)", "Confluence API returned unexpected status: 404 (Not Found)",
) )

View File

@ -6,13 +6,13 @@ import (
"encoding/hex" "encoding/hex"
"io" "io"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/karma-go" "github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log" "github.com/reconquest/pkg/log"
) )
@ -22,36 +22,32 @@ const (
) )
type Attachment struct { type Attachment struct {
ID string ID string
Name string Name string
Filename string Filename string
Path string FileBytes []byte
Checksum string Checksum string
Link string Link string
Replace string Width string
Height string
Replace string
} }
func ResolveAttachments( func ResolveAttachments(
api *confluence.API, api *confluence.API,
page *confluence.PageInfo, page *confluence.PageInfo,
base string, attachments []Attachment,
replacements []string,
) ([]Attachment, error) { ) ([]Attachment, error) {
attaches, err := prepareAttachments(base, replacements) for i := range attachments {
if err != nil { checksum, err := GetChecksum(bytes.NewReader(attachments[i].FileBytes))
return nil, err
}
for i := range attaches {
checksum, err := getChecksum(attaches[i].Path)
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, 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) remotes, err := api.GetAttachments(page.ID)
@ -62,18 +58,18 @@ func ResolveAttachments(
existing := []Attachment{} existing := []Attachment{}
creating := []Attachment{} creating := []Attachment{}
updating := []Attachment{} updating := []Attachment{}
for _, attach := range attaches { for _, attachment := range attachments {
var found bool var found bool
var same bool var same bool
for _, remote := range remotes { for _, remote := range remotes {
if remote.Filename == attach.Filename { if remote.Filename == attachment.Filename {
same = attach.Checksum == strings.TrimPrefix( same = attachment.Checksum == strings.TrimPrefix(
remote.Metadata.Comment, remote.Metadata.Comment,
AttachmentChecksumPrefix, AttachmentChecksumPrefix,
) )
attach.ID = remote.ID attachment.ID = remote.ID
attach.Link = path.Join( attachment.Link = path.Join(
remote.Links.Context, remote.Links.Context,
remote.Links.Download, remote.Links.Download,
) )
@ -86,109 +82,143 @@ func ResolveAttachments(
if found { if found {
if same { if same {
existing = append(existing, attach) existing = append(existing, attachment)
} else { } else {
updating = append(updating, attach) updating = append(updating, attachment)
} }
} else { } else {
creating = append(creating, attach) creating = append(creating, attachment)
} }
} }
for i, attach := range creating { for i, attachment := range creating {
log.Infof(nil, "creating attachment: %q", attach.Name) log.Infof(nil, "creating attachment: %q", attachment.Name)
info, err := api.CreateAttachment( info, err := api.CreateAttachment(
page.ID, page.ID,
attach.Filename, attachment.Filename,
AttachmentChecksumPrefix+attach.Checksum, AttachmentChecksumPrefix+attachment.Checksum,
attach.Path, bytes.NewReader(attachment.FileBytes),
) )
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"unable to create attachment %q", "unable to create attachment %q",
attach.Name, attachment.Name,
) )
} }
attach.ID = info.ID attachment.ID = info.ID
attach.Link = path.Join( attachment.Link = path.Join(
info.Links.Context, info.Links.Context,
info.Links.Download, info.Links.Download,
) )
creating[i] = attach creating[i] = attachment
} }
for i, attach := range updating { for i, attachment := range updating {
log.Infof(nil, "updating attachment: %q", attach.Name) log.Infof(nil, "updating attachment: %q", attachment.Name)
info, err := api.UpdateAttachment( info, err := api.UpdateAttachment(
page.ID, page.ID,
attach.ID, attachment.ID,
attach.Filename, attachment.Filename,
AttachmentChecksumPrefix+attach.Checksum, AttachmentChecksumPrefix+attachment.Checksum,
attach.Path, bytes.NewReader(attachment.FileBytes),
) )
if err != nil { if err != nil {
return nil, karma.Format( return nil, karma.Format(
err, err,
"unable to update attachment %q", "unable to update attachment %q",
attach.Name, attachment.Name,
) )
} }
attach.Link = path.Join( attachment.Link = path.Join(
info.Links.Context, info.Links.Context,
info.Links.Download, info.Links.Download,
) )
updating[i] = attach updating[i] = attachment
} }
for i := range existing { 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{} attachments = []Attachment{}
attaches = append(attaches, existing...) attachments = append(attachments, existing...)
attaches = append(attaches, creating...) attachments = append(attachments, creating...)
attaches = append(attaches, updating...) attachments = append(attachments, updating...)
return attaches, nil return attachments, nil
} }
func prepareAttachments(base string, replacements []string) ([]Attachment, error) { func ResolveLocalAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) {
attaches := []Attachment{} attachments, err := prepareAttachments(opener, base, replacements)
for _, name := range replacements { if err != nil {
attach := Attachment{ return nil, err
Name: name, }
Filename: strings.ReplaceAll(name, "/", "_"),
Path: filepath.Join(base, name), for _, attachment := range attachments {
Replace: name, 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 attachments, err
return attaches, nil
} }
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{} links := map[string]string{}
replaces := []string{} replaces := []string{}
for _, attach := range attaches { for _, attachment := range attachments {
uri, err := url.ParseRequestURI(attach.Link) links[attachment.Replace] = parseAttachmentLink(attachment.Link)
if err != nil { replaces = append(replaces, attachment.Replace)
links[attach.Replace] = strings.ReplaceAll("&", "&amp;", attach.Link)
} else {
links[attach.Replace] = uri.Path +
"?" + url.QueryEscape(uri.Query().Encode())
}
replaces = append(replaces, attach.Replace)
} }
// sort by length so first items will have bigger length // sort by length so first items will have bigger length
@ -240,20 +270,21 @@ func CompileAttachmentLinks(markdown []byte, attaches []Attachment) []byte {
return markdown return markdown
} }
func getChecksum(filename string) (string, error) { func GetChecksum(reader io.Reader) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", karma.Format(
err,
"unable to open file",
)
}
defer file.Close()
hash := sha256.New() hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil { if _, err := io.Copy(hash, reader); err != nil {
return "", err return "", err
} }
return hex.EncodeToString(hash.Sum(nil)), nil return hex.EncodeToString(hash.Sum(nil)), nil
} }
func parseAttachmentLink(attachLink string) string {
uri, err := url.ParseRequestURI(attachLink)
if err != nil {
return strings.ReplaceAll("&", "&amp;", attachLink)
} else {
return uri.Path +
"?" + url.QueryEscape(uri.Query().Encode())
}
}

View File

@ -1,6 +1,9 @@
package mark package mark
import ( import (
"bytes"
"io"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "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) { 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 { if err != nil {
println(err.Error()) println(err.Error())
t.Fatal(err)
} }
assert.Equal(t, "image1.jpg", attaches[0].Name) assert.Equal(t, "image1.jpg", attaches[0].Name)
assert.Equal(t, "image1.jpg", attaches[0].Replace) 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].Name)
assert.Equal(t, "images/image2.jpg", attaches[1].Replace) 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].Name)
assert.Equal(t, "../image3.jpg", attaches[2].Replace) assert.Equal(t, "../image3.jpg", attaches[2].Replace)
assert.Equal(t, "../image3.jpg", attaches[2].Path)
assert.Equal(t, len(attaches), 3) assert.Equal(t, len(attaches), 3)
} }
func TestPrepareAttachmentsWithSubDirBase(t *testing.T) { 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].Name)
assert.Equal(t, "image1.jpg", attaches[0].Replace) 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].Name)
assert.Equal(t, "images/image2.jpg", attaches[1].Replace) 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].Name)
assert.Equal(t, "../image3.jpg", attaches[2].Replace) assert.Equal(t, "../image3.jpg", attaches[2].Replace)
assert.Equal(t, "a/image3.jpg", attaches[2].Path)
assert.Equal(t, len(attaches), 3) assert.Equal(t, len(attaches), 3)
} }

View File

@ -48,17 +48,20 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int {
// Renderer renders anchor [Node]s. // Renderer renders anchor [Node]s.
type ConfluenceRenderer struct { type ConfluenceRenderer struct {
html.Config html.Config
Stdlib *stdlib.Lib Stdlib *stdlib.Lib
MermaidProvider string
LevelMap BlockQuoteLevelMap LevelMap BlockQuoteLevelMap
Attachments []Attachment
} }
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer // 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{ return &ConfluenceRenderer{
Config: html.NewConfig(), Config: html.NewConfig(),
Stdlib: stdlib, Stdlib: stdlib,
LevelMap: nil, MermaidProvider: mermaidProvider,
LevelMap: nil,
Attachments: []Attachment{},
} }
} }
@ -355,29 +358,59 @@ func (r *ConfluenceRenderer) renderFencedCodeBlock(writer util.BufWriter, source
line := node.Lines().At(i) line := node.Lines().At(i)
lval = append(lval, line.Value(source)...) lval = append(lval, line.Value(source)...)
} }
err := r.Stdlib.Templates.ExecuteTemplate(
writer, if lang == "mermaid" && r.MermaidProvider == "mermaid-go" {
"ac:code", attachment, err := processMermaidLocally(title, lval)
struct { if err != nil {
Language string return ast.WalkStop, err
Collapse bool }
Title string r.Attachments = append(r.Attachments, attachment)
Theme string err = r.Stdlib.Templates.ExecuteTemplate(
Linenumbers bool writer,
Firstline int "ac:image",
Text string struct {
}{ Width string
lang, Height string
collapse, Title string
title, Attachment string
theme, }{
linenumbers, attachment.Width,
firstline, attachment.Height,
strings.TrimSuffix(string(lval), "\n"), attachment.Name,
}, attachment.Filename,
) },
if err != nil { )
return ast.WalkStop, err
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 return ast.WalkContinue, nil
@ -430,9 +463,11 @@ func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byt
return ast.WalkContinue, nil 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)) log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
confluenceRenderer := NewConfluenceRenderer(stdlib, mermaidProvider)
converter := goldmark.New( converter := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,
@ -455,7 +490,7 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string {
)) ))
converter.Renderer().AddOptions(renderer.WithNodeRenderers( converter.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewConfluenceRenderer(stdlib), 100), util.Prioritized(confluenceRenderer, 100),
)) ))
var buf bytes.Buffer 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)) 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 // DropDocumentLeadingH1 will drop leading H1 headings to prevent

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, "")
test.EqualValues(string(html), actual, filename+" vs "+htmlname) test.EqualValues(string(html), actual, filename+" vs "+htmlname)
} }
} }

50
pkg/mark/mermaid.go Normal file
View File

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

38
pkg/mark/mermaid_test.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,12 @@ package parser
import ( import (
"bytes" "bytes"
"regexp"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text" "github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
"regexp"
) )
// NewConfluenceTagParser returns an inline parser that parses <ac:* /> and <ri:* /> tags to ensure that Confluence specific tags are parsed // NewConfluenceTagParser returns an inline parser that parses <ac:* /> and <ri:* /> tags to ensure that Confluence specific tags are parsed

View File

@ -42,13 +42,13 @@ func macros(templates *template.Template) ([]macro.Macro, error) {
macros, _, err := macro.ExtractMacros( macros, _, err := macro.ExtractMacros(
"", "",
[]byte(text( text(
`<!-- Macro: @\{([^}]+)\}`, `<!-- Macro: @\{([^}]+)\}`,
` Template: ac:link:user`, ` Template: ac:link:user`,
` Name: ${1} -->`, ` Name: ${1} -->`,
// TODO(seletskiy): more macros here // TODO(seletskiy): more macros here
)), ),
templates, templates,
) )
@ -218,7 +218,9 @@ func templates(api *confluence.API) (*template.Template, error) {
`<ac:emoticon ac:name="{{ .Name }}"/>`, `<ac:emoticon ac:name="{{ .Name }}"/>`,
), ),
`ac:image`: text( `ac:image`: text(
`<ac:image {{ if .Width}}ac:width="{{ .Width }}"{{end}}><ri:attachment ri:filename="{{ .Attachment | convertAttachment }}"/></ac:image>`, `<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"}}`,
), ),
/* https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube */ /* https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube */

19
pkg/mark/vfs/vfs.go Normal file
View File

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