-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Link style #1681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Link style #1681
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b5cfd69
lint svg_text_utils_test grammar
alexcjohnson aa9fc00
fix #1674 - set cursor:pointer on svg text links
alexcjohnson 4e4b8c8
make the whole annotation box a link if that's all the text contains
alexcjohnson 62ef3d7
fix subscript/superscript problem I just created
alexcjohnson 1019d7f
longer wait in sort transform interaction test
alexcjohnson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,19 +221,25 @@ function texToSVG(_texString, _config, _callback) { | |
} | ||
|
||
var TAG_STYLES = { | ||
// would like to use baseline-shift but FF doesn't support it yet | ||
// would like to use baseline-shift for sub/sup but FF doesn't support it | ||
// so we need to use dy along with the uber hacky shift-back-to | ||
// baseline below | ||
sup: 'font-size:70%" dy="-0.6em', | ||
sub: 'font-size:70%" dy="0.3em', | ||
b: 'font-weight:bold', | ||
i: 'font-style:italic', | ||
a: '', | ||
a: 'cursor:pointer', | ||
span: '', | ||
br: '', | ||
em: 'font-style:italic;font-weight:bold' | ||
}; | ||
|
||
// sub/sup: extra tspan with zero-width space to get back to the right baseline | ||
var TAG_CLOSE = { | ||
sup: '<tspan dy="0.42em">​</tspan>', | ||
sub: '<tspan dy="-0.21em">​</tspan>' | ||
}; | ||
|
||
var PROTOCOLS = ['http:', 'https:', 'mailto:']; | ||
|
||
var STRIP_TAGS = new RegExp('</?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g'); | ||
|
@@ -254,6 +260,16 @@ var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function | |
|
||
var NEWLINES = /(\r\n?|\n)/g; | ||
|
||
var SPLIT_TAGS = /(<[^<>]*>)/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐎 |
||
|
||
var ONE_TAG = /<(\/?)([^ >]*)(\s+(.*))?>/i; | ||
|
||
var QUOTEDATTR = '\\s*=\\s*("([^"]*)"|\'([^\']*)\')'; | ||
var STYLEMATCH = new RegExp('(^|[\\s"\'])style' + QUOTEDATTR, 'i'); | ||
var HREFMATCH = new RegExp('(^|[\\s"\'])href' + QUOTEDATTR, 'i'); | ||
|
||
var COLORMATCH = /(^|;)\s*color:/; | ||
|
||
exports.plainText = function(_str) { | ||
// strip out our pseudo-html so we have a readable | ||
// version to put into text fields | ||
|
@@ -280,84 +296,86 @@ function encodeForHTML(_str) { | |
} | ||
|
||
function convertToSVG(_str) { | ||
_str = convertEntities(_str); | ||
|
||
// normalize behavior between IE and others wrt newlines and whitespace:pre | ||
// this combination makes IE barf https://github.com/plotly/plotly.js/issues/746 | ||
// Chrome and FF display \n, \r, or \r\n as a space in this mode. | ||
// I feel like at some point we turned these into <br> but currently we don't so | ||
// I'm just going to cement what we do now in Chrome and FF | ||
_str = _str.replace(NEWLINES, ' '); | ||
_str = convertEntities(_str) | ||
/* | ||
* Normalize behavior between IE and others wrt newlines and whitespace:pre | ||
* this combination makes IE barf https://github.com/plotly/plotly.js/issues/746 | ||
* Chrome and FF display \n, \r, or \r\n as a space in this mode. | ||
* I feel like at some point we turned these into <br> but currently we don't so | ||
* I'm just going to cement what we do now in Chrome and FF | ||
*/ | ||
.replace(NEWLINES, ' '); | ||
|
||
var result = _str | ||
.split(/(<[^<>]*>)/).map(function(d) { | ||
var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i), | ||
tag = match && match[2].toLowerCase(), | ||
style = TAG_STYLES[tag]; | ||
|
||
if(style !== undefined) { | ||
var close = match[1], | ||
extra = match[3], | ||
/** | ||
* extraStyle: any random extra css (that's supported by svg) | ||
* use this like <span style="font-family:Arial"> to change font in the middle | ||
* | ||
* at one point we supported <font family="..." size="..."> but as this isn't even | ||
* valid HTML anymore and we dropped it accidentally for many months, we will not | ||
* resurrect it. | ||
*/ | ||
extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i); | ||
|
||
// anchor and br are the only ones that don't turn into a tspan | ||
.split(SPLIT_TAGS).map(function(d) { | ||
var match = d.match(ONE_TAG); | ||
var tag = match && match[2].toLowerCase(); | ||
var tagStyle = TAG_STYLES[tag]; | ||
|
||
if(tagStyle !== undefined) { | ||
var isClose = match[1]; | ||
if(isClose) return (tag === 'a' ? '</a>' : '</tspan>') + (TAG_CLOSE[tag] || ''); | ||
|
||
// break: later we'll turn these into newline <tspan>s | ||
// but we need to know about all the other tags first | ||
if(tag === 'br') return '<br>'; | ||
|
||
/** | ||
* extra includes href and any random extra css (that's supported by svg) | ||
* use this like <span style="font-family:Arial"> to change font in the middle | ||
* | ||
* at one point we supported <font family="..." size="..."> but as this isn't even | ||
* valid HTML anymore and we dropped it accidentally for many months, we will not | ||
* resurrect it. | ||
*/ | ||
var extra = match[4]; | ||
|
||
var out; | ||
|
||
// anchor is the only tag that doesn't turn into a tspan | ||
if(tag === 'a') { | ||
if(close) return '</a>'; | ||
else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>'; | ||
else { | ||
// remove quotes, leading '=', replace '&' with '&' | ||
var href = extra.substr(4) | ||
.replace(/["']/g, '') | ||
.replace(/=/, ''); | ||
|
||
// check protocol | ||
var hrefMatch = extra && extra.match(HREFMATCH); | ||
var href = hrefMatch && (hrefMatch[3] || hrefMatch[4]); | ||
|
||
out = '<a'; | ||
|
||
if(href) { | ||
// check safe protocols | ||
var dummyAnchor = document.createElement('a'); | ||
dummyAnchor.href = href; | ||
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>'; | ||
|
||
return '<a xlink:show="new" xlink:href="' + encodeForHTML(href) + '">'; | ||
if(PROTOCOLS.indexOf(dummyAnchor.protocol) !== -1) { | ||
out += ' xlink:show="new" xlink:href="' + encodeForHTML(href) + '"'; | ||
} | ||
} | ||
} | ||
else if(tag === 'br') return '<br>'; | ||
else if(close) { | ||
// closing tag | ||
|
||
// sub/sup: extra tspan with zero-width space to get back to the right baseline | ||
if(tag === 'sup') return '</tspan><tspan dy="0.42em">​</tspan>'; | ||
if(tag === 'sub') return '</tspan><tspan dy="-0.21em">​</tspan>'; | ||
else return '</tspan>'; | ||
} | ||
else { | ||
var tspanStart = '<tspan'; | ||
out = '<tspan'; | ||
|
||
if(tag === 'sup' || tag === 'sub') { | ||
// sub/sup: extra zero-width space, fixes problem if new line starts with sub/sup | ||
tspanStart = '​' + tspanStart; | ||
out = '​' + out; | ||
} | ||
} | ||
|
||
if(extraStyle) { | ||
// most of the svg css users will care about is just like html, | ||
// but font color is different. Let our users ignore this. | ||
extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, '$1 fill:'); | ||
style = encodeForHTML(extraStyle) + (style ? ';' + style : ''); | ||
} | ||
// now add style, from both the tag name and any extra css | ||
// Most of the svg css that users will care about is just like html, | ||
// but font color is different (uses fill). Let our users ignore this. | ||
var cssMatch = extra && extra.match(STYLEMATCH); | ||
var css = cssMatch && (cssMatch[3] || cssMatch[4]); | ||
if(css) css = encodeForHTML(css.replace(COLORMATCH, '$1 fill:')); | ||
|
||
return tspanStart + (style ? ' style="' + style + '"' : '') + '>'; | ||
} | ||
if(tagStyle) css = tagStyle + ';' + (css || ''); | ||
|
||
if(css) return out + ' style="' + css + '">'; | ||
|
||
return out + '>'; | ||
} | ||
else { | ||
return exports.xml_entity_encode(d).replace(/</g, '<'); | ||
} | ||
}); | ||
|
||
// now deal with line breaks | ||
var indices = []; | ||
for(var index = result.indexOf('<br>'); index > 0; index = result.indexOf('<br>', index + 1)) { | ||
indices.push(index); | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ var util = require('@src/lib/svg_text_utils'); | |
describe('svg+text utils', function() { | ||
'use strict'; | ||
|
||
describe('convertToTspans should', function() { | ||
describe('convertToTspans', function() { | ||
|
||
function mockTextSVGElement(txt) { | ||
return d3.select('body') | ||
|
@@ -30,7 +30,7 @@ describe('svg+text utils', function() { | |
expect(tspan.attr('style')).toBe(style); | ||
} | ||
|
||
function assertAnchorAttrs(node) { | ||
function assertAnchorAttrs(node, style) { | ||
var a = node.select('a'); | ||
|
||
var WHITE_LIST = ['xlink:href', 'xlink:show', 'style'], | ||
|
@@ -44,6 +44,8 @@ describe('svg+text utils', function() { | |
}); | ||
|
||
expect(hasWrongAttr).toBe(false); | ||
|
||
expect(a.attr('style')).toBe('cursor:pointer;' + (style || '')); | ||
} | ||
|
||
function listAttributes(node) { | ||
|
@@ -60,7 +62,7 @@ describe('svg+text utils', function() { | |
d3.select('#text').remove(); | ||
}); | ||
|
||
it('check for XSS attack in href', function() { | ||
it('checks for XSS attack in href', function() { | ||
var node = mockTextSVGElement( | ||
'<a href="javascript:alert(\'attack\')">XSS</a>' | ||
); | ||
|
@@ -70,7 +72,7 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, null); | ||
}); | ||
|
||
it('check for XSS attack in href (with plenty of white spaces)', function() { | ||
it('checks for XSS attack in href (with plenty of white spaces)', function() { | ||
var node = mockTextSVGElement( | ||
'<a href = " javascript:alert(\'attack\')">XSS</a>' | ||
); | ||
|
@@ -80,7 +82,7 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, null); | ||
}); | ||
|
||
it('whitelist relative hrefs (interpreted as http)', function() { | ||
it('whitelists relative hrefs (interpreted as http)', function() { | ||
var node = mockTextSVGElement( | ||
'<a href="/mylink">mylink</a>' | ||
); | ||
|
@@ -90,7 +92,7 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, '/mylink'); | ||
}); | ||
|
||
it('whitelist http hrefs', function() { | ||
it('whitelists http hrefs', function() { | ||
var node = mockTextSVGElement( | ||
'<a href="http://bl.ocks.org/">bl.ocks.org</a>' | ||
); | ||
|
@@ -100,7 +102,7 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, 'http://bl.ocks.org/'); | ||
}); | ||
|
||
it('whitelist https hrefs', function() { | ||
it('whitelists https hrefs', function() { | ||
var node = mockTextSVGElement( | ||
'<a href="https://plot.ly">plot.ly</a>' | ||
); | ||
|
@@ -110,7 +112,7 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, 'https://plot.ly'); | ||
}); | ||
|
||
it('whitelist mailto hrefs', function() { | ||
it('whitelists mailto hrefs', function() { | ||
var node = mockTextSVGElement( | ||
'<a href="mailto:[email protected]">support</a>' | ||
); | ||
|
@@ -120,7 +122,8 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, 'mailto:[email protected]'); | ||
}); | ||
|
||
it('wrap XSS attacks in href', function() { | ||
it('drops XSS attacks in href', function() { | ||
// "XSS" gets interpreted as a relative link (http) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
var textCases = [ | ||
'<a href="XSS\" onmouseover="alert(1)\" style="font-size:300px">Subtitle</a>', | ||
'<a href="XSS" onmouseover="alert(1)" style="font-size:300px">Subtitle</a>' | ||
|
@@ -130,12 +133,32 @@ describe('svg+text utils', function() { | |
var node = mockTextSVGElement(textCase); | ||
|
||
expect(node.text()).toEqual('Subtitle'); | ||
assertAnchorAttrs(node); | ||
assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px'); | ||
assertAnchorAttrs(node, 'font-size:300px'); | ||
assertAnchorLink(node, 'XSS'); | ||
}); | ||
}); | ||
|
||
it('accepts href and style in <a> in any order and tosses other stuff', function() { | ||
var textCases = [ | ||
'<a href="x" style="y">z</a>', | ||
'<a href=\'x\' style="y">z</a>', | ||
'<A HREF="x"StYlE=\'y\'>z</a>', | ||
'<a style=\'y\'href=\'x\'>z</A>', | ||
'<a \t\r\n href="x" \n\r\t style="y" \n \t \r>z</a>', | ||
'<a magic="true" href="x" weather="cloudy" style="y" speed="42">z</a>', | ||
'<a href="x" style="y">z</a href="nope" style="for real?">', | ||
]; | ||
|
||
textCases.forEach(function(textCase) { | ||
var node = mockTextSVGElement(textCase); | ||
|
||
expect(node.text()).toEqual('z'); | ||
assertAnchorAttrs(node, 'y'); | ||
assertAnchorLink(node, 'x'); | ||
}); | ||
}); | ||
|
||
it('should keep query parameters in href', function() { | ||
it('keeps query parameters in href', function() { | ||
var textCases = [ | ||
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>', | ||
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>' | ||
|
@@ -150,7 +173,7 @@ describe('svg+text utils', function() { | |
}); | ||
}); | ||
|
||
it('allow basic spans', function() { | ||
it('allows basic spans', function() { | ||
var node = mockTextSVGElement( | ||
'<span>text</span>' | ||
); | ||
|
@@ -159,7 +182,7 @@ describe('svg+text utils', function() { | |
assertTspanStyle(node, null); | ||
}); | ||
|
||
it('ignore unquoted styles in spans', function() { | ||
it('ignores unquoted styles in spans', function() { | ||
var node = mockTextSVGElement( | ||
'<span style=unquoted>text</span>' | ||
); | ||
|
@@ -168,7 +191,7 @@ describe('svg+text utils', function() { | |
assertTspanStyle(node, null); | ||
}); | ||
|
||
it('allow quoted styles in spans', function() { | ||
it('allows quoted styles in spans', function() { | ||
var node = mockTextSVGElement( | ||
'<span style="quoted: yeah;">text</span>' | ||
); | ||
|
@@ -177,7 +200,7 @@ describe('svg+text utils', function() { | |
assertTspanStyle(node, 'quoted: yeah;'); | ||
}); | ||
|
||
it('ignore extra stuff after span styles', function() { | ||
it('ignores extra stuff after span styles', function() { | ||
var node = mockTextSVGElement( | ||
'<span style="quoted: yeah;"disallowed: indeed;">text</span>' | ||
); | ||
|
@@ -195,7 +218,7 @@ describe('svg+text utils', function() { | |
assertTspanStyle(node, 'quoted: yeah&\';;'); | ||
}); | ||
|
||
it('decode some HTML entities in text', function() { | ||
it('decodes some HTML entities in text', function() { | ||
var node = mockTextSVGElement( | ||
'100μ & < 10 > 0 ' + | ||
'100 × 20 ± 0.5 °' | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌