Skip to content

Commit c47a2db

Browse files
authored
Merge pull request #835 from plotly/decode-some-html-entities
Decode some html entities
2 parents 21a18ce + 9ba61a3 commit c47a2db

File tree

9 files changed

+121
-56
lines changed

9 files changed

+121
-56
lines changed

src/constants/string_mappings.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
// N.B. HTML entities are listed without the leading '&' and trailing ';'
13+
14+
module.exports = {
15+
16+
entityToUnicode: {
17+
'mu': 'μ',
18+
'amp': '&',
19+
'lt': '<',
20+
'gt': '>',
21+
'nbsp': ' ',
22+
'times': '×',
23+
'plusmn': '±',
24+
'deg': '°'
25+
},
26+
27+
unicodeToEntity: {
28+
'&': 'amp',
29+
'<': 'lt',
30+
'>': 'gt',
31+
'"': 'quot',
32+
'\'': '#x27',
33+
'\/': '#x2F'
34+
}
35+
36+
};

src/lib/html2unicode.js

+3-8
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@
1010
'use strict';
1111

1212
var toSuperScript = require('superscript-text');
13-
14-
var ENTITIES = {
15-
'mu': 'μ',
16-
'amp': '&',
17-
'lt': '<',
18-
'gt': '>'
19-
};
13+
var stringMappings = require('../constants/string_mappings');
2014

2115
function fixSuperScript(x) {
2216
var idx = 0;
@@ -40,6 +34,7 @@ function stripTags(x) {
4034
}
4135

4236
function fixEntities(x) {
37+
var entityToUnicode = stringMappings.entityToUnicode;
4338
var idx = 0;
4439

4540
while((idx = x.indexOf('&', idx)) >= 0) {
@@ -49,7 +44,7 @@ function fixEntities(x) {
4944
continue;
5045
}
5146

52-
var entity = ENTITIES[x.slice(idx + 1, nidx)];
47+
var entity = entityToUnicode[x.slice(idx + 1, nidx)];
5348
if(entity) {
5449
x = x.slice(0, idx) + entity + x.slice(nidx + 1);
5550
} else {

src/lib/svg_text_utils.js

+44-18
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@
1111

1212
/* global MathJax:false */
1313

14-
var Plotly = require('../plotly');
1514
var d3 = require('d3');
1615

1716
var Lib = require('../lib');
1817
var xmlnsNamespaces = require('../constants/xmlns_namespaces');
19-
20-
var util = module.exports = {};
18+
var stringMappings = require('../constants/string_mappings');
2119

2220
// Append SVG
2321

@@ -45,7 +43,7 @@ d3.selection.prototype.appendSVG = function(_svgString) {
4543

4644
// Text utilities
4745

48-
util.html_entity_decode = function(s) {
46+
exports.html_entity_decode = function(s) {
4947
var hiddenDiv = d3.select('body').append('div').style({display: 'none'}).html('');
5048
var replaced = s.replace(/(&[^;]*;)/gi, function(d) {
5149
if(d === '&lt;') { return '&#60;'; } // special handling for brackets
@@ -56,7 +54,7 @@ util.html_entity_decode = function(s) {
5654
return replaced;
5755
};
5856

59-
util.xml_entity_encode = function(str) {
57+
exports.xml_entity_encode = function(str) {
6058
return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&amp;');
6159
};
6260

@@ -66,10 +64,11 @@ function getSize(_selection, _dimension) {
6664
return _selection.node().getBoundingClientRect()[_dimension];
6765
}
6866

69-
util.convertToTspans = function(_context, _callback) {
67+
exports.convertToTspans = function(_context, _callback) {
7068
var str = _context.text();
7169
var converted = convertToSVG(str);
7270
var that = _context;
71+
7372
// Until we get tex integrated more fully (so it can be used along with non-tex)
7473
// allow some elements to prohibit it by attaching 'data-notex' to the original
7574
var tex = (!that.attr('data-notex')) && converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/);
@@ -112,7 +111,7 @@ util.convertToTspans = function(_context, _callback) {
112111
}
113112

114113
if(tex) {
115-
var td = Plotly.Lib.getPlotDiv(that.node());
114+
var td = Lib.getPlotDiv(that.node());
116115
((td && td._promises) || []).push(new Promise(function(resolve) {
117116
that.style({visibility: 'hidden'});
118117
var config = {fontSize: parseInt(that.style('font-size'), 10)};
@@ -195,7 +194,7 @@ function cleanEscapesForTex(s) {
195194
}
196195

197196
function texToSVG(_texString, _config, _callback) {
198-
var randomID = 'math-output-' + Plotly.Lib.randstr([], 64);
197+
var randomID = 'math-output-' + Lib.randstr([], 64);
199198
var tmpDiv = d3.select('body').append('div')
200199
.attr({id: randomID})
201200
.style({visibility: 'hidden', position: 'absolute'})
@@ -236,22 +235,48 @@ var PROTOCOLS = ['http:', 'https:', 'mailto:'];
236235

237236
var STRIP_TAGS = new RegExp('</?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g');
238237

239-
util.plainText = function(_str) {
238+
var ENTITY_TO_UNICODE = Object.keys(stringMappings.entityToUnicode).map(function(k) {
239+
return {
240+
regExp: new RegExp('&' + k + ';', 'g'),
241+
sub: stringMappings.entityToUnicode[k]
242+
};
243+
});
244+
245+
var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function(k) {
246+
return {
247+
regExp: new RegExp(k, 'g'),
248+
sub: '&' + stringMappings.unicodeToEntity[k] + ';'
249+
};
250+
});
251+
252+
exports.plainText = function(_str) {
240253
// strip out our pseudo-html so we have a readable
241254
// version to put into text fields
242255
return (_str || '').replace(STRIP_TAGS, ' ');
243256
};
244257

258+
function replaceFromMapObject(_str, list) {
259+
var out = _str || '';
260+
261+
for(var i = 0; i < list.length; i++) {
262+
var item = list[i];
263+
out = out.replace(item.regExp, item.sub);
264+
}
265+
266+
return out;
267+
}
268+
269+
function convertEntities(_str) {
270+
return replaceFromMapObject(_str, ENTITY_TO_UNICODE);
271+
}
272+
245273
function encodeForHTML(_str) {
246-
return (_str || '').replace(/&/g, '&amp;')
247-
.replace(/</g, '&lt;')
248-
.replace(/>/g, '&gt;')
249-
.replace(/"/g, '&quot;')
250-
.replace(/'/g, '&#x27;')
251-
.replace(/\//g, '&#x2F;');
274+
return replaceFromMapObject(_str, UNICODE_TO_ENTITY);
252275
}
253276

254277
function convertToSVG(_str) {
278+
_str = convertEntities(_str);
279+
255280
var result = _str
256281
.split(/(<[^<>]*>)/).map(function(d) {
257282
var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
@@ -270,6 +295,7 @@ function convertToSVG(_str) {
270295
* resurrect it.
271296
*/
272297
extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i);
298+
273299
// anchor and br are the only ones that don't turn into a tspan
274300
if(tag === 'a') {
275301
if(close) return '</a>';
@@ -316,7 +342,7 @@ function convertToSVG(_str) {
316342
}
317343
}
318344
else {
319-
return Plotly.util.xml_entity_encode(d).replace(/</g, '&lt;');
345+
return exports.xml_entity_encode(d).replace(/</g, '&lt;');
320346
}
321347
});
322348

@@ -397,7 +423,7 @@ function alignHTMLWith(_base, container, options) {
397423

398424
// Editable title
399425

400-
util.makeEditable = function(context, _delegate, options) {
426+
exports.makeEditable = function(context, _delegate, options) {
401427
if(!options) options = {};
402428
var that = this;
403429
var dispatch = d3.dispatch('edit', 'input', 'cancel');
@@ -431,7 +457,7 @@ util.makeEditable = function(context, _delegate, options) {
431457
}
432458

433459
function appendEditable() {
434-
var plotDiv = d3.select(Plotly.Lib.getPlotDiv(that.node())),
460+
var plotDiv = d3.select(Lib.getPlotDiv(that.node())),
435461
container = plotDiv.select('.svg-container'),
436462
div = container.append('div');
437463
div.classed('plugin-editable editable', true)

src/plots/gl2d/convert.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
var Plotly = require('../../plotly');
1313

14-
var htmlToUnicode = require('../../lib/html2unicode');
14+
var convertHTMLToUnicode = require('../../lib/html2unicode');
1515
var str2RGBArray = require('../../lib/str2rgbarray');
1616

1717
function Axes2DOptions(scene) {
@@ -115,7 +115,7 @@ proto.merge = function(options) {
115115

116116
for(j = 0; j <= 2; j += 2) {
117117
this.labelEnable[i + j] = false;
118-
this.labels[i + j] = htmlToUnicode(axTitle);
118+
this.labels[i + j] = convertHTMLToUnicode(axTitle);
119119
this.labelColor[i + j] = str2RGBArray(ax.titlefont.color);
120120
this.labelFont[i + j] = ax.titlefont.family;
121121
this.labelSize[i + j] = ax.titlefont.size;

src/plots/gl2d/scene2d.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var createSelectBox = require('gl-select-box');
1818

1919
var createOptions = require('./convert');
2020
var createCamera = require('./camera');
21-
var htmlToUnicode = require('../../lib/html2unicode');
21+
var convertHTMLToUnicode = require('../../lib/html2unicode');
2222
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
2323

2424
var AXES = ['xaxis', 'yaxis'];
@@ -231,7 +231,7 @@ proto.computeTickMarks = function() {
231231
for(var i = 0; i < nextTicks[j].length; ++i) {
232232
// TODO add support for '\n' in gl-plot2d,
233233
// For now, replace '\n' with ' '
234-
nextTicks[j][i].text = htmlToUnicode(nextTicks[j][i].text + '').replace(/\n/g, ' ');
234+
nextTicks[j][i].text = convertHTMLToUnicode(nextTicks[j][i].text + '').replace(/\n/g, ' ');
235235
}
236236
}
237237

src/plots/gl3d/layout/convert.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
'use strict';
1111

1212
var arrtools = require('arraytools');
13-
var convertHTML = require('../../../lib/html2unicode');
13+
var convertHTMLToUnicode = require('../../../lib/html2unicode');
1414
var str2RgbaArray = require('../../../lib/str2rgbarray');
1515

1616
var arrayCopy1D = arrtools.copy1D;
@@ -77,7 +77,7 @@ proto.merge = function(sceneLayout) {
7777
var axes = sceneLayout[AXES_NAMES[i]];
7878

7979
/////// Axes labels //
80-
opts.labels[i] = convertHTML(axes.title);
80+
opts.labels[i] = convertHTMLToUnicode(axes.title);
8181
if('titlefont' in axes) {
8282
if(axes.titlefont.color) opts.labelColor[i] = str2RgbaArray(axes.titlefont.color);
8383
if(axes.titlefont.family) opts.labelFont[i] = axes.titlefont.family;

src/plots/gl3d/layout/tick_marks.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
module.exports = computeTickMarks;
1515

1616
var Plotly = require('../../../plotly');
17-
var convertHTML = require('../../../lib/html2unicode');
17+
var convertHTMLToUnicode = require('../../../lib/html2unicode');
1818

1919
var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis'];
2020

@@ -70,7 +70,7 @@ function computeTickMarks(scene) {
7070
var dataTicks = Plotly.Axes.calcTicks(axes);
7171
for(var j = 0; j < dataTicks.length; ++j) {
7272
dataTicks[j].x = dataTicks[j].x * scene.dataScale[i];
73-
dataTicks[j].text = convertHTML(dataTicks[j].text);
73+
dataTicks[j].text = convertHTMLToUnicode(dataTicks[j].text);
7474
}
7575
ticks[i] = dataTicks;
7676

test/image/mocks/axes_enumerated_ticks.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"xaxis": {
3434
"ticktext": [
3535
"<span style=\"fill:green\">green</span> eggs",
36-
"& ham",
36+
"&amp; ham",
3737
"H<sub>2</sub>O",
3838
"Gorgonzola"
3939
],

test/jasmine/tests/svg_text_utils_test.js

+29-21
Original file line numberDiff line numberDiff line change
@@ -121,34 +121,33 @@ describe('svg+text utils', function() {
121121
});
122122

123123
it('wrap XSS attacks in href', function() {
124-
var node = mockTextSVGElement(
124+
var textCases = [
125+
'<a href="XSS\" onmouseover="alert(1)\" style="font-size:300px">Subtitle</a>',
125126
'<a href="XSS" onmouseover="alert(1)" style="font-size:300px">Subtitle</a>'
126-
);
127-
128-
expect(node.text()).toEqual('Subtitle');
129-
assertAnchorAttrs(node);
130-
assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px');
131-
});
127+
];
132128

133-
it('wrap XSS attacks with quoted entities in href', function() {
134-
var node = mockTextSVGElement(
135-
'<a href="XSS&quot; onmouseover=&quot;alert(1)&quot; style=&quot;font-size:300px">Subtitle</a>'
136-
);
129+
textCases.forEach(function(textCase) {
130+
var node = mockTextSVGElement(textCase);
137131

138-
console.log(node.select('a').attr('xlink:href'));
139-
expect(node.text()).toEqual('Subtitle');
140-
assertAnchorAttrs(node);
141-
assertAnchorLink(node, 'XSS&quot; onmouseover=&quot;alert(1)&quot; style=&quot;font-size:300px');
132+
expect(node.text()).toEqual('Subtitle');
133+
assertAnchorAttrs(node);
134+
assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px');
135+
});
142136
});
143137

144138
it('should keep query parameters in href', function() {
145-
var node = mockTextSVGElement(
146-
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>'
147-
);
139+
var textCases = [
140+
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>',
141+
'<a href="https://abc.com/myFeature.jsp?name=abc&amp;pwd=def">abc.com?shared-key</a>'
142+
];
148143

149-
assertAnchorAttrs(node);
150-
expect(node.text()).toEqual('abc.com?shared-key');
151-
assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def');
144+
textCases.forEach(function(textCase) {
145+
var node = mockTextSVGElement(textCase);
146+
147+
assertAnchorAttrs(node);
148+
expect(node.text()).toEqual('abc.com?shared-key');
149+
assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def');
150+
});
152151
});
153152

154153
it('allow basic spans', function() {
@@ -195,5 +194,14 @@ describe('svg+text utils', function() {
195194
expect(node.text()).toEqual('text');
196195
assertTspanStyle(node, 'quoted: yeah&\';;');
197196
});
197+
198+
it('decode some HTML entities in text', function() {
199+
var node = mockTextSVGElement(
200+
'100&mu; &amp; &lt; 10 &gt; 0 &nbsp;' +
201+
'100 &times; 20 &plusmn; 0.5 &deg;'
202+
);
203+
204+
expect(node.text()).toEqual('100μ & < 10 > 0  100 × 20 ± 0.5 °');
205+
});
198206
});
199207
});

0 commit comments

Comments
 (0)