any2md 1.0.0__tar.gz → 1.0.1__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.
Files changed (99) hide show
  1. {any2md-1.0.0 → any2md-1.0.1}/CHANGELOG.md +22 -0
  2. {any2md-1.0.0 → any2md-1.0.1}/PKG-INFO +2 -2
  3. {any2md-1.0.0 → any2md-1.0.1}/README.md +1 -1
  4. {any2md-1.0.0 → any2md-1.0.1}/any2md/__init__.py +1 -1
  5. {any2md-1.0.0 → any2md-1.0.1}/any2md/cli.py +18 -2
  6. {any2md-1.0.0 → any2md-1.0.1}/any2md/converters/docx.py +18 -1
  7. {any2md-1.0.0 → any2md-1.0.1}/any2md/converters/pdf.py +24 -2
  8. {any2md-1.0.0 → any2md-1.0.1}/any2md/pipeline/__init__.py +7 -0
  9. {any2md-1.0.0 → any2md-1.0.1}/docs/cli-reference.md +36 -0
  10. {any2md-1.0.0 → any2md-1.0.1}/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +10 -2
  11. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +1426 -0
  12. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +1053 -0
  13. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +936 -0
  14. any2md-1.0.1/tests/cli/test_cli_backend.py +45 -0
  15. any2md-1.0.1/tests/integration/test_docx_backend_override.py +57 -0
  16. any2md-1.0.1/tests/integration/test_pdf_backend_override.py +57 -0
  17. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_runner.py +10 -0
  18. {any2md-1.0.0 → any2md-1.0.1}/.gitignore +0 -0
  19. {any2md-1.0.0 → any2md-1.0.1}/CONTRIBUTING.md +0 -0
  20. {any2md-1.0.0 → any2md-1.0.1}/LICENSE +0 -0
  21. {any2md-1.0.0 → any2md-1.0.1}/any2md/__main__.py +0 -0
  22. {any2md-1.0.0 → any2md-1.0.1}/any2md/_docling.py +0 -0
  23. {any2md-1.0.0 → any2md-1.0.1}/any2md/config.py +0 -0
  24. {any2md-1.0.0 → any2md-1.0.1}/any2md/converters/__init__.py +0 -0
  25. {any2md-1.0.0 → any2md-1.0.1}/any2md/converters/html.py +0 -0
  26. {any2md-1.0.0 → any2md-1.0.1}/any2md/converters/txt.py +0 -0
  27. {any2md-1.0.0 → any2md-1.0.1}/any2md/frontmatter.py +0 -0
  28. {any2md-1.0.0 → any2md-1.0.1}/any2md/pipeline/cleanup.py +0 -0
  29. {any2md-1.0.0 → any2md-1.0.1}/any2md/pipeline/structured.py +0 -0
  30. {any2md-1.0.0 → any2md-1.0.1}/any2md/pipeline/text.py +0 -0
  31. {any2md-1.0.0 → any2md-1.0.1}/any2md/utils.py +0 -0
  32. {any2md-1.0.0 → any2md-1.0.1}/any2md/validators.py +0 -0
  33. {any2md-1.0.0 → any2md-1.0.1}/docs/architecture.md +0 -0
  34. {any2md-1.0.0 → any2md-1.0.1}/docs/output-format.md +0 -0
  35. {any2md-1.0.0 → any2md-1.0.1}/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +0 -0
  36. {any2md-1.0.0 → any2md-1.0.1}/docs/troubleshooting.md +0 -0
  37. {any2md-1.0.0 → any2md-1.0.1}/docs/upgrading-from-0.7.md +0 -0
  38. {any2md-1.0.0 → any2md-1.0.1}/pyproject.toml +0 -0
  39. {any2md-1.0.0 → any2md-1.0.1}/requirements.txt +0 -0
  40. {any2md-1.0.0 → any2md-1.0.1}/scripts/validate-phase1.sh +0 -0
  41. {any2md-1.0.0 → any2md-1.0.1}/tests/__init__.py +0 -0
  42. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/__init__.py +0 -0
  43. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_existing_flags.py +0 -0
  44. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_exit_codes.py +0 -0
  45. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_high_fidelity.py +0 -0
  46. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_meta.py +0 -0
  47. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_ocr_save_images.py +0 -0
  48. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_quiet_verbose.py +0 -0
  49. {any2md-1.0.0 → any2md-1.0.1}/tests/cli/test_cli_strict.py +0 -0
  50. {any2md-1.0.0 → any2md-1.0.1}/tests/conftest.py +0 -0
  51. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/__init__.py +0 -0
  52. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/docs/ligatures_and_softhyphens.txt +0 -0
  53. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/docs/multi_column.pdf +0 -0
  54. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/docs/table_heavy.docx +0 -0
  55. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/docs/web_page.html +0 -0
  56. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/make_fixtures.py +0 -0
  57. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/ligatures_and_softhyphens.md +0 -0
  58. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/multi_column.docling.md +0 -0
  59. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/multi_column.fallback.md +0 -0
  60. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/table_heavy.docling.md +0 -0
  61. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/table_heavy.fallback.md +0 -0
  62. {any2md-1.0.0 → any2md-1.0.1}/tests/fixtures/snapshots/web_page.md +0 -0
  63. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/__init__.py +0 -0
  64. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_docx_docling.py +0 -0
  65. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_docx_mammoth_fallback.py +0 -0
  66. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_html_trafilatura.py +0 -0
  67. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_pdf_docling.py +0 -0
  68. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_pdf_pymupdf_fallback.py +0 -0
  69. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_save_images.py +0 -0
  70. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_snapshots.py +0 -0
  71. {any2md-1.0.0 → any2md-1.0.1}/tests/integration/test_txt.py +0 -0
  72. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/__init__.py +0 -0
  73. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/__init__.py +0 -0
  74. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_footnote_markers.py +0 -0
  75. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_ligatures.py +0 -0
  76. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_nfc.py +0 -0
  77. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_quotes_dashes.py +0 -0
  78. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_soft_hyphens.py +0 -0
  79. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_validate.py +0 -0
  80. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_cleanup_whitespace.py +0 -0
  81. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_structured_compact_tables.py +0 -0
  82. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_structured_heading_hierarchy.py +0 -0
  83. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_structured_lift_figures.py +0 -0
  84. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_structured_normalize_cites.py +0 -0
  85. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_dedupe_paragraphs.py +0 -0
  86. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_dedupe_toc.py +0 -0
  87. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_dehyphenate.py +0 -0
  88. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_repair_line_wraps.py +0 -0
  89. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_restore_lists_code.py +0 -0
  90. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/pipeline/test_text_strip_headers_footers.py +0 -0
  91. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_auto_id.py +0 -0
  92. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_config.py +0 -0
  93. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_content_hash.py +0 -0
  94. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_docling_helper.py +0 -0
  95. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_frontmatter_compose.py +0 -0
  96. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_frontmatter_helpers.py +0 -0
  97. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_meta_overrides.py +0 -0
  98. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_pdf_complexity.py +0 -0
  99. {any2md-1.0.0 → any2md-1.0.1}/tests/unit/test_validators.py +0 -0
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.0.1] — 2026-04-26
8
+
9
+ Patch release. Adds an explicit backend-selection CLI flag.
10
+
11
+ ### Added
12
+ - `--backend {docling,pymupdf4llm,mammoth}` CLI flag and corresponding
13
+ `PipelineOptions.backend` field. Lets users override the automatic
14
+ backend selection. Useful when Docling extracts incorrectly on a
15
+ specific input and the user wants to fall back to pymupdf4llm
16
+ without uninstalling Docling. Mismatched format/backend combinations
17
+ (e.g., `--backend pymupdf4llm` on a DOCX) error out per file.
18
+
19
+ ### Fixes (workaround)
20
+ - Refs #13 — Docling drops list items on certain academic PDFs with
21
+ the "Parent element of the list item is not a ListGroup" warning.
22
+ No upstream fix yet; users hitting this can now `any2md --backend
23
+ pymupdf4llm doc.pdf` to use the lightweight backend instead.
24
+
25
+ ### Note
26
+ - `--backend docling` is equivalent to the existing `--high-fidelity`
27
+ / `-H` flag (both kept). Future releases may consolidate.
28
+
7
29
  ## [1.0.0] — 2026-04-26
8
30
 
9
31
  First stable release of any2md v1.0. Validated end-to-end against
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: any2md
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Convert PDF, DOCX, HTML, and TXT files — or web pages by URL — to clean, LLM-optimized Markdown with YAML frontmatter.
5
5
  Project-URL: Homepage, https://github.com/rocklambros/any2md
6
6
  Project-URL: Issues, https://github.com/rocklambros/any2md/issues
@@ -226,7 +226,7 @@ You'd run this when your corpus has multi-column PDFs, complex tables, or scanne
226
226
  pip install "any2md[high-fidelity]"
227
227
  ```
228
228
 
229
- After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`.
229
+ After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`. Use `--backend pymupdf4llm` to force the lightweight backend even when Docling is installed (useful when Docling produces extraction artifacts on a specific input).
230
230
 
231
231
  ### When do I need high-fidelity?
232
232
 
@@ -196,7 +196,7 @@ You'd run this when your corpus has multi-column PDFs, complex tables, or scanne
196
196
  pip install "any2md[high-fidelity]"
197
197
  ```
198
198
 
199
- After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`.
199
+ After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`. Use `--backend pymupdf4llm` to force the lightweight backend even when Docling is installed (useful when Docling produces extraction artifacts on a specific input).
200
200
 
201
201
  ### When do I need high-fidelity?
202
202
 
@@ -1,3 +1,3 @@
1
1
  """Convert PDF, DOCX, HTML, and TXT files to LLM-optimized Markdown."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.1"
@@ -132,6 +132,16 @@ def main():
132
132
  action="store_true",
133
133
  help="Force the Docling backend (PDF/DOCX). Exit 1 if not installed.",
134
134
  )
135
+ parser.add_argument(
136
+ "--backend",
137
+ choices=("docling", "pymupdf4llm", "mammoth"),
138
+ default=None,
139
+ help="Force a specific extraction backend. By default any2md auto-selects "
140
+ "(prefers Docling when installed). Use 'docling' (equivalent to --high-fidelity), "
141
+ "'pymupdf4llm' to force the lightweight PDF fallback even when Docling is installed, "
142
+ "or 'mammoth' to force the lightweight DOCX fallback. Mismatched format/backend "
143
+ "combinations (e.g., --backend pymupdf4llm on a DOCX) error out per file.",
144
+ )
135
145
  parser.add_argument(
136
146
  "--ocr-figures",
137
147
  action="store_true",
@@ -228,11 +238,16 @@ def main():
228
238
  sys.exit(1)
229
239
  overrides = _deep_merge_overrides(overrides, cli_meta)
230
240
 
231
- if args.high_fidelity or args.ocr_figures or args.save_images:
241
+ if (
242
+ args.high_fidelity
243
+ or args.ocr_figures
244
+ or args.save_images
245
+ or args.backend == "docling"
246
+ ):
232
247
  from any2md._docling import has_docling, INSTALL_HINT_MSG
233
248
  if not has_docling():
234
249
  print(
235
- f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images.\n"
250
+ f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images / --backend docling.\n"
236
251
  f" {INSTALL_HINT_MSG}",
237
252
  file=sys.stderr,
238
253
  )
@@ -252,6 +267,7 @@ def main():
252
267
  auto_id_prefix=auto_id_prefix,
253
268
  auto_id_type_code=auto_id_type_code,
254
269
  frontmatter_overrides=overrides or None,
270
+ backend=args.backend,
255
271
  )
256
272
 
257
273
  # CLI-only output controls (not part of PipelineOptions).
@@ -114,7 +114,24 @@ def convert_docx(
114
114
  return True
115
115
 
116
116
  try:
117
- if has_docling():
117
+ # Backend override (PipelineOptions.backend). When set, honor the
118
+ # caller's choice instead of the auto-select. ``pymupdf4llm`` is
119
+ # invalid for DOCX — fail the file with a clear error.
120
+ if options.backend == "pymupdf4llm":
121
+ print(
122
+ f" FAIL: {docx_path.name} -- backend 'pymupdf4llm' is not "
123
+ f"valid for DOCX input (pymupdf4llm processes PDF only).",
124
+ file=sys.stderr,
125
+ )
126
+ return False
127
+
128
+ if options.backend == "mammoth":
129
+ md_text, extracted_via = _extract_via_mammoth(docx_path, options)
130
+ lane = "text"
131
+ elif options.backend == "docling":
132
+ md_text, extracted_via = _extract_via_docling(docx_path)
133
+ lane = "structured"
134
+ elif has_docling():
118
135
  try:
119
136
  md_text, extracted_via = _extract_via_docling(docx_path)
120
137
  lane = "structured"
@@ -182,9 +182,31 @@ def convert_pdf(
182
182
  return True
183
183
 
184
184
  try:
185
- use_docling = has_docling()
185
+ # Backend override (PipelineOptions.backend). When set, honor the
186
+ # caller's choice instead of the auto-select. ``mammoth`` is
187
+ # invalid for PDFs — fail the file with a clear error.
188
+ if options.backend == "mammoth":
189
+ print(
190
+ f" FAIL: {pdf_path.name} -- backend 'mammoth' is not "
191
+ f"valid for PDF input (mammoth processes DOCX only).",
192
+ file=sys.stderr,
193
+ )
194
+ return False
195
+
196
+ if options.backend == "pymupdf4llm":
197
+ use_docling = False
198
+ elif options.backend == "docling":
199
+ # Same install-required behavior as --high-fidelity. If
200
+ # Docling isn't importable, fall through to the existing
201
+ # auto-select path which will error via _extract_via_docling
202
+ # below if Docling truly isn't usable. The CLI layer
203
+ # pre-checks install for both --high-fidelity and
204
+ # --backend docling, so this branch normally has Docling.
205
+ use_docling = has_docling()
206
+ else:
207
+ use_docling = has_docling()
186
208
 
187
- if not use_docling and pdf_looks_complex(pdf_path):
209
+ if not use_docling and options.backend is None and pdf_looks_complex(pdf_path):
188
210
  install_hint()
189
211
 
190
212
  # Always extract metadata via PyMuPDF — independent of which
@@ -23,6 +23,13 @@ class PipelineOptions:
23
23
  strip_links: bool = False
24
24
  strict: bool = False
25
25
  high_fidelity: bool = False # force Docling backend
26
+ # Explicit backend selection. ``None`` preserves the existing
27
+ # auto-select behavior (Docling when installed, else format-specific
28
+ # fallback). When set, the converter MUST honor the choice or fail
29
+ # the file with a clear error if the backend is incompatible with
30
+ # the input format (e.g. ``"mammoth"`` on a PDF, ``"pymupdf4llm"``
31
+ # on a DOCX).
32
+ backend: Literal["docling", "pymupdf4llm", "mammoth"] | None = None
26
33
  # --auto-id wiring. When ``auto_id`` is True, ``frontmatter.compose``
27
34
  # emits a generated ``document_id``. The prefix and type_code are
28
35
  # used as-is; Phase 4 hardcodes "LOCAL"/"DOC" and Task 5 wires
@@ -145,6 +145,42 @@ You'll see `extracted_via: "docling"` in the frontmatter of every PDF and DOCX
145
145
  output. Without `-H`, any2md uses Docling when it's importable and falls back
146
146
  silently otherwise.
147
147
 
148
+ ### `--backend {docling,pymupdf4llm,mammoth}`
149
+
150
+ Force a specific extraction backend, overriding the automatic Docling-when-
151
+ installed selection. `docling` is equivalent to `--high-fidelity` (and shares
152
+ the same install pre-flight). `pymupdf4llm` forces the lightweight PDF
153
+ fallback even when Docling is installed. `mammoth` forces the lightweight
154
+ DOCX fallback even when Docling is installed.
155
+
156
+ The backend must match the input format. Mismatches fail per file rather
157
+ than the whole run:
158
+
159
+ - `--backend pymupdf4llm` on a DOCX → that file fails with a clear message;
160
+ PDF inputs in the same run still convert.
161
+ - `--backend mammoth` on a PDF → that file fails with a clear message; DOCX
162
+ inputs in the same run still convert.
163
+
164
+ **Use this when** Docling extracts incorrectly on a specific input (e.g.
165
+ [#13](https://github.com/rocklambros/any2md/issues/13) — Docling drops list
166
+ items on certain academic PDFs) and you want to fall back to the lightweight
167
+ backend without uninstalling Docling.
168
+
169
+ **Don't use this when** the auto-detection is doing the right thing — the
170
+ default behavior already prefers Docling when installed and falls back when
171
+ it isn't.
172
+
173
+ ```bash
174
+ # Skip Docling for one specific PDF that triggers a known bug
175
+ any2md --backend pymupdf4llm research_paper.pdf
176
+
177
+ # Force Docling explicitly (same as --high-fidelity)
178
+ any2md --backend docling ./corpus
179
+ ```
180
+
181
+ You'll see `extracted_via: "pymupdf4llm"` (or `"mammoth+markdownify"`, or
182
+ `"docling"`) in the frontmatter, matching the backend you forced.
183
+
148
184
  ### `--ocr-figures`
149
185
 
150
186
  Run OCR on figures embedded in PDFs (Docling path). Implies `--high-fidelity`.
@@ -721,10 +721,11 @@ def test_source_meta_defaults_are_safe():
721
721
  title_hint=None, authors=[], organization=None, date=None,
722
722
  keywords=[], pages=None, word_count=None,
723
723
  source_file="x.txt", source_url=None,
724
- extracted_via="heuristic", lane="text",
724
+ doc_type="txt", extracted_via="heuristic", lane="text",
725
725
  )
726
726
  assert meta.lane == "text"
727
727
  assert meta.extracted_via == "heuristic"
728
+ assert meta.doc_type == "txt"
728
729
  ```
729
730
 
730
731
  - [ ] **Step 2: Run test to verify it fails**
@@ -1918,11 +1919,18 @@ from any2md.pipeline import PipelineOptions
1918
1919
 
1919
1920
 
1920
1921
  def _split_frontmatter(text: str) -> tuple[dict, str]:
1921
- """Helper: split a frontmatter+body string into (yaml_dict, body_str)."""
1922
+ """Helper: split a frontmatter+body string into (yaml_dict, body_str).
1923
+
1924
+ compose() emits a blank-line separator after the closing ---, so we
1925
+ strip a single leading newline from the body to recover the body that
1926
+ was passed into compose().
1927
+ """
1922
1928
  assert text.startswith("---\n")
1923
1929
  end = text.index("\n---\n", 4)
1924
1930
  fm = yaml.safe_load(text[4:end])
1925
1931
  body = text[end + 5 :]
1932
+ if body.startswith("\n"):
1933
+ body = body[1:]
1926
1934
  return fm, body
1927
1935
 
1928
1936