56

I'd like to be able to take my DOM, as is, and convert it to a string. Let's say I open up the inspector and make a change to the margin-left property of a particular element. That change should be reflected in my string.

The function should properly take all the styles currently applied to an element (excluding default values) and include them in the inline style for that element.

I have written a 'solution' which has proven to be inadequate. The getMatchedCSSRules function in webkit is extremely finicky, and I haven't been able to determine why it sometimes works and doesn't work other times. Therefore, I would like to avoid using this function unless it works 100% of the time. Similarly, the getComputedStyle function has problems of its own. If use the inspector to change the #footer element on this page to be 7px solid red rather than 7px solid black, the change will be reflected in when I run getComputedStyle(document.getElementById('footer')).cssText in the console, but it will also give me a host of inherited properties that were never modified by either the user using the inspector or by the stylesheets on the page.

I am looking for a solution that works with webkit--cross browser compatibility is not an issue at the moment.

Thank you!

D-Nice
  • 4,772
  • 14
  • 52
  • 86
  • 1
    Hrmm, would you also need a CSS rules reset for when you go to put that block of text somewhere? – Incognito Jun 07 '11 at 23:27
  • 1
    Just out of curiosity, why exactly do you want this? Since you say you opened the inspector to make a change, do you want to send the modifications you made to the DOM to someone, for some kind of approval? Trying to answer the *why* instead of the *how* could make some different approaches to your problem. – GmonC Jun 08 '11 at 04:08
  • @Incognito, well, if im iterating through the entire body, and taking every css rule, i should pick up the css reset rules. – D-Nice Jun 09 '11 at 06:53
  • @GMonC I'm trying to log my DOM in mobile safari so that i can inspect it in safari on my computer. – D-Nice Jun 09 '11 at 06:58
  • So you want all styles provided by a regular stylesheet to be inlined on each matching element? Like, if you had `` then you'd want *every* `a` tag to look like ``. Why not just copy the ` – MooGoo Jun 09 '11 at 16:44

8 Answers8

86

I think this could be a solution (it took me nearly a whole day!).

It returns a string representing the DOM of any element, with all external styles included in the "style" attributes except default values, and does not permanently modify that element.

For example: console.log(document.body.serializeWithStyles());

You can load this code in Web Inspector command line or from a script tag in the body element but NOT in the head element because it requires the existence of document.body.

I have tested it on desktop Safari 5 (I don't have the mobile version).

It works like this:

For each element in the DOM:
1) caching the value of style.cssText property, which represents the inline style, in an array;
2) calling getComputedStyle on the element;
3) checking if we have the css default values lookup table corresponding to this element's tag name;
4) building it if not;
5) iterating through the result, finding which values are non default using the lookup table;
6) applying those non default style values to the element.
Then storing the outerHTML as the result;
For each element, restoring the inline styles from the cache;
Returning the previously stored result.

The code:

Element.prototype.serializeWithStyles = (function () {  

    // Mapping between tag names and css default values lookup tables. This allows to exclude default values in the result.
    var defaultStylesByTagName = {};

    // Styles inherited from style sheets will not be rendered for elements with these tag names
    var noStyleTags = {"BASE":true,"HEAD":true,"HTML":true,"META":true,"NOFRAME":true,"NOSCRIPT":true,"PARAM":true,"SCRIPT":true,"STYLE":true,"TITLE":true};

    // This list determines which css default values lookup tables are precomputed at load time
    // Lookup tables for other tag names will be automatically built at runtime if needed
    var tagNames = ["A","ABBR","ADDRESS","AREA","ARTICLE","ASIDE","AUDIO","B","BASE","BDI","BDO","BLOCKQUOTE","BODY","BR","BUTTON","CANVAS","CAPTION","CENTER","CITE","CODE","COL","COLGROUP","COMMAND","DATALIST","DD","DEL","DETAILS","DFN","DIV","DL","DT","EM","EMBED","FIELDSET","FIGCAPTION","FIGURE","FONT","FOOTER","FORM","H1","H2","H3","H4","H5","H6","HEAD","HEADER","HGROUP","HR","HTML","I","IFRAME","IMG","INPUT","INS","KBD","KEYGEN","LABEL","LEGEND","LI","LINK","MAP","MARK","MATH","MENU","META","METER","NAV","NOBR","NOSCRIPT","OBJECT","OL","OPTION","OPTGROUP","OUTPUT","P","PARAM","PRE","PROGRESS","Q","RP","RT","RUBY","S","SAMP","SCRIPT","SECTION","SELECT","SMALL","SOURCE","SPAN","STRONG","STYLE","SUB","SUMMARY","SUP","SVG","TABLE","TBODY","TD","TEXTAREA","TFOOT","TH","THEAD","TIME","TITLE","TR","TRACK","U","UL","VAR","VIDEO","WBR"];

    // Precompute the lookup tables.
    for (var i = 0; i < tagNames.length; i++) {
        if(!noStyleTags[tagNames[i]]) {
            defaultStylesByTagName[tagNames[i]] = computeDefaultStyleByTagName(tagNames[i]);
        }
    }

    function computeDefaultStyleByTagName(tagName) {
        var defaultStyle = {};
        var element = document.body.appendChild(document.createElement(tagName));
        var computedStyle = getComputedStyle(element);
        for (var i = 0; i < computedStyle.length; i++) {
            defaultStyle[computedStyle[i]] = computedStyle[computedStyle[i]];
        }
        document.body.removeChild(element); 
        return defaultStyle;
    }

    function getDefaultStyleByTagName(tagName) {
        tagName = tagName.toUpperCase();
        if (!defaultStylesByTagName[tagName]) {
            defaultStylesByTagName[tagName] = computeDefaultStyleByTagName(tagName);
        }
        return defaultStylesByTagName[tagName];
    }

    return function serializeWithStyles() {
        if (this.nodeType !== Node.ELEMENT_NODE) { throw new TypeError(); }
        var cssTexts = [];
        var elements = this.querySelectorAll("*");
        for ( var i = 0; i < elements.length; i++ ) {
            var e = elements[i];
            if (!noStyleTags[e.tagName]) {
                var computedStyle = getComputedStyle(e);
                var defaultStyle = getDefaultStyleByTagName(e.tagName);
                cssTexts[i] = e.style.cssText;
                for (var ii = 0; ii < computedStyle.length; ii++) {
                    var cssPropName = computedStyle[ii];
                    if (computedStyle[cssPropName] !== defaultStyle[cssPropName]) {
                        e.style[cssPropName] = computedStyle[cssPropName];
                    }
                }
            }
        }
        var result = this.outerHTML;
        for ( var i = 0; i < elements.length; i++ ) {
            elements[i].style.cssText = cssTexts[i];
        }
        return result;
    }
})();
Luc125
  • 5,752
  • 34
  • 35
  • 1
    Great work! I've tested this in on a number of different pages and it's worked nearly every time. I'm going to continue to test it tonight and will report back asap. Thanks! – D-Nice Jun 10 '11 at 21:19
  • Thank you for your comment, I really appreciate! Don't hesitate to report bugs if you find any. – Luc125 Jun 11 '11 at 11:21
  • What I'm trying to figure out now is why the error 'TypeError: Cannot read property 'background-attachment' of undefined' appears on many pages. Google.com is one example. background-attachment is the first property, alphabetically, so its probably looping through all properties somewhere and looking up the property on an undefined var. ive tried to add tons of checks to make sure the var isnt undefined, but i havent been able to spot the culprit yet. – D-Nice Jun 11 '11 at 20:38
  • adding `if ((defaultStyle && computedStyle) && computedStyle[cssPropName] !== defaultStyle[cssPropName])` gets rid of the error. – D-Nice Jun 11 '11 at 21:52
  • You're right! I've tested the code on a Google search result page this time, and indeed it showed a type error, `defaultStyle` was equal to undefined. I'm sorry, there was a stupid syntax error in `getDefaultStyleByTagName`, so that it sometimes returned undefined. I've just corrected my post. – Luc125 Jun 12 '11 at 10:18
  • That's pretty awesome code. Well worth the bounty. I'm keeping this one bookmarked. :) – Drazisil Jul 02 '11 at 04:19
  • This is totally awesome. But i don't see why you need to modify the Element prototype unless i'm missing something. If this was just a function that took an element, you could have this anywhere on the page and you wouldn't need that disclaimer about where it can be placed. Also general rule of thumb is not to modify things you don't own. – deweydb Nov 03 '14 at 00:11
  • 1
    @deweydb I agree, if you own a namespace on the page, this method would better fit somewhere in that namespace. No problem, you can quickly make a stand-alone version of it since there are only a few occurences of the `this` keyword in its code. Nonetheless, please note that the disclaimer will still hold because of the reference to `document.body`. By the way, it might be interesting to see if we can bypass that restriction, although loading scripts in the `head` tag is commonly discouraged. – Luc125 Nov 10 '14 at 04:29
  • 2
    shouldn't this be a library? – Yury Solovyov May 22 '16 at 19:22
11

Can't you just do document.getElementsByTagName('body')[0].innerHTML? When I make changes in the inspector and then enter the above javascript in the console, it returns the updated HTML.

EDIT: I just tried putting that script in a function and attaching it to an onclick event. Made some updates in the inspector, clicked button, and it worked:

HTML

<button onclick="printDOM()">Print DOM</button>

Javascript

function printDOM() {
    console.log(document.getElementsByTagName('body')[0].innerHTML) ;
}
squidbe
  • 1,012
  • 1
  • 8
  • 13
  • 5
    This will work to get all changes made via the inspector, but it wont include all the inline styles from outside stylesheets inline. I need to extract the complete DOM, including all styles. – D-Nice Jun 02 '11 at 17:50
4

In case you want to capture the whole page, it is easier to just get all non-inline stylesheets and inline them.

The approach in the accepted answer is magnificent, but quite slow and touches the whole document.

I took the following approach to capture a page including style:

  1. document.documentElement.outerHTML;

  2. get all stylesheets from the document.styleSheets API

Along the lines of:

function captureCss(){
    var cssrules = "";
    var sheets = document.styleSheets;
    for(var i = 0; i<sheets.length; i++){
        if(!sheets[i].disabled && sheets[i].href != null) { // or sheets[i].href.nodeName == 'LINK'
            if(sheets[i].rules == null){ // can be null because of cross origin policy
                try{
                    var fetched = XHR GET(sheets[i].href); // works nicely because it hits the cache
                    if(fetched){
                        cssrules += "<style>\n"+fetched+"\n</style>\n"
                    }
                }catch(e){
                    console.log(e);
                }
                continue;
            }
            for(var j=0;j<sheets[i].rules.length;j++){
                cssrules += "<style>\n"+sheets[i].rules[j].cssText+"\n</style>\n"
            }
        }
    }
    return cssrules;
}
  1. Add the captured cssrules as the first thing of the header in the outerHtml html text

This way you get a self contained styled page.

This is obviously less applicable for partial content.

sleeplessnerd
  • 21,853
  • 1
  • 25
  • 29
  • Nice. This approach has the advantage that it includes styles for pseudo-elements as well (`:hover`, `:after`, etc.). It is [impossible to style pseudo-elements using inline styles](https://stackoverflow.com/a/5293299/2054731). – ArneHugo Jun 13 '17 at 12:31
  • Thank you very much! I'm new to DOM and don't understand what you mean in step 3: "Add the captured `cssrules` as the first thing of the header in the `outerHTML` html text". – Ellen Spertus Dec 11 '19 at 22:05
3

Based on Luc125's answer, I've created a developer tools extension for Chrome that incorporates that code for capturing styles and markup for a page fragment. The extension is in the Chrome Web Store and is on Github. The "Computed Styles" output option uses that method.

Extension Screenshot

ifugu
  • 648
  • 7
  • 11
1

Internet Explorer -> Developer Tools -> DOM Explorer

Select element and right click -> "Copy element with styles".

ion
  • 540
  • 7
  • 20
1

Maybe the Google Closure Library has a solution for you.

There's code that seems to do what you need, i.e., to compute the CSS rules to reproduce the same appearance of an element outside of its current position in the dom (in their case they need that to transfer styles into an iframe to use it as a seamless inline editor).

Quoting from the source file style.js:

/**
 * @fileoverview Provides utility routines for copying modified
 * `CSSRule` objects from the parent document into iframes so that any
 * content in the iframe will be styled as if it was inline in the parent
 * document.
 *
 * <p>
 * For example, you might have this CSS rule:
 *
 * #content .highlighted { background-color: yellow; }
 *
 * And this DOM structure:
 *
 * <div id="content">
 *   <iframe />
 * </div>
 *
 * Then inside the iframe you have:
 *
 * <body>
 * <div class="highlighted">
 * </body>
 *
 * If you copied the CSS rule directly into the iframe, it wouldn't match the
 * .highlighted div. So we rewrite the original stylesheets based on the
 * context where the iframe is going to be inserted. In this case the CSS
 * selector would be rewritten to:
 *
 * body .highlighted { background-color: yellow; }
 * </p>
 */
Luc125
  • 5,752
  • 34
  • 35
alienhard
  • 14,376
  • 9
  • 37
  • 28
1

OK, maybe I'm missing something here, but isn't the string you want just document.documentElement.innerHTML? A quick test w/ Chrome verifies that it picks up the changes made in the Developer Tools to style attributes as you describe. Assigned class names aren't expanded (e.g., you'll have no idea what class="superfuntime" is doing), but if I'm reading your question correctly, you haven't stated a need for that.

JURU
  • 118
  • 3
  • 1
    I would need all properties from class superfuntime inlined – D-Nice Jun 10 '11 at 21:04
  • I ingored this at first as trivial, however at the end (after trying other things) I finally concluded that this works just great! Best solution for me! innerHTML will just do the job, innerHTML is great especially when a dom lement is changed later (e.g. via js, ajax etc).Thanks! – Melsi Apr 27 '14 at 17:39
0

Chrome feature - Printing the DOM:
The --dump-dom flag prints document.body.innerHTML to stdout:

chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/

Read more

Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37
saulsluz
  • 94
  • 10
  • This answer doesn't satisfy this part of the request: "properly take all the styles currently applied to an element (excluding default values) and include them in the inline style for that element." – dgrogan Apr 30 '18 at 18:46