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 { if err != nil {
log.Fatalf(err, "unable to resolve relative links") log.Fatalf(err, "unable to resolve relative links")
} }
markdown = mark.ReplaceRelativeLinks(markdown, links)
markdown = mark.SubstituteLinks(markdown, links)
if dryRun { if dryRun {
compileOnly = true compileOnly = true

View File

@ -10,96 +10,169 @@ import (
"regexp" "regexp"
"github.com/kovetskiy/mark/pkg/confluence" "github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
) )
type Link struct { type LinkSubstitution struct {
MDLink string From string
Link 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( func ResolveRelativeLinks(
api *confluence.API, api *confluence.API,
meta *Meta,
markdown []byte, markdown []byte,
base string, base string,
) (links []Link, collectedErrors error) { ) ([]LinkSubstitution, error) {
currentMarkdownMetadata, onlyMarkdown, err := ExtractMeta(markdown) matches := parseLinks(string(markdown))
if err != nil {
return links, fmt.Errorf("unable to get metadata from handled markdown file. Error %w", err) 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 nil, karma.Format(err, "resolve link: %q", match.full)
}
if resolved == "" {
continue
}
links = append(links, LinkSubstitution{
From: match.full,
To: resolved,
})
} }
currentPageLinkString, collectedErrors := getConfluenceLink(api, currentMarkdownMetadata.Space, currentMarkdownMetadata.Title, collectedErrors) return links, nil
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 func resolveLink(
// directory structure) with links working in Confluence api *confluence.API,
func ReplaceRelativeLinks(markdown []byte, links []Link) []byte { 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
}
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 { 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 = bytes.ReplaceAll(
markdown, markdown,
[]byte(fmt.Sprintf("](%s)", link.MDLink)), []byte(fmt.Sprintf("](%s)", link.From)),
[]byte(fmt.Sprintf("](%s)", link.Link)), []byte(fmt.Sprintf("](%s)", link.To)),
) )
} }
return markdown return markdown
} }
// collectLinksFromMarkdown collects all links from given markdown file func parseLinks(markdown string) []markdownLink {
// (including images and external links)
func collectLinksFromMarkdown(markdown string) [][]string {
re := regexp.MustCompile("\\[[^\\]]+\\]\\((([^\\)#]+)?#?([^\\)]+)?)\\)") re := regexp.MustCompile("\\[[^\\]]+\\]\\((([^\\)#]+)?#?([^\\)]+)?)\\)")
return re.FindAllStringSubmatch(markdown, -1) matches := re.FindAllStringSubmatch(markdown, -1)
}
// getConfluenceLink build (to be) link for Conflunce, and tries to verify from API if there's real link available links := make([]markdownLink, len(matches))
func getConfluenceLink(api *confluence.API, space, title string, collectedErrors error) (string, error) { for i, match := range matches {
link := fmt.Sprintf("%s/display/%s/%s", api.BaseURL, space, url.QueryEscape(title)) links[i] = markdownLink{
confluencePage, err := api.FindPage(space, title) full: match[1],
if err != nil { filename: match[2],
collectedErrors = fmt.Errorf("%w\n "+err.Error(), collectedErrors) hash: match[3],
} 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 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 {
return "", karma.Format(err, "api: find page")
}
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" "github.com/stretchr/testify/assert"
) )
func TestLinkFind(t *testing.T) { func TestParseLinks(t *testing.T) {
markdown := ` markdown := `
[example1](../path/to/example.md#second-heading) [example1](../path/to/example.md#second-heading)
[example2](../path/to/example.md) [example2](../path/to/example.md)
[example3](#heading-in-document) [example3](#heading-in-document)
[Text link that should be put as attachment](../path/to/example.txt) [Text link that should be put as attachment](../path/to/example.txt)
[Image link that should be put as attachment](../path/to/example.png) [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#second-heading", links[0].full)
assert.Equal(t, "../path/to/example.md", links[0][2]) assert.Equal(t, "../path/to/example.md", links[0].filename)
assert.Equal(t, "second-heading", links[0][3]) 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].full)
assert.Equal(t, "../path/to/example.md", links[1][2]) assert.Equal(t, "../path/to/example.md", links[1].filename)
assert.Equal(t, "", links[1][3]) assert.Equal(t, "", links[1].hash)
assert.Equal(t, "#heading-in-document", links[2][1]) assert.Equal(t, "#heading-in-document", links[2].full)
assert.Equal(t, "", links[2][2]) assert.Equal(t, "", links[2].filename)
assert.Equal(t, "heading-in-document", links[2][3]) 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)
} }