/** * 外部 CSS、埋め込み CSS を style 属性として XHTML テキストに割り当てます。 * エンコーディング形式は変換されない点に注意して下さい。 * * @param string $contents 適用対象の XHTML テキスト。 * @return string style 属性を適用した XHTML テキストを返します。 * @author Naomichi Yamakita <*****@*****.**> */ public function assign($contents) { if (null_or_empty($contents)) { return $content; } if (!mb_check_encoding($contents, 'UTF-8')) { $contents = mb_convert_encoding($contents, 'UTF-8', 'Shift_JIS'); } $this->response->setContentType('application/xhtml+xml; charset=Shift_JIS'); // XML 宣言を取り除く (saveXML() コール時に付加されるため) if (preg_match('/^<\\?xml\\s[^>]+?\\?>\\s*/', $contents, $matches)) { $contents = substr($contents, strlen($matches[0])); } // xmlns 属性を取り除く (saveXML() コール時に付加されるため) $contents = preg_replace('/xmlns=["\'][^"\']+["\']/', '', $contents); // charset に UTF-8 以外のエンコーディングが指定されている場合、DOMDocument::loadHTML() が解析に失敗する $contents = preg_replace('/charset=Shift_JIS/i', 'charset=UTF-8', $contents); // 数値文字参照をエスケープ $contents = preg_replace('/&(#(?:\\d+|x[0-9a-fA-F]+)|[A-Za-z0-9]+);/', 'HTMLCSSINLINERESCAPE%$1%::::::::', $contents); try { $dom = new DOMDocument(); $dom->loadXML($contents); $dom->formatOutput = TRUE; $dom->encoding = 'UTF-8'; $dom->xmlStandalone = FALSE; $this->_xpath = new DOMXPath($dom); $this->loadCSS(); // CSS をインライン化 $css = $this->_htmlCSS->toArray(); $styles = array(); foreach ($css as $selector => $style) { // Selector2XPath は疑似要素の解析が不安定のためスルー if (strpos($selector, '@') !== FALSE) { continue; } if (strpos($selector, ':') !== FALSE) { $inline = NULL; foreach ($style as $name => $value) { $inline .= sprintf('%s:%s;', $name, $value); } $styles[] = sprintf('%s{%s}', $selector, $inline); continue; } $xpath = HTML_CSS_Selector2XPath::toXPath($selector); try { $elements = $this->_xpath->query($xpath); if ($elements->length == 0) { continue; } $inline = NULL; foreach ($style as $name => $value) { $inline .= sprintf('%s:%s;', $name, $value); } foreach ($elements as $element) { if ($attributeStyle = $element->attributes->getNamedItem('style')) { $attributeStyle->nodeValue = $inline . $attributeStyle->nodeValue; } else { $element->setAttribute('style', $inline); } } // 無効なセレクタを無視 } catch (Exception $e) { } } // 疑似クラスを <style> タグとして追加する if (sizeof($styles)) { $style = implode(PHP_EOL, $styles); $head = $this->_xpath->query('//head'); $node = new DOMElement('style', $style); $head->item(0)->appendChild($node)->setAttribute('type', 'text/css'); } $result = $dom->saveXML(); $result = preg_replace('/encoding="UTF-8"/i', 'encoding="Shift_JIS"', $result); $result = preg_replace('/charset=UTF-8/i', 'charset=Shift_JIS', $result); $result = preg_replace('/HTMLCSSINLINERESCAPE%(#(?:\\d+|x[0-9a-fA-F]+)|[A-Za-z0-9]+)%::::::::/', '&$1;', $result); return $result; } catch (Exception $e) { // loadXML() がスローする例外が分かりにくいため、問題が起きたテンプレートのパスをメッセージに追加しておく $message = sprintf('Failed to parse template. (hint: %s) [%s]', $e->getMessage(), $this->renderer->getTemplatePath()); throw new Mars_ParseException($message); } }
public function apply($document, $base_dir = '') { if (!$base_dir) { $base_dir = $this->base_dir; } // XHTMLをパース $dom = new DOMDocument(); $dom->loadHTML($document); $dom_xpath = new DOMXPath($dom); // 外部参照のCSSファイルを抽出する $nodes = $dom_xpath->query('//link[@rel="stylesheet" or @type="text/css"] | //style[@type="text/css"]'); $add_style = array(); $psudo_classes = array('a:hover', 'a:link', 'a:focus', 'a:visited'); foreach ($nodes as $key => $node) { // CSSをパース $html_css = new HTML_CSS(); if ($node->tagName == 'link' && ($href = $node->attributes->getNamedItem('href'))) { // linkタグの場合 if (!file_exists($base_dir . $href->nodeValue)) { throw new UnexpectedValueException('ERROR: ' . $base_dir . $href->nodeValue . ' file does not exist'); } #TODO: @importのサポート $css_string = file_get_contents($base_dir . $href->nodeValue); } else { if ($node->tagName == 'style') { // styleタグの場合 $css_string = $node->nodeValue; } } $css_error = $html_css->parseString($css_string); if ($css_error) { throw new RuntimeException('ERROR: css parse error'); } // a:hover, a:link, a:focus a:visited を退避 foreach ($psudo_classes as $psude_class) { $block = $html_css->toInline($psude_class); if (!$block) { continue; } $add_style[] = $psude_class . '{' . $block . '}'; } // CSSをインライン化 $css = $html_css->toArray(); foreach ($css as $selector => $style) { #TODO: 疑似要素のサポート // 疑似要素と@ルールはスルー(selectorToXPath的にバグでやすい) if (strpos($selector, '@') !== false) { continue; } if (strpos($selector, ':') !== false) { continue; } $xpath = selectorToXPath::toXPath($selector); $elements = $dom_xpath->query($xpath); if ($elements->length == 0) { continue; } // inlineにするCSS文を構成(toInline($selector)だとh2, h3 などでうまくいかないバグ?があったため) $inline_style = ''; foreach ($style as $k => $v) { $inline_style .= $k . ':' . $v . ';'; } foreach ($elements as $element) { if ($attr_style = $element->attributes->getNamedItem('style')) { // style要素が存在する場合は追記 if (substr($attr_style->nodeValue, -1) != ';') { $inline_style = ';' . $inline_style; } $attr_style->nodeValue .= $inline_style; } else { // style要素が存在しない場合は追加 $element->setAttribute('style', $inline_style); } } } // 読み込み終わったノードを削除 $parent = $node->parentNode; $parent->removeChild($node); } // 疑似クラスを<style>タグとして追加 if (!empty($add_style)) { $new_style = implode(PHP_EOL, $add_style); $new_style = implode(PHP_EOL, array('<![CDATA[', $new_style, ']]>')); $head = $dom_xpath->query('//head'); $new_style_node = new DOMElement('style', $new_style); $head->item(0)->appendChild($new_style_node)->setAttribute('type', 'text/css'); } return $dom->saveHTML(); }