mirror of
https://github.com/kovetskiy/mark.git
synced 2025-04-24 05:42:40 +08:00
Fix replacing relative links, fix #43
This commit is contained in:
parent
f6e542c6c2
commit
d4008a5b72
5
main.go
5
main.go
@ -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
|
||||
|
195
pkg/mark/link.go
195
pkg/mark/link.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user