Skip to content

Commit 25677d4

Browse files
wkeesescriptcoded
authored andcommitted
feat: add way to style identifiers
Replace "default" segment with "whitespace" and "identifier" segments, with fallback to "unknown" segment. Also, classify backticked identifiers like `foo` as "identifier" rather than "string". This allows for identifiers to be styled independently from strings and whitespace. It also simplifies getSegments() from 30 lines down to 5, by removing the special-case code for the "default" segment. BREAKING CHANGE: The `default` segment has been split into `identifier` and `whitespace` segments. There's also a new `unknown` segment that will only show up for malformed SQL such as an unclosed string. However, the highlight() function works largely the same as before, both normal mode and HTML mode, except for the bug fix to stop classifying identifiers as strings. In other words, SQL like select * from EMP where NAME="John Smith" will get highlighted the same as before, i.e. no syntax highlighting for EMP or NAME. Fixes #147
1 parent ecd93d3 commit 25677d4

File tree

4 files changed

+87
-111
lines changed

4 files changed

+87
-111
lines changed

README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ document.body.innerHTML += highlighted
5959
**Output:**
6060
```html
6161
<span class="sql-hl-keyword">SELECT</span>
62-
<span class="sql-hl-string">`id`</span>
62+
<span class="sql-hl-identifier">`id`</span>
6363
<span class="sql-hl-special">,</span>
64-
<span class="sql-hl-string">`username`</span>
64+
<span class="sql-hl-identifier">`username`</span>
6565
<span class="sql-hl-keyword">FROM</span>
66-
<span class="sql-hl-string">`users`</span>
66+
<span class="sql-hl-identifier">`users`</span>
6767
<span class="sql-hl-keyword">WHERE</span>
68-
<span class="sql-hl-string">`email`</span>
68+
<span class="sql-hl-identifier">`email`</span>
6969
<span class="sql-hl-special">=</span>
7070
<span class="sql-hl-string">'[email protected]'</span>
7171
```
@@ -112,22 +112,22 @@ console.log(segments)
112112
```js
113113
[
114114
{ name: 'keyword', content: 'SELECT' },
115-
{ name: 'default', content: ' ' },
116-
{ name: 'string', content: '`id`' },
115+
{ name: 'whitespace', content: ' ' },
116+
{ name: 'identifier', content: '`id`' },
117117
{ name: 'special', content: ',' },
118-
{ name: 'default', content: ' ' },
119-
{ name: 'string', content: '`username`' },
120-
{ name: 'default', content: ' ' },
118+
{ name: 'whitespace', content: ' ' },
119+
{ name: 'identifier', content: '`username`' },
120+
{ name: 'whitespace', content: ' ' },
121121
{ name: 'keyword', content: 'FROM' },
122-
{ name: 'default', content: ' ' },
123-
{ name: 'string', content: '`users`' },
124-
{ name: 'default', content: ' ' },
122+
{ name: 'whitespace', content: ' ' },
123+
{ name: 'identifier', content: '`users`' },
124+
{ name: 'whitespace', content: ' ' },
125125
{ name: 'keyword', content: 'WHERE' },
126-
{ name: 'default', content: ' ' },
127-
{ name: 'string', content: '`email`' },
128-
{ name: 'default', content: ' ' },
126+
{ name: 'whitespace', content: ' ' },
127+
{ name: 'identifier', content: '`email`' },
128+
{ name: 'whitespace', content: ' ' },
129129
{ name: 'special', content: '=' },
130-
{ name: 'default', content: ' ' },
130+
{ name: 'whitespace', content: ' ' },
131131
{ name: 'string', content: "'[email protected]'" }
132132
]
133133
```

lib/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ declare module 'sql-highlight' {
88
function: string;
99
number: string;
1010
string: string;
11+
identifier: string;
1112
special: string;
1213
bracket: string;
1314
comment: string;

lib/index.js

Lines changed: 24 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,19 @@ const DEFAULT_OPTIONS = {
1212
function: '\x1b[31m',
1313
number: '\x1b[32m',
1414
string: '\x1b[32m',
15+
identifier: '\x1b[0m',
1516
special: '\x1b[33m',
1617
bracket: '\x1b[33m',
1718
comment: '\x1b[2m\x1b[90m',
1819
clear: '\x1b[0m'
1920
}
2021
}
2122

22-
const DEFAULT_KEYWORD = 'default'
23-
2423
const highlighters = [
2524
/\b(?<number>\d+(?:\.\d+)?)\b/,
2625

2726
// Note: Repeating string escapes like 'sql''server' will also work as they are just repeating strings
28-
/(?<string>'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/,
27+
/(?<string>'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/,
2928

3029
/(?<comment>--[^\n\r]*|#[^\n\r]*|\/\*(?:[^*]|\*(?!\/))*\*\/)/,
3130

@@ -34,54 +33,29 @@ const highlighters = [
3433

3534
/(?<bracket>[()])/,
3635

37-
/(?<special>!=|[=%*/\-+,;:<>])/
38-
]
36+
/(?<special>!=|[=%*/\-+,;:<>.])/,
3937

40-
function getRegexString (regex) {
41-
const str = regex.toString()
42-
return str.replace(/^\/|\/\w*$/g, '')
43-
}
38+
/(?<identifier>\b\w+\b|`(?:[^`\\]|\\.)*`)/,
39+
40+
/(?<whitespace>\s+)/,
41+
42+
/(?<unknown>\.+?)/
43+
]
4444

45-
// Regex of the shape /(.*?)|((?<token1>...)|(?<token2>...)|...|$)/y
45+
// Regex of the shape /(?<token1>...)|(?<token2>...)|.../g
4646
const tokenizer = new RegExp(
47-
'(.*?)(' +
48-
'\\b(?<keyword>' + keywords.join('|') + ')\\b|' +
49-
highlighters.map(getRegexString).join('|') +
50-
'|$)', // $ needed to to match "default" till the end of string
51-
'isy'
47+
[
48+
'\\b(?<keyword>' + keywords.join('|') + ')\\b',
49+
...highlighters.map(regex => regex.source)
50+
].join('|'),
51+
'gis'
5252
)
5353

5454
function getSegments (sqlString) {
55-
const segments = []
56-
let match
57-
58-
// Reset the starting position
59-
tokenizer.lastIndex = 0
60-
61-
// This is probably the one time when an assignment inside a condition makes sense
62-
// eslint-disable-next-line no-cond-assign
63-
while (match = tokenizer.exec(sqlString)) {
64-
if (match[1]) {
65-
segments.push({
66-
name: DEFAULT_KEYWORD,
67-
content: match[1]
68-
})
69-
}
70-
71-
if (match[2]) {
72-
const name = Object.keys(match.groups).find(key => match.groups[key])
73-
segments.push({
74-
name,
75-
content: match.groups[name]
76-
})
77-
}
78-
79-
// Stop at the end of string
80-
if (match.index + match[0].length >= sqlString.length) {
81-
break
82-
}
83-
}
84-
55+
const segments = Array.from(sqlString.matchAll(tokenizer), match => ({
56+
name: Object.keys(match.groups).find(key => match.groups[key]),
57+
content: match[0]
58+
}))
8559
return segments
8660
}
8761

@@ -90,14 +64,14 @@ function highlight (sqlString, options) {
9064

9165
return getSegments(sqlString)
9266
.map(({ name, content }) => {
93-
if (name === DEFAULT_KEYWORD) {
94-
return content
95-
}
9667
if (options.html) {
9768
const escapedContent = options.htmlEscaper(content)
98-
return `<span class="${options.classPrefix}${name}">${escapedContent}</span>`
69+
return name === 'whitespace' ? escapedContent : `<span class="${options.classPrefix}${name}">${escapedContent}</span>`
70+
}
71+
if (options.colors[name]) {
72+
return options.colors[name] + content + options.colors.clear
9973
}
100-
return options.colors[name] + content + options.colors.clear
74+
return content
10175
})
10276
.join('')
10377
}

0 commit comments

Comments
 (0)