-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathmultiple-emphasis-patterns.ts
More file actions
100 lines (83 loc) · 3.35 KB
/
multiple-emphasis-patterns.ts
File metadata and controls
100 lines (83 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'
interface Frontmatter {
autogenerated?: boolean
[key: string]: any
}
export const multipleEmphasisPatterns: Rule = {
names: ['GHD050', 'multiple-emphasis-patterns'],
description: 'Do not use more than one emphasis/strong, italics, or uppercase for a string',
tags: ['formatting', 'emphasis', 'style'],
severity: 'warning',
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data as Frontmatter
if (fm && fm.autogenerated) return
const lines = params.lines
let inCodeBlock = false
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
// Track code block state
if (line.trim().startsWith('```')) {
inCodeBlock = !inCodeBlock
continue
}
// Skip code blocks and indented code
if (inCodeBlock || line.trim().startsWith(' ')) continue
// Check for multiple emphasis patterns
checkMultipleEmphasis(line, lineNumber, onError)
}
},
}
/**
* Check for multiple emphasis types in a single text segment
*/
function checkMultipleEmphasis(line: string, lineNumber: number, onError: RuleErrorCallback): void {
// Focus on the clearest violations of the style guide
const multipleEmphasisPatterns: Array<{ regex: RegExp; types: string[] }> = [
// Bold + italic combinations (***text***)
{ regex: /\*\*\*([^*]+)\*\*\*/g, types: ['bold', 'italic'] },
{ regex: /___([^_]+)___/g, types: ['bold', 'italic'] },
// Bold with code nested inside
{ regex: /\*\*([^*]*`[^`]+`[^*]*)\*\*/g, types: ['bold', 'code'] },
{ regex: /__([^_]*`[^`]+`[^_]*)__/g, types: ['bold', 'code'] },
// Code with bold nested inside
{ regex: /`([^`]*\*\*[^*]+\*\*[^`]*)`/g, types: ['code', 'bold'] },
{ regex: /`([^`]*__[^_]+__[^`]*)`/g, types: ['code', 'bold'] },
]
for (const pattern of multipleEmphasisPatterns) {
let match
while ((match = pattern.regex.exec(line)) !== null) {
// Skip if this is likely intentional or very short
if (shouldSkipMatch(match[0], match[1])) continue
const range = getRange(line, match[0])
addError(
onError,
lineNumber,
`Do not use multiple emphasis types in a single string: ${pattern.types.join(' + ')}`,
line,
range,
null, // No auto-fix as this requires editorial judgment
)
}
}
}
/**
* Determine if a match should be skipped (likely intentional formatting)
*/
function shouldSkipMatch(fullMatch: string, content: string): boolean {
// Skip common false positives
if (!content) return true
// Skip very short content (likely intentional single chars)
if (content.trim().length < 2) return true
// Skip if it's mostly code-like content (constants, variables)
if (/^[A-Z_][A-Z0-9_]*$/.test(content.trim())) return true
// Skip file extensions or URLs
if (/\.[a-z]{2,4}$/i.test(content.trim()) || /https?:\/\//.test(content)) return true
return false
}