any2md 1.0.1__tar.gz → 1.0.3__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.
- {any2md-1.0.1 → any2md-1.0.3}/.gitignore +1 -0
- {any2md-1.0.1 → any2md-1.0.3}/CHANGELOG.md +121 -0
- any2md-1.0.3/CODE_OF_CONDUCT.md +37 -0
- {any2md-1.0.1 → any2md-1.0.3}/PKG-INFO +73 -23
- {any2md-1.0.1 → any2md-1.0.3}/README.md +72 -22
- any2md-1.0.3/SECURITY.md +83 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/__init__.py +1 -1
- {any2md-1.0.1 → any2md-1.0.3}/any2md/cli.py +12 -8
- {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/__init__.py +5 -3
- {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/docx.py +20 -7
- {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/html.py +6 -5
- {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/pdf.py +8 -7
- {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/txt.py +17 -5
- {any2md-1.0.1 → any2md-1.0.3}/any2md/frontmatter.py +50 -19
- any2md-1.0.3/any2md/heuristics.py +604 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/__init__.py +6 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/cleanup.py +58 -19
- {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/structured.py +32 -2
- {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/text.py +203 -8
- any2md-1.0.3/assets/logo-original.jpeg +0 -0
- any2md-1.0.3/assets/logo.png +0 -0
- any2md-1.0.3/assets/social-preview.jpg +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/architecture.md +201 -63
- {any2md-1.0.1 → any2md-1.0.3}/docs/cli-reference.md +40 -1
- {any2md-1.0.1 → any2md-1.0.3}/docs/output-format.md +45 -2
- any2md-1.0.3/docs/superpowers/plans/2026-04-26-any2md-v1.0.2-implementation.md +307 -0
- any2md-1.0.3/docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md +694 -0
- any2md-1.0.3/docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md +133 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/troubleshooting.md +15 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/upgrading-from-0.7.md +1 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_high_fidelity.py +7 -2
- any2md-1.0.3/tests/cli/test_cli_no_arxiv_lookup.py +41 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_ocr_save_images.py +6 -3
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_docx_docling.py +1 -1
- any2md-1.0.3/tests/integration/test_docx_mammoth_fallback.py +166 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_html_trafilatura.py +1 -1
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_pdf_docling.py +3 -0
- any2md-1.0.3/tests/integration/test_pdf_pymupdf_fallback.py +76 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_snapshots.py +1 -2
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_txt.py +1 -1
- any2md-1.0.3/tests/unit/pipeline/test_cleanup_html_entities.py +51 -0
- any2md-1.0.3/tests/unit/pipeline/test_cleanup_html_entities_double_encoded.py +45 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_ligatures.py +3 -1
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_runner.py +11 -0
- any2md-1.0.3/tests/unit/pipeline/test_strip_orphan_punctuation.py +40 -0
- any2md-1.0.3/tests/unit/pipeline/test_structured_body_cleanup_stages.py +84 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_compact_tables.py +2 -6
- any2md-1.0.3/tests/unit/pipeline/test_text_dedupe_toc_table.py +106 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dehyphenate.py +1 -4
- any2md-1.0.3/tests/unit/pipeline/test_text_strip_cover_artifacts.py +71 -0
- any2md-1.0.3/tests/unit/pipeline/test_text_strip_repeated_byline.py +45 -0
- any2md-1.0.3/tests/unit/pipeline/test_text_strip_web_fragments.py +35 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_auto_id.py +1 -3
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_docling_helper.py +1 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_frontmatter_compose.py +100 -10
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_frontmatter_helpers.py +24 -11
- any2md-1.0.3/tests/unit/test_heuristics.py +558 -0
- any2md-1.0.1/tests/integration/test_docx_mammoth_fallback.py +0 -42
- any2md-1.0.1/tests/integration/test_pdf_pymupdf_fallback.py +0 -37
- {any2md-1.0.1 → any2md-1.0.3}/CONTRIBUTING.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/LICENSE +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/__main__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/_docling.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/config.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/utils.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/any2md/validators.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/pyproject.toml +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/requirements.txt +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/scripts/validate-phase1.sh +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_backend.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_existing_flags.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_exit_codes.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_meta.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_quiet_verbose.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_strict.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/conftest.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/ligatures_and_softhyphens.txt +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/multi_column.pdf +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/table_heavy.docx +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/web_page.html +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/make_fixtures.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/ligatures_and_softhyphens.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/multi_column.docling.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/multi_column.fallback.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/table_heavy.docling.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/table_heavy.fallback.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/web_page.md +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_docx_backend_override.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_pdf_backend_override.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_save_images.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/__init__.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_footnote_markers.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_nfc.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_quotes_dashes.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_soft_hyphens.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_validate.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_whitespace.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_heading_hierarchy.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_lift_figures.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_normalize_cites.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dedupe_paragraphs.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dedupe_toc.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_repair_line_wraps.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_restore_lists_code.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_strip_headers_footers.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_config.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_content_hash.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_meta_overrides.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_pdf_complexity.py +0 -0
- {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_validators.py +1 -1
|
@@ -4,6 +4,127 @@ 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.3] — 2026-04-26
|
|
8
|
+
|
|
9
|
+
Patch release. Audit on the v1.0.2-regenerated corpus surfaced two
|
|
10
|
+
patterns the previous release did not handle: cover-page-titled
|
|
11
|
+
documents whose first H2 stripped to empty produced an empty
|
|
12
|
+
`title` field, and Docling-lane output retained malformed `|`
|
|
13
|
+
table-row remnants because the lone-punctuation filter was
|
|
14
|
+
text-lane-only. The lane-agnostic body cleanup that v1.0.2's
|
|
15
|
+
text lane already had also now runs on Docling output.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- New lane-agnostic stage **`strip_orphan_punctuation`** — drops
|
|
19
|
+
lines containing only `|` or `>`. Extracted from T10
|
|
20
|
+
`strip_web_fragments` so it can run on the structured (Docling)
|
|
21
|
+
lane in addition to the text (trafilatura) lane.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `heuristics.refine_title` now walks all H2 lines line-by-line
|
|
25
|
+
and skips any whose content strips to empty after removing
|
|
26
|
+
markdown emphasis, instead of always returning the first
|
|
27
|
+
regex match. Also guards the Wikipedia-prefix strip against
|
|
28
|
+
emitting empty when the candidate is the bare prefix.
|
|
29
|
+
- `pipeline/structured.py` now extends `STAGES` with
|
|
30
|
+
`strip_repeated_byline` (T9), `dedupe_toc_table` (T7),
|
|
31
|
+
`strip_cover_artifacts` (T8), and `strip_orphan_punctuation`
|
|
32
|
+
so Docling output gets the same body-cleanup pass as text-lane
|
|
33
|
+
output. T10's short-fragment heuristic stays text-lane-only —
|
|
34
|
+
it would over-strip Docling's deliberate short headings.
|
|
35
|
+
- `pipeline/cleanup.py` `decode_html_entities` (C8) now loops
|
|
36
|
+
`html.unescape` until output stabilizes (max 5 iterations) so
|
|
37
|
+
double-encoded entities like `&` → `&` → `&` are
|
|
38
|
+
fully decoded. Some extractors emit doubly-encoded entities
|
|
39
|
+
that survived a single pass.
|
|
40
|
+
|
|
41
|
+
### Fixes
|
|
42
|
+
- **Empty-title regression**: cover-page H1 documents whose first
|
|
43
|
+
H2 contained only markdown emphasis, NBSP-equivalent unicode,
|
|
44
|
+
or where the H2 regex's `\s+` class spanned a newline into the
|
|
45
|
+
next paragraph produced `title: ""`. Walk H2 lines line-by-line
|
|
46
|
+
with a non-empty guard.
|
|
47
|
+
- **Orphan-punctuation in Docling output**: malformed table-row
|
|
48
|
+
remnants (lone `|`) survived in structured-lane output because
|
|
49
|
+
T10 was text-lane-only. New `strip_orphan_punctuation` runs on
|
|
50
|
+
both lanes.
|
|
51
|
+
- **Double-encoded HTML entities**: `&` left a residual
|
|
52
|
+
`&` after a single unescape pass. Loop until stable.
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
- 4 new tests in `tests/unit/test_heuristics.py::TestRefineTitle`
|
|
56
|
+
covering NBSP-only H2, emphasis-only H2 (skip-and-pick-next),
|
|
57
|
+
all-empty-H2 fallthrough, and Wikipedia-prefix-only candidate.
|
|
58
|
+
- New `tests/unit/pipeline/test_strip_orphan_punctuation.py`
|
|
59
|
+
covering aggressive removal, conservative no-op, and preservation
|
|
60
|
+
of lines containing other content alongside `|`/`>`.
|
|
61
|
+
- New `tests/unit/pipeline/test_structured_body_cleanup_stages.py`
|
|
62
|
+
asserting the lane-agnostic stages are registered and that the
|
|
63
|
+
targeted patterns are removed from structured-lane output.
|
|
64
|
+
- New `tests/unit/pipeline/test_cleanup_html_entities_double_encoded.py`
|
|
65
|
+
covering the C8 loop.
|
|
66
|
+
|
|
67
|
+
## [1.0.2] — 2026-04-26
|
|
68
|
+
|
|
69
|
+
Patch release. Closes issue #15 plus 8 additional quality issues
|
|
70
|
+
discovered during deep investigation against four real-world inputs:
|
|
71
|
+
arxiv academic paper, ISO/IEC 27002 standard, COMP4441 academic
|
|
72
|
+
DOCX, and a Wikipedia article via URL.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- New `any2md/heuristics.py` module — pure functions for frontmatter
|
|
76
|
+
field refinement: `refine_title`, `refine_abstract`, `extract_authors`,
|
|
77
|
+
`filter_organization`, `arxiv_lookup`, `is_arxiv_filename`. Called
|
|
78
|
+
from `frontmatter.compose()` and from converter modules.
|
|
79
|
+
- New `produced_by` extension field on `SourceMeta` and frontmatter
|
|
80
|
+
(between `extracted_via` and `pages`). Records the software that
|
|
81
|
+
produced the source file (PDF `Creator`, DOCX `Application`).
|
|
82
|
+
Distinct from `extracted_via` which records the any2md backend
|
|
83
|
+
that produced the markdown.
|
|
84
|
+
- New shared cleanup stage **C8 `decode_html_entities`** — universal
|
|
85
|
+
removal of `&`, `>`, `<`, numeric entities (`—`,
|
|
86
|
+
`—`) from body. Code-block aware (skips fenced ` ``` ` blocks).
|
|
87
|
+
- New text-lane stages T7-T10 (aggressive/maximum profile only):
|
|
88
|
+
- **T7 `dedupe_toc_table`** — strips table-formatted TOCs that T4's
|
|
89
|
+
text-formatted-TOC heuristic doesn't catch. Common in academic PDFs.
|
|
90
|
+
- **T8 `strip_cover_artifacts`** — drops cover-page noise (QR-code
|
|
91
|
+
blurbs, version stamps like "Third edition 2022-02") in the first
|
|
92
|
+
~30 lines, before the first H2.
|
|
93
|
+
- **T9 `strip_repeated_byline`** — removes "Author's Contact
|
|
94
|
+
Information:" and similar lines that duplicate a byline.
|
|
95
|
+
- **T10 `strip_web_fragments`** — drops trafilatura extraction
|
|
96
|
+
fragments (orphan `|` / `>` lines, short incomplete sentences
|
|
97
|
+
surrounded by blank lines).
|
|
98
|
+
- New CLI flag `--no-arxiv-lookup` to disable the arxiv API metadata
|
|
99
|
+
enrichment. Arxiv enrichment is on by default for filenames matching
|
|
100
|
+
`\d{4}\.\d{4,5}` (e.g., `2501.17755v1.pdf`); SSRF-guarded with 5s
|
|
101
|
+
timeout; failures emit non-blocking warnings.
|
|
102
|
+
- Project logo and brand assets under `assets/`.
|
|
103
|
+
|
|
104
|
+
### Fixes
|
|
105
|
+
The eleven quality issues — see issue #15 and
|
|
106
|
+
`docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md`:
|
|
107
|
+
- A1: authors not extracted from PDF body byline (academic PDFs).
|
|
108
|
+
- A2: abstract picked the byline / cover blurb / TOC line.
|
|
109
|
+
- A3: organization populated with PDF Creator software junk.
|
|
110
|
+
- B1: HTML entities leaked to body universally.
|
|
111
|
+
- C1: ISO/TR titles detected as "INTERNATIONAL STANDARD" cover header.
|
|
112
|
+
- C3: DOCX titles concatenated course code + project title.
|
|
113
|
+
- C4: Wikipedia titles kept "Wikipedia:" namespace prefix.
|
|
114
|
+
- D1: TOCs dumped as markdown tables in academic PDFs.
|
|
115
|
+
- D2: trafilatura fragments leaked into web outputs.
|
|
116
|
+
- D3: ISO cover-page QR-code blurb leaked into body.
|
|
117
|
+
- E1: "Author's Contact Information:" line duplicated byline.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
- `frontmatter.compose()` now consults `heuristics.py` to refine
|
|
121
|
+
title / abstract / authors before YAML emission.
|
|
122
|
+
- `--profile conservative` now also gates the new heuristic
|
|
123
|
+
aggressiveness (skip-lists active; speculative inferences off).
|
|
124
|
+
- PDF `Creator` software values (LaTeX, acmart, Adobe InDesign,
|
|
125
|
+
Microsoft Word, etc.) no longer populate `organization`. They
|
|
126
|
+
go to the new `produced_by` field instead.
|
|
127
|
+
|
|
7
128
|
## [1.0.1] — 2026-04-26
|
|
8
129
|
|
|
9
130
|
Patch release. Adds an explicit backend-selection CLI flag.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project adopts the **[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)**
|
|
4
|
+
as its code of conduct. The full text is available at the link above and
|
|
5
|
+
the canonical source is at https://www.contributor-covenant.org/.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
The covenant applies to all project spaces, including but not limited to:
|
|
10
|
+
|
|
11
|
+
- The GitHub repository (issues, pull requests, discussions, wiki, code
|
|
12
|
+
review comments, commit messages).
|
|
13
|
+
- Any community channel that explicitly identifies itself as a project
|
|
14
|
+
space (mailing list, chat room, project event).
|
|
15
|
+
- Public spaces when an individual is officially representing the project.
|
|
16
|
+
|
|
17
|
+
## Reporting
|
|
18
|
+
|
|
19
|
+
Reports of abuse, harassment, or otherwise unacceptable behavior may be
|
|
20
|
+
sent to **rock@rockcyber.com**. Reports are confidential and will be
|
|
21
|
+
reviewed within 5 business days.
|
|
22
|
+
|
|
23
|
+
If the report concerns the maintainer, please contact a trusted
|
|
24
|
+
intermediary or use a public reporting channel of your choice.
|
|
25
|
+
|
|
26
|
+
## Enforcement
|
|
27
|
+
|
|
28
|
+
The maintainer is responsible for enforcement and may take any action
|
|
29
|
+
deemed appropriate, ranging from a private warning to a permanent ban
|
|
30
|
+
from project spaces. Enforcement actions follow the
|
|
31
|
+
[Contributor Covenant Enforcement Guidelines](https://www.contributor-covenant.org/version/2/1/code_of_conduct/#enforcement-guidelines).
|
|
32
|
+
|
|
33
|
+
## Attribution
|
|
34
|
+
|
|
35
|
+
Code of conduct adapted from the
|
|
36
|
+
[Contributor Covenant](https://www.contributor-covenant.org), version 2.1,
|
|
37
|
+
available at https://www.contributor-covenant.org/version/2/1/code_of_conduct/.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: any2md
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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
|
|
@@ -28,10 +28,25 @@ Provides-Extra: high-fidelity
|
|
|
28
28
|
Requires-Dist: docling>=2.0.0; extra == 'high-fidelity'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="assets/logo.png" alt="any2md" width="500"/>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
31
35
|
# any2md
|
|
32
36
|
|
|
33
37
|
Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
|
|
34
38
|
|
|
39
|
+
## What's new in 1.0.3
|
|
40
|
+
|
|
41
|
+
A patch release closing two regressions surfaced by audit on the v1.0.2-regenerated corpus, plus a Docling-lane reach for the v1.0.2 body-cleanup stages:
|
|
42
|
+
|
|
43
|
+
- **Empty-title fix**: cover-page-titled documents (`# INTERNATIONAL STANDARD` etc.) whose first H2 stripped to empty (markdown emphasis only, NBSP-equivalent unicode, regex span crossing into the next paragraph) emitted `title: ""`. `heuristics.refine_title` now walks H2 lines line-by-line and skips any that strip to empty after dropping markdown emphasis. The Wikipedia-prefix strip got the same non-empty guard.
|
|
44
|
+
- **Docling-lane orphan-punctuation**: lone `|` or `>` lines from Docling's malformed table parsing leaked through because T10 `strip_web_fragments` was text-lane-only by design. The orphan-punctuation portion is extracted into a new lane-agnostic stage `strip_orphan_punctuation` that runs on Docling output too. T10's trafilatura-specific short-fragment heuristic stays text-lane-only.
|
|
45
|
+
- **Lane-agnostic body cleanup on Docling lane**: T7 `dedupe_toc_table`, T8 `strip_cover_artifacts`, and T9 `strip_repeated_byline` are now appended to the structured-lane `STAGES` list, so Docling output gets the same body-cleanup pass that text-lane output already had.
|
|
46
|
+
- **Double-encoded HTML entities**: C8 `decode_html_entities` now loops `html.unescape` until output stabilizes (max 5 iterations) so `&amp;` → `&` → `&` is fully decoded.
|
|
47
|
+
|
|
48
|
+
The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md](docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md). v1.0.2 highlights — `any2md/heuristics.py`, `produced_by`, C8, T7–T10, `--no-arxiv-lookup` — remain available; see [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md).
|
|
49
|
+
|
|
35
50
|
## What this is
|
|
36
51
|
|
|
37
52
|
any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
|
|
@@ -144,6 +159,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
|
|
|
144
159
|
type: "docx" # source format (any2md extension field)
|
|
145
160
|
word_count: 14289 # any2md extension field
|
|
146
161
|
extracted_via: "docling" # which backend produced the markdown
|
|
162
|
+
produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
|
|
147
163
|
---
|
|
148
164
|
```
|
|
149
165
|
|
|
@@ -408,6 +424,10 @@ type_code = "GD"
|
|
|
408
424
|
|
|
409
425
|
With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
|
|
410
426
|
|
|
427
|
+
### Network behavior
|
|
428
|
+
|
|
429
|
+
For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
|
|
430
|
+
|
|
411
431
|
### Output verbosity and exit codes
|
|
412
432
|
|
|
413
433
|
The CLI has three verbosity flags that pair with a four-value exit-code contract:
|
|
@@ -442,7 +462,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
|
|
|
442
462
|
any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
|
|
443
463
|
|
|
444
464
|
```
|
|
445
|
-
any2md v1.0
|
|
465
|
+
any2md v1.0.3
|
|
446
466
|
│
|
|
447
467
|
┌────┴────┐
|
|
448
468
|
CLI │ cli.py │ parse args, classify URL/file/dir,
|
|
@@ -459,43 +479,73 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
459
479
|
│ pymupdf4llm│ mammoth+md │ │
|
|
460
480
|
└────┬───────┴────┬───────┴───┬───┘
|
|
461
481
|
│ │ │
|
|
482
|
+
│ SourceMeta carries:
|
|
483
|
+
│ organization, produced_by ◀ NEW in v1.0.2
|
|
484
|
+
│ (split via heuristics.filter_organization)
|
|
485
|
+
│
|
|
462
486
|
structured-md structured-md text-md
|
|
463
487
|
│ │ │
|
|
464
488
|
└─────┬──────┴───────────┘
|
|
465
489
|
▼
|
|
466
490
|
┌─────────────────────────┐
|
|
467
491
|
│ pipeline/structured.py │ structured lane
|
|
468
|
-
│
|
|
469
|
-
│
|
|
470
|
-
│
|
|
471
|
-
│
|
|
492
|
+
│ S1 figure caption pass │ (trusts layout)
|
|
493
|
+
│ S2 table compactor │
|
|
494
|
+
│ S3 cite normalizer │
|
|
495
|
+
│ S4 heading hierarchy │
|
|
496
|
+
│ T9 strip repeated │ ◀ NEW v1.0.3
|
|
497
|
+
│ byline │
|
|
498
|
+
│ T7 dedupe TOC table │ ◀ NEW v1.0.3
|
|
499
|
+
│ T8 strip cover │ ◀ NEW v1.0.3
|
|
500
|
+
│ artifacts │
|
|
501
|
+
│ strip_orphan_punct │ ◀ NEW v1.0.3
|
|
472
502
|
└────────────┬────────────┘
|
|
473
503
|
│
|
|
474
504
|
┌────────────┴────────────┐
|
|
475
505
|
│ pipeline/text.py │ text lane
|
|
476
|
-
│
|
|
477
|
-
│
|
|
478
|
-
│
|
|
479
|
-
│
|
|
480
|
-
│
|
|
481
|
-
│
|
|
506
|
+
│ T1 line-wrap repair │
|
|
507
|
+
│ T2 dehyphenate │
|
|
508
|
+
│ T9 strip repeated │ ◀ NEW (E1)
|
|
509
|
+
│ byline │
|
|
510
|
+
│ T3 dedupe paragraphs │
|
|
511
|
+
│ T4 dedupe TOC block │
|
|
512
|
+
│ T7 dedupe TOC table │ ◀ NEW (D1)
|
|
513
|
+
│ T5 hdr/ftr strip │
|
|
514
|
+
│ T10 strip web │ ◀ NEW (D2)
|
|
515
|
+
│ fragments │
|
|
516
|
+
│ ↳ strip_orphan_punct ◀ split out v1.0.3
|
|
517
|
+
│ T6 list/code restore │
|
|
518
|
+
│ T8 strip cover │ ◀ NEW (D3)
|
|
519
|
+
│ artifacts │
|
|
482
520
|
└────────────┬────────────┘
|
|
483
521
|
│
|
|
484
522
|
┌────────────┴────────────┐
|
|
485
523
|
│ pipeline/cleanup.py │ shared lane
|
|
486
|
-
│
|
|
487
|
-
│
|
|
488
|
-
│
|
|
489
|
-
│
|
|
490
|
-
│
|
|
491
|
-
│
|
|
492
|
-
│
|
|
524
|
+
│ C1 NFC normalize │ (always last)
|
|
525
|
+
│ C2 soft hyphen strip │
|
|
526
|
+
│ C3 ligature normalize │
|
|
527
|
+
│ C4 quote/dash normalize│
|
|
528
|
+
│ C5 whitespace collapse │
|
|
529
|
+
│ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
|
|
530
|
+
│ C6 footnote marker strip│
|
|
531
|
+
│ C7 validate │
|
|
493
532
|
└────────────┬────────────┘
|
|
494
533
|
│
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
534
|
+
│ ┌──────────────────────────┐
|
|
535
|
+
│ │ any2md/heuristics.py │ ◀ NEW v1.0.2
|
|
536
|
+
│ │ - refine_title │
|
|
537
|
+
│ │ - refine_abstract │
|
|
538
|
+
│ │ - extract_authors │
|
|
539
|
+
│ │ (+ optional arxiv API)│
|
|
540
|
+
│ │ - filter_organization │
|
|
541
|
+
│ └────────────┬─────────────┘
|
|
542
|
+
│ │
|
|
543
|
+
▼ │
|
|
544
|
+
┌─────────────────────────┐ │
|
|
545
|
+
│ frontmatter.py │ ◀─────────┘ (consults heuristics
|
|
546
|
+
│ - merge user overrides │ before YAML emission)
|
|
498
547
|
│ - derive title/date/… │
|
|
548
|
+
│ - emit produced_by │ ◀ NEW v1.0.2
|
|
499
549
|
│ - compute content_hash │
|
|
500
550
|
│ - emit YAML │
|
|
501
551
|
└────────────┬────────────┘
|
|
@@ -504,7 +554,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
504
554
|
<output>.md ← frontmatter + cleaned body
|
|
505
555
|
```
|
|
506
556
|
|
|
507
|
-
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
557
|
+
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
508
558
|
|
|
509
559
|
## Migrating from v0.7
|
|
510
560
|
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="any2md" width="500"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# any2md
|
|
2
6
|
|
|
3
7
|
Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
|
|
4
8
|
|
|
9
|
+
## What's new in 1.0.3
|
|
10
|
+
|
|
11
|
+
A patch release closing two regressions surfaced by audit on the v1.0.2-regenerated corpus, plus a Docling-lane reach for the v1.0.2 body-cleanup stages:
|
|
12
|
+
|
|
13
|
+
- **Empty-title fix**: cover-page-titled documents (`# INTERNATIONAL STANDARD` etc.) whose first H2 stripped to empty (markdown emphasis only, NBSP-equivalent unicode, regex span crossing into the next paragraph) emitted `title: ""`. `heuristics.refine_title` now walks H2 lines line-by-line and skips any that strip to empty after dropping markdown emphasis. The Wikipedia-prefix strip got the same non-empty guard.
|
|
14
|
+
- **Docling-lane orphan-punctuation**: lone `|` or `>` lines from Docling's malformed table parsing leaked through because T10 `strip_web_fragments` was text-lane-only by design. The orphan-punctuation portion is extracted into a new lane-agnostic stage `strip_orphan_punctuation` that runs on Docling output too. T10's trafilatura-specific short-fragment heuristic stays text-lane-only.
|
|
15
|
+
- **Lane-agnostic body cleanup on Docling lane**: T7 `dedupe_toc_table`, T8 `strip_cover_artifacts`, and T9 `strip_repeated_byline` are now appended to the structured-lane `STAGES` list, so Docling output gets the same body-cleanup pass that text-lane output already had.
|
|
16
|
+
- **Double-encoded HTML entities**: C8 `decode_html_entities` now loops `html.unescape` until output stabilizes (max 5 iterations) so `&amp;` → `&` → `&` is fully decoded.
|
|
17
|
+
|
|
18
|
+
The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md](docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md). v1.0.2 highlights — `any2md/heuristics.py`, `produced_by`, C8, T7–T10, `--no-arxiv-lookup` — remain available; see [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md).
|
|
19
|
+
|
|
5
20
|
## What this is
|
|
6
21
|
|
|
7
22
|
any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
|
|
@@ -114,6 +129,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
|
|
|
114
129
|
type: "docx" # source format (any2md extension field)
|
|
115
130
|
word_count: 14289 # any2md extension field
|
|
116
131
|
extracted_via: "docling" # which backend produced the markdown
|
|
132
|
+
produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
|
|
117
133
|
---
|
|
118
134
|
```
|
|
119
135
|
|
|
@@ -378,6 +394,10 @@ type_code = "GD"
|
|
|
378
394
|
|
|
379
395
|
With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
|
|
380
396
|
|
|
397
|
+
### Network behavior
|
|
398
|
+
|
|
399
|
+
For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
|
|
400
|
+
|
|
381
401
|
### Output verbosity and exit codes
|
|
382
402
|
|
|
383
403
|
The CLI has three verbosity flags that pair with a four-value exit-code contract:
|
|
@@ -412,7 +432,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
|
|
|
412
432
|
any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
|
|
413
433
|
|
|
414
434
|
```
|
|
415
|
-
any2md v1.0
|
|
435
|
+
any2md v1.0.3
|
|
416
436
|
│
|
|
417
437
|
┌────┴────┐
|
|
418
438
|
CLI │ cli.py │ parse args, classify URL/file/dir,
|
|
@@ -429,43 +449,73 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
429
449
|
│ pymupdf4llm│ mammoth+md │ │
|
|
430
450
|
└────┬───────┴────┬───────┴───┬───┘
|
|
431
451
|
│ │ │
|
|
452
|
+
│ SourceMeta carries:
|
|
453
|
+
│ organization, produced_by ◀ NEW in v1.0.2
|
|
454
|
+
│ (split via heuristics.filter_organization)
|
|
455
|
+
│
|
|
432
456
|
structured-md structured-md text-md
|
|
433
457
|
│ │ │
|
|
434
458
|
└─────┬──────┴───────────┘
|
|
435
459
|
▼
|
|
436
460
|
┌─────────────────────────┐
|
|
437
461
|
│ pipeline/structured.py │ structured lane
|
|
438
|
-
│
|
|
439
|
-
│
|
|
440
|
-
│
|
|
441
|
-
│
|
|
462
|
+
│ S1 figure caption pass │ (trusts layout)
|
|
463
|
+
│ S2 table compactor │
|
|
464
|
+
│ S3 cite normalizer │
|
|
465
|
+
│ S4 heading hierarchy │
|
|
466
|
+
│ T9 strip repeated │ ◀ NEW v1.0.3
|
|
467
|
+
│ byline │
|
|
468
|
+
│ T7 dedupe TOC table │ ◀ NEW v1.0.3
|
|
469
|
+
│ T8 strip cover │ ◀ NEW v1.0.3
|
|
470
|
+
│ artifacts │
|
|
471
|
+
│ strip_orphan_punct │ ◀ NEW v1.0.3
|
|
442
472
|
└────────────┬────────────┘
|
|
443
473
|
│
|
|
444
474
|
┌────────────┴────────────┐
|
|
445
475
|
│ pipeline/text.py │ text lane
|
|
446
|
-
│
|
|
447
|
-
│
|
|
448
|
-
│
|
|
449
|
-
│
|
|
450
|
-
│
|
|
451
|
-
│
|
|
476
|
+
│ T1 line-wrap repair │
|
|
477
|
+
│ T2 dehyphenate │
|
|
478
|
+
│ T9 strip repeated │ ◀ NEW (E1)
|
|
479
|
+
│ byline │
|
|
480
|
+
│ T3 dedupe paragraphs │
|
|
481
|
+
│ T4 dedupe TOC block │
|
|
482
|
+
│ T7 dedupe TOC table │ ◀ NEW (D1)
|
|
483
|
+
│ T5 hdr/ftr strip │
|
|
484
|
+
│ T10 strip web │ ◀ NEW (D2)
|
|
485
|
+
│ fragments │
|
|
486
|
+
│ ↳ strip_orphan_punct ◀ split out v1.0.3
|
|
487
|
+
│ T6 list/code restore │
|
|
488
|
+
│ T8 strip cover │ ◀ NEW (D3)
|
|
489
|
+
│ artifacts │
|
|
452
490
|
└────────────┬────────────┘
|
|
453
491
|
│
|
|
454
492
|
┌────────────┴────────────┐
|
|
455
493
|
│ pipeline/cleanup.py │ shared lane
|
|
456
|
-
│
|
|
457
|
-
│
|
|
458
|
-
│
|
|
459
|
-
│
|
|
460
|
-
│
|
|
461
|
-
│
|
|
462
|
-
│
|
|
494
|
+
│ C1 NFC normalize │ (always last)
|
|
495
|
+
│ C2 soft hyphen strip │
|
|
496
|
+
│ C3 ligature normalize │
|
|
497
|
+
│ C4 quote/dash normalize│
|
|
498
|
+
│ C5 whitespace collapse │
|
|
499
|
+
│ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
|
|
500
|
+
│ C6 footnote marker strip│
|
|
501
|
+
│ C7 validate │
|
|
463
502
|
└────────────┬────────────┘
|
|
464
503
|
│
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
504
|
+
│ ┌──────────────────────────┐
|
|
505
|
+
│ │ any2md/heuristics.py │ ◀ NEW v1.0.2
|
|
506
|
+
│ │ - refine_title │
|
|
507
|
+
│ │ - refine_abstract │
|
|
508
|
+
│ │ - extract_authors │
|
|
509
|
+
│ │ (+ optional arxiv API)│
|
|
510
|
+
│ │ - filter_organization │
|
|
511
|
+
│ └────────────┬─────────────┘
|
|
512
|
+
│ │
|
|
513
|
+
▼ │
|
|
514
|
+
┌─────────────────────────┐ │
|
|
515
|
+
│ frontmatter.py │ ◀─────────┘ (consults heuristics
|
|
516
|
+
│ - merge user overrides │ before YAML emission)
|
|
468
517
|
│ - derive title/date/… │
|
|
518
|
+
│ - emit produced_by │ ◀ NEW v1.0.2
|
|
469
519
|
│ - compute content_hash │
|
|
470
520
|
│ - emit YAML │
|
|
471
521
|
└────────────┬────────────┘
|
|
@@ -474,7 +524,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
474
524
|
<output>.md ← frontmatter + cleaned body
|
|
475
525
|
```
|
|
476
526
|
|
|
477
|
-
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
527
|
+
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
478
528
|
|
|
479
529
|
## Migrating from v0.7
|
|
480
530
|
|
any2md-1.0.3/SECURITY.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
| Version | Status |
|
|
6
|
+
|---|---|
|
|
7
|
+
| 1.0.x | ✅ Supported (current stable). Patch releases for security and quality fixes are published as needed. |
|
|
8
|
+
| 0.7.x | ⚠️ Receives critical security fixes only until 2026-10-26 (six months after the v1.0.0 GA on 2026-04-26). |
|
|
9
|
+
| < 0.7 | ❌ Not supported. Please upgrade. |
|
|
10
|
+
|
|
11
|
+
## Reporting a vulnerability
|
|
12
|
+
|
|
13
|
+
**Please do not file public issues for security vulnerabilities.** Public
|
|
14
|
+
disclosure before a fix is published puts users at risk.
|
|
15
|
+
|
|
16
|
+
Email security disclosures to **rock@rockcyber.com** with the subject line
|
|
17
|
+
`any2md security disclosure`. Encrypted email is welcome — request a
|
|
18
|
+
public key in your initial message and one will be provided.
|
|
19
|
+
|
|
20
|
+
Include in your report:
|
|
21
|
+
|
|
22
|
+
- A description of the issue and the affected component.
|
|
23
|
+
- Affected version(s) — e.g., `pip show any2md` output, or commit SHA.
|
|
24
|
+
- Reproduction steps (smallest possible repro is ideal).
|
|
25
|
+
- Any proof-of-concept (please redact sensitive data).
|
|
26
|
+
- Whether you intend to publish your own write-up; if so, a proposed
|
|
27
|
+
embargo date so we can coordinate.
|
|
28
|
+
|
|
29
|
+
### Response timeline
|
|
30
|
+
|
|
31
|
+
- **Acknowledgement** within 5 business days.
|
|
32
|
+
- **Triage and severity assessment** within 10 business days.
|
|
33
|
+
- **Critical issues** (remote code execution, data exfiltration via
|
|
34
|
+
crafted input, supply-chain compromise of a published wheel): patched
|
|
35
|
+
and released within 30 days of confirmation, with a coordinated public
|
|
36
|
+
advisory.
|
|
37
|
+
- **High** (denial-of-service, information disclosure with limited blast
|
|
38
|
+
radius): patched within 60 days.
|
|
39
|
+
- **Medium / low** issues are tracked in private until a fix lands in a
|
|
40
|
+
scheduled release, then disclosed publicly via the GitHub Security
|
|
41
|
+
Advisories page.
|
|
42
|
+
|
|
43
|
+
## Scope
|
|
44
|
+
|
|
45
|
+
In scope:
|
|
46
|
+
|
|
47
|
+
- The `any2md` library and CLI.
|
|
48
|
+
- The published wheels and sdists on PyPI.
|
|
49
|
+
- The release pipeline (`.github/workflows/publish.yml`) and any
|
|
50
|
+
side-effects of running the workflow.
|
|
51
|
+
- The default conversion pipeline (PDF / DOCX / HTML / URL / TXT) and
|
|
52
|
+
the optional Docling backend integration.
|
|
53
|
+
- The arxiv API enrichment path (network call + XML parsing).
|
|
54
|
+
- The SSRF guards in the URL fetcher.
|
|
55
|
+
|
|
56
|
+
Out of scope:
|
|
57
|
+
|
|
58
|
+
- Vulnerabilities in third-party dependencies (please file directly with
|
|
59
|
+
the upstream — Docling, PyMuPDF, mammoth, trafilatura, BeautifulSoup,
|
|
60
|
+
lxml, etc.). We monitor upstream advisories via Dependabot.
|
|
61
|
+
- Issues that require local filesystem access already granted to the
|
|
62
|
+
running Python process.
|
|
63
|
+
- Local timing attacks against your own machine.
|
|
64
|
+
- Content correctness issues in converted Markdown (those are
|
|
65
|
+
conversion-quality reports — please use the
|
|
66
|
+
[Conversion quality issue template](.github/ISSUE_TEMPLATE/conversion_quality.md)
|
|
67
|
+
instead).
|
|
68
|
+
|
|
69
|
+
## Hardening reference
|
|
70
|
+
|
|
71
|
+
If you operate any2md in a hardened environment:
|
|
72
|
+
|
|
73
|
+
- Pass `--no-arxiv-lookup` to disable the arxiv API call.
|
|
74
|
+
- Pass `--max-file-size` with a budget appropriate for your workload.
|
|
75
|
+
- Run conversions in a sandbox that cannot reach private networks
|
|
76
|
+
(the SSRF guard validates resolved IPs, but defense in depth is best).
|
|
77
|
+
- Pin `any2md` and its extras (`any2md[high-fidelity]==<exact-version>`)
|
|
78
|
+
to a verified version and audit the lockfile.
|
|
79
|
+
|
|
80
|
+
## Disclosure history
|
|
81
|
+
|
|
82
|
+
No publicly disclosed vulnerabilities to date. Future advisories will
|
|
83
|
+
appear at https://github.com/rocklambros/any2md/security/advisories.
|
|
@@ -65,11 +65,7 @@ def _deep_merge_overrides(
|
|
|
65
65
|
"""
|
|
66
66
|
out = dict(base)
|
|
67
67
|
for key, val in overrides.items():
|
|
68
|
-
if (
|
|
69
|
-
key in out
|
|
70
|
-
and isinstance(out[key], dict)
|
|
71
|
-
and isinstance(val, dict)
|
|
72
|
-
):
|
|
68
|
+
if key in out and isinstance(out[key], dict) and isinstance(val, dict):
|
|
73
69
|
out[key] = _deep_merge_overrides(out[key], val)
|
|
74
70
|
else:
|
|
75
71
|
out[key] = val
|
|
@@ -132,6 +128,14 @@ def main():
|
|
|
132
128
|
action="store_true",
|
|
133
129
|
help="Force the Docling backend (PDF/DOCX). Exit 1 if not installed.",
|
|
134
130
|
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--no-arxiv-lookup",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="Disable the arxiv API lookup that enriches frontmatter for "
|
|
135
|
+
"PDFs whose filename matches the arxiv ID pattern (\\d{4}.\\d{4,5}). "
|
|
136
|
+
"Useful in airgapped envs or when you don't want any2md to make "
|
|
137
|
+
"network calls. Default: arxiv lookup is enabled.",
|
|
138
|
+
)
|
|
135
139
|
parser.add_argument(
|
|
136
140
|
"--backend",
|
|
137
141
|
choices=("docling", "pymupdf4llm", "mammoth"),
|
|
@@ -216,9 +220,7 @@ def main():
|
|
|
216
220
|
|
|
217
221
|
if args.meta_file is not None:
|
|
218
222
|
if not args.meta_file.is_file():
|
|
219
|
-
print(
|
|
220
|
-
f"Error: --meta-file not found: {args.meta_file}", file=sys.stderr
|
|
221
|
-
)
|
|
223
|
+
print(f"Error: --meta-file not found: {args.meta_file}", file=sys.stderr)
|
|
222
224
|
sys.exit(1)
|
|
223
225
|
cfg = load_toml(args.meta_file)
|
|
224
226
|
overrides = _deep_merge_overrides(overrides, extract_meta_overrides(cfg))
|
|
@@ -245,6 +247,7 @@ def main():
|
|
|
245
247
|
or args.backend == "docling"
|
|
246
248
|
):
|
|
247
249
|
from any2md._docling import has_docling, INSTALL_HINT_MSG
|
|
250
|
+
|
|
248
251
|
if not has_docling():
|
|
249
252
|
print(
|
|
250
253
|
f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images / --backend docling.\n"
|
|
@@ -268,6 +271,7 @@ def main():
|
|
|
268
271
|
auto_id_type_code=auto_id_type_code,
|
|
269
272
|
frontmatter_overrides=overrides or None,
|
|
270
273
|
backend=args.backend,
|
|
274
|
+
arxiv_lookup=not args.no_arxiv_lookup,
|
|
271
275
|
)
|
|
272
276
|
|
|
273
277
|
# CLI-only output controls (not part of PipelineOptions).
|