From 6686564a254b63ef7d8f4d984a62a73199fc2d7b Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Fri, 18 Sep 2026 00:47:31 -0400 Subject: [PATCH] feat(website): add --link-css and --embed-css for custom CSS (#288) generate_website.py can layer custom CSS on top of the built-in style chosen with --style. --embed-css appends a local file to the page's {external_css_link} +""" + + # Continue with body + html_content += f""" + +
+
+ + + + + +
+ {nav_html} + +
+
+
+
+

{escape_html(bot_name)}

+
+
+ {escape_html(introduction).replace(chr(10), '
')}{channels_suffix} +
+
+
+ +
+ {commands_html} + {channels_html} +
+ +
+

Generated command reference for {escape_html(bot_name)}

+
+
+
+ + + +""" + + return html_content + + +def generate_builtin_css(style: str) -> str: + """Generate the built-in CSS for a given style""" + return f""" :root {{ {STYLES[style]['css_vars']} }} @@ -2368,136 +2516,7 @@ def generate_html(bot_name: str, title: str, introduction: str, commands: list[t }} /* Style-specific CSS overrides */ - {STYLES[style].get('css_overrides', '')} - - - -
-
- - - - - -
- {nav_html} - -
-
-
-
-

{escape_html(bot_name)}

-
-
- {escape_html(introduction).replace(chr(10), '
')}{channels_suffix} -
-
-
- -
- {commands_html} - {channels_html} -
- -
-

Generated command reference for {escape_html(bot_name)}

-
-
-
- - - -""" - - return html_content + {STYLES[style].get('css_overrides', '')}""" def list_styles(): @@ -2513,7 +2532,7 @@ def list_styles(): print(f" {' ' * max_len} {style_info['description']}\n") -def generate_samples(config_file): +def generate_samples(config_file, link_css: Optional[str] = None, custom_css: Optional[str] = None): """Generate sample HTML files for all styles with an index page.""" logger = logging.getLogger(__name__) @@ -2582,7 +2601,9 @@ def generate_samples(config_file): commands=sorted_commands, monitor_channels=monitor_channels, channels_data=channels_data, - style=style_name + style=style_name, + link_css=link_css, + custom_css=custom_css ) output_path = os.path.join(output_dir, f'{style_name}.html') @@ -2730,6 +2751,18 @@ def main(): help='Generate sample pages for all styles with an index.html' ) + parser.add_argument( + '--link-css', + metavar='URL', + help='Link a stylesheet after the built-in style so its rules override it' + ) + + parser.add_argument( + '--embed-css', + metavar='FILE', + help='Inline CSS from a local file after the built-in style so its rules override it' + ) + args = parser.parse_args() # Handle --list-styles flag @@ -2737,9 +2770,19 @@ def main(): list_styles() sys.exit(0) + # Read --embed-css up front so a bad path fails before plugins load + custom_css = None + if args.embed_css: + try: + with open(args.embed_css, encoding='utf-8') as f: + custom_css = f.read() + except (OSError, UnicodeDecodeError) as e: + logger.error(f"Could not read --embed-css file {args.embed_css}: {e}") + sys.exit(1) + # Handle --sample flag if args.sample: - generate_samples(args.config) + generate_samples(args.config, link_css=args.link_css, custom_css=custom_css) sys.exit(0) config_file = args.config @@ -2822,7 +2865,7 @@ def main(): # Generate HTML logger.info("Generating HTML...") - html_content = generate_html(bot_name, title, introduction, sorted_commands, monitor_channels, channels_data, style) + html_content = generate_html(bot_name, title, introduction, sorted_commands, monitor_channels, channels_data, style, args.link_css, custom_css) # Create website directory website_dir = os.path.join(bot_root, "website") diff --git a/tests/test_generate_website_include_css.py b/tests/test_generate_website_include_css.py new file mode 100644 index 0000000..692771a --- /dev/null +++ b/tests/test_generate_website_include_css.py @@ -0,0 +1,137 @@ +"""Tests for the --link-css and --embed-css options in generate_website.py""" + +import sys + +import pytest + +import generate_website +from generate_website import STYLES, generate_builtin_css, generate_html + + +def _render(**kwargs) -> str: + return generate_html( + bot_name="TestBot", + title="Test Site", + introduction="Test intro", + commands=[], + monitor_channels=[], + channels_data={}, + **kwargs, + ) + + +def test_generate_builtin_css_includes_css_variables(): + css = generate_builtin_css('default') + + assert ':root' in css + assert '--bg-primary' in css + assert '--accent-blue' in css + assert 'body {' in css + + +def test_generate_builtin_css_includes_style_overrides(): + css = generate_builtin_css('minimalist') + + assert STYLES['minimalist']['css_overrides'].strip() in css + + +def test_default_output_has_no_custom_css(): + html = _render(style='default') + + assert 'Custom CSS overrides' not in html + stylesheets = [line for line in html.splitlines() if 'rel="stylesheet"' in line] + assert len(stylesheets) == 1 + assert 'fonts.googleapis.com/css2' in stylesheets[0] + + +def test_backwards_compatible_call_without_css_params(): + html = generate_html("TestBot", "Test Site", "Test intro", [], [], {}, 'default') + + assert '') < html.index(link) < html.index('') + assert 'fonts.googleapis.com' in html + assert '--bg-primary' in html + + +def test_link_css_is_escaped(): + html = _render(style='default', link_css='style.css?v=1&theme=dark">') + + assert 'href="style.css?v=1&theme=dark"><script>' in html + assert '