mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-08-09 07:25:18 +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']
|
||||
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.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']
|
||||
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['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/vue-3": "2.0.3",
|
||||
"@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/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
|
@ -68,6 +71,9 @@
|
|||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"lodash.flattendeep": "^4.4.0",
|
||||
"lodash.last": "^3.0.0",
|
||||
"marked": "^10.0.0",
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
|
|
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
|
@ -26,6 +26,15 @@ dependencies:
|
|||
'@types/figlet':
|
||||
specifier: ^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':
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
|
@ -104,6 +113,15 @@ dependencies:
|
|||
lodash:
|
||||
specifier: ^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:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
|
@ -2951,6 +2969,24 @@ packages:
|
|||
'@types/lodash': 4.14.192
|
||||
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:
|
||||
resolution: {integrity: sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==}
|
||||
|
||||
|
@ -3351,7 +3387,7 @@ packages:
|
|||
dependencies:
|
||||
'@unhead/dom': 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
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
|
@ -3993,10 +4029,10 @@ packages:
|
|||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared@10.7.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==}
|
||||
/@vueuse/shared@10.9.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
|
||||
dependencies:
|
||||
vue-demi: 0.14.6(vue@3.3.4)
|
||||
vue-demi: 0.14.7(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
@ -6694,6 +6730,18 @@ packages:
|
|||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
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:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
@ -9151,8 +9199,8 @@ packages:
|
|||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/vue-demi@0.14.6(vue@3.3.4):
|
||||
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
||||
/vue-demi@0.14.7(vue@3.3.4):
|
||||
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
|
@ -9442,6 +9490,7 @@ packages:
|
|||
|
||||
/workbox-google-analytics@7.0.0:
|
||||
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:
|
||||
workbox-background-sync: 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 safelinkDecoder } from './safelink-decoder';
|
||||
import { tool as folderStructureDiagram } from './folder-structure-diagram';
|
||||
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
|
@ -128,6 +129,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
httpStatusCodes,
|
||||
jsonDiff,
|
||||
safelinkDecoder,
|
||||
folderStructureDiagram,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue