Compare commits

..

4 commits

Author SHA1 Message Date
Yax
13d3d653de Make internal helper functions private
Added underscore prefix to functions used only internally:
- _strip_tags_and_truncate (used only in parse_post_file)
- _parse_headers (used only in parse_post_file)
- _rfc_2822_format (used only in parse_post_file)
- _make_links_absolute (used only in parse_post_file)
- _strip_html_tags (used only in parse_post_file)
- _get_friendly_date (used only in _setup_page_params)
- _exit_program (used only in monitor.py main loop)

This improves encapsulation by clearly marking implementation details.
2026-01-07 13:42:43 +01:00
Yax
424ea20bcf Refactor function names for clarity and accuracy
Renamed functions to better reflect their actual behavior:
- truncate → strip_tags_and_truncate (does both operations)
- fix_relative_links → make_links_absolute (converts, not fixes)
- clean_html_tag → strip_html_tags (more explicit)
- clean_site → rebuild_site_directory (removes and recreates)
- read_headers → parse_headers (parses, not just reads)
- read_content → parse_post_file (full processing pipeline)
- get_nb_of_comments → get_comment_count (removes French abbreviation)
2026-01-07 13:42:43 +01:00
Yax
f5df515d2b Add comprehensive docstrings to all functions
Added Google-style docstrings to all functions and classes in makesite.py and monitor.py with descriptions, argument types, and return values.
2026-01-07 13:42:43 +01:00
Yax
584904bbf9 Add type checking support with mypy
Added mypy and type stubs for external dependencies (requests, Pygments) to dev dependencies. Type checking now passes cleanly for makesite.py and monitor.py.
2026-01-07 13:42:43 +01:00
3 changed files with 138 additions and 30 deletions

View file

@ -28,7 +28,18 @@ FRENCH_MONTHS = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
class HighlightRenderer(mistune.HTMLRenderer):
"""Custom Mistune renderer that adds syntax highlighting to code blocks using Pygments."""
def block_code(self, code, info=None):
"""Render code blocks with syntax highlighting.
Args:
code: The code content to render
info: Optional language identifier for syntax highlighting
Returns:
str: HTML with syntax-highlighted code or plain pre/code tags
"""
if info:
lexer = get_lexer_by_name(info, stripall=True)
formatter = html.HtmlFormatter()
@ -60,20 +71,20 @@ def log(msg, *args):
sys.stderr.write(msg.format(*args) + "\n")
def truncate(text, words=25):
"""Remove tags and truncate text to the specified number of words."""
def _strip_tags_and_truncate(text, words=25):
"""Remove HTML tags and truncate text to the specified number of words."""
return " ".join(re.sub(r"(?s)<.*?>", " ", text).split()[:words])
def read_headers(text):
"""Parse headers in text and yield (key, value, end-index) tuples."""
def _parse_headers(text):
"""Parse HTML comment headers and yield (key, value, end-index) tuples."""
for match in re.finditer(r"\s*<!--\s*(.+?)\s*:\s*(.+?)\s*-->\s*|.+", text):
if not match.group(1):
break
yield match.group(1), match.group(2), match.end()
def rfc_2822_format(date_str):
def _rfc_2822_format(date_str):
"""Convert yyyy-mm-dd date string to RFC 2822 format date string."""
d = datetime.datetime.strptime(date_str, "%Y-%m-%d")
return d \
@ -96,8 +107,8 @@ def slugify(value):
return re.sub(r"[-\s]+", "-", value)
def read_content(filename, params):
"""Read content and metadata from file into a dictionary."""
def parse_post_file(filename, params):
"""Parse post file: read, extract metadata, convert markdown, and generate summary."""
# Read file content.
text = fread(filename)
@ -108,7 +119,7 @@ def read_content(filename, params):
# Read headers.
end = 0
for key, val, end in read_headers(text):
for key, val, end in _parse_headers(text):
content[key] = val
# slugify post title
@ -121,20 +132,20 @@ def read_content(filename, params):
if filename.endswith((".md", ".mkd", ".mkdn", ".mdown", ".markdown")):
summary_index = text.find("<!-- more")
if summary_index > 0:
summary = markdown(clean_html_tag(text[:summary_index]))
summary = markdown(_strip_html_tags(text[:summary_index]))
else:
summary = truncate(markdown(clean_html_tag(text)))
summary = _strip_tags_and_truncate(markdown(_strip_html_tags(text)))
clean_text = text.replace("<!-- more -->", "")
text = markdown(clean_text)
else:
summary = truncate(text)
summary = _strip_tags_and_truncate(text)
# Update the dictionary with content and RFC 2822 date.
content.update(
{
"content": text,
"content_rss": fix_relative_links(params["site_url"], text),
"rfc_2822_date": rfc_2822_format(content["date"]),
"content_rss": _make_links_absolute(params["site_url"], text),
"rfc_2822_date": _rfc_2822_format(content["date"]),
"summary": summary,
}
)
@ -142,16 +153,16 @@ def read_content(filename, params):
return content
def fix_relative_links(site_url, text):
"""Absolute links needed in RSS feed"""
def _make_links_absolute(site_url, text):
"""Convert relative links to absolute URLs for RSS feed."""
# TODO externalize links replacement configuration
return text \
.replace("src=\"/images/20", "src=\"" + site_url + "/images/20") \
.replace("href=\"/20", "href=\"" + site_url + "/20")
def clean_html_tag(text):
"""Remove HTML tags."""
def _strip_html_tags(text):
"""Remove HTML tags from text."""
while True:
original_text = text
text = re.sub(r"<\w+.*?>", "", text)
@ -171,6 +182,15 @@ def render(template, **params):
def get_header_list_value(header_name, page_params):
"""Extract and parse a space-separated list from a header value.
Args:
header_name: Name of the header to extract (e.g., 'category', 'tag')
page_params: Dict containing page parameters
Returns:
list: List of stripped string values from the header
"""
header_list = []
if header_name in page_params:
for s in page_params[header_name].split(" "):
@ -265,7 +285,15 @@ def _process_comments(page_params, stacosys_url, comment_layout,
return len(comments), comments_html, comment_section_html
def get_friendly_date(date_str):
def _get_friendly_date(date_str):
"""Convert date string to French-formatted readable date.
Args:
date_str: Date string in YYYY-MM-DD format
Returns:
str: French-formatted date (e.g., "15 janv. 2024")
"""
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d")
french_month = FRENCH_MONTHS[dt.month - 1]
return f"{dt.day:02d} {french_month} {dt.year}"
@ -306,7 +334,7 @@ def _setup_page_params(content, params):
page_params["header"] = ""
page_params["footer"] = ""
page_params["date_path"] = page_params["date"].replace("-", "/")
page_params["friendly_date"] = get_friendly_date(page_params["date"])
page_params["friendly_date"] = _get_friendly_date(page_params["date"])
page_params["year"] = page_params["date"].split("-")[0]
page_params["post_url"] = (
page_params["year"] + "/" + page_params["slug"] + "/"
@ -323,7 +351,7 @@ def make_posts(
for posix_path in Path(src).glob(src_pattern):
src_path = str(posix_path)
content = read_content(src_path, params)
content = parse_post_file(src_path, params)
# render text / summary for basic fields
content["content"] = render(content["content"], **params)
@ -376,7 +404,7 @@ def make_notes(
for posix_path in Path(src).glob(src_pattern):
src_path = str(posix_path)
content = read_content(src_path, params)
content = parse_post_file(src_path, params)
# render text / summary for basic fields
content["content"] = render(content["content"], **params)
@ -407,7 +435,17 @@ def make_list(
posts, dst, list_layout, item_layout,
header_layout, footer_layout, **params
):
"""Generate list page for a blog."""
"""Generate list page for a blog.
Args:
posts: List of post dictionaries to include in the list
dst: Destination path for the generated HTML file
list_layout: Template for the overall list page
item_layout: Template for individual list items
header_layout: Template for page header (None to skip)
footer_layout: Template for page footer (None to skip)
**params: Additional parameters for template rendering
"""
# header
if header_layout is None:
@ -447,6 +485,16 @@ def make_list(
def create_blog(page_layout, list_in_page_layout, params):
"""Create blog posts and paginated index pages.
Args:
page_layout: Template for individual pages
list_in_page_layout: Template for list pages wrapped in page layout
params: Global site parameters
Returns:
list: Sorted list of all post dictionaries (newest first)
"""
banner_layout = fread("layout/banner.html")
paging_layout = fread("layout/paging.html")
post_layout = fread("layout/post.html")
@ -509,6 +557,14 @@ def create_blog(page_layout, list_in_page_layout, params):
def generate_categories(list_in_page_layout, item_nosummary_layout,
posts, params):
"""Generate category pages grouping posts by category.
Args:
list_in_page_layout: Template for list pages
item_nosummary_layout: Template for list items without summaries
posts: List of all blog posts
params: Global site parameters
"""
category_title_layout = fread("layout/category_title.html")
cat_post = {}
for post in posts:
@ -532,6 +588,15 @@ def generate_categories(list_in_page_layout, item_nosummary_layout,
def generate_archives(blog_posts, list_in_page_layout, item_nosummary_layout,
archive_title_layout, params):
"""Generate archives page with all blog posts.
Args:
blog_posts: List of all blog posts
list_in_page_layout: Template for list pages
item_nosummary_layout: Template for list items without summaries
archive_title_layout: Template for archive page header
params: Global site parameters
"""
make_list(
blog_posts,
"_site/archives/index.html",
@ -545,6 +610,14 @@ def generate_archives(blog_posts, list_in_page_layout, item_nosummary_layout,
def generate_notes(page_layout, archive_title_layout,
list_in_page_layout, params):
"""Generate notes pages and notes index.
Args:
page_layout: Template for individual pages
archive_title_layout: Template for notes index header
list_in_page_layout: Template for list pages
params: Global site parameters
"""
note_layout = fread("layout/note.html")
item_note_layout = fread("layout/item_note.html")
note_layout = render(page_layout, content=note_layout)
@ -569,6 +642,12 @@ def generate_notes(page_layout, archive_title_layout,
def generate_rss_feeds(posts, params):
"""Generate RSS feeds: main feed and per-tag feeds.
Args:
posts: List of all blog posts
params: Global site parameters
"""
rss_xml = fread("layout/rss.xml")
rss_item_xml = fread("layout/rss_item.xml")
@ -606,6 +685,12 @@ def generate_rss_feeds(posts, params):
def generate_sitemap(posts, params):
"""Generate XML sitemap for all posts.
Args:
posts: List of all blog posts
params: Global site parameters
"""
sitemap_xml = fread("layout/sitemap.xml")
sitemap_item_xml = fread("layout/sitemap_item.xml")
make_list(
@ -620,6 +705,14 @@ def generate_sitemap(posts, params):
def get_params(param_file):
"""Load site parameters from JSON file with defaults.
Args:
param_file: Path to JSON parameters file
Returns:
dict: Site parameters with defaults and loaded values
"""
# Default parameters.
params = {
"title": "Blog",
@ -636,18 +729,24 @@ def get_params(param_file):
return params
def clean_site():
def rebuild_site_directory():
"""Remove existing _site directory and recreate from static files."""
if os.path.isdir("_site"):
shutil.rmtree("_site")
shutil.copytree("static", "_site")
def main(param_file):
"""Main entry point for static site generation.
Args:
param_file: Path to JSON parameters file
"""
params = get_params(param_file)
# Create a new _site directory from scratch.
clean_site()
rebuild_site_directory()
# Load layouts.
page_layout = fread("layout/page.html")

View file

@ -14,7 +14,12 @@ def fread(filename):
return f.read()
def get_nb_of_comments():
def get_comment_count():
"""Fetch the total number of comments from Stacosys API.
Returns:
int: Total comment count, or 0 if request fails
"""
req_url = params["stacosys_url"] + "/comments/count"
query_params = dict(
token=params["stacosys_token"]
@ -23,7 +28,8 @@ def get_nb_of_comments():
return 0 if not resp.ok else int(resp.json()["count"])
def exit_program():
def _exit_program():
"""Exit the program with status code 0."""
sys.exit(0)
@ -39,14 +45,14 @@ if os.path.isfile("params.json"):
params.update(json.loads(fread("params.json")))
external_check_cmd = params["external_check"]
initial_count = get_nb_of_comments()
initial_count = get_comment_count()
print(f"Comments = {initial_count}")
while True:
# check number of comments every 60 seconds
for _ in range(15):
time.sleep(60)
if initial_count != get_nb_of_comments():
exit_program()
if initial_count != get_comment_count():
_exit_program()
# check if git repo changed every 15 minutes
if external_check_cmd and os.system(external_check_cmd):
exit_program()
_exit_program()

View file

@ -16,4 +16,7 @@ dependencies = [
[dependency-groups]
dev = [
"black>=24.10.0",
"mypy>=1.19.1",
"types-pygments>=2.19.0.20251121",
"types-requests>=2.32.4.20260107",
]