下林明正のブログ

個人的かつ雑多なブログです。

PhantomJSを使ってCSSをインライン展開する

CSSをインライン展開したいという需要がある。

例えばHTMLメールを送る場合、外部CSSは解釈されない、headタグ内のstyleタグも解釈されない、各タグのstyle属性に記述するのが一番だ、というようなシチュエーションがある。

そんなわけで、CSSを簡単にインライン展開できないものかと調べていた。

先人が様々な試みをしているが、

というような感じで、複雑なCSSをぶち込むとまともに動きそうなものが簡単には見つからない。

そういったところで、ふと「PhantomJSを使ってComputed Styleを取得すれば完璧なんじゃね?」という天啓を授かったので、やってみた。

結論から言うと、まだきちんとできてないです。

var page = require('webpage').create();
var url = 'http://www.phantomjs.org/';
page.open(url, function (status) {
    //Page is loaded!
    var style = page.evaluate(function () {
        var e = document.getElementsByTagName('span')[0];
        var style = window.getComputedStyle(e, null);
        return style;
    });
    var cssText = style.cssText
    cssText = cssText.replace(/;/g, ";\n");
    cssText = cssText.replace(/ +/g, "");
    console.log(cssText);
    phantom.exit();
});

このコードを実行すると、

background-attachment:scroll;
background-clip:border-box;
background-color:rgba(0,0,0,0);
background-image:none;
background-origin:padding-box;
background-position:0%0%;
background-repeat:repeat;
background-size:autoauto;
border-bottom-color:rgb(255,255,255);
border-bottom-left-radius:0px;
border-bottom-right-radius:0px;
border-bottom-style:none;
border-bottom-width:0px;
border-collapse:separate;
border-left-color:rgb(255,255,255);
border-left-style:none;
border-left-width:0px;
border-right-color:rgb(255,255,255);
border-right-style:none;
border-right-width:0px;
border-top-color:rgb(255,255,255);
border-top-left-radius:0px;
border-top-right-radius:0px;
border-top-style:none;
border-top-width:0px;
bottom:auto;
box-shadow:none;
box-sizing:content-box;
caption-side:top;
clear:none;
clip:auto;
color:rgb(255,255,255);
cursor:auto;
direction:ltr;
display:inline;
empty-cells:show;
float:none;
font-family:'DroidSans','LucidaGrande',sans-serif;
font-size:24px;
font-style:normal;
font-variant:normal;
font-weight:normal;
height:0px;
left:auto;
letter-spacing:normal;
line-height:54px;
list-style-image:none;
list-style-position:outside;
list-style-type:disc;
margin-bottom:0px;
margin-left:0px;
margin-right:0px;
margin-top:0px;
max-height:none;
max-width:none;
min-height:0px;
min-width:0px;
opacity:1;
orphans:2;
outline-color:rgb(255,255,255);
outline-style:none;
outline-width:0px;
overflow-x:visible;
overflow-y:visible;
padding-bottom:0px;
padding-left:0px;
padding-right:0px;
padding-top:0px;
page-break-after:auto;
page-break-before:auto;
page-break-inside:auto;
pointer-events:auto;
position:static;
resize:none;
right:auto;
speak:normal;
table-layout:auto;
text-align:-webkit-auto;
text-decoration:none;
text-indent:0px;
text-rendering:auto;
text-shadow:rgb(61,151,0)0px-1px0px;
text-overflow:clip;
text-transform:none;
top:auto;
unicode-bidi:normal;
vertical-align:baseline;
visibility:visible;
white-space:normal;
widows:2;
width:0px;
word-break:normal;
word-spacing:0px;
word-wrap:normal;
z-index:auto;
zoom:1;
-webkit-animation-delay:0s;
-webkit-animation-direction:normal;
-webkit-animation-duration:0s;
-webkit-animation-fill-mode:none;
-webkit-animation-iteration-count:1;
-webkit-animation-name:none;
-webkit-animation-play-state:running;
-webkit-animation-timing-function:cubic-bezier(0.25,0.1,0.25,1);
-webkit-appearance:none;
-webkit-backface-visibility:visible;
-webkit-background-clip:border-box;
-webkit-background-composite:source-over;
-webkit-background-origin:padding-box;
-webkit-background-size:autoauto;
-webkit-border-fit:border;
-webkit-border-horizontal-spacing:0px;
-webkit-border-image:none;
-webkit-border-vertical-spacing:0px;
-webkit-box-align:stretch;
-webkit-box-direction:normal;
-webkit-box-flex:0;
-webkit-box-flex-group:1;
-webkit-box-lines:single;
-webkit-box-ordinal-group:1;
-webkit-box-orient:horizontal;
-webkit-box-pack:start;
-webkit-box-reflect:none;
-webkit-box-shadow:none;
-webkit-color-correction:default;
-webkit-column-break-after:auto;
-webkit-column-break-before:auto;
-webkit-column-break-inside:auto;
-webkit-column-count:auto;
-webkit-column-gap:normal;
-webkit-column-rule-color:rgb(255,255,255);
-webkit-column-rule-style:none;
-webkit-column-rule-width:0px;
-webkit-column-span:1;
-webkit-column-width:auto;
-webkit-font-smoothing:auto;
-webkit-highlight:none;
-webkit-hyphenate-character:auto;
-webkit-hyphenate-limit-after:auto;
-webkit-hyphenate-limit-before:auto;
-webkit-hyphens:manual;
-webkit-line-box-contain:blockinlinereplaced;
-webkit-line-break:normal;
-webkit-line-clamp:none;
-webkit-locale:auto;
-webkit-margin-before-collapse:collapse;
-webkit-margin-after-collapse:collapse;
-webkit-marquee-direction:auto;
-webkit-marquee-increment:6px;
-webkit-marquee-repetition:infinite;
-webkit-marquee-style:scroll;
-webkit-mask-attachment:scroll;
-webkit-mask-box-image:none;
-webkit-mask-clip:border-box;
-webkit-mask-composite:source-over;
-webkit-mask-image:none;
-webkit-mask-origin:border-box;
-webkit-mask-position:0%0%;
-webkit-mask-repeat:repeat;
-webkit-mask-size:autoauto;
-webkit-nbsp-mode:normal;
-webkit-perspective:none;
-webkit-perspective-origin:0px0px;
-webkit-rtl-ordering:logical;
-webkit-text-combine:none;
-webkit-text-decorations-in-effect:none;
-webkit-text-emphasis-color:rgb(255,255,255);
-webkit-text-emphasis-position:over;
-webkit-text-emphasis-style:none;
-webkit-text-fill-color:rgb(255,255,255);
-webkit-text-orientation:vertical-right;
-webkit-text-security:none;
-webkit-text-stroke-color:rgb(255,255,255);
-webkit-text-stroke-width:0px;
-webkit-transform:none;
-webkit-transform-origin:0px0px;
-webkit-transform-style:flat;
-webkit-transition-delay:0s;
-webkit-transition-duration:0s;
-webkit-transition-property:all;
-webkit-transition-timing-function:cubic-bezier(0.25,0.1,0.25,1);
-webkit-user-drag:auto;
-webkit-user-modify:read-only;
-webkit-user-select:text;
-webkit-writing-mode:horizontal-tb;
clip-path:none;
clip-rule:nonzero;
mask:none;
filter:none;
flood-color:rgb(0,0,0);
flood-opacity:1;
lighting-color:rgb(255,255,255);
stop-color:rgb(0,0,0);
stop-opacity:1;
color-interpolation:srgb;
color-interpolation-filters:linearrgb;
color-rendering:auto;
fill:#000000;
fill-opacity:1;
fill-rule:nonzero;
image-rendering:auto;
marker-end:none;
marker-mid:none;
marker-start:none;
shape-rendering:auto;
stroke:none;
stroke-dasharray:none;
stroke-dashoffset:0;
stroke-linecap:butt;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-width:1;
alignment-baseline:auto;
baseline-shift:baseline;
dominant-baseline:auto;
kerning:0;
text-anchor:start;
writing-mode:lr-tb;
glyph-orientation-horizontal:0deg;
glyph-orientation-vertical:auto;
-webkit-svg-shadow:none;
vector-effect:none;

このような実行結果を得られる。

これだと読む気が失せるので、予め空のdivのcssTextをnothing-css.txtとかに出力しておいて、 diff <(phantomjs get-css-text.js) nothing-css.txt とか実行すると、

9c9
< border-bottom-color:rgb(255,255,255);
---
> border-bottom-color:rgb(0,0,0);
15c15
< border-left-color:rgb(255,255,255);
---
> border-left-color:rgb(0,0,0);
18c18
< border-right-color:rgb(255,255,255);
---
> border-right-color:rgb(0,0,0);
21c21
< border-top-color:rgb(255,255,255);
---
> border-top-color:rgb(0,0,0);
32c32
< color:rgb(255,255,255);
---
> color:rgb(0,0,0);
35c35
< display:inline;
---
> display:block;
38,39c38,39
< font-family:'DroidSans','LucidaGrande',sans-serif;
< font-size:24px;
---
> font-family:'TimesNewRoman';
> font-size:16px;
46c46
< line-height:54px;
---
> line-height:normal;
60c60
< outline-color:rgb(255,255,255);
---
> outline-color:rgb(0,0,0);
82c82
< text-shadow:rgb(61,151,0)0px-1px0px;
---
> text-shadow:none;
91c91
< width:0px;
---
> width:384px;
131c131
< -webkit-column-rule-color:rgb(255,255,255);
---
> -webkit-column-rule-color:rgb(0,0,0);
163c163
< -webkit-perspective-origin:0px0px;
---
> -webkit-perspective-origin:192px0px;
167c167
< -webkit-text-emphasis-color:rgb(255,255,255);
---
> -webkit-text-emphasis-color:rgb(0,0,0);
170c170
< -webkit-text-fill-color:rgb(255,255,255);
---
> -webkit-text-fill-color:rgb(0,0,0);
173c173
< -webkit-text-stroke-color:rgb(255,255,255);
---
> -webkit-text-stroke-color:rgb(0,0,0);
176c176
< -webkit-transform-origin:0px0px;
---
> -webkit-transform-origin:192px0px;

という感じでそれなりにヒューマンリーダブルになる。

しかしながら残された問題として、あくまで取得したいのは「インライン展開されたCSS」であって「計算された結果のCSS」ではないので、この方法で得られるCSSは冗長すぎるというところがある。

Google ChromeのInspectorとかだとShow inheritedみたいなトグルオプションがあるけど、まさにああいう感じで取得したい。

僕はJavaScriptもPhantomJSも知らんという人間なのでもしかたら上手に取得できる方法があるのかも知れないけれど、簡単に調べてみた感じではその方法は分からなかった。

そんなわけで、誰か良いソリューションあったら教えて下さい。疲れた。