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 {
|
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
|
||||||
|
209
pkg/mark/link.go
209
pkg/mark/link.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user