Skip to content

Commit afe06e6

Browse files
authored
Merge PR #153: Plan 87: add GitHub Alerts detection and fix to MDS034
2 parents 22695b4 + 62670fc commit afe06e6

14 files changed

Lines changed: 338 additions & 26 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/mdsmith
22
eval/corpus/config.local.yml
33
coverage.out
4+
cover.out

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ footer: |
4444
| 84 | 🔲 | [Symlink default-deny for file discovery](plan/84_symlink-default-deny.md) |
4545
| 85 | 🔳 | [Increase test coverage to 95% by extracting shared rule helpers](plan/85_coverage-to-95-percent.md) |
4646
| 86 | 🔳 | [Markdown flavor validation](plan/86_markdown-flavor-validation.md) |
47-
| 87 | 🔲 | [Flavor validation for GitHub Alerts](plan/87_markdown-flavor-github-alerts.md) |
47+
| 87 | | [Flavor validation for GitHub Alerts](plan/87_markdown-flavor-github-alerts.md) |
4848
| 88 || [TOC directive migration aid](plan/88_toc-directive-migration.md) |
4949
| 89 | 🔲 | [TOC generator directive and MDS035 auto-fix](plan/89_toc-generator-directive.md) |
5050
<?/catalog?>

internal/rules/MDS034-markdown-flavor/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ flavor does not render.
1515
- **Name**: `markdown-flavor`
1616
- **Status**: ready
1717
- **Default**: disabled
18-
- **Fixable**: no (fix pipeline lands in a follow-up)
18+
- **Fixable**: partially (GitHub Alerts only)
1919
- **Implementation**:
2020
[source](./)
2121
- **Category**: meta
@@ -73,7 +73,7 @@ rules:
7373
7474
## Detected features
7575
76-
MDS034 tracks twelve syntax features whose
76+
MDS034 tracks thirteen syntax features whose
7777
support varies across Markdown flavors.
7878
7979
Eleven features are detected from the goldmark AST
@@ -89,6 +89,13 @@ detector scans text nodes from the main parse for
8989
URL-shaped text. It skips links, autolinks, code
9090
spans, and code blocks.
9191
92+
GitHub Alerts are detected from the CommonMark
93+
AST. The detector matches the five GFM tokens
94+
(`NOTE`, `TIP`, `IMPORTANT`, `WARNING`,
95+
`CAUTION`) on the first line of a blockquote
96+
paragraph. Matching is case-sensitive per the
97+
GFM spec.
98+
9299
`flavor: any` accepts every feature and is omitted
93100
from the table below.
94101

@@ -106,6 +113,7 @@ from the table below.
106113
| math blocks | no | no | no | yes | no | yes | yes |
107114
| inline math | no | no | no | yes | no | yes | yes |
108115
| abbreviations | no | no | no | no | yes | yes | no |
116+
| github alerts | no | yes | no | no | no | no | no |
109117

110118
## Examples
111119

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
settings:
3+
flavor: commonmark
4+
diagnostics:
5+
- line: 3
6+
column: 1
7+
message: "github alerts are not supported by commonmark"
8+
---
9+
# Alerts
10+
11+
> [!NOTE]
12+
> Something to remember.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
settings:
3+
flavor: goldmark
4+
diagnostics:
5+
- line: 3
6+
column: 1
7+
message: "github alerts are not supported by goldmark"
8+
---
9+
# Alerts
10+
11+
> [!WARNING]
12+
> Something to remember.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Alerts
2+
3+
> Something to remember.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Alerts
2+
3+
> Something to remember.

internal/rules/markdownflavor/detect.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package markdownflavor
22

33
import (
4+
"bytes"
45
"regexp"
56
"sort"
67

@@ -45,6 +46,10 @@ type HeadingIDExtra struct {
4546
AttrEnd int // byte offset one past '}'
4647
}
4748

49+
// alertTokenRe matches the exact content of a GitHub Alert marker line
50+
// inside a blockquote (case-sensitive per GFM spec).
51+
var alertTokenRe = regexp.MustCompile(`^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$`)
52+
4853
// bareURLPattern mirrors goldmark's linkify http/https/ftp URL regex
4954
// closely enough to catch bare URLs in text. Anchors removed so it can
5055
// match anywhere inside a Text segment. The TLD class accepts both
@@ -92,6 +97,10 @@ func DetectFiltered(f *lint.File, accept func(Feature) bool) []Finding {
9297
out = append(out, detectBareURLs(f)...)
9398
}
9499

100+
if keep(FeatureGitHubAlerts) {
101+
out = append(out, detectGitHubAlerts(f)...)
102+
}
103+
95104
sort.SliceStable(out, func(i, j int) bool {
96105
return out[i].Start < out[j].Start
97106
})
@@ -488,3 +497,35 @@ func insideNonBareContext(n ast.Node) bool {
488497
}
489498
return false
490499
}
500+
501+
// detectGitHubAlerts walks f.AST for Blockquote nodes whose first paragraph
502+
// child starts with a GFM alert token (e.g. [!NOTE]).
503+
func detectGitHubAlerts(f *lint.File) []Finding {
504+
var findings []Finding
505+
_ = ast.Walk(f.AST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
506+
if !entering {
507+
return ast.WalkContinue, nil
508+
}
509+
bq, ok := n.(*ast.Blockquote)
510+
if !ok {
511+
return ast.WalkContinue, nil
512+
}
513+
if isGitHubAlert(bq, f.Source) {
514+
findings = append(findings, blockFinding(f, bq, FeatureGitHubAlerts))
515+
}
516+
return ast.WalkContinue, nil
517+
})
518+
return findings
519+
}
520+
521+
// isGitHubAlert reports whether bq is a GitHub Alert blockquote: its first
522+
// paragraph child's first line matches one of the five GFM alert tokens.
523+
func isGitHubAlert(bq *ast.Blockquote, source []byte) bool {
524+
para, ok := bq.FirstChild().(*ast.Paragraph)
525+
if !ok {
526+
return false
527+
}
528+
seg := para.Lines().At(0)
529+
firstLine := bytes.TrimRight(source[seg.Start:seg.Stop], "\r\n")
530+
return alertTokenRe.Match(firstLine)
531+
}

internal/rules/markdownflavor/detect_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,33 @@ func TestDetectFindingsAreSortedByStart(t *testing.T) {
231231
i-1, fs[i-1], i, fs[i])
232232
}
233233
}
234+
235+
func TestDetectGitHubAlerts(t *testing.T) {
236+
tokens := []string{"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"}
237+
for _, tok := range tokens {
238+
t.Run(tok, func(t *testing.T) {
239+
fs := findings(t, "> [!"+tok+"]\n> Something.\n")
240+
assert.True(t, hasFeature(fs, FeatureGitHubAlerts))
241+
})
242+
}
243+
}
244+
245+
func TestDetectGitHubAlertsLowercaseNoMatch(t *testing.T) {
246+
for _, src := range []string{
247+
"> [!note]\n> text.\n",
248+
"> [!INFO]\n> text.\n",
249+
} {
250+
fs := findings(t, src)
251+
assert.False(t, hasFeature(fs, FeatureGitHubAlerts), "should not match: %q", src)
252+
}
253+
}
254+
255+
func TestDetectGitHubAlertsMixedContent(t *testing.T) {
256+
fs := findings(t, "> [!NOTE]\n> Line one.\n> Line two.\n")
257+
assert.True(t, hasFeature(fs, FeatureGitHubAlerts))
258+
}
259+
260+
func TestDetectGitHubAlertsOnlyLine(t *testing.T) {
261+
fs := findings(t, "> [!WARNING]\n")
262+
assert.True(t, hasFeature(fs, FeatureGitHubAlerts))
263+
}

internal/rules/markdownflavor/features.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const (
108108
FeatureMathBlock
109109
FeatureMathInline
110110
FeatureAbbreviations
111+
FeatureGitHubAlerts
111112
)
112113

113114
// AllFeatures returns every tracked feature in declaration order.
@@ -125,6 +126,7 @@ func AllFeatures() []Feature {
125126
FeatureMathBlock,
126127
FeatureMathInline,
127128
FeatureAbbreviations,
129+
FeatureGitHubAlerts,
128130
}
129131
}
130132

@@ -136,6 +138,8 @@ func (f Feature) Verb() string {
136138
case FeatureStrikethrough, FeatureSuperscript, FeatureSubscript,
137139
FeatureMathInline:
138140
return "is"
141+
case FeatureGitHubAlerts:
142+
return "are"
139143
}
140144
return "are"
141145
}
@@ -167,6 +171,8 @@ func (f Feature) Name() string {
167171
return "inline math"
168172
case FeatureAbbreviations:
169173
return "abbreviations"
174+
case FeatureGitHubAlerts:
175+
return "github alerts"
170176
}
171177
return ""
172178
}
@@ -183,6 +189,7 @@ var support = map[Flavor]map[Feature]bool{
183189
FeatureTaskLists: true,
184190
FeatureStrikethrough: true,
185191
FeatureBareURLAutolinks: true,
192+
FeatureGitHubAlerts: true,
186193
},
187194
FlavorGoldmark: {
188195
FeatureTables: true,

0 commit comments

Comments
 (0)