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