/* * File: css.cc * * Copyright 2008-2009 Johannes Hofmann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ #include #include "../dlib/dlib.h" #include "msg.h" #include "html_common.hh" #include "css.hh" using namespace dw::core::style; void CssProperty::print () { fprintf (stderr, "%s - %d\n", CssParser::propertyNameString((CssPropertyName)name), (int)value.intVal); } CssPropertyList::CssPropertyList (const CssPropertyList &p, bool deep) : lout::misc::SimpleVector (p) { refCount = 0; safe = p.safe; if (deep) { for (int i = 0; i < size (); i++) { CssProperty *p = getRef(i); switch (p->type) { case CSS_TYPE_STRING: case CSS_TYPE_SYMBOL: p->value.strVal = dStrdup (p->value.strVal); break; default: break; } } ownerOfStrings = true; } else { ownerOfStrings = false; } }; CssPropertyList::~CssPropertyList () { if (ownerOfStrings) for (int i = 0; i < size (); i++) getRef (i)->free (); } /** * \brief Set property to a given name and type. */ void CssPropertyList::set (CssPropertyName name, CssValueType type, CssPropertyValue value) { CssProperty *prop; if (name == CSS_PROPERTY_DISPLAY || name == CSS_PROPERTY_BACKGROUND_IMAGE) safe = false; for (int i = 0; i < size (); i++) { prop = getRef (i); if (prop->name == name) { if (ownerOfStrings) prop->free (); prop->type = type; prop->value = value; return; } } increase (); prop = getRef (size () - 1); prop->name = name; prop->type = type; prop->value = value; } /** * \brief Merge properties into argument property list. */ void CssPropertyList::apply (CssPropertyList *props) { for (int i = 0; i < size (); i++) props->set ((CssPropertyName) getRef (i)->name, (CssValueType) getRef (i)->type, getRef (i)->value); } void CssPropertyList::print () { for (int i = 0; i < size (); i++) getRef (i)->print (); } CssSelector::CssSelector () { struct CombinatorAndSelector *cs; refCount = 0; selectorList = new lout::misc::SimpleVector (1); selectorList->increase (); cs = selectorList->getRef (selectorList->size () - 1); cs->notMatchingBefore = -1; cs->combinator = COMB_NONE; cs->selector = new CssSimpleSelector (); }; CssSelector::~CssSelector () { for (int i = selectorList->size () - 1; i >= 0; i--) delete selectorList->getRef (i)->selector; delete selectorList; } /** * \brief Return whether selector matches at a given node in the document tree. */ bool CssSelector::match (Doctree *docTree, const DoctreeNode *node, int i, Combinator comb) { assert (node); if (i < 0) return true; struct CombinatorAndSelector *cs = selectorList->getRef (i); CssSimpleSelector *sel = cs->selector; switch (comb) { case COMB_NONE: break; case COMB_CHILD: node = docTree->parent (node); break; case COMB_ADJACENT_SIBLING: node = docTree->sibling (node); break; case COMB_DESCENDANT: node = docTree->parent (node); for (const DoctreeNode *n = node; n && n->num > cs->notMatchingBefore; n = docTree->parent (n)) if (sel->match (n) && match (docTree, n, i - 1, cs->combinator)) return true; if (node) // remember that it didn't match to avoid future tests cs->notMatchingBefore = node->num; return false; break; default: return false; // \todo implement other combinators } if (!node || !sel->match (node)) return false; // tail recursion should be optimized by the compiler return match (docTree, node, i - 1, cs->combinator); } void CssSelector::addSimpleSelector (Combinator c) { struct CombinatorAndSelector *cs; selectorList->increase (); cs = selectorList->getRef (selectorList->size () - 1); cs->combinator = c; cs->notMatchingBefore = -1; cs->selector = new CssSimpleSelector (); } bool CssSelector::checksPseudoClass () { for (int i = 0; i < selectorList->size (); i++) if (selectorList->getRef (i)->selector->getPseudoClass ()) return true; return false; } /** * \brief Return the specificity of the selector. * * The specificity of a CSS selector is defined in * http://www.w3.org/TR/CSS21/cascade.html#specificity */ int CssSelector::specificity () { int spec = 0; for (int i = 0; i < selectorList->size (); i++) spec += selectorList->getRef (i)->selector->specificity (); return spec; } void CssSelector::print () { for (int i = 0; i < selectorList->size (); i++) { selectorList->getRef (i)->selector->print (); if (i < selectorList->size () - 1) { switch (selectorList->getRef (i + 1)->combinator) { case COMB_CHILD: fprintf (stderr, "> "); break; case COMB_DESCENDANT: fprintf (stderr, "\" \" "); break; case COMB_ADJACENT_SIBLING: fprintf (stderr, "+ "); break; default: fprintf (stderr, "? "); break; } } } fprintf (stderr, "\n"); } CssSimpleSelector::CssSimpleSelector () { element = ELEMENT_ANY; klass = NULL; id = NULL; pseudo = NULL; } CssSimpleSelector::~CssSimpleSelector () { if (klass) { for (int i = 0; i < klass->size (); i++) dFree (klass->get (i)); delete klass; } dFree (id); dFree (pseudo); } void CssSimpleSelector::setSelect (SelectType t, const char *v) { switch (t) { case SELECT_CLASS: if (klass == NULL) klass = new lout::misc::SimpleVector (1); klass->increase (); klass->set (klass->size () - 1, dStrdup (v)); break; case SELECT_PSEUDO_CLASS: if (pseudo == NULL) pseudo = dStrdup (v); break; case SELECT_ID: if (id == NULL) id = dStrdup (v); break; default: break; } } /** * \brief Return whether simple selector matches at a given node of * the document tree. */ bool CssSimpleSelector::match (const DoctreeNode *n) { assert (n); if (element != ELEMENT_ANY && element != n->element) return false; if (pseudo != NULL && (n->pseudo == NULL || dStrAsciiCasecmp (pseudo, n->pseudo) != 0)) return false; if (id != NULL && (n->id == NULL || dStrAsciiCasecmp (id, n->id) != 0)) return false; if (klass != NULL) { for (int i = 0; i < klass->size (); i++) { bool found = false; if (n->klass != NULL) { for (int j = 0; j < n->klass->size (); j++) { if (dStrAsciiCasecmp (klass->get(i), n->klass->get(j)) == 0) { found = true; break; } } } if (! found) return false; } } return true; } /** * \brief Return the specificity of the simple selector. * * The result is used in CssSelector::specificity (). */ int CssSimpleSelector::specificity () { int spec = 0; if (id) spec += 1 << 20; if (klass) spec += klass->size() << 10; if (pseudo) spec += 1 << 10; if (element != ELEMENT_ANY) spec += 1; return spec; } void CssSimpleSelector::print () { fprintf (stderr, "Element %d, pseudo %s, id %s ", element, pseudo, id); if (klass != NULL) { fprintf (stderr, "class "); for (int i = 0; i < klass->size (); i++) fprintf (stderr, ".%s", klass->get (i)); } } CssRule::CssRule (CssSelector *selector, CssPropertyList *props, int pos) { assert (selector->size () > 0); this->selector = selector; this->selector->ref (); this->props = props; this->props->ref (); this->pos = pos; spec = selector->specificity (); }; CssRule::~CssRule () { selector->unref (); props->unref (); }; void CssRule::apply (CssPropertyList *props, Doctree *docTree, const DoctreeNode *node) { if (selector->match (docTree, node)) this->props->apply (props); } void CssRule::print () { selector->print (); props->print (); } /* * \brief Insert rule with increasing specificity. * * If two rules have the same specificity, the one that was added later * will be added behind the others. * This gives later added rules more weight. */ void CssStyleSheet::RuleList::insert (CssRule *rule) { increase (); int i = size () - 1; while (i > 0 && rule->specificity () < get (i - 1)->specificity ()) { *getRef (i) = get (i - 1); i--; } *getRef (i) = rule; } /** * \brief Insert a rule into CssStyleSheet. * * To improve matching performance the rules are organized into * rule lists based on the topmost simple selector of their selector. */ void CssStyleSheet::addRule (CssRule *rule) { CssSimpleSelector *top = rule->selector->top (); RuleList *ruleList = NULL; lout::object::ConstString *string; if (top->getId ()) { string = new lout::object::ConstString (top->getId ()); ruleList = idTable.get (string); if (ruleList == NULL) { ruleList = new RuleList (); idTable.put (string, ruleList); } else { delete string; } } else if (top->getClass () && top->getClass ()->size () > 0) { string = new lout::object::ConstString (top->getClass ()->get (0)); ruleList = classTable.get (string); if (ruleList == NULL) { ruleList = new RuleList; classTable.put (string, ruleList); } else { delete string; } } else if (top->getElement () >= 0 && top->getElement () < ntags) { ruleList = &elementTable[top->getElement ()]; } else if (top->getElement () == CssSimpleSelector::ELEMENT_ANY) { ruleList = &anyTable; } if (ruleList) { ruleList->insert (rule); } else { assert (top->getElement () == CssSimpleSelector::ELEMENT_NONE); delete rule; } } /** * \brief Apply a stylesheet to a property list. * * The properties are set as defined by the rules in the stylesheet that * match at the given node in the document tree. */ void CssStyleSheet::apply (CssPropertyList *props, Doctree *docTree, const DoctreeNode *node) { static const int maxLists = 32; RuleList *ruleList[maxLists]; int numLists = 0, index[maxLists] = {0}; if (node->id) { lout::object::ConstString idString (node->id); ruleList[numLists] = idTable.get (&idString); if (ruleList[numLists]) numLists++; } if (node->klass) { for (int i = 0; i < node->klass->size (); i++) { if (i >= maxLists - 4) { MSG_WARN("Maximum number of classes per element exceeded.\n"); break; } lout::object::ConstString classString (node->klass->get (i)); ruleList[numLists] = classTable.get (&classString); if (ruleList[numLists]) numLists++; } } ruleList[numLists] = &elementTable[node->element]; if (ruleList[numLists]) numLists++; ruleList[numLists] = &anyTable; if (ruleList[numLists]) numLists++; // Apply potentially matching rules from ruleList[0-numLists] with // ascending specificity. // If specificity is equal, rules are applied in order of appearance. // Each ruleList is sorted already. while (true) { int minSpec = 1 << 30; int minPos = 1 << 30; int minSpecIndex = -1; for (int i = 0; i < numLists; i++) { RuleList *rl = ruleList[i]; if (rl && rl->size () > index[i] && (rl->get(index[i])->specificity () < minSpec || (rl->get(index[i])->specificity () == minSpec && rl->get(index[i])->position () < minPos))) { minSpec = rl->get(index[i])->specificity (); minPos = rl->get(index[i])->position (); minSpecIndex = i; } } if (minSpecIndex >= 0) { ruleList[minSpecIndex]->get (index[minSpecIndex])->apply (props, docTree, node); index[minSpecIndex]++; } else { break; } } } CssContext::CssContext () { pos = 0; } /** * \brief Apply a CSS context to a property list. * * The stylesheets in the context are applied one after the other * in the ordering defined by CSS 2.1. * Stylesheets that are applied later can overwrite properties set * by previous stylesheets. * This allows e.g. user styles to overwrite author styles. */ void CssContext::apply (CssPropertyList *props, Doctree *docTree, DoctreeNode *node, CssPropertyList *tagStyle, CssPropertyList *tagStyleImportant, CssPropertyList *nonCssHints) { sheet[CSS_PRIMARY_USER_AGENT].apply (props, docTree, node); sheet[CSS_PRIMARY_USER].apply (props, docTree, node); if (nonCssHints) nonCssHints->apply (props); sheet[CSS_PRIMARY_AUTHOR].apply (props, docTree, node); if (tagStyle) tagStyle->apply (props); sheet[CSS_PRIMARY_AUTHOR_IMPORTANT].apply (props, docTree, node); if (tagStyleImportant) tagStyleImportant->apply (props); sheet[CSS_PRIMARY_USER_IMPORTANT].apply (props, docTree, node); } void CssContext::addRule (CssSelector *sel, CssPropertyList *props, CssPrimaryOrder order) { if (props->size () > 0) { CssRule *rule = new CssRule (sel, props, pos++); if ((order == CSS_PRIMARY_AUTHOR || order == CSS_PRIMARY_AUTHOR_IMPORTANT) && !rule->isSafe ()) { MSG_WARN ("Ignoring unsafe author style that might reveal browsing history\n"); delete rule; } else { sheet[order].addRule (rule); } } }