-
-
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 1 commit
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 |
---|---|---|
|
@@ -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) { | ||
|
@@ -120,7 +122,8 @@ describe('svg+text utils', function() { | |
assertAnchorLink(node, 'mailto:[email protected]'); | ||
}); | ||
|
||
it('wraps 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,8 +133,28 @@ 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'); | ||
}); | ||
}); | ||
|
||
|
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.
👌