diff --git a/bin/jora b/bin/jora old mode 100644 new mode 100755 diff --git a/index.js b/index.js index f3a1101..fd374ec 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const cli = require('clap'); const jora = require('jora/dist/jora'); +const colorize = require('./utils/colorize'); function readFromStream(stream, processBuffer) { const buffer = []; @@ -88,7 +89,8 @@ function processStream(options) { if (options.outputFile) { fs.writeFileSync(options.outputFile, serializedResult, 'utf-8'); } else { - console.log(serializedResult); + const result = options.noColor ? serializeResult : colorize(serializedResult); + console.log(result); } }); } @@ -101,6 +103,7 @@ var command = cli.create('jora', '[query]') .option('-p, --pretty [indent]', 'Pretty print with optionally specified indentation(4 spaces by default)', value => value === undefined ? 4 : Number(value) || false , false) + .option('--no-color', 'Suppress color output') .action(function(args) { var options = processOptions(this.values, args); diff --git a/package-lock.json b/package-lock.json index 29b8ec4..f88a964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,9 +208,12 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, "append-transform": { "version": "1.0.0", @@ -337,15 +340,13 @@ "dev": true }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "chardet": { @@ -368,6 +369,30 @@ "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", "requires": { "chalk": "^1.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } } }, "cli-cursor": { @@ -428,7 +453,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -436,8 +460,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colors": { "version": "0.5.1", @@ -1138,8 +1161,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "hasha": { "version": "3.0.0", @@ -2509,9 +2531,12 @@ "dev": true }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } }, "table": { "version": "5.4.1", diff --git a/package.json b/package.json index 3690521..60f71cb 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "coveralls": "nyc report --reporter=text-lcov | coveralls" }, "dependencies": { + "chalk": "^2.4.2", "clap": "^1.0.9", "jora": "1.0.0-alpha.10" }, diff --git a/utils/colorize.js b/utils/colorize.js new file mode 100644 index 0000000..ffa2b73 --- /dev/null +++ b/utils/colorize.js @@ -0,0 +1,47 @@ +const { + STRING, + STRING_KEY, + WHITESPACE, + COLON, + RIGHT_BRACE, + RIGHT_BRACKET +} = require('./constants').TOKENS; +const { TOKEN_COLORS } = require('./constants'); +const tokenize = require('./tokenize'); + +module.exports = (input) => { + let result = ''; + + const tokens = tokenize(input); + + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + + if (TOKEN_COLORS[token.type]) { + if (token.type === STRING) { + for (let j = i + 1; j < tokens.length; j++) { + if (tokens[j].type === WHITESPACE) { + continue; + } + if (tokens[j].type === COLON) { + token.type = STRING_KEY; + break; + } + if ( + tokens[j].type === STRING || + tokens[j].type === RIGHT_BRACE || + tokens[j].type === RIGHT_BRACKET + ) { + break; + } + } + } + result += TOKEN_COLORS[token.type](token.value); + } else { + result += token.value; + } + } + + + return result; +}; diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 0000000..3443af2 --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,87 @@ +const chalk = require('chalk'); + +const TOKENS = { + LEFT_BRACE: 0, // { + RIGHT_BRACE: 1, // } + LEFT_BRACKET: 2, // [ + RIGHT_BRACKET: 3, // ] + COLON: 4, // : + COMMA: 5, // , + STRING: 6, // + STRING_KEY: 7, // + NUMBER: 8, // + TRUE: 9, // true + FALSE: 10, // false + NULL: 11, // null + WHITESPACE: 12 // +}; + +const TOKEN_COLORS = { + [TOKENS.LEFT_BRACE]: chalk.bold.white, + [TOKENS.RIGHT_BRACE]: chalk.bold.white, + [TOKENS.LEFT_BRACKET]: chalk.bold.white, + [TOKENS.RIGHT_BRACKET]: chalk.bold.white, + [TOKENS.COLON]: chalk.bold.white, + [TOKENS.COMMA]: chalk.bold.white, + [TOKENS.STRING]: chalk.green, + [TOKENS.STRING_KEY]: chalk.green.yellow, + [TOKENS.NUMBER]: chalk.blue, + [TOKENS.TRUE]: chalk.cyan, + [TOKENS.FALSE]: chalk.cyan, + [TOKENS.NULL]: chalk.red +}; + +const PUNCTUATOR_TOKENS_MAP = { + '{': TOKENS.LEFT_BRACE, + '}': TOKENS.RIGHT_BRACE, + '[': TOKENS.LEFT_BRACKET, + ']': TOKENS.RIGHT_BRACKET, + ':': TOKENS.COLON, + ',': TOKENS.COMMA +}; + +const KEYWORD_TOKENS_MAP = { + 'true': TOKENS.TRUE, + 'false': TOKENS.FALSE, + 'null': TOKENS.NULL +}; + +const STRING_STATES = { + _START_: 0, + START_QUOTE_OR_CHAR: 1, + ESCAPE: 2 +}; + +const ESCAPES = { + '"': 0, // Quotation mask + '\\': 1, // Reverse solidus + '/': 2, // Solidus + 'b': 3, // Backspace + 'f': 4, // Form feed + 'n': 5, // New line + 'r': 6, // Carriage return + 't': 7, // Horizontal tab + 'u': 8 // 4 hexadecimal digits +}; + +const NUMBER_STATES = { + _START_: 0, + MINUS: 1, + ZERO: 2, + DIGIT: 3, + POINT: 4, + DIGIT_FRACTION: 5, + EXP: 6, + EXP_DIGIT_OR_SIGN: 7 +}; + + +module.exports = { + TOKENS, + TOKEN_COLORS, + PUNCTUATOR_TOKENS_MAP, + KEYWORD_TOKENS_MAP, + STRING_STATES, + ESCAPES, + NUMBER_STATES +}; diff --git a/utils/tokenize.js b/utils/tokenize.js new file mode 100644 index 0000000..298226f --- /dev/null +++ b/utils/tokenize.js @@ -0,0 +1,325 @@ +const { + STRING, + NUMBER, + WHITESPACE +} = require('./constants').TOKENS; +const { + PUNCTUATOR_TOKENS_MAP, + KEYWORD_TOKENS_MAP, + STRING_STATES, + NUMBER_STATES, + ESCAPES +} = require('./constants'); + +// HELPERS + +function isDigit1to9(char) { + return char >= '1' && char <= '9'; +} + +function isDigit(char) { + return char >= '0' && char <= '9'; +} + +function isHex(char) { + return ( + isDigit(char) || + (char >= 'a' && char <= 'f') || + (char >= 'A' && char <= 'F') + ); +} + +function isExp(char) { + return char === 'e' || char === 'E'; +} + +// PARSERS + +function parseWhitespace(input, index, line, column) { + const char = input.charAt(index); + + if (char === '\r') { // CR (Unix) + index++; + line++; + column = 1; + if (input.charAt(index) === '\n') { // CRLF (Windows) + index++; + } + } else if (char === '\n') { // LF (MacOS) + index++; + line++; + column = 1; + } else if (char === '\t' || char === ' ') { + index++; + column++; + } else { + return null; + } + + return { + index, + line, + column, + value: char + }; +} + +function parseChar(input, index, line, column) { + const char = input.charAt(index); + + if (char in PUNCTUATOR_TOKENS_MAP) { + return { + type: PUNCTUATOR_TOKENS_MAP[char], + line, + column: column + 1, + index: index + 1, + value: char + }; + } + + return null; +} + +function parseKeyword(input, index, line, column) { + for (const name in KEYWORD_TOKENS_MAP) { + if (KEYWORD_TOKENS_MAP.hasOwnProperty(name) && input.substr(index, name.length) === name) { + return { + type: KEYWORD_TOKENS_MAP[name], + line, + column: column + name.length, + index: index + name.length, + value: name + }; + } + } + + return null; +} + +function parseString(input, index, line, column) { + const startIndex = index; + let state = STRING_STATES._START_; + + while (index < input.length) { + const char = input.charAt(index); + + switch (state) { + case STRING_STATES._START_: { + if (char === '"') { + index++; + state = STRING_STATES.START_QUOTE_OR_CHAR; + } else { + return null; + } + break; + } + + case STRING_STATES.START_QUOTE_OR_CHAR: { + if (char === '\\') { + index++; + state = STRING_STATES.ESCAPE; + } else if (char === '"') { + index++; + const value = input.slice(startIndex, index); + + return { + type: STRING, + line, + column: column + index - startIndex, + index, + value + }; + } else { + index++; + } + break; + } + + case STRING_STATES.ESCAPE: { + if (char in ESCAPES) { + index++; + if (char === 'u') { + for (let i = 0; i < 4; i++) { + const curChar = input.charAt(index); + if (curChar && isHex(curChar)) { + index++; + } else { + return null; + } + } + } + state = STRING_STATES.START_QUOTE_OR_CHAR; + } else { + return null; + } + break; + } + } + } +} + +function parseNumber(input, index, line, column) { + const startIndex = index; + let passedValueIndex = index; + let state = NUMBER_STATES._START_; + + iterator: while (index < input.length) { + const char = input.charAt(index); + + switch (state) { + case NUMBER_STATES._START_: { + if (char === '-') { + state = NUMBER_STATES.MINUS; + } else if (char === '0') { + passedValueIndex = index + 1; + state = NUMBER_STATES.ZERO; + } else if (isDigit1to9(char)) { + passedValueIndex = index + 1; + state = NUMBER_STATES.DIGIT; + } else { + return null; + } + break; + } + + case NUMBER_STATES.MINUS: { + if (char === '0') { + passedValueIndex = index + 1; + state = NUMBER_STATES.ZERO; + } else if (isDigit1to9(char)) { + passedValueIndex = index + 1; + state = NUMBER_STATES.DIGIT; + } else { + return null; + } + break; + } + + case NUMBER_STATES.ZERO: { + if (char === '.') { + state = NUMBER_STATES.POINT; + } else if (isExp(char)) { + state = NUMBER_STATES.EXP; + } else { + break iterator; + } + break; + } + + case NUMBER_STATES.DIGIT: { + if (isDigit(char)) { + passedValueIndex = index + 1; + } else if (char === '.') { + state = NUMBER_STATES.POINT; + } else if (isExp(char)) { + state = NUMBER_STATES.EXP; + } else { + break iterator; + } + break; + } + + case NUMBER_STATES.POINT: { + if (isDigit(char)) { + passedValueIndex = index + 1; + state = NUMBER_STATES.DIGIT_FRACTION; + } else { + break iterator; + } + break; + } + + case NUMBER_STATES.DIGIT_FRACTION: { + if (isDigit(char)) { + passedValueIndex = index + 1; + } else if (isExp(char)) { + state = NUMBER_STATES.EXP; + } else { + break iterator; + } + break; + } + + case NUMBER_STATES.EXP: { + if (char === '+' || char === '-') { + state = NUMBER_STATES.EXP_DIGIT_OR_SIGN; + } else if (isDigit(char)) { + passedValueIndex = index + 1; + state = NUMBER_STATES.EXP_DIGIT_OR_SIGN; + } else { + break iterator; + } + break; + } + + case NUMBER_STATES.EXP_DIGIT_OR_SIGN: { + if (isDigit(char)) { + passedValueIndex = index + 1; + } else { + break iterator; + } + break; + } + } + + index++; + } + + if (passedValueIndex > 0) { + return { + type: NUMBER, + line, + column: column + passedValueIndex - startIndex, + index: passedValueIndex, + value: input.slice(startIndex, passedValueIndex) + }; + } + + return null; +} + +module.exports = (input) => { + let line = 1; + let column = 1; + let index = 0; + + const tokens = []; + + while (index < input.length) { + const args = [input, index, line, column]; + const whitespace = parseWhitespace(...args); + + if (whitespace) { + index = whitespace.index; + line = whitespace.line; + column = whitespace.column; + + tokens.push({ + type: WHITESPACE, + value: whitespace.value + }); + + continue; + } + + const matched = ( + parseChar(...args) || + parseKeyword(...args) || + parseString(...args) || + parseNumber(...args) + ); + + if (matched) { + tokens.push({ + type: matched.type, + value: matched.value + }); + + index = matched.index; + line = matched.line; + column = matched.column; + } + } + + return tokens; +};