Skip to content

Commit 8a43473

Browse files
committed
feat(docs): add nexus page BBCode generator
- README.md: add user-facing Requirements, Installation, Compatibility block wrapped in nexus:start/end markers before dev Prerequisites - docs/nexus-page.md: wrap generated sections in generated:start/end markers - scripts/generate-nexus-page.py: extract nexus block from README, convert Markdown to BBCode, inject into nexus-page.md template, print to stdout
1 parent 221a514 commit 8a43473

3 files changed

Lines changed: 197 additions & 1 deletion

File tree

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,33 @@ Supports building on **Linux** (cross-compilation via `clang-cl` + [xwin](https:
77

88
---
99

10-
## Prerequisites
10+
<!-- nexus:start -->
11+
## Requirements
12+
13+
- [SKSE64](https://skse.silverlock.org/)
14+
- [Address Library for SKSE Plugins](https://www.nexusmods.com/skyrimspecialedition/mods/32444)
15+
16+
## Installation
17+
18+
**Mod manager (recommended):**
19+
1. Install the requirements above.
20+
2. Install ExampleMod via your mod manager.
21+
3. Launch Skyrim via SKSE.
22+
23+
**Manual:**
24+
1. Install the requirements above.
25+
2. Copy `ExampleMod.dll` to `Data\SKSE\Plugins\`.
26+
3. Launch Skyrim via SKSE.
27+
28+
## Compatibility
29+
30+
- Compatible with Skyrim SE and AE.
31+
- No ESP/ESL required.
32+
<!-- nexus:end -->
33+
34+
---
35+
36+
## Prerequisites (Developer)
1137

1238
### All platforms
1339
- [Git](https://git-scm.com/)

docs/nexus-page.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Describe what the mod does.
2626
2727
[line]
2828
29+
<!-- generated:start -->
2930
[size=4][b]Requirements[/b][/size]
3031
3132
[list]
@@ -59,6 +60,7 @@ Describe what the mod does.
5960
[*]Compatible with Skyrim SE and AE.
6061
[*]No ESP/ESL required.
6162
[/list]
63+
<!-- generated:end -->
6264
6365
[line]
6466

scripts/generate-nexus-page.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python3
2+
"""Generate nexus-page BBCode from README.md user-facing content.
3+
4+
Reads the block between <!-- nexus:start --> and <!-- nexus:end --> in README.md,
5+
converts it to BBCode, injects it into the <!-- generated:start/end --> block in
6+
docs/nexus-page.md, and prints the final BBCode to stdout.
7+
8+
Usage:
9+
python scripts/generate-nexus-page.py
10+
python scripts/generate-nexus-page.py | xclip -selection clipboard
11+
"""
12+
13+
import argparse
14+
import re
15+
import sys
16+
from pathlib import Path
17+
18+
README = Path(__file__).parent.parent / "README.md"
19+
NEXUS_TEMPLATE = Path(__file__).parent.parent / "docs" / "nexus-page.md"
20+
21+
NEXUS_START = "<!-- nexus:start -->"
22+
NEXUS_END = "<!-- nexus:end -->"
23+
GEN_START = "<!-- generated:start -->"
24+
GEN_END = "<!-- generated:end -->"
25+
BBCODE_FENCE_OPEN = "```bbcode"
26+
BBCODE_FENCE_CLOSE = "```"
27+
28+
29+
def extract_block(text: str, start_marker: str, end_marker: str) -> str:
30+
start = text.find(start_marker)
31+
end = text.find(end_marker)
32+
if start == -1:
33+
sys.exit(f"error: marker not found: {start_marker!r}")
34+
if end == -1:
35+
sys.exit(f"error: marker not found: {end_marker!r}")
36+
if start >= end:
37+
sys.exit(f"error: end marker {end_marker!r} appears before start marker {start_marker!r}")
38+
return text[start + len(start_marker):end].strip()
39+
40+
41+
def md_to_bbcode(md: str) -> str:
42+
lines = md.splitlines()
43+
output: list[str] = []
44+
i = 0
45+
first_heading = True
46+
47+
while i < len(lines):
48+
line = lines[i]
49+
50+
# ## Heading
51+
if line.startswith("## "):
52+
heading = line[3:].strip()
53+
if not first_heading:
54+
output.append("")
55+
output.append("[line]")
56+
output.append("")
57+
first_heading = False
58+
output.append(f"[size=4][b]{heading}[/b][/size]")
59+
i += 1
60+
continue
61+
62+
# Unordered list block
63+
if line.startswith("- "):
64+
items: list[str] = []
65+
while i < len(lines) and lines[i].startswith("- "):
66+
items.append(convert_inline(lines[i][2:].strip()))
67+
i += 1
68+
output.append("[list]")
69+
for item in items:
70+
output.append(f"[*]{item}")
71+
output.append("[/list]")
72+
continue
73+
74+
# Ordered list block — may be preceded by a bold label line
75+
if re.match(r"\d+\. ", line):
76+
items = []
77+
while i < len(lines) and re.match(r"\d+\. ", lines[i]):
78+
items.append(convert_inline(re.sub(r"^\d+\. ", "", lines[i])))
79+
i += 1
80+
output.append("[list=1]")
81+
for item in items:
82+
output.append(f"[*]{item}")
83+
output.append("[/list]")
84+
continue
85+
86+
# Bold label (e.g. **Mod manager (recommended):**)
87+
if re.match(r"\*\*.+\*\*", line):
88+
output.append(convert_inline(line))
89+
i += 1
90+
continue
91+
92+
# Blank line
93+
if line.strip() == "":
94+
output.append("")
95+
i += 1
96+
continue
97+
98+
# Unsupported construct
99+
stripped = line.strip()
100+
if stripped and not stripped.startswith("<!--"):
101+
sys.exit(f"error: unsupported Markdown construct on line {i + 1}: {line!r}")
102+
103+
i += 1
104+
105+
# Collapse consecutive blank lines to single blank
106+
result = re.sub(r"\n{3,}", "\n\n", "\n".join(output))
107+
return result.strip()
108+
109+
110+
def convert_inline(text: str) -> str:
111+
# [text](url) → [url=url]text[/url]
112+
# Note: link text containing ] or URLs containing unbalanced ) are not supported.
113+
text = re.sub(r"\[([^\]]+)\]\(([^)]*(?:\([^)]*\)[^)]*)*)\)", r"[url=\2]\1[/url]", text)
114+
# **text** → [b]text[/b]
115+
text = re.sub(r"\*\*(.+?)\*\*", r"[b]\1[/b]", text)
116+
# `code` → [font=Courier New]code[/font]
117+
text = re.sub(r"`([^`]+)`", r"[font=Courier New]\1[/font]", text)
118+
return text
119+
120+
121+
def extract_bbcode_fence(text: str) -> str:
122+
start = text.find(BBCODE_FENCE_OPEN)
123+
if start == -1:
124+
sys.exit(f"error: {BBCODE_FENCE_OPEN!r} fence not found in nexus-page.md")
125+
start = text.index("\n", start) + 1
126+
end = text.find(f"\n{BBCODE_FENCE_CLOSE}", start)
127+
if end == -1:
128+
sys.exit("error: closing ``` fence not found in nexus-page.md")
129+
return text[start:end]
130+
131+
132+
def inject_generated(bbcode_content: str, generated: str) -> str:
133+
start = bbcode_content.find(GEN_START)
134+
end = bbcode_content.find(GEN_END)
135+
if start == -1:
136+
sys.exit(f"error: marker not found: {GEN_START!r}")
137+
if end == -1:
138+
sys.exit(f"error: marker not found: {GEN_END!r}")
139+
if start >= end:
140+
sys.exit(f"error: end marker {GEN_END!r} appears before start marker {GEN_START!r}")
141+
return (
142+
bbcode_content[:start]
143+
+ GEN_START + "\n"
144+
+ generated + "\n"
145+
+ bbcode_content[end:]
146+
)
147+
148+
149+
def main() -> None:
150+
argparse.ArgumentParser(
151+
description=__doc__,
152+
formatter_class=argparse.RawDescriptionHelpFormatter,
153+
).parse_args()
154+
155+
readme = README.read_text(encoding="utf-8")
156+
template = NEXUS_TEMPLATE.read_text(encoding="utf-8")
157+
158+
md_block = extract_block(readme, NEXUS_START, NEXUS_END)
159+
bbcode_generated = md_to_bbcode(md_block)
160+
161+
bbcode_content = extract_bbcode_fence(template)
162+
result = inject_generated(bbcode_content, bbcode_generated)
163+
164+
print(result)
165+
166+
167+
if __name__ == "__main__":
168+
main()

0 commit comments

Comments
 (0)