diff --git a/closure/goog/string/BUILD b/closure/goog/string/BUILD index 8f9e77ec28..0a244d4b0e 100644 --- a/closure/goog/string/BUILD +++ b/closure/goog/string/BUILD @@ -22,8 +22,10 @@ closure_js_library( name = "linkify", srcs = ["linkify.js"], deps = [ + ":const", ":string", "//closure/goog/html:safehtml", + "//closure/goog/html:uncheckedconversions", ], ) diff --git a/closure/goog/string/linkify.js b/closure/goog/string/linkify.js index c9fb966bfd..be2e5e4dce 100644 --- a/closure/goog/string/linkify.js +++ b/closure/goog/string/linkify.js @@ -11,7 +11,9 @@ goog.provide('goog.string.linkify'); goog.require('goog.html.SafeHtml'); +goog.require('goog.html.uncheckedconversions'); goog.require('goog.string'); +goog.require('goog.string.Const'); /** @@ -26,21 +28,53 @@ goog.require('goog.string'); * target=''. * @param {boolean=} opt_preserveNewlines Whether to preserve newlines with * <br>. + * @param {boolean=} opt_preserveSpacesAndTabs Whether to preserve spaces with + * non-breaking spaces and tabs with * @return {!goog.html.SafeHtml} Linkified HTML. Any text that is not part of a * link will be HTML-escaped. */ goog.string.linkify.linkifyPlainTextAsHtml = function( - text, opt_attributes, opt_preserveNewlines) { + text, opt_attributes, opt_preserveNewlines, opt_preserveSpacesAndTabs) { 'use strict'; + + /** + * @param {string} plainText + * @return {!goog.html.SafeHtml} html + */ + const htmlEscape = function(plainText) { + if (opt_preserveSpacesAndTabs) { + const html = goog.html.SafeHtml.htmlEscape(plainText); + let modifiedHtml = + goog.html.SafeHtml + .unwrap(html) + // Leading space is converted into a non-breaking space, and + // spaces following whitespace are converted into non-breaking + // spaces. This must happen first, to ensure we preserve spaces + // after newlines. + .replace(/(^|[\n\r\t\ ])\ /g, '$1 ') + // Preserve tabs by using style="white-space:pre" + .replace(/(\t+)/g, '$1'); + if (opt_preserveNewlines) { + modifiedHtml = goog.string.newLineToBr(modifiedHtml); + } + return goog.html.uncheckedconversions + .safeHtmlFromStringKnownToSatisfyTypeContract( + goog.string.Const.from('Escaped plain text'), modifiedHtml, + html.getDirection()); + } else if (opt_preserveNewlines) { + return goog.html.SafeHtml.htmlEscapePreservingNewlines(plainText); + } else { + return goog.html.SafeHtml.htmlEscape(plainText); + } + }; + // This shortcut makes linkifyPlainText ~10x faster if text doesn't contain // URLs or email addresses and adds insignificant performance penalty if it // does. if (text.indexOf('@') == -1 && text.indexOf('://') == -1 && text.indexOf('www.') == -1 && text.indexOf('Www.') == -1 && text.indexOf('WWW.') == -1) { - return opt_preserveNewlines ? - goog.html.SafeHtml.htmlEscapePreservingNewlines(text) : - goog.html.SafeHtml.htmlEscape(text); + return htmlEscape(text); } const attributesMap = {}; @@ -66,10 +100,7 @@ goog.string.linkify.linkifyPlainTextAsHtml = function( goog.string.linkify.FIND_LINKS_RE_, function(part, before, original, email, protocol) { 'use strict'; - output.push( - opt_preserveNewlines ? - goog.html.SafeHtml.htmlEscapePreservingNewlines(before) : - before); + output.push(htmlEscape(before)); if (!original) { return ''; } @@ -123,10 +154,7 @@ goog.string.linkify.linkifyPlainTextAsHtml = function( } attributesMap['href'] = href + linkText; output.push(goog.html.SafeHtml.create('a', attributesMap, linkText)); - output.push( - opt_preserveNewlines ? - goog.html.SafeHtml.htmlEscapePreservingNewlines(afterLink) : - afterLink); + output.push(htmlEscape(afterLink)); return ''; }); return goog.html.SafeHtml.concat(output); @@ -225,9 +253,10 @@ goog.string.linkify.WWW_START_ = 'www\\.'; * @const * @private */ -goog.string.linkify.URL_RE_STRING_ = '(?:' + - goog.string.linkify.PROTOCOL_START_ + '|' + goog.string.linkify.WWW_START_ + - ')[' + goog.string.linkify.ACCEPTABLE_URL_CHARS_ + ']+'; +goog.string.linkify.URL_RE_STRING_ = + '(?:' + goog.string.linkify.PROTOCOL_START_ + '|' + + goog.string.linkify.WWW_START_ + ')[' + + goog.string.linkify.ACCEPTABLE_URL_CHARS_ + ']+'; /** diff --git a/closure/goog/string/linkify_test.js b/closure/goog/string/linkify_test.js index e4c47e86f5..eb4543cab0 100644 --- a/closure/goog/string/linkify_test.js +++ b/closure/goog/string/linkify_test.js @@ -19,11 +19,17 @@ const testingDom = goog.require('goog.testing.dom'); /** @type {!HTMLDivElement} */ const div = dom.createElement(TagName.DIV); -function assertLinkify(comment, input, expected, preserveNewlines = undefined) { +/** + * @private + */ +function assertLinkify( + comment, input, expected, preserveNewlines = undefined, + preserveSpacesAndTabs = undefined) { assertEquals( comment, expected, SafeHtml.unwrap(linkify.linkifyPlainTextAsHtml( - input, {rel: '', target: ''}, preserveNewlines))); + input, {rel: '', target: ''}, preserveNewlines, + preserveSpacesAndTabs))); } testSuite({ @@ -557,4 +563,50 @@ testSuite({ 'Line 1
Line 2', /* preserveNewlines */ true); }, + + testPreserveSpacesAndTabs() { + assertLinkify( + 'Preserving spaces', ' Example:\n http://www.google.com/ ', + ' Example:\n http://www.google.com/<\/a> ', + /* preserveNewlines */ false, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving spaces with no links', ' Line 1\n Line 2 ', + ' Line 1\n  Line 2 ', + /* preserveNewlines */ false, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving tabs', 'Example:\thttp://www.google.com/', + 'Example:\thttp://www.google.com/<\/a>', + /* preserveNewlines */ false, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving tabs with no links', 'Column 1\t\tColumn 2', + 'Column 1\t\tColumn 2', + /* preserveNewlines */ false, + /* preserveSpacesAndTabs */ true); + }, + + testPreserveNewlinesSpacesAndTabs() { + assertLinkify( + 'Preserving spaces', ' Example:\n http://www.google.com/ ', + ' Example:
 
http://www.google.com/<\/a> ', + /* preserveNewlines */ true, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving spaces with no links', ' Line 1\n Line 2 ', + ' Line 1
  Line 2 ', + /* preserveNewlines */ true, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving tabs', 'Example:\n\thttp://www.google.com/', + 'Example:
\t
http://www.google.com/<\/a>', + /* preserveNewlines */ true, + /* preserveSpacesAndTabs */ true); + assertLinkify( + 'Preserving tabs with no links', 'Line 1\n\t\tLine 2', + 'Line 1
\t\tLine 2', + /* preserveNewlines */ true, + /* preserveSpacesAndTabs */ true); + }, });