mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-10-21 21:31:21 +00:00
feat(new tool): folder tree generator
This commit is contained in:
parent
9a1d8b10c3
commit
05ccbf36fa
5 changed files with 183 additions and 1 deletions
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { buildTree, renderTree } from './folder-tree-generator.models';
|
||||
|
||||
describe('folder-tree-generator: buildTree', () => {
|
||||
it('builds a hierarchy from indentation', () => {
|
||||
const tree = buildTree('src\n main.ts\n tools\n index.ts\npackage.json');
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'src',
|
||||
children: [
|
||||
{ name: 'main.ts', children: [] },
|
||||
{ name: 'tools', children: [{ name: 'index.ts', children: [] }] },
|
||||
],
|
||||
},
|
||||
{ name: 'package.json', children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats tabs as four spaces', () => {
|
||||
const tree = buildTree('a\n\tb');
|
||||
expect(tree[0]?.children[0]?.name).toBe('b');
|
||||
});
|
||||
|
||||
it('skips empty lines', () => {
|
||||
const tree = buildTree('a\n\n b\n');
|
||||
expect(tree).toEqual([{ name: 'a', children: [{ name: 'b', children: [] }] }]);
|
||||
});
|
||||
|
||||
it('returns to root level after deep nesting', () => {
|
||||
const tree = buildTree('a\n b\n c\nd');
|
||||
expect(tree).toHaveLength(2);
|
||||
expect(tree[1]?.name).toBe('d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('folder-tree-generator: renderTree', () => {
|
||||
it('renders box-drawing output like the tree command', () => {
|
||||
const tree = buildTree('src\n main.ts\n tools\n index.ts\npackage.json');
|
||||
expect(renderTree(tree)).toBe([
|
||||
'src',
|
||||
'├── main.ts',
|
||||
'└── tools',
|
||||
' └── index.ts',
|
||||
'package.json',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('uses the vertical continuation line for non-last parents', () => {
|
||||
const tree = buildTree('a\n b\n c');
|
||||
expect(renderTree(tree)).toBe(['a', '├── b', '└── c'].join('\n'));
|
||||
});
|
||||
|
||||
it('renders nested siblings with continuation prefixes', () => {
|
||||
const tree = buildTree('a\n x\n y\n deep\nz');
|
||||
expect(renderTree(tree)).toBe([
|
||||
'a',
|
||||
'├── x',
|
||||
'└── y',
|
||||
' └── deep',
|
||||
'z',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
export interface TreeNode {
|
||||
name: string
|
||||
children: TreeNode[]
|
||||
}
|
||||
|
||||
/** Build a tree from indented lines. Depth = indentation (tabs or a fixed
|
||||
* number of spaces). Lines ending with '/' or equal to '.'/'..' style
|
||||
* markers keep their name as-is. */
|
||||
export function buildTree(input: string): TreeNode[] {
|
||||
const root: TreeNode = { name: '', children: [] };
|
||||
const stack: { node: TreeNode; depth: number }[] = [{ node: root, depth: -1 }];
|
||||
|
||||
for (const rawLine of input.split('\n')) {
|
||||
const line = rawLine.replace(/\r$/, '').replace(/\t/g, ' ');
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indent = line.length - line.trimStart().length;
|
||||
const name = line.trim();
|
||||
|
||||
while (stack.length > 1 && stack[stack.length - 1]!.depth >= indent) {
|
||||
stack.pop();
|
||||
}
|
||||
const parent = stack[stack.length - 1]!.node;
|
||||
const node: TreeNode = { name, children: [] };
|
||||
parent.children.push(node);
|
||||
stack.push({ node, depth: indent });
|
||||
}
|
||||
|
||||
return root.children;
|
||||
}
|
||||
|
||||
/** Render as the classic `tree` box-drawing output. */
|
||||
export function renderTree(nodes: TreeNode[]): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
const walk = (children: TreeNode[], prefixes: string[], isRoot = false) => {
|
||||
children.forEach((child, index) => {
|
||||
const isLast = index === children.length - 1;
|
||||
// root-level entries render without any tree prefix, matching the
|
||||
// reference tools and the `tree` command's own output style
|
||||
const branch = isRoot ? '' : (isLast ? '└── ' : '├── ');
|
||||
lines.push([...prefixes, branch].join('') + child.name);
|
||||
if (child.children.length > 0) {
|
||||
const childPrefixes = isRoot ? [] : [...prefixes, isLast ? ' ' : '│ '];
|
||||
walk(child.children, childPrefixes);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(nodes, [], true);
|
||||
return lines.join('\n');
|
||||
}
|
||||
52
src/tools/folder-tree-generator/folder-tree-generator.vue
Normal file
52
src/tools/folder-tree-generator/folder-tree-generator.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { buildTree, renderTree } from './folder-tree-generator.models';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const input = ref(`src
|
||||
main.ts
|
||||
tools
|
||||
csv-to-json
|
||||
index.ts
|
||||
package.json
|
||||
README.md`);
|
||||
|
||||
const tree = computed(() => withDefaultOnError(() => renderTree(buildTree(input.value)), ''));
|
||||
const { copy } = useCopy({ source: tree, text: 'Folder tree copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-grid cols="2" x-gap="12">
|
||||
<n-gi>
|
||||
<c-input-text
|
||||
v-model:value="input"
|
||||
label="Indented structure"
|
||||
placeholder="Paste an indented file/folder listing (tabs or spaces)..."
|
||||
|
||||
rows="18"
|
||||
raw-text autosize multiline
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<c-input-text
|
||||
:value="tree"
|
||||
label="Folder tree"
|
||||
placeholder="The tree output will be here..."
|
||||
multiline
|
||||
raw-text
|
||||
rows="18"
|
||||
autosize
|
||||
readonly
|
||||
monospace
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<div mt-3 flex justify-center>
|
||||
<c-button :disabled="!tree" @click="copy()">
|
||||
Copy tree to clipboard
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
12
src/tools/folder-tree-generator/index.ts
Normal file
12
src/tools/folder-tree-generator/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Sitemap } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Folder tree generator',
|
||||
path: '/folder-tree-generator',
|
||||
description: 'Turn an indented file/folder listing into a box-drawing tree, like the tree command',
|
||||
keywords: ['folder', 'tree', 'directory', 'structure', 'ascii', 'diagram', 'indent'],
|
||||
component: () => import('./folder-tree-generator.vue'),
|
||||
icon: Sitemap,
|
||||
createdAt: new Date('2026-10-01'),
|
||||
});
|
||||
|
|
@ -21,6 +21,7 @@ import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
|
|||
import { tool as stringObfuscator } from './string-obfuscator';
|
||||
import { tool as textDiff } from './text-diff';
|
||||
import { tool as emojiPicker } from './emoji-picker';
|
||||
import { tool as folderTreeGenerator } from './folder-tree-generator';
|
||||
import { tool as passwordStrengthAnalyser } from './password-strength-analyser';
|
||||
import { tool as yamlToToml } from './yaml-to-toml';
|
||||
import { tool as jsonToToml } from './json-to-toml';
|
||||
|
|
@ -179,7 +180,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
components: [
|
||||
loremIpsumGenerator,
|
||||
textStatistics,
|
||||
emojiPicker,
|
||||
emojiPicker, folderTreeGenerator,
|
||||
stringObfuscator,
|
||||
textDiff,
|
||||
numeronymGenerator,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue