flatten-tool 1.4.0 → 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 +4 -0
  2. package/index.ts +58 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -113,6 +113,10 @@ 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
+
116
120
  ### v1.4.0
117
121
  - Added project file tree to the beginning of merged Markdown output for better navigation.
118
122
 
package/index.ts CHANGED
@@ -7,63 +7,65 @@ import yargs from 'yargs';
7
7
  import { hideBin } from 'yargs/helpers';
8
8
  import { globby } from 'globby';
9
9
  import pkg from './package.json' assert { type: 'json' };
10
- import treeify from 'treeify';
11
10
 
12
11
  function escapePathComponent(component: string): string {
13
12
  return component.replace(/_/g, '__');
14
13
  }
15
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
+
16
23
  function buildTreeObject(relPaths: string[]): any {
17
24
  const tree: any = {};
18
-
19
- relPaths.forEach((path) => {
20
- // Normalize paths
21
- path = path.replace(/\\/g, '/');
22
- if (path.startsWith('./')) path = path.slice(2);
23
-
25
+ for (const path of relPaths) {
24
26
  const parts = path.split('/');
25
27
  let node = tree;
26
-
27
- parts.forEach((part, index) => {
28
+ const currentParts: string[] = [];
29
+ for (const [index, part] of parts.entries()) {
30
+ currentParts.push(part);
28
31
  const isDir = index < parts.length - 1;
29
32
  const key = isDir ? `${part}/` : part;
30
-
31
- if (!node[key]) {
32
- node[key] = isDir ? {} : null;
33
+ if (node[key] === undefined) {
34
+ node[key] = isDir ? {} : currentParts.join('/');
33
35
  }
34
-
35
36
  if (isDir) {
36
37
  node = node[key];
37
38
  }
38
- });
39
- });
40
-
41
- // Recursively sort: directories first, then files, case-insensitive
42
- function sortNode(node: any): void {
43
- if (node === null || typeof node !== 'object') return;
44
-
45
- const entries = Object.entries(node);
46
- entries.sort(([a], [b]) => {
47
- const aIsDir = a.endsWith('/');
48
- const bIsDir = b.endsWith('/');
49
- if (aIsDir !== bIsDir) return aIsDir ? -1 : 1; // dirs before files
50
- return a.toLowerCase().localeCompare(b.toLowerCase());
51
- });
52
-
53
- // Rebuild node in sorted order (preserves insertion order for rendering)
54
- const sorted: any = {};
55
- entries.forEach(([key, value]) => {
56
- sorted[key] = value;
57
- sortNode(value);
58
- });
59
-
60
- Object.keys(node).forEach((k) => delete node[k]);
61
- Object.assign(node, sorted);
39
+ }
62
40
  }
41
+ return tree;
42
+ }
63
43
 
64
- sortNode(tree);
44
+ function renderMarkdownTree(node: any, depth: number): string {
45
+ let result = '';
46
+ const indent = ' '.repeat(depth);
47
+ const entries: [string, any][] = Object.entries(node);
65
48
 
66
- return tree;
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;
67
69
  }
68
70
 
69
71
  async function removeEmptyDirs(dir: string, root?: string): Promise<void> {
@@ -127,42 +129,40 @@ export async function flattenDirectory(
127
129
  if (err.code !== 'ENOENT') throw err;
128
130
  }
129
131
 
130
- const relPaths: string[] = files.map((srcPath) =>
131
- relative(absSource, srcPath).replace(/\\/g, '/')
132
- );
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()));
133
138
 
134
- // Sort paths for consistent insertion
135
- relPaths.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
139
+ const relPaths = fileEntries.map(e => e.relPath);
136
140
 
141
+ // Build tree structure
137
142
  const treeObj = buildTreeObject(relPaths);
138
143
 
139
- // Wrap with root for nice "." header (empty dirs show just ".")
140
- const rootTree = { '.': treeObj };
144
+ // Render as clickable nested Markdown list
145
+ let treeMarkdown = "# Project File Tree\n\n- .\n";
146
+ treeMarkdown += renderMarkdownTree(treeObj, 1);
147
+ treeMarkdown += "\n";
141
148
 
142
149
  const writeStream = createWriteStream(absTarget);
143
150
  writeStream.setMaxListeners(0);
144
151
 
145
- writeStream.write("# Project File Tree\n\n");
146
- writeStream.write("```\n");
147
- writeStream.write(treeify.asTree(rootTree));
148
- writeStream.write("```\n\n");
152
+ writeStream.write(treeMarkdown);
149
153
 
150
- for (const srcPath of files) {
151
- const relPath = relative(absSource, srcPath).replace(/\\/g, '/');
154
+ // Write file contents (now in sorted order)
155
+ for (const { srcPath, relPath } of fileEntries) {
152
156
  let ext = extname(srcPath).slice(1) || 'text';
157
+ const lang = ext;
153
158
  const isMd = ['md', 'markdown'].includes(ext.toLowerCase());
154
159
  const ticks = isMd ? '````' : '```';
155
160
 
156
- // Write header synchronously
157
- writeStream.write(`# ${relPath}\n\n${ticks}${ext}\n`);
161
+ writeStream.write(`## ${relPath}\n\n${ticks}${lang}\n`);
158
162
 
159
- // Stream file content (preserve original UTF-8 text behavior)
160
163
  const readStream = createReadStream(srcPath, { encoding: 'utf8' });
161
-
162
- // Pipe with { end: false } so writeStream stays open for next files
163
164
  await pipeline(readStream, writeStream, { end: false });
164
165
 
165
- // Write closing fence
166
166
  writeStream.write(`\n${ticks}\n\n`);
167
167
  }
168
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flatten-tool",
3
- "version": "1.4.0",
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",