flatten-tool 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/index.ts +80 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install -g flatten-tool
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
-
By default, the tool merges all file contents into a single Markdown file, with each file's content
|
|
19
|
+
By default, the tool merges all file contents into a single Markdown file, starting with a project file tree for navigation, followed by each file's content under a header with its relative path, in a code block with appropriate language highlighting based on the file extension. Ignores and filters are applied as usual.
|
|
20
20
|
|
|
21
21
|
The `<source>` argument is optional and defaults to the current directory (`.`). The `<target>` argument is also optional and defaults to `flattened.md` (or `flattened/` when using `--directory`).
|
|
22
22
|
|
|
@@ -113,6 +113,13 @@ This project uses Bun for runtime, TypeScript for type safety, and follows the g
|
|
|
113
113
|
|
|
114
114
|
## Changelog
|
|
115
115
|
|
|
116
|
+
### v1.5.0
|
|
117
|
+
- Improved navigation: project file tree is now a clickable nested Markdown list with links to each file's content section using standard markdown anchors.
|
|
118
|
+
- Simplified file headers: removed custom anchors from section headers.
|
|
119
|
+
|
|
120
|
+
### v1.4.0
|
|
121
|
+
- Added project file tree to the beginning of merged Markdown output for better navigation.
|
|
122
|
+
|
|
116
123
|
### v1.3.1
|
|
117
124
|
- Fixed GIF exclusion pattern to work recursively in subdirectories.
|
|
118
125
|
|
package/index.ts
CHANGED
|
@@ -12,6 +12,62 @@ function escapePathComponent(component: string): string {
|
|
|
12
12
|
return component.replace(/_/g, '__');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function generateMarkdownAnchor(text: string): string {
|
|
16
|
+
return text
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^\w\s-]/g, '') // Remove punctuation except hyphens and underscores
|
|
19
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
20
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildTreeObject(relPaths: string[]): any {
|
|
24
|
+
const tree: any = {};
|
|
25
|
+
for (const path of relPaths) {
|
|
26
|
+
const parts = path.split('/');
|
|
27
|
+
let node = tree;
|
|
28
|
+
const currentParts: string[] = [];
|
|
29
|
+
for (const [index, part] of parts.entries()) {
|
|
30
|
+
currentParts.push(part);
|
|
31
|
+
const isDir = index < parts.length - 1;
|
|
32
|
+
const key = isDir ? `${part}/` : part;
|
|
33
|
+
if (node[key] === undefined) {
|
|
34
|
+
node[key] = isDir ? {} : currentParts.join('/');
|
|
35
|
+
}
|
|
36
|
+
if (isDir) {
|
|
37
|
+
node = node[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return tree;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderMarkdownTree(node: any, depth: number): string {
|
|
45
|
+
let result = '';
|
|
46
|
+
const indent = ' '.repeat(depth);
|
|
47
|
+
const entries: [string, any][] = Object.entries(node);
|
|
48
|
+
|
|
49
|
+
entries.sort(([a], [b]) => {
|
|
50
|
+
const aDir = a.endsWith('/');
|
|
51
|
+
const bDir = b.endsWith('/');
|
|
52
|
+
if (aDir !== bDir) return aDir ? -1 : 1;
|
|
53
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of entries) {
|
|
57
|
+
const isDir = key.endsWith('/');
|
|
58
|
+
const display = isDir ? key.slice(0, -1) + '/' : key;
|
|
59
|
+
if (isDir) {
|
|
60
|
+
result += `${indent}- ${display}\n`;
|
|
61
|
+
result += renderMarkdownTree(value, depth + 1);
|
|
62
|
+
} else {
|
|
63
|
+
const fullPath = value as string;
|
|
64
|
+
const anchor = generateMarkdownAnchor(fullPath);
|
|
65
|
+
result += `${indent}- [${display}](#${anchor})\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
15
71
|
async function removeEmptyDirs(dir: string, root?: string): Promise<void> {
|
|
16
72
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
17
73
|
for (const entry of entries) {
|
|
@@ -52,7 +108,7 @@ export async function flattenDirectory(
|
|
|
52
108
|
ignoreFiles.push('**/*.gif');
|
|
53
109
|
}
|
|
54
110
|
|
|
55
|
-
|
|
111
|
+
const files = await globby(['**', ...negativeIgnores], {
|
|
56
112
|
cwd: absSource,
|
|
57
113
|
gitignore: respectGitignore,
|
|
58
114
|
absolute: true,
|
|
@@ -73,25 +129,40 @@ export async function flattenDirectory(
|
|
|
73
129
|
if (err.code !== 'ENOENT') throw err;
|
|
74
130
|
}
|
|
75
131
|
|
|
132
|
+
// Sort files for consistent content order
|
|
133
|
+
const fileEntries = files.map(srcPath => ({
|
|
134
|
+
srcPath,
|
|
135
|
+
relPath: relative(absSource, srcPath).replace(/\\/g, '/')
|
|
136
|
+
}));
|
|
137
|
+
fileEntries.sort((a, b) => a.relPath.toLowerCase().localeCompare(b.relPath.toLowerCase()));
|
|
138
|
+
|
|
139
|
+
const relPaths = fileEntries.map(e => e.relPath);
|
|
140
|
+
|
|
141
|
+
// Build tree structure
|
|
142
|
+
const treeObj = buildTreeObject(relPaths);
|
|
143
|
+
|
|
144
|
+
// Render as clickable nested Markdown list
|
|
145
|
+
let treeMarkdown = "# Project File Tree\n\n- .\n";
|
|
146
|
+
treeMarkdown += renderMarkdownTree(treeObj, 1);
|
|
147
|
+
treeMarkdown += "\n";
|
|
148
|
+
|
|
76
149
|
const writeStream = createWriteStream(absTarget);
|
|
77
150
|
writeStream.setMaxListeners(0);
|
|
78
151
|
|
|
79
|
-
|
|
80
|
-
|
|
152
|
+
writeStream.write(treeMarkdown);
|
|
153
|
+
|
|
154
|
+
// Write file contents (now in sorted order)
|
|
155
|
+
for (const { srcPath, relPath } of fileEntries) {
|
|
81
156
|
let ext = extname(srcPath).slice(1) || 'text';
|
|
157
|
+
const lang = ext;
|
|
82
158
|
const isMd = ['md', 'markdown'].includes(ext.toLowerCase());
|
|
83
159
|
const ticks = isMd ? '````' : '```';
|
|
84
160
|
|
|
85
|
-
|
|
86
|
-
writeStream.write(`# ${relPath}\n\n${ticks}${ext}\n`);
|
|
161
|
+
writeStream.write(`## ${relPath}\n\n${ticks}${lang}\n`);
|
|
87
162
|
|
|
88
|
-
// Stream file content (preserve original UTF-8 text behavior)
|
|
89
163
|
const readStream = createReadStream(srcPath, { encoding: 'utf8' });
|
|
90
|
-
|
|
91
|
-
// Pipe with { end: false } so writeStream stays open for next files
|
|
92
164
|
await pipeline(readStream, writeStream, { end: false });
|
|
93
165
|
|
|
94
|
-
// Write closing fence
|
|
95
166
|
writeStream.write(`\n${ticks}\n\n`);
|
|
96
167
|
}
|
|
97
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flatten-tool",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI tool to flatten directory structures: merge files into a single Markdown file (default) or copy/move to a flat directory with escaped filenames. Respects .gitignore, supports move/overwrite, and more.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"globby": "^16.1.0",
|
|
50
50
|
"ignore": "^7.0.5",
|
|
51
51
|
"minimatch": "^10.1.2",
|
|
52
|
+
"treeify": "^1.1.0",
|
|
52
53
|
"yargs": "^18.0.0"
|
|
53
54
|
}
|
|
54
55
|
}
|