Skip to content

Commit a0b5eca

Browse files
committed
fix: support repeated markdown headers
Signed-off-by: bwplotka <bwplotka@gmail.com>
1 parent 65d9272 commit a0b5eca

3 files changed

Lines changed: 56 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ We use *breaking* word for marking changes that are not backward compatible (rel
1313
### Added
1414

1515
* [#86](https://github.com/bwplotka/mdox/pull/86) Add configuration options for sending HTTP requests (to help avoid intermittent errors).
16+
* [#180](https://github.com/bwplotka/mdox/pull/180) Allow ignoring/re-anchoring local links via localValidator.
1617

1718
### Fixed
1819

1920
* [#84](https://github.com/bwplotka/mdox/pull/84) Allow quotes in first header.
21+
* [#181](https://github.com/bwplotka/mdox/pull/181) Add support for repeated markdown headers.
2022

2123
## [v0.9.0](https://github.com/bwplotka/mdox/releases/tag/v0.9.0)
2224

pkg/mdformatter/linktransformer/link.go

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -527,50 +527,63 @@ func (l localLinksCache) addRelLinks(localLink string) error {
527527

528528
// File present, cache presence.
529529
ids := make([]string, 0)
530-
531-
var b []byte
530+
idDup := map[string]struct{}{}
532531
reader := bufio.NewReader(file)
532+
533533
for {
534-
b, err = reader.ReadBytes('\n')
534+
line, err := reader.ReadBytes('\n')
535535
if err != nil {
536536
if err != io.EOF {
537537
return fmt.Errorf("failed to read file %v: %w", localLink, err)
538538
}
539539
break
540540
}
541541

542-
if bytes.HasPrefix(b, []byte(`#`)) {
543-
ids = append(ids, toHeaderID(b))
542+
if !bytes.HasPrefix(line, []byte("#")) {
543+
if err == io.EOF {
544+
break
545+
}
546+
continue
547+
}
548+
549+
headerID := toHeaderID(line)
550+
uniqueID := headerID
551+
for i := 1; ; i++ {
552+
if _, exists := idDup[uniqueID]; !exists {
553+
break
554+
}
555+
uniqueID = fmt.Sprintf("%s-%d", headerID, i)
556+
}
557+
idDup[uniqueID] = struct{}{}
558+
ids = append(ids, uniqueID)
559+
560+
if err == io.EOF {
561+
break
544562
}
545563
}
546564

547565
l[localLink] = &ids
548566
return nil
549567
}
550568

569+
var punctuationRe = regexp.MustCompile(`[^\p{L}\p{N}\p{M}-# ]`)
570+
551571
func toHeaderID(header []byte) string {
572+
clean := punctuationRe.ReplaceAll(header, nil)
573+
text := bytes.TrimLeft(bytes.ToLower(clean), "# ")
574+
552575
var id []byte
553-
// Remove punctuation from header except '-' or '#'.
554-
// '\p{L}\p{N}\p{M}' is the Unicode equivalent of '\w', https://www.regular-expressions.info/unicode.html.
555-
punctuation := regexp.MustCompile(`[^\p{L}\p{N}\p{M}-# ]`)
556-
header = punctuation.ReplaceAll(header, []byte(""))
557-
headerText := bytes.TrimLeft(bytes.ToLower(header), "#")
558-
// If header is just punctuation it comes up empty, so it cannot be linked.
559-
if len(headerText) <= 1 {
560-
return ""
561-
}
562-
563-
for _, h := range headerText[1:] {
564-
switch h {
576+
for _, r := range string(text) {
577+
switch r {
565578
case '{':
566579
return string(id)
567-
case ' ', '-':
580+
case ' ', '-', '\n', '\r':
568581
id = append(id, '-')
569582
default:
570-
id = append(id, h)
583+
id = append(id, string(r)...)
571584
}
572585
}
573-
return string(id)
586+
return strings.Trim(string(id), "-")
574587
}
575588

576589
func absLocalLink(anchorDir string, docPath string, destination string) string {

pkg/mdformatter/linktransformer/link_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,27 @@ func TestValidator_TransformDestination(t *testing.T) {
148148
testutil.Ok(t, err)
149149
testutil.Equals(t, 0, len(diff), diff.String())
150150
})
151+
t.Run("check valid link; repeated headers", func(t *testing.T) {
152+
testFile := filepath.Join(tmpDir, "repo", "docs", "test", "valid-link.md")
153+
testutil.Ok(t, os.WriteFile(testFile, []byte(`# Yolo
154+
155+
# Yolo
156+
157+
# Yolo
158+
159+
I should link to [1](#yolo-2) and [2](#yolo-1). Also [3](#yolo).
160+
`), os.ModePerm))
161+
162+
diff, err := mdformatter.IsFormatted(context.TODO(), logger, []string{testFile})
163+
testutil.Ok(t, err)
164+
testutil.Equals(t, 0, len(diff), diff.String())
165+
166+
diff, err = mdformatter.IsFormatted(context.TODO(), logger, []string{testFile}, mdformatter.WithLinkTransformer(
167+
MustNewValidator(logger, []byte(""), anchorDir, nil),
168+
))
169+
testutil.Ok(t, err)
170+
testutil.Equals(t, 0, len(diff), diff.String())
171+
})
151172

152173
t.Run("check valid but same link in diff files", func(t *testing.T) {
153174
testFile := filepath.Join(tmpDir, "repo", "docs", "test", "valid-link.md")

0 commit comments

Comments
 (0)