Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,33 @@ Supports building on **Linux** (cross-compilation via `clang-cl` + [xwin](https:

---

## Prerequisites
<!-- nexus:start -->
## Requirements

- [SKSE64](https://skse.silverlock.org/)
- [Address Library for SKSE Plugins](https://www.nexusmods.com/skyrimspecialedition/mods/32444)

## Installation

**Mod manager (recommended):**
1. Install the requirements above.
2. Install ExampleMod via your mod manager.
3. Launch Skyrim via SKSE.

**Manual:**
1. Install the requirements above.
2. Copy `ExampleMod.dll` to `Data\SKSE\Plugins\`.
3. Launch Skyrim via SKSE.

## Compatibility

- Compatible with Skyrim SE and AE.
- No ESP/ESL required.
<!-- nexus:end -->

---

## Prerequisites (Developer)

### All platforms
- [Git](https://git-scm.com/)
Expand Down
16 changes: 11 additions & 5 deletions docs/nexus-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@

## Long Description (BBCode)

<!-- The Requirements, Installation, and Compatibility sections below are generated.
Run `python3 scripts/generate-nexus-page.py` and copy the output to Nexus Mods.
Do not copy this file directly — the generated block markers are not valid BBCode. -->

```bbcode
[center][size=5][b]ExampleMod[/b][/size]
[i]Tagline here[/i][/center]

[line]

[size=4][b]Overview[/b][/size]
[size=4][b][color=#B8953E]Overview[/color][/b][/size]

Describe what the mod does.

[line]

[size=4][b]Requirements[/b][/size]
<!-- generated:start -->
[size=4][b][color=#B8953E]Requirements[/color][/b][/size]

Comment thread
codepuncher marked this conversation as resolved.
[list]
[*][url=https://skse.silverlock.org/]SKSE64[/url]
Expand All @@ -35,7 +40,7 @@ Describe what the mod does.

[line]

[size=4][b]Installation[/b][/size]
[size=4][b][color=#B8953E]Installation[/color][/b][/size]

[b]Mod manager (recommended):[/b]
[list=1]
Expand All @@ -53,16 +58,17 @@ Describe what the mod does.

[line]

[size=4][b]Compatibility[/b][/size]
[size=4][b][color=#B8953E]Compatibility[/color][/b][/size]

[list]
[*]Compatible with Skyrim SE and AE.
[*]No ESP/ESL required.
[/list]
<!-- generated:end -->

[line]

[size=4][b]Credits[/b][/size]
[size=4][b][color=#B8953E]Credits[/color][/b][/size]

[list]
[*][url=https://skse.silverlock.org/]SKSE[/url] by the SKSE Team
Expand Down
168 changes: 168 additions & 0 deletions scripts/generate-nexus-page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Generate nexus-page BBCode from README.md user-facing content.

Reads the block between <!-- nexus:start --> and <!-- nexus:end --> in README.md,
converts it to BBCode, substitutes it into the <!-- generated:start/end --> region
of docs/nexus-page.md in memory, and prints the result to stdout.
Comment thread
codepuncher marked this conversation as resolved.

Usage:
python3 scripts/generate-nexus-page.py
python3 scripts/generate-nexus-page.py | xclip -selection clipboard
"""

import argparse
import re
import sys
from pathlib import Path

README = Path(__file__).parent.parent / "README.md"
NEXUS_TEMPLATE = Path(__file__).parent.parent / "docs" / "nexus-page.md"

NEXUS_START = "<!-- nexus:start -->"
NEXUS_END = "<!-- nexus:end -->"
GEN_START = "<!-- generated:start -->"
GEN_END = "<!-- generated:end -->"
BBCODE_FENCE_OPEN = "```bbcode"
BBCODE_FENCE_CLOSE = "```"


def extract_block(text: str, start_marker: str, end_marker: str) -> str:
start = text.find(start_marker)
if start == -1:
sys.exit(f"error: marker not found: {start_marker!r}")
end = text.find(end_marker, start + len(start_marker))
if end == -1:
sys.exit(f"error: marker not found: {end_marker!r}")
return text[start + len(start_marker):end].strip()
Comment thread
codepuncher marked this conversation as resolved.


def md_to_bbcode(md: str) -> str:
lines = md.splitlines()
output: list[str] = []
i = 0
first_heading = True

while i < len(lines):
line = lines[i]

# ## Heading
if line.startswith("## "):
heading = line[3:].strip()
if not first_heading:
output.append("")
output.append("[line]")
output.append("")
first_heading = False
output.append(f"[size=4][b][color=#B8953E]{heading}[/color][/b][/size]")
i += 1
continue

# Unordered list block
if line.startswith("- "):
items: list[str] = []
while i < len(lines) and lines[i].startswith("- "):
items.append(convert_inline(lines[i][2:].strip()))
i += 1
output.append("[list]")
for item in items:
output.append(f"[*]{item}")
output.append("[/list]")
continue

# Ordered list block — may be preceded by a bold label line
if re.match(r"\d+\. ", line):
items = []
while i < len(lines) and re.match(r"\d+\. ", lines[i]):
items.append(convert_inline(re.sub(r"^\d+\. ", "", lines[i])))
i += 1
output.append("[list=1]")
for item in items:
output.append(f"[*]{item}")
output.append("[/list]")
continue

# Bold label (e.g. **Mod manager (recommended):**)
if re.match(r"\*\*.+\*\*", line):
output.append(convert_inline(line))
i += 1
continue

# Blank line
if line.strip() == "":
output.append("")
i += 1
continue

# Plain paragraph (pass through inline conversion)
stripped = line.strip()
if stripped and not stripped.startswith("<!--"):
output.append(convert_inline(line))

i += 1

# Collapse consecutive blank lines to single blank
result = re.sub(r"\n{3,}", "\n\n", "\n".join(output))
return result.strip()


def convert_inline(text: str) -> str:
# [text](url) → [url=url]text[/url]
# Note: link text containing ] or URLs containing unbalanced ) are not supported.
text = re.sub(r"\[([^\]]+)\]\(([^)]*(?:\([^)]*\)[^)]*)*)\)", r"[url=\2]\1[/url]", text)
# **text** → [b]text[/b]
text = re.sub(r"\*\*(.+?)\*\*", r"[b]\1[/b]", text)
# `code` → [font=Courier New]code[/font]
text = re.sub(r"`([^`]+)`", r"[font=Courier New]\1[/font]", text)
return text


def extract_bbcode_fence(text: str) -> str:
start = text.find(BBCODE_FENCE_OPEN)
if start == -1:
sys.exit(f"error: {BBCODE_FENCE_OPEN!r} fence not found in nexus-page.md")
newline = text.find("\n", start)
if newline == -1:
sys.exit(f"error: no newline after {BBCODE_FENCE_OPEN!r} in nexus-page.md")
start = newline + 1
end = text.find(f"\n{BBCODE_FENCE_CLOSE}", start)
if end == -1:
sys.exit("error: closing ``` fence not found in nexus-page.md")
return text[start:end]
Comment thread
codepuncher marked this conversation as resolved.


def inject_generated(bbcode_content: str, generated: str) -> str:
start = bbcode_content.find(GEN_START)
if start == -1:
sys.exit(f"error: marker not found: {GEN_START!r}")
end = bbcode_content.find(GEN_END, start + len(GEN_START))
if end == -1:
sys.exit(f"error: marker not found: {GEN_END!r}")
return (
bbcode_content[:start]
+ GEN_START + "\n"
+ generated + "\n"
+ bbcode_content[end:]
Comment thread
codepuncher marked this conversation as resolved.
)


def main() -> None:
argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
).parse_args()

readme = README.read_text(encoding="utf-8")
template = NEXUS_TEMPLATE.read_text(encoding="utf-8")

md_block = extract_block(readme, NEXUS_START, NEXUS_END)
bbcode_generated = md_to_bbcode(md_block)

bbcode_content = extract_bbcode_fence(template)
result = inject_generated(bbcode_content, bbcode_generated)
result = result.replace(GEN_START + "\n", "", 1).replace("\n" + GEN_END, "", 1)

Comment thread
codepuncher marked this conversation as resolved.
print(result)


if __name__ == "__main__":
main()