|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import fnmatch |
| 4 | +from md_utils import get_project_root, compute_page_id |
| 5 | +#import json |
| 6 | + |
| 7 | +# Directories to exclude explicitly (relative to the project root). |
| 8 | +EXCLUDED_DIRS = [ |
| 9 | + "Doc", |
| 10 | + "Media", |
| 11 | + "DiligentCore/BuildTools/Android", |
| 12 | + "DiligentSamples/Android", |
| 13 | + "DiligentSamples/Samples/Asteroids/SDK", |
| 14 | + "DiligentFX/Shaders/PostProcess/SuperResolution/private/fsr1", |
| 15 | + "DiligentTools/NativeApp/Android", |
| 16 | + "Tests" |
| 17 | +] |
| 18 | + |
| 19 | +# Additional exclusion patterns. |
| 20 | +ADDITIONAL_EXCLUDE_PATTERNS = [ |
| 21 | + "build/*", |
| 22 | + "*/build/*", |
| 23 | + "*/ThirdParty/*", |
| 24 | + ".*/*", |
| 25 | + "*/.*/*", |
| 26 | + "DiligentSamples/*/assets/*", |
| 27 | + "*/Tests/*", |
| 28 | + "*/__pycache__/*" |
| 29 | +] |
| 30 | + |
| 31 | +def should_exclude(rel_dir): |
| 32 | + """ |
| 33 | + Return True if the given relative directory (relative to the project root) |
| 34 | + should be excluded based on explicit directories, additional patterns, |
| 35 | + or if any directory component starts with a dot. |
| 36 | + """ |
| 37 | + # Normalize to forward slashes. |
| 38 | + norm_rel_dir = rel_dir.replace(os.sep, '/') |
| 39 | + |
| 40 | + # Check explicit exclusions. |
| 41 | + for ex in EXCLUDED_DIRS: |
| 42 | + if norm_rel_dir == ex or norm_rel_dir.startswith(ex + '/'): |
| 43 | + return True |
| 44 | + |
| 45 | + # Check additional patterns. |
| 46 | + for pattern in ADDITIONAL_EXCLUDE_PATTERNS: |
| 47 | + if fnmatch.fnmatch(norm_rel_dir, pattern) or fnmatch.fnmatch(norm_rel_dir + '/', pattern): |
| 48 | + return True |
| 49 | + |
| 50 | + return False |
| 51 | + |
| 52 | +def build_md_tree(root_dir, rel_dir=""): |
| 53 | + """ |
| 54 | + Recursively build a tree structure starting at root_dir. |
| 55 | + |
| 56 | + Parameters: |
| 57 | + - root_dir: the absolute path to the project root. |
| 58 | + - rel_dir: the relative directory path from the root (default is "", meaning the root itself). |
| 59 | + |
| 60 | + Returns: |
| 61 | + A dictionary node with keys: |
| 62 | + "path": the relative path of the node, |
| 63 | + "files": a sorted list of Markdown file names in that directory, |
| 64 | + "subdirs": a list of child nodes (for subdirectories that contain Markdown files), |
| 65 | + or None if neither the current directory nor any of its subdirectories contain Markdown files. |
| 66 | + """ |
| 67 | + current_path = os.path.join(root_dir, rel_dir) |
| 68 | + |
| 69 | + # Skip if the current relative directory should be excluded. |
| 70 | + if rel_dir and should_exclude(rel_dir): |
| 71 | + return None |
| 72 | + |
| 73 | + try: |
| 74 | + entries = os.listdir(current_path) |
| 75 | + except Exception as e: |
| 76 | + # If we cannot list the directory, skip it. |
| 77 | + return None |
| 78 | + |
| 79 | + files = [] |
| 80 | + subdirs = [] |
| 81 | + for entry in entries: |
| 82 | + full_path = os.path.join(current_path, entry) |
| 83 | + if os.path.isfile(full_path) and entry.lower().endswith('.md'): |
| 84 | + files.append(entry) |
| 85 | + elif os.path.isdir(full_path): |
| 86 | + subdirs.append(entry) |
| 87 | + |
| 88 | + # Recursively build nodes for subdirectories. |
| 89 | + children = [] |
| 90 | + for d in sorted(subdirs): |
| 91 | + child_rel = os.path.join(rel_dir, d) if rel_dir else d |
| 92 | + child_node = build_md_tree(root_dir, child_rel) |
| 93 | + if child_node is not None: |
| 94 | + children.append(child_node) |
| 95 | + |
| 96 | + # If there are no markdown files here and no children with markdown files, skip this directory. |
| 97 | + if not files and not children: |
| 98 | + return None |
| 99 | + |
| 100 | + return { |
| 101 | + "path": rel_dir, |
| 102 | + "files": sorted(files), |
| 103 | + "subdirs": children |
| 104 | + } |
| 105 | + |
| 106 | + |
| 107 | +def clean_level1(name): |
| 108 | + """If a level-1 folder name starts with 'Diligent' (case-insensitive), remove that prefix.""" |
| 109 | + if name.lower().startswith("diligent"): |
| 110 | + return name[len("Diligent"):] |
| 111 | + return name |
| 112 | + |
| 113 | + |
| 114 | +def compute_container(node, root_dir, level = 0): |
| 115 | + """ |
| 116 | + Recursively process the tree node and compute its container ID and page text. |
| 117 | +
|
| 118 | + Rules: |
| 119 | + - If the node contains exactly one Markdown file and no subdirectories, |
| 120 | + then its page text is empty (the file's page will be used as a subpage) |
| 121 | + and the function returns that file's page ID. |
| 122 | + - Otherwise, a container page is generated. Its page text is a block |
| 123 | + that starts with a \page definition (using a synthetic container ID and |
| 124 | + a title) and then lists as subpages all container IDs returned from: |
| 125 | + * Each Markdown file in the node |
| 126 | + * Each child node (from recursive invocation) |
| 127 | + The container ID is stored in node["container_id"] and the page text in node["page_text"]. |
| 128 | + The function returns the container ID. |
| 129 | + """ |
| 130 | + files = node.get("files", []) |
| 131 | + children = node.get("subdirs", []) |
| 132 | + rel_path = node.get("path", "") # relative path from the project root |
| 133 | + |
| 134 | + # Case 1: Leaf node (exactly one Markdown file and no subdirectories). |
| 135 | + if len(files) == 1 and not children: |
| 136 | + md_file = files[0] |
| 137 | + full_path = os.path.join(root_dir, rel_path, md_file) if rel_path else os.path.join(root_dir, md_file) |
| 138 | + file_id = compute_page_id(root_dir, full_path) |
| 139 | + node["container_id"] = file_id |
| 140 | + node["page_text"] = "" # no container page text for a single-file node |
| 141 | + return file_id |
| 142 | + |
| 143 | + # Case 2: Container node. |
| 144 | + # Create a synthetic container ID. |
| 145 | + if rel_path == "": |
| 146 | + container_id = "root" |
| 147 | + title = "User Guide" |
| 148 | + else: |
| 149 | + container_id = "dir_" + rel_path.replace(os.sep, "_") |
| 150 | + title = os.path.basename(rel_path) |
| 151 | + if level == 1: |
| 152 | + title = clean_level1(title) |
| 153 | + node["container_id"] = container_id |
| 154 | + |
| 155 | + # Gather subpage IDs from local Markdown files. |
| 156 | + subpage_ids = [] |
| 157 | + for f in files: |
| 158 | + full_path = os.path.join(root_dir, rel_path, f) if rel_path else os.path.join(root_dir, f) |
| 159 | + file_id = compute_page_id(root_dir, full_path) |
| 160 | + subpage_ids.append(file_id) |
| 161 | + |
| 162 | + # Recurse into each child node. |
| 163 | + for child in children: |
| 164 | + child_id = compute_container(child, root_dir, level + 1) |
| 165 | + subpage_ids.append(child_id) |
| 166 | + |
| 167 | + # Construct the page text for this container node. |
| 168 | + # It defines a page with the container ID and title, and lists subpages. |
| 169 | + lines = [] |
| 170 | + lines.append("/**\n") |
| 171 | + lines.append(f" * \\page {container_id} {title}\n") |
| 172 | + for sp in subpage_ids: |
| 173 | + lines.append(f" *\n") |
| 174 | + lines.append(f" * \\subpage {sp}\n") |
| 175 | + lines.append(" */\n\n") |
| 176 | + page_text = "".join(lines) |
| 177 | + node["page_text"] = page_text |
| 178 | + |
| 179 | + return container_id |
| 180 | + |
| 181 | +def write_pages(node, out_stream): |
| 182 | + """ |
| 183 | + Recursively traverse the tree and write out the page text for each node, |
| 184 | + if non-empty. |
| 185 | + """ |
| 186 | + if node.get("page_text"): |
| 187 | + out_stream.write(node["page_text"]) |
| 188 | + for child in node.get("subdirs", []): |
| 189 | + write_pages(child, out_stream) |
| 190 | + |
| 191 | + |
| 192 | +def main(): |
| 193 | + project_root = get_project_root() |
| 194 | + output_file = sys.argv[1] if len(sys.argv) >= 2 else None |
| 195 | + |
| 196 | + tree = build_md_tree(project_root) |
| 197 | + if tree is None: |
| 198 | + sys.stderr.write("No Markdown files found in the project.\n") |
| 199 | + return |
| 200 | + |
| 201 | + #print(json.dumps(tree, indent=4)) |
| 202 | + |
| 203 | + compute_container(tree, project_root) |
| 204 | + |
| 205 | + if output_file: |
| 206 | + out_stream = open(output_file, 'w', encoding='utf-8') |
| 207 | + else: |
| 208 | + out_stream = sys.stdout |
| 209 | + write_pages(tree, out_stream) |
| 210 | + |
| 211 | +if __name__ == "__main__": |
| 212 | + main() |
0 commit comments