diff --git a/remarkbox/lib/pandoc.py b/remarkbox/lib/pandoc.py index e0433be..9703fe0 100644 --- a/remarkbox/lib/pandoc.py +++ b/remarkbox/lib/pandoc.py @@ -206,7 +206,15 @@ def convert(source, from_format="markdown", to_format="html5", title=None, stand cmd.append("--standalone") if title: - cmd.extend(["--metadata", "title={}".format(title)]) + # For HTML-family outputs (and PDF, which we render through HTML via + # wkhtmltopdf), set the template's pagetitle so we get without + # an extra title-block in the body — the rendered document already + # carries its own H1 from the thread data. For every other format, + # set the proper document metadata. + if to_format in ("html", "html5", "html4", "chunkedhtml", "pdf"): + cmd.extend(["-V", "pagetitle={}".format(title)]) + else: + cmd.extend(["--metadata", "title={}".format(title)]) # PDF needs explicit engine since no pdflatex. if to_format == "pdf": @@ -240,6 +248,33 @@ def convert(source, from_format="markdown", to_format="html5", title=None, stand return result.stdout +def _node_data_as_markdown(node): + """Return node.data rendered as markdown. + + Nodes authored in non-markdown source_formats (rst, mediawiki, latex, html, + ...) keep their raw source in .data. For export we need real markdown, so + convert through pandoc when needed. A pandoc failure falls back to the raw + source — better a rough dump than an empty export. + """ + if not node.data: + return "" + src_fmt = (getattr(node, "source_format", None) or "markdown").lower() + if src_fmt in ("markdown", "gfm", "commonmark", "commonmark_x"): + return node.data + try: + return convert( + node.data, from_format=src_fmt, to_format="markdown", + standalone=False, + ).rstrip() + except Exception: + log.exception( + "Pandoc failed converting node %s from %s to markdown; " + "falling back to raw source", + node.id, src_fmt, + ) + return node.data + + def node_tree_to_markdown(root_node, nodes, include_root=True): """Render a node tree as a nested markdown document. @@ -271,12 +306,11 @@ def node_tree_to_markdown(root_node, nodes, include_root=True): lines = [] - if include_root and root_node.title: - lines.append("# {}".format(root_node.title)) - lines.append("") - + # Thread bodies already carry their own H1 (or RST ==== underline that + # becomes H1 after conversion). Don't prepend another `# {title}` — it + # triples the title when pandoc then also adds a title-block in HTML/PDF. if include_root and root_node.data: - lines.append(root_node.data) + lines.append(_node_data_as_markdown(root_node)) lines.append("") def _render_children(parent_id, depth): @@ -290,7 +324,7 @@ def node_tree_to_markdown(root_node, nodes, include_root=True): lines.append("{} {} — {}".format("#" * heading_level, author, date)) lines.append("") if child.data: - lines.append(child.data) + lines.append(_node_data_as_markdown(child)) lines.append("") _render_children(child.id, depth + 1) @@ -325,7 +359,7 @@ def namespace_to_markdown(namespace, roots, node_fetcher): lines.append("## {}".format(chapter_title)) lines.append("") if root.data: - lines.append(root.data) + lines.append(_node_data_as_markdown(root)) lines.append("") # Fetch all nodes in this thread @@ -354,7 +388,7 @@ def namespace_to_markdown(namespace, roots, node_fetcher): )) lines.append("") if child.data: - lines.append(child.data) + lines.append(_node_data_as_markdown(child)) lines.append("") _render(child.id, depth + 1) diff --git a/remarkbox/tests/test_pandoc.py b/remarkbox/tests/test_pandoc.py index c15da4e..37f77bf 100644 --- a/remarkbox/tests/test_pandoc.py +++ b/remarkbox/tests/test_pandoc.py @@ -173,7 +173,9 @@ class TestNodeTreeToMarkdown(unittest.TestCase): def test_single_root_node(self): root = MockNode(1, title="Thread Title", data="Root content.") md = node_tree_to_markdown(root, []) - self.assertIn("# Thread Title", md) + # We no longer prepend `# {title}`; the thread data carries its own + # heading. Duplicating it produced three title copies in HTML/PDF. + self.assertNotIn("# Thread Title", md) self.assertIn("Root content.", md) def test_root_with_children(self): @@ -181,7 +183,6 @@ class TestNodeTreeToMarkdown(unittest.TestCase): child = MockNode(2, data="Reply text.", parent_id=1, created=1, user=MockUser("alice")) md = node_tree_to_markdown(root, [child]) - self.assertIn("# Thread", md) self.assertIn("Root.", md) self.assertIn("alice", md) self.assertIn("Reply text.", md) diff --git a/remarkbox/tests/test_undigg.py b/remarkbox/tests/test_undigg.py index 2ef0da1..ebc8df5 100644 --- a/remarkbox/tests/test_undigg.py +++ b/remarkbox/tests/test_undigg.py @@ -123,7 +123,8 @@ class TestExportThread(UndiggFunctionalTests): ) self.assertEqual(res.status_int, 200) self.assertIn("text/markdown", res.content_type) - self.assertIn(b"Test Thread", res.body) + # Markdown export is a short-circuit — no pandoc, no title block; + # we render the thread data exactly so it carries its own heading. self.assertIn(b"Thread content.", res.body) def test_export_thread_html(self):