Skip to content

Commit 8150d15

Browse files
authored
feat(getSegments): custom highlighter (#57)
1 parent e321ac4 commit 8150d15

File tree

4 files changed

+138
-32
lines changed

4 files changed

+138
-32
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,41 @@ The following options may be passed to the `highlight` function.
8989
}
9090
```
9191

92+
**Get segments for custom highlighter**
93+
```js
94+
const { getSegments } = require('sql-highlight')
95+
96+
const sqlString = "SELECT `id`, `username` FROM `users` WHERE `email` = '[email protected]'"
97+
98+
const segments = getSegments(sqlString)
99+
100+
console.log(segments)
101+
```
102+
103+
**Output:**
104+
```js
105+
[
106+
{ name: 'keyword', content: 'SELECT' },
107+
{ name: 'default', content: ' ' },
108+
{ name: 'string', content: '`id`' },
109+
{ name: 'special', content: ',' },
110+
{ name: 'default', content: ' ' },
111+
{ name: 'string', content: '`username`' },
112+
{ name: 'default', content: ' ' },
113+
{ name: 'keyword', content: 'FROM' },
114+
{ name: 'default', content: ' ' },
115+
{ name: 'string', content: '`users`' },
116+
{ name: 'default', content: ' ' },
117+
{ name: 'keyword', content: 'WHERE' },
118+
{ name: 'default', content: ' ' },
119+
{ name: 'string', content: '`email`' },
120+
{ name: 'default', content: ' ' },
121+
{ name: 'special', content: '=' },
122+
{ name: 'default', content: ' ' },
123+
{ name: 'string', content: "'[email protected]'" }
124+
]
125+
```
126+
92127
## Contributing
93128

94129
See the [contribution guidelines](CONTRIBUTING.md).

index.test.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { highlight } = require('./lib')
1+
const { highlight, getSegments } = require('./lib')
22

33
const OPTIONS = {
44
colors: {
@@ -182,3 +182,49 @@ describe('html', () => {
182182
.toBe('<span class="sql-hl-keyword">SELECT</span> id <span class="sql-hl-keyword">FROM</span> users')
183183
})
184184
})
185+
186+
describe('getSegments', () => {
187+
it('complex query', () => {
188+
expect(getSegments("SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = '[email protected]' AND `foo` = 'BAR' OR 1=1"))
189+
.toStrictEqual([
190+
{ name: 'keyword', content: 'SELECT' },
191+
{ name: 'default', content: ' ' },
192+
{ name: 'function', content: 'COUNT' },
193+
{ name: 'bracket', content: '(' },
194+
{ name: 'default', content: 'id' },
195+
{ name: 'bracket', content: ')' },
196+
{ name: 'special', content: ',' },
197+
{ name: 'default', content: ' ' },
198+
{ name: 'string', content: '`id`' },
199+
{ name: 'special', content: ',' },
200+
{ name: 'default', content: ' ' },
201+
{ name: 'string', content: '`username`' },
202+
{ name: 'default', content: ' ' },
203+
{ name: 'keyword', content: 'FROM' },
204+
{ name: 'default', content: ' ' },
205+
{ name: 'string', content: '`users`' },
206+
{ name: 'default', content: ' ' },
207+
{ name: 'keyword', content: 'WHERE' },
208+
{ name: 'default', content: ' ' },
209+
{ name: 'string', content: '`email`' },
210+
{ name: 'default', content: ' ' },
211+
{ name: 'special', content: '=' },
212+
{ name: 'default', content: ' ' },
213+
{ name: 'string', content: "'[email protected]'" },
214+
{ name: 'default', content: ' ' },
215+
{ name: 'keyword', content: 'AND' },
216+
{ name: 'default', content: ' ' },
217+
{ name: 'string', content: '`foo`' },
218+
{ name: 'default', content: ' ' },
219+
{ name: 'special', content: '=' },
220+
{ name: 'default', content: ' ' },
221+
{ name: 'string', content: "'BAR'" },
222+
{ name: 'default', content: ' ' },
223+
{ name: 'keyword', content: 'OR' },
224+
{ name: 'default', content: ' ' },
225+
{ name: 'number', content: '1' },
226+
{ name: 'special', content: '=' },
227+
{ name: 'number', content: '1' }
228+
])
229+
})
230+
})

lib/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ declare module 'sql-highlight' {
1212
clear: string;
1313
};
1414
}
15+
16+
export interface Segment {
17+
name: string;
18+
content: string;
19+
}
1520

21+
export function getSegments(sqlString: string): Array<Segment>;
1622
export function highlight(sqlString: string, options?: HighlightOptions): string;
1723
}

lib/index.js

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const DEFAULT_OPTIONS = {
1818

1919
const SPLIT_CHARS = '[^a-zA-Z_]'
2020

21+
const DEFAULT_KEYWORD = 'default'
22+
2123
const highlighters = [
2224
{
2325
name: 'keyword',
@@ -47,9 +49,7 @@ const highlighters = [
4749
}
4850
]
4951

50-
function highlight (sqlString, options) {
51-
options = Object.assign({}, DEFAULT_OPTIONS, options)
52-
52+
function getSegments (sqlString) {
5353
const matches = []
5454

5555
for (const hl of highlighters) {
@@ -70,53 +70,72 @@ function highlight (sqlString, options) {
7070
}
7171
}
7272

73+
const trimmedText = hl.trimEnd
74+
? text.substring(0, text.length - hl.trimEnd)
75+
: text
7376
matches.push({
7477
name: hl.name,
7578
start: match.index + boringLength,
76-
length: (hl.trimEnd ? text.substr(0, text.length - hl.trimEnd) : text).length
79+
length: trimmedText.length
7780
})
7881
}
7982
}
8083

8184
const sortedMatches = matches.slice().sort((a, b) => a.start - b.start)
8285

8386
// filter/exclude nested matches (matches within the last match)
84-
const filteredMatches = []
87+
const segments = []
8588
let upperBound = 0
8689
for (let i = 0; i < sortedMatches.length; i++) {
87-
if (sortedMatches[i].start >= upperBound) {
88-
filteredMatches.push(sortedMatches[i])
89-
upperBound = sortedMatches[i].start + sortedMatches[i].length
90-
}
91-
}
92-
93-
let highlighted = ''
90+
if (sortedMatches[i].start < upperBound) { break }
9491

95-
for (let i = 0; i < filteredMatches.length; i++) {
96-
const match = filteredMatches[i]
97-
const nextMatch = filteredMatches[i + 1]
92+
// If no match, add a default segment
93+
if (sortedMatches[i].start > upperBound) {
94+
segments.push({
95+
name: DEFAULT_KEYWORD,
96+
content: sqlString.substring(upperBound, sortedMatches[i].start)
97+
})
98+
}
9899

99-
const stringMatch = sqlString.substr(match.start, match.length)
100+
segments.push({
101+
name: sortedMatches[i].name,
102+
content: sqlString.substring(
103+
sortedMatches[i].start,
104+
sortedMatches[i].start + sortedMatches[i].length
105+
)
106+
})
107+
upperBound = sortedMatches[i].start + sortedMatches[i].length
108+
}
100109

101-
if (options.html) {
102-
highlighted += `<span class="${options.classPrefix}${match.name}">`
103-
highlighted += stringMatch
104-
highlighted += '</span>'
105-
} else {
106-
highlighted += options.colors[match.name]
107-
highlighted += stringMatch
108-
highlighted += options.colors.clear
109-
}
110-
if (nextMatch) {
111-
highlighted += sqlString.substr(match.start + match.length, nextMatch.start - (match.start + match.length))
112-
} else if (sqlString.length > (match.start + match.length)) {
113-
highlighted += sqlString.substr(match.start + match.length)
114-
}
110+
if (upperBound < sqlString.length - 1) {
111+
segments.push({
112+
name: DEFAULT_KEYWORD,
113+
content: sqlString.substring(
114+
upperBound,
115+
upperBound + sqlString.length + 1
116+
)
117+
})
115118
}
119+
return segments
120+
}
116121

117-
return highlighted
122+
function highlight (sqlString, options) {
123+
options = Object.assign({}, DEFAULT_OPTIONS, options)
124+
125+
return getSegments(sqlString)
126+
.map(({ name, content }) => {
127+
if (name === DEFAULT_KEYWORD) {
128+
return content
129+
}
130+
if (options.html) {
131+
return `<span class="${options.classPrefix}${name}">${content}</span>`
132+
}
133+
return options.colors[name] + content + options.colors.clear
134+
})
135+
.join('')
118136
}
119137

120138
module.exports = {
139+
getSegments,
121140
highlight
122141
}

0 commit comments

Comments
 (0)