mirror of https://github.com/mickael-menu/zk
Generate internal links to notes (#32)
parent
083c0dae73
commit
2bb4cbdff4
@ -0,0 +1,25 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mickael-menu/zk/internal/core"
|
||||||
|
"github.com/mickael-menu/zk/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLinkHelper creates a new template helper to generate an internal link
|
||||||
|
// using a LinkFormatter.
|
||||||
|
//
|
||||||
|
// {{link "path/to/note.md" "An interesting subject"}} -> (depends on the LinkFormatter)
|
||||||
|
// [[path/to/note]]
|
||||||
|
// [An interesting subject](path/to/note)
|
||||||
|
func NewLinkHelper(formatter core.LinkFormatter, logger util.Logger) interface{} {
|
||||||
|
return func(path string, opt interface{}) string {
|
||||||
|
title, _ := opt.(string)
|
||||||
|
link, err := formatter(path, title)
|
||||||
|
if err != nil {
|
||||||
|
logger.Err(err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mickael-menu/zk/internal/util/errors"
|
||||||
|
"github.com/mickael-menu/zk/internal/util/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinkFormatter formats internal links according to user configuration.
|
||||||
|
type LinkFormatter func(path string, title string) (string, error)
|
||||||
|
|
||||||
|
// NewLinkFormatter generates a new LinkFormatter from the user Markdown
|
||||||
|
// configuration.
|
||||||
|
func NewLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader) (LinkFormatter, error) {
|
||||||
|
var formatter LinkFormatter
|
||||||
|
var err error
|
||||||
|
switch config.LinkFormat {
|
||||||
|
case "markdown", "":
|
||||||
|
formatter, err = newMarkdownLinkFormatter(config)
|
||||||
|
case "wiki":
|
||||||
|
formatter, err = newWikiLinkFormatter(config)
|
||||||
|
default:
|
||||||
|
formatter, err = newCustomLinkFormatter(config, templateLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(path, title string) (string, error) {
|
||||||
|
if config.LinkDropExtension {
|
||||||
|
path = paths.DropExt(path)
|
||||||
|
}
|
||||||
|
if config.LinkEncodePath {
|
||||||
|
path = strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter(path, title)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMarkdownLinkFormatter(config MarkdownConfig) (LinkFormatter, error) {
|
||||||
|
return func(path, title string) (string, error) {
|
||||||
|
if !config.LinkEncodePath {
|
||||||
|
path = strings.ReplaceAll(path, `\`, `\\`)
|
||||||
|
path = strings.ReplaceAll(path, `)`, `\)`)
|
||||||
|
}
|
||||||
|
title = strings.ReplaceAll(title, `\`, `\\`)
|
||||||
|
title = strings.ReplaceAll(title, `]`, `\]`)
|
||||||
|
return fmt.Sprintf("[%s](%s)", title, path), nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWikiLinkFormatter(config MarkdownConfig) (LinkFormatter, error) {
|
||||||
|
return func(path, title string) (string, error) {
|
||||||
|
if !config.LinkEncodePath {
|
||||||
|
path = strings.ReplaceAll(path, `\`, `\\`)
|
||||||
|
path = strings.ReplaceAll(path, `]]`, `\]]`)
|
||||||
|
}
|
||||||
|
return "[[" + path + "]]", nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCustomLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader) (LinkFormatter, error) {
|
||||||
|
wrap := errors.Wrapperf("failed to render custom link with format: %s", config.LinkFormat)
|
||||||
|
template, err := templateLoader.LoadTemplate(config.LinkFormat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(path, title string) (string, error) {
|
||||||
|
return template.Render(customLinkRenderContext{Path: path, Title: title})
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type customLinkRenderContext struct {
|
||||||
|
Path string
|
||||||
|
Title string
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mickael-menu/zk/internal/util/test/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarkdownLinkFormatter(t *testing.T) {
|
||||||
|
newTester := func(encodePath, dropExtension bool) func(path, title, expected string) {
|
||||||
|
formatter, err := NewLinkFormatter(MarkdownConfig{
|
||||||
|
LinkFormat: "markdown",
|
||||||
|
LinkEncodePath: encodePath,
|
||||||
|
LinkDropExtension: dropExtension,
|
||||||
|
}, &NullTemplateLoader)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
return func(path, title, expected string) {
|
||||||
|
actual, err := formatter(path, title)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test := newTester(false, false)
|
||||||
|
test("path/to note.md", "", "[](path/to note.md)")
|
||||||
|
test("", "", "[]()")
|
||||||
|
test("path/to note.md", "An interesting subject", "[An interesting subject](path/to note.md)")
|
||||||
|
test(`path/(no\te).md`, `An [interesting] \subject`, `[An [interesting\] \\subject](path/(no\\te\).md)`)
|
||||||
|
test = newTester(true, false)
|
||||||
|
test("path/to note.md", "An interesting subject", "[An interesting subject](path/to%20note.md)")
|
||||||
|
test(`path/(no\te).md`, `An [interesting] \subject`, `[An [interesting\] \\subject](path/%28no%5Cte%29.md)`)
|
||||||
|
test = newTester(false, true)
|
||||||
|
test("path/to note.md", "An interesting subject", "[An interesting subject](path/to note)")
|
||||||
|
test = newTester(true, true)
|
||||||
|
test("path/to note.md", "An interesting subject", "[An interesting subject](path/to%20note)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiLinkFormatter(t *testing.T) {
|
||||||
|
newTester := func(encodePath, dropExtension bool) func(path, title, expected string) {
|
||||||
|
formatter, err := NewLinkFormatter(MarkdownConfig{
|
||||||
|
LinkFormat: "wiki",
|
||||||
|
LinkEncodePath: encodePath,
|
||||||
|
LinkDropExtension: dropExtension,
|
||||||
|
}, &NullTemplateLoader)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
return func(path, title, expected string) {
|
||||||
|
actual, err := formatter(path, title)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test := newTester(false, false)
|
||||||
|
test("", "", "[[]]")
|
||||||
|
test("path/to note.md", "title", "[[path/to note.md]]")
|
||||||
|
test(`path/[no\te].md`, "title", `[[path/[no\\te].md]]`)
|
||||||
|
test(`path/[[no\te]].md`, "title", `[[path/[[no\\te\]].md]]`)
|
||||||
|
test = newTester(true, false)
|
||||||
|
test("path/to note.md", "title", "[[path/to%20note.md]]")
|
||||||
|
test(`path/[no\te].md`, "title", "[[path/%5Bno%5Cte%5D.md]]")
|
||||||
|
test(`path/[[no\te]].md`, "title", "[[path/%5B%5Bno%5Cte%5D%5D.md]]")
|
||||||
|
test = newTester(false, true)
|
||||||
|
test("path/to note.md", "title", "[[path/to note]]")
|
||||||
|
test = newTester(true, true)
|
||||||
|
test("path/to note.md", "title", "[[path/to%20note]]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomLinkFormatter(t *testing.T) {
|
||||||
|
newTester := func(encodePath, dropExtension bool) func(path, title string, expected customLinkRenderContext) {
|
||||||
|
return func(path, title string, expected customLinkRenderContext) {
|
||||||
|
loader := newTemplateLoaderMock()
|
||||||
|
template := loader.SpyString("custom")
|
||||||
|
|
||||||
|
formatter, err := NewLinkFormatter(MarkdownConfig{
|
||||||
|
LinkFormat: "custom",
|
||||||
|
LinkEncodePath: encodePath,
|
||||||
|
LinkDropExtension: dropExtension,
|
||||||
|
}, loader)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
actual, err := formatter(path, title)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, actual, "custom")
|
||||||
|
assert.Equal(t, template.Contexts, []interface{}{expected})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test := newTester(false, false)
|
||||||
|
test("path/to note.md", "", customLinkRenderContext{Path: "path/to note.md"})
|
||||||
|
test("", "", customLinkRenderContext{})
|
||||||
|
test("path/to note.md", "An interesting subject", customLinkRenderContext{
|
||||||
|
Title: "An interesting subject",
|
||||||
|
Path: "path/to note.md",
|
||||||
|
})
|
||||||
|
test(`path/(no\te).md`, `An [interesting] \subject`, customLinkRenderContext{
|
||||||
|
Title: `An [interesting] \subject`,
|
||||||
|
Path: `path/(no\te).md`,
|
||||||
|
})
|
||||||
|
test = newTester(true, false)
|
||||||
|
test("path/to note.md", "An interesting subject", customLinkRenderContext{
|
||||||
|
Title: "An interesting subject",
|
||||||
|
Path: "path/to%20note.md",
|
||||||
|
})
|
||||||
|
test = newTester(false, true)
|
||||||
|
test("path/to note.md", "An interesting subject", customLinkRenderContext{
|
||||||
|
Title: "An interesting subject",
|
||||||
|
Path: "path/to note",
|
||||||
|
})
|
||||||
|
test = newTester(true, true)
|
||||||
|
test("path/to note.md", "An interesting subject", customLinkRenderContext{
|
||||||
|
Title: "An interesting subject",
|
||||||
|
Path: "path/to%20note",
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// lazyStringer implements Stringer and wait for String() to be called the first
|
||||||
|
// time before computing its value.
|
||||||
|
type lazyStringer struct {
|
||||||
|
value *string
|
||||||
|
render func() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLazyStringer(render func() string) *lazyStringer {
|
||||||
|
return &lazyStringer{render: render}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements Stringer.
|
||||||
|
func (s *lazyStringer) String() string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if s.value == nil {
|
||||||
|
str := s.render()
|
||||||
|
s.value = &str
|
||||||
|
}
|
||||||
|
return *s.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *lazyStringer) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(`"` + s.String() + `"`), nil
|
||||||
|
}
|
Loading…
Reference in New Issue