Search code examples
cssiframegoogle-chrome-extension

How not to inherit styles in a chrome extension content script


I'm writing a Google Chrome extension that runs a content script on every page. In my content script, I inject a <div> with some <ul> and <li> children into the page. I specify a few styles for these elements in a stylesheet.

But I've found that on some random pages my elements will inherit styles from those defined on the webpage since I haven't specified every single style property for my divs.

What's the best way I can stop my injected elements from inheriting these styles?

It seems to me I could either:

  • specify every single style in my stylesheet (eg. by looking at what the computed styles are when there is no interference), or
  • I could put my <div> inside an <iframe>. However, then I'll have to do hella message passing between my content script's iframe and the source page since the chrome:// URL of my iframe src and the http:// urls of the source pages would be considered cross-origin.

Solution

  • I would go with the first choice--to fully specify the style of the elements you use. But this is a bit more involved than I thought.

    First, you have to completely specify the container element. Then, for its descendants, you have to say that they should also use the default values or inherit from their parent (up to the container). Finally, you have to specify the look of every other element so that they're not all plain spans.

    The relevant APIs are getComputedStyle and the CSSStyleSheet interface from DOM Level 2 Style. You can use all the values there except width and height, which should be auto by default. You also need to download a default stylesheet, such as the Webkit user agent stylesheet. Then you can call the following function to create a complete stylesheet that you can inject into the document.

    Note that when you insert the stylesheet into the target document, you'll have to make the container selector as specific as possible because the webpage could conceivably give rules that have a higher specificity than your rules. For example, in <html id=a><head id=b><style>#a #b * {weird overrides}</style></head>, #a #b * has a higher specificity than #yourId div would. But I imagine that this is uncommon.

    Note: for some reason, Chrome is giving me error "Failed to load resource" when I load the CSS, unless it is already in a <link> of the current document. So you should include html.css in the page that calls this function too.

    // CSS 2.1 inherited prpoerties
    var inheritedProperties = [
        'azimuth', 'border-collapse', 'border-spacing', 'caption-side',
        'color', 'cursor', 'direction', 'elevation', 'empty-cells',
        'font-family', 'font-size', 'font-style', 'font-variant',
        'font-weight', 'font', 'letter-spacing', 'line-height',
        'list-style-image', 'list-style-position', 'list-style-type',
        'list-style', 'orphans', 'pitch-range', 'pitch', 'quotes',
        'richness', 'speak-header', 'speak-numeral', 'speak-punctuation',
        'speak', 'speech-rate', 'stress', 'text-align', 'text-indent',
        'text-transform', 'visibility', 'voice-family', 'volume',
        'white-space', 'widows', 'word-spacing'];
    // CSS Text Level 3 properties that inherit http://www.w3.org/TR/css3-text/
    inheritedProperties.push(
        'hanging-punctuation', 'line-break', 'punctuation-trim',
        'text-align-last', 'text-autospace', 'text-decoration-skip',
        'text-emphasis', 'text-emphasis-color', 'text-emphasis-position',
        'text-emphasis-style', 'text-justify', 'text-outline',
        'text-shadow', 'text-underline-position', 'text-wrap',
        'white-space-collapsing', 'word-break', 'word-wrap');
    /**
     * Example usage:
           var fullStylesheet = completeStylesheet('#container', 'html.css').map(
               function(ruleInfo) {
                   return ruleInfo.selectorText + ' {' + ruleInfo.cssText + '}';
               }).join('\n');
     * @param {string} containerSelector The most specific selector you can think
     *     of for the container element; e.g. #container. It had better be more
     *     specific than any other selector that might affect the elements inside.
     * @param {string=} defaultStylesheetLocation If specified, the location of the
     *     default stylesheet. Note that this script must be able to access that
     *     locatoin under same-origin policy.
     * @return {Array.<{selectorText: string, cssText: string}>} rules
     */
    var completeStylesheet = function(containerSelector,
                                      defaultStylesheetLocation) {
      var rules = [];
      var iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      document.body.appendChild(iframe);  // initializes contentDocument
      try {
        var span = iframe.contentDocument.createElement('span');
        iframe.contentDocument.body.appendChild(span);
        /** @type {CSSStyleDeclaration} */
        var basicStyle = iframe.contentDocument.defaultView.getComputedStyle(span);
        var allPropertyValues = {};
        Array.prototype.forEach.call(basicStyle, function(property) {
          allPropertyValues[property] = basicStyle[property];
        });
        // Properties whose used value differs from computed value, and that
        // don't have a default value of 0, should stay at 'auto'.
        allPropertyValues['width'] = allPropertyValues['height'] = 'auto';
        var declarations = [];
        for (var property in allPropertyValues) {
          var declaration = property + ': ' + allPropertyValues[property] + ';';
          declarations.push(declaration);
        }
        // Initial values of all properties for the container element and
        // its descendants
        rules.push({selectorText: containerSelector + ', ' +
                                  containerSelector + ' *',
                    cssText: declarations.join(' ')});
    
        // For descendants, some of the properties should inherit instead
        // (mostly dealing with text).
        rules.push({selectorText: containerSelector + ' *',
                    cssText: inheritedProperties.map(
                        function(property) {
                          return property + ': inherit;'
                        }).join(' ')});
    
        if (defaultStylesheetLocation) {
          var link = iframe.contentDocument.createElement('link');
          link.rel = 'stylesheet';
          link.href = defaultStylesheetLocation;
          iframe.contentDocument.head.appendChild(link);
          /** @type {CSSStyleSheet} */
          var sheet = link.sheet;
          Array.prototype.forEach.call(
              sheet.cssRules,
              /** @param {CSSStyleRule} cssRule */
              function(cssRule) {
            rules.push({
                selectorText: containerSelector + ' ' + cssRule.selectorText,
                cssText: cssRule.style.cssText});
          });
        }
        return rules;
      } finally {
        document.body.removeChild(iframe);
      }
    };