diff --git a/docs/policy/oss.md b/docs/policy/oss.md index 42b9ee7..89e9fca 100644 --- a/docs/policy/oss.md +++ b/docs/policy/oss.md @@ -118,8 +118,7 @@ Each entry presents these five fields in order, with blank lines between them: by ` | `, and ending with a period. Label local standard texts by license name, such as `MIT` or `Apache 2.0`. Label upstream files by their actual filenames, such as `COPYRIGHT`, `LICENSE`, `NOTICE`, or `PATENTS`. -5. **Source repository link**, rendered as a Markdown link labeled `Source` to - the exact commit URL, followed by a period. +5. **Source repository link**, rendered exactly as `[Source](COMMIT_URL).`. Use the upstream [attribution example](https://gitea.maximumdirect.net/eric/policy/src/commit/a469273adda336625f96b9d769ea27b33999f146/examples/licensing/attribution.md) to create the project’s `docs/oss/attribution.md`. diff --git a/internal/doccheck/doccheck_test.go b/internal/doccheck/doccheck_test.go index 5913762..daaab2d 100644 --- a/internal/doccheck/doccheck_test.go +++ b/internal/doccheck/doccheck_test.go @@ -12,6 +12,46 @@ import ( ) var markdownLinkPattern = regexp.MustCompile(`\[[^\]]+\]\(([^)]+)\)`) +var markdownBackticks = regexp.MustCompile("`+") + +// documentationLinks excludes literal examples enclosed by matching backtick +// runs before applying the existing lightweight link check. +func documentationLinks(markdown string) [][]string { + runs := markdownBackticks.FindAllStringIndex(markdown, -1) + text := []byte(markdown) + for i := 0; i < len(runs); i++ { + width := runs[i][1] - runs[i][0] + for j := i + 1; j < len(runs); j++ { + if runs[j][1]-runs[j][0] != width { + continue + } + for k := runs[i][0]; k < runs[j][1]; k++ { + text[k] = ' ' + } + i = j + break + } + } + return markdownLinkPattern.FindAllStringSubmatch(string(text), -1) +} + +func TestDocumentationLinksIgnoreCodeExamples(t *testing.T) { + for _, example := range []string{ + "`[Source](COMMIT_URL).`", + "``[Source](COMMIT_URL) with a ` character``", + "```markdown\n[Source](COMMIT_URL)\n```", + } { + t.Run(example, func(t *testing.T) { + matches := documentationLinks("[before](before.md) " + example + " [after](after.md)") + if len(matches) != 2 || matches[0][1] != "before.md" || matches[1][1] != "after.md" { + t.Fatalf("links = %#v, want only the surrounding real links", matches) + } + }) + } + if matches := documentationLinks("unmatched ` [real](real.md)"); len(matches) != 1 || matches[0][1] != "real.md" { + t.Fatalf("unmatched backtick hid a real link: %#v", matches) + } +} func TestDocumentationLinksResolve(t *testing.T) { root := repositoryRoot(t) @@ -163,7 +203,7 @@ func walkDocumentationLinks(t *testing.T, root, directory string) { if err != nil { return err } - for _, match := range markdownLinkPattern.FindAllStringSubmatch(string(data), -1) { + for _, match := range documentationLinks(string(data)) { target := strings.TrimSpace(match[1]) if !isLocalDocumentationTarget(target) { continue