1
0
Fork 0
mirror of https://github.com/CorentinTh/it-tools.git synced 2025-08-09 07:25:18 +02:00
This commit is contained in:
sharevb 2024-05-20 18:37:31 +00:00 committed by GitHub
commit abc8a5e456
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 723 additions and 6 deletions

1
components.d.ts vendored
View file

@ -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']

View file

@ -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
View file

@ -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

View file

@ -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>

View 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'),
});

View 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
}

View 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);
});
});

View 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);
}

View 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: ' ',
},
};

View 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');

View 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);
});
});

View 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,
};
});
}

View file

@ -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,
],
},
{