mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-08-09 15:35:19 +02:00
Merge 6e113a850e
into e876d03608
This commit is contained in:
commit
abc8a5e456
13 changed files with 723 additions and 6 deletions
1
components.d.ts
vendored
1
components.d.ts
vendored
|
@ -78,6 +78,7 @@ declare module '@vue/runtime-core' {
|
||||||
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
|
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
|
||||||
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
|
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
|
||||||
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
||||||
|
FolderStructureDiagram: typeof import('./src/tools/folder-structure-diagram/folder-structure-diagram.vue')['default']
|
||||||
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
||||||
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
|
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
|
||||||
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
|
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
|
||||||
|
|
|
@ -42,6 +42,9 @@
|
||||||
"@tiptap/starter-kit": "2.1.6",
|
"@tiptap/starter-kit": "2.1.6",
|
||||||
"@tiptap/vue-3": "2.0.3",
|
"@tiptap/vue-3": "2.0.3",
|
||||||
"@types/figlet": "^1.5.8",
|
"@types/figlet": "^1.5.8",
|
||||||
|
"@types/lodash.defaultsdeep": "^4.6.9",
|
||||||
|
"@types/lodash.flattendeep": "^4.4.9",
|
||||||
|
"@types/lodash.last": "^3.0.9",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
|
@ -68,6 +71,9 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"libphonenumber-js": "^1.10.28",
|
"libphonenumber-js": "^1.10.28",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
|
"lodash.flattendeep": "^4.4.0",
|
||||||
|
"lodash.last": "^3.0.0",
|
||||||
"marked": "^10.0.0",
|
"marked": "^10.0.0",
|
||||||
"mathjs": "^11.9.1",
|
"mathjs": "^11.9.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
|
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
|
@ -26,6 +26,15 @@ dependencies:
|
||||||
'@types/figlet':
|
'@types/figlet':
|
||||||
specifier: ^1.5.8
|
specifier: ^1.5.8
|
||||||
version: 1.5.8
|
version: 1.5.8
|
||||||
|
'@types/lodash.defaultsdeep':
|
||||||
|
specifier: ^4.6.9
|
||||||
|
version: 4.6.9
|
||||||
|
'@types/lodash.flattendeep':
|
||||||
|
specifier: ^4.4.9
|
||||||
|
version: 4.4.9
|
||||||
|
'@types/lodash.last':
|
||||||
|
specifier: ^3.0.9
|
||||||
|
version: 3.0.9
|
||||||
'@vicons/material':
|
'@vicons/material':
|
||||||
specifier: ^0.12.0
|
specifier: ^0.12.0
|
||||||
version: 0.12.0
|
version: 0.12.0
|
||||||
|
@ -104,6 +113,15 @@ dependencies:
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
lodash.defaultsdeep:
|
||||||
|
specifier: ^4.6.1
|
||||||
|
version: 4.6.1
|
||||||
|
lodash.flattendeep:
|
||||||
|
specifier: ^4.4.0
|
||||||
|
version: 4.4.0
|
||||||
|
lodash.last:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
marked:
|
marked:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
|
@ -2951,6 +2969,24 @@ packages:
|
||||||
'@types/lodash': 4.14.192
|
'@types/lodash': 4.14.192
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/lodash.defaultsdeep@4.6.9:
|
||||||
|
resolution: {integrity: sha512-pLtCFK0YkHfGtGLYLNMTbFB5/G5+RsmQCIbbHH8GOAXjv+gDkVilY98kILfe8JH2Kev0OCReYxp1AjxEjP8ixA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.14.200
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/lodash.flattendeep@4.4.9:
|
||||||
|
resolution: {integrity: sha512-Oacs/ZMuMvVWkhMqvj+Spad457Beln5pnkauif+6s65fE2cSL7J7NoMfwkxjuQsOsr4DUCDH/iDbmuZo81Nypw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.14.200
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/lodash.last@3.0.9:
|
||||||
|
resolution: {integrity: sha512-HMIEyk2LJWDumrHsuSh7ZokUs+xyXa0nZO/s3paaKjOvwrjBNoQHsM0UqcpmzEt7CJ29a16XNe3vr8buvHeMCg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.14.200
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/lodash@4.14.192:
|
/@types/lodash@4.14.192:
|
||||||
resolution: {integrity: sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==}
|
resolution: {integrity: sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==}
|
||||||
|
|
||||||
|
@ -3351,7 +3387,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@unhead/dom': 0.5.1
|
'@unhead/dom': 0.5.1
|
||||||
'@unhead/schema': 0.5.1
|
'@unhead/schema': 0.5.1
|
||||||
'@vueuse/shared': 10.7.2(vue@3.3.4)
|
'@vueuse/shared': 10.9.0(vue@3.3.4)
|
||||||
unhead: 0.5.1
|
unhead: 0.5.1
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -3993,10 +4029,10 @@ packages:
|
||||||
- vue
|
- vue
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vueuse/shared@10.7.2(vue@3.3.4):
|
/@vueuse/shared@10.9.0(vue@3.3.4):
|
||||||
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==}
|
resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.6(vue@3.3.4)
|
vue-demi: 0.14.7(vue@3.3.4)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
@ -6694,6 +6730,18 @@ packages:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.defaultsdeep@4.6.1:
|
||||||
|
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.flattendeep@4.4.0:
|
||||||
|
resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.last@3.0.0:
|
||||||
|
resolution: {integrity: sha512-14mq7rSkCxG4XMy9lF2FbIOqqgF0aH0NfPuQ3LPR3vIh0kHnUvIYP70dqa1Hf47zyXfQ8FzAg0MYOQeSuE1R7A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -9151,8 +9199,8 @@ packages:
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vue-demi@0.14.6(vue@3.3.4):
|
/vue-demi@0.14.7(vue@3.3.4):
|
||||||
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
@ -9442,6 +9490,7 @@ packages:
|
||||||
|
|
||||||
/workbox-google-analytics@7.0.0:
|
/workbox-google-analytics@7.0.0:
|
||||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||||
|
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||||
dependencies:
|
dependencies:
|
||||||
workbox-background-sync: 7.0.0
|
workbox-background-sync: 7.0.0
|
||||||
workbox-core: 7.0.0
|
workbox-core: 7.0.0
|
||||||
|
|
|
@ -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>
|
12
src/tools/folder-structure-diagram/index.ts
Normal file
12
src/tools/folder-structure-diagram/index.ts
Normal file
|
@ -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'),
|
||||||
|
});
|
20
src/tools/folder-structure-diagram/lib/FileStructure.ts
Normal file
20
src/tools/folder-structure-diagram/lib/FileStructure.ts
Normal file
|
@ -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
|
||||||
|
}
|
165
src/tools/folder-structure-diagram/lib/generate-tree.test.ts
Normal file
165
src/tools/folder-structure-diagram/lib/generate-tree.test.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
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
src/tools/folder-structure-diagram/lib/generate-tree.ts
Normal file
138
src/tools/folder-structure-diagram/lib/generate-tree.ts
Normal file
|
@ -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);
|
||||||
|
}
|
33
src/tools/folder-structure-diagram/lib/line-strings.ts
Normal file
33
src/tools/folder-structure-diagram/lib/line-strings.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Represents an object that contains the
|
||||||
|
* actual strings used to render the tree
|
||||||
|
*/
|
||||||
|
export interface LineStringSet {
|
||||||
|
/** The string to render immediately before non-last children */
|
||||||
|
CHILD: string
|
||||||
|
|
||||||
|
/** The string to render immediately before the last child */
|
||||||
|
LAST_CHILD: string
|
||||||
|
|
||||||
|
/** The string to render for parent directories */
|
||||||
|
DIRECTORY: string
|
||||||
|
|
||||||
|
/** The string to render for empty space */
|
||||||
|
EMPTY: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contains all strings for tree rendering */
|
||||||
|
export const LINE_STRINGS: { [charset: string]: LineStringSet } = {
|
||||||
|
'ascii': {
|
||||||
|
CHILD: '|-- ',
|
||||||
|
LAST_CHILD: '`-- ',
|
||||||
|
DIRECTORY: '| ',
|
||||||
|
EMPTY: ' ',
|
||||||
|
},
|
||||||
|
'utf-8': {
|
||||||
|
CHILD: '├── ',
|
||||||
|
LAST_CHILD: '└── ',
|
||||||
|
DIRECTORY: '│ ',
|
||||||
|
EMPTY: ' ',
|
||||||
|
},
|
||||||
|
};
|
19
src/tools/folder-structure-diagram/lib/mock-input.ts
Normal file
19
src/tools/folder-structure-diagram/lib/mock-input.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// not using a template string in order
|
||||||
|
// to have more control over whitespace
|
||||||
|
export const mockInput = [
|
||||||
|
'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');
|
147
src/tools/folder-structure-diagram/lib/parse-input.test.ts
Normal file
147
src/tools/folder-structure-diagram/lib/parse-input.test.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { FileStructure } from './FileStructure';
|
||||||
|
import { mockInput } from './mock-input';
|
||||||
|
import { parseInput, splitInput } from './parse-input';
|
||||||
|
|
||||||
|
describe('parse-input', () => {
|
||||||
|
it('parses plain text input into a FileStructure object', () => {
|
||||||
|
const actual = parseInput(mockInput);
|
||||||
|
|
||||||
|
const root: FileStructure = {
|
||||||
|
name: '.',
|
||||||
|
children: [],
|
||||||
|
indentCount: -1,
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const myApp: FileStructure = {
|
||||||
|
name: 'my-app',
|
||||||
|
children: [],
|
||||||
|
indentCount: 0,
|
||||||
|
parent: root,
|
||||||
|
};
|
||||||
|
root.children.push(myApp);
|
||||||
|
|
||||||
|
const src: FileStructure = {
|
||||||
|
name: 'src',
|
||||||
|
children: [],
|
||||||
|
indentCount: 2,
|
||||||
|
parent: myApp,
|
||||||
|
};
|
||||||
|
myApp.children.push(src);
|
||||||
|
|
||||||
|
const srcIndexHtml: FileStructure = {
|
||||||
|
name: 'index.html',
|
||||||
|
children: [],
|
||||||
|
indentCount: 4,
|
||||||
|
parent: src,
|
||||||
|
};
|
||||||
|
src.children.push(srcIndexHtml);
|
||||||
|
|
||||||
|
const mainTs: FileStructure = {
|
||||||
|
name: 'main.ts',
|
||||||
|
children: [],
|
||||||
|
indentCount: 4,
|
||||||
|
parent: src,
|
||||||
|
};
|
||||||
|
src.children.push(mainTs);
|
||||||
|
|
||||||
|
const mainScss: FileStructure = {
|
||||||
|
name: 'main.scss',
|
||||||
|
children: [],
|
||||||
|
indentCount: 3,
|
||||||
|
parent: src,
|
||||||
|
};
|
||||||
|
src.children.push(mainScss);
|
||||||
|
|
||||||
|
const build: FileStructure = {
|
||||||
|
name: 'build',
|
||||||
|
children: [],
|
||||||
|
indentCount: 2,
|
||||||
|
parent: myApp,
|
||||||
|
};
|
||||||
|
myApp.children.push(build);
|
||||||
|
|
||||||
|
const buildIndexHtml: FileStructure = {
|
||||||
|
name: 'index.html',
|
||||||
|
children: [],
|
||||||
|
indentCount: 4,
|
||||||
|
parent: build,
|
||||||
|
};
|
||||||
|
build.children.push(buildIndexHtml);
|
||||||
|
|
||||||
|
const mainJs: FileStructure = {
|
||||||
|
name: 'main.js',
|
||||||
|
children: [],
|
||||||
|
indentCount: 4,
|
||||||
|
parent: build,
|
||||||
|
};
|
||||||
|
build.children.push(mainJs);
|
||||||
|
|
||||||
|
const mainCss: FileStructure = {
|
||||||
|
name: 'main.css',
|
||||||
|
children: [],
|
||||||
|
indentCount: 4,
|
||||||
|
parent: build,
|
||||||
|
};
|
||||||
|
build.children.push(mainCss);
|
||||||
|
|
||||||
|
const prettierRcJson: FileStructure = {
|
||||||
|
name: '.prettierrc.json',
|
||||||
|
children: [],
|
||||||
|
indentCount: 2,
|
||||||
|
parent: myApp,
|
||||||
|
};
|
||||||
|
myApp.children.push(prettierRcJson);
|
||||||
|
|
||||||
|
const gitlabCiYml: FileStructure = {
|
||||||
|
name: '.gitlab-ci.yml',
|
||||||
|
children: [],
|
||||||
|
indentCount: 2,
|
||||||
|
parent: myApp,
|
||||||
|
};
|
||||||
|
myApp.children.push(gitlabCiYml);
|
||||||
|
|
||||||
|
const readmeMd: FileStructure = {
|
||||||
|
name: 'README.md',
|
||||||
|
children: [],
|
||||||
|
indentCount: 2,
|
||||||
|
parent: myApp,
|
||||||
|
};
|
||||||
|
myApp.children.push(readmeMd);
|
||||||
|
|
||||||
|
const emptyDir: FileStructure = {
|
||||||
|
name: 'empty dir',
|
||||||
|
children: [],
|
||||||
|
indentCount: 0,
|
||||||
|
parent: root,
|
||||||
|
};
|
||||||
|
root.children.push(emptyDir);
|
||||||
|
|
||||||
|
expect(actual).toEqual(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('splitInput', () => {
|
||||||
|
it('splits plain text into an array of File Structure objects', () => {
|
||||||
|
const actual = splitInput(mockInput);
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ name: 'my-app', children: [], indentCount: 0, parent: null },
|
||||||
|
{ name: 'src', children: [], indentCount: 2, parent: null },
|
||||||
|
{ name: 'index.html', children: [], indentCount: 4, parent: null },
|
||||||
|
{ name: 'main.ts', children: [], indentCount: 4, parent: null },
|
||||||
|
{ name: 'main.scss', children: [], indentCount: 3, parent: null },
|
||||||
|
{ name: 'build', children: [], indentCount: 2, parent: null },
|
||||||
|
{ name: 'index.html', children: [], indentCount: 4, parent: null },
|
||||||
|
{ name: 'main.js', children: [], indentCount: 4, parent: null },
|
||||||
|
{ name: 'main.css', children: [], indentCount: 4, parent: null },
|
||||||
|
{ name: '.prettierrc.json', children: [], indentCount: 2, parent: null },
|
||||||
|
{ name: '.gitlab-ci.yml', children: [], indentCount: 2, parent: null },
|
||||||
|
{ name: 'README.md', children: [], indentCount: 2, parent: null },
|
||||||
|
{ name: 'empty dir', children: [], indentCount: 0, parent: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
81
src/tools/folder-structure-diagram/lib/parse-input.ts
Normal file
81
src/tools/folder-structure-diagram/lib/parse-input.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import last from 'lodash.last';
|
||||||
|
import type { FileStructure } from './FileStructure';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the whitespace in front of a file name.
|
||||||
|
* Also will match a markdown bullet point if included.
|
||||||
|
* For example, testing against " - hello" will return
|
||||||
|
* a positive match with the first capturing group
|
||||||
|
* with " - " and a second with " "
|
||||||
|
*/
|
||||||
|
const leadingWhitespaceAndBulletRegex = /^((\s*)(?:-\s)?)/;
|
||||||
|
|
||||||
|
/** Matches lines that only contain whitespace */
|
||||||
|
const onlyWhitespaceRegex = /^\s*$/;
|
||||||
|
|
||||||
|
/** Used to split a block of text into individual lines */
|
||||||
|
const newlineSplitterRegex = /[^\r\n]+/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a block of user-created text into
|
||||||
|
* a nested FileStructure structure
|
||||||
|
* @param input The plain-text input from the user
|
||||||
|
*/
|
||||||
|
export function parseInput(input: string): FileStructure {
|
||||||
|
const structures = splitInput(input);
|
||||||
|
|
||||||
|
const root: FileStructure = {
|
||||||
|
name: '.',
|
||||||
|
children: [],
|
||||||
|
indentCount: -1,
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = [root];
|
||||||
|
for (const s of structures) {
|
||||||
|
while (last(path)!.indentCount >= s.indentCount) {
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = last(path) as FileStructure;
|
||||||
|
parent.children.push(s);
|
||||||
|
s.parent = parent;
|
||||||
|
|
||||||
|
path.push(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a block of user-created text into
|
||||||
|
* individual, un-nested FileStructure objects.
|
||||||
|
* Used internally as part of `parseInput`.
|
||||||
|
* @param input The plain-text input from the user
|
||||||
|
*/
|
||||||
|
export function splitInput(input: string): FileStructure[] {
|
||||||
|
let lines = input.match(newlineSplitterRegex)?.map(m => m) || [];
|
||||||
|
|
||||||
|
// filter out empty lines
|
||||||
|
lines = lines.filter(l => !onlyWhitespaceRegex.test(l));
|
||||||
|
|
||||||
|
return lines.map((l) => {
|
||||||
|
const matchResult = leadingWhitespaceAndBulletRegex.exec(l);
|
||||||
|
|
||||||
|
if (!matchResult) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to execute leadingWhitespaceAndBulletRegex against string: "${l}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = l.replace(matchResult[1], '');
|
||||||
|
const indentCount = matchResult[2].length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
children: [],
|
||||||
|
indentCount,
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
||||||
|
|
||||||
import { tool as textToUnicode } from './text-to-unicode';
|
import { tool as textToUnicode } from './text-to-unicode';
|
||||||
import { tool as safelinkDecoder } from './safelink-decoder';
|
import { tool as safelinkDecoder } from './safelink-decoder';
|
||||||
|
import { tool as folderStructureDiagram } from './folder-structure-diagram';
|
||||||
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||||
|
@ -128,6 +129,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
httpStatusCodes,
|
httpStatusCodes,
|
||||||
jsonDiff,
|
jsonDiff,
|
||||||
safelinkDecoder,
|
safelinkDecoder,
|
||||||
|
folderStructureDiagram,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue