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