/** * 外部 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); } }
/** * apply CSSをインライン化 * * @param string $document 変換を行うHTML文書 * @param string $base_dir CSSのベースディレクトリ(setBaseDirより優先) * @return string 変換されたHTML */ public function apply($document, $base_dir = '') { /**************************************** * 前処理 ****************************************/ if ($base_dir) { $this->base_dir = $base_dir; } // loadHTML/saveHTMLのバグに対応。XML宣言の一時退避 $declaration = ''; if (preg_match('/^<\\?xml\\s[^>]+?\\?>\\s*/', $document, $e)) { $declaration = $e[0]; $document = substr($document, strlen($declaration)); } // 文字参照をエスケープ $document = preg_replace('/&(#(?:\\d+|x[0-9a-fA-F]+)|[A-Za-z0-9]+);/', 'HTMLCSSINLINERESCAPE%$1%::::::::', $document); // 機種依存文字がエラーになる問題を回避するため、UTF-8に変換して処理 $doc_encoding = mb_detect_encoding($document, 'sjis-win, UTF-8, eucjp-win'); switch (strtolower($doc_encoding)) { case 'sjis-win': $html_encoding = 'Shift_JIS'; break; case 'eucjp-win': $html_encoding = 'EUC-JP'; break; default: $html_encoding = ''; break; } if ($doc_encoding != 'UTF-8') { $document = str_replace(array('UTF-8', $html_encoding), array('@####UTF8####@', 'UTF-8'), $document); $document = mb_convert_encoding($document, 'UTF-8', $doc_encoding); } /**************************************** * 本処理 ****************************************/ // XHTMLをパース $this->dom = new DOMDocument(); $this->dom->loadHTML($document); $this->dom_xpath = new DOMXPath($this->dom); $this->loadCSS(); // CSSをインライン化 $css = $this->html_css->toArray(); $add_style = array(); foreach ($css as $selector => $style) { // 疑似要素は退避。@ルールはスルー(Selector2XPath的にバグでやすい) if (strpos($selector, '@') !== false) { continue; } if (strpos($selector, ':') !== false) { $add_style[] = $selector . '{' . $this->html_css->toInline($selector) . '}'; continue; } $xpath = HTML_CSS_Selector2XPath::toXPath($selector); $elements = $this->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要素が存在する場合は前方追記 #TODO: できれば、重複回避もしたい。少しロジックがまどろっこしい順序になってしまうのだが。。。 $attr_style->nodeValue = $inline_style . $attr_style->nodeValue; } else { // style要素が存在しない場合は追加 $element->setAttribute('style', $inline_style); } } } // 疑似クラスを<style>タグとして追加 if (!empty($add_style)) { $new_style = implode(PHP_EOL, $add_style); $new_style = str_replace(']]>', ']]]><![CDATA[]>', $new_style); $new_style = implode(PHP_EOL, array('<![CDATA[', $new_style, ']]>')); $head = $this->dom_xpath->query('//head'); $new_style_node = new DOMElement('style', $new_style); $head->item(0)->appendChild($new_style_node)->setAttribute('type', 'text/css'); } $result = $this->dom->saveHTML(); /**************************************** * 後処理 ****************************************/ // 文字コードを元に戻す if ($doc_encoding != 'UTF-8') { $result = mb_convert_encoding($result, $doc_encoding, 'UTF-8'); $result = str_replace(array('UTF-8', '@####UTF8####@'), array($html_encoding, 'UTF-8'), $result); } // エスケープしていた参照を復元 $result = preg_replace('/HTMLCSSINLINERESCAPE%(#(?:\\d+|x[0-9a-fA-F]+)|[A-Za-z0-9]+)%::::::::/', '&$1;', $result); // 退避したXML宣言を復元 if (!empty($declaration)) { $result = $declaration . $result; } return $result; }