From c8100b48f4f27adfc88cfa1a8efb288b673155af Mon Sep 17 00:00:00 2001 From: geopanther Date: Thu, 9 Apr 2026 17:56:26 +0200 Subject: [PATCH 1/4] feat(document): add --skip-subtrees-wo-markdown support to directory traversal Add _subtree_has_markdown() helper that walks a directory tree checking for .md files while respecting gitignore rules. Add skip_subtrees_wo_markdown parameter to get_pages_from_directory() that prunes directories in-place during os.walk to skip subtrees containing no markdown files. --- md2cf/document.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/md2cf/document.py b/md2cf/document.py index 5ea2592..79728eb 100644 --- a/md2cf/document.py +++ b/md2cf/document.py @@ -71,6 +71,18 @@ def __repr__(self): ) +def _subtree_has_markdown(path: Path, git_repo: GitRepository) -> bool: + """Check if a directory tree contains any .md files (respecting gitignore).""" + for dirpath, _, filenames in os.walk(path): + dirpath = Path(dirpath).resolve() + if git_repo.is_ignored(dirpath): + continue + for fname in filenames: + if fname.endswith(".md") and not git_repo.is_ignored(dirpath / fname): + return True + return False + + def find_non_empty_parent_path( current_dir: Path, folder_data: Dict[Path, Dict[str, Any]], default: Path ) -> Path: @@ -91,6 +103,7 @@ def get_pages_from_directory( remove_text_newlines: bool = False, use_gitignore: bool = True, enable_relative_links: bool = False, + skip_subtrees_wo_markdown: bool = False, ) -> List[Page]: """ Collect a list of markdown files recursively under the file_path directory. @@ -107,6 +120,8 @@ def get_pages_from_directory( search :param enable_relative_links: extract all relative links and replace them with placeholders + :param skip_subtrees_wo_markdown: skip directory subtrees that contain no markdown + files :return: A list of paths to the markdown files to upload. """ processed_pages = list() @@ -130,6 +145,13 @@ def get_pages_from_directory( path for path in markdown_files if not git_repo.is_ignored(path) ] + if skip_subtrees_wo_markdown: + directories[:] = [ + d + for d in directories + if _subtree_has_markdown(Path(current_path, d), git_repo) + ] + folder_data[current_path] = {"n_files": len(markdown_files)} # we'll capture title and path of the parent folder for this folder: From 7efc041d4273b58ebfcb0a1653a0488afc5e84af Mon Sep 17 00:00:00 2001 From: geopanther Date: Thu, 9 Apr 2026 17:56:47 +0200 Subject: [PATCH 2/4] feat(cli): add --skip-subtrees-wo-markdown command line option Add the --skip-subtrees-wo-markdown flag to the directory arguments group and wire it through collect_pages_to_upload() to get_pages_from_directory(). --- md2cf/__main__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/md2cf/__main__.py b/md2cf/__main__.py index 7db41fb..0c6bfe5 100644 --- a/md2cf/__main__.py +++ b/md2cf/__main__.py @@ -211,6 +211,11 @@ def get_parser(): action="store_true", help="if a folder doesn't contain documents, skip it", ) + dir_group.add_argument( + "--skip-subtrees-wo-markdown", + action="store_true", + help="skip directory subtrees that do not contain any markdown files", + ) relative_links_group = parser.add_argument_group("relative links arguments") relative_links_group.add_argument( @@ -697,6 +702,7 @@ def collect_pages_to_upload(args): use_pages_file=args.use_pages_file, use_gitignore=args.use_gitignore, enable_relative_links=args.enable_relative_links, + skip_subtrees_wo_markdown=args.skip_subtrees_wo_markdown, ) else: try: From 06d4ba984aaccab4158ad09b656205bb22b2fc5a Mon Sep 17 00:00:00 2001 From: geopanther Date: Thu, 9 Apr 2026 17:57:16 +0200 Subject: [PATCH 3/4] test(document): add unit tests for --skip-subtrees-wo-markdown Add 5 tests covering: basic subtree pruning, deeply nested markdown, root-level markdown with pruned subtrees, all-empty directory case, and flag-disabled behavior comparison. --- test_package/unit/test_document.py | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test_package/unit/test_document.py b/test_package/unit/test_document.py index 0d1cb69..a495a62 100644 --- a/test_package/unit/test_document.py +++ b/test_package/unit/test_document.py @@ -319,3 +319,94 @@ def test_get_document_frontmatter_empty(): """ assert doc.get_document_frontmatter(source_markdown.splitlines(keepends=True)) == {} + + +def test_get_pages_from_directory_skip_subtrees_wo_markdown(fs): + """Subtrees without any markdown files are skipped entirely.""" + fs.create_file("/root-folder/docs/readme.md") + fs.create_file("/root-folder/images/logo.png") + fs.create_dir("/root-folder/images/icons") + fs.create_file("/root-folder/images/icons/favicon.ico") + fs.create_file("/root-folder/data/config.yaml") + + result = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=True + ) + assert result == [ + FakePage(title="docs", file_path=None, parent_title=None), + FakePage( + title="readme", + file_path=Path("/root-folder/docs/readme.md"), + parent_title="docs", + ), + ] + + +def test_get_pages_from_directory_skip_subtrees_wo_markdown_nested(fs): + """A subtree with markdown deeply nested is kept, but sibling subtrees without + markdown are pruned.""" + fs.create_file("/root-folder/a/b/c/deep.md") + fs.create_file("/root-folder/a/b/other/data.txt") + fs.create_file("/root-folder/empty-tree/sub/file.txt") + + result = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=True + ) + assert result == [ + FakePage(title="a", file_path=None, parent_title=None), + FakePage(title="b", file_path=None, parent_title="a"), + FakePage(title="c", file_path=None, parent_title="b"), + FakePage( + title="deep", + file_path=Path("/root-folder/a/b/c/deep.md"), + parent_title="c", + ), + ] + + +def test_get_pages_from_directory_skip_subtrees_wo_markdown_root_has_md(fs): + """Root-level markdown files are still included; only subtrees without markdown + are pruned.""" + fs.create_file("/root-folder/index.md") + fs.create_file("/root-folder/no-md/data.csv") + + result = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=True + ) + assert result == [ + FakePage( + title="index", + file_path=Path("/root-folder/index.md"), + parent_title=None, + ), + ] + + +def test_get_pages_from_directory_skip_subtrees_wo_markdown_all_empty(fs): + """When no subtree contains markdown, nothing is returned.""" + fs.create_file("/root-folder/images/logo.png") + fs.create_file("/root-folder/data/config.yaml") + + result = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=True + ) + assert result == [] + + +def test_get_pages_from_directory_skip_subtrees_wo_markdown_disabled(fs): + """Without the flag, subtrees without markdown are still traversed and produce + folder pages when they have subdirectories.""" + fs.create_file("/root-folder/docs/readme.md") + fs.create_file("/root-folder/images/icons/favicon.ico") + + result_with = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=True + ) + result_without = doc.get_pages_from_directory( + Path("/root-folder"), skip_subtrees_wo_markdown=False + ) + # With the flag, images/ subtree is excluded + assert FakePage(title="images") not in result_with + # Without the flag, images/ subtree is included as a folder page + # (it has a subdirectory, so it gets a page entry) + assert FakePage(title="images") in result_without From c0a51d27638ffb9fd974461634d7d45cd2ba8801 Mon Sep 17 00:00:00 2001 From: geopanther Date: Thu, 9 Apr 2026 17:57:44 +0200 Subject: [PATCH 4/4] docs: document --skip-subtrees-wo-markdown option in README Add a new section under 'Directory arguments' with a description and collapsible example showing how subtrees without markdown are pruned. --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 18750cf..50329b8 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,35 @@ folderA/ ``` +#### Skipping subtrees without markdown + +Passing `--skip-subtrees-wo-markdown` will skip any directory subtree that does not contain any markdown files. This is useful when your folder structure contains non-documentation directories (e.g. images, data, configurations) that you don't want to appear in Confluence. + +
+Example +```text +document.md +docs/ + guide.md +images/ + icons/ + favicon.ico + logo.png +data/ + config.yaml +``` + +will be uploaded as: + +```text +document +docs/ + guide +``` + +The `images/` and `data/` subtrees are entirely skipped because they contain no markdown files. +
+ ## Terminal output format By default, `md2cf` produces rich output with animated progress bars that are meant for human consumption. If the output is redirected to a file, the progress bars will not be displayed and only the final result will be written to the file. Error messages are always printed to standard error.