From 008a5aa38c043a6d471b3c194eac2ae657aa8e26 Mon Sep 17 00:00:00 2001 From: Tyler Gibbs Date: Fri, 24 Jul 2026 01:33:23 -0500 Subject: [PATCH] support display titles for documentation files --- WRITING_DOCS.md | 11 ++++++++- openscad_docsgen/blocks.py | 14 +++++++++--- openscad_docsgen/parser.py | 14 +++++++++--- tests/test_blocks.py | 29 +++++++++++++++++++++++ tests/test_parser.py | 47 +++++++++++++++++++++++++++++++++++++- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/WRITING_DOCS.md b/WRITING_DOCS.md index a4c6826..9415e2e 100644 --- a/WRITING_DOCS.md +++ b/WRITING_DOCS.md @@ -100,6 +100,16 @@ You can specify what group of files this .scad file is a part of with the `// Fi This affects the ordering of files in Table of Contents and CheatSheet files. This doesn't generate any output text otherwise. +FileTitle Block +--------------- + +You can give a file a display title that is shown instead of its source filename in generated page headings, tables of contents, sidebars, and cheat sheets: + + // FileTitle: Basic Shapes + +The filename from the `File` or `LibFile` block is still used for output files and link targets. + + FileSummary Block ----------------- @@ -1142,4 +1152,3 @@ You can also use the DefineHeader block in the config file to make custom block DefineHeader(Table;Headers=^Anchor Name|Position): Extra Anchors - diff --git a/openscad_docsgen/blocks.py b/openscad_docsgen/blocks.py index 7db137c..3ecfea5 100644 --- a/openscad_docsgen/blocks.py +++ b/openscad_docsgen/blocks.py @@ -351,13 +351,19 @@ def __init__(self, title, subtitle, body, origin): self.includes = [] self.common_code = [] self.footnotes = [] + self.file_title = "" self.summary = "" self.group = "" + @property + def display_title(self): + return self.file_title if self.file_title else self.subtitle + def get_data(self): d = super().get_data() d["includes"] = self.includes d["commoncode"] = self.common_code + d["filetitle"] = self.file_title d["group"] = self.group d["summary"] = self.summary d["footnotes"] = [ @@ -387,7 +393,7 @@ def get_tocfile_lines(self, controller, target, n=1, currfile=""): sect for sect in self.children if isinstance(sect, SectionBlock) ] - link = self.get_link(target, label=self.subtitle, currfile=currfile) + link = self.get_link(target, label=self.display_title, currfile=currfile) out = [] out.extend(target.header("{}. {}".format(n, link), lev=target.SECTION, esc=False)) if self.summary: @@ -419,12 +425,14 @@ def get_cheatsheet_lines(self, controller, target): lines.extend(child.get_cheatsheet_lines(controller, target)) out = [] if lines: - out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.SUBSECTION)) + title = self.file_title if self.file_title else str(self) + out.extend(target.header(title, lev=target.SUBSECTION)) out.extend(lines) return out def get_file_lines(self, controller, target): - out = target.header(str(self), lev=target.FILE) + title = self.file_title if self.file_title else str(self) + out = target.header(title, lev=target.FILE) out.extend(target.markdown_block(self.get_markdown_body(controller, target))) for child in self.children: if not isinstance(child, SectionBlock): diff --git a/openscad_docsgen/parser.py b/openscad_docsgen/parser.py index ba0a522..ae3bfcc 100644 --- a/openscad_docsgen/parser.py +++ b/openscad_docsgen/parser.py @@ -378,6 +378,13 @@ def _parse_block(self, lines, line_num=0, src_file=None): raise DocsGenException(title, "Must provide a subtitle when declaring block:") self._check_filenode(title, origin) self.curr_file_block.summary = subtitle.strip() + elif title == "FileTitle": + if body: + raise DocsGenException(title, "Body not supported, while declaring block:") + if not subtitle: + raise DocsGenException(title, "Must provide a subtitle when declaring block:") + self._check_filenode(title, origin) + self.curr_file_block.file_title = subtitle.strip() elif title == "FileGroup": if not subtitle: raise DocsGenException(title, "Must provide a subtitle when declaring block:") @@ -799,8 +806,9 @@ def write_toc_file(self): if fblock.group != group: continue file = fblock.subtitle - anch = target.header_link("{}. {}".format(fnum+1, file)) - link = target.get_link(file, anchor=anch, literalize=False) + title = fblock.display_title + anch = target.header_link("{}. {}".format(fnum+1, title)) + link = target.get_link(title, anchor=anch, literalize=False) filelink = target.get_link("docs", file=file, literalize=False) tags = {tag: text for tag, text, origin in fblock.footnotes} marks = target.mouseover_tags(tags, "#file-footnotes") @@ -1053,7 +1061,7 @@ def write_sidebar_file(self): if fblock.group != group: continue file = fblock.subtitle - link = target.get_link(file, file=file, literalize=False) + link = target.get_link(fblock.display_title, file=file, literalize=False) for mark, note, origin in fblock.footnotes: try: if mark not in footmarks: diff --git a/tests/test_blocks.py b/tests/test_blocks.py index b7588b2..e8b9d44 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -151,6 +151,35 @@ def test_generic_block_get_data(): assert d["line"] == 5 +# --- FileBlock --- + +def test_file_block_uses_filename_as_default_display_title(): + block = FileBlock("LibFile", "mylib.scad", [], make_origin("mylib.scad")) + assert block.display_title == "mylib.scad" + + +def test_file_block_uses_explicit_display_title(): + block = FileBlock("LibFile", "mylib.scad", [], make_origin("mylib.scad")) + block.file_title = "Core Shapes" + SectionBlock("Section", "Basics", [], make_origin("mylib.scad"), parent=block) + assert block.display_title == "Core Shapes" + assert block.get_data()["filetitle"] == "Core Shapes" + + +def test_file_block_heading_uses_explicit_display_title(): + block = FileBlock("LibFile", "mylib.scad", [], make_origin("mylib.scad")) + block.file_title = "Core Shapes" + lines = block.get_file_lines(make_controller(), make_target()) + assert lines[0] == "# Core Shapes" + assert "mylib.scad" not in lines[0] + + +def test_file_block_heading_keeps_legacy_title_by_default(): + block = FileBlock("LibFile", "mylib.scad", [], make_origin("mylib.scad")) + lines = block.get_file_lines(make_controller(), make_target()) + assert lines[0] == "# LibFile: mylib.scad" + + def test_sort_children_back_blocks_last(): parent = GenericBlock("Parent", "p", [], make_origin()) c_example = GenericBlock("Example", "e", [], make_origin(), parent=parent) diff --git a/tests/test_parser.py b/tests/test_parser.py index e90fb24..130e883 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -96,7 +96,19 @@ def test_header_pat_constant(): def make_parser(tmp_path): target = Target_Wiki(docs_dir=str(tmp_path)) - opts = types.SimpleNamespace(target=target, strict=False, quiet=True) + opts = types.SimpleNamespace( + target=target, + strict=False, + quiet=True, + gen_toc=True, + gen_index=False, + gen_topics=False, + gen_glossary=False, + gen_cheat=False, + sidebar_header=[], + sidebar_middle=[], + sidebar_footer=[], + ) return DocsGenParser(opts) @@ -143,6 +155,39 @@ def test_parse_libfile_creates_file_block(tmp_path, monkeypatch): assert parser.file_blocks[0].subtitle == "test.scad" +# --- Integration: parse FileTitle block --- + +FILE_TITLE_SCAD = """\ +// LibFile: test.scad +// FileTitle: Core Shapes +""" + + +def test_parse_file_title_sets_display_title(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + parser = make_parser(tmp_path) + parser.parse_lines(FILE_TITLE_SCAD.splitlines(), src_file="test.scad") + assert parser.file_blocks[0].file_title == "Core Shapes" + assert parser.file_blocks[0].display_title == "Core Shapes" + + +def test_file_title_is_used_in_toc_and_sidebar(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + parser = make_parser(tmp_path) + parser.parse_lines(FILE_TITLE_SCAD.splitlines(), src_file="test.scad") + + parser.write_toc_file() + parser.write_sidebar_file() + + toc = (tmp_path / "TOC.md").read_text() + sidebar = (tmp_path / "_Sidebar.md").read_text() + assert "[Core Shapes](#1-core-shapes)" in toc + assert "## 1. [Core Shapes](test.scad)" in toc + assert "[Core Shapes](test.scad)" in sidebar + assert "[test.scad]" not in toc + assert "[test.scad]" not in sidebar + + # --- Integration: parse Section --- SECTION_SCAD = """\