Skip to content

Commit dd8a1ab

Browse files
jesserockzclaude
andauthored
Lint (#1279)
Co-authored-by: Claude <[email protected]>
1 parent b5f28c9 commit dd8a1ab

File tree

482 files changed

+8663
-6661
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

482 files changed

+8663
-6661
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- name: Checkout code
19-
uses: actions/checkout@v5
19+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2020
- name: markdownlint-cli
21-
uses: nosborn/github-action-markdown-cli@v1.1.1
21+
uses: nosborn/github-action-markdown-cli@508d6cefd8f0cc99eab5d2d4685b1d5f470042c1 # v3.5.0
2222
with:
2323
config_file: ".markdownlintrc"
24-
files: .
24+
files: src
2525
yaml_lint:
2626
name: YAML Lint
2727
runs-on: ubuntu-latest

.markdownlintrc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{
2-
"MD013": false,
3-
"MD034": false,
4-
"MD033": { "allowed_elements": ["details", "summary"] }
2+
"MD013": {
3+
"line_length": 120,
4+
"code_blocks": false,
5+
"tables": false,
6+
"headings": false
7+
},
8+
"MD033": {
9+
"allowed_elements": [
10+
"details",
11+
"summary"
12+
]
13+
}
514
}

scripts/wrap_markdown_lines.py

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Wrap long lines in markdown files to comply with line length limits.
4+
5+
This script wraps lines that exceed a specified length (default 120 characters)
6+
while preserving markdown formatting, lists, links, and code blocks.
7+
8+
Links are never broken - if a link would be split, the entire link is moved to
9+
a new line.
10+
11+
Usage:
12+
python3 wrap_markdown_lines.py <file1.md> [file2.md ...]
13+
python3 wrap_markdown_lines.py --max-length 100 <file.md>
14+
"""
15+
16+
import sys
17+
import re
18+
import argparse
19+
from pathlib import Path
20+
21+
22+
def is_in_code_block(lines, index):
23+
"""Check if line at index is inside a code block."""
24+
code_block_count = 0
25+
for i in range(index):
26+
if lines[i].strip().startswith('```'):
27+
code_block_count += 1
28+
return code_block_count % 2 == 1
29+
30+
31+
def is_table_line(line):
32+
"""Check if line is part of a markdown table."""
33+
stripped = line.strip()
34+
# Table lines have pipes and typically start/end with pipe or have multiple pipes
35+
return '|' in stripped and (stripped.count('|') > 1 or stripped.startswith('|'))
36+
37+
38+
def is_heading(line):
39+
"""Check if line is a markdown heading."""
40+
return line.strip().startswith('#')
41+
42+
43+
def get_list_indent(line):
44+
"""Get indentation for a list item."""
45+
match = re.match(r'^(\s*)([-*+]|\d+\.)\s+', line)
46+
if match:
47+
# Return spaces equal to the full match length for continuation
48+
return ' ' * len(match.group(0))
49+
return None
50+
51+
52+
def find_markdown_links(text):
53+
"""
54+
Find all markdown links in text and return their positions.
55+
Returns list of (start, end, link_text) tuples for [text](url) style links.
56+
"""
57+
links = []
58+
# Match [text](url) style links
59+
for match in re.finditer(r'\[([^\]]+)\]\(([^)]+)\)', text):
60+
links.append((match.start(), match.end(), match.group(0)))
61+
return links
62+
63+
64+
def find_wrap_point(text, max_len):
65+
"""
66+
Find the best point to wrap text, preferring spaces.
67+
Never breaks inside a markdown link - if we'd break a link, we move the whole link to next line.
68+
"""
69+
if len(text) <= max_len:
70+
return -1
71+
72+
# Find all links in the text
73+
links = find_markdown_links(text)
74+
75+
# Find last space before max_len
76+
for i in range(max_len, 0, -1):
77+
if text[i] == ' ':
78+
# Check if this position is inside a link
79+
inside_link = False
80+
for link_start, link_end, _ in links:
81+
if link_start < i < link_end:
82+
# This would break a link - instead, wrap before the link starts
83+
# Find the space before the link
84+
for j in range(link_start - 1, -1, -1):
85+
if text[j] == ' ':
86+
return j
87+
# No space before link, can't wrap nicely
88+
return -1
89+
inside_link = True
90+
break
91+
92+
if inside_link:
93+
continue
94+
95+
# Check if we're not breaking inline code
96+
backtick_count = text[:i].count('`')
97+
if backtick_count % 2 == 1: # Odd number means we're inside code
98+
continue
99+
100+
return i
101+
102+
# No good break point found
103+
return -1
104+
105+
106+
def wrap_line(line, max_length=120):
107+
"""
108+
Wrap a single line to max_length, preserving markdown structure.
109+
Returns a list of wrapped lines.
110+
"""
111+
if len(line.rstrip()) <= max_length:
112+
return [line]
113+
114+
# Check if it's a list item
115+
list_indent = get_list_indent(line)
116+
117+
# Strip trailing newline for processing
118+
text = line.rstrip('\n')
119+
wrapped_lines = []
120+
121+
if list_indent:
122+
# For list items, preserve the marker on first line
123+
# Get the part after the list marker
124+
match = re.match(r'^(\s*(?:[-*+]|\d+\.)\s+)', text)
125+
prefix = match.group(1)
126+
content = text[len(prefix):]
127+
128+
# Wrap the content
129+
current = prefix + content
130+
first_line = True
131+
max_iterations = 1000 # Safety limit to prevent infinite loops
132+
iteration = 0
133+
while len(current) > max_length and iteration < max_iterations:
134+
iteration += 1
135+
prev_len = len(current)
136+
wrap_point = find_wrap_point(current, max_length)
137+
if wrap_point <= 0:
138+
# Can't wrap nicely, keep as is
139+
wrapped_lines.append(current)
140+
break
141+
142+
wrapped_lines.append(current[:wrap_point])
143+
# Continue with proper indentation
144+
remainder = current[wrap_point:].lstrip()
145+
current = list_indent + remainder if remainder else ''
146+
147+
# Safety check: ensure we're making progress
148+
if len(current) >= prev_len:
149+
# Not making progress, line can't be wrapped further
150+
if current:
151+
wrapped_lines.append(current)
152+
break
153+
first_line = False
154+
else:
155+
if current: # Add remaining text if any
156+
wrapped_lines.append(current)
157+
else:
158+
# Regular paragraph text
159+
current = text
160+
max_iterations = 1000 # Safety limit to prevent infinite loops
161+
iteration = 0
162+
while len(current) > max_length and iteration < max_iterations:
163+
iteration += 1
164+
prev_len = len(current)
165+
wrap_point = find_wrap_point(current, max_length)
166+
if wrap_point <= 0:
167+
# Can't wrap nicely, keep as is
168+
wrapped_lines.append(current)
169+
break
170+
171+
wrapped_lines.append(current[:wrap_point])
172+
remainder = current[wrap_point:].lstrip()
173+
current = remainder if remainder else ''
174+
175+
# Safety check: ensure we're making progress
176+
if current and len(current) >= prev_len:
177+
# Not making progress, line can't be wrapped further
178+
wrapped_lines.append(current)
179+
break
180+
else:
181+
if current: # Add remaining text if any
182+
wrapped_lines.append(current)
183+
184+
# Add newlines back
185+
return [l + '\n' for l in wrapped_lines]
186+
187+
188+
def process_markdown_file(filepath, max_length=120, dry_run=False):
189+
"""
190+
Process a markdown file to wrap long lines.
191+
192+
Args:
193+
filepath: Path to the markdown file
194+
max_length: Maximum line length (default 120)
195+
dry_run: If True, don't write changes, just report
196+
197+
Returns:
198+
Number of lines wrapped
199+
"""
200+
filepath = Path(filepath)
201+
202+
with open(filepath, 'r', encoding='utf-8') as f:
203+
lines = f.readlines()
204+
205+
new_lines = []
206+
lines_wrapped = 0
207+
208+
for i, line in enumerate(lines):
209+
# Skip code blocks
210+
if is_in_code_block(lines, i):
211+
new_lines.append(line)
212+
continue
213+
214+
# Skip table lines
215+
if is_table_line(line):
216+
new_lines.append(line)
217+
continue
218+
219+
# Skip headings
220+
if is_heading(line):
221+
new_lines.append(line)
222+
continue
223+
224+
# Check if line needs wrapping
225+
if len(line.rstrip()) > max_length:
226+
wrapped = wrap_line(line, max_length)
227+
new_lines.extend(wrapped)
228+
if len(wrapped) > 1:
229+
lines_wrapped += 1
230+
else:
231+
new_lines.append(line)
232+
233+
if not dry_run and lines_wrapped > 0:
234+
with open(filepath, 'w', encoding='utf-8') as f:
235+
f.writelines(new_lines)
236+
237+
return lines_wrapped
238+
239+
240+
def main():
241+
parser = argparse.ArgumentParser(
242+
description='Wrap long lines in markdown files to comply with line length limits.'
243+
)
244+
parser.add_argument(
245+
'files',
246+
nargs='+',
247+
help='Markdown files to process'
248+
)
249+
parser.add_argument(
250+
'--max-length',
251+
type=int,
252+
default=120,
253+
help='Maximum line length (default: 120)'
254+
)
255+
parser.add_argument(
256+
'--dry-run',
257+
action='store_true',
258+
help='Show what would be changed without making changes'
259+
)
260+
261+
args = parser.parse_args()
262+
263+
total_wrapped = 0
264+
for filepath in args.files:
265+
if not Path(filepath).exists():
266+
print(f"Warning: {filepath} not found, skipping", file=sys.stderr)
267+
continue
268+
269+
wrapped = process_markdown_file(filepath, args.max_length, args.dry_run)
270+
if wrapped > 0:
271+
action = "Would wrap" if args.dry_run else "Wrapped"
272+
print(f"{action} {wrapped} line(s) in {filepath}")
273+
total_wrapped += wrapped
274+
275+
if total_wrapped > 0:
276+
action = "would be" if args.dry_run else "were"
277+
print(f"\nTotal: {total_wrapped} line(s) {action} wrapped")
278+
else:
279+
print("No lines needed wrapping")
280+
281+
return 0 if total_wrapped >= 0 else 1
282+
283+
284+
if __name__ == '__main__':
285+
sys.exit(main())

src/docs/adding-devices.md

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,28 @@ permalink: /adding-devices/
66

77
## Create device folder and markdown file
88

9-
1. To add a new device create a new folder named after your device under the `src/docs/devices` directory in the [GitHub Repository](https://github.com/esphome/esphome-devices). In that folder, create a markdown (`.md`) file named `index.md` with the content. Please avoid using underscores or spaces in the filenames and use hypens instead as this makes for easier to understand the URLs generated when the site is built. When using the _Add file_ -> _Create new file_ button in the `Devices` folder or by following [this link](https://github.com/esphome/esphome-devices/new/main/src/docs/devices), Github will automatically create a fork of the repository and a new branch for your changes. Just type the device name for the folder followed by a `/index.md` (including the slash).
10-
11-
<script async defer src="https://buttons.github.io/buttons.js"></script>
12-
13-
<a class="github-button" href="https://github.com/esphome/esphome-devices/fork" data-icon="octicon-repo-forked" data-size="large" data-show-count="true" aria-label="Fork esphome-devices/esphome-devices on GitHub">Fork</a>
14-
15-
2. Once you have written your file commit your changes and raise a pull request on GitHub. A guide for creating a pull request from a fork can be found [here](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork) if you are unsure.
9+
1. To add a new device create a new folder named after your device under the `src/docs/devices` directory in the
10+
[GitHub Repository](https://github.com/esphome/esphome-devices). In that folder, create a markdown (`.md`) file named
11+
`index.md` with the content. Please avoid using underscores or spaces in the filenames and use hypens instead as this
12+
makes for easier to understand the URLs generated when the site is built. When using the _Add file_ -> _Create new
13+
file_
14+
button in the `Devices` folder or by following
15+
[this link](https://github.com/esphome/esphome-devices/new/main/src/docs/devices),
16+
Github will automatically create a fork of the repository and a new branch for your changes. Just type the device
17+
name for
18+
the folder followed by a `/index.md` (including the slash).
19+
20+
[Fork](https://github.com/esphome/esphome-devices/fork) on GitHub
21+
22+
2. Once you have written your file commit your changes and raise a pull request on GitHub. A guide for creating a pull
23+
request from a fork can be found
24+
[in the GitHub documentation](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork)
25+
if you are unsure.
1626

1727
## YAML Front Matter
1828

19-
Each `.md` file created needs to contain front matter in order for the page to be generated. Details of the front matter required (and optional) is detailed below:
29+
Each `.md` file created needs to contain front matter in order for the page to be generated. Details of the front matter
30+
required (and optional) is detailed below:
2031

2132
```yaml
2233
---
@@ -27,16 +38,16 @@ standard: uk, us
2738
---
2839
```
2940

30-
| Field | Description | Allowable Options | Required? |
31-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
32-
| `title` | Device Title | | Yes |
33-
| `date-published` | Date Published | Formatting: `YYYY-MM-DD HH:MM:SS +/-TTTT` (Time and Timezone offset are optional) | Yes |
34-
| `type` | Type of Device | `plug`, `light`, `switch`, `dimmer` , `relay`, `sensor`, `misc` | Yes |
35-
| `standard` | Electrical standard country | `uk`, `us`, `eu`, `au`, `in`, `global` | Yes |
36-
| `board` | Type of board used in product | `esp8266`, `esp32`, `rp2040`, `bk72xx`, `rtl87xx` | No (but required to show on Boards page) |
37-
| `project-url` | URL for product or GitHub. This should point directly to a working Yaml file or page where the yaml file is easily accessible (ie. a Github Repo) Repo | | No |
38-
| `made-for-esphome` | Has the manufacturer certified the device for ESPHome | `True`, `False` | No |
39-
| `difficulty` | Difficulty rating | `1`: Comes with ESPHome, `2`: Plug-n-flash, `3`: Disassembly required, `4`: Soldering required, `5`: Chip needs replacement | No |
41+
| Field | Description | Allowable Options | Required? |
42+
| ------------------ | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
43+
| `title` | Device Title | | Yes |
44+
| `date-published` | Date Published | Formatting: `YYYY-MM-DD HH:MM:SS +/-TTTT` (Time and Timezone offset are optional) | Yes |
45+
| `type` | Type of Device | `plug`, `light`, `switch`, `dimmer` , `relay`, `sensor`, `misc` | Yes |
46+
| `standard` | Electrical standard country | `uk`, `us`, `eu`, `au`, `in`, `global` | Yes |
47+
| `board` | Type of board used in product | `esp8266`, `esp32`, `rp2040`, `bk72xx`, `rtl87xx` | No (but required to show on Boards page) |
48+
| `project-url` | URL for product or GitHub. Points to working Yaml file or page where yaml file is easily accessible | | No |
49+
| `made-for-esphome` | Has the manufacturer certified the device for ESPHome | `True`, `False` | No |
50+
| `difficulty` | Difficulty rating | `1`: Comes with ESPHome, `2`: Plug-n-flash, `3`: Disassembly required, `4`: Soldering required, `5`: Chip needs replacement | No |
4051

4152
## Images
4253

0 commit comments

Comments
 (0)