diff --git a/common/static/js/capa/spec/formula_equation_preview_spec.js b/common/static/js/capa/spec/formula_equation_preview_spec.js index ad0a903e03..5e9cf7a487 100644 --- a/common/static/js/capa/spec/formula_equation_preview_spec.js +++ b/common/static/js/capa/spec/formula_equation_preview_spec.js @@ -1,3 +1,29 @@ +describe('escapeSelector', function() { + 'use strict'; + var escapeSelector = window.escapeSelector; + + it('correctly escapes css', function() { + // tests borrowed from https://github.com/jquery/jquery/blob/3edfa1bcdc50bca41ac58b2642b12f3feee03a3b/test/unit/selector.js#L2030 + expect(escapeSelector('-')).toEqual('\\-'); + expect(escapeSelector('-a')).toEqual('-a'); + expect(escapeSelector('--')).toEqual('--'); + expect(escapeSelector('--a')).toEqual('--a'); + expect(escapeSelector('\uFFFD')).toEqual('\uFFFD'); + expect(escapeSelector('\uFFFDb')).toEqual('\uFFFDb'); + expect(escapeSelector('a\uFFFDb')).toEqual('a\uFFFDb'); + expect(escapeSelector('1a')).toEqual('\\31 a'); + expect(escapeSelector('a\0b')).toEqual('a\uFFFDb'); + expect(escapeSelector('a3b')).toEqual('a3b'); + expect(escapeSelector('-4a')).toEqual('-\\34 a'); + expect(escapeSelector('\x01\x02\x1E\x1F')).toEqual('\\1 \\2 \\1e \\1f '); + + // This is the important one; xblocks and course ids often contain invalid characters, so if these aren't + // escaped when embedding/searching xblock IDs using css selectors, bad things happen. + expect(escapeSelector('course-v1:edX+DemoX+Demo_Course')).toEqual('course-v1\\:edX\\+DemoX\\+Demo_Course'); + expect(escapeSelector('block-v1:edX+DemoX+Demo_Course+type@sequential+block')).toEqual('block-v1\\:edX\\+DemoX\\+Demo_Course\\+type\\@sequential\\+block'); + }); +}); + describe('Formula Equation Preview', function() { 'use strict'; var formulaEquationPreview = window.formulaEquationPreview; diff --git a/common/static/js/capa/src/formula_equation_preview.js b/common/static/js/capa/src/formula_equation_preview.js index 646ba0809c..1838bf498b 100644 --- a/common/static/js/capa/src/formula_equation_preview.js +++ b/common/static/js/capa/src/formula_equation_preview.js @@ -1,3 +1,37 @@ +function escapeSelector(id) { + // Wrapper around window.CSS.escape that uses a fallback method if CSS.escape is not available. + // This is designed to serialize a string to be used as a valid css selector. See https://drafts.csswg.org/cssom/#the-css.escape()-method + // For example, can be used with xblock and course ids, which often contain invalid characters that must be escaped + // to function properly in css selectors. + + // TODO: if this escaping is also required elsewhere, it may be useful to add a global CSS.escape polyfill and + // use that directly. + if (window.CSS && window.CSS.escape) { + return CSS.escape(id); + } else { + // CSS escape alternative borrowed from https://api.jquery.com/jQuery.escapeSelector/ source. When we upgrade to jQuery 3.0, we can use $.escapeSelector() instead of this shim escapeSelector function. + // source: https://github.com/jquery/jquery/blob/3edfa1bc/src/selector/escapeSelector.js + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + } + // ensure string and then run the replacements + return (id + "").replace(rcssescape, fcssescape); + } +} + var formulaEquationPreview = { minDelay: 300, // Minimum time between requests sent out. errorDelay: 1500 // Wait time before showing error (prevent frustration). @@ -13,7 +47,7 @@ formulaEquationPreview.enable = function() { function setupInput() { var $this = $(this); // cache the jQuery object - var $preview = $('#' + this.id + '_preview'); + var $preview = $('#' + escapeSelector(this.id) + '_preview'); var inputData = { // These are the mutable values