diff --git a/go.mod b/go.mod index d11febe..0021bd7 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,6 @@ require ( github.com/reconquest/pkg v0.0.0-20201028091908-8e9a5e0226ef github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 github.com/russross/blackfriday v1.5.2 - github.com/stretchr/testify v1.5.1 // indirect + github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 715d930..82167a8 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,6 @@ github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwn github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= -github.com/kovetskiy/gopencils v0.0.0-20201103141120-610929377f9b h1:+PnJcuiUVcU3ixOvpvhyjswKPkxBVN+a2CaFCNNBfvw= -github.com/kovetskiy/gopencils v0.0.0-20201103141120-610929377f9b/go.mod h1:rn9YsgK4kxBDPZn+hOwSmg6MdtWfF2ejC3tvgDjWyBM= github.com/kovetskiy/gopencils v0.0.0-20201105104258-2a0bfdd710fb h1:e8UwTXL3Nauw5T847OMHZRhAfQy4ntgQ7PUwgZ2ct4w= github.com/kovetskiy/gopencils v0.0.0-20201105104258-2a0bfdd710fb/go.mod h1:rn9YsgK4kxBDPZn+hOwSmg6MdtWfF2ejC3tvgDjWyBM= github.com/kovetskiy/ko v0.0.0-20190324102900-26b8dd0988bf h1:4QsqgCcPoqDB91dcp4GffoV6TjwfVURaWpjKWFi0ae0= diff --git a/main.go b/main.go index 40d0082..24084d5 100644 --- a/main.go +++ b/main.go @@ -213,6 +213,12 @@ func main() { } } + links, err := mark.ResolveRelativeLinks(api, markdown, ".") + if err != nil { + log.Fatalf(err, "unable to resolve relative links") + } + markdown = mark.ReplaceRelativeLinks(markdown, links) + if dryRun { compileOnly = true diff --git a/pkg/confluence/api.go b/pkg/confluence/api.go index 40327b0..56174de 100644 --- a/pkg/confluence/api.go +++ b/pkg/confluence/api.go @@ -26,7 +26,8 @@ type API struct { // it's deprecated accordingly to Atlassian documentation, // but it's only way to set permissions - json *gopencils.Resource + json *gopencils.Resource + BaseURL string } type PageInfo struct { @@ -87,8 +88,9 @@ func NewAPI(baseURL string, username string, password string) *API { } return &API{ - rest: rest, - json: json, + rest: rest, + json: json, + BaseURL: strings.TrimSuffix(baseURL, "/"), } } diff --git a/pkg/mark/link.go b/pkg/mark/link.go new file mode 100644 index 0000000..48c98bb --- /dev/null +++ b/pkg/mark/link.go @@ -0,0 +1,105 @@ +package mark + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "regexp" + + "github.com/kovetskiy/mark/pkg/confluence" +) + +type Link struct { + MDLink string + Link string +} + +// ResolveRelativeLinks finds links in the markdown, and replaces one pointing +// to other Markdowns either by own link (if not created to Confluence yet) or +// witn actual Confluence link +func ResolveRelativeLinks( + api *confluence.API, + markdown []byte, + base string, +) (links []Link, collectedErrors error) { + currentMarkdownMetadata, onlyMarkdown, err := ExtractMeta(markdown) + if err != nil { + return links, fmt.Errorf("unable to get metadata from handled markdown file. Error %w", err) + } + + currentPageLinkString, collectedErrors := getConfluenceLink(api, currentMarkdownMetadata.Space, currentMarkdownMetadata.Title, collectedErrors) + + submatchall := collectLinksFromMarkdown(string(onlyMarkdown)) + + for _, element := range submatchall { + link := Link{ + MDLink: element[1], + Link: currentPageLinkString, + } + // If link points to markdown like target, we build link for that in Confluence + if len(element[2]) > 0 { + possibleMDFile := element[2] + filepath := filepath.Join(base, possibleMDFile) + if _, err := os.Stat(filepath); err == nil { + linkMarkdown, err := ioutil.ReadFile(filepath) + if err != nil { + collectedErrors = fmt.Errorf("%w\n unable to read markdown file "+filepath, collectedErrors) + continue + } + // This helps to determine if found link points to file that's not markdown + // or have mark required metadata + meta, _, err := ExtractMeta(linkMarkdown) + if err != nil { + collectedErrors = fmt.Errorf("%w\n unable to get metadata from markdown file "+filepath, collectedErrors) + continue + } + + link.Link, collectedErrors = getConfluenceLink(api, meta.Space, meta.Title, collectedErrors) + } + } + + if len(element[3]) > 0 { + link.Link = currentPageLinkString + "#" + element[2] + } + + links = append(links, link) + } + return links, collectedErrors +} + +// ReplaceRelativeLinks replaces relative links between md files (in same +// directory structure) with links working in Confluence +func ReplaceRelativeLinks(markdown []byte, links []Link) []byte { + for _, link := range links { + markdown = bytes.ReplaceAll( + markdown, + []byte(fmt.Sprintf("](%s)", link.MDLink)), + []byte(fmt.Sprintf("](%s)", link.Link)), + ) + } + return markdown +} + +// collectLinksFromMarkdown collects all links from given markdown file +// (including images and external links) +func collectLinksFromMarkdown(markdown string) [][]string { + re := regexp.MustCompile("\\[[^\\]]+\\]\\((([^\\)#]+)?#?([^\\)]+)?)\\)") + return re.FindAllStringSubmatch(markdown, -1) +} + +// getConfluenceLink build (to be) link for Conflunce, and tries to verify from API if there's real link available +func getConfluenceLink(api *confluence.API, space, title string, collectedErrors error) (string, error) { + link := fmt.Sprintf("%s/display/%s/%s", api.BaseURL, space, url.QueryEscape(title)) + confluencePage, err := api.FindPage(space, title) + if err != nil { + collectedErrors = fmt.Errorf("%w\n "+err.Error(), collectedErrors) + } else if confluencePage != nil { + // Needs baseURL, as REST api response URL doesn't contain subpath ir confluence is server from that + link = api.BaseURL + confluencePage.Links.Full + } + + return link, collectedErrors +} diff --git a/pkg/mark/link_test.go b/pkg/mark/link_test.go new file mode 100644 index 0000000..2b84de2 --- /dev/null +++ b/pkg/mark/link_test.go @@ -0,0 +1,33 @@ +package mark + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLinkFind(t *testing.T) { + markdown := ` + [example1](../path/to/example.md#second-heading) + [example2](../path/to/example.md) + [example3](#heading-in-document) + [Text link that should be put as attachment](../path/to/example.txt) + [Image link that should be put as attachment](../path/to/example.png) + ` + + links := collectLinksFromMarkdown(markdown) + + assert.Equal(t, "../path/to/example.md#second-heading", links[0][1]) + assert.Equal(t, "../path/to/example.md", links[0][2]) + assert.Equal(t, "second-heading", links[0][3]) + + assert.Equal(t, "../path/to/example.md", links[1][1]) + assert.Equal(t, "../path/to/example.md", links[1][2]) + assert.Equal(t, "", links[1][3]) + + assert.Equal(t, "#heading-in-document", links[2][1]) + assert.Equal(t, "", links[2][2]) + assert.Equal(t, "heading-in-document", links[2][3]) + + assert.Equal(t, len(links), 5) +}