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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/index.ts +80 -9
  3. 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 placed under a header with its relative path, followed by a code block with appropriate language highlighting based on the file extension. Ignores and filters are applied as usual.
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
- const files = await globby(['**', ...negativeIgnores], {
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
- for (const srcPath of files) {
80
- const relPath = relative(absSource, srcPath).replace(/\\/g, '/');
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
- // Write header synchronously
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.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
  }