diff --git a/pkg/mdformatter/linktransformer/link.go b/pkg/mdformatter/linktransformer/link.go index 5fa029a..2f6044f 100644 --- a/pkg/mdformatter/linktransformer/link.go +++ b/pkg/mdformatter/linktransformer/link.go @@ -129,7 +129,7 @@ type validator struct { } type futureKey struct { - filepath, dest string + filepath, dest, lineNumbers string } type futureResult struct { @@ -234,7 +234,7 @@ func MustNewValidator(logger log.Logger, linksValidateConfig []byte, anchorDir s } func (v *validator) TransformDestination(ctx mdformatter.SourceContext, destination []byte) (_ []byte, err error) { - v.visit(ctx.Filepath, string(destination)) + v.visit(ctx.Filepath, string(destination), ctx.LineNumbers) return destination, nil } @@ -266,19 +266,19 @@ func (v *validator) Close(ctx mdformatter.SourceContext) error { f := v.destFutures[k] if err := f.resultFn(); err != nil { if f.cases == 1 { - merr.Add(errors.Wrapf(err, "%v", path)) + merr.Add(errors.Wrapf(err, "%v:%v", path, k.lineNumbers)) continue } - merr.Add(errors.Wrapf(err, "%v (%v occurrences)", path, f.cases)) + merr.Add(errors.Wrapf(err, "%v:%v (%v occurrences)", path, k.lineNumbers, f.cases)) } } return merr.Err() } -func (v *validator) visit(filepath string, dest string) { +func (v *validator) visit(filepath string, dest string, lineNumbers string) { v.futureMu.Lock() defer v.futureMu.Unlock() - k := futureKey{filepath: filepath, dest: dest} + k := futureKey{filepath: filepath, dest: dest, lineNumbers: lineNumbers} if _, ok := v.destFutures[k]; ok { v.destFutures[k].cases++ return diff --git a/pkg/mdformatter/linktransformer/link_test.go b/pkg/mdformatter/linktransformer/link_test.go index d1d9a49..7407c8c 100644 --- a/pkg/mdformatter/linktransformer/link_test.go +++ b/pkg/mdformatter/linktransformer/link_test.go @@ -247,10 +247,10 @@ func TestValidator_TransformDestination(t *testing.T) { testutil.NotOk(t, err) testutil.Equals(t, fmt.Sprintf("%v: 4 errors: "+ - "%v: link ../test2/invalid-local-links.md, normalized to: %v/repo/docs/test2/invalid-local-links.md: file not found; "+ - "%v: link ../test/invalid-local-links.md#not-yolo, normalized to: link %v/repo/docs/test/invalid-local-links.md#not-yolo, existing ids: [yolo]: file exists, but does not have such id; "+ - "%v: link ../test/doc.md, normalized to: %v/repo/docs/test/doc.md: file not found; "+ - "%v: link #not-yolo, normalized to: link %v/repo/docs/test/invalid-local-links.md#not-yolo, existing ids: [yolo]: file exists, but does not have such id", + "%v:3: link ../test2/invalid-local-links.md, normalized to: %v/repo/docs/test2/invalid-local-links.md: file not found; "+ + "%v:3: link ../test/invalid-local-links.md#not-yolo, normalized to: link %v/repo/docs/test/invalid-local-links.md#not-yolo, existing ids: [yolo]: file exists, but does not have such id; "+ + "%v:3: link ../test/doc.md, normalized to: %v/repo/docs/test/doc.md: file not found; "+ + "%v:3: link #not-yolo, normalized to: link %v/repo/docs/test/invalid-local-links.md#not-yolo, existing ids: [yolo]: file exists, but does not have such id", tmpDir+filePath, relDirPath+filePath, tmpDir, relDirPath+filePath, tmpDir, relDirPath+filePath, tmpDir, relDirPath+filePath, tmpDir), err.Error()) }) @@ -271,7 +271,7 @@ func TestValidator_TransformDestination(t *testing.T) { MustNewValidator(logger, []byte(""), anchorDir), )) testutil.NotOk(t, err) - testutil.Equals(t, fmt.Sprintf("%v%v: %v%v: \"https://bwplotka.dev/does-not-exists\" not accessible; status code 404: Not Found", tmpDir, filePath, relDirPath, filePath), err.Error()) + testutil.Equals(t, fmt.Sprintf("%v%v: %v%v:1: \"https://bwplotka.dev/does-not-exists\" not accessible; status code 404: Not Found", tmpDir, filePath, relDirPath, filePath), err.Error()) }) t.Run("check valid & 404 link with validate config", func(t *testing.T) { diff --git a/pkg/mdformatter/mdformatter.go b/pkg/mdformatter/mdformatter.go index 87cf5a7..71793f3 100644 --- a/pkg/mdformatter/mdformatter.go +++ b/pkg/mdformatter/mdformatter.go @@ -30,7 +30,8 @@ import ( type SourceContext struct { context.Context - Filepath string + Filepath string + LineNumbers string } type FrontMatterTransformer interface { @@ -348,6 +349,7 @@ func (f *Formatter) Format(file *os.File, out io.Writer) error { wrapped: markdown.NewRenderer(), sourceCtx: sourceCtx, link: f.link, cb: f.cb, + frontMatterLen: len(frontMatter), } if err := goldmark.New( goldmark.WithExtensions(extension.GFM), diff --git a/pkg/mdformatter/transformer.go b/pkg/mdformatter/transformer.go index 4ec8d10..09037fa 100644 --- a/pkg/mdformatter/transformer.go +++ b/pkg/mdformatter/transformer.go @@ -6,6 +6,8 @@ package mdformatter import ( "bytes" "io" + "regexp" + "strconv" "github.com/efficientgo/tools/core/pkg/merrors" "github.com/yuin/goldmark/ast" @@ -29,8 +31,9 @@ type transformer struct { sourceCtx SourceContext - link LinkTransformer - cb CodeBlockTransformer + link LinkTransformer + cb CodeBlockTransformer + frontMatterLen int } func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { @@ -75,6 +78,7 @@ func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { if token.Attr[i].Key != "src" { continue } + t.sourceCtx.LineNumbers = getLinkLines(source, []byte(token.Attr[i].Val), t.frontMatterLen) dest, err := t.link.TransformDestination(t.sourceCtx, []byte(token.Attr[i].Val)) if err != nil { return ast.WalkStop, err @@ -87,6 +91,7 @@ func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { if token.Attr[i].Key != "href" { continue } + t.sourceCtx.LineNumbers = getLinkLines(source, []byte(token.Attr[i].Val), t.frontMatterLen) dest, err := t.link.TransformDestination(t.sourceCtx, []byte(token.Attr[i].Val)) if err != nil { return ast.WalkStop, err @@ -111,6 +116,7 @@ func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { if !entering || t.link == nil { return ast.WalkSkipChildren, nil } + t.sourceCtx.LineNumbers = getLinkLines(source, typedNode.Destination, t.frontMatterLen) typedNode.Destination, err = t.link.TransformDestination(t.sourceCtx, typedNode.Destination) if err != nil { return ast.WalkStop, err @@ -119,6 +125,7 @@ func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { if !entering || t.link == nil || typedNode.AutoLinkType != ast.AutoLinkURL { return ast.WalkSkipChildren, nil } + t.sourceCtx.LineNumbers = getLinkLines(source, typedNode.URL(source), t.frontMatterLen) dest, err := t.link.TransformDestination(t.sourceCtx, typedNode.URL(source)) if err != nil { return ast.WalkStop, err @@ -135,6 +142,7 @@ func (t *transformer) Render(w io.Writer, source []byte, node ast.Node) error { if !entering || t.link == nil { return ast.WalkSkipChildren, nil } + t.sourceCtx.LineNumbers = getLinkLines(source, typedNode.Destination, t.frontMatterLen) typedNode.Destination, err = t.link.TransformDestination(t.sourceCtx, typedNode.Destination) if err != nil { return ast.WalkStop, err @@ -178,3 +186,28 @@ func replaceContent(b *ast.BaseBlock, lastSegmentStop int, content []byte) { s.Append(text.NewSegment(lastSegmentStop, lastSegmentStop+len(content))) b.SetLines(s) } + +// getLinkLines returns line numbers in source where link is present. +func getLinkLines(source []byte, link []byte, lenfm int) string { + var targetLines string + sourceLines := bytes.Split(source, []byte("\n")) + // frontMatter is present so would need to account for `---` lines. + if lenfm > 0 { + lenfm += 2 + } + // Using regex, as two links may have same host but diff params. Same in case of local links. + linkRe := regexp.MustCompile(`(^|[^/\-~&=#?@%a-zA-Z0-9])` + string(link) + `($|[^/\-~&=#?@%a-zA-Z0-9])`) + for i, line := range sourceLines { + if linkRe.Match(line) { + // Easier to just return int slice, but then cannot use it in futureKey. + // https://golang.org/ref/spec#Map_types. + add := strconv.Itoa(i + 1 + lenfm) + if targetLines != "" { + add = "," + strconv.Itoa(i+1+lenfm) + } + targetLines += add + } + } + // If same link is found multiple times returns string like *,*,*... + return targetLines +}