flowmark 0.6.0__tar.gz → 0.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowmark-0.6.0 → flowmark-0.6.2}/Makefile +7 -1
- {flowmark-0.6.0 → flowmark-0.6.2}/PKG-INFO +1 -1
- flowmark-0.6.2/devtools/benchmark.py +226 -0
- flowmark-0.6.2/docs/project/specs/active/plan-2026-01-14-list-spacing-control.md +329 -0
- flowmark-0.6.2/docs/project/specs/active/valid-2026-01-14-list-spacing-control.md +85 -0
- flowmark-0.6.2/docs/project/specs/done/plan-2026-01-14-atomic-tag-wrapping.md +424 -0
- flowmark-0.6.2/docs/project/specs/done/plan-2026-01-14-markdoc-tag-compatibility.md +475 -0
- flowmark-0.6.2/docs/project/specs/done/valid-2026-01-14-atomic-tag-wrapping.md +115 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/__init__.py +2 -2
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/cli.py +13 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/formats/flowmark_markdown.py +66 -11
- flowmark-0.6.2/src/flowmark/linewrapping/atomic_patterns.py +189 -0
- flowmark-0.6.2/src/flowmark/linewrapping/block_heuristics.py +151 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/linewrapping/line_wrappers.py +20 -12
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/linewrapping/markdown_filling.py +16 -5
- flowmark-0.6.2/src/flowmark/linewrapping/protocols.py +15 -0
- flowmark-0.6.2/src/flowmark/linewrapping/tag_handling.py +531 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/linewrapping/text_filling.py +4 -2
- flowmark-0.6.2/src/flowmark/linewrapping/text_wrapping.py +258 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/reformat_api.py +14 -3
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/typography/smartquotes.py +86 -44
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_escape_handling.py +3 -3
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_filling.py +2 -1
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_heading_spacing.py +2 -1
- flowmark-0.6.2/tests/test_list_spacing.py +410 -0
- flowmark-0.6.2/tests/test_tag_formatting.py +688 -0
- flowmark-0.6.2/tests/test_wrapping.py +1269 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/testdoc.expected.auto.md +292 -117
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/testdoc.expected.cleaned.md +291 -116
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/testdoc.expected.plain.md +292 -118
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/testdoc.expected.semantic.md +291 -116
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/testdoc.orig.md +232 -0
- flowmark-0.6.0/src/flowmark/linewrapping/text_wrapping.py +0 -313
- flowmark-0.6.0/tests/test_list_spacing.py +0 -312
- flowmark-0.6.0/tests/test_wrapping.py +0 -535
- {flowmark-0.6.0 → flowmark-0.6.2}/.copier-answers.yml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/automatic-shortcut-triggers.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/backward-compatibility-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/convex-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/flowmark.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/general-coding-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/general-comment-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/general-eng-assistant-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/general-style-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/general-testing-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/python-rules-opinionated.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/python-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/tool-development-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/typescript-cli-tool-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.cursor/rules/typescript-rules.mdc +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.github/workflows/ci.yml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.github/workflows/publish.yml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.gitignore +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.speculate/copier-answers.yml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.speculate/settings.yml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/.vscode/settings.json +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/LICENSE +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/README.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/devtools/lint.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/development.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/development.npm.sample.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/docs-overview.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-guidelines/general-tdd-guidelines.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-guidelines/golden-testing-guidelines.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-guidelines/typescript-dependency-injection-guidelines.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-guidelines/typescript-testing-guidelines.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/automatic-shortcut-triggers.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/backward-compatibility-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/convex-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/general-coding-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/general-comment-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/general-eng-assistant-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/general-style-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/general-testing-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/python-rules-opinionated.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/python-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/tool-development-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/typescript-cli-tool-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-rules/typescript-rules.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-setup/shortcut:setup-beads.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-setup/shortcut:setup-github-cli.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:cleanup-all.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:cleanup-remove-trivial-tests.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:cleanup-update-docstrings.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:coding-spike.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:commit-code.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:create-or-update-pr-with-validation-plan.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:create-or-update-validation-plan.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:create-pr-simple.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:implement-beads.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:implement-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:merge-upstream.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-architecture-doc.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-implementation-beads-from-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-implementation-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-plan-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-research-brief.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:new-validation-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:precommit-process.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:refine-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:review-all-code-specs-docs-convex.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:review-pr-and-fix-with-beads.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:review-pr.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:revise-architecture-doc.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:update-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/agent-shortcuts/shortcut:update-specs-status.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/archive/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/research-cli-golden-testing.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/research-code-coverage-typescript.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/research-convex-db-limits-best-practices.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/research-modern-typescript-cli-patterns.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/general/research/current/research-modern-typescript-monorepo-patterns.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/installation.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/markdoc-tag-support-plan.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/architecture/archive/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/architecture/current/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/architecture/template-architecture.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/research/archive/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/research/current/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/research/template-research-brief.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/active/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/backlog/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/done/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/paused/.empty +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/template-bugfix.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/template-implementation-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/template-plan-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/project/specs/template-validation-spec.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/docs/publishing.md +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/pyproject.toml +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/formats/frontmatter.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/linewrapping/sentence_split_regex.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/py.typed +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/transforms/doc_cleanups.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/transforms/doc_transforms.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/src/flowmark/typography/ellipses.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_alerts.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_cleanups.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_ellipses.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_fenced_code_blocks.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_frontmatter.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_ref_docs.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_sentences.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_smartquotes.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/test_width_options.py +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/tests/testdocs/.gitignore +0 -0
- {flowmark-0.6.0 → flowmark-0.6.2}/uv.lock +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
.DEFAULT_GOAL := default
|
|
6
6
|
|
|
7
|
-
.PHONY: default install lint test upgrade build clean format-docs
|
|
7
|
+
.PHONY: default install lint test upgrade build clean format-docs benchmark profile
|
|
8
8
|
|
|
9
9
|
default: format-docs install lint test
|
|
10
10
|
|
|
@@ -34,6 +34,12 @@ clean:
|
|
|
34
34
|
format-docs:
|
|
35
35
|
uv run flowmark --auto README.md docs/*.md
|
|
36
36
|
|
|
37
|
+
benchmark:
|
|
38
|
+
uv run devtools/benchmark.py --compare 0.6.0
|
|
39
|
+
|
|
40
|
+
profile:
|
|
41
|
+
uv run devtools/benchmark.py --profile
|
|
42
|
+
|
|
37
43
|
# Reset the expected reference docs to the actual ones currently produced.
|
|
38
44
|
reset-ref-docs:
|
|
39
45
|
cp tests/testdocs/testdoc.actual.auto.md tests/testdocs/testdoc.expected.auto.md
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Benchmark script for Flowmark performance testing.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# Benchmark current dev version
|
|
7
|
+
uv run devtools/benchmark.py
|
|
8
|
+
|
|
9
|
+
# Benchmark a specific released version
|
|
10
|
+
uv run devtools/benchmark.py --version 0.6.0
|
|
11
|
+
|
|
12
|
+
# Benchmark with profiling enabled
|
|
13
|
+
uv run devtools/benchmark.py --profile
|
|
14
|
+
|
|
15
|
+
# Compare current dev against a release
|
|
16
|
+
uv run devtools/benchmark.py --compare 0.6.0
|
|
17
|
+
|
|
18
|
+
# Custom test file and iterations
|
|
19
|
+
uv run devtools/benchmark.py --file tests/testdocs/testdoc.orig.md --iterations 10
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import cProfile
|
|
26
|
+
import io
|
|
27
|
+
import pstats
|
|
28
|
+
import statistics
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from textwrap import dedent
|
|
34
|
+
|
|
35
|
+
DEFAULT_TEST_FILE = Path("tests/testdocs/testdoc.orig.md")
|
|
36
|
+
DEFAULT_ITERATIONS = 10
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def benchmark_current(test_file: Path, iterations: int, semantic: bool = True) -> list[float]:
|
|
40
|
+
"""Benchmark the current dev version by importing directly."""
|
|
41
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
42
|
+
from flowmark import reformat_text
|
|
43
|
+
|
|
44
|
+
content = test_file.read_text()
|
|
45
|
+
print("Benchmarking current dev version")
|
|
46
|
+
print(f"File: {test_file} ({len(content)} chars, {len(content.splitlines())} lines)")
|
|
47
|
+
print(f"Running {iterations} iterations...\n")
|
|
48
|
+
|
|
49
|
+
times: list[float] = []
|
|
50
|
+
for i in range(iterations):
|
|
51
|
+
start = time.perf_counter()
|
|
52
|
+
reformat_text(content, semantic=semantic)
|
|
53
|
+
elapsed = time.perf_counter() - start
|
|
54
|
+
times.append(elapsed)
|
|
55
|
+
print(f" Run {i + 1}: {elapsed * 1000:.1f}ms")
|
|
56
|
+
|
|
57
|
+
return times
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def benchmark_version(
|
|
61
|
+
version: str, test_file: Path, iterations: int, semantic: bool = True
|
|
62
|
+
) -> list[float]:
|
|
63
|
+
"""Benchmark a specific released version using uvx."""
|
|
64
|
+
content = test_file.read_text()
|
|
65
|
+
print(f"Benchmarking v{version}")
|
|
66
|
+
print(f"File: {test_file} ({len(content)} chars, {len(content.splitlines())} lines)")
|
|
67
|
+
print(f"Running {iterations} iterations...\n")
|
|
68
|
+
|
|
69
|
+
benchmark_script = dedent(f'''
|
|
70
|
+
import time
|
|
71
|
+
import statistics
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
from flowmark import reformat_text
|
|
74
|
+
|
|
75
|
+
content = Path("{test_file}").read_text()
|
|
76
|
+
times = []
|
|
77
|
+
for i in range({iterations}):
|
|
78
|
+
start = time.perf_counter()
|
|
79
|
+
reformat_text(content, semantic={semantic})
|
|
80
|
+
elapsed = time.perf_counter() - start
|
|
81
|
+
times.append(elapsed)
|
|
82
|
+
print(f" Run {{i + 1}}: {{elapsed * 1000:.1f}}ms")
|
|
83
|
+
|
|
84
|
+
# Output times as comma-separated for parsing
|
|
85
|
+
print("TIMES:" + ",".join(str(t) for t in times))
|
|
86
|
+
''').strip()
|
|
87
|
+
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
["uvx", f"--with=flowmark=={version}", "python", "-c", benchmark_script],
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=300,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if result.returncode != 0:
|
|
96
|
+
print(f"Error running v{version}: {result.stderr}", file=sys.stderr)
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
# Print the run output (excluding the TIMES line)
|
|
100
|
+
for line in result.stdout.splitlines():
|
|
101
|
+
if not line.startswith("TIMES:"):
|
|
102
|
+
print(line)
|
|
103
|
+
|
|
104
|
+
# Parse times from output
|
|
105
|
+
for line in result.stdout.splitlines():
|
|
106
|
+
if line.startswith("TIMES:"):
|
|
107
|
+
return [float(t) for t in line[6:].split(",")]
|
|
108
|
+
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def print_stats(name: str, times: list[float]) -> None:
|
|
113
|
+
"""Print statistics for timing results."""
|
|
114
|
+
if not times:
|
|
115
|
+
print(f"{name}: No valid results")
|
|
116
|
+
return
|
|
117
|
+
print(f"\n{name}:")
|
|
118
|
+
print(f" Mean: {statistics.mean(times) * 1000:.1f}ms")
|
|
119
|
+
print(f" Median: {statistics.median(times) * 1000:.1f}ms")
|
|
120
|
+
print(f" Min: {min(times) * 1000:.1f}ms")
|
|
121
|
+
print(f" Max: {max(times) * 1000:.1f}ms")
|
|
122
|
+
if len(times) > 1:
|
|
123
|
+
print(f" StdDev: {statistics.stdev(times) * 1000:.1f}ms")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def profile_current(test_file: Path, semantic: bool = True) -> None:
|
|
127
|
+
"""Profile the current dev version and show top functions."""
|
|
128
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
129
|
+
from flowmark import reformat_text
|
|
130
|
+
|
|
131
|
+
content = test_file.read_text()
|
|
132
|
+
print("Profiling current dev version")
|
|
133
|
+
print(f"File: {test_file} ({len(content)} chars, {len(content.splitlines())} lines)\n")
|
|
134
|
+
|
|
135
|
+
profiler = cProfile.Profile()
|
|
136
|
+
profiler.enable()
|
|
137
|
+
reformat_text(content, semantic=semantic)
|
|
138
|
+
profiler.disable()
|
|
139
|
+
|
|
140
|
+
stream = io.StringIO()
|
|
141
|
+
stats = pstats.Stats(profiler, stream=stream)
|
|
142
|
+
stats.sort_stats("cumulative")
|
|
143
|
+
stats.print_stats(40)
|
|
144
|
+
print(stream.getvalue())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main() -> None:
|
|
148
|
+
parser = argparse.ArgumentParser(
|
|
149
|
+
description="Benchmark Flowmark performance",
|
|
150
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
151
|
+
epilog=__doc__,
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--version",
|
|
155
|
+
"-v",
|
|
156
|
+
help="Benchmark a specific released version (e.g., 0.6.0)",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--compare",
|
|
160
|
+
"-c",
|
|
161
|
+
help="Compare current dev against a specific version (e.g., 0.6.0)",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--profile",
|
|
165
|
+
"-p",
|
|
166
|
+
action="store_true",
|
|
167
|
+
help="Run profiling on current dev version",
|
|
168
|
+
)
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--file",
|
|
171
|
+
"-f",
|
|
172
|
+
type=Path,
|
|
173
|
+
default=DEFAULT_TEST_FILE,
|
|
174
|
+
help=f"Test file to use (default: {DEFAULT_TEST_FILE})",
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--iterations",
|
|
178
|
+
"-n",
|
|
179
|
+
type=int,
|
|
180
|
+
default=DEFAULT_ITERATIONS,
|
|
181
|
+
help=f"Number of iterations (default: {DEFAULT_ITERATIONS})",
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--plain",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="Use plain line wrapping instead of semantic",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
semantic = not args.plain
|
|
191
|
+
|
|
192
|
+
if not args.file.exists():
|
|
193
|
+
print(f"Error: Test file not found: {args.file}", file=sys.stderr)
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
if args.profile:
|
|
197
|
+
profile_current(args.file, semantic=semantic)
|
|
198
|
+
elif args.compare:
|
|
199
|
+
current_times = benchmark_current(args.file, args.iterations, semantic=semantic)
|
|
200
|
+
print()
|
|
201
|
+
old_times = benchmark_version(args.compare, args.file, args.iterations, semantic=semantic)
|
|
202
|
+
|
|
203
|
+
print_stats("Current dev version", current_times)
|
|
204
|
+
print_stats(f"v{args.compare}", old_times)
|
|
205
|
+
|
|
206
|
+
if current_times and old_times:
|
|
207
|
+
current_mean = statistics.mean(current_times)
|
|
208
|
+
old_mean = statistics.mean(old_times)
|
|
209
|
+
ratio = current_mean / old_mean
|
|
210
|
+
diff_pct = (ratio - 1) * 100
|
|
211
|
+
print("\nComparison:")
|
|
212
|
+
if ratio > 1:
|
|
213
|
+
print(f" Current is {diff_pct:.1f}% slower than v{args.compare}")
|
|
214
|
+
else:
|
|
215
|
+
print(f" Current is {abs(diff_pct):.1f}% faster than v{args.compare}")
|
|
216
|
+
print(f" Ratio: {ratio:.2f}x")
|
|
217
|
+
elif args.version:
|
|
218
|
+
times = benchmark_version(args.version, args.file, args.iterations, semantic=semantic)
|
|
219
|
+
print_stats(f"v{args.version}", times)
|
|
220
|
+
else:
|
|
221
|
+
times = benchmark_current(args.file, args.iterations, semantic=semantic)
|
|
222
|
+
print_stats("Current dev version", times)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Plan Spec: List Spacing Control (`--list-spacing` Option)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This is a technical design doc for adding a `--list-spacing` CLI option to Flowmark that
|
|
6
|
+
controls how tight vs loose list formatting is handled during Markdown normalization.
|
|
7
|
+
|
|
8
|
+
**Terminology Note**: CommonMark uses "tight" and "loose" as the official terms for list
|
|
9
|
+
spacing. A tight list has no blank lines between items; a loose list has blank lines.
|
|
10
|
+
|
|
11
|
+
## Background
|
|
12
|
+
|
|
13
|
+
Flowmark has historically converted **all lists to loose format** (blank lines between
|
|
14
|
+
items). This was an intentional design decision documented in
|
|
15
|
+
[markdown_filling.py:51-52](src/flowmark/linewrapping/markdown_filling.py#L51-L52):
|
|
16
|
+
|
|
17
|
+
> "Also enforces that all list items have two newlines between them, so that items are
|
|
18
|
+
> separate paragraphs when viewed as plaintext."
|
|
19
|
+
|
|
20
|
+
**CommonMark Spec Reference:**
|
|
21
|
+
|
|
22
|
+
Per [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/#lists):
|
|
23
|
+
|
|
24
|
+
| List Type | Definition | HTML Rendering |
|
|
25
|
+
|-----------|------------|----------------|
|
|
26
|
+
| **Tight** | No blank lines between items | Content NOT wrapped in `<p>` tags |
|
|
27
|
+
| **Loose** | Blank lines between items OR internal blank lines | Content wrapped in `<p>` tags |
|
|
28
|
+
|
|
29
|
+
A list becomes loose if:
|
|
30
|
+
1. Any blank line separates consecutive items
|
|
31
|
+
2. Any item contains multiple block elements with blank lines between them
|
|
32
|
+
|
|
33
|
+
**Current Behavior Issues:**
|
|
34
|
+
|
|
35
|
+
- Cannot round-trip documents (tight lists become loose)
|
|
36
|
+
- Author intent for compact lists is not preserved
|
|
37
|
+
- HTML rendering changes semantically (`<li>text</li>` vs `<li><p>text</p></li>`)
|
|
38
|
+
- Files become larger due to extra newlines
|
|
39
|
+
|
|
40
|
+
**Key Finding:**
|
|
41
|
+
|
|
42
|
+
Marko (the Markdown parser) exposes a `tight` boolean attribute on parsed `List` elements,
|
|
43
|
+
making it feasible to detect and preserve the original list style.
|
|
44
|
+
|
|
45
|
+
## Summary of Task
|
|
46
|
+
|
|
47
|
+
Add a `--list-spacing` CLI option with three modes:
|
|
48
|
+
|
|
49
|
+
| Mode | Behavior |
|
|
50
|
+
|------|----------|
|
|
51
|
+
| `preserve` | Keep lists tight or loose as authored (NEW DEFAULT) |
|
|
52
|
+
| `loose` | Convert all lists to loose format (current behavior) |
|
|
53
|
+
| `tight` | Convert all lists to tight format where possible |
|
|
54
|
+
|
|
55
|
+
This will be exposed as:
|
|
56
|
+
- CLI: `--list-spacing={preserve|tight|loose}` (default: `preserve`)
|
|
57
|
+
- API: `list_spacing: Literal["preserve", "tight", "loose"] = "preserve"` parameter
|
|
58
|
+
|
|
59
|
+
## Backward Compatibility
|
|
60
|
+
|
|
61
|
+
**BACKWARD COMPATIBILITY REQUIREMENTS:**
|
|
62
|
+
|
|
63
|
+
- **Code types, methods, and function signatures**: KEEP DEPRECATED - existing callers
|
|
64
|
+
using default parameters should get the new `preserve` behavior, but we need to ensure
|
|
65
|
+
no breaking changes for anyone explicitly passing parameters
|
|
66
|
+
|
|
67
|
+
- **Library APIs**: KEEP DEPRECATED for `reformat_text()` and `fill_markdown()` - add new
|
|
68
|
+
`list_spacing` parameter with default that maintains current output for most use cases
|
|
69
|
+
|
|
70
|
+
- **CLI behavior**: BREAKING CHANGE (intentional) - the new default `preserve` will
|
|
71
|
+
produce different output for tight lists. This is the desired behavior per user request.
|
|
72
|
+
|
|
73
|
+
- **File formats**: SUPPORT BOTH - existing Markdown files should produce similar output
|
|
74
|
+
for already-loose lists; tight lists will now remain tight by default
|
|
75
|
+
|
|
76
|
+
**Key Compatibility Note:**
|
|
77
|
+
|
|
78
|
+
The `testdoc.expected.*.md` files will need to be regenerated since they contain tight
|
|
79
|
+
lists that are currently converted to loose. The new expected output will preserve tight
|
|
80
|
+
lists.
|
|
81
|
+
|
|
82
|
+
## Stage 1: Planning Stage
|
|
83
|
+
|
|
84
|
+
### Minimum Viable Feature
|
|
85
|
+
|
|
86
|
+
1. Add `--list-spacing` CLI option with `preserve`, `loose`, and `tight` modes
|
|
87
|
+
2. Default to `preserve` (breaking change from current `loose` behavior)
|
|
88
|
+
3. Detect list tightness via Marko's `list.tight` attribute
|
|
89
|
+
4. Thread spacing mode through to list rendering
|
|
90
|
+
5. Update test expectations
|
|
91
|
+
|
|
92
|
+
### Not In Scope
|
|
93
|
+
|
|
94
|
+
- Per-list overrides (e.g., `<!-- flowmark: loose -->` comments)
|
|
95
|
+
- Heuristic-based auto-detection of "should be tight" vs "should be loose"
|
|
96
|
+
- Different behavior for nested lists vs top-level lists
|
|
97
|
+
|
|
98
|
+
### Acceptance Criteria
|
|
99
|
+
|
|
100
|
+
1. `flowmark --list-spacing=preserve file.md` preserves tight lists as tight
|
|
101
|
+
2. `flowmark --list-spacing=preserve file.md` preserves loose lists as loose
|
|
102
|
+
3. `flowmark --list-spacing=loose file.md` converts all lists to loose (current behavior)
|
|
103
|
+
4. `flowmark --list-spacing=tight file.md` converts all lists to tight where possible
|
|
104
|
+
5. `flowmark file.md` uses `preserve` by default
|
|
105
|
+
6. Nested lists are controlled independently (each preserves its own tightness)
|
|
106
|
+
7. Lists with multi-paragraph items remain loose regardless of mode (CommonMark requirement)
|
|
107
|
+
8. `make lint` and `make test` pass
|
|
108
|
+
|
|
109
|
+
### Resolved Questions
|
|
110
|
+
|
|
111
|
+
- [x] **Q1**: Should `tight` mode force-convert loose lists, or only convert lists that
|
|
112
|
+
CAN be tight (no multi-paragraph items)?
|
|
113
|
+
**Decision**: Only convert lists that can be tight; leave multi-paragraph items loose.
|
|
114
|
+
|
|
115
|
+
- [x] **Q2**: How should nested lists behave? Same mode as parent, or independent?
|
|
116
|
+
**Decision**: Each list independently uses the mode—nested tight stays tight, nested
|
|
117
|
+
loose stays loose in `preserve` mode.
|
|
118
|
+
|
|
119
|
+
- [x] **Q3**: Should `--auto` flag continue to use `loose`, or switch to `preserve`?
|
|
120
|
+
**Decision**: All modes use `preserve` as the default. Explicit `--list-spacing=X`
|
|
121
|
+
overrides any other settings (including `--auto`).
|
|
122
|
+
|
|
123
|
+
## Stage 2: Architecture Stage
|
|
124
|
+
|
|
125
|
+
### Current Architecture
|
|
126
|
+
|
|
127
|
+
List rendering is handled in
|
|
128
|
+
[flowmark_markdown.py:226-261](src/flowmark/formats/flowmark_markdown.py#L226-L261):
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
def render_list(self, element: block.List) -> str:
|
|
132
|
+
# Currently ignores element.tight
|
|
133
|
+
result: list[str] = []
|
|
134
|
+
for i, child in enumerate(element.children):
|
|
135
|
+
# ... configure prefix ...
|
|
136
|
+
with self.container(prefix, subsequent_indent):
|
|
137
|
+
rendered_item = self.render(child)
|
|
138
|
+
result.append(rendered_item)
|
|
139
|
+
return "".join(result)
|
|
140
|
+
|
|
141
|
+
def render_list_item(self, element: block.ListItem) -> str:
|
|
142
|
+
result = ""
|
|
143
|
+
# We want all list items to have two newlines between them.
|
|
144
|
+
if self._suppress_item_break:
|
|
145
|
+
self._suppress_item_break = False
|
|
146
|
+
else:
|
|
147
|
+
result += self._second_prefix.strip() + "\n" # <-- Always adds blank line
|
|
148
|
+
result += self.render_children(element)
|
|
149
|
+
return result
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Key Issue**: `render_list_item()` always adds a blank line between items via
|
|
153
|
+
`_suppress_item_break` logic, regardless of the original list tightness.
|
|
154
|
+
|
|
155
|
+
### Proposed Changes
|
|
156
|
+
|
|
157
|
+
#### Change 1: Add `ListSpacing` Enum
|
|
158
|
+
|
|
159
|
+
**Location**: New in `flowmark_markdown.py` or separate types module
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from enum import StrEnum
|
|
163
|
+
|
|
164
|
+
class ListSpacing(StrEnum):
|
|
165
|
+
preserve = "preserve"
|
|
166
|
+
loose = "loose"
|
|
167
|
+
tight = "tight"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Change 2: Thread Spacing Mode Through Renderer
|
|
171
|
+
|
|
172
|
+
**Location**: `MarkdownNormalizer.__init__()` and `flowmark_markdown()`
|
|
173
|
+
|
|
174
|
+
Add `_list_spacing: ListSpacing` instance variable that controls list rendering behavior.
|
|
175
|
+
|
|
176
|
+
#### Change 3: Modify `render_list()` to Detect Tightness
|
|
177
|
+
|
|
178
|
+
**Location**: `render_list()` method
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
def render_list(self, element: block.List) -> str:
|
|
182
|
+
# Determine effective tightness based on mode
|
|
183
|
+
if self._list_spacing == ListSpacing.preserve:
|
|
184
|
+
is_tight = element.tight
|
|
185
|
+
elif self._list_spacing == ListSpacing.tight:
|
|
186
|
+
is_tight = self._can_be_tight(element) # Check for multi-para items
|
|
187
|
+
else: # loose
|
|
188
|
+
is_tight = False
|
|
189
|
+
|
|
190
|
+
# Pass is_tight to render_list_item via instance variable
|
|
191
|
+
old_tight = self._current_list_tight
|
|
192
|
+
self._current_list_tight = is_tight
|
|
193
|
+
# ... existing rendering logic ...
|
|
194
|
+
self._current_list_tight = old_tight
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Change 4: Modify `render_list_item()` to Respect Tightness
|
|
198
|
+
|
|
199
|
+
**Location**: `render_list_item()` method
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
def render_list_item(self, element: block.ListItem) -> str:
|
|
203
|
+
result = ""
|
|
204
|
+
# Only add blank line for loose lists
|
|
205
|
+
if not self._current_list_tight:
|
|
206
|
+
if self._suppress_item_break:
|
|
207
|
+
self._suppress_item_break = False
|
|
208
|
+
else:
|
|
209
|
+
result += self._second_prefix.strip() + "\n"
|
|
210
|
+
# ... rest of method
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Change 5: Update CLI and API
|
|
214
|
+
|
|
215
|
+
**Locations**:
|
|
216
|
+
- `cli.py`: Add `--list-spacing` argument
|
|
217
|
+
- `reformat_api.py`: Add `list_spacing` parameter to `reformat_text()`, `reformat_file()`,
|
|
218
|
+
`reformat_files()`
|
|
219
|
+
- `markdown_filling.py`: Add `list_spacing` parameter to `fill_markdown()`
|
|
220
|
+
|
|
221
|
+
### Architecture Diagram
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
CLI: --list-spacing=preserve
|
|
225
|
+
│
|
|
226
|
+
▼
|
|
227
|
+
reformat_file()
|
|
228
|
+
│
|
|
229
|
+
▼
|
|
230
|
+
fill_markdown(list_spacing="preserve")
|
|
231
|
+
│
|
|
232
|
+
▼
|
|
233
|
+
flowmark_markdown(line_wrapper, list_spacing="preserve")
|
|
234
|
+
│
|
|
235
|
+
▼
|
|
236
|
+
MarkdownNormalizer(line_wrapper, list_spacing="preserve")
|
|
237
|
+
│
|
|
238
|
+
├─► render_list() ──► reads element.tight
|
|
239
|
+
│ │
|
|
240
|
+
│ ▼
|
|
241
|
+
│ Determines effective tightness based on mode
|
|
242
|
+
│ │
|
|
243
|
+
│ ▼
|
|
244
|
+
└─► render_list_item() ──► adds blank line only if not tight
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Stage 3: Implementation Phases
|
|
248
|
+
|
|
249
|
+
### Phase 1: Core Infrastructure
|
|
250
|
+
|
|
251
|
+
- [x] Add `ListSpacing` enum
|
|
252
|
+
- [x] Add `list_spacing` parameter to `MarkdownNormalizer.__init__()`
|
|
253
|
+
- [x] Add `_current_list_tight` instance variable for threading state
|
|
254
|
+
- [x] Add `_can_be_tight()` helper method for `tight` mode
|
|
255
|
+
|
|
256
|
+
### Phase 2: List Rendering Changes
|
|
257
|
+
|
|
258
|
+
- [x] Modify `render_list()` to compute effective tightness
|
|
259
|
+
- [x] Modify `render_list_item()` to conditionally add blank lines
|
|
260
|
+
- [x] Handle edge cases (nested lists, code blocks in items, etc.)
|
|
261
|
+
|
|
262
|
+
### Phase 3: API Updates
|
|
263
|
+
|
|
264
|
+
- [x] Add `list_spacing` parameter to `fill_markdown()`
|
|
265
|
+
- [x] Add `list_spacing` parameter to `reformat_text()`
|
|
266
|
+
- [x] Add `list_spacing` parameter to `reformat_file()` and `reformat_files()`
|
|
267
|
+
|
|
268
|
+
### Phase 4: CLI Updates
|
|
269
|
+
|
|
270
|
+
- [x] Add `--list-spacing` argument to CLI
|
|
271
|
+
- [x] Update `--auto` flag to use `preserve`
|
|
272
|
+
- [x] Update help text
|
|
273
|
+
|
|
274
|
+
### Phase 5: Testing and Documentation
|
|
275
|
+
|
|
276
|
+
- [x] Add unit tests for `ListSpacing` modes
|
|
277
|
+
- [x] Update `test_list_spacing.py` with preserve/tight mode tests
|
|
278
|
+
- [x] Regenerate `testdoc.expected.*.md` files
|
|
279
|
+
- [x] Update README/docstrings
|
|
280
|
+
|
|
281
|
+
## Stage 4: Validation Stage
|
|
282
|
+
|
|
283
|
+
### Test Strategy
|
|
284
|
+
|
|
285
|
+
1. **Unit Tests**: Test each spacing mode with tight and loose input lists
|
|
286
|
+
2. **Edge Cases**:
|
|
287
|
+
- Nested lists (tight outer, loose inner and vice versa)
|
|
288
|
+
- Lists with code blocks (must remain loose per CommonMark)
|
|
289
|
+
- Lists with multiple paragraphs (must remain loose)
|
|
290
|
+
- Mixed content lists
|
|
291
|
+
3. **Integration Tests**: Full document processing with various modes
|
|
292
|
+
4. **Regression Tests**: Ensure loose mode produces identical output to current behavior
|
|
293
|
+
|
|
294
|
+
### Test Cases
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# Tight list stays tight in preserve mode
|
|
298
|
+
def test_tight_list_preserved():
|
|
299
|
+
input = "- one\n- two\n- three"
|
|
300
|
+
output = fill_markdown(input, list_spacing="preserve")
|
|
301
|
+
assert output == "- one\n- two\n- three\n"
|
|
302
|
+
|
|
303
|
+
# Tight list becomes loose in loose mode
|
|
304
|
+
def test_tight_list_to_loose():
|
|
305
|
+
input = "- one\n- two\n- three"
|
|
306
|
+
output = fill_markdown(input, list_spacing="loose")
|
|
307
|
+
assert output == "- one\n\n- two\n\n- three\n"
|
|
308
|
+
|
|
309
|
+
# Loose list becomes tight in tight mode
|
|
310
|
+
def test_loose_list_to_tight():
|
|
311
|
+
input = "- one\n\n- two\n\n- three"
|
|
312
|
+
output = fill_markdown(input, list_spacing="tight")
|
|
313
|
+
assert output == "- one\n- two\n- three\n"
|
|
314
|
+
|
|
315
|
+
# Multi-paragraph item stays loose even in tight mode
|
|
316
|
+
def test_multi_para_stays_loose():
|
|
317
|
+
input = "- para1\n\n para2\n- item2"
|
|
318
|
+
output = fill_markdown(input, list_spacing="tight")
|
|
319
|
+
# Item with multiple paragraphs forces loose
|
|
320
|
+
assert "\n\n" in output
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Success Criteria
|
|
324
|
+
|
|
325
|
+
- [x] All existing tests pass (with updated expectations)
|
|
326
|
+
- [x] New spacing mode tests pass
|
|
327
|
+
- [x] `make lint` passes
|
|
328
|
+
- [x] `make test` passes
|
|
329
|
+
- [x] Manual testing with real documents confirms expected behavior
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Feature Validation: List Spacing Control (`--list-spacing` Option)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This is a validation spec for the `--list-spacing` CLI option that controls how tight vs
|
|
6
|
+
loose list formatting is handled during Markdown normalization.
|
|
7
|
+
|
|
8
|
+
**Feature Plan:** [plan-2026-01-14-list-spacing-control.md](plan-2026-01-14-list-spacing-control.md)
|
|
9
|
+
|
|
10
|
+
## Stage 4: Validation Stage
|
|
11
|
+
|
|
12
|
+
## Automated Validation (Testing Performed)
|
|
13
|
+
|
|
14
|
+
### Unit Testing
|
|
15
|
+
|
|
16
|
+
The following unit tests have been added or updated in `tests/test_list_spacing.py`:
|
|
17
|
+
|
|
18
|
+
**Preserve Mode Tests (default):**
|
|
19
|
+
- `test_tight_list_preserved` - Tight lists stay tight
|
|
20
|
+
- `test_loose_list_preserved` - Loose lists stay loose
|
|
21
|
+
- `test_preserve_is_default` - Confirm preserve is the default
|
|
22
|
+
- `test_numbered_list_preserve` - Numbered lists preserve tightness
|
|
23
|
+
|
|
24
|
+
**Loose Mode Tests:**
|
|
25
|
+
- `test_tight_list_to_loose` - Tight lists become loose
|
|
26
|
+
- `test_loose_list_stays_loose` - Loose lists stay loose
|
|
27
|
+
- `test_numbered_list_to_loose` - Numbered lists become loose
|
|
28
|
+
|
|
29
|
+
**Tight Mode Tests:**
|
|
30
|
+
- `test_loose_list_to_tight` - Loose lists become tight
|
|
31
|
+
- `test_tight_list_stays_tight` - Tight lists stay tight
|
|
32
|
+
- `test_multi_para_stays_loose_in_tight_mode` - Multi-paragraph items force loose (CommonMark
|
|
33
|
+
requirement)
|
|
34
|
+
|
|
35
|
+
**Nested List Tests:**
|
|
36
|
+
- `test_nested_lists_independent_preserve` - Each nested list independently preserves
|
|
37
|
+
tightness
|
|
38
|
+
- `test_nested_lists_loose_outer_tight_inner` - Mixed outer/inner tightness preserved
|
|
39
|
+
|
|
40
|
+
**Complex Content Tests:**
|
|
41
|
+
- `test_list_items_with_code_blocks_preserve` - Code blocks preserve tightness
|
|
42
|
+
- `test_list_items_with_code_blocks_loose` - Code blocks with loose mode
|
|
43
|
+
- `test_list_items_with_quote_blocks` - Quote blocks preserve tightness
|
|
44
|
+
|
|
45
|
+
**Spacing Normalization Tests:**
|
|
46
|
+
- `test_input_spacing_normalization_loose` - Various input spacings normalize to loose
|
|
47
|
+
- `test_input_spacing_normalization_tight` - Various input spacings normalize to tight
|
|
48
|
+
- `test_complex_content_with_loose_mode` - Complex content spacing in loose mode
|
|
49
|
+
- `test_multi_paragraph_spacing_loose_mode` - Multi-paragraph items in loose mode
|
|
50
|
+
|
|
51
|
+
**Other Updated Tests:**
|
|
52
|
+
- `test_escape_handling.py` - Updated to use `list_spacing="loose"` for escape tests
|
|
53
|
+
- `test_filling.py` - Updated to use `list_spacing="loose"` for backward compatibility
|
|
54
|
+
- `test_heading_spacing.py` - Updated to use `list_spacing="loose"` where needed
|
|
55
|
+
|
|
56
|
+
### Integration and End-to-End Testing
|
|
57
|
+
|
|
58
|
+
- All 172 tests pass (`make test`)
|
|
59
|
+
- Linting passes (`make lint`)
|
|
60
|
+
- Reference documents (`tests/testdocs/testdoc.expected.*.md`) regenerated for new default
|
|
61
|
+
behavior
|
|
62
|
+
|
|
63
|
+
## Manual Testing Needed
|
|
64
|
+
|
|
65
|
+
All core functionality is covered by automated tests. Manual review is only needed for:
|
|
66
|
+
|
|
67
|
+
### Code Review Items
|
|
68
|
+
|
|
69
|
+
1. **Help text clarity** - Review `flowmark --help` to confirm the `--list-spacing` option
|
|
70
|
+
description is clear and understandable
|
|
71
|
+
|
|
72
|
+
2. **Breaking change communication** - Confirm the breaking change (default behavior changed
|
|
73
|
+
from `loose` to `preserve`) is adequately documented in:
|
|
74
|
+
- The commit message
|
|
75
|
+
- The PR description
|
|
76
|
+
- The plan spec
|
|
77
|
+
|
|
78
|
+
### No Additional Manual Testing Required
|
|
79
|
+
|
|
80
|
+
The following are all covered by automated tests:
|
|
81
|
+
- All three modes (`preserve`, `loose`, `tight`)
|
|
82
|
+
- Nested list handling
|
|
83
|
+
- Multi-paragraph items
|
|
84
|
+
- CLI argument parsing
|
|
85
|
+
- File processing (in-place and stdout)
|