Fix replacing relative links, fix #43

This commit is contained in:
Egor Kovetskiy 2020-12-04 00:28:52 +03:00
parent f6e542c6c2
commit d4008a5b72
3 changed files with 174 additions and 82 deletions

View File

@ -213,11 +213,12 @@ func main() {
}
}
links, err := mark.ResolveRelativeLinks(api, markdown, ".")
links, err := mark.ResolveRelativeLinks(api, meta, markdown, ".")
if err != nil {
log.Fatalf(err, "unable to resolve relative links")
}
markdown = mark.ReplaceRelativeLinks(markdown, links)
markdown = mark.SubstituteLinks(markdown, links)
if dryRun {
compileOnly = true

View File

@ -10,96 +10,169 @@ import (
"regexp"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
type Link struct {
MDLink string
Link string
type LinkSubstitution struct {
From string
To string
}
type markdownLink struct {
full string
filename string
hash 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,
meta *Meta,
markdown []byte,
base string,
) (links []Link, collectedErrors error) {
currentMarkdownMetadata, onlyMarkdown, err := ExtractMeta(markdown)
) ([]LinkSubstitution, error) {
matches := parseLinks(string(markdown))
links := []LinkSubstitution{}
for _, match := range matches {
log.Tracef(
nil,
"found a relative link: full=%s filename=%s hash=%s",
match.full,
match.filename,
match.hash,
)
resolved, err := resolveLink(api, base, match)
if err != nil {
return links, fmt.Errorf("unable to get metadata from handled markdown file. Error %w", err)
return nil, karma.Format(err, "resolve link: %q", match.full)
}
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)
if resolved == "" {
continue
}
link.Link, collectedErrors = getConfluenceLink(api, meta.Space, meta.Title, collectedErrors)
}
links = append(links, LinkSubstitution{
From: match.full,
To: resolved,
})
}
if len(element[3]) > 0 {
link.Link = currentPageLinkString + "#" + element[2]
return links, nil
}
links = append(links, link)
}
return links, collectedErrors
func resolveLink(
api *confluence.API,
base string,
link markdownLink,
) (string, error) {
var result string
if len(link.filename) > 0 {
filepath := filepath.Join(base, link.filename)
if _, err := os.Stat(filepath); err != nil {
return "", nil
}
// 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)),
linkContents, err := ioutil.ReadFile(filepath)
if err != nil {
return "", karma.Format(err, "read file: %s", filepath)
}
// This helps to determine if found link points to file that's
// not markdown or have mark required metadata
linkMeta, _, err := ExtractMeta(linkContents)
if err != nil {
log.Errorf(
err,
"unable to extract metadata from %q; ignoring the relative link",
filepath,
)
return "", nil
}
if linkMeta == nil {
return "", nil
}
result, err = getConfluenceLink(api, linkMeta.Space, linkMeta.Title)
if err != nil {
return "", karma.Format(
err,
"find confluence page: %s / %s / %s",
filepath,
linkMeta.Space,
linkMeta.Title,
)
}
if result == "" {
return "", nil
}
}
if len(link.hash) > 0 {
result = result + "#" + link.hash
}
return result, nil
}
func SubstituteLinks(markdown []byte, links []LinkSubstitution) []byte {
for _, link := range links {
if link.From == link.To {
continue
}
log.Tracef(nil, "substitute link: %q -> %q", link.From, link.To)
markdown = bytes.ReplaceAll(
markdown,
[]byte(fmt.Sprintf("](%s)", link.From)),
[]byte(fmt.Sprintf("](%s)", link.To)),
)
}
return markdown
}
// collectLinksFromMarkdown collects all links from given markdown file
// (including images and external links)
func collectLinksFromMarkdown(markdown string) [][]string {
func parseLinks(markdown string) []markdownLink {
re := regexp.MustCompile("\\[[^\\]]+\\]\\((([^\\)#]+)?#?([^\\)]+)?)\\)")
return re.FindAllStringSubmatch(markdown, -1)
matches := re.FindAllStringSubmatch(markdown, -1)
links := make([]markdownLink, len(matches))
for i, match := range matches {
links[i] = markdownLink{
full: match[1],
filename: match[2],
hash: match[3],
}
}
// 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)
return links
}
// 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) (string, error) {
link := fmt.Sprintf(
"%s/display/%s/%s",
api.BaseURL,
space,
url.QueryEscape(title),
)
page, 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 "", karma.Format(err, "api: find page")
}
return link, collectedErrors
if page != nil {
// Needs baseURL, as REST api response URL doesn't contain subpath ir
// confluence is server from that
link = api.BaseURL + page.Links.Full
}
return link, nil
}

View File

@ -6,28 +6,46 @@ import (
"github.com/stretchr/testify/assert"
)
func TestLinkFind(t *testing.T) {
func TestParseLinks(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)
[relative link without dots](relative-link-without-dots.md)
[relative link without dots but with hash](relative-link-without-dots-but-with-hash.md#hash)
`
links := collectLinksFromMarkdown(markdown)
links := parseLinks(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#second-heading", links[0].full)
assert.Equal(t, "../path/to/example.md", links[0].filename)
assert.Equal(t, "second-heading", links[0].hash)
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, "../path/to/example.md", links[1].full)
assert.Equal(t, "../path/to/example.md", links[1].filename)
assert.Equal(t, "", links[1].hash)
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, "#heading-in-document", links[2].full)
assert.Equal(t, "", links[2].filename)
assert.Equal(t, "heading-in-document", links[2].hash)
assert.Equal(t, len(links), 5)
assert.Equal(t, "../path/to/example.txt", links[3].full)
assert.Equal(t, "../path/to/example.txt", links[3].filename)
assert.Equal(t, "", links[3].hash)
assert.Equal(t, "../path/to/example.png", links[4].full)
assert.Equal(t, "../path/to/example.png", links[4].filename)
assert.Equal(t, "", links[4].hash)
assert.Equal(t, "relative-link-without-dots.md", links[5].full)
assert.Equal(t, "relative-link-without-dots.md", links[5].filename)
assert.Equal(t, "", links[5].hash)
assert.Equal(t, "relative-link-without-dots-but-with-hash.md#hash", links[6].full)
assert.Equal(t, "relative-link-without-dots-but-with-hash.md", links[6].filename)
assert.Equal(t, "hash", links[6].hash)
assert.Equal(t, len(links), 7)
}