Skip to content

Commit e88e0a8

Browse files
authored
fix: support repeated markdown headers (#181)
1 parent f0404ad commit e88e0a8

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
@@ -485,50 +485,63 @@ func (l localLinksCache) addRelLinks(localLink string) error {
485485

486486
// File present, cache presence.
487487
ids := make([]string, 0)
488-
489-
var b []byte
488+
idDup := map[string]struct{}{}
490489
reader := bufio.NewReader(file)
490+
491491
for {
492-
b, err = reader.ReadBytes('\n')
492+
line, err := reader.ReadBytes('\n')
493493
if err != nil {
494494
if err != io.EOF {
495495
return fmt.Errorf("failed to read file %v: %w", localLink, err)
496496
}
497497
break
498498
}
499499

500-
if bytes.HasPrefix(b, []byte(`#`)) {
501-
ids = append(ids, toHeaderID(b))
500+
if !bytes.HasPrefix(line, []byte("#")) {
501+
if err == io.EOF {
502+
break
503+
}
504+
continue
505+
}
506+
507+
headerID := toHeaderID(line)
508+
uniqueID := headerID
509+
for i := 1; ; i++ {
510+
if _, exists := idDup[uniqueID]; !exists {
511+
break
512+
}
513+
uniqueID = fmt.Sprintf("%s-%d", headerID, i)
514+
}
515+
idDup[uniqueID] = struct{}{}
516+
ids = append(ids, uniqueID)
517+
518+
if err == io.EOF {
519+
break
502520
}
503521
}
504522

505523
l[localLink] = &ids
506524
return nil
507525
}
508526

527+
var punctuationRe = regexp.MustCompile(`[^\p{L}\p{N}\p{M}-# ]`)
528+
509529
func toHeaderID(header []byte) string {
530+
clean := punctuationRe.ReplaceAll(header, nil)
531+
text := bytes.TrimLeft(bytes.ToLower(clean), "# ")
532+
510533
var id []byte
511-
// Remove punctuation from header except '-' or '#'.
512-
// '\p{L}\p{N}\p{M}' is the Unicode equivalent of '\w', https://www.regular-expressions.info/unicode.html.
513-
punctuation := regexp.MustCompile(`[^\p{L}\p{N}\p{M}-# ]`)
514-
header = punctuation.ReplaceAll(header, []byte(""))
515-
headerText := bytes.TrimLeft(bytes.ToLower(header), "#")
516-
// If header is just punctuation it comes up empty, so it cannot be linked.
517-
if len(headerText) <= 1 {
518-
return ""
519-
}
520-
521-
for _, h := range headerText[1:] {
522-
switch h {
534+
for _, r := range string(text) {
535+
switch r {
523536
case '{':
524537
return string(id)
525-
case ' ', '-':
538+
case ' ', '-', '\n', '\r':
526539
id = append(id, '-')
527540
default:
528-
id = append(id, h)
541+
id = append(id, string(r)...)
529542
}
530543
}
531-
return string(id)
544+
return strings.Trim(string(id), "-")
532545
}
533546

534547
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)