Skip to content

Commit 672fc9c

Browse files
committed
feat: escape HTML entities
Escape HTML entities within generated HTML tags. Add option to allow user to provide custom escaper. Could be used to disable escaping completely. While this is considered a bug the solution requires a feature implementation to integrate nicely, hence the feat and not fix label. Refs: #90
1 parent 23269a1 commit 672fc9c

File tree

7 files changed

+135
-9
lines changed

7 files changed

+135
-9
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ console.log(highlighted)
4040

4141
**Output:**
4242

43-
![Screenshot](screenshot.png)
43+
![Screenshot](screenshot.png
4444

4545
**HTML mode:**
4646

@@ -76,6 +76,7 @@ The following options may be passed to the `highlight` function.
7676
| Option | Value | Default | Description |
7777
| --- | --- | --- | --- |
7878
| html | `boolean` | `false` | Set to true to render HTML instead of Unicode.
79+
| htmlEscaper | `(str: string) => string` | Basic escaper | Function to escape HTML entities. Uses a basic escaper by default. If HTML mode is used in a browser environment this could be useful to escape strings using the DOM.
7980
| classPrefix | `string` | `'sql-hl-'` | Prefix to prepend to classes for HTML span-tags. Is appended with entity name.
8081
| colors | `Object` | _See below_* | What color codes to use for Unicode rendering. A list of basic color codes can be found [here](https://docs.rs/embedded-text/0.4.0/embedded_text/style/index.html#standard-color-codes).
8182

escapeHtml.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const escapeHtml = require('./lib/escapeHtml')
2+
3+
describe('escapeHtml', () => {
4+
it('does not escape strings without special characters', () => {
5+
expect(escapeHtml('Hello, world!'))
6+
.toBe('Hello, world!')
7+
})
8+
9+
it('escapes brackets', () => {
10+
expect(escapeHtml('<br />'))
11+
.toBe('&lt;br /&gt;')
12+
})
13+
14+
it('escapes quotes', () => {
15+
expect(escapeHtml('"\''))
16+
.toBe('&quot;&#39;')
17+
})
18+
19+
it('escapes ampersands', () => {
20+
expect(escapeHtml('&'))
21+
.toBe('&amp;')
22+
})
23+
24+
it('keeps leading and trailing test', () => {
25+
expect(escapeHtml('leading & trailing'))
26+
.toBe('leading &amp; trailing')
27+
})
28+
29+
it('escapes multiple segments', () => {
30+
expect(escapeHtml('> to >'))
31+
.toBe('&gt; to &gt;')
32+
})
33+
34+
it('escapes complex string', () => {
35+
expect(escapeHtml('<div onClick="javascript:alert(\'inject\')">Hello, world! This, that & those.</div>'))
36+
.toBe('&lt;div onClick=&quot;javascript:alert(&#39;inject&#39;)&quot;&gt;Hello, world! This, that &amp; those.&lt;/div&gt;')
37+
})
38+
})

index.test.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,22 +119,22 @@ describe('unicode', () => {
119119
describe('html', () => {
120120
it('strings (single quotes)', () => {
121121
expect(hlHtml("'Hello, world!'"))
122-
.toBe('<span class="sql-hl-string">\'Hello, world!\'</span>')
122+
.toBe('<span class="sql-hl-string">&#39;Hello, world!&#39;</span>')
123123
})
124124

125125
it('strings (double quotes)', () => {
126126
expect(hlHtml('"Hello, world!"'))
127-
.toBe('<span class="sql-hl-string">"Hello, world!"</span>')
127+
.toBe('<span class="sql-hl-string">&quot;Hello, world!&quot;</span>')
128128
})
129129

130130
it('strings (mixing quotes)', () => {
131131
expect(hlHtml('\'"`\' "\'`" `"\'`'))
132-
.toBe('<span class="sql-hl-string">\'"`\'</span> <span class="sql-hl-string">"\'`"</span> <span class="sql-hl-string">`"\'`</span>')
132+
.toBe('<span class="sql-hl-string">&#39;&quot;`&#39;</span> <span class="sql-hl-string">&quot;&#39;`&quot;</span> <span class="sql-hl-string">`&quot;&#39;`</span>')
133133
})
134134

135135
it('strings (scaping quotes)', () => {
136136
expect(hlHtml('\'\\\'\' "\\"" `\\``'))
137-
.toBe('<span class="sql-hl-string">\'\\\'\'</span> <span class="sql-hl-string">"\\""</span> <span class="sql-hl-string">`\\``</span>')
137+
.toBe('<span class="sql-hl-string">&#39;\\&#39;&#39;</span> <span class="sql-hl-string">&quot;\\&quot;&quot;</span> <span class="sql-hl-string">`\\``</span>')
138138
})
139139

140140
it('integers', () => {
@@ -169,7 +169,7 @@ describe('html', () => {
169169

170170
it('numbers within strings', () => {
171171
expect(hlHtml("'34'"))
172-
.toBe("<span class=\"sql-hl-string\">'34'</span>")
172+
.toBe('<span class="sql-hl-string">&#39;34&#39;</span>')
173173
})
174174

175175
it('alphanumeric', () => {
@@ -184,12 +184,12 @@ describe('html', () => {
184184

185185
it('basic query', () => {
186186
expect(hlHtml("SELECT * FROM `users` WHERE `email` = '[email protected]'"))
187-
.toBe("<span class=\"sql-hl-keyword\">SELECT</span> <span class=\"sql-hl-special\">*</span> <span class=\"sql-hl-keyword\">FROM</span> <span class=\"sql-hl-string\">`users`</span> <span class=\"sql-hl-keyword\">WHERE</span> <span class=\"sql-hl-string\">`email`</span> <span class=\"sql-hl-special\">=</span> <span class=\"sql-hl-string\">'[email protected]'</span>")
187+
.toBe('<span class="sql-hl-keyword">SELECT</span> <span class="sql-hl-special">*</span> <span class="sql-hl-keyword">FROM</span> <span class="sql-hl-string">`users`</span> <span class="sql-hl-keyword">WHERE</span> <span class="sql-hl-string">`email`</span> <span class="sql-hl-special">=</span> <span class="sql-hl-string">&#39;[email protected]&#39;</span>')
188188
})
189189

190190
it('complex query', () => {
191191
expect(hlHtml("SELECT COUNT(id), `id`, `username` FROM `users` WHERE `email` = '[email protected]' AND `foo` = 'BAR' OR 1=1"))
192-
.toBe("<span class=\"sql-hl-keyword\">SELECT</span> <span class=\"sql-hl-function\">COUNT</span><span class=\"sql-hl-bracket\">(</span>id<span class=\"sql-hl-bracket\">)</span><span class=\"sql-hl-special\">,</span> <span class=\"sql-hl-string\">`id`</span><span class=\"sql-hl-special\">,</span> <span class=\"sql-hl-string\">`username`</span> <span class=\"sql-hl-keyword\">FROM</span> <span class=\"sql-hl-string\">`users`</span> <span class=\"sql-hl-keyword\">WHERE</span> <span class=\"sql-hl-string\">`email`</span> <span class=\"sql-hl-special\">=</span> <span class=\"sql-hl-string\">'[email protected]'</span> <span class=\"sql-hl-keyword\">AND</span> <span class=\"sql-hl-string\">`foo`</span> <span class=\"sql-hl-special\">=</span> <span class=\"sql-hl-string\">'BAR'</span> <span class=\"sql-hl-keyword\">OR</span> <span class=\"sql-hl-number\">1</span><span class=\"sql-hl-special\">=</span><span class=\"sql-hl-number\">1</span>")
192+
.toBe('<span class="sql-hl-keyword">SELECT</span> <span class="sql-hl-function">COUNT</span><span class="sql-hl-bracket">(</span>id<span class="sql-hl-bracket">)</span><span class="sql-hl-special">,</span> <span class="sql-hl-string">`id`</span><span class="sql-hl-special">,</span> <span class="sql-hl-string">`username`</span> <span class="sql-hl-keyword">FROM</span> <span class="sql-hl-string">`users`</span> <span class="sql-hl-keyword">WHERE</span> <span class="sql-hl-string">`email`</span> <span class="sql-hl-special">=</span> <span class="sql-hl-string">&#39;[email protected]&#39;</span> <span class="sql-hl-keyword">AND</span> <span class="sql-hl-string">`foo`</span> <span class="sql-hl-special">=</span> <span class="sql-hl-string">&#39;BAR&#39;</span> <span class="sql-hl-keyword">OR</span> <span class="sql-hl-number">1</span><span class="sql-hl-special">=</span><span class="sql-hl-number">1</span>')
193193
})
194194

195195
it('query with identifiers without apostrophes', () => {
@@ -206,6 +206,11 @@ describe('html', () => {
206206
expect(hlHtml('SELECT * FROM a;SELECT * FROM b;'))
207207
.toBe('<span class="sql-hl-keyword">SELECT</span> <span class="sql-hl-special">*</span> <span class="sql-hl-keyword">FROM</span> a<span class="sql-hl-special">;</span><span class="sql-hl-keyword">SELECT</span> <span class="sql-hl-special">*</span> <span class="sql-hl-keyword">FROM</span> b<span class="sql-hl-special">;</span>')
208208
})
209+
210+
it('escapes HTML entities', () => {
211+
expect(hlHtml("select * from a where b = 'array<map<string,string>>';"))
212+
.toBe('<span class="sql-hl-keyword">select</span> <span class="sql-hl-special">*</span> <span class="sql-hl-keyword">from</span> a <span class="sql-hl-keyword">where</span> b <span class="sql-hl-special">=</span> <span class="sql-hl-string">&#39;array&lt;map&lt;string,string&gt;&gt;&#39;</span><span class="sql-hl-special">;</span>')
213+
})
209214
})
210215

211216
describe('getSegments', () => {
@@ -253,3 +258,20 @@ describe('getSegments', () => {
253258
])
254259
})
255260
})
261+
262+
describe('custom escaper', () => {
263+
it('uses default escaper', () => {
264+
expect(highlight("'array<map<string,string>>'", { html: true }))
265+
.toBe('<span class="sql-hl-string">&#39;array&lt;map&lt;string,string&gt;&gt;&#39;</span>')
266+
})
267+
268+
it('works with dud escaper', () => {
269+
expect(highlight("'array<map<string,string>>'", { html: true, htmlEscaper: (s) => s }))
270+
.toBe('<span class="sql-hl-string">\'array<map<string,string>>\'</span>')
271+
})
272+
273+
it('works with bad escaper', () => {
274+
expect(highlight("'array<map<string,string>>'", { html: true, htmlEscaper: () => 'foobar' }))
275+
.toBe('<span class="sql-hl-string">foobar</span>')
276+
})
277+
})

lib/escapeHtml.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Simplified version of the escape-html library which can be found at
3+
* https://github.com/component/escape-html
4+
*
5+
* Original license:
6+
* (The MIT License)
7+
*
8+
* Copyright (c) 2012-2013 TJ Holowaychuk
9+
* Copyright (c) 2015 Andreas Lubbe
10+
* Copyright (c) 2015 Tiancheng "Timothy" Gu
11+
*
12+
* Permission is hereby granted, free of charge, to any person obtaining
13+
* a copy of this software and associated documentation files (the
14+
* 'Software'), to deal in the Software without restriction, including
15+
* without limitation the rights to use, copy, modify, merge, publish,
16+
* distribute, sublicense, and/or sell copies of the Software, and to
17+
* permit persons to whom the Software is furnished to do so, subject to
18+
* the following conditions:
19+
*
20+
* The above copyright notice and this permission notice shall be
21+
* included in all copies or substantial portions of the Software.
22+
*
23+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
24+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
26+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
27+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
28+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
29+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30+
*/
31+
32+
const charCodeMap = {
33+
34: '&quot;', // "
34+
38: '&amp;', // &
35+
39: '&#39;', // '
36+
60: '&lt;', // <
37+
62: '&gt;' // >
38+
}
39+
40+
function escapeHtml (str) {
41+
let html = ''
42+
let lastIndex = 0
43+
44+
for (let i = 0; i < str.length; i++) {
45+
const escape = charCodeMap[str.charCodeAt(i)]
46+
if (!escape) continue
47+
48+
if (lastIndex !== i) {
49+
html += str.substring(lastIndex, i)
50+
}
51+
52+
lastIndex = i + 1
53+
html += escape
54+
}
55+
56+
return html + str.substring(lastIndex)
57+
}
58+
59+
module.exports = escapeHtml

lib/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
declare module 'sql-highlight' {
22
export interface HighlightOptions {
33
html?: boolean;
4+
htmlEscaper?: (str: string) => string
45
classPrefix?: string;
56
colors?: {
67
keyword: string;

lib/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use strict'
22

33
const keywords = require('./keywords')
4+
const escapeHtml = require('./escapeHtml')
45

56
const DEFAULT_OPTIONS = {
67
html: false,
8+
htmlEscaper: escapeHtml,
79
classPrefix: 'sql-hl-',
810
colors: {
911
keyword: '\x1b[35m',
@@ -128,7 +130,8 @@ function highlight (sqlString, options) {
128130
return content
129131
}
130132
if (options.html) {
131-
return `<span class="${options.classPrefix}${name}">${content}</span>`
133+
const escapedContent = options.htmlEscaper(content)
134+
return `<span class="${options.classPrefix}${name}">${escapedContent}</span>`
132135
}
133136
return options.colors[name] + content + options.colors.clear
134137
})

test/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ console.log(highlight('SELECT "This is a \\"text\\" test" AS text;'))
2525
console.log(highlight('DROP PROCEDURE IF EXISTS `some-database`.`some-table`;'))
2626

2727
console.log(highlight('SELECT * FROM a;SELECT * FROM b;'))
28+
29+
console.log(highlight("select * from a where b = 'array<map<string,string>>';", { html: true }))

0 commit comments

Comments
 (0)