From 9eac9cb2a9d23619b0d3c044f1bc1385aab427e4 Mon Sep 17 00:00:00 2001 From: sharevb Date: Mon, 22 Apr 2024 08:45:51 +0200 Subject: [PATCH 01/41] fix(integer base converter): support bigint (#872) --- .../integer-base-converter.model.test.ts | 3 +++ .../integer-base-converter.model.ts | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tools/integer-base-converter/integer-base-converter.model.test.ts b/src/tools/integer-base-converter/integer-base-converter.model.test.ts index d0387b64..c7d7db79 100644 --- a/src/tools/integer-base-converter/integer-base-converter.model.test.ts +++ b/src/tools/integer-base-converter/integer-base-converter.model.test.ts @@ -11,6 +11,9 @@ describe('integer-base-converter', () => { expect(convertBase({ value: '10100101', fromBase: 2, toBase: 16 })).toEqual('a5'); expect(convertBase({ value: '192654', fromBase: 10, toBase: 8 })).toEqual('570216'); expect(convertBase({ value: 'zz', fromBase: 64, toBase: 10 })).toEqual('2275'); + expect(convertBase({ value: '42540766411283223938465490632011909384', fromBase: 10, toBase: 10 })).toEqual('42540766411283223938465490632011909384'); + expect(convertBase({ value: '42540766411283223938465490632011909384', fromBase: 10, toBase: 16 })).toEqual('20010db8000085a300000000ac1f8908'); + expect(convertBase({ value: '20010db8000085a300000000ac1f8908', fromBase: 16, toBase: 10 })).toEqual('42540766411283223938465490632011909384'); }); }); }); diff --git a/src/tools/integer-base-converter/integer-base-converter.model.ts b/src/tools/integer-base-converter/integer-base-converter.model.ts index b4470e57..da0fe77f 100644 --- a/src/tools/integer-base-converter/integer-base-converter.model.ts +++ b/src/tools/integer-base-converter/integer-base-converter.model.ts @@ -5,16 +5,16 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa let decValue = value .split('') .reverse() - .reduce((carry: number, digit: string, index: number) => { + .reduce((carry: bigint, digit: string, index: number) => { if (!fromRange.includes(digit)) { throw new Error(`Invalid digit "${digit}" for base ${fromBase}.`); } - return (carry += fromRange.indexOf(digit) * fromBase ** index); - }, 0); + return (carry += BigInt(fromRange.indexOf(digit)) * BigInt(fromBase) ** BigInt(index)); + }, 0n); let newValue = ''; while (decValue > 0) { - newValue = toRange[decValue % toBase] + newValue; - decValue = (decValue - (decValue % toBase)) / toBase; + newValue = toRange[Number(decValue % BigInt(toBase))] + newValue; + decValue = (decValue - (decValue % BigInt(toBase))) / BigInt(toBase); } return newValue || '0'; } From cb5b462e119d4231b5c07b4ef0ca24ec84ceee8b Mon Sep 17 00:00:00 2001 From: Pavel Gordon Date: Mon, 29 Apr 2024 11:58:08 +0200 Subject: [PATCH 02/41] fix(url-encoder, validation): typo in validation of url-encoder.vue #1024 --- src/tools/url-encoder/url-encoder.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/url-encoder/url-encoder.vue b/src/tools/url-encoder/url-encoder.vue index c43f8193..19025190 100644 --- a/src/tools/url-encoder/url-encoder.vue +++ b/src/tools/url-encoder/url-encoder.vue @@ -23,7 +23,7 @@ const decodeInput = ref('Hello%20world%20%3A)'); const decodeOutput = computed(() => withDefaultOnError(() => decodeURIComponent(decodeInput.value), '')); const decodeValidation = useValidation({ - source: encodeInput, + source: decodeInput, rules: [ { validator: value => isNotThrowing(() => decodeURIComponent(value)), From 221ddfa75c5731d7a5dc1f0b03663ba4fd9e7965 Mon Sep 17 00:00:00 2001 From: Christopher Conley Date: Sun, 5 May 2024 12:49:31 -0400 Subject: [PATCH 03/41] fix(language): English language cleanup (#1036) Fix possessive vs. contraction form of "it," clarification, and some general cleanup --- locales/en.yml | 90 +++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 50d48af9..d09d435a 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -7,7 +7,7 @@ home: toggleMenu: 'Toggle menu' home: Home uiLib: 'UI Lib' - support: 'Support IT Tools development' + support: 'Support IT-Tools development' buyMeACoffee: 'Buy me a coffee' follow: title: 'You like it-tools?' @@ -15,7 +15,7 @@ home: githubRepository: 'IT-Tools GitHub repository' p2: 'or follow us on' twitterAccount: 'IT-Tools Twitter account' - thankYou: 'Thank you !' + thankYou: 'Thank you!' nav: github: 'GitHub repository' githubRepository: 'IT-Tools GitHub repository' @@ -72,7 +72,7 @@ tools: password-strength-analyser: title: Password strength analyser - description: Discover the strength of your password with this client side only password strength analyser and crack time estimation tool. + description: Discover the strength of your password with this client-side-only password strength analyser and crack time estimation tool. chronometer: title: Chronometer @@ -98,7 +98,7 @@ tools: svg-placeholder-generator: title: SVG placeholder generator - description: Generate svg images to use as placeholder in your applications. + description: Generate svg images to use as a placeholder in your applications. json-to-csv: title: JSON to CSV @@ -126,11 +126,11 @@ tools: crontab-generator: title: Crontab generator - description: Validate and generate crontab and get the human readable description of the cron schedule. + description: Validate and generate crontab and get the human-readable description of the cron schedule. http-status-codes: title: HTTP status codes - description: The list of all HTTP status codes their name and their meaning. + description: The list of all HTTP status codes, their name, and their meaning. sql-prettify: title: SQL prettify and format @@ -142,7 +142,7 @@ tools: git-memo: title: Git cheatsheet - description: Git is a decentralized version management software. With this cheatsheet you will have a quick access to the most common git commands. + description: Git is a decentralized version management software. With this cheatsheet, you will have quick access to the most common git commands. slugify-string: title: Slugify string @@ -150,7 +150,7 @@ tools: encryption: title: Encrypt / decrypt text - description: Encrypt and decrypt text clear text using crypto algorithm like AES, TripleDES, Rabbit or RC4. + description: Encrypt clear text and decrypt ciphertext using crypto algorithms like AES, TripleDES, Rabbit or RC4. random-port-generator: title: Random port generator @@ -158,11 +158,11 @@ tools: yaml-prettify: title: YAML prettify and format - description: Prettify your YAML string to a human friendly readable format. + description: Prettify your YAML string into a friendly, human-readable format. eta-calculator: title: ETA calculator - description: An ETA (Estimated Time of Arrival) calculator to know the approximate end time of a task, for example the moment of ending of a download. + description: An ETA (Estimated Time of Arrival) calculator to determine the approximate end time of a task, for example, the end time and duration of a file download. roman-numeral-converter: title: Roman numeral converter @@ -174,11 +174,11 @@ tools: bip39-generator: title: BIP39 passphrase generator - description: Generate BIP39 passphrase from existing or random mnemonic, or get the mnemonic from the passphrase. + description: Generate a BIP39 passphrase from an existing or random mnemonic, or get the mnemonic from the passphrase. base64-file-converter: title: Base64 file converter - description: Convert string, files or images into a it\'s base64 representation. + description: Convert a string, file, or image into its base64 representation. list-converter: title: List converter @@ -186,7 +186,7 @@ tools: base64-string-converter: title: Base64 string encoder/decoder - description: Simply encode and decode string into a their base64 representation. + description: Simply encode and decode strings into their base64 representation. toml-to-yaml: title: TOML to YAML @@ -198,15 +198,15 @@ tools: json-to-yaml-converter: title: JSON to YAML converter - description: Simply convert JSON to YAML with this live online converter. + description: Simply convert JSON to YAML with this online live converter. url-parser: - title: Url parser - description: Parse an url string to get all the different parts (protocol, origin, params, port, username-password, ...) + title: URL parser + description: Parse a URL into its separate constituent parts (protocol, origin, params, port, username-password, ...) iban-validator-and-parser: title: IBAN validator and parser - description: Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format. + description: Validate and parse IBAN numbers. Check if an IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format. user-agent-parser: title: User-agent parser @@ -218,27 +218,27 @@ tools: case-converter: title: Case converter - description: Change the case of a string and chose between different formats + description: Transform the case of a string and choose between different formats html-entities: - title: Escape html entities - description: Escape or unescape html entities (replace <,>, &, " and \' to their html version) + title: Escape HTML entities + description: Escape or unescape HTML entities (replace characters like <,>, &, " and \' with their HTML version) json-prettify: title: JSON prettify and format - description: Prettify your JSON string to a human friendly readable format. + description: Prettify your JSON string into a friendly, human-readable format. docker-run-to-docker-compose-converter: title: Docker run to Docker compose converter - description: Turns docker run commands into docker-compose files! + description: Transforms "docker run" commands into docker-compose files! mac-address-lookup: title: MAC address lookup description: Find the vendor and manufacturer of a device by its MAC address. mime-types: - title: Mime types - description: Convert mime types to extensions and vice-versa. + title: MIME types + description: Convert MIME types to file extensions and vice-versa. toml-to-json: title: TOML to JSON @@ -250,19 +250,19 @@ tools: qrcode-generator: title: QR Code generator - description: Generate and download QR-code for an url or just a text and customize the background and foreground colors. + description: Generate and download a QR code for a URL (or just plain text), and customize the background and foreground colors. wifi-qrcode-generator: title: WiFi QR Code generator - description: Generate and download QR-codes for quick connections to WiFi networks. + description: Generate and download QR codes for quick connections to WiFi networks. xml-formatter: title: XML formatter - description: Prettify your XML string to a human friendly readable format. + description: Prettify your XML string into a friendly, human-readable format. temperature-converter: title: Temperature converter - description: Temperature degrees conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur and Rømer. + description: Degrees temperature conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur, and Rømer. chmod-calculator: title: Chmod calculator @@ -270,11 +270,11 @@ tools: rsa-key-pair-generator: title: RSA key pair generator - description: Generate new random RSA private and public key pem certificates. + description: Generate a new random RSA private and public pem certificate key pair. html-wysiwyg-editor: title: HTML WYSIWYG editor - description: Online HTML editor with feature-rich WYSIWYG editor, get the source code of the content immediately. + description: Online, feature-rich WYSIWYG HTML editor which generates the source code of the content immediately. yaml-to-toml: title: YAML to TOML @@ -302,15 +302,15 @@ tools: ipv4-subnet-calculator: title: IPv4 subnet calculator - description: Parse your IPv4 CIDR blocks and get all the info you need about your sub network. + description: Parse your IPv4 CIDR blocks and get all the info you need about your subnet. og-meta-generator: title: Open graph meta generator - description: Generate open-graph and socials html meta tags for your website. + description: Generate open-graph and socials HTML meta tags for your website. ipv6-ula-generator: title: IPv6 ULA generator - description: Generate your own local, non-routable IP addresses on your network according to RFC4193. + description: Generate your own local, non-routable IP addresses for your network according to RFC4193. hash-text: title: Hash text @@ -330,7 +330,7 @@ tools: json-minify: title: JSON minify - description: Minify and compress your JSON by removing unnecessary white spaces. + description: Minify and compress your JSON by removing unnecessary whitespace. ulid-generator: title: ULID generator @@ -342,31 +342,31 @@ tools: base-converter: title: Integer base converter - description: Convert number between different bases (decimal, hexadecimal, binary, octal, base64, ...) + description: Convert a number between different bases (decimal, hexadecimal, binary, octal, base64, ...) yaml-to-json-converter: title: YAML to JSON converter - description: Simply convert YAML to JSON with this live online converter. + description: Simply convert YAML to JSON with this online live converter. uuid-generator: title: UUIDs generator description: A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!). ipv4-address-converter: - title: Ipv4 address converter - description: Convert an ip address into decimal, binary, hexadecimal or event in ipv6 + title: IPv4 address converter + description: Convert an IP address into decimal, binary, hexadecimal, or even an IPv6 representation of it. text-statistics: title: Text statistics - description: Get information about a text, the amount of characters, the amount of words, it\'s size, ... + description: Get information about a text, the number of characters, the number of words, its size in bytes, ... text-to-nato-alphabet: title: Text to NATO alphabet - description: Transform text into NATO phonetic alphabet for oral transmission. + description: Transform text into the NATO phonetic alphabet for oral transmission. basic-auth-generator: title: Basic auth generator - description: Generate a base64 basic auth header from an username and a password. + description: Generate a base64 basic auth header from a username and password. text-to-unicode: title: Text to Unicode @@ -374,7 +374,7 @@ tools: ipv4-range-expander: title: IPv4 range expander - description: Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation. + description: Given a start and an end IPv4 address, this tool calculates a valid IPv4 subnet along with its CIDR notation. text-diff: title: Text diff @@ -385,9 +385,9 @@ tools: description: Generate and validate time-based OTP (one time password) for multi-factor authentication. url-encoder: - title: Encode/decode url formatted strings - description: Encode to url-encoded format (also known as "percent-encoded") or decode from it. + title: Encode/decode URL-formatted strings + description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it. text-to-binary: title: Text to ASCII binary - description: Convert text to its ASCII binary representation and vice versa. + description: Convert text to its ASCII binary representation and vice-versa. From 2c2fb216e36e5c4be0a6f6e2977da7a7363535f7 Mon Sep 17 00:00:00 2001 From: steffenrapp <88974099+steffenrapp@users.noreply.github.com> Date: Fri, 10 May 2024 10:57:42 +0200 Subject: [PATCH 04/41] feat(i18n): added German translation (#1038) * feat(i18n): German translation * more * more * more * rest --- locales/de.yml | 455 ++++++++++++++++++ .../i18n/components/locale-selector.vue | 1 + 2 files changed, 456 insertions(+) create mode 100644 locales/de.yml diff --git a/locales/de.yml b/locales/de.yml new file mode 100644 index 00000000..5a47c85d --- /dev/null +++ b/locales/de.yml @@ -0,0 +1,455 @@ +'404': + notFound: 404 Nicht gefunden + sorry: Entschuldigung, diese Seite scheint nicht zu existieren + maybe: >- + Vielleicht macht der Cache etwas Seltsames. Mit einem erzwungenen Neuladen + versuchen? + backHome: Zurück zur Startseite +home: + categories: + newestTools: Neueste Tools + favoriteTools: Deine Lieblingstools + allTools: Alle Tools + subtitle: Praktische Tools für Entwickler + toggleMenu: Menü umschalten + home: Startseite + uiLib: UI-Bibliothek + support: Unterstütze die Entwicklung von IT-Tools + buyMeACoffee: Kauf mir einen Kaffee + follow: + title: Magst du IT-Tools? + p1: Gib uns einen Stern auf + githubRepository: IT-Tools GitHub-Repository + p2: oder folge uns auf + twitterAccount: IT-Tools Twitter-Konto + thankYou: Vielen Dank! + nav: + github: GitHub-Repository + githubRepository: IT-Tools GitHub-Repository + twitter: Twitter-Konto + twitterAccount: IT-Tools Twitter-Konto + about: Über IT-Tools + aboutLabel: Über + darkMode: Dunkelmodus + lightMode: Hellmodus + mode: Wechseln zwischen dunklem/hellem Modus +about: + content: > + # Über IT-Tools + + Diese wunderbare Website, erstellt mit ❤ von [Corentin + Thomasset](https://github.com/CorentinTh), sammelt nützliche Tools für + Entwickler und Menschen, die in der IT arbeiten. Wenn du sie nützlich + findest, teile sie gerne mit Personen, von denen du denkst, dass sie sie + ebenfalls nützlich finden könnten, und vergiss nicht, sie in deiner + Lesezeichenleiste zu speichern! + + IT-Tools ist Open Source (unter der MIT-Lizenz) und kostenlos und wird es + immer sein, aber es kostet mich Geld, die Website zu hosten und den + Domainnamen zu erneuern. Wenn du meine Arbeit unterstützen möchtest und mich + ermutigen möchtest, mehr Tools hinzuzufügen, überlege bitte, mich durch + [Sponsoring](https://www.buymeacoffee.com/cthmsst) zu unterstützen. + + ## Technologien + + IT-Tools wurde mit Vue.js (Vue 3) und der Naive UI-Komponentenbibliothek + erstellt und wird von Vercel gehostet und kontinuierlich bereitgestellt. In + einigen Tools werden Drittanbieter-Open-Source-Bibliotheken verwendet. Du + findest die vollständige Liste in der + [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json)-Datei + des Repositorys. + + ## Einen Fehler gefunden? Ein Tool fehlt? + + Wenn du ein Tool benötigst, das hier noch nicht vorhanden ist, und du + denkst, dass es nützlich sein könnte, bist du herzlich eingeladen, einen + Feature-Request im + [Issues-Bereich](https://github.com/CorentinTh/it-tools/issues/new/choose) + im GitHub-Repository einzureichen. + + Und wenn du einen Fehler gefunden hast oder etwas nicht wie erwartet + funktioniert, melde bitte einen Fehler im + [Issues-Bereich](https://github.com/CorentinTh/it-tools/issues/new/choose) + im GitHub-Repository. +favoriteButton: + remove: Aus Favoriten entfernen + add: Zu Favoriten hinzufügen +toolCard: + new: Neu +search: + label: Suche +tools: + categories: + favorite-tools: Deine Lieblingstools + crypto: Krypto + converter: Konverter + web: Web + images and videos: Bilder & Videos + development: Entwicklung + network: Netzwerk + math: Mathematik + measurement: Messung + text: Text + data: Daten + password-strength-analyser: + title: Passwortstärken-Analysator + description: >- + Ermittle die Stärke deines Passworts mit diesem Client-seitigen + Passwortstärken-Analysator und Tool zur Schätzung der Knackzeit. + chronometer: + title: Chronometer + description: >- + Überwache die Dauer einer Sache. Im Grunde ein Chronometer mit einfachen + Chronometerfunktionen. + token-generator: + title: Token-Generator + description: >- + Generiere eine zufällige Zeichenfolge mit den von dir gewünschten Zeichen, + Groß- oder Kleinbuchstaben, Zahlen und/oder Symbolen. + uppercase: Großbuchstaben (ABC...) + lowercase: Kleinbuchstaben (abc...) + numbers: Zahlen (123...) + symbols: Symbole (!-;...) + length: Länge + tokenPlaceholder: Der Token ... + copied: Token in die Zwischenablage kopiert + button: + copy: Kopieren + refresh: Aktualisieren + percentage-calculator: + title: Prozentrechner + description: >- + Berechne einfach Prozentsätze von einem Wert zu einem anderen Wert oder + von einem Prozentsatz zu einem Wert. + svg-placeholder-generator: + title: SVG-Platzhalter-Generator + description: >- + Generiere SVG-Bilder, die als Platzhalter in deinen Anwendungen verwendet + werden können. + json-to-csv: + title: JSON zu CSV + description: Konvertiere JSON mit automatischer Headererkennung in CSV. + camera-recorder: + title: Kamera-Rekorder + description: Mache ein Foto oder nimm ein Video von deiner Webcam oder Kamera auf. + keycode-info: + title: Keycode-Info + description: >- + Finde den JavaScript-Keycode, den Code, den Standort und die Modifikatoren + einer beliebigen gedrückten Taste. + emoji-picker: + title: Emoji-Picker + description: >- + Einfaches Kopieren und Einfügen von Emojis. Erhalte außerdem den Unicode- + und Codepunkt-Wert jedes Emojis. + color-converter: + title: Farbkonverter + description: >- + Konvertiere Farben zwischen den verschiedenen Formaten (Hex, RGB, HSL und + CSS-Name). + bcrypt: + title: Bcrypt + description: >- + Hashen und Vergleichen von Strings mit bcrypt. Bcrypt ist eine auf der + Blowfish-Chiffre basierende Hash-Funktion. + crontab-generator: + title: Crontab-Generator + description: >- + Überprüfe und generiere Crontab und erhalte die menschenlesbare + Beschreibung des Cron-Zeitplans. + http-status-codes: + title: HTTP-Statuscodes + description: Liste aller HTTP-Statuscodes, ihrer Namen und ihrer Bedeutung. + sql-prettify: + title: SQL verschönern und formatieren + description: >- + Formatiere und verschönere deine SQL-Abfragen online (unterstützt + verschiedene SQL-Dialekte). + benchmark-builder: + title: Benchmark-Builder + description: >- + Vergleiche ganz einfach die Ausführungszeit von Aufgaben mit diesem sehr + einfachen Online-Benchmark-Builder. + git-memo: + title: Git-Spickzettel + description: >- + Git ist eine dezentrale Versionsverwaltungssoftware. Mit diesem + Spickzettel hast du schnellen Zugriff auf die gängigsten Git-Befehle. + slugify-string: + title: Slugify String + description: Mache einen String URL-, Dateinamen- und ID-sicher. + encryption: + title: Text verschlüsseln / entschlüsseln + description: >- + Verschlüssele und entschlüssele Klartext mithilfe von Kryptoalgorithmen + wie AES, TripleDES, Rabbit oder RC4. + random-port-generator: + title: Zufälliger Port-Generator + description: >- + Generiere zufällige Portnummern außerhalb des Bereichs der "bekannten" + Ports (0-1023). + yaml-prettify: + title: YAML verschönern und formatieren + description: Verschönere deinen YAML-String in ein menschenlesbares Format. + eta-calculator: + title: ETA-Rechner + description: >- + Ein ETA (Estimated Time of Arrival)-Rechner, um die ungefähre Endzeit + einer Aufgabe zu erfahren, z. B. den Zeitpunkt des Endes eines Downloads. + roman-numeral-converter: + title: Römische Zahlen Konverter + description: >- + Konvertiere römische Zahlen in Dezimalzahlen und Dezimalzahlen in römische + Zahlen. + hmac-generator: + title: HMAC-Generator + description: >- + Berechnet einen hashbasierten Nachrichtenauthentifizierungscode (HMAC) + unter Verwendung eines geheimen Schlüssels und deiner bevorzugten + Hash-Funktion. + bip39-generator: + title: BIP39-Passphrasengenerator + description: >- + Generiere BIP39-Passphrasen aus vorhandener oder zufälliger Mnemonik oder + erhalte die Mnemonik aus der Passphrase. + base64-file-converter: + title: Base64-Dateikonverter + description: Konvertiere Strings, Dateien oder Bilder in ihre Base64-Repräsentation. + list-converter: + title: Listenkonverter + description: >- + Dieses Tool kann spaltenbasierte Daten verarbeiten und verschiedene + Änderungen (transponieren, Präfix und Suffix hinzufügen, Liste umkehren, + Liste sortieren, Werte in Kleinbuchstaben umwandeln, Werte abschneiden) + auf jede Zeile anwenden. + base64-string-converter: + title: Base64-String-Encoder/Decoder + description: Codiere und decodiere Strings einfach in ihre Base64-Repräsentation. + toml-to-yaml: + title: TOML zu YAML + description: Parse und konvertiere TOML zu YAML. + math-evaluator: + title: Mathematischer Auswerter + description: >- + Ein Taschenrechner zum Auswerten mathematischer Ausdrücke. Du kannst + Funktionen wie sqrt, cos, sin, abs usw. verwenden. + json-to-yaml-converter: + title: JSON zu YAML + description: Konvertiere JSON einfach in YAML mit diesem Live-Online-Konverter. + url-parser: + title: URL-Parser + description: >- + Parse eine URL-Zeichenfolge, um alle verschiedenen Teile (Protokoll, + Ursprung, Parameter, Port, Benutzername-Passwort usw.) zu erhalten. + iban-validator-and-parser: + title: IBAN-Validator und -Parser + description: >- + Validiere und parse IBAN-Nummern. Überprüfe, ob die IBAN gültig ist, und + erhalte das Land, BBAN, ob es sich um eine QR-IBAN handelt und das + IBAN-freundliche Format. + user-agent-parser: + title: User-Agent-Parser + description: >- + Erkenne und parse Browser, Engine, Betriebssystem, CPU und + Gerätetyp/-modell aus einer User-Agent-Zeichenfolge. + numeronym-generator: + title: Numeronym-Generator + description: >- + Ein Numeronym ist ein Wort, bei dem eine Zahl verwendet wird, um eine + Abkürzung zu bilden. Zum Beispiel ist "i18n" ein Numeronym für + "internationalization", wobei 18 für die Anzahl der Buchstaben zwischen + dem ersten "i" und dem letzten "n" im Wort steht. + case-converter: + title: Fall-Konverter + description: >- + Ändere den Fall eines Strings und wähle zwischen verschiedenen Formaten + aus. + html-entities: + title: HTML-Entity-Escape + description: >- + Escape oder unescape HTML-Entitäten (ersetze <, >, &, " und ' durch ihre + HTML-Version). + json-prettify: + title: JSON verschönern und formatieren + description: Verschönere deinen JSON-String in ein menschenlesbares Format. + docker-run-to-docker-compose-converter: + title: Docker run zu Docker compose Konverter + description: Wandle docker run-Befehle in docker-compose-Dateien um! + mac-address-lookup: + title: MAC-Adressensuche + description: Finde den Anbieter und Hersteller eines Geräts anhand seiner MAC-Adresse. + mime-types: + title: MIME-Typen + description: Konvertiere MIME-Typen in Erweiterungen und umgekehrt. + toml-to-json: + title: TOML zu JSON + description: Parse und konvertiere TOML zu JSON. + lorem-ipsum-generator: + title: Lorem Ipsum Generator + description: >- + Lorem Ipsum ist ein Platzhaltertext, der häufig verwendet wird, um die + visuelle Form eines Dokuments oder einer Schriftart ohne Verwendung von + bedeutendem Inhalt zu demonstrieren. + qrcode-generator: + title: QR-Code-Generator + description: >- + Generiere und downloade QR-Codes für eine URL oder einfach einen Text und + passe die Hintergrund- und Vordergrundfarben an. + wifi-qrcode-generator: + title: WLAN-QR-Code-Generator + description: >- + Generiere und lade QR-Codes für schnelle Verbindungen zu WLAN-Netzwerken + herunter. + xml-formatter: + title: XML-Formatter + description: Verschönere deinen XML-String in ein menschenlesbares Format. + temperature-converter: + title: Temperaturkonverter + description: >- + Temperaturgradumrechnungen für Kelvin, Celsius, Fahrenheit, Rankine, + Delisle, Newton, Réaumur und Rømer. + chmod-calculator: + title: Chmod-Rechner + description: >- + Berechne deine Chmod-Berechtigungen und -Befehle mit diesem + Online-Chmod-Rechner. + rsa-key-pair-generator: + title: RSA-Schlüsselpaar-Generator + description: Generiere neue zufällige RSA-Private- und Public-Key-PEM-Zertifikate. + html-wysiwyg-editor: + title: HTML-WYSIWYG-Editor + description: >- + Online-HTML-Editor mit funktionsreichem WYSIWYG-Editor, erhalte sofort den + Quellcode des Inhalts. + yaml-to-toml: + title: YAML zu TOML + description: Parse und konvertiere YAML zu TOML. + mac-address-generator: + title: MAC-Adressen-Generator + description: >- + Gebe die Menge und das Präfix ein. MAC-Adressen werden in deiner gewählten + Schreibweise (Groß- oder Kleinbuchstaben) generiert. + json-diff: + title: JSON-Unterschied + description: Vergleiche zwei JSON-Objekte und erhalte die Unterschiede zwischen ihnen. + jwt-parser: + title: JWT-Parser + description: >- + Parse und decodiere deinen JSON-Web-Token (JWT) und zeige dessen Inhalt + an. + date-converter: + title: Datum-Uhrzeit-Konverter + description: Konvertiere Datum und Uhrzeit in verschiedene Formate. + phone-parser-and-formatter: + title: Telefonnummer-Parser und -Formatter + description: >- + Parse, validiere und formatiere Telefonnummern. Erhalte Informationen zur + Telefonnummer, wie z. B. die Landesvorwahl, den Typ usw. + ipv4-subnet-calculator: + title: IPv4-Subnetzrechner + description: >- + Parse deine IPv4-CIDR-Blöcke und erhalte alle Informationen, die du über + dein Subnetz benötigst. + og-meta-generator: + title: Open Graph Meta-Generator + description: Generiere Open Graph- und Social-HTML-Metatags für deine Website. + ipv6-ula-generator: + title: IPv6-ULA-Generator + description: >- + Generiere deine eigenen lokalen, nicht routbaren IP-Adressen in deinem + Netzwerk gemäß RFC4193. + hash-text: + title: Text hashen + description: >- + Hashe einen Text-String mit der von dir benötigten Funktion: MD5, SHA1, + SHA256, SHA224, SHA512, SHA384, SHA3 oder RIPEMD160 + json-to-toml: + title: JSON zu TOML + description: Parse und konvertiere JSON zu TOML. + device-information: + title: Geräteinformationen + description: >- + Informationen zu deinem aktuellen Gerät (Bildschirmgröße, Pixelverhältnis, + Benutzeragent, ...) erhalten. + pdf-signature-checker: + title: PDF-Signaturprüfer + description: >- + Überprüfe die Signaturen einer PDF-Datei. Eine signierte PDF-Datei enthält + eine oder mehrere Signaturen, die verwendet werden können, um + festzustellen, ob der Inhalt der Datei seit dem Zeitpunkt der Signierung + geändert wurde. + json-minify: + title: JSON minifizieren + description: >- + Minifiziere und komprimiere dein JSON, indem unnötige Leerzeichen entfernt + werden. + ulid-generator: + title: ULID-Generator + description: >- + Generiere zufällige Universally Unique Lexicographically Sortable + Identifier (ULID). + string-obfuscator: + title: String-Verschleierer + description: >- + Verschleiere einen String (wie ein Secret, eine IBAN oder ein Token), um + ihn weitergeben zu können und identifizierbar zu machen, ohne seinen + Inhalt preiszugeben. + base-converter: + title: Ganzzahl-Basiskonverter + description: >- + Konvertiere Zahlen zwischen verschiedenen Basen (Dezimal, Hexadezimal, + Binär, Oktal, Base64, ...). + yaml-to-json-converter: + title: YAML zu JSON + description: Konvertiere YAML einfach in JSON mit diesem Live-Online-Konverter. + uuid-generator: + title: UUID-Generator + description: >- + Ein Universally Unique Identifier (UUID) ist eine 128-Bit-Nummer, die zur + Identifizierung von Informationen in Computersystemen verwendet wird. Die + Anzahl der möglichen UUIDs beträgt 16^32, was 2^128 oder etwa 3,4x10^38 + entspricht (was ziemlich viel ist!). + ipv4-address-converter: + title: IPv4-Adresskonverter + description: >- + Konvertiere eine IP-Adresse in Dezimal, Binär, Hexadezimal oder sogar in + IPv6. + text-statistics: + title: Textstatistiken + description: >- + Informationen zu einem Text erhalten, wie die Anzahl der Zeichen, die + Anzahl der Wörter, die Größe usw. + text-to-nato-alphabet: + title: Text zu NATO-Alphabet + description: >- + Wandle Text in das NATO-Phonetik-Alphabet für die mündliche Übermittlung + um. + basic-auth-generator: + title: Basic-Auth-Generator + description: >- + Generiere einen Base64-Basic-Auth-Header aus einem Benutzernamen und einem + Passwort. + text-to-unicode: + title: Text zu Unicode + description: Parse und konvertiere Text in Unicode und umgekehrt. + ipv4-range-expander: + title: IPv4-Bereichserweiterer + description: >- + Bei Angabe einer Start- und End-IPv4-Adresse berechnet dieses Tool ein + gültiges IPv4-Netzwerk mit seiner CIDR-Notation. + text-diff: + title: Textunterschied + description: Vergleiche zwei Texte und sieh die Unterschiede zwischen ihnen. + otp-generator: + title: OTP-Code-Generator + description: >- + Generiere und validiere zeitbasierte OTPs (Einmalpasswörter) für + Multi-Faktor-Authentifizierung. + url-encoder: + title: Kodieren/Decodieren von URL-formatierten Zeichenfolgen + description: >- + Kodiere zum URL-kodierten Format (auch als "prozentkodiert" bekannt) oder + decodiere es. + text-to-binary: + title: Text zu ASCII-Binär + description: Konvertiere Text in seine ASCII-Binärrepräsentation und umgekehrt. diff --git a/src/modules/i18n/components/locale-selector.vue b/src/modules/i18n/components/locale-selector.vue index 3f0c461c..45732bf9 100644 --- a/src/modules/i18n/components/locale-selector.vue +++ b/src/modules/i18n/components/locale-selector.vue @@ -3,6 +3,7 @@ const { availableLocales, locale } = useI18n(); const localesLong: Record = { en: 'English', + de: 'Deutsch', es: 'Español', fr: 'Français', pt: 'Português', From a7992340f7059bda743d3b0458038b229cb63a35 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Fri, 10 May 2024 11:28:45 +0200 Subject: [PATCH 05/41] chore(issues): improved bug issue template (#1046) --- .github/ISSUE_TEMPLATE/bug-report.md | 34 ------------------- .github/ISSUE_TEMPLATE/bug-report.yml | 48 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 34 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index c4bf532b..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve our tools -title: '[BUG] ' -labels: bug -assignees: CorentinTh ---- - -**Which tool is impacted?** -Example: the token generator - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Configuration (please complete the following information):** - -- Device: [e.g. iPhone6, ] -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..fb9964d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,48 @@ +name: Bug Report +description: File a bug report. +labels: ['bug', 'triage'] +assignees: + - CorentinTh +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! + placeholder: Bug description + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? If you have a screenshot, you can paste it here. + placeholder: Tell us what you see! + value: 'A bug happened!' + validations: + required: true + + - type: textarea + id: version + attributes: + label: System information + description: What is you environment? You can use the `npx envinfo --system --browsers` command to get this information. + validations: + required: true + + - type: dropdown + id: app-type + attributes: + label: Where did you encounter the bug? + options: + - Public app (it-tools.tech) + - Self hosting + - Other (installations, docker, etc.) + validations: + required: true From 124284278f0b2e66a96cd8c12b416a3f574a6c2b Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 11:36:49 +0200 Subject: [PATCH 06/41] refactor(auto-imports): regen auto imports --- components.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/components.d.ts b/components.d.ts index e31119b3..f2c3146f 100644 --- a/components.d.ts +++ b/components.d.ts @@ -159,6 +159,7 @@ declare module '@vue/runtime-core' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] + SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] From 2852c30e1f9e5b0f1c4ce2c6f4bf039dda68f96a Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 11:37:14 +0200 Subject: [PATCH 07/41] chore(issues): improved issues template --- .github/ISSUE_TEMPLATE/bug-report.yml | 4 +- .github/ISSUE_TEMPLATE/feature-request.yml | 56 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index fb9964d1..d338fa3f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,4 +1,4 @@ -name: Bug Report +name: 🐞 Bug Report description: File a bug report. labels: ['bug', 'triage'] assignees: @@ -42,7 +42,7 @@ body: label: Where did you encounter the bug? options: - Public app (it-tools.tech) - - Self hosting + - A self hosted - Other (installations, docker, etc.) validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..ed666db9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,56 @@ +name: 🚀 New feature proposal +description: Propose a new feature to be added to IT-Tools. +labels: ['enhancement', 'triage'] + +body: + - type: markdown + attributes: + value: | + Thanks for your interest in the project and taking the time to fill out this feature report! + + - type: dropdown + id: request-type + attributes: + label: What type of request is this? + options: + - New tool idea + - New feature for an existing tool + - Deployment or CI/CD improvement + - Self-hosting improvement + - Other + validations: + required: true + + - type: textarea + id: feature-description + attributes: + label: Clear and concise description of the feature you are proposing + description: A clear and concise description of what the feature is. + placeholder: 'Example: a token generator tool' + validations: + required: true + + - type: textarea + id: alternative + attributes: + label: Is their example of this tool in the wild? + description: Provide link to already existing tool (like websites, apps, cli, ...) or npm packages that could be used or provide inspiration for the feature. + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Any other context or screenshots about the feature request here. + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check the feature is not already implemented in the project. + required: true + - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate. + required: true + - label: Check that the feature can be implemented in a client side only app (IT-Tools is client side only, no server). + required: true From 33e5294a948dbea05cdc3e123cc275f5e5b93b69 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 12:07:50 +0200 Subject: [PATCH 08/41] refactor(lint): removed extra semi --- src/tools/token-generator/token-generator.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/token-generator/token-generator.service.ts b/src/tools/token-generator/token-generator.service.ts index 3733a884..f928a415 100644 --- a/src/tools/token-generator/token-generator.service.ts +++ b/src/tools/token-generator/token-generator.service.ts @@ -20,7 +20,7 @@ export function createToken({ withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '', withNumbers ? '0123456789' : '', withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '', - ].join(''); ; + ].join(''); return shuffleString(allAlphabet.repeat(length)).substring(0, length); } From 9dfd347edfc36eb181baea7768cc85d65974cc86 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 12:08:15 +0200 Subject: [PATCH 09/41] docs(changelog): update changelog for 2024.05.10-33e5294 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c12c11c..7d908d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## Version 2024.05.10-33e5294 + +### Features +- **i18n**: added German translation (#1038) (2c2fb21) +- **new tool**: Outlook Safelink Decoder (#911) (d3b32cc) +- **new tool**: ascii art generator (#886) (fe349ad) +- **i18n**: get locales on build (#880) (dc04615) +- **i18n**: added vi tools translations (#876) (079aa21) +- **i18n**: added zh tools translations (#874) (9c6b122) +- **i18n**: added missing locale files in tools (#863) (7f5fa00) +- **i18n**: added vietnamese language (#859) (1334bff) +- **i18n**: added spanish language (#854) (85b50bb) +- **i18n**: added portuguese language (#813) (c65ffb6) +- **i18n**: added ukrainian language (#827) (693f362) +- **new-tool**: yaml formater (#779) (fc06f01) +- **new-tool**: added unicode conversion utilities (#858) (c46207f) + +### Bug fixes +- **language**: English language cleanup (#1036) (221ddfa) +- **url-encoder, validation**: typo in validation of url-encoder.vue #1024 (cb5b462) +- **integer base converter**: support bigint (#872) (9eac9cb) +- **bcrypt tool**: allow salt rounds up to 100 (#987) (23f82d9) + +### Refactoring +- **lint**: removed extra semi (33e5294) +- **auto-imports**: regen auto imports (1242842) +- **home**: lightened tool cards (#882) (a07806c) +- **home**: removed n-grid to prevent layout shift (#881) (10e56b3) +- **i18n**: added locales per tool (#861) (95698cb) + +### Chores +- **issues**: improved issues template (2852c30) +- **issues**: improved bug issue template (#1046) (a799234) + ## Version 2023.12.21-5ed3693 ### Features From 38d568798c6de5e8968349f1e1f1ec079d3bb3b6 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 12:08:16 +0200 Subject: [PATCH 10/41] chore(version): release 2024.05.10-33e5294 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd6c02e6..b8b1eeee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "it-tools", - "version": "2023.12.21-5ed3693", + "version": "2024.5.10-33e5294", "description": "Collection of handy online tools for developers, with great UX. ", "keywords": [ "productivity", From b59942ad9ff793b16d8d497d7f35f34a00e95469 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 10 May 2024 13:26:36 +0200 Subject: [PATCH 11/41] chore(node): upgraded node version in CI workflows --- .github/workflows/codeql-analysis.yml | 69 -------------------- .github/workflows/docker-nightly-release.yml | 2 +- .github/workflows/releases.yml | 2 +- 3 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 6d11e825..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,69 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ dev ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ dev ] - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/docker-nightly-release.yml b/.github/workflows/docker-nightly-release.yml index 81a0898c..41dbb155 100644 --- a/.github/workflows/docker-nightly-release.yml +++ b/.github/workflows/docker-nightly-release.yml @@ -32,7 +32,7 @@ jobs: - run: corepack enable - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 8ed4099d..d0d3febd 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -61,7 +61,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 cache: 'pnpm' - name: Install dependencies From 5a7b0f9636de45f71296165a47cb12931ea02097 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Mon, 13 May 2024 10:11:47 +0200 Subject: [PATCH 12/41] chore(issues): removed old issue templates (#1077) --- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/ISSUE_TEMPLATE/new-tool-request.md | 19 ------------------- .github/ISSUE_TEMPLATE/other-request.md | 13 ------------- .github/ISSUE_TEMPLATE/tool-improvement.md | 13 ------------- 4 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/new-tool-request.md delete mode 100644 .github/ISSUE_TEMPLATE/other-request.md delete mode 100644 .github/ISSUE_TEMPLATE/tool-improvement.md diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index ed666db9..edae822e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,5 +1,5 @@ name: 🚀 New feature proposal -description: Propose a new feature to be added to IT-Tools. +description: Propose a new feature/enhancement or tool idea for IT-Tools labels: ['enhancement', 'triage'] body: diff --git a/.github/ISSUE_TEMPLATE/new-tool-request.md b/.github/ISSUE_TEMPLATE/new-tool-request.md deleted file mode 100644 index a67a9cd0..00000000 --- a/.github/ISSUE_TEMPLATE/new-tool-request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: New tool request -about: Suggest a new tool idea -title: '[NEW TOOL]' -labels: new tool -assignees: CorentinTh ---- - -**What tool do you want?** -Example: a token generator - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Is their example of this tool in the wild?** -Provide link to already existing tool or npm packages if any exists - -**Additional context** -Add any other context about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/other-request.md b/.github/ISSUE_TEMPLATE/other-request.md deleted file mode 100644 index b4901ed4..00000000 --- a/.github/ISSUE_TEMPLATE/other-request.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Other request -about: Any request that does not concern a tool creation, a new feature request on a tool or a bug -title: '[OTHER] ' -labels: -assignees: CorentinTh ---- - -**Describe the solution you'd like** -A clear and concise description of what you want. - -**Additional context** -Add any other context about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/tool-improvement.md b/.github/ISSUE_TEMPLATE/tool-improvement.md deleted file mode 100644 index 8ff8bb6c..00000000 --- a/.github/ISSUE_TEMPLATE/tool-improvement.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Tool improvement -about: Improvement on an existing tool -title: '[TOOL IMPROVEMENT]' -labels: enhancement -assignees: CorentinTh ---- - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Additional context** -Add any other context about the feature request here. From a0bc3468b256739d83b08ef25fb5709281260f69 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Mon, 13 May 2024 10:44:04 +0200 Subject: [PATCH 13/41] chore(issues): prevent empty issues (#1078) --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false From 81cf6b54835499952ee39c5cb9bb20a196adc625 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Mon, 13 May 2024 10:54:18 +0200 Subject: [PATCH 14/41] docs(changelog): update changelog for 2024.05.13-a0bc346 --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d908d43..984fa58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## Version 2024.05.10-33e5294 +## Version 2024.05.13-a0bc346 ### Features - **i18n**: added German translation (#1038) (2c2fb21) @@ -33,12 +33,20 @@ All notable changes to this project will be documented in this file. See [standa - **i18n**: added locales per tool (#861) (95698cb) ### Chores +- **issues**: prevent empty issues (#1078) (a0bc346) +- **issues**: removed old issue templates (#1077) (5a7b0f9) +- **node**: upgraded node version in CI workflows (b59942a) +- **version**: release 2024.05.10-33e5294 (38d5687) - **issues**: improved issues template (2852c30) - **issues**: improved bug issue template (#1046) (a799234) +### Documentation +- **changelog**: update changelog for 2024.05.10-33e5294 (9dfd347) + ## Version 2023.12.21-5ed3693 ### Features + - **i18n**: improve chinese i18n (#757) (2e56641) - **i18n**: add tooltip and favoriteButton i18n (#756) (a1037cf) - **i18n**: add Chinese translation base (#718) (8f99eb6) @@ -46,6 +54,7 @@ All notable changes to this project will be documented in this file. See [standa - **new tool**: numeronym generator (#729) (e07e2ae) ### Bug fixes + - **jwt-parser**: jwt claim array support (#799) (5ed3693) - **camera-recorder**: stop camera on navigation (#782) (80e46c9) - **doc**: updated create new tool command in readme (#762) (7a70dbb) @@ -54,6 +63,7 @@ All notable changes to this project will be documented in this file. See [standa - **eta**: corrected example (#737) (821cbea) ### Refactoring + - **about, i18n**: improved i18n dx with markdown (#753) (bd3edcb) - **token, i18n**: complete fr translation (#752) (de1ee69) - **uuid generator**: uuid version picker (#751) (38586ca) @@ -63,6 +73,7 @@ All notable changes to this project will be documented in this file. See [standa - **bcrypt**: fix input label align (#721) (093ff31) ### Chores + - **deps**: switched from oui to oui-data for mac address lookup (#693) (0fe9a20) - **deps**: update unocss monorepo to ^0.57.0 (#638) (2e396d8) - **docker**: added armv7 plateform for docker releases (#722) (fe1de8c) @@ -70,19 +81,23 @@ All notable changes to this project will be documented in this file. See [standa ## Version 2023.11.02-7d94e11 ### Features + - **i18n**: language selector (#710) (e86fd96) ### Bug fixes + - **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11) - **encryption**: alert on decryption error (#711) (02b0d0d) ### Refactoring + - **math-evaluator**: improved description (e87f4b1) - **math-evaluator**: improved search and UX (#713) (58de897) ## Version 2023.11.01-e164afb ### Features + - **command-palette**: clear prompt on palette close (#708) (d013696) - **command-palette**: added about page in command palette (99b1eb9) - **new tool**: random MAC address generator (#657) (cc3425d) @@ -101,11 +116,13 @@ All notable changes to this project will be documented in this file. See [standa - **new tool**: text diff and comparator (#588) (81bfe57) ### Bug fixes + - **deps**: fix issue on slugify (#593) (#673) (720201a) - **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7) - **deps**: update dependency sql-formatter to v13 (#606) (c7d4562) ### Refactoring + - **ui**: better ui demo preview menu (#664) (015c673) - **color-converter**: improved color-converter UX (#701) (abb8335) - **docker**: improved docker config (#700) (020e9cb) @@ -122,6 +139,7 @@ All notable changes to this project will be documented in this file. See [standa - **bcrypt**: fix typo (#604) (e18bae1) ### Chores + - **deps**: clean unused dependencies (#709) (e164afb) - **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226) - **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1) @@ -136,19 +154,23 @@ All notable changes to this project will be documented in this file. See [standa - **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc) ### Doc + - **readme**: added contributors list (#622) (557b304) - **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547) ## Version 2023.08.21-6f93cba ### Features + - **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba) - **new tool**: string obfuscator (#575) (c58d6e3) ### Bug fixes + - **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a) ### Chores + - **deps**: switched to fucking typescript v5 (#501) (76b2761) - **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01) - **deps**: update dependency prettier to v3 (#564) (a2b9b15) @@ -158,6 +180,7 @@ All notable changes to this project will be documented in this file. See [standa ## Version 2023.08.16-9bd4ad4 ### Features + - **Case Converter**: Add lowercase and uppercase (#534) (7b6232a) - **new tool**: emoji picker (#551) (93f7cf0) - **ui**: added c-select in the ui lib (#550) (dfa1ba8) @@ -178,6 +201,7 @@ All notable changes to this project will be documented in this file. See [standa - **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c) ### Bug fixes + - **deps**: update dependency uuid to v9 (#566) (5e12991) - **deps**: update dependency mathjs to v11 (#519) (7924456) - **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c) @@ -197,6 +221,7 @@ All notable changes to this project will be documented in this file. See [standa - **ipv4-converter**: removed readonly on input (7aed9c5) ### Refactoring + - **navbar**: consistent spacing in navbar buttons (#507) (30f88fc) - **ui**: remove n-text (#506) (72c98a3) - **ui**: replaced some n-input to c-input (#505) (05ea545) @@ -209,6 +234,7 @@ All notable changes to this project will be documented in this file. See [standa - **ui**: replaced some n-input with c-input-text (f7fc779) ### Chores + - **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4) - **deps**: update dependency node to v18.17.1 (#560) (65a9474) - **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8) @@ -249,47 +275,58 @@ All notable changes to this project will be documented in this file. See [standa - **lint**: switched to a better lint config (33c9b66) ### Refacor + - **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d) ### Documentation + - **ide**: updated vscode extensions settings (#472) (847323c) ### Chors + - **deps**: updated vueuse dependency version (8515c24) ## Version 2023.05.14-77f2efc ### Features + - **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b) - **new tool**: phone parser and normalizer (ce3150c) ### Bug fixes + - **phone-parser**: use default country code (a43c546) - **home**: prevent weird blue border on card (3f6c8f0) ### Refactoring + - **ui**: replaced some n-input with c-input-text (77f2efc) ### Chores + - **issues**: updated new tool request issue template (edae4c6) ### Ui-lib + - **new-component**: added text input component in the c-lib (aad8d84) - **button**: size variants (401f13f) ## Version 2023.04.23-92bd835 ### Features + - **ui-lib**: demo pages for c-lib components (92bd835) - **new-tool**: diff of two json objects (362f2fa) - **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2) - **date converter**: auto focus main input (6d22025) ### Bug fixes + - **ts**: cleaned legacy typechecking warning (e88c1d5) - **mac-address-lookup**: added copy handler on button click (c311e38) ### Refactoring + - **ui-lib**: prevent c-button to shrink (61ece23) - **ui**: replaced naive ui cards with custom ones (f080933) - **clean**: removed unused lodash import (bb32513) @@ -299,48 +336,60 @@ All notable changes to this project will be documented in this file. See [standa ## Version 2023.04.14-dbad773 ### Features + - **new-tool**: http status codes (8355bd2) ### Refactoring + - **uuid-generator**: prevent NaN in quantity (6fb4994) ### Chores + - **release**: create a github release on new version (dbad773) - **version**: reset CHANGELOG content to support new format (85cb0ff) ## Version 2023.04.14-f9b77b7 ### Features + - **new-tool**: http status codes (8355bd2) ### Refactoring + - **uuid-generator**: prevent NaN in quantity (6fb4994) ### Chores + - **release**: create a github release on new version (f9b77b7) - **version**: reset CHANGELOG content to support new format (85cb0ff) ## Version 2023.04.14-2f0d239 ### Features + - **new-tool**: http status codes (8355bd2) ### Refactoring + - **uuid-generator**: prevent NaN in quantity (6fb4994) ### Chores + - **release**: create a github release on new version (2f0d239) - **version**: reset CHANGELOG content to support new format (85cb0ff) ## Version 2023.04.14-474cae4 ### Features + - **new-tool**: http status codes (8355bd2) ### Refactoring + - **uuid-generator**: prevent NaN in quantity (6fb4994) ### Chores + - **release**: create a github release on new version (474cae4) - **version**: reset CHANGELOG content to support new format (85cb0ff) diff --git a/package.json b/package.json index b8b1eeee..801f5c0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "it-tools", - "version": "2024.5.10-33e5294", + "version": "2024.5.13-b62e12f", "description": "Collection of handy online tools for developers, with great UX. ", "keywords": [ "productivity", From e876d0360812eab1255e414102a44d65f7c1bfaf Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Mon, 13 May 2024 10:54:18 +0200 Subject: [PATCH 15/41] chore(version): release 2024.05.13-a0bc346 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 801f5c0f..9f39ff1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "it-tools", - "version": "2024.5.13-b62e12f", + "version": "2024.5.13-a0bc346", "description": "Collection of handy online tools for developers, with great UX. ", "keywords": [ "productivity", From 30144aa3f51f5d0dc90f64cf888bc6f83de11b10 Mon Sep 17 00:00:00 2001 From: sharevb Date: Mon, 20 May 2024 22:13:55 +0200 Subject: [PATCH 16/41] feat(base64): Base64 enhancements (#905) * fix(base64): use js-base64 to handle non ascii text Use js-base64 to handle non ascii text and ignore whitespaces Fix #879 and #409 * fix(base64): use js-base64 to handle non ascii text Use js-base64 to handle non ascii text and ignore whitespaces Fix #879 and #409 * feat(base64 file converter): add a filename and extension fields Add filename and extension (auto filled if data url) to allow downloading with right extension and filename Fix #788 * feat(base64 file converter): add a preview image Fix #594. Taken from #595 (thanks @SAF2k) --- package.json | 1 + pnpm-lock.yaml | 19 ++-- src/composable/downloadBase64.ts | 91 +++++++++++++++---- .../base64-file-converter.vue | 67 +++++++++++++- src/utils/base64.test.ts | 13 +-- src/utils/base64.ts | 11 ++- 6 files changed, 164 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 9f39ff1d..c6cb7757 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", + "js-base64": "^3.7.6", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6c38c9..8619d8c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ dependencies: ibantools: specifier: ^4.3.3 version: 4.3.3 + js-base64: + specifier: ^3.7.6 + version: 3.7.7 json5: specifier: ^2.2.3 version: 2.2.3 @@ -3351,7 +3354,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.8.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3993,10 +3996,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.8.0(vue@3.3.4): + resolution: {integrity: sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==} dependencies: - vue-demi: 0.14.6(vue@3.3.4) + vue-demi: 0.14.7(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -6472,6 +6475,10 @@ packages: hasBin: true dev: true + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + dev: false + /js-beautify@1.14.6: resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==} engines: {node: '>=10'} @@ -9151,8 +9158,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 diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts index 37b0428d..3bc20226 100644 --- a/src/composable/downloadBase64.ts +++ b/src/composable/downloadBase64.ts @@ -1,8 +1,13 @@ -import { extension as getExtensionFromMime } from 'mime-types'; +import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types'; import type { Ref } from 'vue'; import _ from 'lodash'; -export { getMimeTypeFromBase64, useDownloadFileFromBase64 }; +export { + getMimeTypeFromBase64, + getMimeTypeFromExtension, getExtensionFromMimeType, + useDownloadFileFromBase64, useDownloadFileFromBase64Refs, + previewImageFromBase64, +}; const commonMimeTypesSignatures = { 'JVBERi0': 'application/pdf', @@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({ defaultExtension?: string }) { if (mimeType) { - return getExtensionFromMime(mimeType) ?? defaultExtension; + return getExtensionFromMimeType(mimeType) ?? defaultExtension; } return defaultExtension; } -function useDownloadFileFromBase64({ source, filename }: { source: Ref; filename?: string }) { +function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }: +{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) { + if (sourceValue === '') { + throw new Error('Base64 string is empty'); + } + + const defaultExtension = extension ?? 'txt'; + const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue }); + let base64String = sourceValue; + if (!mimeType) { + const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension); + base64String = `data:${targetMimeType};base64,${sourceValue}`; + } + + const cleanExtension = extension ?? getFileExtensionFromMimeType( + { mimeType, defaultExtension }); + let cleanFileName = filename ?? `file.${cleanExtension}`; + if (extension && !cleanFileName.endsWith(`.${extension}`)) { + cleanFileName = `${cleanFileName}.${cleanExtension}`; + } + + const a = document.createElement('a'); + a.href = base64String; + a.download = cleanFileName; + a.click(); +} + +function useDownloadFileFromBase64( + { source, filename, extension, fileMimeType }: + { source: Ref; filename?: string; extension?: string; fileMimeType?: string }) { return { download() { - if (source.value === '') { - throw new Error('Base64 string is empty'); - } - - const { mimeType } = getMimeTypeFromBase64({ base64String: source.value }); - const base64String = mimeType - ? source.value - : `data:text/plain;base64,${source.value}`; - - const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`; - - const a = document.createElement('a'); - a.href = base64String; - a.download = cleanFileName; - a.click(); + downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType }); }, }; } + +function useDownloadFileFromBase64Refs( + { source, filename, extension }: + { source: Ref; filename?: Ref; extension?: Ref }) { + return { + download() { + downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value }); + }, + }; +} + +function previewImageFromBase64(base64String: string): HTMLImageElement { + if (base64String === '') { + throw new Error('Base64 string is empty'); + } + + const img = document.createElement('img'); + img.src = base64String; + + const container = document.createElement('div'); + container.appendChild(img); + + const previewContainer = document.getElementById('previewContainer'); + if (previewContainer) { + previewContainer.innerHTML = ''; + previewContainer.appendChild(container); + } + else { + throw new Error('Preview container element not found'); + } + + return img; +} diff --git a/src/tools/base64-file-converter/base64-file-converter.vue b/src/tools/base64-file-converter/base64-file-converter.vue index 377625bd..a489f9a1 100644 --- a/src/tools/base64-file-converter/base64-file-converter.vue +++ b/src/tools/base64-file-converter/base64-file-converter.vue @@ -2,12 +2,19 @@ import { useBase64 } from '@vueuse/core'; import type { Ref } from 'vue'; import { useCopy } from '@/composable/copy'; -import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; +import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64'; import { useValidation } from '@/composable/validation'; import { isValidBase64 } from '@/utils/base64'; +const fileName = ref('file'); +const fileExtension = ref(''); const base64Input = ref(''); -const { download } = useDownloadFileFromBase64({ source: base64Input }); +const { download } = useDownloadFileFromBase64Refs( + { + source: base64Input, + filename: fileName, + extension: fileExtension, + }); const base64InputValidation = useValidation({ source: base64Input, rules: [ @@ -18,6 +25,35 @@ const base64InputValidation = useValidation({ ], }); +watch( + base64Input, + (newValue, _) => { + const { mimeType } = getMimeTypeFromBase64({ base64String: newValue }); + if (mimeType) { + fileExtension.value = getExtensionFromMimeType(mimeType) || fileExtension.value; + } + }, +); + +function previewImage() { + if (!base64InputValidation.isValid) { + return; + } + try { + const image = previewImageFromBase64(base64Input.value); + image.style.maxWidth = '100%'; + image.style.maxHeight = '400px'; + const previewContainer = document.getElementById('previewContainer'); + if (previewContainer) { + previewContainer.innerHTML = ''; + previewContainer.appendChild(image); + } + } + catch (_) { + // + } +} + function downloadFile() { if (!base64InputValidation.isValid) { return; @@ -44,6 +80,24 @@ async function onUpload(file: File) { From f1a5489e2194aed7619db8f95472987dad49aa0d Mon Sep 17 00:00:00 2001 From: sharevb Date: Fri, 9 Aug 2024 22:11:39 +0200 Subject: [PATCH 21/41] feat(new tools): JSON to XML and XML to JSON (#1231) * feat(new tool): JSON <> XML Fix https://github.com/CorentinTh/it-tools/issues/314 * Update src/tools/xml-to-json/index.ts * Update src/tools/json-to-xml/index.ts * Update src/tools/json-to-xml/index.ts --------- Co-authored-by: Corentin THOMASSET --- components.d.ts | 9 ++----- package.json | 1 + pnpm-lock.yaml | 35 +++++++++++++++++---------- src/tools/index.ts | 4 +++ src/tools/json-to-xml/index.ts | 12 +++++++++ src/tools/json-to-xml/json-to-xml.vue | 32 ++++++++++++++++++++++++ src/tools/xml-to-json/index.ts | 12 +++++++++ src/tools/xml-to-json/xml-to-json.vue | 32 ++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 src/tools/json-to-xml/index.ts create mode 100644 src/tools/json-to-xml/json-to-xml.vue create mode 100644 src/tools/xml-to-json/index.ts create mode 100644 src/tools/xml-to-json/xml-to-json.vue diff --git a/components.d.ts b/components.d.ts index f2c3146f..1eb39cae 100644 --- a/components.d.ts +++ b/components.d.ts @@ -110,6 +110,7 @@ declare module '@vue/runtime-core' { JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default'] JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default'] JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default'] + JsonToXml: typeof import('./src/tools/json-to-xml/json-to-xml.vue')['default'] JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default'] JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default'] JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default'] @@ -130,21 +131,14 @@ declare module '@vue/runtime-core' { NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] - NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NFormItem: typeof import('naive-ui')['NFormItem'] - NGi: typeof import('naive-ui')['NGi'] - NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] - NLabel: typeof import('naive-ui')['NLabel'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSpin: typeof import('naive-ui')['NSpin'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] @@ -186,6 +180,7 @@ declare module '@vue/runtime-core' { UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default'] XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] + XmlToJson: typeof import('./src/tools/xml-to-json/xml-to-json.vue')['default'] YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] YamlViewer: typeof import('./src/tools/yaml-viewer/yaml-viewer.vue')['default'] diff --git a/package.json b/package.json index d1e6e458..180790c7 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "vue-router": "^4.1.6", "vue-tsc": "^1.8.1", "xml-formatter": "^3.3.2", + "xml-js": "^1.6.11", "yaml": "^2.2.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8619d8c0..ebe8ccbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ dependencies: xml-formatter: specifier: ^3.3.2 version: 3.3.2 + xml-js: + specifier: ^1.6.11 + version: 1.6.11 yaml: specifier: ^2.2.1 version: 2.2.1 @@ -3354,7 +3357,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.8.0(vue@3.3.4) + '@vueuse/shared': 10.11.1(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3987,19 +3990,19 @@ packages: - vue dev: false - /@vueuse/shared@10.3.0(vue@3.3.4): - resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} + /@vueuse/shared@10.11.1(vue@3.3.4): + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} dependencies: - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue dev: false - /@vueuse/shared@10.8.0(vue@3.3.4): - resolution: {integrity: sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==} + /@vueuse/shared@10.3.0(vue@3.3.4): + resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} dependencies: - vue-demi: 0.14.7(vue@3.3.4) + vue-demi: 0.14.5(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -7959,8 +7962,6 @@ packages: /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} requiresBuild: true - dev: true - optional: true /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} @@ -9143,8 +9144,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.5(vue@3.3.4): - resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} + /vue-demi@0.14.10(vue@3.3.4): + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9158,8 +9159,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.7(vue@3.3.4): - resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} + /vue-demi@0.14.5(vue@3.3.4): + resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9449,6 +9450,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 @@ -9549,6 +9551,13 @@ packages: xml-parser-xo: 4.0.5 dev: false + /xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.2.4 + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} diff --git a/src/tools/index.ts b/src/tools/index.ts index aa861c93..14cf4dd6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -6,6 +6,8 @@ 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 xmlToJson } from './xml-to-json'; +import { tool as jsonToXml } from './json-to-xml'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as macAddressGenerator } from './mac-address-generator'; @@ -107,6 +109,8 @@ export const toolsByCategory: ToolCategory[] = [ listConverter, tomlToJson, tomlToYaml, + xmlToJson, + jsonToXml, ], }, { diff --git a/src/tools/json-to-xml/index.ts b/src/tools/json-to-xml/index.ts new file mode 100644 index 00000000..c35ace2b --- /dev/null +++ b/src/tools/json-to-xml/index.ts @@ -0,0 +1,12 @@ +import { Braces } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'JSON to XML', + path: '/json-to-xml', + description: 'Convert JSON to XML', + keywords: ['json', 'xml'], + component: () => import('./json-to-xml.vue'), + icon: Braces, + createdAt: new Date('2024-08-09'), +}); diff --git a/src/tools/json-to-xml/json-to-xml.vue b/src/tools/json-to-xml/json-to-xml.vue new file mode 100644 index 00000000..96a7cf16 --- /dev/null +++ b/src/tools/json-to-xml/json-to-xml.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/tools/xml-to-json/index.ts b/src/tools/xml-to-json/index.ts new file mode 100644 index 00000000..8d83f4fe --- /dev/null +++ b/src/tools/xml-to-json/index.ts @@ -0,0 +1,12 @@ +import { Braces } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'XML to JSON', + path: '/xml-to-json', + description: 'Convert XML to JSON', + keywords: ['xml', 'json'], + component: () => import('./xml-to-json.vue'), + icon: Braces, + createdAt: new Date('2024-08-09'), +}); diff --git a/src/tools/xml-to-json/xml-to-json.vue b/src/tools/xml-to-json/xml-to-json.vue new file mode 100644 index 00000000..e1e5a477 --- /dev/null +++ b/src/tools/xml-to-json/xml-to-json.vue @@ -0,0 +1,32 @@ + + + From 318fb6efb99fb916aee0418b7767a05a382b6275 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Thu, 15 Aug 2024 15:29:58 +0200 Subject: [PATCH 22/41] feat(new-tool): add email normalizer (#1243) --- components.d.ts | 6 ++ package.json | 1 + pnpm-lock.yaml | 15 +++++ .../email-normalizer/email-normalizer.vue | 65 +++++++++++++++++++ src/tools/email-normalizer/index.ts | 12 ++++ src/tools/index.ts | 2 + 6 files changed, 101 insertions(+) create mode 100644 src/tools/email-normalizer/email-normalizer.vue create mode 100644 src/tools/email-normalizer/index.ts diff --git a/components.d.ts b/components.d.ts index 1eb39cae..89f41f80 100644 --- a/components.d.ts +++ b/components.d.ts @@ -72,6 +72,7 @@ declare module '@vue/runtime-core' { DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default'] DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default'] Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default'] + EmailNormalizer: typeof import('./src/tools/email-normalizer/email-normalizer.vue')['default'] EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default'] EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default'] EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default'] @@ -132,13 +133,18 @@ declare module '@vue/runtime-core' { NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + NForm: typeof import('naive-ui')['NForm'] + NFormItem: typeof import('naive-ui')['NFormItem'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] + NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSlider: typeof import('naive-ui')['NSlider'] + NSwitch: typeof import('naive-ui')['NSwitch'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/package.json b/package.json index 180790c7..6191f702 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "crypto-js": "^4.1.1", "date-fns": "^2.29.3", "dompurify": "^3.0.6", + "email-normalizer": "^1.0.0", "emojilib": "^3.0.10", "figlet": "^1.7.0", "figue": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebe8ccbc..3044541a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: dompurify: specifier: ^3.0.6 version: 3.0.6 + email-normalizer: + specifier: ^1.0.0 + version: 1.0.0 emojilib: specifier: ^3.0.10 version: 3.0.10 @@ -4976,6 +4979,12 @@ packages: /electron-to-chromium@1.4.572: resolution: {integrity: sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA==} + /email-normalizer@1.0.0: + resolution: {integrity: sha512-wZYuuMtL4kUOmg/TPtCrf9hAZjbFq+FcjWA85Z5nr2lGllRnWJPxCJw3gy4Cx+adMoyVw4VJfGGvt/OHgIW+qg==} + dependencies: + typescript: 5.5.4 + dev: false + /emitter-component@1.1.1: resolution: {integrity: sha512-G+mpdiAySMuB7kesVRLuyvYRqDmshB7ReKEVuyBPkzQlmiDiLrt7hHHIy4Aff552bgknVN7B2/d3lzhGO5dvpQ==} dev: false @@ -8604,6 +8613,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + /typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + /ua-parser-js@1.0.35: resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==} dev: false diff --git a/src/tools/email-normalizer/email-normalizer.vue b/src/tools/email-normalizer/email-normalizer.vue new file mode 100644 index 00000000..eae97c4e --- /dev/null +++ b/src/tools/email-normalizer/email-normalizer.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/tools/email-normalizer/index.ts b/src/tools/email-normalizer/index.ts new file mode 100644 index 00000000..299a30f7 --- /dev/null +++ b/src/tools/email-normalizer/index.ts @@ -0,0 +1,12 @@ +import { Mail } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Email normalizer', + path: '/email-normalizer', + description: 'Normalize email addresses to a standard format for easier comparison. Useful for deduplication and data cleaning.', + keywords: ['email', 'normalizer'], + component: () => import('./email-normalizer.vue'), + icon: Mail, + createdAt: new Date('2024-08-15'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 14cf4dd6..b4c161ef 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -152,6 +153,7 @@ export const toolsByCategory: ToolCategory[] = [ dockerRunToDockerComposeConverter, xmlFormatter, yamlViewer, + emailNormalizer, ], }, { From 87984e2081de4e436adac552f520c02e07e371ab Mon Sep 17 00:00:00 2001 From: sharevb Date: Sun, 25 Aug 2024 22:57:07 +0200 Subject: [PATCH 23/41] feat(new tool): Markdown to HTML (#916) * feat(new tool): Markdown to HTML Fix partially #538 * feat: add print button * Update src/tools/markdown-to-html/index.ts * Update src/tools/markdown-to-html/markdown-to-html.vue --------- Co-authored-by: Corentin THOMASSET --- components.d.ts | 2 + package.json | 2 + pnpm-lock.yaml | 61 ++++++++++++++++--- src/components/TextareaCopyable.vue | 2 + src/tools/index.ts | 2 + src/tools/markdown-to-html/index.ts | 12 ++++ .../markdown-to-html/markdown-to-html.vue | 44 +++++++++++++ 7 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 src/tools/markdown-to-html/index.ts create mode 100644 src/tools/markdown-to-html/markdown-to-html.vue diff --git a/components.d.ts b/components.d.ts index 89f41f80..8e59366a 100644 --- a/components.d.ts +++ b/components.d.ts @@ -121,6 +121,7 @@ declare module '@vue/runtime-core' { LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default'] MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default'] MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default'] + MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default'] MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default'] MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default'] MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] @@ -129,6 +130,7 @@ declare module '@vue/runtime-core' { MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] + NButton: typeof import('naive-ui')['NButton'] NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] diff --git a/package.json b/package.json index 6191f702..63e5856a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", + "@types/markdown-it": "^13.0.7", "@types/figlet": "^1.5.8", "@vicons/material": "^0.12.0", "@vicons/tabler": "^0.12.0", @@ -70,6 +71,7 @@ "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", "lodash": "^4.17.21", + "markdown-it": "^14.0.0", "marked": "^10.0.0", "mathjs": "^11.9.1", "mime-types": "^2.1.35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3044541a..2311f3af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: '@types/figlet': specifier: ^1.5.8 version: 1.5.8 + '@types/markdown-it': + specifier: ^13.0.7 + version: 13.0.9 '@vicons/material': specifier: ^0.12.0 version: 0.12.0 @@ -110,6 +113,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + markdown-it: + specifier: ^14.0.0 + version: 14.1.0 marked: specifier: ^10.0.0 version: 10.0.0 @@ -2952,7 +2958,6 @@ packages: /@types/linkify-it@3.0.2: resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} - dev: true /@types/lodash-es@4.17.10: resolution: {integrity: sha512-YJP+w/2khSBwbUSFdGsSqmDvmnN3cCKoPOL7Zjle6s30ZtemkkqhjVfFqGwPN7ASil5VyjE2GtyU/yqYY6mC0A==} @@ -2974,6 +2979,13 @@ packages: '@types/mdurl': 1.0.2 dev: true + /@types/markdown-it@13.0.9: + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + dependencies: + '@types/linkify-it': 3.0.2 + '@types/mdurl': 1.0.2 + dev: false + /@types/mdast@3.0.11: resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==} dependencies: @@ -2982,7 +2994,6 @@ packages: /@types/mdurl@1.0.2: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} - dev: true /@types/mime-types@2.1.1: resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} @@ -3360,7 +3371,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.11.1(vue@3.3.4) + '@vueuse/shared': 11.0.3(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3993,19 +4004,19 @@ packages: - vue dev: false - /@vueuse/shared@10.11.1(vue@3.3.4): - resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + /@vueuse/shared@10.3.0(vue@3.3.4): + resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} dependencies: - vue-demi: 0.14.10(vue@3.3.4) + vue-demi: 0.14.5(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue dev: false - /@vueuse/shared@10.3.0(vue@3.3.4): - resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} + /@vueuse/shared@11.0.3(vue@3.3.4): + resolution: {integrity: sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==} dependencies: - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -5016,7 +5027,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: true /errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} @@ -6688,6 +6698,12 @@ packages: dependencies: uc.micro: 1.0.6 + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.1.0 + dev: false + /local-pkg@0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} @@ -6825,6 +6841,18 @@ packages: mdurl: 1.0.1 uc.micro: 1.0.6 + /markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + dev: false + /marked@10.0.0: resolution: {integrity: sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==} engines: {node: '>= 18'} @@ -6873,6 +6901,10 @@ packages: /mdurl@1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -7682,6 +7714,11 @@ packages: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -8626,6 +8663,10 @@ packages: /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + /uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + dev: false + /ufo@1.1.2: resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} diff --git a/src/components/TextareaCopyable.vue b/src/components/TextareaCopyable.vue index 8b0aae61..9585177d 100644 --- a/src/components/TextareaCopyable.vue +++ b/src/components/TextareaCopyable.vue @@ -7,6 +7,7 @@ import sqlHljs from 'highlight.js/lib/languages/sql'; import xmlHljs from 'highlight.js/lib/languages/xml'; import yamlHljs from 'highlight.js/lib/languages/yaml'; import iniHljs from 'highlight.js/lib/languages/ini'; +import markdownHljs from 'highlight.js/lib/languages/markdown'; import { useCopy } from '@/composable/copy'; const props = withDefaults( @@ -30,6 +31,7 @@ hljs.registerLanguage('html', xmlHljs); hljs.registerLanguage('xml', xmlHljs); hljs.registerLanguage('yaml', yamlHljs); hljs.registerLanguage('toml', iniHljs); +hljs.registerLanguage('markdown', markdownHljs); const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; diff --git a/src/tools/index.ts b/src/tools/index.ts index b4c161ef..c9003fe8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -9,6 +9,7 @@ import { tool as textToUnicode } from './text-to-unicode'; import { tool as safelinkDecoder } from './safelink-decoder'; import { tool as xmlToJson } from './xml-to-json'; import { tool as jsonToXml } from './json-to-xml'; +import { tool as markdownToHtml } from './markdown-to-html'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as macAddressGenerator } from './mac-address-generator'; @@ -112,6 +113,7 @@ export const toolsByCategory: ToolCategory[] = [ tomlToYaml, xmlToJson, jsonToXml, + markdownToHtml, ], }, { diff --git a/src/tools/markdown-to-html/index.ts b/src/tools/markdown-to-html/index.ts new file mode 100644 index 00000000..73a6cfb3 --- /dev/null +++ b/src/tools/markdown-to-html/index.ts @@ -0,0 +1,12 @@ +import { Markdown } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Markdown to HTML', + path: '/markdown-to-html', + description: 'Convert Markdown to Html and allow to print (as PDF)', + keywords: ['markdown', 'html', 'converter', 'pdf'], + component: () => import('./markdown-to-html.vue'), + icon: Markdown, + createdAt: new Date('2024-08-25'), +}); diff --git a/src/tools/markdown-to-html/markdown-to-html.vue b/src/tools/markdown-to-html/markdown-to-html.vue new file mode 100644 index 00000000..c84d44ec --- /dev/null +++ b/src/tools/markdown-to-html/markdown-to-html.vue @@ -0,0 +1,44 @@ + + + From 67094980c9687652c10847bf4112ae0f36a31ce9 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Fri, 13 Sep 2024 20:56:32 +0200 Subject: [PATCH 24/41] chore(readme): updated logos (#1294) --- .github/logo-dark.png | Bin 0 -> 40398 bytes .github/logo-white.png | Bin 0 -> 39632 bytes .github/logo.png | Bin 8000 -> 0 bytes README.md | 6 +++++- 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .github/logo-dark.png create mode 100644 .github/logo-white.png delete mode 100644 .github/logo.png diff --git a/.github/logo-dark.png b/.github/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9bee8a9cc024cb685e4159137bfa738b9f68a3a2 GIT binary patch literal 40398 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
2JWcE?hs*UvQR{de2#=l#t>wuk4Y$G+~L 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 f=29F2|MUN|_Z)oiV8bnWBamU9u6{1-oD!M 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 QdJQtl)78&qol`;+0FP$w1^@s6 diff --git a/README.md b/README.md index a51f9c53..b9726864 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -![logo](.github/logo.png) + + + + logo + Useful tools for developer and people working in IT. [Have a look !](https://it-tools.tech). From f5c4ab19bc10ea468a453a1460b1de2d7ccab87b Mon Sep 17 00:00:00 2001 From: sharevb Date: Fri, 20 Sep 2024 20:39:40 +0200 Subject: [PATCH 25/41] feat(new tool): Regex Tester (and Cheatsheet) (#1030) * feat(new tool): Regex Tester Fix https://github.com/CorentinTh/it-tools/issues/1007, https://github.com/CorentinTh/it-tools/issues/991, https://github.com/CorentinTh/it-tools/issues/936, https://github.com/CorentinTh/it-tools/issues/761, https://github.com/CorentinTh/it-tools/issues/649 https://github.com/CorentinTh/it-tools/issues/644, https://github.com/CorentinTh/it-tools/issues/554 https://github.com/CorentinTh/it-tools/issues/308 * fix: refactor to service + add regex diagram + ui enhancements * fix: update queryParams * fix: deps * fix: svg style bug in @regexper/render @regexper/render use a stylesheet in svg that cause bugs in whole site. So add regexper in a shadow root * feat(new tool): added Regex Cheatsheet * Update src/tools/regex-memo/index.ts * Update src/tools/regex-tester/index.ts --------- Co-authored-by: Corentin THOMASSET --- components.d.ts | 15 +- package.json | 5 +- pnpm-lock.yaml | 46 +++++ src/composable/queryParams.ts | 33 ++- src/composable/validation.ts | 19 +- src/main.ts | 2 + src/tools/index.ts | 4 + src/tools/regex-memo/index.ts | 12 ++ src/tools/regex-memo/regex-memo.content.md | 121 +++++++++++ src/tools/regex-memo/regex-memo.vue | 32 +++ src/tools/regex-tester/index.ts | 12 ++ .../regex-tester/regex-tester.service.test.ts | 106 ++++++++++ .../regex-tester/regex-tester.service.ts | 61 ++++++ src/tools/regex-tester/regex-tester.vue | 193 ++++++++++++++++++ 14 files changed, 650 insertions(+), 11 deletions(-) create mode 100644 src/tools/regex-memo/index.ts create mode 100644 src/tools/regex-memo/regex-memo.content.md create mode 100644 src/tools/regex-memo/regex-memo.vue create mode 100644 src/tools/regex-tester/index.ts create mode 100644 src/tools/regex-tester/regex-tester.service.test.ts create mode 100644 src/tools/regex-tester/regex-tester.service.ts create mode 100644 src/tools/regex-tester/regex-tester.vue diff --git a/components.d.ts b/components.d.ts index 8e59366a..3e65c3cc 100644 --- a/components.d.ts +++ b/components.d.ts @@ -130,23 +130,19 @@ declare module '@vue/runtime-core' { MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] - NButton: typeof import('naive-ui')['NButton'] - NCode: typeof import('naive-ui')['NCode'] + NCheckbox: typeof import('naive-ui')['NCheckbox'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NForm: typeof import('naive-ui')['NForm'] - NFormItem: typeof import('naive-ui')['NFormItem'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] - NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSlider: typeof import('naive-ui')['NSlider'] - NSwitch: typeof import('naive-ui')['NSwitch'] + NSpace: typeof import('naive-ui')['NSpace'] + NTable: typeof import('naive-ui')['NTable'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] @@ -156,6 +152,9 @@ declare module '@vue/runtime-core' { PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] + RegexMemo: typeof import('./src/tools/regex-memo/regex-memo.vue')['default'] + 'RegexMemo.content': typeof import('./src/tools/regex-memo/regex-memo.content.md')['default'] + RegexTester: typeof import('./src/tools/regex-tester/regex-tester.vue')['default'] ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default'] RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/package.json b/package.json index 63e5856a..f5dedc0c 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,13 @@ "dependencies": { "@it-tools/bip39": "^0.0.4", "@it-tools/oggen": "^1.3.0", + "@regexper/render": "^1.0.0", "@sindresorhus/slugify": "^2.2.1", "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", - "@types/markdown-it": "^13.0.7", "@types/figlet": "^1.5.8", + "@types/markdown-it": "^13.0.7", "@vicons/material": "^0.12.0", "@vicons/tabler": "^0.12.0", "@vueuse/core": "^10.3.0", @@ -84,6 +85,7 @@ "pinia": "^2.0.34", "plausible-tracker": "^0.3.8", "qrcode": "^1.5.1", + "randexp": "^0.5.3", "sql-formatter": "^13.0.0", "ua-parser-js": "^1.0.35", "ulid": "^2.3.0", @@ -93,6 +95,7 @@ "vue": "^3.3.4", "vue-i18n": "^9.9.1", "vue-router": "^4.1.6", + "vue-shadow-dom": "^4.2.0", "vue-tsc": "^1.8.1", "xml-formatter": "^3.3.2", "xml-js": "^1.6.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2311f3af..e43a3217 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@it-tools/oggen': specifier: ^1.3.0 version: 1.3.0 + '@regexper/render': + specifier: ^1.0.0 + version: 1.0.0 '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -152,6 +155,9 @@ dependencies: qrcode: specifier: ^1.5.1 version: 1.5.1 + randexp: + specifier: ^0.5.3 + version: 0.5.3 sql-formatter: specifier: ^13.0.0 version: 13.0.0 @@ -179,6 +185,9 @@ dependencies: vue-router: specifier: ^4.1.6 version: 4.1.6(vue@3.3.4) + vue-shadow-dom: + specifier: ^4.2.0 + version: 4.2.0 vue-tsc: specifier: ^1.8.1 version: 1.8.1(typescript@5.2.2) @@ -2480,6 +2489,17 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@regexper/parser@1.0.0: + resolution: {integrity: sha512-S8AWIGpCNdl9PNHdbhI6TpXZsPk6FDU/RTZI+6UFF4rVFqDQKjCIbZSgFu7NihoEZKq57wKFPbbT1EzrjVvPHA==} + dev: false + + /@regexper/render@1.0.0: + resolution: {integrity: sha512-xYm9RUgnhhZotTtf8UZpK1PG2CcTRXQ3JPwfTlYUZsy2J+UcTVc7BaO/MJadpMoVuT8jrIyptH4Y0HLzqhI3hQ==} + dependencies: + '@regexper/parser': 1.0.0 + '@svgdotjs/svg.js': 3.2.4 + dev: false + /@remirror/core-constants@2.0.1: resolution: {integrity: sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==} dependencies: @@ -2629,6 +2649,10 @@ packages: string.prototype.matchall: 4.0.10 dev: true + /@svgdotjs/svg.js@3.2.4: + resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==} + dev: false + /@tiptap/core@2.1.12(@tiptap/pm@2.1.6): resolution: {integrity: sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==} peerDependencies: @@ -4962,6 +4986,11 @@ packages: tslib: 2.5.0 dev: false + /drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true @@ -7758,6 +7787,14 @@ packages: ret: 0.1.15 dev: false + /randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + dependencies: + drange: 1.1.1 + ret: 0.2.2 + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -7928,6 +7965,11 @@ packages: engines: {node: '>=0.12'} dev: false + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9268,6 +9310,10 @@ packages: vue: 3.3.4 dev: false + /vue-shadow-dom@4.2.0: + resolution: {integrity: sha512-lguI064rT2HT/dxqSmXtz860KOvCq+W3nU1jMqroTmX3K1H46q22BMR4emh/Ld3ozy35XJKOaNGcr6mkJ/t/yg==} + dev: false + /vue-template-compiler@2.7.14: resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} dependencies: diff --git a/src/composable/queryParams.ts b/src/composable/queryParams.ts index 9699abbc..7cc8cc0d 100644 --- a/src/composable/queryParams.ts +++ b/src/composable/queryParams.ts @@ -1,7 +1,8 @@ import { useRouteQuery } from '@vueuse/router'; import { computed } from 'vue'; +import { useStorage } from '@vueuse/core'; -export { useQueryParam }; +export { useQueryParam, useQueryParamOrStorage }; const transformers = { number: { @@ -16,6 +17,12 @@ const transformers = { fromQuery: (value: string) => value.toLowerCase() === 'true', toQuery: (value: boolean) => (value ? 'true' : 'false'), }, + object: { + fromQuery: (value: string) => { + return JSON.parse(value); + }, + toQuery: (value: object) => JSON.stringify(value), + }, }; function useQueryParam({ name, defaultValue }: { name: string; defaultValue: T }) { @@ -33,3 +40,27 @@ function useQueryParam({ name, defaultValue }: { name: string; defaultValue: }, }); } + +function useQueryParamOrStorage({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) { + const type = typeof defaultValue; + const transformer = transformers[type as keyof typeof transformers] ?? transformers.string; + + const storageRef = useStorage(storageName, defaultValue); + const proxyDefaultValue = transformer.toQuery(defaultValue as never); + const proxy = useRouteQuery(name, proxyDefaultValue); + + const r = ref(defaultValue); + + watch(r, + (value) => { + proxy.value = transformer.toQuery(value as never); + storageRef.value = value as never; + }, + { deep: true }); + + r.value = (proxy.value && proxy.value !== proxyDefaultValue + ? transformer.fromQuery(proxy.value) as unknown as T + : storageRef.value as T) as never; + + return r; +} diff --git a/src/composable/validation.ts b/src/composable/validation.ts index 472ca4b2..33a6a7b2 100644 --- a/src/composable/validation.ts +++ b/src/composable/validation.ts @@ -3,9 +3,11 @@ import _ from 'lodash'; import { type Ref, reactive, watch } from 'vue'; type ValidatorReturnType = unknown; +type GetErrorMessageReturnType = string; export interface UseValidationRule { validator: (value: T) => ValidatorReturnType + getErrorMessage?: (value: T) => GetErrorMessageReturnType message: string } @@ -24,6 +26,15 @@ export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { } } +export function getErrorMessageOrThrown(cb: () => GetErrorMessageReturnType): string { + try { + return cb() || ''; + } + catch (e: any) { + return e.toString(); + } +} + export interface ValidationAttrs { feedback: string validationStatus: string | undefined @@ -61,7 +72,13 @@ export function useValidation({ for (const rule of get(rules)) { if (isFalsyOrHasThrown(() => rule.validator(source.value))) { - state.message = rule.message; + if (rule.getErrorMessage) { + const getErrorMessage = rule.getErrorMessage; + state.message = rule.message.replace('{0}', getErrorMessageOrThrown(() => getErrorMessage(source.value))); + } + else { + state.message = rule.message; + } state.status = 'error'; } } diff --git a/src/main.ts b/src/main.ts index 36ba3b7f..19d28bf2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { createPinia } from 'pinia'; import { createHead } from '@vueuse/head'; import { registerSW } from 'virtual:pwa-register'; +import shadow from 'vue-shadow-dom'; import { plausible } from './plugins/plausible.plugin'; import 'virtual:uno.css'; @@ -23,5 +24,6 @@ app.use(i18nPlugin); app.use(router); app.use(naive); app.use(plausible); +app.use(shadow); app.mount('#app'); diff --git a/src/tools/index.ts b/src/tools/index.ts index c9003fe8..388cfaf4 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -9,6 +9,8 @@ import { tool as textToUnicode } from './text-to-unicode'; import { tool as safelinkDecoder } from './safelink-decoder'; import { tool as xmlToJson } from './xml-to-json'; import { tool as jsonToXml } from './json-to-xml'; +import { tool as regexTester } from './regex-tester'; +import { tool as regexMemo } from './regex-memo'; import { tool as markdownToHtml } from './markdown-to-html'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; @@ -156,6 +158,8 @@ export const toolsByCategory: ToolCategory[] = [ xmlFormatter, yamlViewer, emailNormalizer, + regexTester, + regexMemo, ], }, { diff --git a/src/tools/regex-memo/index.ts b/src/tools/regex-memo/index.ts new file mode 100644 index 00000000..f1f56489 --- /dev/null +++ b/src/tools/regex-memo/index.ts @@ -0,0 +1,12 @@ +import { BrandJavascript } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Regex cheatsheet', + path: '/regex-memo', + description: 'Javascript Regex/Regular Expression cheatsheet', + keywords: ['regex', 'regular', 'expression', 'javascript', 'memo', 'cheatsheet'], + component: () => import('./regex-memo.vue'), + icon: BrandJavascript, + createdAt: new Date('2024-09-20'), +}); diff --git a/src/tools/regex-memo/regex-memo.content.md b/src/tools/regex-memo/regex-memo.content.md new file mode 100644 index 00000000..0f779401 --- /dev/null +++ b/src/tools/regex-memo/regex-memo.content.md @@ -0,0 +1,121 @@ +### Normal characters + +Expression | Description +:--|:-- +`.` or `[^\n\r]` | any character *excluding* a newline or carriage return +`[A-Za-z]` | alphabet +`[a-z]` | lowercase alphabet +`[A-Z]` | uppercase alphabet +`\d` or `[0-9]` | digit +`\D` or `[^0-9]` | non-digit +`_` | underscore +`\w` or `[A-Za-z0-9_]` | alphabet, digit or underscore +`\W` or `[^A-Za-z0-9_]` | inverse of `\w` +`\S` | inverse of `\s` + +### Whitespace characters + +Expression | Description +:--|:-- +` ` | space +`\t` | tab +`\n` | newline +`\r` | carriage return +`\s` | space, tab, newline or carriage return + +### Character set + +Expression | Description +:--|:-- +`[xyz]` | either `x`, `y` or `z` +`[^xyz]` | neither `x`, `y` nor `z` +`[1-3]` | either `1`, `2` or `3` +`[^1-3]` | neither `1`, `2` nor `3` + +- Think of a character set as an `OR` operation on the single characters that are enclosed between the square brackets. +- Use `^` after the opening `[` to “negate” the character set. +- Within a character set, `.` means a literal period. + +### Characters that require escaping + +#### Outside a character set + +Expression | Description +:--|:-- +`\.` | period +`\^` | caret +`\$` | dollar sign +`\|` | pipe +`\\` | back slash +`\/` | forward slash +`\(` | opening bracket +`\)` | closing bracket +`\[` | opening square bracket +`\]` | closing square bracket +`\{` | opening curly bracket +`\}` | closing curly bracket + +#### Inside a character set + +Expression | Description +:--|:-- +`\\` | back slash +`\]` | closing square bracket + +- A `^` must be escaped only if it occurs immediately after the opening `[` of the character set. +- A `-` must be escaped only if it occurs between two alphabets or two digits. + +### Quantifiers + +Expression | Description +:--|:-- +`{2}` | exactly 2 +`{2,}` | at least 2 +`{2,7}` | at least 2 but no more than 7 +`*` | 0 or more +`+` | 1 or more +`?` | exactly 0 or 1 + +- The quantifier goes *after* the expression to be quantified. + +### Boundaries + +Expression | Description +:--|:-- +`^` | start of string +`$` | end of string +`\b` | word boundary + +- How word boundary matching works: + - At the beginning of the string if the first character is `\w`. + - Between two adjacent characters within the string, if the first character is `\w` and the second character is `\W`. + - At the end of the string if the last character is `\w`. + +### Matching + +Expression | Description +:--|:-- +`foo\|bar` | match either `foo` or `bar` +`foo(?=bar)` | match `foo` if it’s before `bar` +`foo(?!bar)` | match `foo` if it’s *not* before `bar` +`(?<=bar)foo` | match `foo` if it’s after `bar` +`(? +import { useThemeVars } from 'naive-ui'; +import Memo from './regex-memo.content.md'; + +const themeVars = useThemeVars(); + + + + + diff --git a/src/tools/regex-tester/index.ts b/src/tools/regex-tester/index.ts new file mode 100644 index 00000000..9cc542f0 --- /dev/null +++ b/src/tools/regex-tester/index.ts @@ -0,0 +1,12 @@ +import { Language } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Regex Tester', + path: '/regex-tester', + description: 'Regex Tester', + keywords: ['regex', 'tester', 'sample', 'expression'], + component: () => import('./regex-tester.vue'), + icon: Language, + createdAt: new Date('2024-09-20'), +}); diff --git a/src/tools/regex-tester/regex-tester.service.test.ts b/src/tools/regex-tester/regex-tester.service.test.ts new file mode 100644 index 00000000..bd4efbbc --- /dev/null +++ b/src/tools/regex-tester/regex-tester.service.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; +import { matchRegex } from './regex-tester.service'; + +const regexesData = [ + { + regex: '', + text: '', + flags: '', + result: [], + }, + { + regex: '.*', + text: '', + flags: '', + result: [], + }, + { + regex: '', + text: 'aaa', + flags: '', + result: [], + }, + { + regex: 'a', + text: 'baaa', + flags: '', + result: [ + { + captures: [], + groups: [], + index: 1, + value: 'a', + }, + ], + }, + { + regex: '(.)(?r)', + text: 'azertyr', + flags: 'g', + result: [ + { + captures: [ + { + end: 3, + name: '1', + start: 2, + value: 'e', + }, + { + end: 4, + name: '2', + start: 3, + value: 'r', + }, + ], + groups: [ + { + end: 4, + name: 'g', + start: 3, + value: 'r', + }, + ], + index: 2, + value: 'er', + }, + { + captures: [ + { + end: 6, + name: '1', + start: 5, + value: 'y', + }, + { + end: 7, + name: '2', + start: 6, + value: 'r', + }, + ], + groups: [ + { + end: 7, + name: 'g', + start: 6, + value: 'r', + }, + ], + index: 5, + value: 'yr', + }, + ], + }, +]; + +describe('regex-tester', () => { + for (const reg of regexesData) { + const { regex, text, flags, result: expected_result } = reg; + it(`Should matchRegex("${regex}","${text}","${flags}") return correct result`, async () => { + const result = matchRegex(regex, text, `${flags}d`); + + expect(result).to.deep.equal(expected_result); + }); + } +}); diff --git a/src/tools/regex-tester/regex-tester.service.ts b/src/tools/regex-tester/regex-tester.service.ts new file mode 100644 index 00000000..ec8682c5 --- /dev/null +++ b/src/tools/regex-tester/regex-tester.service.ts @@ -0,0 +1,61 @@ +interface RegExpGroupIndices { + [name: string]: [number, number] +} +interface RegExpIndices extends Array<[number, number]> { + groups: RegExpGroupIndices +} +interface RegExpExecArrayWithIndices extends RegExpExecArray { + indices: RegExpIndices +} +interface GroupCapture { + name: string + value: string + start: number + end: number +}; + +export function matchRegex(regex: string, text: string, flags: string) { + // if (regex === '' || text === '') { + // return []; + // } + + let lastIndex = -1; + const re = new RegExp(regex, flags); + const results = []; + let match = re.exec(text) as RegExpExecArrayWithIndices; + while (match !== null) { + if (re.lastIndex === lastIndex || match[0] === '') { + break; + } + const indices = match.indices; + const captures: Array = []; + Object.entries(match).forEach(([captureName, captureValue]) => { + if (captureName !== '0' && captureName.match(/\d+/)) { + captures.push({ + name: captureName, + value: captureValue, + start: indices[Number(captureName)][0], + end: indices[Number(captureName)][1], + }); + } + }); + const groups: Array = []; + Object.entries(match.groups || {}).forEach(([groupName, groupValue]) => { + groups.push({ + name: groupName, + value: groupValue, + start: indices.groups[groupName][0], + end: indices.groups[groupName][1], + }); + }); + results.push({ + index: match.index, + value: match[0], + captures, + groups, + }); + lastIndex = re.lastIndex; + match = re.exec(text) as RegExpExecArrayWithIndices; + } + return results; +} diff --git a/src/tools/regex-tester/regex-tester.vue b/src/tools/regex-tester/regex-tester.vue new file mode 100644 index 00000000..a1fa7958 --- /dev/null +++ b/src/tools/regex-tester/regex-tester.vue @@ -0,0 +1,193 @@ + + + From 72517002f39d5c5ab21d8473c2fcc04f12818069 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 27 Sep 2024 10:40:56 +0200 Subject: [PATCH 26/41] refactor(regex-tester): better description --- src/tools/regex-tester/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/regex-tester/index.ts b/src/tools/regex-tester/index.ts index 9cc542f0..62a5e234 100644 --- a/src/tools/regex-tester/index.ts +++ b/src/tools/regex-tester/index.ts @@ -4,7 +4,7 @@ import { defineTool } from '../tool'; export const tool = defineTool({ name: 'Regex Tester', path: '/regex-tester', - description: 'Regex Tester', + description: 'Test your regular expressions with sample text.', keywords: ['regex', 'tester', 'sample', 'expression'], component: () => import('./regex-tester.vue'), icon: Language, From 1c35ac37043f05bd9d0c7a026dd67d82aa026796 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Fri, 27 Sep 2024 14:49:11 +0200 Subject: [PATCH 27/41] docs(author): updated author links (#1316) --- README.md | 2 +- locales/de.yml | 2 +- locales/en.yml | 2 +- locales/es.yml | 6 +++--- locales/fr.yml | 2 +- locales/pt.yml | 2 +- locales/uk.yml | 2 +- locales/vi.yml | 2 +- locales/zh.yml | 2 +- package.json | 16 ++++++++-------- src/layouts/base.layout.vue | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b9726864..93c7aa73 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Big thanks to all the people who have already contributed! ## Credits -Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr). +Coded with ❤️ by [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=readme). This project is continuously deployed using [vercel.com](https://vercel.com). diff --git a/locales/de.yml b/locales/de.yml index 5a47c85d..e59ca114 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -38,7 +38,7 @@ about: # Über IT-Tools Diese wunderbare Website, erstellt mit ❤ von [Corentin - Thomasset](https://github.com/CorentinTh), sammelt nützliche Tools für + Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about), sammelt nützliche Tools für Entwickler und Menschen, die in der IT arbeiten. Wenn du sie nützlich findest, teile sie gerne mit Personen, von denen du denkst, dass sie sie ebenfalls nützlich finden könnten, und vergiss nicht, sie in deiner diff --git a/locales/en.yml b/locales/en.yml index d09d435a..d1cd21c4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -30,7 +30,7 @@ about: content: > # About IT-Tools - This wonderful website, made with ❤ by [Corentin Thomasset](https://github.com/CorentinTh) , aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar! + This wonderful website, made with ❤ by [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) , aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar! IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by [sponsoring me](https://www.buymeacoffee.com/cthmsst). diff --git a/locales/es.yml b/locales/es.yml index b87502fc..2d8b2515 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -30,7 +30,7 @@ about: content: > # Sobre IT-Tools - Este maravilloso sitio web, hecho con ❤ por [Corentin Thomasset](https://github.com/CorentinTh) , agrega herramientas útiles para desarrolladores y personas que trabajan en IT. Si lo encuentra útil, no dude en compartirlo con las personas que crea que también pueden encontrarlo útil y ¡no olvide marcarlo como favorito en su barra de accesos directos! + Este maravilloso sitio web, hecho con ❤ por [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) , agrega herramientas útiles para desarrolladores y personas que trabajan en IT. Si lo encuentra útil, no dude en compartirlo con las personas que crea que también pueden encontrarlo útil y ¡no olvide marcarlo como favorito en su barra de accesos directos! IT Tools es de código abierto (under the MIT license) y gratis, y siempre lo será, pero me cuesta dinero alojar y renovar el nombre de dominio. Si desea apoyar mi trabajo y animarme a agregar más herramientas, considere apoyarme a través de[sponsoring me](https://www.buymeacoffee.com/cthmsst). @@ -39,7 +39,7 @@ about: IT Tools está creado en Vue.js (Vue 3) con la biblioteca de componentes Naive UI y Vercel lo aloja y lo implementa continuamente. En algunas herramientas se utilizan bibliotecas de código abierto de terceros; puede encontrar la lista completa en [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) archivo del repositorio. ## ¿Encontraste un error? ¿Falta una herramienta? - + Si necesita una herramienta que actualmente no está presente aquí y cree que puede ser útil, puede enviar una solicitud de función en el [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) en el repositorio de GitHub. Y si encontró un error o algo no funciona como se esperaba, presente un reporte de error en el [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) en el repositorio de GitHub. @@ -68,4 +68,4 @@ tools: math: Math measurement: Measurement text: Text - data: Data \ No newline at end of file + data: Data diff --git a/locales/fr.yml b/locales/fr.yml index 35c7df2e..cd5cc0e8 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -29,7 +29,7 @@ about: content: > # À propos de IT-Tools - Ce merveilleux site, fait avec ❤ par [Corentin Thomasset](https://github.com/CorentinTh), regroupe des outils utiles pour les développeurs et les personnes travaillant dans l'informatique. Si vous le trouvez utile, n'hésitez pas à le partager et n'oubliez pas de le mettre dans vos favoris ! + Ce merveilleux site, fait avec ❤ par [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about), regroupe des outils utiles pour les développeurs et les personnes travaillant dans l'informatique. Si vous le trouvez utile, n'hésitez pas à le partager et n'oubliez pas de le mettre dans vos favoris ! IT Tools est open-source (sous licence MIT) et gratuit, et le restera toujours, mais cela me coûte de l'argent pour l'héberger et renouveler le nom de domaine. Si vous voulez soutenir mon travail, et m'encourager à ajouter plus d'outils, n'hésitez pas à me [soutenir](https://www.buymeacoffee.com/cthmsst). diff --git a/locales/pt.yml b/locales/pt.yml index 96fdaed4..822bea8a 100644 --- a/locales/pt.yml +++ b/locales/pt.yml @@ -30,7 +30,7 @@ about: content: > # Sobre o IT-Tools - Este site maravilhoso, feito com ❤ por [Corentin Thomasset](https://github.com/CorentinTh), junta ferramentas úteis para desenvolvedores e outras pessoas que trabalham com TI. Se você achar o site útil, fique à vontade para compartilhar com quem também possa gostar e não esqueça de salvar o bookmark na sua barra de atalhos! + Este site maravilhoso, feito com ❤ por [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about), junta ferramentas úteis para desenvolvedores e outras pessoas que trabalham com TI. Se você achar o site útil, fique à vontade para compartilhar com quem também possa gostar e não esqueça de salvar o bookmark na sua barra de atalhos! O IT Tools é código aberto (sob a licença MIT), é gratuito, e sempre será, mas custa dinheiro para hospedar e renovar o domínio. Se quiser apoiar meu trabalho e me encorajar a adicionar mais ferramentas, por favor considere [ser patrocinador](https://www.buymeacoffee.com/cthmsst). diff --git a/locales/uk.yml b/locales/uk.yml index 2d28f157..ad455a7d 100644 --- a/locales/uk.yml +++ b/locales/uk.yml @@ -30,7 +30,7 @@ about: content: > # Про IT-Tools - Цей чудовий вебсайт, створений з ❤ [Corentin Thomasset](https://github.com/CorentinTh), агрегує корисні інструменти для розробників і людей, які працюють в сфері IT. Якщо вам це корисно, будь ласка, поділіться цим з людьми, які, на вашу думку, також можуть знайти його корисним, і не забудьте додати його до закладок у вашій панелі швидкого доступу! + Цей чудовий вебсайт, створений з ❤ [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about), агрегує корисні інструменти для розробників і людей, які працюють в сфері IT. Якщо вам це корисно, будь ласка, поділіться цим з людьми, які, на вашу думку, також можуть знайти його корисним, і не забудьте додати його до закладок у вашій панелі швидкого доступу! IT Tools є відкритим програмним забезпеченням (під ліцензією MIT) і безкоштовним, і завжди буде таким, але мені коштує гроші для хостингу і продовження доменного імені. Якщо ви хочете підтримати мою роботу і підтримати мене у додаванні нових інструментів, розгляньте можливість підтримки, [спонсоруючи мене](https://www.buymeacoffee.com/cthmsst). diff --git a/locales/vi.yml b/locales/vi.yml index 9eb16bf0..de574a6c 100644 --- a/locales/vi.yml +++ b/locales/vi.yml @@ -30,7 +30,7 @@ about: content: > # Về IT-Tools - Website tuyệt vời này, được tạo ra bằng ❤ bởi [Corentin Thomasset](https://github.com/CorentinTh), tổng hợp các công cụ hữu ích cho nhà phát triển và những người làm việc trong lĩnh vực IT. Nếu bạn thấy nó hữu ích, xin đừng ngần ngại chia sẻ cho những người mà bạn nghĩ sẽ thấy nó hữu ích và đừng quên đánh dấu nó trong thanh lối tắt của bạn! + Website tuyệt vời này, được tạo ra bằng ❤ bởi [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about), tổng hợp các công cụ hữu ích cho nhà phát triển và những người làm việc trong lĩnh vực IT. Nếu bạn thấy nó hữu ích, xin đừng ngần ngại chia sẻ cho những người mà bạn nghĩ sẽ thấy nó hữu ích và đừng quên đánh dấu nó trong thanh lối tắt của bạn! IT Tools là mã nguồn mở (dưới giấy phép MIT) và miễn phí, và sẽ luôn như vậy, nhưng tôi phải trả tiền để lưu trữ và gia hạn tên miền. Nếu bạn muốn hỗ trợ công việc của tôi, và khích lệ tôi thêm nhiều công cụ hơn, hãy xem xét hỗ trợ bằng cách [tài trợ cho tôi](https://www.buymeacoffee.com/cthmsst). diff --git a/locales/zh.yml b/locales/zh.yml index 160fe1fa..9b065682 100644 --- a/locales/zh.yml +++ b/locales/zh.yml @@ -30,7 +30,7 @@ about: content: > # 关于 IT-Tools - IT-Tools 由 [Corentin Thomasset](https://github.com/CorentinTh) 用 ❤ 开发,汇集了对开发人员和 IT 从业者有用的工具。如果对您有帮助,请将其分享给您的朋友,并且添加到收藏夹中! + IT-Tools 由 [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) 用 ❤ 开发,汇集了对开发人员和 IT 从业者有用的工具。如果对您有帮助,请将其分享给您的朋友,并且添加到收藏夹中! IT-Tools 永久免费且开源(MIT 许可证),但需要资金用于托管和续订域名。如果您想支持我的工作,并鼓励我添加更多工具,请考虑通过 [赞助我](https://www.buymeacoffee.com/cthmsst) 进行支持。 diff --git a/package.json b/package.json index f5dedc0c..8a1c069f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,14 @@ { "name": "it-tools", "version": "2024.5.13-a0bc346", + "packageManager": "pnpm@8.15.3", "description": "Collection of handy online tools for developers, with great UX. ", + "author": "Corentin Th (https://corentin.tech)", + "license": "GNU GPLv3", + "repository": { + "type": "git", + "url": "https://github.com/CorentinTh/it-tools" + }, "keywords": [ "productivity", "converter", @@ -13,12 +20,6 @@ "developer-tools", "developer-productivity" ], - "author": "Corentin Th (https://github.com/CorentinTh)", - "license": "GNU GPLv3", - "repository": { - "type": "git", - "url": "https://github.com/CorentinTh/it-tools" - }, "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build", @@ -145,6 +146,5 @@ "vitest": "^0.34.0", "workbox-window": "^7.0.0", "zx": "^7.2.1" - }, - "packageManager": "pnpm@8.15.3" + } } diff --git a/src/layouts/base.layout.vue b/src/layouts/base.layout.vue index 62ac5113..ef534209 100644 --- a/src/layouts/base.layout.vue +++ b/src/layouts/base.layout.vue @@ -81,7 +81,7 @@ const tools = computed(() => [
© {{ new Date().getFullYear() }} - + Corentin Thomasset
From f962c416a33e1481fd7d1f4eca927bbf5810cde7 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Thu, 3 Oct 2024 00:01:09 +0200 Subject: [PATCH 28/41] chore(sponsors): fern sponsor banners (#1314) * chore(sponsors): readme banner * chore(sponsors): app sponsor --- .github/fern-banner.svg | 11 +++++++++++ README.md | 4 ++++ src/config.ts | 6 ++++++ src/pages/Home.page.vue | 30 ++++++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .github/fern-banner.svg diff --git a/.github/fern-banner.svg b/.github/fern-banner.svg new file mode 100644 index 00000000..20b918f7 --- /dev/null +++ b/.github/fern-banner.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/README.md b/README.md index 93c7aa73..dfe7f3d4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Useful tools for developer and people working in IT. [Have a look !](https://it-tools.tech). +## Sponsors + +[![Fern banner](./.github/fern-banner.svg)](https://bit.ly/3zBl7DG) + ## Functionalities and roadmap Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented. diff --git a/src/config.ts b/src/config.ts index 2558d038..fa2421ef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -59,6 +59,12 @@ export const config = figue({ default: false, env: 'VITE_SHOW_BANNER', }, + showSponsorBanner: { + doc: 'Show the sponsor banner', + format: 'boolean', + default: false, + env: 'VITE_SHOW_SPONSOR_BANNER', + }, }) .loadEnv({ ...import.meta.env, diff --git a/src/pages/Home.page.vue b/src/pages/Home.page.vue index dfd12b60..190601c1 100644 --- a/src/pages/Home.page.vue +++ b/src/pages/Home.page.vue @@ -15,8 +15,8 @@ const { t } = useI18n();