Add new templates and tests for improved functionality

- Created index.html template for the homepage with service cards and partner logos.
- Added page_from_md.html template for rendering pages from markdown.
- Developed services.html template detailing various services offered.
- Implemented tests for link handling in markdown, ensuring external links open in new tabs and internal links function correctly.
- Enhanced markdown parser tests to validate heading extraction, content rendering, and link safety.
- Introduced utility tests for template rendering, HTML minification, and JavaScript minification.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-02 13:05:43 +02:00
parent 559a6e4c56
commit 9f0a216c5e
79 changed files with 4700 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
from lib.markdown_parser import markdown_to_html_lines
def test_external_link_gets_target_rel():
md = '[external](https://example.com)'
html = markdown_to_html_lines(md)
assert 'href="https://example.com"' in html
assert 'target="_blank"' in html
assert 'rel="noopener noreferrer"' in html
def test_internal_link_no_target():
md = '[internal](/about)'
html = markdown_to_html_lines(md)
assert 'href="/about"' in html
assert 'target=' not in html
+133
View File
@@ -0,0 +1,133 @@
import textwrap
from lib import markdown_parser
def make_empty_state():
return {
'page': {'title': None, 'sections': []},
'current_section': None,
'current_card': None,
'current_detail': None,
'content_buffer': [],
'detail_buffer': [],
}
def test_flush_detail_buffer():
state = make_empty_state()
state['current_detail'] = {'title': 'D', 'content': ''}
state['detail_buffer'] = ['Line 1', 'Line 2']
markdown_parser.flush_detail_buffer(state)
assert 'Line 1' in state['current_detail']['content']
assert state['detail_buffer'] == []
def test_flush_content_buffer_to_card():
state = make_empty_state()
state['current_card'] = {'title': 'C', 'content': ''}
state['content_buffer'] = ['para 1', 'para 2']
markdown_parser.flush_content_buffer_to_card(state)
assert 'para 1' in state['current_card']['content']
def test_flush_content_buffer_to_section():
state = make_empty_state()
state['current_section'] = {'title': 'S', 'content': '', 'cards': []}
state['content_buffer'] = ['a', 'b']
markdown_parser.flush_content_buffer_to_section(state)
assert 'a' in state['current_section']['content']
def test_start_section_and_card_and_detail_and_process_line():
state = make_empty_state()
# start a section
markdown_parser.start_section('Sec1', state)
assert state['current_section']['title'] == 'Sec1'
assert state['page']['sections'][0]['title'] == 'Sec1'
# start a card
markdown_parser.start_card('Card1', state)
assert state['current_card']['title'] == 'Card1'
assert state['page']['sections'][0]['cards'][0]['title'] == 'Card1'
# start a detail
markdown_parser.start_detail('Det1', state)
assert state['current_detail']['title'] == 'Det1'
# process a normal content line into detail_buffer
markdown_parser.process_line_with_state('Detail line 1', state)
assert 'Detail line 1' in state['detail_buffer'][0]
# process an H3 to start a new card
markdown_parser.process_line_with_state('### NewCard', state)
assert state['current_card']['title'] == 'NewCard'
# process an H2 to start a new section
markdown_parser.process_line_with_state('## NewSection', state)
assert state['current_section']['title'] == 'NewSection'
def test_start_card_without_section_is_noop():
state = make_empty_state()
markdown_parser.start_card('Orphan', state)
assert state['current_card'] is None
assert state['page']['sections'] == []
def test_start_detail_without_card_is_noop():
state = make_empty_state()
markdown_parser.start_detail('Detail', state)
assert state['current_detail'] is None
assert state['detail_buffer'] == []
def test_process_h4_without_card_is_treated_as_content():
state = make_empty_state()
markdown_parser.process_line_with_state('#### Heading without card', state)
assert '#### Heading without card' in state['content_buffer']
def test_process_new_h1_flushes_section_content():
state = make_empty_state()
markdown_parser.start_section('First', state)
state['content_buffer'].append('Paragraph text')
markdown_parser.process_line_with_state('# New Page', state)
assert 'Paragraph text' in state['page']['sections'][0]['content']
assert state['page']['title'] == 'New Page'
def test_build_component_structure_matches_integration():
md = textwrap.dedent("""
# Title
## Section A
### Card A
Intro A
#### Detail X
Line X
""")
page = markdown_parser.build_component_structure(md, 'f.md')
assert page['title'] == 'Title'
assert page['sections'][0]['title'] == 'Section A'
card = page['sections'][0]['cards'][0]
assert card['title'] == 'Card A'
assert 'Intro A' in card['content']
assert 'Detail X' in card['details'][0]['title']
+351
View File
@@ -0,0 +1,351 @@
import textwrap
import pytest
from bs4 import BeautifulSoup
from markdown import Markdown
from xml.etree.ElementTree import Element, SubElement
from lib.markdown_parser import (
HeadingCollector,
HeadingExtension,
build_component_structure,
markdown_to_html_lines,
parse_markdown_file,
)
def test_h4_creates_card_details():
md = textwrap.dedent("""
# Page Title
## Section One
### Card One
Card intro paragraph.
#### Detail A
Detail content line 1.
- item 1
- item 2
#### Detail B
Another detail paragraph with [a link](https://example.com).
""")
page = build_component_structure(md, "test.md")
assert page['title'] == 'Page Title'
assert len(page['sections']) == 1
section = page['sections'][0]
assert section['title'] == 'Section One'
assert len(section['cards']) == 1
card = section['cards'][0]
assert card['title'] == 'Card One'
# content should include the intro paragraph converted to HTML
assert 'Card intro paragraph' in card['content']
# details should be present
assert 'details' in card
assert len(card['details']) == 2
assert card['details'][0]['title'] == 'Detail A'
assert 'item 1' in card['details'][0]['content']
assert card['details'][1]['title'] == 'Detail B'
assert 'https://example.com' in card['details'][1]['content']
def test_section_content_preserves_lists_before_first_card():
md = textwrap.dedent("""
# Title
## Section One
- Item A
- Item B
### Card Title
Card body
""")
page = build_component_structure(md, "test.md")
section = page['sections'][0]
assert '<ul>' in section['content']
assert '<li>Item A</li>' in section['content']
assert section['cards'][0]['title'] == 'Card Title'
def test_card_content_keeps_lists_when_section_changes():
md = textwrap.dedent("""
# Title
## Section One
### Card One
- First
- Second
## Section Two
Details
""")
page = build_component_structure(md, "test.md")
first_section = page['sections'][0]
card = first_section['cards'][0]
assert '<ul>' in card['content']
assert '<li>First</li>' in card['content']
def test_card_body_renders_list_items():
md = textwrap.dedent("""
# Title
## Section
### Card
- Alpha
- Beta
- Gamma
""")
page = build_component_structure(md, "test.md")
card = page['sections'][0]['cards'][0]
assert '<ul>' in card['content']
assert '<li>Alpha</li>' in card['content']
assert '<li>Gamma</li>' in card['content']
def test_external_links_add_target_attributes():
md = textwrap.dedent("""
# Title
## Section
Visit [Example](https://example.com) now.
""")
page = build_component_structure(md, "test.md")
section_html = page['sections'][0]['content']
soup = BeautifulSoup(section_html, 'html.parser')
anchor = soup.find('a')
assert anchor is not None
assert anchor['href'] == 'https://example.com'
assert anchor.get('target') == '_blank'
assert anchor.get('rel') == ['noopener', 'noreferrer']
def test_unsafe_links_are_neutralized():
md = textwrap.dedent("""
# Title
## Section
[Bad](javascript:alert('xss'))
""")
page = build_component_structure(md, "test.md")
section_html = page['sections'][0]['content']
soup = BeautifulSoup(section_html, 'html.parser')
anchor = soup.find('a')
assert anchor is not None
assert anchor['href'] == '#unsafe'
assert 'target' not in anchor.attrs
assert 'rel' not in anchor.attrs
def test_index_page_sections_and_headings_extracted():
text = """
# Title
Main intro paragraph.
## Section 1
Section 1 intro.
- List item 1 a
- List item 1 b
## Section 2
Section 2 intro.
- List item 2 a
- List item 2 b
"""
md = Markdown(extensions=[HeadingExtension()])
md.convert(text)
collector = md.treeprocessors['heading_collector']
headings = [h for h in getattr(collector, 'headings', []) if 'level' in h]
assert headings[0]['level'] == 1
assert headings[0]['text'] == 'Title'
assert headings[1]['level'] == 2
assert headings[1]['text'] == 'Section 1'
assert headings[2]['level'] == 2
assert headings[2]['text'] == 'Section 2'
def test_index_page_main_intro_extracted():
text = """
# Title
This is the main introduction paragraph for the index page.
## Section 1
Section content.
"""
md = Markdown(extensions=[HeadingExtension()])
md.convert(text)
collector = md.treeprocessors['heading_collector']
main_intro = getattr(collector, 'main_intro', '')
assert 'This is the main introduction paragraph for the index page.' in main_intro
def test_heading_extension_collects_headings_and_lists():
md = Markdown(extensions=[HeadingExtension()])
md.convert(textwrap.dedent("""
# Title
## Section with **Bold** and [Link](#target)
- Item A
- [Item B](#b) and more
"""))
collector = md.treeprocessors['heading_collector']
headings = getattr(collector, 'headings', [])
assert headings[0]['level'] == 1
assert headings[0]['text'] == 'Title'
assert headings[1]['level'] == 2
assert headings[1]['text'] == 'Section with Bold and Link'
list_entry = next(entry for entry in headings if entry.get('type') == 'ul')
assert list_entry['items'][0] == 'Item A'
assert 'Item B' in list_entry['items'][1]
def test_heading_collector_extract_text_handles_children():
collector = HeadingCollector(Markdown())
parent = Element('p')
child = SubElement(parent, 'strong')
child.text = 'Bold'
child.tail = ' tail'
nested = SubElement(child, 'em')
nested.text = 'inner'
text = collector._extract_text(parent)
assert 'Bold' in text
assert 'tail' in text
assert 'inner' in text
collector.headings = []
collector._process_element(parent)
assert collector.headings == []
def test_parse_markdown_file_sets_title_from_filename(tmp_path):
file_path = tmp_path / 'sample_page.md'
file_path.write_text('Just text without heading', encoding='utf-8')
page = parse_markdown_file(str(file_path))
assert page['title'] == 'Sample Page'
def test_parse_markdown_file_missing(tmp_path):
missing = tmp_path / 'not_there.md'
with pytest.raises(FileNotFoundError):
parse_markdown_file(str(missing))
def test_markdown_to_html_lines_handles_empty_and_blank_links():
assert markdown_to_html_lines('') == ''
html = markdown_to_html_lines(
'Before [Empty]() and [External](https://example.com)')
soup = BeautifulSoup(html, 'html.parser')
anchors = soup.find_all('a')
assert anchors[0]['href'] == ''
assert 'target' not in anchors[0].attrs
assert anchors[1]['target'] == '_blank'
def test_markdown_images_normalize_src_and_alt():
html = markdown_to_html_lines(
'![IT Consulting](assets/icons/it-consulting.svg)')
soup = BeautifulSoup(html, 'html.parser')
image = soup.find('img')
assert image is not None
assert image['src'] == 'img/it-consulting.svg'
assert image['alt'] == 'IT Consulting'
def test_markdown_images_add_alt_fallback():
html = markdown_to_html_lines('![](logo.svg)')
soup = BeautifulSoup(html, 'html.parser')
image = soup.find('img')
assert image is not None
assert image['src'] == 'img/logo.svg'
assert image['alt'] == 'logo'
def test_markdown_heading_with_inline_image_preserves_image():
html = markdown_to_html_lines('## Heading ![Graphic](img/graphic.svg)')
soup = BeautifulSoup(html, 'html.parser')
heading = soup.find('h2')
image = heading.find('img') if heading else None
assert heading is not None
assert heading.get_text(strip=True) == 'Heading'
assert image is not None
assert image['src'] == 'img/graphic.svg'
assert image['alt'] == 'Graphic'
def test_build_component_structure_converts_section_images():
md = textwrap.dedent("""
# Title
## Section With Visual
Intro text before image.
![Vision Diagram](assets/diagrams/vision.png)
""")
page = build_component_structure(md, "section.md")
section_html = page['sections'][0]['content']
soup = BeautifulSoup(section_html, 'html.parser')
image = soup.find('img')
assert image is not None
assert image['src'] == 'img/vision.png'
assert image['alt'] == 'Vision Diagram'
def test_build_component_structure_preserves_external_image_src():
md = textwrap.dedent("""
# Title
## Section
![Remote Logo](https://cdn.example.com/logo.svg)
""")
page = build_component_structure(md, "external.md")
section_html = page['sections'][0]['content']
soup = BeautifulSoup(section_html, 'html.parser')
image = soup.find('img')
assert image is not None
assert image['src'] == 'https://cdn.example.com/logo.svg'
assert image['alt'] == 'Remote Logo'
+15
View File
@@ -0,0 +1,15 @@
from lib.markdown_parser import markdown_to_html_lines
def test_javascript_link_neutralized():
md = '[bad](javascript:alert(1))'
html = markdown_to_html_lines(md)
assert 'href="#unsafe"' in html
assert 'javascript:' not in html
def test_data_link_neutralized():
md = '[bad](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)'
html = markdown_to_html_lines(md)
assert 'href="#unsafe"' in html
assert 'data:' not in html
+101
View File
@@ -0,0 +1,101 @@
import os
from pathlib import Path
import pytest
from lib.utils import (
get_template_files,
render_template,
set_active_page_by_url,
minify_html,
js_minifier,
)
def test_get_template_files_filters_partials_and_non_html(
tmp_path: Path) -> None:
template_dir = tmp_path / "templates"
template_dir.mkdir()
(template_dir / "page.html").write_text("<h1>Page</h1>", encoding="utf-8")
(template_dir / "_partial.html").write_text("<p>Ignore</p>", encoding="utf-8")
(template_dir / "notes.txt").write_text("ignore", encoding="utf-8")
result = get_template_files(str(template_dir))
assert sorted(result) == ["page.html"]
def test_render_template_renders_with_context(tmp_path: Path) -> None:
template_dir = tmp_path / "templates"
template_dir.mkdir()
(template_dir /
"hello.html").write_text("Hello {{ name }}!", encoding="utf-8")
rendered = render_template(
"hello.html", {
"name": "World"}, template_dir=str(template_dir))
assert rendered == "Hello World!"
def test_set_active_page_by_url_marks_only_requested_page_active() -> None:
nav = {
"index.html": {"active": False},
"about.html": {"active": False},
}
set_active_page_by_url(nav, "about.html")
assert nav["about.html"]["active"] is True
assert nav["index.html"]["active"] is False
def test_minify_html_removes_comments_and_extra_whitespace() -> None:
html = "<div> Text </div> <!-- comment -->\n"
minified = minify_html(html)
assert minified == "<div> Text</div>"
def test_js_minifier_strips_comments_and_rewrites_files(
tmp_path: Path) -> None:
output_dir = tmp_path / "site"
js_dir = output_dir / "js"
js_dir.mkdir(parents=True)
js_path = js_dir / "app.js"
js_path.write_text(
"// comment\nvar x = 1; \n/* block */\nfunction test() { console.log(x); }\n",
encoding="utf-8",
)
# Ensure non-JS files are ignored
(js_dir / "readme.txt").write_text("skip", encoding="utf-8")
js_minifier(str(output_dir))
content = js_path.read_text(encoding="utf-8")
assert content == "var x = 1; function test() { console.log(x); }"
assert os.path.exists(js_dir / "readme.txt")
@pytest.mark.parametrize(
"initial, expected",
[
({"a.js": "const a = 1;"}, "const a = 1;"),
],
)
def test_js_minifier_handles_multiple_invocations(
tmp_path: Path, initial, expected) -> None:
output_dir = tmp_path / "dist"
js_dir = output_dir / "js"
js_dir.mkdir(parents=True)
for filename, content in initial.items():
(js_dir / filename).write_text(content, encoding="utf-8")
js_minifier(str(output_dir))
js_minifier(str(output_dir))
for filename in initial:
content = (js_dir / filename).read_text(encoding="utf-8")
assert content == expected