|
1 | 1 | package markdownflavor |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "regexp" |
5 | 6 | "sort" |
6 | 7 |
|
@@ -45,6 +46,10 @@ type HeadingIDExtra struct { |
45 | 46 | AttrEnd int // byte offset one past '}' |
46 | 47 | } |
47 | 48 |
|
| 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 | + |
48 | 53 | // bareURLPattern mirrors goldmark's linkify http/https/ftp URL regex |
49 | 54 | // closely enough to catch bare URLs in text. Anchors removed so it can |
50 | 55 | // 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 { |
92 | 97 | out = append(out, detectBareURLs(f)...) |
93 | 98 | } |
94 | 99 |
|
| 100 | + if keep(FeatureGitHubAlerts) { |
| 101 | + out = append(out, detectGitHubAlerts(f)...) |
| 102 | + } |
| 103 | + |
95 | 104 | sort.SliceStable(out, func(i, j int) bool { |
96 | 105 | return out[i].Start < out[j].Start |
97 | 106 | }) |
@@ -488,3 +497,35 @@ func insideNonBareContext(n ast.Node) bool { |
488 | 497 | } |
489 | 498 | return false |
490 | 499 | } |
| 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 | +} |
0 commit comments