diff --git a/pkg/api/types.go b/pkg/api/types.go index 6a537e28..41b444c5 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -183,7 +183,9 @@ type LocalityDomainValue struct { // This applies only to markdown for links and images. // - A fixed string that will replace the whole original link // destination. - LinkSubstitutes Substitutes + // The keys in the substitution map are matched against documents + // links as exact string matches. + LinkSubstitutes Substitutes `yaml:"linkSubstitutes,omitempty"` // DownloadSubstitutes is an optional map of resource names in this // locality domain and their substitutions. Use it to override the // default downloads naming: @@ -197,7 +199,7 @@ type LocalityDomainValue struct { // - $path: the original path of the resource in this domain (may be empty) // - $uuid: the identifier generated for the downloaded resource // Example expression: $name-$uuid - DownloadSubstitutes Substitutes + DownloadSubstitutes Substitutes `yaml:"downloadSubstitutes,omitempty"` } // Substitutes is map of ... diff --git a/pkg/hugo/processor.go b/pkg/hugo/processor.go index 94f4b545..e9c51793 100755 --- a/pkg/hugo/processor.go +++ b/pkg/hugo/processor.go @@ -43,7 +43,7 @@ func (f *Processor) Process(documentBlob []byte, node *api.Node) ([]byte, error) if err != nil { return nil, err } - if documentBlob, err = mdutil.TransformLinks(contentBytes, func(destination []byte) ([]byte, error) { + if documentBlob, err = mdutil.UpdateLinkRefDestinations(contentBytes, func(destination []byte) ([]byte, error) { return f.rewriteDestination(destination, node.Name) }); err != nil { return nil, err diff --git a/pkg/markdown/util.go b/pkg/markdown/util.go index 47a3ba05..e9422908 100644 --- a/pkg/markdown/util.go +++ b/pkg/markdown/util.go @@ -15,17 +15,68 @@ import ( ) // OnLink is a callback function invoked on each link -// by mardown#TransformLinks +// by mardown#UpdateLinkRefDestinations type OnLink func(destination []byte) ([]byte, error) const ( extensions = parser.CommonExtensions | parser.AutoHeadingIDs ) -//TransformLinks transforms document links destinations, delegating -// the transformation to a callback invoked on each link +func removeDestination(node ast.Node) { + children := node.GetParent().GetChildren() + idx := -1 + for i, p := range children { + if p == node { + idx = i + break + } + } + if idx > -1 { + if link, ok := node.(*ast.Link); ok { + textNode := link.Children[0] + if textNode != nil && len(textNode.AsLeaf().Literal) > 0 { + // if prev sibling is text node, add this link text to it + if idx > 0 { + _n := children[idx-1] + if t, ok := _n.(*ast.Text); ok { + t.Literal = append(t.Literal, textNode.AsLeaf().Literal...) + children = removeNode(children, idx) + node.GetParent().SetChildren(children) + return + } + } + // if next sibling is text node, add this link text to it + if idx < len(children)-1 { + _n := children[idx+1] + if t, ok := _n.(*ast.Text); ok { + t.Literal = append(t.Literal, textNode.AsLeaf().Literal...) + children = removeNode(children, idx) + node.GetParent().SetChildren(children) + return + } + } + node.GetParent().AsContainer().Children[idx] = textNode + return + } + } + if _, ok := node.(*ast.Image); ok { + children = removeNode(children, idx) + node.GetParent().SetChildren(children) + return + } + } +} +func removeNode(n []ast.Node, i int) []ast.Node { + return append(n[:i], n[i+1:]...) +} + +// UpdateLinkRefDestinations changes document links destinations, consulting +// with callback on the destination to use on each link or image in document. +// If a callback returns "" for a destination, this is interpreted as +// request to remove the link destination and leave only the link text or in +// case it's an image - to remvoe it completely. // TODO: failfast vs fault tolerance support -func TransformLinks(documentBlob []byte, callback OnLink) ([]byte, error) { +func UpdateLinkRefDestinations(documentBlob []byte, callback OnLink) ([]byte, error) { mdParser := parser.NewWithExtensions(extensions) document := markdown.Parse(documentBlob, mdParser) ast.WalkFunc(document, func(_node ast.Node, entering bool) ast.WalkStatus { @@ -38,6 +89,10 @@ func TransformLinks(documentBlob []byte, callback OnLink) ([]byte, error) { if destination, err = callback(l.Destination); err != nil { return ast.Terminate } + if destination == nil { + removeDestination(l) + return ast.GoToNext + } l.Destination = destination return ast.GoToNext } @@ -45,6 +100,10 @@ func TransformLinks(documentBlob []byte, callback OnLink) ([]byte, error) { if destination, err = callback(l.Destination); err != nil { return ast.Terminate } + if destination == nil { + removeDestination(l) + return ast.GoToNext + } l.Destination = destination return ast.GoToNext } diff --git a/pkg/markdown/util_test.go b/pkg/markdown/util_test.go index aca523a7..b401b1ae 100644 --- a/pkg/markdown/util_test.go +++ b/pkg/markdown/util_test.go @@ -2,6 +2,11 @@ package markdown import ( "testing" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/ast" + "github.com/gomarkdown/markdown/parser" + "github.com/stretchr/testify/assert" ) func TestStripFrontMatter(t *testing.T) { @@ -73,3 +78,52 @@ title: Core Components }) } } + +func TestRemoveLink(t *testing.T) { + testCases := []struct { + in string + wantLinksCount int + wantImgsCount int + wantTexts []string + }{ + { + `A [a0](b.md) [a1](b.md "c") ![](a.png) B`, + 0, 0, + []string{"A a0", " a1", " ", " B"}, + }, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + mdParser := parser.NewWithExtensions(extensions) + document := markdown.Parse([]byte(tc.in), mdParser) + ast.WalkFunc(document, func(node ast.Node, entering bool) ast.WalkStatus { + if l, ok := node.(*ast.Link); ok { + removeDestination(l) + } + if l, ok := node.(*ast.Image); ok { + removeDestination(l) + } + return ast.GoToNext + }) + var ( + links, images int + texts = make([]string, 0) + ) + ast.WalkFunc(document, func(node ast.Node, entering bool) ast.WalkStatus { + if _, ok := node.(*ast.Link); ok { + links++ + } + if _, ok := node.(*ast.Image); ok { + images++ + } + if t, ok := node.(*ast.Text); ok { + texts = append(texts, string(t.Literal)) + } + return ast.GoToNext + }) + assert.Equal(t, tc.wantLinksCount, links) + assert.Equal(t, tc.wantLinksCount, images) + assert.Equal(t, tc.wantTexts, texts) + }) + } +} diff --git a/pkg/reactor/content_processor.go b/pkg/reactor/content_processor.go index ed1fcd45..9adc1460 100644 --- a/pkg/reactor/content_processor.go +++ b/pkg/reactor/content_processor.go @@ -95,7 +95,7 @@ func (c *NodeContentProcessor) ReconcileLinks(ctx context.Context, node *api.Nod func (c *NodeContentProcessor) reconcileMDLinks(ctx context.Context, docNode *api.Node, contentBytes []byte, contentSourcePath string) ([]byte, error) { var errors *multierror.Error - contentBytes, _ = markdown.TransformLinks(contentBytes, func(destination []byte) ([]byte, error) { + contentBytes, _ = markdown.UpdateLinkRefDestinations(contentBytes, func(destination []byte) ([]byte, error) { var ( _destination string downloadLink string @@ -111,6 +111,9 @@ func (c *NodeContentProcessor) reconcileMDLinks(ctx context.Context, docNode *ap if len(downloadLink) > 0 { c.schedule(ctx, downloadLink, resourceName, contentSourcePath) } + if len(_destination) < 1 { + return nil, nil + } return []byte(_destination), nil }) if c.failFast && errors != nil && errors.Len() > 0 { @@ -171,6 +174,10 @@ func (c *NodeContentProcessor) processLink(ctx context.Context, node *api.Node, if node != nil { ld = resolveLocalityDomain(node, c.localityDomain) } + if absLink = ld.SubstituteLink(absLink); len(absLink) == 0 { + // substitution is a request to remove this link + return "", "", "", nil + } absLink, inLD := ld.MatchPathInLocality(absLink, c.ResourceHandlers) if _a != absLink { klog.V(6).Infof("[%s] Link converted %s -> %s\n", contentSourcePath, _a, absLink) diff --git a/pkg/reactor/localitydomain.go b/pkg/reactor/localitydomain.go index 89169065..9f249472 100644 --- a/pkg/reactor/localitydomain.go +++ b/pkg/reactor/localitydomain.go @@ -195,6 +195,19 @@ func (ld localityDomain) PathInLocality(link string, rhs resourcehandlers.Regist return false } +func (ld localityDomain) SubstituteLink(link string) string { + if len(link) > 0 { + for _, d := range ld { + if len(d.LinkSubstitutes) > 0 { + if s, ok := d.LinkSubstitutes[link]; ok { + return s + } + } + } + } + return link +} + // setLocalityDomainForNode visits all content selectors in the node and its // descendants to build a localityDomain func localityDomainFromNode(node *api.Node, rhs resourcehandlers.Registry) (localityDomain, error) { @@ -299,6 +312,7 @@ func merge(a, b *localityDomainValue) *localityDomainValue { _e[k] = v } } + a.LinkSubstitutes = _e } if len(b.DownloadSubstitutes) > 0 { _e := a.DownloadSubstitutes @@ -309,6 +323,7 @@ func merge(a, b *localityDomainValue) *localityDomainValue { _e[k] = v } } + a.DownloadSubstitutes = _e } return a } diff --git a/pkg/reactor/localitydomain_test.go b/pkg/reactor/localitydomain_test.go index b81496e4..b7823cb8 100644 --- a/pkg/reactor/localitydomain_test.go +++ b/pkg/reactor/localitydomain_test.go @@ -6,6 +6,7 @@ import ( "github.com/gardener/docforge/pkg/resourcehandlers" "github.com/gardener/docforge/pkg/resourcehandlers/github" + "github.com/stretchr/testify/assert" "github.com/gardener/docforge/pkg/api" ) @@ -227,3 +228,42 @@ func Test_SetLocalityDomainForNode(t *testing.T) { }) } } + +func Test_SubstituteLink(t *testing.T) { + testCases := []struct { + link string + substitutes map[string]string + want string + }{ + { + "abc", + map[string]string{ + "abc": "cda", + }, + "cda", + }, + { + "abc", + map[string]string{}, + "abc", + }, + { + "", + map[string]string{ + "abc": "cda", + }, + "", + }, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + ld := localityDomain{ + "": &localityDomainValue{ + LinkSubstitutes: tc.substitutes, + }, + } + got := ld.SubstituteLink(tc.link) + assert.Equal(t, tc.want, got) + }) + } +}