diff --git a/README.md b/README.md index b326a5b..716e3ca 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,13 @@ document.body.innerHTML += highlighted **Output:** ```html SELECT -`id` +`id` , -`username` +`username` FROM -`users` +`users` WHERE -`email` +`email` = 'test@example.com' ``` @@ -112,22 +112,22 @@ console.log(segments) ```js [ { name: 'keyword', content: 'SELECT' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`id`' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`id`' }, { name: 'special', content: ',' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`username`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`username`' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'FROM' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`users`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`users`' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'WHERE' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`email`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`email`' }, + { name: 'whitespace', content: ' ' }, { name: 'special', content: '=' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'string', content: "'test@example.com'" } ] ``` diff --git a/lib/index.d.ts b/lib/index.d.ts index 28a3cfd..96ae839 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -8,6 +8,7 @@ declare module 'sql-highlight' { function: string; number: string; string: string; + identifier: string; special: string; bracket: string; comment: string; diff --git a/lib/index.js b/lib/index.js index ba2d539..4a77ef0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,6 +12,7 @@ const DEFAULT_OPTIONS = { function: '\x1b[31m', number: '\x1b[32m', string: '\x1b[32m', + identifier: '\x1b[0m', special: '\x1b[33m', bracket: '\x1b[33m', comment: '\x1b[2m\x1b[90m', @@ -19,13 +20,11 @@ const DEFAULT_OPTIONS = { } } -const DEFAULT_KEYWORD = 'default' - const highlighters = [ /\b(?\d+(?:\.\d+)?)\b/, // Note: Repeating string escapes like 'sql''server' will also work as they are just repeating strings - /(?'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/, + /(?'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/, /(?--[^\n\r]*|#[^\n\r]*|\/\*(?:[^*]|\*(?!\/))*\*\/)/, @@ -34,54 +33,29 @@ const highlighters = [ /(?[()])/, - /(?!=|[=%*/\-+,;:<>])/ -] + /(?!=|[=%*/\-+,;:<>.])/, -function getRegexString (regex) { - const str = regex.toString() - return str.replace(/^\/|\/\w*$/g, '') -} + /(?\b\w+\b|`(?:[^`\\]|\\.)*`)/, + + /(?\s+)/, + + /(?\.+?)/ +] -// Regex of the shape /(.*?)|((?...)|(?...)|...|$)/y +// Regex of the shape /(?...)|(?...)|.../g const tokenizer = new RegExp( - '(.*?)(' + - '\\b(?' + keywords.join('|') + ')\\b|' + - highlighters.map(getRegexString).join('|') + - '|$)', // $ needed to to match "default" till the end of string - 'isy' + [ + '\\b(?' + keywords.join('|') + ')\\b', + ...highlighters.map(regex => regex.source) + ].join('|'), + 'gis' ) function getSegments (sqlString) { - const segments = [] - let match - - // Reset the starting position - tokenizer.lastIndex = 0 - - // This is probably the one time when an assignment inside a condition makes sense - // eslint-disable-next-line no-cond-assign - while (match = tokenizer.exec(sqlString)) { - if (match[1]) { - segments.push({ - name: DEFAULT_KEYWORD, - content: match[1] - }) - } - - if (match[2]) { - const name = Object.keys(match.groups).find(key => match.groups[key]) - segments.push({ - name, - content: match.groups[name] - }) - } - - // Stop at the end of string - if (match.index + match[0].length >= sqlString.length) { - break - } - } - + const segments = Array.from(sqlString.matchAll(tokenizer), match => ({ + name: Object.keys(match.groups).find(key => match.groups[key]), + content: match[0] + })) return segments } @@ -90,14 +64,14 @@ function highlight (sqlString, options) { return getSegments(sqlString) .map(({ name, content }) => { - if (name === DEFAULT_KEYWORD) { - return content - } if (options.html) { const escapedContent = options.htmlEscaper(content) - return `${escapedContent}` + return name === 'whitespace' ? escapedContent : `${escapedContent}` + } + if (options.colors[name]) { + return options.colors[name] + content + options.colors.clear } - return options.colors[name] + content + options.colors.clear + return content }) .join('') } diff --git a/test/index.test.js b/test/index.test.js index a208a3a..ab04fe3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -6,6 +6,7 @@ const OPTIONS = { function: '[function]', number: '[number]', string: '[string]', + identifier: '[identifier]', special: '[special]', bracket: '[bracket]', comment: '[comment]', @@ -33,12 +34,12 @@ describe('unicode', () => { it('strings (mixing quotes)', () => { expect(hlUni('\'"`\' "\'`" `"\'`')) - .toBe('[string]\'"`\'[clear] [string]"\'`"[clear] [string]`"\'`[clear]') + .toBe('[string]\'"`\'[clear] [string]"\'`"[clear] [identifier]`"\'`[clear]') }) - it('strings (scaping quotes)', () => { + it('strings (escaping quotes)', () => { expect(hlUni('\'\\\'\' "\\"" `\\``')) - .toBe('[string]\'\\\'\'[clear] [string]"\\""[clear] [string]`\\``[clear]') + .toBe('[string]\'\\\'\'[clear] [string]"\\""[clear] [identifier]`\\``[clear]') }) it('integers', () => { @@ -83,47 +84,47 @@ describe('unicode', () => { it('alphanumeric', () => { expect(hlUni('(f1)')) - .toBe('[bracket]([clear]f1[bracket])[clear]') + .toBe('[bracket]([clear][identifier]f1[clear][bracket])[clear]') }) it('functions', () => { expect(hlUni('COUNT(`id`)')) - .toBe('[function]COUNT[clear][bracket]([clear][string]`id`[clear][bracket])[clear]') + .toBe('[function]COUNT[clear][bracket]([clear][identifier]`id`[clear][bracket])[clear]') }) it('basic query', () => { expect(hlUni("SELECT * FROM `users` WHERE `email` = 'test@example.com'")) - .toBe("[keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] [string]`users`[clear] [keyword]WHERE[clear] [string]`email`[clear] [special]=[clear] [string]'test@example.com'[clear]") + .toBe("[keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] [identifier]`users`[clear] [keyword]WHERE[clear] [identifier]`email`[clear] [special]=[clear] [string]'test@example.com'[clear]") }) it('complex query', () => { expect(hlUni("SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = 'test@example.com' AND `foo` = 'BAR' OR 1=1")) - .toBe("[keyword]SELECT[clear] [function]COUNT[clear][bracket]([clear]id[bracket])[clear][special],[clear] [string]`id`[clear][special],[clear] [string]`username`[clear] [keyword]FROM[clear] [string]`users`[clear] [keyword]WHERE[clear] [string]`email`[clear] [special]=[clear] [string]'test@example.com'[clear] [keyword]AND[clear] [string]`foo`[clear] [special]=[clear] [string]'BAR'[clear] [keyword]OR[clear] [number]1[clear][special]=[clear][number]1[clear]") + .toBe("[keyword]SELECT[clear] [function]COUNT[clear][bracket]([clear][identifier]id[clear][bracket])[clear][special],[clear] [identifier]`id`[clear][special],[clear] [identifier]`username`[clear] [keyword]FROM[clear] [identifier]`users`[clear] [keyword]WHERE[clear] [identifier]`email`[clear] [special]=[clear] [string]'test@example.com'[clear] [keyword]AND[clear] [identifier]`foo`[clear] [special]=[clear] [string]'BAR'[clear] [keyword]OR[clear] [number]1[clear][special]=[clear][number]1[clear]") }) it('query with identifiers without apostrophes', () => { expect(hlUni('SELECT id FROM users')) - .toBe('[keyword]SELECT[clear] id [keyword]FROM[clear] users') + .toBe('[keyword]SELECT[clear] [identifier]id[clear] [keyword]FROM[clear] [identifier]users[clear]') }) it('query with nested segments (minus in string)', () => { expect(hlUni('DROP PROCEDURE IF EXISTS `some-database`.`some-table`;')) - .toBe('[keyword]DROP[clear] [keyword]PROCEDURE[clear] [keyword]IF[clear] [keyword]EXISTS[clear] [string]`some-database`[clear].[string]`some-table`[clear][special];[clear]') + .toBe('[keyword]DROP[clear] [keyword]PROCEDURE[clear] [keyword]IF[clear] [keyword]EXISTS[clear] [identifier]`some-database`[clear][special].[clear][identifier]`some-table`[clear][special];[clear]') }) it('multiple queries', () => { expect(hlUni('SELECT * FROM a;SELECT * FROM b;')) - .toBe('[keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] a[special];[clear][keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] b[special];[clear]') + .toBe('[keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] [identifier]a[clear][special];[clear][keyword]SELECT[clear] [special]*[clear] [keyword]FROM[clear] [identifier]b[clear][special];[clear]') }) it('comment single line', () => { expect(hlUni('-- comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; -- comment 3')) - .toBe('[comment]-- comment 1 "comment" /* still */ comment 2[clear]\n[keyword]SELECT[clear] [string]`not comment`[clear][special];[clear] [comment]-- comment 3[clear]') + .toBe('[comment]-- comment 1 "comment" /* still */ comment 2[clear]\n[keyword]SELECT[clear] [identifier]`not comment`[clear][special];[clear] [comment]-- comment 3[clear]') }) it('comment mysql', () => { expect(hlUni('# comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; # comment 3')) - .toBe('[comment]# comment 1 "comment" /* still */ comment 2[clear]\n[keyword]SELECT[clear] [string]`not comment`[clear][special];[clear] [comment]# comment 3[clear]') + .toBe('[comment]# comment 1 "comment" /* still */ comment 2[clear]\n[keyword]SELECT[clear] [identifier]`not comment`[clear][special];[clear] [comment]# comment 3[clear]') }) it('comment multiline', () => { @@ -150,12 +151,12 @@ describe('html', () => { it('strings (mixing quotes)', () => { expect(hlHtml('\'"`\' "\'`" `"\'`')) - .toBe(''"`' "'`" `"'`') + .toBe(''"`' "'`" `"'`') }) it('strings (scaping quotes)', () => { expect(hlHtml('\'\\\'\' "\\"" `\\``')) - .toBe(''\\'' "\\"" `\\``') + .toBe(''\\'' "\\"" `\\``') }) it('integers', () => { @@ -195,52 +196,52 @@ describe('html', () => { it('alphanumeric', () => { expect(hlHtml('(f1)')) - .toBe('(f1)') + .toBe('(f1)') }) it('functions', () => { expect(hlHtml('COUNT(`id`)')) - .toBe('COUNT(`id`)') + .toBe('COUNT(`id`)') }) it('basic query', () => { expect(hlHtml("SELECT * FROM `users` WHERE `email` = 'test@example.com'")) - .toBe('SELECT * FROM `users` WHERE `email` = 'test@example.com'') + .toBe('SELECT * FROM `users` WHERE `email` = 'test@example.com'') }) it('complex query', () => { expect(hlHtml("SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = 'test@example.com' AND `foo` = 'BAR' OR 1=1")) - .toBe('SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = 'test@example.com' AND `foo` = 'BAR' OR 1=1') + .toBe('SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = 'test@example.com' AND `foo` = 'BAR' OR 1=1') }) it('query with identifiers without apostrophes', () => { expect(hlHtml('SELECT id FROM users')) - .toBe('SELECT id FROM users') + .toBe('SELECT id FROM users') }) it('query with nested segments (minus in string)', () => { expect(hlHtml('DROP PROCEDURE IF EXISTS `some-database`.`some-table`;')) - .toBe('DROP PROCEDURE IF EXISTS `some-database`.`some-table`;') + .toBe('DROP PROCEDURE IF EXISTS `some-database`.`some-table`;') }) it('multiple queries', () => { expect(hlHtml('SELECT * FROM a;SELECT * FROM b;')) - .toBe('SELECT * FROM a;SELECT * FROM b;') + .toBe('SELECT * FROM a;SELECT * FROM b;') }) it('escapes HTML entities', () => { expect(hlHtml("select * from a where b = 'array>';")) - .toBe('select * from a where b = 'array<map<string,string>>';') + .toBe('select * from a where b = 'array<map<string,string>>';') }) it('comment single line', () => { expect(hlHtml('-- comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; -- comment 3')) - .toBe('-- comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; -- comment 3') + .toBe('-- comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; -- comment 3') }) it('comment mysql', () => { expect(hlHtml('# comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; # comment 3')) - .toBe('# comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; # comment 3') + .toBe('# comment 1 "comment" /* still */ comment 2\nSELECT `not comment`; # comment 3') }) it('comment multiline', () => { @@ -259,40 +260,40 @@ describe('getSegments', () => { expect(getSegments("SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = 'test@example.com' AND `foo` = 'BAR' OR 1=1")) .toStrictEqual([ { name: 'keyword', content: 'SELECT' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'function', content: 'COUNT' }, { name: 'bracket', content: '(' }, - { name: 'default', content: 'id' }, + { name: 'identifier', content: 'id' }, { name: 'bracket', content: ')' }, { name: 'special', content: ',' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`id`' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`id`' }, { name: 'special', content: ',' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`username`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`username`' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'FROM' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`users`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`users`' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'WHERE' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`email`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`email`' }, + { name: 'whitespace', content: ' ' }, { name: 'special', content: '=' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'string', content: "'test@example.com'" }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'AND' }, - { name: 'default', content: ' ' }, - { name: 'string', content: '`foo`' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, + { name: 'identifier', content: '`foo`' }, + { name: 'whitespace', content: ' ' }, { name: 'special', content: '=' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'string', content: "'BAR'" }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'keyword', content: 'OR' }, - { name: 'default', content: ' ' }, + { name: 'whitespace', content: ' ' }, { name: 'number', content: '1' }, { name: 'special', content: '=' }, { name: 'number', content: '1' }