_pipeline = null;
$this->_order = 0;
$this->_base_url = array();
}
function push_base_url($url) {
array_unshift($this->_base_url, $url);
}
function pop_base_url() {
array_shift($this->_base_url);
}
function peek_base_url() {
return $this->_base_url[0];
}
function make_next_order() {
$this->_order++;
return $this->_order;
}
function get_pipeline() {
return $this->_pipeline;
}
function set_pipeline(&$pipeline) {
$this->_pipeline =& $pipeline;
}
/**
* Traverse DOM tree recursively starting from the $node.
* 'style' and 'link' nodes are handled separately.
*/
function &scan_node(&$node, &$ruleset) {
// Handle 'style' and 'link' tags specifically
if ($node->node_type() == XML_ELEMENT_NODE) {
$tagname = strtolower($node->tagname());
switch ($tagname) {
case 'style':
$ruleset->merge($this->process_style_node($node));
break;
case 'link':
$ruleset->merge($this->process_link_node($node));
break;
};
};
// Scan all child nodes of "normal" and document nodes
if (in_array($node->node_type(),
array(XML_ELEMENT_NODE,
XML_DOCUMENT_NODE))) {
$child = $node->first_child();
while ($child) {
$this->scan_node($child, $ruleset);
$child = $child->next_sibling();
};
};
return $css_ruleset;
}
function process_link_node(&$node) {
$ruleset =& new CSSRuleset();
$rel = strtolower($node->get_attribute('rel'));
$type = strtolower($node->get_attribute('type'));
// See HTML 4.01 p.14.2.3: This attribute (read: 'media') specifies the intended destination medium for style information.
// It may be a single media descriptor or a comma-separated list. The default value for this attribute is "screen".
if ($node->has_attribute('media')) {
$media = explode(',', strtolower($node->get_attribute('media')));
} else {
$media = array('screen');
};
// Subconditions for the large condition below
// 1. This is a stylesheet
$is_stylesheet = ($rel == 'stylesheet');
// 2. This is a CSS stylesheet
// Though HTML 4.01 standard says in p.14.2.3 "Authors must supply a value for this attribute; there is no default value for this attribute",
// a lot of coders do not obey the standard; let's assume that if 'type' is omitted, we're dealing with CSS
$is_css = ($type == 'text/css' || $type == '');
// 3. This stylesheet should be processed with current media
// HTML 4.01 p.14.2.3 "This attribute specifies the intended destination medium for style information.
// It may be a single media descriptor or a comma-separated list. The default value for this attribute is "screen".
// Yet again, let's be tolerant: if coder did not specify anything in the 'media' attribute value,
// we assume that this stylesheet should be processed.
// Note that if 'media' is omitted, we're using its default value - 'screent' (see above)
$is_media_to_be_processed = (count($media) == 0 || $this->is_allowed_media($media));
if ($is_stylesheet &&
$is_css &&
$is_media_to_be_processed) {
// Attempt to escape URL automaticaly
$url_autofix = new AutofixUrl();
$src = $url_autofix->apply(trim($node->get_attribute('href')));
if ($src) {
$ruleset = $this->import($src);
};
};
return $ruleset;
}
function &process_style_node(&$node) {
$pipeline =& $this->get_pipeline();
$ruleset = $this->import_source($node->get_content(),
$pipeline->get_base_url());
return $ruleset;
}
function import($url) {
// Import the referenced file
$pipeline =& $this->get_pipeline();
$data =& $pipeline->fetch($url);
if (!is_null($data)) {
return $this->import_source($data->content, $url);
};
return new CSSRuleset();
}
function import_source($css, $url) {
$this->push_base_url($url);
$ruleset = new CSSRuleset();
$stream = new CSSStreamString($css);
$lexer = new CSSLexer($stream);
$parser = new CSSParser($lexer);
$result = $parser->parse();
// Log CSS parse errors
$errors = $parser->get_errors();
foreach ($errors as $error) {
$error->log($url);
};
if (!$result) {
error_log(sprintf('Unrecoverable syntax error while parsing stylesheet at "%s"',
$url));
};
$stylesheet = $parser->get_context();
// Process @import
$imports = $stylesheet->get_imports();
foreach ($imports as $import) {
$media_list = $import->get_media();
if (empty($media_list) ||
in_array('all', $media_list) ||
$this->is_allowed_media($import->get_media())) {
$pipeline = $this->get_pipeline();
$full_url = $pipeline->guess_url(css_remove_value_quotes($import->get_url()));
$ruleset->merge($this->import($full_url));
};
};
// Process @media
$media = $stylesheet->get_media();
foreach ($media as $media_item) {
if ($this->is_allowed_media($media_item->get_media())) {
$ruleset->merge($this->import_rulesets($media_item->get_rulesets()));
};
}
// Process usual rules
$ruleset->merge($this->import_rulesets($stylesheet->get_rulesets()));
$this->pop_base_url();
return $ruleset;
}
function import_source_ruleset($css, $url) {
$this->push_base_url($url);
$property_collection = new CSSPropertyCollection();
$stream = new CSSStreamString(sprintf('* { %s }', $css));
$lexer = new CSSLexer($stream);
$parser = new CSSParser($lexer);
$result = $parser->parse_ruleset();
if (!$result) {
error_log(sprintf('Unrecoverable syntax error while parsing stylesheet at "%s"',
$url));
};
$ruleset = $parser->get_context();
return $this->import_ruleset_body($ruleset);
}
function import_rulesets($syntax_rulesets_collection) {
$ruleset = new CSSRuleset();
$syntax_rulesets = $syntax_rulesets_collection->get();
foreach ($syntax_rulesets as $syntax_ruleset) {
$selectors = $this->import_ruleset_selectors($syntax_ruleset);
$body = $this->import_ruleset_body($syntax_ruleset);
foreach ($selectors as $selector) {
$rule =& new CSSRule($selector,
$body,
$this->peek_base_url(),
$this->make_next_order());
};
$ruleset->add_rule($rule);
};
return $ruleset;
}
function import_ruleset_selector($syntax_selector) {
$selector = array();
$syntax_selectors_simple = $syntax_selector->get_selectors();
$syntax_combinators = $syntax_selector->get_combinators();
$selector = $this->import_ruleset_selector_simple($syntax_selectors_simple[0]);
for ($i = 0, $size = count($syntax_selectors_simple); $i < $size-1; $i++) {
$syntax_selector_simple = $syntax_selectors_simple[$i+1];
$syntax_combinator = $syntax_combinators[$i];
switch ($syntax_combinator->get_type()) {
case COMBINATOR_PLUS:
$selector = array(SELECTOR_SEQUENCE,
array($this->import_ruleset_selector_simple($syntax_selector_simple),
array(SELECTOR_SIBLING,
$selector)));
break;
case COMBINATOR_GREATER:
$selector = array(SELECTOR_SEQUENCE,
array($this->import_ruleset_selector_simple($syntax_selector_simple),
array(SELECTOR_DIRECT_PARENT,
$selector)));
break;
case COMBINATOR_EMPTY:
$selector = array(SELECTOR_SEQUENCE,
array($this->import_ruleset_selector_simple($syntax_selector_simple),
array(SELECTOR_PARENT,
$selector)));
break;
};
};
return $selector;
}
function import_ruleset_selector_simple($syntax_selector) {
$selector = array(SELECTOR_ANY);
if (!is_null($syntax_selector->get_element())) {
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_TAG, $syntax_selector->get_element()),
$selector));
};
$ids = $syntax_selector->get_ids();
foreach ($ids as $id) {
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_ID, substr($id, 1)), // Strip # from the beginning of the id string
$selector));
};
$classes = $syntax_selector->get_classes();
foreach ($classes as $class) {
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_CLASS, $class),
$selector));
};
$attribs = $syntax_selector->get_attribs();
foreach ($attribs as $attrib) {
switch ($attrib->get_op()) {
case ATTRIB_OP_EQUAL:
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_ATTR_VALUE,
$attrib->get_name(),
css_remove_value_quotes($attrib->get_value())),
$selector));
break;
case ATTRIB_OP_DASHMATCH:
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_ATTR_VALUE_WORD_HYPHEN,
$attrib->get_name(),
css_remove_value_quotes($attrib->get_value())),
$selector));
break;
case ATTRIB_OP_INCLUDES:
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_ATTR_VALUE_WORD,
$attrib->get_name(),
css_remove_value_quotes($attrib->get_value())),
$selector));
break;
default:
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_ATTR,
$attrib->get_name()),
$selector));
break;
};
};
$pseudo = $syntax_selector->get_pseudo();
foreach ($pseudo as $pseudo_item) {
switch ($pseudo_item->get_name()) {
case 'link':
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_PSEUDOCLASS_LINK),
$selector));
break;
case 'before':
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_PSEUDOELEMENT_BEFORE),
$selector));
break;
case 'after':
$selector = array(SELECTOR_SEQUENCE,
array(array(SELECTOR_PSEUDOELEMENT_AFTER),
$selector));
break;
case 'visited':
case 'active':
case 'hover':
// No nothing, as html2ps is not interactive user-agent
break;
default:
error_log(sprintf('Unknown pseudo selector: "%s"', $pseudo_item->get_name()));
};
};
return $selector;
}
function import_ruleset_selectors($syntax_ruleset) {
$selectors = array();
$syntax_selectors_collection = $syntax_ruleset->get_selectors();
$syntax_selectors = $syntax_selectors_collection->get();
foreach ($syntax_selectors as $syntax_selector) {
$selectors[] = $this->import_ruleset_selector($syntax_selector);
};
return $selectors;
}
function import_ruleset_body($syntax_ruleset) {
$body = new CSSPropertyCollection();
$declarations_obj = $syntax_ruleset->get_declarations();
$declarations = $declarations_obj->get();
foreach ($declarations as $declaration) {
$property = $this->import_declaration($declaration);
if ($property) {
$body->add_property($property);
};
};
return $body;
}
function import_declaration($declaration) {
$syntax_property = $declaration->get_property();
$code = CSS::name2code($syntax_property->get_name());
if (is_null($code)) {
return null;
};
$handler = CSS::get_handler($code);
$property = new CSSPropertyDeclaration();
$property->set_code($code);
$expr = $declaration->get_expr();
$property->set_value($handler->parse($expr->to_string(),
$this->get_pipeline(),
$expr));
$property->set_important($declaration->get_important());
return $property;
}
function is_allowed_media($media_list) {
// Now we've got the list of media this style can be applied to;
// check if at least one of this media types is being used by the script
//
$allowed_media = config_get_allowed_media();
$allowed_found = false;
// Note that media names should be case-insensitive;
// it is not guaranteed that $media_list contains lower-case variants,
// as well as it is not guaranteed that configuration data contains them.
// Thus, media name lists should be explicitly converted to lowercase
$media_list = array_map('strtolower', $media_list);
$allowed_media = array_map('strtolower', $allowed_media);
foreach ($media_list as $media) {
$allowed_found |= (array_search($media, $allowed_media) !== false);
};
return $allowed_found;
}
}
?>