|
16 | 16 | '^(' + '|'.join(rf'\{char}+' for char in FORBIDDEN_HEADING_CHARS) + ')\n$' |
17 | 17 | ) |
18 | 18 | GIT_CONFLICT_MARKERS = ['<' * 7, '>' * 7] |
| 19 | +ALLOWED_EARLY_BREAK_RE = re.compile(r'^\s*(\.\. |:\S+:\s+)', re.IGNORECASE) # Contains markup. |
19 | 20 |
|
20 | 21 |
|
21 | 22 | @sphinxlint.checker('.rst') |
@@ -103,6 +104,48 @@ def check_heading_spacing(file, lines, options=None): |
103 | 104 | yield heading_lno + 1, "the heading should be followed by a blank line" |
104 | 105 |
|
105 | 106 |
|
| 107 | +@sphinxlint.checker('.rst', enabled=False) |
| 108 | +def check_early_line_breaks(file, lines, options=None): |
| 109 | + """ Checks that no line breaks early, i.e., before using as much of the max length as possible. |
| 110 | +
|
| 111 | + Note: `make review` only |
| 112 | + """ |
| 113 | + |
| 114 | + def is_valid_line(line_, forbidden_starting_chars_): |
| 115 | + """ Allowed to break early - handle tables and bullets """ |
| 116 | + return not ALLOWED_EARLY_BREAK_RE.search(line_) \ |
| 117 | + and not HEADING_DELIMITER_RE.search(line_) \ |
| 118 | + and not line_.startswith('\n') \ |
| 119 | + and not line_.lstrip().startswith(forbidden_starting_chars_) \ |
| 120 | + and len(line_) <= options.max_line_length |
| 121 | + |
| 122 | + def get_next_line_first_word(next_line_): |
| 123 | + """ Return the first word of the next line """ |
| 124 | + if next_line_.startswith(' '): |
| 125 | + next_line_dict = { |
| 126 | + '*': lambda x: x.split('* ', 1)[0], |
| 127 | + '-': lambda x: x.split('- ', 1)[0], |
| 128 | + '#.': lambda x: x.split('#. ', 1)[0], |
| 129 | + 'default': lambda x: x.split(' ', 1)[0] |
| 130 | + } |
| 131 | + return next_line_dict.get(next_line_.lstrip()[:2], next_line_dict["default"])( |
| 132 | + next_line_.lstrip() |
| 133 | + ) |
| 134 | + else: |
| 135 | + return next_line_.split(' ', 1)[0] |
| 136 | + |
| 137 | + for lno, line in enumerate(lines): |
| 138 | + if lno + 1 < len(lines): |
| 139 | + next_line = lines[lno + 1] |
| 140 | + if (is_valid_line(line, ('+', '|')) |
| 141 | + and is_valid_line(next_line, ('+', '|', '- ', '* ', '#. ')) |
| 142 | + ): |
| 143 | + current_line_remaining_space = options.max_line_length - len(line) |
| 144 | + next_line_first_word = get_next_line_first_word(next_line) |
| 145 | + if current_line_remaining_space > len(next_line_first_word): |
| 146 | + yield lno + 1, f"consider moving \"{next_line_first_word}\" to line {lno + 1}" |
| 147 | + |
| 148 | + |
106 | 149 | @sphinxlint.checker('.rst', '.py', '.js', '.xml', '.css', '.sass', '.less', '.po', '.pot') |
107 | 150 | def check_git_conflict_markers(file, lines, options=None): |
108 | 151 | """ Check that there are no conflict markers. """ |
|
0 commit comments