Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 12 additions & 4 deletions src/dirdoc.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <string.h>
#include "dirdoc.h"
#include "writer.h" // Include writer.h to set split options
#include "reconstruct.h"

#if !defined(UNIT_TEST)
/**
Expand All @@ -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 <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 <pattern> Ignore files matching the specified pattern (supports wildcards). Can be specified multiple times.\n\n"
" --ignore <pattern> 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"
Expand Down Expand Up @@ -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];
Expand All @@ -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)) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
117 changes: 117 additions & 0 deletions src/reconstruct.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#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;
}

19 changes: 19 additions & 0 deletions src/reconstruct.h
Original file line number Diff line number Diff line change
@@ -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 */
2 changes: 2 additions & 0 deletions tests/test_dirdoc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");

Expand Down
71 changes: 71 additions & 0 deletions tests/test_reconstruct.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#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");
}
Loading