From d25221bb74f2f33e5eddfdfbc1b43c5922319e3f Mon Sep 17 00:00:00 2001 From: istos Date: Fri, 31 Jul 2026 15:01:26 +0200 Subject: [PATCH] The drawer reads a wrapped list as one item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit md() split a block into physical lines and made each one a unit. Task files are hard-wrapped at ~74 columns, so the second line of an item became its own bullet, `- [ ]` rendered as a literal bracket pair, nested lists flattened, and prose kept the author's ragged edge as
. "Enough for these task files" was exactly what it was not. Lists are now grouped into logical items before rendering: a new item begins only at a marker, and a line without one is continuation text joined with a space. Indentation is honoured — a marker past its level opens a nested list, a shallower one closes back to the level that fits — and one entry point serves both bullets and ordered lists, so an
    nests under a ")) + + def test_a_wrapped_child_is_still_one_child(self): + html = self.render( + "- parent\n" + " - the child wraps across\n" + " two source lines\n") + self.assertIn("
  1. the child wraps across two source lines
  2. ", html) + + def test_deeper_nesting_degrades_rather_than_breaks(self): + html = self.render( + "- a\n - b\n - c\n- d\n") + self.assertEqual(html.count("")) + for text in ("a", "b", "c", "d"): + self.assertIn(f"
  3. {text}", html) + + def test_a_nested_ordered_list_under_a_bullet_keeps_its_tag(self): + html = self.render("- parent\n 1. first\n 2. second\n") + self.assertIn("
    1. first
    2. second
    ", html) + + +@unittest.skipUnless(NODE, "node is needed to run the page's own md()") +class ReflowTests(RendererCase): + """Prose wraps to the drawer, not to the author's editor.""" + + def test_a_paragraph_has_no_hard_break(self): + html = self.render( + "The renderer is line-based and the task files are\n" + "hard-wrapped, so almost every list on the board comes\n" + "out wrong.\n") + self.assertNotIn("
    ", html) + self.assertIn("task files are hard-wrapped", html) + + def test_a_blockquote_reflows_too(self): + html = self.render("> a quoted line\n> and its continuation\n" + .replace(">", ">")) + self.assertNotIn("
    ", html) + self.assertIn("a quoted line and its continuation", html) + + def test_no_br_survives_anywhere_in_the_corpus(self): + """Every card on the board plus AGENTS.md: the author's wrap column + must not reach the browser.""" + docs = sorted(ROOT.glob("tasks/*/*.md")) + [ROOT / "AGENTS.md"] + self.assertGreater(len(docs), 5, "no task files found to render") + for doc in docs: + with self.subTest(doc=doc.relative_to(ROOT).as_posix()): + self.assertNotIn("
    ", self.render( + doc.read_text(encoding="utf-8"))) + + +def source_bullets(src: str) -> int: + """How many logical list items a document contains, counted from the + source the way a reader counts them: markers only, fences skipped, and + only in blocks that actually open with one.""" + marker = re.compile(r"^[ \t]*(?:[-*]|\d+\.)[ \t]+") + total, fence = 0, False + for block in re.split(r"\n{2,}", src): + ticks = block.count("```") + if fence or block.startswith("```"): + fence = (ticks % 2 == 0) if fence else (ticks % 2 == 1) + continue + lines = block.rstrip().split("\n") + if len(lines) >= 2 and re.match(r"^\s*\|.*\|\s*$", lines[0]) \ + and re.match(r"^\s*\|[\s:|-]+\|\s*$", lines[1]): + continue # a table, not a list + if marker.match(lines[0]): + total += sum(1 for l in lines if marker.match(l)) + return total + + +@unittest.skipUnless(NODE, "node is needed to run the page's own md()") +class CorpusTests(RendererCase): + """The whole board, not a fixture: one bullet per marker, no more.""" + + def docs(self) -> list[Path]: + found = sorted(ROOT.glob("tasks/*/*.md")) + [ROOT / "AGENTS.md"] + self.assertGreater(len(found), 5, "no task files found to render") + return found + + def test_every_document_renders_one_bullet_per_marker(self): + """Before the fix a hard-wrapped item produced a bullet per source + line; this is the acceptance criterion applied to every card.""" + for doc in self.docs(): + with self.subTest(doc=doc.relative_to(ROOT).as_posix()): + src = doc.read_text(encoding="utf-8") + self.assertEqual(self.render(src).count("'), html.count("(.*?)", html, re.S).group(1) + self.assertEqual(body.rstrip("\n").split("\n"), [ + ".task-manager/", + "├── AGENTS.md ← This file", + "│ ├── VERSION, board.py", + "└── manager/", + ]) + + def test_a_fence_spanning_blank_lines_still_closes(self): + """The fence state machine spans blocks — a blank line inside a + fence must not end it, and a bullet inside must stay literal.""" + html = self.render("```\nfirst\n\n- not a bullet\n```\n\nafter\n") + self.assertEqual(html.count("
    "), 1)
    +        self.assertNotIn("
  4. ", html) + self.assertIn("

    after

    ", html) + + def test_a_table_after_a_list_is_still_a_table(self): + """Card 30's wrong/right table is the live case.""" + html = self.render( + "- a bullet that wraps\n onto a second line\n\n" + "| Turn 1 says | bench actually |\n| --- | --- |\n" + "| `bench.toml` | `manager/local/.env` |\n") + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertEqual(html.count("What to build", html) + self.assertIn("
    ", html) + self.assertIn("
  5. item
  6. ", html) + + def test_html_in_the_source_is_still_escaped(self): + html = self.render("- an item with in it\n") + self.assertNotIn("
    Turn 1 saysbench.toml