Skip to content

Commit 1dd4eca

Browse files
Update to v2.0.0
1 parent e72ec87 commit 1dd4eca

16 files changed

Lines changed: 680 additions & 32 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Add a resource
2+
description: Suggest a new link to include in this Awesome List.
3+
title: "Add: <resource name>"
4+
body:
5+
- type: input
6+
id: name
7+
attributes:
8+
label: Resource name
9+
placeholder: "Example Tool / Guide / Site"
10+
validations:
11+
required: true
12+
13+
- type: input
14+
id: url
15+
attributes:
16+
label: URL
17+
placeholder: "https://example.com"
18+
validations:
19+
required: true
20+
21+
- type: textarea
22+
id: description
23+
attributes:
24+
label: Short description
25+
description: One sentence describing what it is and why it belongs here.
26+
placeholder: "A curated directory of…"
27+
validations:
28+
required: true
29+
30+
- type: textarea
31+
id: placement
32+
attributes:
33+
label: Where should it go?
34+
description: Section name + (optional) suggested ordering.
35+
placeholder: "Section: Tools → Validation"
36+
validations:
37+
required: false
38+
39+
- type: checkboxes
40+
id: checks
41+
attributes:
42+
label: Checks
43+
options:
44+
- label: I confirmed the link loads in a browser.
45+
required: true
46+
- label: I confirmed this isn’t a duplicate of an existing entry.
47+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: true
2+
contact_links:
3+
- name: Questions / Discussion
4+
url: https://github.com
5+
about: Use Discussions (if enabled) or open a blank issue with context.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Report a broken link
2+
description: Report a dead or moved URL in this Awesome List.
3+
title: "Broken link: <resource name>"
4+
body:
5+
- type: input
6+
id: entry
7+
attributes:
8+
label: Which entry is broken?
9+
placeholder: "Resource name (as listed)"
10+
validations:
11+
required: true
12+
13+
- type: input
14+
id: url
15+
attributes:
16+
label: Broken URL
17+
placeholder: "https://example.com/broken"
18+
validations:
19+
required: true
20+
21+
- type: input
22+
id: replacement
23+
attributes:
24+
label: Replacement URL (if you found one)
25+
placeholder: "https://example.com/new"
26+
validations:
27+
required: false
28+
29+
- type: textarea
30+
id: notes
31+
attributes:
32+
label: Notes
33+
placeholder: "Any context (redirects, 404, access restrictions, etc.)"
34+
validations:
35+
required: false

.github/pull_request_template.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Thank you for your contribution!
2+
3+
Please make sure your pull request follows our guidelines:
4+
5+
### Checklist
6+
7+
- [ ] My submission is useful and relevant to this list.
8+
- [ ] I've added a short description for the resource.
9+
- [ ] The link is not a duplicate.
10+
- [ ] The link is working and publicly accessible.
11+
- [ ] I've checked that the resource follows our quality and content standards.
12+
- [ ] I have not added a link to my own project if it's not notable or widely used.
13+
14+
### Description of the Change
15+
16+
<!-- Describe why you are adding this resource and why it's Awesome -->
17+
18+
### Additional Notes (if any)
19+
20+
<!-- Optional: Any notes for reviewers -->
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Awesome List lint (strict on resources, permissive on TOC)
4+
5+
Errors:
6+
- README.md missing
7+
- Missing "Contribute"/"Contributing" heading (accepts contribut* variants)
8+
- Missing "License" heading
9+
- TAB characters in README.md
10+
- Trailing whitespace on list lines that start with "- [" or "* ["
11+
- External resource entries must be in the format:
12+
- [Name](https://example.com) — Short, neutral description.
13+
(also allows '-' instead of '—')
14+
15+
Allows:
16+
- TOC / internal anchor bullets:
17+
- [Section](#section)
18+
(no description required)
19+
20+
Notes:
21+
- Ignores code blocks.
22+
- Duplicate headings are warnings, not failures.
23+
"""
24+
from __future__ import annotations
25+
26+
import re
27+
from pathlib import Path
28+
from collections import Counter
29+
30+
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
31+
CODE_FENCE_RE = re.compile(r"^\s*```")
32+
33+
# Basic markdown bullet + link capture
34+
BULLET_LINK_RE = re.compile(r"^[-*]\s+\[[^\]]+\]\(([^)]+)\)\s*$")
35+
36+
# External resource entry with description
37+
RESOURCE_WITH_DESC_RE = re.compile(
38+
r"^[-*]\s+\[[^\]]+\]\((https?://[^\s)]+)\)\s*(—|-)\s+.+"
39+
)
40+
41+
def strip_code_blocks(lines: list[str]) -> list[tuple[int, str]]:
42+
in_code = False
43+
out: list[tuple[int, str]] = []
44+
for i, line in enumerate(lines, start=1):
45+
if CODE_FENCE_RE.match(line):
46+
in_code = not in_code
47+
continue
48+
if not in_code:
49+
out.append((i, line))
50+
return out
51+
52+
def main() -> int:
53+
readme = Path("README.md")
54+
if not readme.exists():
55+
print("ERROR: README.md not found.")
56+
return 1
57+
58+
text = readme.read_text(encoding="utf-8", errors="ignore")
59+
if "\t" in text:
60+
print("ERROR: README.md contains TAB characters. Replace tabs with spaces.")
61+
return 1
62+
63+
lines = text.splitlines()
64+
noncode = strip_code_blocks(lines)
65+
66+
# headings
67+
headings: list[str] = []
68+
for _ln, line in noncode:
69+
m = HEADING_RE.match(line)
70+
if m:
71+
headings.append(m.group(2).strip())
72+
73+
lower = [h.lower().strip() for h in headings]
74+
75+
if not any(h in ("contribute", "contributing") or "contribut" in h for h in lower):
76+
print("ERROR: Missing a 'Contribute'/'Contributing' section heading in README.md.")
77+
return 1
78+
79+
if not any("license" in h for h in lower):
80+
print("ERROR: Missing a 'License' section heading in README.md.")
81+
return 1
82+
83+
# Duplicate heading warning (non-fatal)
84+
c = Counter(lower)
85+
dups = [h for h, n in c.items() if n > 1]
86+
if dups:
87+
print("WARNING: Duplicate section headings detected:")
88+
for h in sorted(dups):
89+
print(f" - {h}")
90+
print()
91+
92+
errors = 0
93+
94+
for ln, line in noncode:
95+
s = line.rstrip("\n")
96+
97+
# Only lint bullets that look like markdown-link bullets
98+
if not (s.startswith("- [") or s.startswith("* [")):
99+
continue
100+
101+
# Trailing whitespace (fail)
102+
if s != s.rstrip():
103+
print(f"ERROR: Trailing whitespace on list line {ln}.")
104+
errors += 1
105+
106+
# If it's exactly a TOC-style bullet like "- [Section](#section)", allow it.
107+
m = BULLET_LINK_RE.match(s.strip())
108+
if m:
109+
url = m.group(1).strip()
110+
if url.startswith("#"):
111+
continue # TOC anchor bullet is valid
112+
113+
# For external resources, require description format
114+
if s.find("(http://") != -1 or s.find("(https://") != -1:
115+
if RESOURCE_WITH_DESC_RE.match(s):
116+
continue
117+
118+
print(f"ERROR: Resource entry bullet malformed on line {ln}. Expected:")
119+
print(" - [Name](https://example.com) — Short, neutral description.")
120+
print("or")
121+
print(" - [Name](https://example.com) - Short, neutral description.")
122+
print(f" {s}")
123+
errors += 1
124+
125+
if errors:
126+
print(f"Found {errors} lint error(s).")
127+
return 1
128+
129+
print("Awesome list lint: OK")
130+
return 0
131+
132+
if __name__ == "__main__":
133+
raise SystemExit(main())
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Detect duplicate URLs in Awesome List resource entries.
4+
5+
Only scans lines that start with "- [" or "* [" (resource entries).
6+
Plain bullets elsewhere are ignored.
7+
8+
Fails if the same URL appears multiple times within the same file's resource entries.
9+
10+
Template-friendly:
11+
- Ignores placeholder example.com URLs by default.
12+
"""
13+
from __future__ import annotations
14+
15+
import re
16+
import sys
17+
from collections import defaultdict
18+
from pathlib import Path
19+
20+
RESOURCE_LINE_RE = re.compile(r"^[-*]\s+\[")
21+
URL_RE = re.compile(r"\[[^\]]+\]\((https?://[^\s)]+)\)")
22+
23+
IGNORE_URLS = {
24+
"http://example.com",
25+
"https://example.com",
26+
"http://www.example.com",
27+
"https://www.example.com",
28+
}
29+
30+
def iter_md_files(paths: list[str]) -> list[Path]:
31+
if not paths:
32+
return [p for p in Path(".").rglob("*.md") if ".git" not in p.parts]
33+
files: list[Path] = []
34+
for s in paths:
35+
p = Path(s)
36+
if p.is_dir():
37+
files.extend([x for x in p.rglob("*.md") if ".git" not in x.parts])
38+
elif p.is_file():
39+
files.append(p)
40+
return files
41+
42+
def normalize(url: str) -> str:
43+
url = url.strip().rstrip(".,;:!?)\"]'")
44+
# normalize common trailing slash differences
45+
if url.endswith("/") and len(url) > 10:
46+
url = url[:-1]
47+
return url
48+
49+
def main() -> int:
50+
files = iter_md_files(sys.argv[1:])
51+
if not files:
52+
print("No markdown files found.")
53+
return 0
54+
55+
failures = 0
56+
57+
for f in sorted(set(files)):
58+
text = f.read_text(encoding="utf-8", errors="ignore")
59+
urls: list[str] = []
60+
61+
for line in text.splitlines():
62+
if RESOURCE_LINE_RE.match(line):
63+
m = URL_RE.search(line)
64+
if m:
65+
u = normalize(m.group(1))
66+
if u in IGNORE_URLS:
67+
continue
68+
urls.append(u)
69+
70+
counts = defaultdict(int)
71+
for u in urls:
72+
counts[u] += 1
73+
74+
dups = {u: c for u, c in counts.items() if c > 1}
75+
if dups:
76+
failures += 1
77+
print(f"Duplicate resource URLs in {f}:")
78+
for u, c in sorted(dups.items(), key=lambda x: (-x[1], x[0])):
79+
print(f" ({c}x) {u}")
80+
print()
81+
82+
if failures:
83+
print(f"Found duplicates in {failures} file(s).")
84+
return 1
85+
86+
print("No duplicate resource URLs detected.")
87+
return 0
88+
89+
if __name__ == "__main__":
90+
raise SystemExit(main())

.github/workflows/awesome-lint.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Awesome List Lint
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches: ["main"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
lint:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.x"
23+
24+
- name: Run awesome list lint
25+
run: |
26+
set -euo pipefail
27+
python3 .github/scripts/awesome_list_lint.py
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Duplicate Link Detector
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches: ["main"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
duplicates:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.x"
23+
24+
- name: Detect duplicate URLs in markdown
25+
run: |
26+
set -euo pipefail
27+
python3 .github/scripts/detect_duplicate_links.py

0 commit comments

Comments
 (0)