From a587a6d5b30d636f8c8e1d61cc87e4c5e557c127 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Mon, 31 Aug 2026 19:50:34 -0600 Subject: [PATCH] feat: add --keep-footnote-position flag Reuses mdit_py_plugins.footnote_plugin's move_to_end=False mode so definitions stay at their source position instead of moving to the document end. Orphan handling for this mode is deferred to a follow-up. --- mdformat_footnote/plugin.py | 23 ++++++++++-- tests/fixtures/cli_integration.md | 15 ++++++++ tests/fixtures/keep_position.md | 59 +++++++++++++++++++++++++++++++ tests/test_cli_integration.py | 35 +++++++++++++++++- tests/test_fixtures.py | 2 ++ 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/keep_position.md diff --git a/mdformat_footnote/plugin.py b/mdformat_footnote/plugin.py index 523411b..03da324 100644 --- a/mdformat_footnote/plugin.py +++ b/mdformat_footnote/plugin.py @@ -19,6 +19,11 @@ def _keep_orphans(options: ContextOptions) -> bool: return bool(get_conf(options, "keep_orphans")) or False +def _keep_position(options: ContextOptions) -> bool: + """Check if footnote definitions should stay at their source position.""" + return bool(get_conf(options, "keep_position")) or False + + def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: """Add options to the mdformat CLI. @@ -34,16 +39,29 @@ def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: "(default: remove them)" ), ) + group.add_argument( + "--keep-footnote-position", + action="store_const", + const=True, + dest="keep_position", + help=( + "Keep footnote definitions at their source position " + "(default: move them to the end of the document)" + ), + ) def update_mdit(mdit: MarkdownIt) -> None: """Update the parser, adding the footnote plugin.""" - mdit.use(footnote_plugin) + keep_position = _keep_position(mdit.options) + mdit.use(footnote_plugin, move_to_end=not keep_position) # Disable inline footnotes for now, since we don't have rendering # support for them yet. mdit.disable("footnote_inline") + if keep_position: + return # Reorder footnotes by reference order, fix IDs, and handle orphans. - # Must run before footnote_tail. + # Must run before footnote_tail, which only exists when move_to_end is set. keep_orphans = _keep_orphans(mdit.options) reorder_fn = partial(reorder_footnotes_by_definition, keep_orphans=keep_orphans) mdit.core.ruler.before("footnote_tail", "reorder_footnotes", reorder_fn) @@ -89,6 +107,7 @@ def _render_children(node: RenderTreeNode, context: RenderContext) -> str: RENDERERS: Mapping[str, Render] = { "footnote": _footnote_renderer, + "footnote_reference": _footnote_renderer, "footnote_ref": _footnote_ref_renderer, "footnote_block": _render_children, } diff --git a/tests/fixtures/cli_integration.md b/tests/fixtures/cli_integration.md index 223838a..958519d 100644 --- a/tests/fixtures/cli_integration.md +++ b/tests/fixtures/cli_integration.md @@ -12,3 +12,18 @@ Referenced [^used] [^orphan]: This is never referenced . + +CLI keep position flag test +. +Para one.[^a] + +[^a]: Definition A. + +Para two. +. +Para one.[^a] + +[^a]: Definition A. + +Para two. +. diff --git a/tests/fixtures/keep_position.md b/tests/fixtures/keep_position.md new file mode 100644 index 0000000..beaf673 --- /dev/null +++ b/tests/fixtures/keep_position.md @@ -0,0 +1,59 @@ +Single footnote stays at its source position +. +Para one.[^a] + +[^a]: Definition A. + +Para two. +. +Para one.[^a] + +[^a]: Definition A. + +Para two. +. + +Multiple footnotes each stay near their own reference +. +Para one.[^a] + +[^a]: Definition A. + +Para two.[^b] + +[^b]: Definition B. +. +Para one.[^a] + +[^a]: Definition A. + +Para two.[^b] + +[^b]: Definition B. +. + +Definition before its reference stays in place +. +[^early]: Defined early. + +Para references it here.[^early] +. +[^early]: Defined early. + +Para references it here.[^early] +. + +Nested footnote body stays at its own source position +. +Body text.[^a] + +[^a]: First, references another.[^b] + +[^b]: Second. +. +Body text.[^a] + +[^a]: First, references another.[^b] + +[^b]: Second. +. diff --git a/tests/test_cli_integration.py b/tests/test_cli_integration.py index 0de68f9..2b2694c 100644 --- a/tests/test_cli_integration.py +++ b/tests/test_cli_integration.py @@ -40,8 +40,40 @@ def test_cli_keep_orphans_flag(): assert output_keep.strip() == expected_keep.strip() +def test_cli_keep_position_flag(): + """Test --keep-footnote-position flag from command line.""" + text, expected_keep = get_fixture( + "cli_integration.md", "CLI keep position flag test" + ) + + with tempfile.TemporaryDirectory() as tmpdir: + input_file = Path(tmpdir) / "test.md" + input_file.write_text(text) + + # Default behavior: move definitions to the end of the document + result = subprocess.run( + ["python", "-m", "mdformat", str(input_file)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + output_default = input_file.read_text() + assert output_default.strip() != expected_keep.strip() + + # With --keep-footnote-position: preserve source position + input_file.write_text(text) # Reset file + result = subprocess.run( + ["python", "-m", "mdformat", "--keep-footnote-position", str(input_file)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + output_keep = input_file.read_text() + assert output_keep.strip() == expected_keep.strip() + + def test_cli_help_shows_option(): - """Test that --keep-footnote-orphans appears in help.""" + """Test that --keep-footnote-orphans and --keep-footnote-position appear in help.""" result = subprocess.run( ["python", "-m", "mdformat", "--help"], capture_output=True, @@ -49,3 +81,4 @@ def test_cli_help_shows_option(): ) assert result.returncode == 0 assert "--keep-footnote-orphans" in result.stdout + assert "--keep-footnote-position" in result.stdout diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 9e9a26d..fdad346 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -16,6 +16,8 @@ def _get_options(filename: str, title: str) -> dict: return {"wrap": 40} if "keep orphans" in title.lower(): return {"keep_orphans": True} + if "keep position" in title.lower() or filename == "keep_position.md": + return {"keep_position": True} return {}