forked from CorentinTh/it-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix CorentinTh#938
- Loading branch information
Showing
10 changed files
with
659 additions
and
0 deletions.
There are no files selected for viewing
44 changes: 44 additions & 0 deletions
44
src/tools/folder-structure-diagram/folder-structure-diagram.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<script setup lang="ts"> | ||
import { generateTree } from './lib/generate-tree'; | ||
import { parseInput } from './lib/parse-input'; | ||
import { withDefaultOnError } from '@/utils/defaults'; | ||
const inputStructure = ref([ | ||
'my-app', | ||
' src', | ||
' index.html', | ||
' main.ts', | ||
' main.scss', | ||
' - build', | ||
' - index.html', | ||
' main.js', | ||
' main.css', | ||
'', | ||
' ', | ||
' .prettierrc.json', | ||
' .gitlab-ci.yml', | ||
' README.md', | ||
'empty dir', | ||
].join('\n')); | ||
const outputTree = computed(() => withDefaultOnError(() => generateTree(parseInput(inputStructure.value)), '')); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<c-input-text | ||
v-model:value="inputStructure" | ||
label="Your indented structure" | ||
placeholder="Paste your indented structure here..." | ||
rows="20" | ||
multiline | ||
raw-text | ||
monospace | ||
/> | ||
|
||
<n-divider /> | ||
|
||
<n-form-item label="Your tree-like structure:"> | ||
<TextareaCopyable :value="outputTree" /> | ||
</n-form-item> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Folder } from '@vicons/tabler'; | ||
import { defineTool } from '../tool'; | ||
|
||
export const tool = defineTool({ | ||
name: 'Folder Structure Diagram', | ||
path: '/folder-structure-diagram', | ||
description: 'tree-like utility for generating ASCII folder structure diagrams', | ||
keywords: ['folder', 'structure', 'diagram', 'tree', 'ascii'], | ||
component: () => import('./folder-structure-diagram.vue'), | ||
icon: Folder, | ||
createdAt: new Date('2024-04-20'), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* Represents a single item in a file system | ||
* (i.e. a file or a folder) | ||
*/ | ||
export interface FileStructure { | ||
/** The name of the file or folder */ | ||
name: string | ||
|
||
/** If a folder, the contents of the folder */ | ||
children: FileStructure[] | ||
|
||
/** | ||
* The number of spaces in front of the name | ||
* in the original source string | ||
*/ | ||
indentCount: number | ||
|
||
/** The parent directory of this file or folder */ | ||
parent: FileStructure | null | ||
} |
164 changes: 164 additions & 0 deletions
164
src/tools/folder-structure-diagram/lib/generate-tree.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { generateTree } from './generate-tree'; | ||
import { mockInput } from './mock-input'; | ||
import { parseInput } from './parse-input'; | ||
|
||
describe('generateTree', () => { | ||
it('returns an UTF-8 representation of the provided FileStructure object', () => { | ||
const actual = generateTree(parseInput(mockInput)); | ||
|
||
const expected = ` | ||
. | ||
├── my-app | ||
│ ├── src | ||
│ │ ├── index.html | ||
│ │ ├── main.ts | ||
│ │ └── main.scss | ||
│ ├── build | ||
│ │ ├── index.html | ||
│ │ ├── main.js | ||
│ │ └── main.css | ||
│ ├── .prettierrc.json | ||
│ ├── .gitlab-ci.yml | ||
│ └── README.md | ||
└── empty dir | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
|
||
it('returns an ASCII representation of the provided FileStructure object', () => { | ||
const actual = generateTree(parseInput(mockInput), { charset: 'ascii' }); | ||
|
||
const expected = ` | ||
. | ||
|-- my-app | ||
| |-- src | ||
| | |-- index.html | ||
| | |-- main.ts | ||
| | \`-- main.scss | ||
| |-- build | ||
| | |-- index.html | ||
| | |-- main.js | ||
| | \`-- main.css | ||
| |-- .prettierrc.json | ||
| |-- .gitlab-ci.yml | ||
| \`-- README.md | ||
\`-- empty dir | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
|
||
it('does not render lines for parent directories that have already printed all of their children', () => { | ||
const input = ` | ||
grandparent | ||
parent | ||
child | ||
parent | ||
child | ||
grandchild | ||
`; | ||
|
||
const actual = generateTree(parseInput(input)); | ||
|
||
const expected = ` | ||
. | ||
└── grandparent | ||
├── parent | ||
│ └── child | ||
└── parent | ||
└── child | ||
└── grandchild | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
|
||
it('appends a trailing slash to directories if trailingDirSlash === true', () => { | ||
const input = ` | ||
grandparent | ||
parent/ | ||
child | ||
parent// | ||
child | ||
grandchild | ||
`; | ||
|
||
const actual = generateTree(parseInput(input), { trailingDirSlash: true }); | ||
|
||
const expected = ` | ||
. | ||
└── grandparent/ | ||
├── parent/ | ||
│ └── child | ||
└── parent// | ||
└── child/ | ||
└── grandchild | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
|
||
it('prints each items\' full path if fullPath === true', () => { | ||
const input = ` | ||
grandparent | ||
parent/ | ||
child | ||
parent// | ||
child | ||
grandchild | ||
`; | ||
|
||
const actual = generateTree(parseInput(input), { fullPath: true }); | ||
|
||
const expected = ` | ||
. | ||
└── ./grandparent | ||
├── ./grandparent/parent/ | ||
│ └── ./grandparent/parent/child | ||
└── ./grandparent/parent// | ||
└── ./grandparent/parent//child | ||
└── ./grandparent/parent//child/grandchild | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
|
||
it('does not render the root dot if rootDot === false', () => { | ||
const input = ` | ||
grandparent | ||
parent | ||
child | ||
parent | ||
child | ||
grandchild | ||
`; | ||
|
||
const actual = generateTree(parseInput(input), { rootDot: false }); | ||
|
||
const expected = ` | ||
grandparent | ||
├── parent | ||
│ └── child | ||
└── parent | ||
└── child | ||
└── grandchild | ||
`.trim(); | ||
|
||
expect(actual).toEqual(expected); | ||
}); | ||
}); |
138 changes: 138 additions & 0 deletions
138
src/tools/folder-structure-diagram/lib/generate-tree.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import type { RecursiveArray } from 'lodash'; | ||
import defaultsDeep from 'lodash.defaultsdeep'; | ||
import flattenDeep from 'lodash.flattendeep'; | ||
import last from 'lodash.last'; | ||
import type { FileStructure } from './FileStructure'; | ||
import { LINE_STRINGS } from './line-strings'; | ||
|
||
/** | ||
* Represents all rendering options available | ||
* when calling `generateTree` | ||
*/ | ||
interface GenerateTreeOptions { | ||
/** | ||
* Which set of characters to use when | ||
* rendering directory lines | ||
*/ | ||
charset?: 'ascii' | 'utf-8' | ||
|
||
/** | ||
* Whether or not to append trailing slashes | ||
* to directories. Items that already include a | ||
* trailing slash will not have another appended. | ||
*/ | ||
trailingDirSlash?: boolean | ||
|
||
/** | ||
* Whether or not to print the full | ||
* path of the item | ||
*/ | ||
fullPath?: boolean | ||
|
||
/** | ||
* Whether or not to render a dot as the root of the tree | ||
*/ | ||
rootDot?: boolean | ||
} | ||
|
||
/** The default options if no options are provided */ | ||
const defaultOptions: GenerateTreeOptions = { | ||
charset: 'utf-8', | ||
trailingDirSlash: false, | ||
fullPath: false, | ||
rootDot: true, | ||
}; | ||
|
||
/** | ||
* Generates an ASCII tree diagram, given a FileStructure | ||
* @param structure The FileStructure object to convert into ASCII | ||
* @param options The rendering options | ||
*/ | ||
export function generateTree(structure: FileStructure, | ||
options?: GenerateTreeOptions): string { | ||
return flattenDeep([ | ||
getAsciiLine(structure, defaultsDeep({}, options, defaultOptions)), | ||
structure.children.map(c => generateTree(c, options)) as RecursiveArray< | ||
string | ||
>, | ||
]) | ||
// Remove null entries. Should only occur for the very first node | ||
// when `options.rootDot === false` | ||
.filter(line => line != null) | ||
.join('\n'); | ||
} | ||
|
||
/** | ||
* Returns a line of ASCII that represents | ||
* a single FileStructure object | ||
* @param structure The file to render | ||
* @param options The rendering options | ||
*/ | ||
function getAsciiLine(structure: FileStructure, | ||
options: GenerateTreeOptions): string | null { | ||
const lines = LINE_STRINGS[options.charset as string]; | ||
|
||
// Special case for the root element | ||
if (!structure.parent) { | ||
return options.rootDot ? structure.name : null; | ||
} | ||
|
||
const chunks = [ | ||
isLastChild(structure) ? lines.LAST_CHILD : lines.CHILD, | ||
getName(structure, options), | ||
]; | ||
|
||
let current = structure.parent; | ||
while (current && current.parent) { | ||
chunks.unshift(isLastChild(current) ? lines.EMPTY : lines.DIRECTORY); | ||
current = current.parent; | ||
} | ||
|
||
// Join all the chunks together to create the final line. | ||
// If we're not rendering the root `.`, chop off the first 4 characters. | ||
return chunks.join('').substring(options.rootDot ? 0 : lines.CHILD.length); | ||
} | ||
|
||
/** | ||
* Returns the name of a file or folder according to the | ||
* rules specified by the rendering rules | ||
* @param structure The file or folder to get the name of | ||
* @param options The rendering options | ||
*/ | ||
function getName(structure: FileStructure, | ||
options: GenerateTreeOptions): string { | ||
const nameChunks = [structure.name]; | ||
|
||
// Optionally append a trailing slash | ||
if ( | ||
// if the trailing slash option is enabled | ||
options.trailingDirSlash | ||
// and if the item has at least one child | ||
&& structure.children.length > 0 | ||
// and if the item doesn't already have a trailing slash | ||
&& !/\/\s*$/.test(structure.name) | ||
) { | ||
nameChunks.push('/'); | ||
} | ||
|
||
// Optionally prefix the name with its full path | ||
if (options.fullPath && structure.parent && structure.parent) { | ||
nameChunks.unshift( | ||
getName( | ||
structure.parent, | ||
defaultsDeep({}, { trailingDirSlash: true }, options), | ||
), | ||
); | ||
} | ||
|
||
return nameChunks.join(''); | ||
} | ||
|
||
/** | ||
* A utility function do determine if a file or folder | ||
* is the last child of its parent | ||
* @param structure The file or folder to test | ||
*/ | ||
function isLastChild(structure: FileStructure): boolean { | ||
return Boolean(structure.parent && last(structure.parent.children) === structure); | ||
} |
Oops, something went wrong.