diff --git a/README.md b/README.md index 38fb157..35613d2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This packaged document includes a comprehensive directory structure, file conten - **.gitignore Integration:** Honors .gitignore files, letting you exclude specific files or directories. - **Output Splitting:** Can split the output into multiple files if the generated document exceeds a specified size. - **Flexible Modes:** Optionally generate structure-only documentation or include file contents. +- **Codebase Reconstruction:** Rebuild a directory and its files from a dirdoc-generated Markdown document. Binary files are recreated as empty placeholders. ## Example Output @@ -179,6 +180,11 @@ dirdoc --help dirdoc --include-git /path/to/dir ``` +- **Reconstruct a codebase from documentation:** Binary files will be restored as empty files. + ```bash + dirdoc --reconstruct -o ./restored project_documentation.md + ``` + ## Use Cases dirdoc is ideal for: diff --git a/src/dirdoc.c b/src/dirdoc.c index 42c3113..b0be9fd 100644 --- a/src/dirdoc.c +++ b/src/dirdoc.c @@ -7,6 +7,7 @@ #include #include "dirdoc.h" #include "writer.h" // Include writer.h to set split options +#include "reconstruct.h" #if !defined(UNIT_TEST) /** @@ -22,7 +23,8 @@ static void print_help() { " -sp, --split Enable split output. Optionally, use -l/--limit to specify maximum file size in MB (default: 18).\n" " -l, --limit Set maximum file size in MB for each split file (used with -sp).\n" " -ig, --include-git Include .git folders in documentation (default: ignored).\n" - " --ignore Ignore files matching the specified pattern (supports wildcards). Can be specified multiple times.\n\n" + " --ignore Ignore files matching the specified pattern (supports wildcards). Can be specified multiple times.\n" + " -rc, --reconstruct Reconstruct a directory from a dirdoc markdown. Use -o to specify the output directory.\n\n" "Examples:\n" " dirdoc /path/to/dir\n" " dirdoc -o custom.md /path/to/dir\n" @@ -51,6 +53,7 @@ int main(int argc, char *argv[]) { const char *output_file = NULL; int flags = 0; double split_limit_mb = 18.0; // Default split limit in MB + int reconstruct_mode = 0; #define MAX_IGNORE_PATTERNS 64 char *ignore_patterns[MAX_IGNORE_PATTERNS]; @@ -72,6 +75,8 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Error: --output requires a filename argument.\n"); return 1; } + } else if ((strcmp(argv[i], "--reconstruct") == 0) || (strcmp(argv[i], "-rc") == 0)) { + reconstruct_mode = 1; } else if ((strcmp(argv[i], "--no-gitignore") == 0) || (strcmp(argv[i], "-ngi") == 0)) { flags |= IGNORE_GITIGNORE; } else if ((strcmp(argv[i], "-s") == 0) || (strcmp(argv[i], "--structure-only") == 0)) { @@ -123,11 +128,16 @@ int main(int argc, char *argv[]) { } if (!input_dir) { - fprintf(stderr, "Error: No directory specified.\n"); + fprintf(stderr, "Error: No input path specified.\n"); print_help(); return 1; } + if (reconstruct_mode) { + const char *out_dir = output_file ? output_file : "."; + return reconstruct_from_markdown(input_dir, out_dir); + } + // Set split options in writer module if SPLIT_OUTPUT flag is enabled. if (flags & SPLIT_OUTPUT) { set_split_options(1, split_limit_mb); @@ -138,10 +148,8 @@ int main(int argc, char *argv[]) { set_extra_ignore_patterns(ignore_patterns, ignore_patterns_count); } - // Call document_directory from the writer module. int result = document_directory(input_dir, output_file, flags); - // Now free the patterns after document_directory is done with them free_extra_ignore_patterns(); return result; diff --git a/src/reconstruct.c b/src/reconstruct.c new file mode 100644 index 0000000..c209c0c --- /dev/null +++ b/src/reconstruct.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +#include "reconstruct.h" +#include "dirdoc.h" // for MAX_PATH_LEN and BUFFER_SIZE + +static int mkdirs(const char *path) { + char tmp[MAX_PATH_LEN]; + snprintf(tmp, sizeof(tmp), "%s", path); + size_t len = strlen(tmp); + if (len == 0) + return 0; + if (tmp[len - 1] == '/') + tmp[len - 1] = '\0'; + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755); // final component +} + +static int is_fence_start(const char *line, int *len) { + int i = 0; + while (line[i] == '`') i++; + if (i >= 3) { + *len = i; + return 1; + } + return 0; +} + +static int is_fence_end(const char *line, int len) { + for (int i = 0; i < len; i++) { + if (line[i] != '`') return 0; + } + char c = line[len]; + return c == '\n' || c == '\0' || c == '\r'; +} + +int reconstruct_from_markdown(const char *md_path, const char *out_dir) { + FILE *in = fopen(md_path, "r"); + if (!in) { + fprintf(stderr, "Error: cannot open %s\n", md_path); + return 1; + } + + char line[BUFFER_SIZE]; + char file_path[MAX_PATH_LEN]; + FILE *out = NULL; + int in_code = 0; + int fence_len = 0; + int skip_file = 0; + + while (fgets(line, sizeof(line), in)) { + if (!in_code && strncmp(line, "### 📄 ", 8) == 0) { + if (out) { + fclose(out); + out = NULL; + } + skip_file = 0; + line[strcspn(line, "\r\n")] = '\0'; + snprintf(file_path, sizeof(file_path), "%s/%s", out_dir, line + 9); + char dir[MAX_PATH_LEN]; + snprintf(dir, sizeof(dir), "%s", file_path); + char *p = strrchr(dir, '/'); + if (p) { + *p = '\0'; + mkdirs(dir); + } else { + mkdirs(out_dir); + } + out = fopen(file_path, "w"); + continue; + } + + if (!in_code) { + if (is_fence_start(line, &fence_len)) { + in_code = 1; + if (out) ftruncate(fileno(out), 0); // ensure empty before writing + continue; + } + } else { + if (is_fence_end(line, fence_len)) { + in_code = 0; + if (out) { + fclose(out); + out = NULL; + } + continue; + } + if (out && !skip_file) { + if (strncmp(line, "*Binary file*", 13) == 0 || strncmp(line, "*Error", 6) == 0) { + /* Leave an empty file in place of binary or errored files */ + skip_file = 1; + if (out) { + fclose(out); + out = NULL; + } + continue; + } + fputs(line, out); + } + } + } + + if (out) + fclose(out); + fclose(in); + return 0; +} + diff --git a/src/reconstruct.h b/src/reconstruct.h new file mode 100644 index 0000000..0854ef1 --- /dev/null +++ b/src/reconstruct.h @@ -0,0 +1,19 @@ +#ifndef RECONSTRUCT_H +#define RECONSTRUCT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Reconstructs a directory from a dirdoc generated markdown file. + * md_path: path to the documentation markdown + * out_dir: directory to create reconstructed files + * Returns 0 on success, non-zero on failure. + */ +int reconstruct_from_markdown(const char *md_path, const char *out_dir); + +#ifdef __cplusplus +} +#endif + +#endif /* RECONSTRUCT_H */ diff --git a/tests/test_dirdoc.c b/tests/test_dirdoc.c index e641b6e..c712b4d 100644 --- a/tests/test_dirdoc.c +++ b/tests/test_dirdoc.c @@ -21,6 +21,7 @@ void test_smart_split(); void run_tiktoken_tests(); void run_split_tests(); int run_file_deletion_tests(void); +void run_reconstruct_tests(); #ifndef MAX_PATH_LEN #define MAX_PATH_LEN 4096 @@ -704,6 +705,7 @@ int main(int argc, char *argv[]) { run_tiktoken_tests(); run_split_tests(); run_file_deletion_tests(); + run_reconstruct_tests(); printf("✅ All tests passed!\n"); diff --git a/tests/test_reconstruct.c b/tests/test_reconstruct.c new file mode 100644 index 0000000..e0db0c1 --- /dev/null +++ b/tests/test_reconstruct.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +#include "reconstruct.h" + +/* Functions from test_dirdoc.c */ +char *create_temp_dir(); +int remove_directory_recursive(const char *path); + +void test_reconstruct_basic() { + const char *md = "example_project/example_project_documentation.md"; + char *out_dir = create_temp_dir(); + int ret = reconstruct_from_markdown(md, out_dir); + assert(ret == 0); + + char path[512]; + snprintf(path, sizeof(path), "%s/src/example_main.c", out_dir); + FILE *f = fopen(path, "r"); + assert(f != NULL); + char line[64]; + fgets(line, sizeof(line), f); + fclose(f); + assert(strstr(line, "/*") != NULL); + + remove_directory_recursive(out_dir); + free(out_dir); + printf("✔ test_reconstruct_basic passed\n"); +} + +void test_reconstruct_binary_placeholder() { + const char *markdown = + "# Directory Documentation:\n\n" + "### \xF0\x9F\x93\x84 bin/file.bin\n\n" + "```\n" + "*Binary file*\n" + "```\n"; + + char *out_dir = create_temp_dir(); + char md_path[512]; + snprintf(md_path, sizeof(md_path), "%s/doc.md", out_dir); + FILE *md = fopen(md_path, "w"); + assert(md != NULL); + fputs(markdown, md); + fclose(md); + + int ret = reconstruct_from_markdown(md_path, out_dir); + assert(ret == 0); + + char path[512]; + snprintf(path, sizeof(path), "%s/bin/file.bin", out_dir); + FILE *f = fopen(path, "r"); + assert(f != NULL); + fseek(f, 0, SEEK_END); + long size = ftell(f); + fclose(f); + assert(size == 0); + + remove_directory_recursive(out_dir); + free(out_dir); + printf("✔ test_reconstruct_binary_placeholder passed\n"); +} + +void run_reconstruct_tests() { + printf("Running reconstruct tests...\n"); + test_reconstruct_basic(); + test_reconstruct_binary_placeholder(); + printf("All reconstruct tests passed!\n"); +}