Skip to content

Commit 4fef765

Browse files
committed
feat: add option to include DOCX comments
1 parent 9dc0d65 commit 4fef765

6 files changed

Lines changed: 107 additions & 9 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ You can also pipe content:
8888
cat path-to-file.pdf | markitdown
8989
```
9090

91+
Reviewer comments are excluded from DOCX output by default. Include them, along
92+
with references to their locations in the document, with `--include-comments`:
93+
94+
```bash
95+
markitdown reviewed-document.docx --include-comments
96+
```
97+
9198
### Optional Dependencies
9299
MarkItDown has optional dependencies for activating various file formats. Earlier in this document, we installed all optional dependencies with the `[all]` option. However, you can also install them individually for more control. For example:
93100

@@ -258,6 +265,17 @@ result = md.convert("test.xlsx")
258265
print(result.text_content)
259266
```
260267

268+
To include reviewer comments when converting a DOCX file, pass
269+
`include_comments=True`. The option can be supplied to an individual conversion
270+
or to the `MarkItDown` constructor:
271+
272+
```python
273+
result = md.convert("reviewed-document.docx", include_comments=True)
274+
275+
md_with_comments = MarkItDown(include_comments=True)
276+
result = md_with_comments.convert("reviewed-document.docx")
277+
```
278+
261279
Document Intelligence conversion in Python:
262280

263281
```python

packages/markitdown/src/markitdown/__main__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ def main():
138138
help="Keep data URIs (like base64-encoded images) in the output. By default, data URIs are truncated.",
139139
)
140140

141+
parser.add_argument(
142+
"--include-comments",
143+
action="store_true",
144+
help="Include reviewer comments when converting DOCX files.",
145+
)
146+
141147
parser.add_argument("filename", nargs="?")
142148
args = parser.parse_args()
143149

@@ -244,15 +250,23 @@ def main():
244250
else:
245251
markitdown = MarkItDown(enable_plugins=args.use_plugins)
246252

253+
conversion_kwargs: Dict[str, Any] = {
254+
"keep_data_uris": args.keep_data_uris,
255+
}
256+
if args.include_comments:
257+
conversion_kwargs["include_comments"] = True
258+
247259
if args.filename is None:
248260
result = markitdown.convert_stream(
249261
sys.stdin.buffer,
250262
stream_info=stream_info,
251-
keep_data_uris=args.keep_data_uris,
263+
**conversion_kwargs,
252264
)
253265
else:
254266
result = markitdown.convert(
255-
args.filename, stream_info=stream_info, keep_data_uris=args.keep_data_uris
267+
args.filename,
268+
stream_info=stream_info,
269+
**conversion_kwargs,
256270
)
257271

258272
_handle_output(args, result)

packages/markitdown/src/markitdown/_markitdown.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __init__(
126126
self._llm_prompt: Union[str | None] = None
127127
self._exiftool_path: Union[str | None] = None
128128
self._style_map: Union[str | None] = None
129+
self._include_comments: Union[bool | None] = None
129130

130131
# Register the converters
131132
self._converters: List[ConverterRegistration] = []
@@ -151,6 +152,7 @@ def enable_builtins(self, **kwargs) -> None:
151152
self._llm_prompt = kwargs.get("llm_prompt")
152153
self._exiftool_path = kwargs.get("exiftool_path")
153154
self._style_map = kwargs.get("style_map")
155+
self._include_comments = kwargs.get("include_comments")
154156

155157
if self._exiftool_path is None:
156158
self._exiftool_path = os.getenv("EXIFTOOL_PATH")
@@ -597,6 +599,12 @@ def _convert(
597599
if "style_map" not in _kwargs and self._style_map is not None:
598600
_kwargs["style_map"] = self._style_map
599601

602+
if (
603+
"include_comments" not in _kwargs
604+
and self._include_comments is not None
605+
):
606+
_kwargs["include_comments"] = self._include_comments
607+
600608
if "exiftool_path" not in _kwargs and self._exiftool_path is not None:
601609
_kwargs["exiftool_path"] = self._exiftool_path
602610

packages/markitdown/src/markitdown/converters/_docx_converter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
ACCEPTED_FILE_EXTENSIONS = [".docx"]
2929

30+
COMMENT_STYLE_MAP = "comment-reference => "
31+
3032

3133
class DocxConverter(HtmlConverter):
3234
"""
@@ -76,6 +78,10 @@ def convert(
7678
)
7779

7880
style_map = kwargs.get("style_map", None)
81+
if kwargs.get("include_comments", False):
82+
style_map = (
83+
f"{style_map}\n{COMMENT_STYLE_MAP}" if style_map else COMMENT_STYLE_MAP
84+
)
7985
pre_process_stream = pre_process_docx(file_stream)
8086
return self._html_converter.convert_string(
8187
mammoth.convert_to_html(pre_process_stream, style_map=style_map).value,

packages/markitdown/tests/test_cli_misc.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
#!/usr/bin/env python3 -m pytest
22
import subprocess
3+
import sys
4+
from pathlib import Path
35
from markitdown import __version__
46

7+
8+
TEST_FILES_DIR = Path(__file__).parent / "test_files"
9+
510
# This file contains CLI tests that are not directly tested by the FileTestVectors.
611
# This includes things like help messages, version numbers, and invalid flags.
712

813

914
def test_version() -> None:
1015
result = subprocess.run(
11-
["python", "-m", "markitdown", "--version"], capture_output=True, text=True
16+
[sys.executable, "-m", "markitdown", "--version"],
17+
capture_output=True,
18+
text=True,
1219
)
1320

1421
assert result.returncode == 0, f"CLI exited with error: {result.stderr}"
@@ -17,7 +24,9 @@ def test_version() -> None:
1724

1825
def test_invalid_flag() -> None:
1926
result = subprocess.run(
20-
["python", "-m", "markitdown", "--foobar"], capture_output=True, text=True
27+
[sys.executable, "-m", "markitdown", "--foobar"],
28+
capture_output=True,
29+
text=True,
2130
)
2231

2332
assert result.returncode != 0, f"CLI exited with error: {result.stderr}"
@@ -27,6 +36,23 @@ def test_invalid_flag() -> None:
2736
assert "SYNTAX" in result.stderr, "Expected 'SYNTAX' to appear in STDERR"
2837

2938

39+
def test_include_docx_comments() -> None:
40+
result = subprocess.run(
41+
[
42+
sys.executable,
43+
"-m",
44+
"markitdown",
45+
str(TEST_FILES_DIR / "test_with_comment.docx"),
46+
"--include-comments",
47+
],
48+
capture_output=True,
49+
text=True,
50+
)
51+
52+
assert result.returncode == 0, f"CLI exited with error: {result.stderr}"
53+
assert "This is a test comment. 12df-321a" in result.stdout
54+
55+
3056
if __name__ == "__main__":
3157
"""Runs this file's tests from the command line."""
3258
test_version()

packages/markitdown/tests/test_module_misc.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,39 @@ def test_file_uris() -> None:
253253

254254

255255
def test_docx_comments() -> None:
256-
# Test DOCX processing, with comments and setting style_map on init
257-
markitdown_with_style_map = MarkItDown(style_map="comment-reference => ")
258-
result = markitdown_with_style_map.convert(
259-
os.path.join(TEST_FILES_DIR, "test_with_comment.docx")
260-
)
256+
docx_file = os.path.join(TEST_FILES_DIR, "test_with_comment.docx")
257+
258+
# Comments remain excluded by default for backwards compatibility.
259+
result = MarkItDown().convert(docx_file)
260+
validate_strings(result, [], exclude_strings=DOCX_COMMENT_TEST_STRINGS[-2:])
261+
262+
# Comments can be enabled for a single conversion.
263+
result = MarkItDown().convert(docx_file, include_comments=True)
264+
validate_strings(result, DOCX_COMMENT_TEST_STRINGS)
265+
assert "(#comment-0)" in result.text_content
266+
assert "(#comment-ref-0)" in result.text_content
267+
268+
# The option can also be set when constructing a reusable converter.
269+
markitdown_with_comments = MarkItDown(include_comments=True)
270+
result = markitdown_with_comments.convert(docx_file)
261271
validate_strings(result, DOCX_COMMENT_TEST_STRINGS)
262272

273+
# Per-conversion options take precedence over constructor options.
274+
result = markitdown_with_comments.convert(docx_file, include_comments=False)
275+
validate_strings(result, [], exclude_strings=DOCX_COMMENT_TEST_STRINGS[-2:])
276+
277+
278+
def test_docx_comments_preserve_custom_style_map() -> None:
279+
docx_file = os.path.join(TEST_FILES_DIR, "test_with_comment.docx")
280+
result = MarkItDown().convert(
281+
docx_file,
282+
include_comments=True,
283+
style_map="p[style-name='heading 1'] => h6:fresh",
284+
)
285+
286+
assert "###### Abstract" in result.text_content
287+
validate_strings(result, DOCX_COMMENT_TEST_STRINGS[-2:])
288+
263289

264290
def test_docx_equations() -> None:
265291
markitdown = MarkItDown()

0 commit comments

Comments
 (0)