Skip to content

Commit cadb0a9

Browse files
committed
feat: Add way to style identifiers.
Replace "default" token with "whitespace" and "identifier" tokens, with fallback to "unknown" token. 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" token. Fixes: #147.
1 parent 4a3aa67 commit cadb0a9

File tree

3 files changed

+85
-111
lines changed

3 files changed

+85
-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.js

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ const DEFAULT_OPTIONS = {
1919
}
2020
}
2121

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

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

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

@@ -34,54 +32,29 @@ const highlighters = [
3432

3533
/(?<bracket>[()])/,
3634

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

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

45-
// Regex of the shape /(.*?)|((?<token1>...)|(?<token2>...)|...|$)/y
44+
// Regex of the shape /(?<token1>...)|(?<token2>...)|.../g
4645
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'
46+
[
47+
'\\b(?<keyword>' + keywords.join('|') + ')\\b',
48+
...highlighters.map(regex => regex.source)
49+
].join('|'),
50+
'gis'
5251
)
5352

5453
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-
54+
const segments = Array.from(sqlString.matchAll(tokenizer), match => ({
55+
name: Object.keys(match.groups).find(key => match.groups[key]),
56+
content: match[0]
57+
}))
8558
return segments
8659
}
8760

@@ -90,14 +63,14 @@ function highlight (sqlString, options) {
9063

9164
return getSegments(sqlString)
9265
.map(({ name, content }) => {
93-
if (name === DEFAULT_KEYWORD) {
94-
return content
95-
}
9666
if (options.html) {
9767
const escapedContent = options.htmlEscaper(content)
98-
return `<span class="${options.classPrefix}${name}">${escapedContent}</span>`
68+
return name === 'whitespace' ? escapedContent : `<span class="${options.classPrefix}${name}">${escapedContent}</span>`
69+
}
70+
if (options.colors[name]) {
71+
return options.colors[name] + content + options.colors.clear
9972
}
100-
return options.colors[name] + content + options.colors.clear
73+
return content
10174
})
10275
.join('')
10376
}

0 commit comments

Comments
 (0)