├── .firebaserc ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── Assets │ ├── Logo.svg │ └── changelog.md ├── CLI │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── utils.js ├── Components │ ├── App.js │ ├── BeautifyButton.js │ ├── Changelog.js │ ├── CopyCSV.js │ ├── CopyJSON.js │ ├── Csv.js │ ├── DownloadCSV.js │ ├── DownloadJSON.js │ ├── Footer.js │ ├── Header.js │ ├── IndentSelector.js │ ├── Json.js │ ├── Menu.js │ ├── MultipleDeletions.js │ ├── MultipleMerge.js │ ├── MultipleReplaces.js │ ├── RandomButton.js │ ├── Seo.js │ ├── TextareaIndent.js │ ├── TransformToCSV.js │ ├── TransformToJSON.js │ ├── UglifyButton.js │ ├── UploadCSV.js │ ├── UploadFiles.js │ └── UploadJSON.js ├── Functions │ ├── Firebase.js │ ├── useUndoableState.js │ └── useWindowDimensions.js ├── Styles │ └── index.css ├── Tests │ └── App.test.js └── index.js └── yarn.lock /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "jsonmatic-pro" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: erikmartinjordan 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to deploy functions to Firebase 2 | 3 | name: deployToFirebase 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "build" 14 | build: 15 | # The type of runner that the job will run on 16 | runs-on: macos-latest 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2.1.2 23 | with: 24 | node-version: '12' 25 | 26 | # Install npm 27 | - name: Installing npm 28 | run: npm install 29 | 30 | # Build the project 31 | - name: Build project 32 | run: npm run build 33 | env: 34 | CI: "" 35 | 36 | # Running tests 37 | - name: Running tests 38 | run: npm run test 39 | 40 | # Firebase deploy 41 | - name: Firebase deploy 42 | run: npm install -g firebase-tools && firebase deploy --token ${{ secrets.FIREBASE_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .firebase/hosting.YnVpbGQ.cache 25 | 26 | # CLI node-modules 27 | src/CLI/node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Erik Martín Jordán 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 |

8 | 9 | The easiest way to transform a CSV into a JSON and vice versa. You can try the [webapp](https://jsonmatic-pro.web.app/) without installing any package. The app runs in your browser; **data isn't uploaded to any server**. 10 | 11 |

12 | 13 |

14 | 15 | ## Features 16 | 17 | * Transform a CSV into a JSON (and vice versa) 18 | * Beautify/uglify the JSON 19 | * Configure indentation 20 | * Replace/delete a property in multiple JSON files at once 21 | * Merge JSON files 22 | 23 | ## Install 24 | 25 | You can also install and use the app through the CLI: 26 | 27 | ``` 28 | npm i jsonmatic -g 29 | ``` 30 | 31 | ## Use 32 | 33 | Transform a CSV into JSON and vice versa: 34 | 35 | ``` 36 | jsonmatic transform 37 | ``` 38 | 39 | Replace a property in multiple JSON files at once: 40 | 41 | ``` 42 | jsonmatic replace [options] 43 | ``` 44 | 45 | Merge multiple JSON files: 46 | 47 | ``` 48 | jsonmatic merge 49 | ``` 50 | 51 | ## Note 52 | 53 | The first column is reserved for unique keys. Use dot notation to create properties and subproperties. 54 | 55 | ## Author 56 | 57 | [Erik Martín Jordán](https://erikmartinjordan.com) 58 | 59 | ## License 60 | 61 | This project is open source and available under the MIT License. 62 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spreadsheet-json", 3 | "version": "0.2.4", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@primer/octicons-react": "^12.1.0", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "dreamjs": "^0.2.0", 12 | "file-saver": "^2.0.5", 13 | "firebase": "^8.3.1", 14 | "moment": "^2.29.1", 15 | "react": "^17.0.1", 16 | "react-data-grid": "^7.0.0-canary.36", 17 | "react-dom": "^17.0.1", 18 | "react-markdown": "^6.0.1", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "4.0.2", 21 | "web-vitals": "^1.0.1" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": {} 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikmartinjordan/jsonmatic/7b28f776be943093762721f2daa15c91f41b9940/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | jsonmatic 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "jsonmatic", 3 | "name": "Transform a CSV into a JSON", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Assets/Logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/changelog.md: -------------------------------------------------------------------------------- 1 | ## 18.06.21 - Replace with the CLI 2 | 3 | * You can replace a property in multiple JSON files using the CLI 4 | 5 | ## 17.06.21 – CLI 6 | 7 | ![CLI screenshot](https://github.com/erikmartinjordan/Screenshots/blob/master/Captura%20de%20pantalla%202021-06-17%20a%20las%2011.27.29.png?raw=true) 8 | 9 | * CLI creation 10 | * Transform a CSV into a JSON and vice versa 11 | * Merge multiple JSONs 12 | 13 | 14 | ## 07.06.21 – Project restructuration 15 | 16 | ![Fixed screen issues mobile devices](https://github.com/erikmartinjordan/Screenshots/blob/master/Jun-07-2021%2012-42-31.gif?raw=true) 17 | 18 | * Restructured project in Functions, Components, Assets, Styles 19 | * Changed icon of transform buttons depending on screen resolution 20 | * Fixed screen issues on mobile devices 21 | 22 | ## 04.06.21 – New copy/download options and bug fixing 23 | 24 | * Fixed splitting on reading a CSV 25 | * Updated textarea font to `monospace` as suggested on HN 26 | * Removed `fingerprintjs` and analytics component 27 | * Added CSV copy/download options 28 | * Replaced dot separator with `__separator__` on TransformToCSV component 29 | * Autofocus on table cell 30 | 31 | ## 03.06.21 – Upload CSV 32 | 33 | * Option to upload CSV instead of just copy/paste 34 | * Fixed textarea on mobile devices 35 | 36 | ## 01.06.21 – Randomize JSON 37 | 38 | * Create a dummy JSON 39 | 40 | ## 24.05.21 – Keyboard shortcut to upload files 41 | 42 | * Added keyboard control to upload files 43 | * Added dummy CSV animation when transforming JSON 44 | * Deleted '+' sign on keyboard shortcuts hint because it can create confusion 45 | * Added animation to keyboard hint on 'Upload' component 46 | 47 | ## 21.05.21 — Resize table 48 | 49 | ![Resize table](https://github.com/erikmartinjordan/Screenshots/blob/master/May-21-2021%2011-31-56.gif?raw=true) 50 | 51 | * Resizable table 52 | * Autoresize on double click 53 | * Disabled "Generate CSV" button while editing JSON 54 | 55 | ## 20.05.21 — Transform JSON into CSV 56 | 57 | * Transform JSON into CSV 58 | 59 | ## 12.05.21 — JSON edition 60 | 61 | ![JSON edition](https://github.com/erikmartinjordan/Screenshots/blob/master/May-12-2021%2013-05-23.gif?raw=true) 62 | 63 | * Edit JSON textarea 64 | * Buttons are disabled while JSON edition 65 | * Fixed bug: listener of keyboard active when some table cell is selected 66 | 67 | ## 05.05.21 — Multiple JSON merge 68 | 69 | ![Merge JSON files](https://github.com/erikmartinjordan/Screenshots/blob/master/May-05-2021%2012-47-05.gif?raw=true) 70 | 71 | * Merge multiple JSON files 72 | * Select merge order (priority) 73 | * Download merged file 74 | 75 | ## 02.05.21 — Multiple JSON deletion 76 | 77 | * Delete several properties of multiple JSON files separating them with commas: `prop1.subprop1`, `prop2.subprop2`, etc. 78 | * Delete a single property using rules (*equal to*, *greater than*, and *lesser than*) 79 | * Displaying number of deletions 80 | 81 | ## 30.04.21 — Rules 82 | 83 | * Replace fields if a property is greater/lesser than a certain value 84 | * Added tilt effect on hover buttons 💅 85 | * Added file name as title into JSON box 86 | * Displaying number of replaces 87 | 88 | ## 29.04.21 — Multiple JSON replacement 89 | 90 | ![Multiple JSON replacement](https://github.com/erikmartinjordan/Screenshots/blob/master/May-05-2021%2012-46-00.gif?raw=true) 91 | 92 | * Added a component to replace a property in multiple JSON files 93 | * Replace/download 94 | 95 | ## 27.04.21 — Changelog 96 | 97 | * Added changelog 98 | 99 | ## 17.03.21 — Launch 🚀 100 | 101 | Transform a CSV into a JSON automatically: 102 | 103 | * Copy a CSV and paste it 104 | * Add/delete columns & rows 105 | * Undo/redo options 106 | * Keyboard shortcuts 107 | * Download the JSON as file or copy it 108 | * Beautify/uglify the JSON 109 | * 2-space/4-space indentation -------------------------------------------------------------------------------- /src/CLI/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import chalk from 'chalk'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import { program } from 'commander'; 6 | import * as utils from './utils.js'; 7 | 8 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | // Defining the program 10 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | let { version } = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url))); 12 | 13 | program 14 | .version(version) 15 | 16 | program 17 | .option('-h, --help') 18 | .action(options => { 19 | 20 | console.log(chalk.cyan(``)); 21 | console.log(chalk.cyan(` ██╗███████╗ ██████╗ ███╗ ██╗███╗ ███╗ █████╗ ████████╗██╗ ██████╗`)); 22 | console.log(chalk.cyan(` ██║██╔════╝██╔═══██╗████╗ ██║████╗ ████║██╔══██╗╚══██╔══╝██║██╔════╝`)); 23 | console.log(chalk.cyan(` ██║███████╗██║ ██║██╔██╗ ██║██╔████╔██║███████║ ██║ ██║██║ `)); 24 | console.log(chalk.cyan(`██ ██║╚════██║██║ ██║██║╚██╗██║██║╚██╔╝██║██╔══██║ ██║ ██║██║`)); 25 | console.log(chalk.cyan(`╚█████╔╝███████║╚██████╔╝██║ ╚████║██║ ╚═╝ ██║██║ ██║ ██║ ██║╚██████╗`)); 26 | console.log(chalk.cyan(` ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝`)); 27 | console.log(chalk.cyan(`⚗️ https://github.com/erikmartinjordan/jsonmatic`)); 28 | console.log(chalk.cyan(``)); 29 | console.log(program.helpInformation()); 30 | 31 | }) 32 | 33 | program 34 | .command('transform') 35 | .arguments(' ') 36 | .description('✨ transform a CSV into a JSON or vice versa') 37 | .action((source, destination) => { 38 | 39 | console.log('Transforming...'); 40 | if (source.endsWith('csv') && destination.endsWith('json')) generateJSON(source, destination); 41 | else if(source.endsWith('json') && destination.endsWith('csv')) generateCSV(source, destination); 42 | else console.log(chalk.red('❌ The files are not valid.')); 43 | 44 | }); 45 | 46 | program 47 | .command('replace') 48 | .arguments(' ') 49 | .option('-g, --greater', 'is greater than') 50 | .option('-e, --equal', 'is equal to') 51 | .option('-l, --lesser', 'is less than') 52 | .description('↔️ replace a property in multiple JSON files') 53 | .action((property, currentValue, replaceValue, files, options) => { 54 | 55 | console.log('Replacing...'); 56 | if (Object.keys(options).length !== 1) console.log(chalk.red('❌ You must select only one option. Type jsonmatic --help to get more info.')); 57 | else if(files.some(file => !file.endsWith('json'))) console.log(chalk.red('❌ The files are not valid.')); 58 | else generateReplace(property, currentValue, replaceValue, files, Object.keys(options)[0]); 59 | 60 | 61 | }); 62 | 63 | program 64 | .command('merge') 65 | .arguments('') 66 | .description('➕ merge multiple JSON files into one unique file') 67 | .action(files => { 68 | 69 | console.log('Generating files...'); 70 | if (files.every(file => file.endsWith('json'))) generateMerge(files); 71 | else console.log(chalk.red('❌ The files are not valid.')); 72 | 73 | }); 74 | 75 | program 76 | .parse(); 77 | 78 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | // Defining the JSON generator 80 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | function generateJSON(source, destination){ 82 | 83 | try{ 84 | 85 | let rawdata = fs.readFileSync(source); 86 | let csv = rawdata.toString().split(/\r\n|\n|\r/).map(line => line.split(',')); 87 | 88 | utils.validateCSV(csv); 89 | 90 | let json = utils.transformToJSON(csv); 91 | 92 | fs.writeFileSync(destination, JSON.stringify(json, null, 2)); 93 | 94 | console.log(chalk.green(`✅ ${source} was transformed into ${destination}`)); 95 | 96 | } 97 | catch(e){ 98 | 99 | console.log(chalk.red(`❌ ${e}`)); 100 | process.exit(1); 101 | 102 | } 103 | 104 | } 105 | 106 | 107 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 108 | // Defining the CSV generator 109 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | function generateCSV(source, destination){ 111 | 112 | try{ 113 | 114 | let rawdata = fs.readFileSync(source); 115 | let json = JSON.parse(rawdata); 116 | 117 | utils.validateJSON(json); 118 | 119 | let csv = utils.transformToCSV(json); 120 | 121 | fs.writeFileSync(destination, csv.map(row => row.join(',')).join('\n')); 122 | 123 | console.log(chalk.green(`✅ ${source} was transformed into ${destination}`)); 124 | 125 | } 126 | catch(e){ 127 | 128 | console.log(chalk.red(`❌ ${e}`)); 129 | process.exit(1); 130 | 131 | } 132 | 133 | } 134 | 135 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 136 | // Defining the replace generator 137 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | function generateReplace(property, currentValue, replaceValue, files, operation){ 139 | 140 | try{ 141 | 142 | let jsonfiles = files.map(file => ({ 143 | 144 | name: file, 145 | json: JSON.parse(fs.readFileSync(file)) 146 | 147 | })); 148 | 149 | let [replaced, numReplaces] = utils.replaceMultipleJSONs(property, currentValue, replaceValue, jsonfiles, operation); 150 | 151 | replaced.forEach(file => fs.writeFileSync(file.name, JSON.stringify(file.json, null, 2))); 152 | 153 | console.log(chalk.green(`✅ ${numReplaces} fields replaced.`)); 154 | 155 | } 156 | catch(e){ 157 | 158 | console.log(chalk.red(`❌ ${e}`)); 159 | process.exit(1); 160 | 161 | } 162 | 163 | } 164 | 165 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 166 | // Defining the merge generator 167 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 168 | function generateMerge(files){ 169 | 170 | try{ 171 | 172 | let jsonfiles = files.map(file => ({ 173 | 174 | name: file, 175 | json: JSON.parse(fs.readFileSync(file)) 176 | 177 | })); 178 | 179 | let merge = utils.mergeMultipleJSONs(jsonfiles); 180 | 181 | fs.writeFileSync('merge.json', JSON.stringify(merge, null, 2)); 182 | 183 | console.log(chalk.green(`✅ ${files} merged into merge.json`)); 184 | 185 | } 186 | catch(e){ 187 | 188 | console.log(chalk.red(`❌ ${e}`)); 189 | process.exit(1); 190 | 191 | } 192 | 193 | } -------------------------------------------------------------------------------- /src/CLI/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonmatic", 3 | "version": "0.0.9", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-styles": { 8 | "version": "4.3.0", 9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 10 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 11 | "requires": { 12 | "color-convert": "^2.0.1" 13 | } 14 | }, 15 | "chalk": { 16 | "version": "4.1.1", 17 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", 18 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", 19 | "requires": { 20 | "ansi-styles": "^4.1.0", 21 | "supports-color": "^7.1.0" 22 | } 23 | }, 24 | "color-convert": { 25 | "version": "2.0.1", 26 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 27 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 28 | "requires": { 29 | "color-name": "~1.1.4" 30 | } 31 | }, 32 | "color-name": { 33 | "version": "1.1.4", 34 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 35 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 36 | }, 37 | "commander": { 38 | "version": "7.2.0", 39 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 40 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" 41 | }, 42 | "has-flag": { 43 | "version": "4.0.0", 44 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 45 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 46 | }, 47 | "supports-color": { 48 | "version": "7.2.0", 49 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 50 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 51 | "requires": { 52 | "has-flag": "^4.0.0" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/CLI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonmatic", 3 | "version": "0.2.4", 4 | "description": "⚗️ Transform a CSV (spreadsheet) into a JSON", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node index.js", 10 | "prepublish": "cp ../../README.md README.md", 11 | "postpublish": "rm README.md" 12 | }, 13 | "bin": { 14 | "jsonmatic": "index.js" 15 | }, 16 | "keywords": [ 17 | "JSON", 18 | "CSV", 19 | "transform", 20 | "spreadsheet" 21 | ], 22 | "author": "Erik Martín Jordán", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/erikmartinjordan/jsonmatic/issues" 26 | }, 27 | "homepage": "https://github.com/erikmartinjordan/jsonmatic#readme", 28 | "dependencies": { 29 | "chalk": "^4.1.1", 30 | "commander": "^7.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CLI/utils.js: -------------------------------------------------------------------------------- 1 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | // Validates a CSV 3 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | const validateCSV = (csv) => { 5 | 6 | let empty = {}; 7 | 8 | let firstCol = csv.map(e => e[0]); 9 | let firstRow = csv[0]; 10 | 11 | let duplicateKeys = firstCol.some(e => empty[e] ? true : (empty[e] = true, false)); 12 | let firstRowEmpty = firstRow.every(e => e === ''); 13 | 14 | if(duplicateKeys) return {error: `JSON has duplicated keys`}; 15 | if(firstRowEmpty) return {error: `First row shouldn't be empty`}; 16 | 17 | return `ok`; 18 | 19 | } 20 | 21 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | // Validates a JSON 23 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | const validateJSON = (json) => { 25 | 26 | try{ 27 | 28 | JSON.parse(json); 29 | return 'ok'; 30 | 31 | } 32 | catch(e){ 33 | 34 | return {error: 'Invalid JSON'}; 35 | 36 | } 37 | 38 | } 39 | 40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | // Transforms a JSON into a CSV 42 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | const transformToCSV = (json) => { 44 | 45 | const getDeepKeys = (obj) => { 46 | 47 | let keys = Object.keys(obj).map(key => { 48 | 49 | if(typeof obj[key] === 'object'){ 50 | 51 | let subkeys = getDeepKeys(obj[key]); 52 | 53 | return subkeys.map(subkey => key + '__separator__' + subkey); 54 | 55 | } 56 | else{ 57 | 58 | return key; 59 | 60 | } 61 | 62 | }); 63 | 64 | return keys.flat(Infinity); 65 | 66 | } 67 | 68 | let keys = getDeepKeys(json); 69 | 70 | let firstRow = ['key', ...new Set(keys.map(key => key.split('__separator__').slice(1).join('__separator__')))]; 71 | let firstCol = ['key', ...new Set(keys.map(key => key.split('__separator__').shift()))]; 72 | 73 | let csv = new Array(firstCol.length).fill('').map(() => new Array(firstRow.length).fill('')); 74 | 75 | for(let i = 0; i < firstCol.length; i ++){ 76 | for(let j = 0; j < firstRow.length; j ++){ 77 | 78 | if(i === 0) csv[0][j] = firstRow[j].split('__separator__').join('.'); 79 | if(j === 0) csv[i][0] = firstCol[i].split('__separator__').join('.'); 80 | if(i && j) csv[i][j] = `${firstCol[i]}__separator__${firstRow[j]}`.split('__separator__').reduce((ref, prop) => ref = ref?.[prop], json); 81 | 82 | } 83 | } 84 | 85 | return csv; 86 | 87 | } 88 | 89 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | // Transforms a CSV into a JSON 91 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 92 | const transformToJSON = (csv) => { 93 | 94 | let json = {}; 95 | 96 | csv.forEach((row, i) => { 97 | csv[i].forEach((column, j) => { 98 | 99 | let key = csv[i][0]; 100 | let value = csv[i][j]; 101 | let properties = csv[0][j]; 102 | 103 | if(i > 0 && j > 0 && key && value){ 104 | 105 | json[key] = json[key] || {}; 106 | 107 | let ref = json[key]; 108 | let subproperties = properties.split('.'); 109 | let last = subproperties.pop(); 110 | 111 | subproperties.forEach(property => { 112 | 113 | ref[property] = ref[property] || {}; 114 | ref = ref[property]; 115 | 116 | }); 117 | 118 | ref[last] = isNaN(value) ? value : parseFloat(value); 119 | 120 | } 121 | 122 | }) 123 | }); 124 | 125 | return json; 126 | 127 | } 128 | 129 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 130 | // Replace multiple JSONs props 131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | const replaceMultipleJSONs = (property, currentValue, replaceValue, jsonfiles, operation) => { 133 | 134 | let numReplaces = 0; 135 | 136 | let jsonFilesReplaced = jsonfiles.map(({name, json}) => { 137 | 138 | let clone = {...json}; 139 | 140 | let props = property.split('.'); 141 | 142 | let val = props.slice(0 ).reduce((ref, prop) => ref = ref?.[prop], clone); 143 | let ref = props.slice(0, -1).reduce((ref, prop) => ref = ref?.[prop], clone); 144 | 145 | let last = props.pop(); 146 | 147 | let _currentValue = isNaN(currentValue) ? currentValue : parseFloat(currentValue); 148 | let _replaceValue = isNaN(replaceValue) ? replaceValue : parseFloat(replaceValue); 149 | 150 | let rule = { 151 | 152 | 'equal': () => val === _currentValue, 153 | 'greater': () => val > _currentValue, 154 | 'lesser': () => val < _currentValue 155 | 156 | }[operation](); 157 | 158 | if(rule){ 159 | 160 | ref[last] = _replaceValue; 161 | numReplaces ++; 162 | 163 | } 164 | 165 | return { 166 | name: name, 167 | json: clone 168 | }; 169 | 170 | }); 171 | 172 | return [jsonFilesReplaced, numReplaces]; 173 | 174 | } 175 | 176 | 177 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 178 | // Merge multiple JSONs into one 179 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | const mergeMultipleJSONs = (jsonfiles) => { 181 | 182 | const union = (objA, objB) => { 183 | 184 | if(typeof objA === 'object'){ 185 | 186 | let merged = {...objA}; 187 | 188 | Object.keys(objB).forEach(key => { 189 | 190 | merged[key] = merged[key] ? union(merged[key], objB[key]) : objB[key]; 191 | 192 | }); 193 | 194 | return merged; 195 | 196 | } 197 | 198 | return objA; 199 | 200 | } 201 | 202 | let merged = jsonfiles.reduce((acc, {json}) => union(acc, json), null); 203 | 204 | return merged; 205 | 206 | } 207 | 208 | export { transformToCSV, transformToJSON, validateCSV, validateJSON, mergeMultipleJSONs, replaceMultipleJSONs } -------------------------------------------------------------------------------- /src/Components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Header from './Header'; 3 | import Json from './Json'; 4 | import Csv from './Csv'; 5 | import TransformToJSON from './TransformToJSON'; 6 | import TransformToCSV from './TransformToCSV'; 7 | import Menu from './Menu'; 8 | import useUndoableState from '../Functions/useUndoableState'; 9 | 10 | const App = () => { 11 | 12 | const [csv, setCsv, undo, redo] = useUndoableState([ 13 | 14 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'], 15 | ['1', 'AP-7', 42.02, 2.82, '🦄'], 16 | ['2', 'C-32', 41.35, 2.09, '🦧'], 17 | ['3', 'B-20', 41.44, 2.18, '🐰'], 18 | 19 | ]); 20 | 21 | const [json, setJson] = useState( 22 | 23 | { 24 | "1": { 25 | "road": "AP-7", 26 | "coord": { 27 | "lat": 42.02, 28 | "lng": 2.82 29 | }, 30 | "elem": "🦄" 31 | }, 32 | "2": { 33 | "road": "C-32", 34 | "coord": { 35 | "lat": 41.35, 36 | "lng": 2.09 37 | }, 38 | "elem": "🦧" 39 | }, 40 | "3": { 41 | "road": "B-20", 42 | "coord": { 43 | "lat": 41.44, 44 | "lng": 2.18 45 | }, 46 | "elem": "🐰" 47 | } 48 | } 49 | 50 | ); 51 | 52 | const [select, setSelect] = useState(['', '', '', '']); 53 | const [edit, setEdit] = useState(false); 54 | 55 | return ( 56 |
57 |
58 |
59 | 65 |
66 | 71 | 77 |
78 | 84 |
85 | 93 |
94 | ); 95 | 96 | } 97 | 98 | export default App; -------------------------------------------------------------------------------- /src/Components/BeautifyButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SmileyIcon } from '@primer/octicons-react'; 3 | 4 | const BeautifyButton = ({edit, indent, setIndent}) => { 5 | 6 | const beautify = () => { 7 | 8 | if(!indent) 9 | setIndent(2); 10 | 11 | } 12 | 13 | return( 14 | 15 | ); 16 | 17 | } 18 | 19 | export default BeautifyButton; -------------------------------------------------------------------------------- /src/Components/Changelog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import changelog from '../Assets/changelog.md'; 4 | 5 | const Changelog = () => { 6 | 7 | const [markdown, setMarkdown] = useState(null); 8 | 9 | useEffect(() => { 10 | 11 | const fetchMarkdown = async () => { 12 | 13 | let res = await fetch(changelog); 14 | let text = await res.text(); 15 | 16 | setMarkdown(text); 17 | 18 | } 19 | 20 | fetchMarkdown(); 21 | 22 | }, []); 23 | 24 | return( 25 |
26 |

What's new?

27 | Changelog 31 | }} 32 | /> 33 |
34 | ); 35 | 36 | } 37 | 38 | export default Changelog; -------------------------------------------------------------------------------- /src/Components/CopyCSV.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ClippyIcon, CheckCircleIcon } from '@primer/octicons-react'; 3 | 4 | const CopyCSV = ({csv}) => { 5 | 6 | const [alert, setAlert] = useState(null); 7 | 8 | const copy = () => { 9 | 10 | let text = csv.join('\n').split(',').join('\t'); 11 | 12 | navigator.clipboard.writeText(text); 13 | 14 | setAlert('copied'); 15 | 16 | setTimeout(() => setAlert(null), 1500); 17 | 18 | } 19 | 20 | return( 21 | 22 | ); 23 | 24 | } 25 | 26 | export default CopyCSV; -------------------------------------------------------------------------------- /src/Components/CopyJSON.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ClippyIcon, CheckCircleIcon } from '@primer/octicons-react'; 3 | 4 | const CopyCSV = ({edit, json, indent}) => { 5 | 6 | const [alert, setAlert] = useState(null); 7 | 8 | const copy = () => { 9 | 10 | navigator.clipboard.writeText(JSON.stringify(json, null, parseInt(indent))); 11 | 12 | setAlert('copied'); 13 | 14 | setTimeout(() => setAlert(null), 1500); 15 | 16 | } 17 | 18 | return( 19 | 20 | ); 21 | 22 | } 23 | 24 | export default CopyCSV; -------------------------------------------------------------------------------- /src/Components/Csv.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import UploadCSV from './UploadCSV'; 3 | import CopyCSV from './CopyCSV'; 4 | import DownloadCSV from './DownloadCSV'; 5 | 6 | const Csv = ({csv, setCsv, select, setSelect}) => { 7 | 8 | const [drag, setDrag] = useState(false); 9 | const [resize, setResize] = useState(false); 10 | 11 | useEffect(() => { 12 | 13 | const onClick = async (e) => { 14 | 15 | let ref = document.getElementById('Table'); 16 | 17 | if(ref && !ref.contains(e.target)) 18 | setSelect(['', '', '', '']); 19 | 20 | } 21 | 22 | window.addEventListener('click', onClick); 23 | 24 | return () => window.removeEventListener('click', onClick); 25 | 26 | }, [select, setSelect]); 27 | 28 | const selectBox = (row, col) => { 29 | 30 | setSelect([row, col, row, col]); 31 | 32 | } 33 | 34 | const handleMouseDown = (e, row, col) => { 35 | 36 | let [iniRow, iniCol, endRow, endCol] = select; 37 | 38 | if(e.button === 0){ 39 | 40 | setSelect([row, col, row, col]); 41 | 42 | setDrag(true); 43 | 44 | } 45 | if(e.button === 2 && iniRow === endRow && iniCol === endCol){ 46 | 47 | setSelect([row, col, row, col]); 48 | 49 | } 50 | 51 | } 52 | 53 | const handleMouseUp = () => { 54 | 55 | setDrag(false); 56 | 57 | } 58 | 59 | const handleMultipleSel = (e, row, col) => { 60 | 61 | if(drag){ 62 | 63 | e.preventDefault(); 64 | 65 | let [iniRow, iniCol, endRow, endCol] = select; 66 | 67 | if(iniRow <= row && iniCol <= col) 68 | setSelect([iniRow, iniCol, row, col]); 69 | 70 | if(iniRow >= row && iniCol >= col) 71 | setSelect([row, col, endRow, endCol]) 72 | 73 | } 74 | 75 | } 76 | 77 | const editValue = (e, row, col) => { 78 | 79 | const copyCsv = JSON.parse(JSON.stringify(csv)); 80 | 81 | copyCsv[row][col] = e.target.value; 82 | 83 | setCsv(copyCsv); 84 | 85 | } 86 | 87 | const getClassName = (row, col) => { 88 | 89 | let [iniRow, iniCol, endRow, endCol] = select; 90 | 91 | if(select.every(el => el === '')) 92 | return ''; 93 | 94 | if(row >= iniRow && row <= endRow && col >= iniCol && col <= endCol) 95 | return 'Selected'; 96 | 97 | } 98 | 99 | const resizeColumn = (e, col) => { 100 | 101 | e.preventDefault(); 102 | 103 | if(resize.col === col && e.clientX){ 104 | 105 | let elem = document.getElementById(`input0${col}`); 106 | 107 | elem.style.width = `${Math.max(75, parseInt(resize.initialSize) + parseInt(e.clientX - resize.initialPos))}px`; 108 | 109 | } 110 | 111 | } 112 | 113 | const autoResizeColumn = (e, col) => { 114 | 115 | let longestRow = csv.reduce((acc, elem, row) => csv[acc][col].toString().length > csv[row][col].toString().length ? acc : row, 0); 116 | 117 | csv.forEach((_, row) => document.getElementById(`input${row}${col}`).style.width = csv[longestRow][col].toString().length + 0.5 + 'ch'); 118 | 119 | } 120 | 121 | const handleMouseDownDragger = (e, col) => { 122 | 123 | e.stopPropagation(); 124 | 125 | let elem = document.getElementById(`input0${col}`); 126 | 127 | setResize({ 128 | col: col, 129 | initialPos: e.clientX, 130 | initialSize: elem.offsetWidth 131 | }); 132 | 133 | } 134 | 135 | const handleMouseUpDragger = (e) => { 136 | 137 | setResize(false); 138 | 139 | } 140 | 141 | return( 142 |
143 |
144 | 145 | 146 | {csv.map((row, i) => 147 | {csv[i].map((column, j) => 148 | )} 170 | 171 | )} 172 | 173 |
selectBox(i, j)} 151 | onMouseDown = {(e) => handleMouseDown(e, i, j)} 152 | onMouseUp = {(e) => handleMouseUp(e, i, j)} 153 | onMouseMove = {(e) => handleMultipleSel(e, i, j)} 154 | className = {getClassName(i, j)}> 155 | editValue(e, i, j)}> 159 | 160 |
handleMouseDownDragger(e, j)} 165 | onMouseUp = {(e) => handleMouseUpDragger(e)} 166 | onDrag = {(e) => resizeColumn(e, j)} 167 | onDoubleClick = {(e) => autoResizeColumn(e, j)}> 168 |
169 |
174 |
175 |
176 | 179 | 182 | 185 |
186 |
187 | ); 188 | 189 | } 190 | 191 | export default Csv; -------------------------------------------------------------------------------- /src/Components/DownloadCSV.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { saveAs } from 'file-saver'; 3 | import { DownloadIcon, CheckCircleIcon } from '@primer/octicons-react'; 4 | 5 | const DownloadCSV = ({csv}) => { 6 | 7 | const [alert, setAlert] = useState(null); 8 | 9 | const download = () => { 10 | 11 | var fileName = 'result.csv'; 12 | 13 | var fileToSave = new Blob([csv.map(row => row.join(',')).join('\n')], { 14 | type: 'application/csv', 15 | name: fileName 16 | }); 17 | 18 | saveAs(fileToSave, fileName); 19 | 20 | setAlert('downloaded'); 21 | 22 | setTimeout(() => setAlert(null), 1500); 23 | 24 | } 25 | 26 | return( 27 | 28 | ); 29 | 30 | } 31 | 32 | export default DownloadCSV; -------------------------------------------------------------------------------- /src/Components/DownloadJSON.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { saveAs } from 'file-saver'; 3 | import { DownloadIcon, CheckCircleIcon } from '@primer/octicons-react'; 4 | 5 | const DownloadJSON = ({json, edit, indent}) => { 6 | 7 | const [alert, setAlert] = useState(null); 8 | 9 | const download = () => { 10 | 11 | var fileName = 'result.json'; 12 | 13 | var fileToSave = new Blob([JSON.stringify(json, null, parseInt(indent))], { 14 | type: 'application/json', 15 | name: fileName 16 | }); 17 | 18 | saveAs(fileToSave, fileName); 19 | 20 | setAlert('downloaded'); 21 | 22 | setTimeout(() => setAlert(null), 1500); 23 | 24 | } 25 | 26 | return( 27 | 28 | ); 29 | 30 | } 31 | 32 | export default DownloadJSON; -------------------------------------------------------------------------------- /src/Components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { ReactComponent as Logo } from '../Assets/Logo.svg'; 4 | 5 | const Footer = () => { 6 | 7 | return( 8 |
9 | 10 | Home 11 | What's new? 12 | Replace 13 | Merge 14 | Delete 15 | GitHub 16 |
17 | ); 18 | 19 | } 20 | 21 | export default Footer; -------------------------------------------------------------------------------- /src/Components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactComponent as Logo } from '../Assets/Logo.svg'; 3 | 4 | const Header = () => { 5 | 6 | return( 7 |
8 | 9 |

jsonmatic – transform a CSV into a JSON

10 |

First column is reserved for unique object keys. You can use dot notation in the header cells to create subproperties.

11 |
12 | ); 13 | 14 | } 15 | 16 | export default Header; -------------------------------------------------------------------------------- /src/Components/IndentSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TriangleDownIcon } from '@primer/octicons-react'; 3 | 4 | const IndentSelector = ({edit, indent, setIndent}) => { 5 | 6 | return( 7 |
8 | 13 | 14 |
15 | ); 16 | 17 | } 18 | 19 | export default IndentSelector; -------------------------------------------------------------------------------- /src/Components/Json.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import IndentSelector from './IndentSelector'; 3 | import BeautifyButton from './BeautifyButton'; 4 | import UglifyButton from './UglifyButton'; 5 | import RandomButton from './RandomButton'; 6 | import TextareaIndent from './TextareaIndent'; 7 | import UploadJSON from './UploadJSON'; 8 | import CopyJSON from './CopyJSON'; 9 | import DownloadJSON from './DownloadJSON'; 10 | 11 | const Json = ({edit, json, setEdit, setJson}) => { 12 | 13 | const [indent, setIndent] = useState(2); 14 | 15 | return( 16 |
17 |
18 | 22 | 27 | 32 | 37 |
38 | 45 |
46 | 51 | 55 | 60 |
61 |
62 | ); 63 | 64 | } 65 | 66 | export default Json; -------------------------------------------------------------------------------- /src/Components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { CopyIcon, ClippyIcon, EllipsisIcon, ArrowLeftIcon, ArrowRightIcon } from '@primer/octicons-react'; 3 | 4 | const Menu = ({select, setSelect, csv, setCsv, undo, redo}) => { 5 | 6 | const [menu, setMenu] = useState(false); 7 | const [pos, setPos] = useState({x: 0, y: 0}); 8 | const [OS, setOS] = useState(null); 9 | 10 | useEffect(() => { 11 | 12 | let macOS = ['iPhone', 'iPad', 'Mac', 'iPod']; 13 | 14 | let currentOS = navigator.platform; 15 | 16 | let isMac = macOS.some(device => currentOS.includes(device)); 17 | 18 | if(isMac) 19 | setOS('Mac'); 20 | 21 | }, []); 22 | 23 | const merge = (arr1, arr2, x, y) => { 24 | 25 | let arr1_rows = arr1.length; 26 | let arr2_rows = arr2.length; 27 | 28 | let arr1_cols = arr1[0].length; 29 | let arr2_cols = arr2[0].length; 30 | 31 | let merg_rows = Math.max(arr1_rows, arr2_rows, x + arr2_rows); 32 | let merg_cols = Math.max(arr1_cols, arr2_cols, y + arr2_cols); 33 | 34 | let merged = Array(merg_rows).fill('').map(e => Array(merg_cols).fill('')); 35 | 36 | for(let i = 0; i < arr1_rows; i ++){ 37 | 38 | merged[i].splice(0, arr1[i].length, ...arr1[i]); 39 | 40 | } 41 | 42 | for(let i = x, j = 0; i < x + arr2_rows; i ++, j ++){ 43 | 44 | merged[i].splice(y, arr2[j].length, ...arr2[j]); 45 | 46 | } 47 | 48 | setCsv(merged); 49 | 50 | } 51 | 52 | const copy = () => { 53 | 54 | let text = ''; 55 | 56 | let [iniRow, iniCol, endRow, endCol] = select; 57 | 58 | for(let i = iniRow; i <= endRow; i ++){ 59 | for(let j = iniCol; j <= endCol; j ++){ 60 | 61 | text += csv[i][j]; 62 | 63 | if(j !== endCol) 64 | text += '\t'; 65 | 66 | } 67 | 68 | if(i !== endRow) 69 | text += '\n'; 70 | 71 | } 72 | 73 | navigator.clipboard.writeText(text); 74 | 75 | } 76 | 77 | const paste = async () => { 78 | 79 | let clipboard = await navigator.clipboard.readText(); 80 | 81 | clipboard = clipboard.split('\n').map(row => row.split('\t')); 82 | 83 | merge(csv, clipboard, select[0], select[1]); 84 | 85 | } 86 | 87 | const addRow = () => { 88 | 89 | let temp = JSON.parse(JSON.stringify(csv)); 90 | 91 | let row = Array(temp[0].length).fill(''); 92 | 93 | temp.splice(select[0] + 1, 0, row); 94 | 95 | setCsv(temp); 96 | setMenu(false); 97 | 98 | } 99 | 100 | const addCol = () => { 101 | 102 | let temp = JSON.parse(JSON.stringify(csv)); 103 | 104 | temp.forEach(row => row.splice(select[1] + 1, 0, '')); 105 | 106 | setCsv(temp); 107 | setMenu(false); 108 | 109 | } 110 | 111 | const deleteRows = () => { 112 | 113 | let temp = JSON.parse(JSON.stringify(csv)); 114 | 115 | let [iniRow, iniCol, endRow, endCol] = select; // eslint-disable-line no-unused-vars 116 | 117 | temp.splice(iniRow, endRow - iniRow + 1); 118 | 119 | setCsv(temp); 120 | setMenu(false); 121 | 122 | } 123 | 124 | const deleteCols = () => { 125 | 126 | let temp = JSON.parse(JSON.stringify(csv)); 127 | 128 | let [iniRow, iniCol, endRow, endCol] = select; // eslint-disable-line no-unused-vars 129 | 130 | temp.forEach(row => row.splice(iniCol, endCol - iniCol + 1)); 131 | 132 | setCsv(temp); 133 | setMenu(false); 134 | 135 | } 136 | 137 | const deleteValues = () => { 138 | 139 | let temp = JSON.parse(JSON.stringify(csv)); 140 | 141 | let [iniRow, iniCol, endRow, endCol] = select; 142 | 143 | for(let i = iniRow; i <= endRow; i ++) 144 | for(let j = iniCol; j <= endCol; j ++) 145 | temp[i][j] = ''; 146 | 147 | setCsv(temp); 148 | 149 | } 150 | 151 | const moveSelection = (direction) => { 152 | 153 | let temp = [...select]; 154 | 155 | let [iniRow, iniCol, endRow, endCol] = temp; 156 | 157 | let cols = csv[0].length; 158 | let rows = csv.length; 159 | 160 | if(temp.every(el => el === '') || iniRow !== endRow || iniCol !== endCol){ 161 | 162 | setSelect([0, 0, 0, 0]); 163 | return; 164 | } 165 | 166 | switch(direction){ 167 | 168 | case 'ArrowRight': temp[1] = temp[3] = Math.min(cols - 1, temp[1] + 1); break; 169 | case 'ArrowLeft': temp[1] = temp[3] = Math.max(0, temp[1] - 1); break; 170 | case 'ArrowDown': temp[0] = temp[2] = Math.min(rows - 1, temp[2] + 1); break; 171 | case 'ArrowUp': temp[0] = temp[2] = Math.max(0, temp[2] - 1); break; 172 | default: break; 173 | 174 | } 175 | 176 | setSelect(temp); 177 | 178 | } 179 | 180 | const selectAll = () => { 181 | 182 | let cols = csv[0].length; 183 | let rows = csv.length; 184 | 185 | setSelect([0, 0, rows - 1, cols - 1]); 186 | 187 | } 188 | 189 | const focus = () => { 190 | 191 | let [iniRow, iniCol] = select; 192 | 193 | let ref = document.getElementById(`input${iniRow}${iniCol}`); 194 | 195 | if(ref) 196 | ref.focus(); 197 | 198 | } 199 | 200 | useEffect(() => { 201 | 202 | const onDown = async (e) => { 203 | 204 | if((e.ctrlKey && e.key === 'v') || (e.metaKey && e.key === 'v')){ 205 | 206 | e.preventDefault(); 207 | 208 | paste(); 209 | 210 | } 211 | 212 | if((e.ctrlKey && e.key === 'a') || (e.metaKey && e.key === 'a')){ 213 | 214 | e.preventDefault(); 215 | 216 | selectAll(); 217 | 218 | } 219 | 220 | if(e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'ArrowDown'){ 221 | 222 | e.preventDefault(); 223 | 224 | moveSelection(e.key); 225 | 226 | } 227 | 228 | if((e.ctrlKey && e.key === 'c') || (e.metaKey && e.key === 'c')){ 229 | 230 | e.preventDefault(); 231 | 232 | copy(); 233 | 234 | } 235 | 236 | if((e.ctrlKey && e.key === 'ArrowRight') || (e.metaKey && e.key === 'ArrowRight')){ 237 | 238 | e.preventDefault(); 239 | 240 | addCol(); 241 | 242 | } 243 | 244 | if((e.ctrlKey && e.key === 'ArrowDown') || (e.metaKey && e.key === 'ArrowDown')){ 245 | 246 | e.preventDefault(); 247 | 248 | addRow(); 249 | 250 | } 251 | 252 | if((e.ctrlKey && e.key === 'ArrowLeft') || (e.metaKey && e.key === 'ArrowLeft')){ 253 | 254 | e.preventDefault(); 255 | 256 | deleteCols(); 257 | 258 | } 259 | 260 | if((e.ctrlKey && e.key === 'ArrowUp') || (e.metaKey && e.key === 'ArrowUp')){ 261 | 262 | e.preventDefault(); 263 | 264 | deleteRows(); 265 | 266 | } 267 | 268 | if(e.key === 'Delete'){ 269 | 270 | deleteValues(); 271 | 272 | } 273 | 274 | if(e.key.length === 1){ 275 | 276 | focus(); 277 | 278 | } 279 | 280 | } 281 | 282 | const onLeftClick = (e) => { 283 | 284 | let menu = document.getElementById('Menu'); 285 | 286 | if(!menu || !menu.contains(e.target)){ 287 | 288 | setMenu(false); 289 | 290 | } 291 | 292 | } 293 | 294 | const onRightClick = (e) => { 295 | 296 | let table = document.getElementById('Table'); 297 | 298 | if(table.contains(e.target)){ 299 | 300 | e.preventDefault(); 301 | 302 | setPos({x: e.pageX, y: e.pageY}); 303 | setMenu(true); 304 | 305 | window.addEventListener('click', onLeftClick); 306 | 307 | } 308 | 309 | } 310 | 311 | if(select.some(el => el !== '')){ 312 | 313 | window.addEventListener('keydown', onDown); 314 | window.addEventListener('contextmenu', onRightClick); 315 | window.addEventListener('click', onLeftClick); 316 | 317 | } 318 | 319 | return () => { 320 | 321 | window.removeEventListener('keydown', onDown); 322 | window.removeEventListener('contextmenu', onRightClick); 323 | window.removeEventListener('click', onLeftClick); 324 | 325 | } 326 | 327 | }, [csv, select]); // eslint-disable-line react-hooks/exhaustive-deps 328 | 329 | return( 330 | 331 | { menu 332 | ?
333 |
    334 |
  • Copy
    {OS === 'Mac' ? '⌘' : 'ctrl'}C
  • 335 |
  • Paste
    {OS === 'Mac' ? '⌘' : 'ctrl'}V
  • 336 |
    337 |
  • Undo
    {OS === 'Mac' ? '⌘' : 'ctrl'}Z
  • 338 |
  • Redo
    {OS === 'Mac' ? '⌘' : 'ctrl'}⇧Z
  • 339 |
    340 |
  • Add row
    {OS === 'Mac' ? '⌘' : 'ctrl'}↓
  • 341 |
  • Add column
    {OS === 'Mac' ? '⌘' : 'ctrl'}→
  • 342 |
    343 |
  • Delete row(s)
    {OS === 'Mac' ? '⌘' : 'ctrl'}↑
  • 344 |
  • Delete column(s)
    {OS === 'Mac' ? '⌘' : 'ctrl'}←
  • 345 |
346 |
347 | : null 348 | } 349 |
350 | ); 351 | 352 | } 353 | 354 | export default Menu; -------------------------------------------------------------------------------- /src/Components/MultipleDeletions.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { saveAs } from 'file-saver'; 3 | import UploadFiles from './UploadFiles'; 4 | import { DownloadIcon, TriangleDownIcon } from '@primer/octicons-react'; 5 | 6 | const MultipleReplaces = () => { 7 | 8 | const [jsonfiles, setJsonfiles] = useState([]); 9 | const [numDeletions, setNumdeletions] = useState(null); 10 | const [operation, setOperation] = useState('equal'); 11 | const [path, setPath] = useState(''); 12 | const [currentValue, setCurrentValue] = useState(''); 13 | 14 | const deletion = () => { 15 | 16 | let numDeletions = 0; 17 | 18 | let jsonFilesReplaced = jsonfiles.map(({name, json}) => { 19 | 20 | let clone = {...json}; 21 | 22 | path.split(',').forEach(path => { 23 | 24 | let props = path.trim().split('.'); 25 | 26 | let val = props.slice(0 ).reduce((ref, prop) => ref = ref?.[prop], clone); 27 | let ref = props.slice(0, -1).reduce((ref, prop) => ref = ref?.[prop], clone); 28 | 29 | let last = props.pop(); 30 | 31 | let _currentValue = isNaN(currentValue) ? currentValue : parseFloat(currentValue); 32 | 33 | let rule = { 34 | 35 | 'equal': () => val === _currentValue, 36 | 'greater': () => val > _currentValue, 37 | 'lesser': () => val < _currentValue 38 | 39 | }[operation](); 40 | 41 | if(rule || _currentValue === '*'){ 42 | 43 | try{ 44 | 45 | delete ref[last]; 46 | numDeletions ++; 47 | 48 | }catch(e){ 49 | 50 | // Ignore deletion because property doesn't exist 51 | 52 | } 53 | 54 | } 55 | 56 | }); 57 | 58 | return { 59 | name: name, 60 | json: clone 61 | }; 62 | 63 | }); 64 | 65 | 66 | setJsonfiles(jsonFilesReplaced); 67 | setNumdeletions(numDeletions); 68 | 69 | } 70 | 71 | const download = () => { 72 | 73 | jsonfiles.forEach(({name, json}) => { 74 | 75 | let fileName = name; 76 | 77 | let fileToSave = new Blob([JSON.stringify(json, null, 2)], { 78 | type: 'application/json', 79 | name: fileName 80 | }); 81 | 82 | saveAs(fileToSave, fileName); 83 | 84 | }); 85 | 86 | } 87 | 88 | return( 89 |
90 |

Delete multiple properties in JSON files at once

91 |

Select your files (they won't be uploaded to any server), they remain in your browser.

92 | 96 | { jsonfiles.length > 0 97 | ? 98 |

Rules

99 |
100 | If 101 | setPath(e.target.value)} value = {path}/> 102 |
103 | 108 | 109 |
110 | setCurrentValue(e.target.value)} value = {currentValue}/> 111 |
112 |
113 |

Hint: Separate multiple properties with commas and set value to * for brute force deletion

114 |
115 |
116 | 117 | 118 |
119 |
120 |
{numDeletions !== null ? `${numDeletions} fields deleted` : null}
121 |
122 |
123 | : null 124 | } 125 |
126 | ); 127 | 128 | } 129 | 130 | export default MultipleReplaces; -------------------------------------------------------------------------------- /src/Components/MultipleMerge.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { saveAs } from 'file-saver'; 3 | import UploadFiles from './UploadFiles'; 4 | import { DownloadIcon, GrabberIcon, GitMergeIcon } from '@primer/octicons-react'; 5 | import { mergeMultipleJSONs } from '../CLI/utils'; 6 | 7 | const MultipleReplaces = () => { 8 | 9 | const [jsonfiles, setJsonfiles] = useState([]); 10 | const [dragged, setDragged] = useState(null); 11 | const [merged, setMerged] = useState(null); 12 | 13 | const merge = () => { 14 | 15 | let merged = mergeMultipleJSONs(jsonfiles); 16 | 17 | setMerged(merged); 18 | 19 | } 20 | 21 | const download = () => { 22 | 23 | let fileName = `merged.json`; 24 | 25 | let fileToSave = new Blob([JSON.stringify(merged, null, 2)], { 26 | type: 'application/json', 27 | name: fileName 28 | }); 29 | 30 | saveAs(fileToSave, fileName); 31 | 32 | } 33 | 34 | const handleDragStart = (e) => { 35 | 36 | setDragged(e.target.id); 37 | 38 | } 39 | 40 | const handleDragOver = (e) => { 41 | 42 | let newPosition = parseInt(e.target.id); 43 | 44 | if(newPosition >= 0){ 45 | 46 | let copy = [...jsonfiles]; 47 | 48 | ((from, to, elem) => { 49 | 50 | copy.splice(from, 1); 51 | copy.splice(to, 0, elem); 52 | 53 | })(dragged, newPosition, jsonfiles[dragged]); 54 | 55 | setJsonfiles(copy); 56 | setDragged(newPosition); 57 | 58 | } 59 | 60 | } 61 | 62 | const handleDragEnd = () => { 63 | 64 | setDragged(null); 65 | 66 | } 67 | 68 | return( 69 |
70 |

Merge multiple JSON files

71 |

Select your files (they won't be uploaded to any server), they remain in your browser.

72 | 76 | { jsonfiles.length > 0 77 | ? 78 |

Select merge order

79 |

Higher priority files are on the left. Files with higher priority overwrite them with less priority:

80 |
81 | {jsonfiles.map(({name}, key) => 82 | 83 |
{name}
84 |
85 | )} 86 | 87 |
88 | { merged 89 | ?
90 |
91 |
merged.json
92 | {JSON.stringify(merged, null, 2)} 93 |
94 | 95 |
96 | : null 97 | } 98 |
99 | : null 100 | } 101 |
102 | ); 103 | 104 | } 105 | 106 | export default MultipleReplaces; -------------------------------------------------------------------------------- /src/Components/MultipleReplaces.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { saveAs } from 'file-saver'; 3 | import UploadFiles from './UploadFiles'; 4 | import { DownloadIcon, TriangleDownIcon } from '@primer/octicons-react'; 5 | import { replaceMultipleJSONs } from '../CLI/utils'; 6 | 7 | const MultipleReplaces = () => { 8 | 9 | const [jsonfiles, setJsonfiles] = useState([]); 10 | const [numReplaces, setNumReplaces] = useState(null); 11 | const [operation, setOperation] = useState('equal'); 12 | const [path, setPath] = useState(''); 13 | const [replaceValue, setReplaceValue] = useState(''); 14 | const [currentValue, setCurrentValue] = useState(''); 15 | 16 | const replace = () => { 17 | 18 | let res = replaceMultipleJSONs(path, currentValue, replaceValue, jsonfiles, operation); 19 | 20 | setJsonfiles(res[0]); 21 | setNumReplaces(res[1]); 22 | 23 | } 24 | 25 | const download = () => { 26 | 27 | jsonfiles.forEach(({name, json}) => { 28 | 29 | let fileName = name; 30 | 31 | let fileToSave = new Blob([JSON.stringify(json, null, 2)], { 32 | type: 'application/json', 33 | name: fileName 34 | }); 35 | 36 | saveAs(fileToSave, fileName); 37 | 38 | }); 39 | 40 | } 41 | 42 | return( 43 |
44 |

Replace multiple properties in JSON files at once

45 |

Select your files (they won't be uploaded to any server), they remain in your browser.

46 | 50 | { jsonfiles.length > 0 51 | ? 52 |

Rules

53 |
54 | If 55 | setPath(e.target.value)} value = {path}/> 56 |
57 | 62 | 63 |
64 | setCurrentValue(e.target.value)} value = {currentValue}/> 65 | replace with 66 | setReplaceValue(e.target.value)} value = {replaceValue}/> 67 |
68 |
69 | 70 | 71 |
72 |
73 |
{numReplaces !== null ? `${numReplaces} fields replaced` : null}
74 |
75 |
76 | : null 77 | } 78 |
79 | ); 80 | 81 | } 82 | 83 | export default MultipleReplaces; -------------------------------------------------------------------------------- /src/Components/RandomButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dream from 'dreamjs'; 3 | import { FlameIcon } from '@primer/octicons-react'; 4 | 5 | const RandomButton = ({edit, setJson}) => { 6 | 7 | const randomize = () => { 8 | 9 | let people = ~~(Math.random() * 100); 10 | 11 | dream 12 | .schema({ 13 | name: 'name', 14 | age: 'age', 15 | address: 'address', 16 | contact: { 17 | phone: 'phone', 18 | servicePhone: /^(800[1-9]{6})$/ 19 | } 20 | }) 21 | .generateRnd(people) 22 | .output((err, result) => { 23 | 24 | let json = {}; 25 | 26 | result.forEach((obj, i) => json[i] = obj) 27 | 28 | setJson(json); 29 | 30 | }); 31 | 32 | } 33 | 34 | return( 35 | 36 | ); 37 | 38 | } 39 | 40 | export default RandomButton; -------------------------------------------------------------------------------- /src/Components/Seo.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | const Seo = () => { 5 | 6 | let sitemap = { 7 | 8 | '/': { 9 | 'title': `Jsonmatic`, 10 | 'description': `Transform a CSV (spreadsheet) into a JSON.` 11 | }, 12 | '/changelog': { 13 | 'title': `What's new — Jsonmatic`, 14 | 'description': `Changelog and relevant changes of the website.` 15 | }, 16 | '/replace': { 17 | 'title': `Replace — Jsonmatic`, 18 | 'description': `Replace a property from multiple JSON files at once, and download the new file.` 19 | }, 20 | '/merge': { 21 | 'title': `Merge — Jsonmatic`, 22 | 'description': `Merge multiple JSON files, and download the new merged file.` 23 | }, 24 | '/delete': { 25 | 'title': `Delete — Jsonmatic`, 26 | 'description': `Replace a property from multiple JSON files at once, and download the new file.` 27 | } 28 | 29 | } 30 | 31 | useEffect(() => { 32 | 33 | let url = window.location.pathname; 34 | 35 | document.title = sitemap[url].title; 36 | document.querySelector(`meta[name = 'description']`).content = sitemap[url].description; 37 | 38 | }); 39 | 40 | return null; 41 | 42 | } 43 | 44 | export default withRouter(Seo); -------------------------------------------------------------------------------- /src/Components/TextareaIndent.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { validateJSON } from '../CLI/utils'; 3 | 4 | const TextareaIndent = ({edit, setEdit, indent, json, setJson}) => { 5 | 6 | const [alert, setAlert] = useState(null); 7 | const [text, setText] = useState({value: '', caret: -1, target: null}); 8 | 9 | useEffect(() => { 10 | 11 | setText({value: JSON.stringify(json, null, parseInt(indent))}); 12 | 13 | }, [json, indent]); 14 | 15 | useEffect(() => { 16 | 17 | if(text.caret >= 0){ 18 | 19 | text.target.setSelectionRange(text.caret + indent, text.caret + indent); 20 | 21 | } 22 | 23 | }, [indent, text]); 24 | 25 | const iniEdition = () => { 26 | 27 | setEdit(true); 28 | setAlert(null); 29 | 30 | } 31 | 32 | const endEdition = () => { 33 | 34 | let validation = validateJSON(text.value); 35 | 36 | if(validation === 'ok'){ 37 | 38 | setJson(JSON.parse(text.value)); 39 | setEdit(false); 40 | 41 | } 42 | else{ 43 | 44 | setAlert(validation.error); 45 | 46 | } 47 | 48 | } 49 | 50 | const handleTab = (e) => { 51 | 52 | let content = e.target.value; 53 | let caret = e.target.selectionStart; 54 | 55 | if(e.key === 'Tab'){ 56 | 57 | e.preventDefault(); 58 | 59 | let newText = content.substring(0, caret) + ' '.repeat(indent) + content.substring(caret); 60 | 61 | setText({value: newText, caret: caret, target: e.target}); 62 | 63 | } 64 | 65 | } 66 | 67 | const handleText = (e) => setText({value: e.target.value, caret: -1, target: e.target}); 68 | 69 | return( 70 |
71 |
{alert}
72 |