diff --git a/src/components/script/style/.gitignore b/src/components/script/style/.gitignore new file mode 100644 index 00000000000..55369470c0c --- /dev/null +++ b/src/components/script/style/.gitignore @@ -0,0 +1,10 @@ +*.o +*.a +*.so +*.dylib +*.dll +*.dummy +*.pyc +*-test +Makefile +properties/mod.rs diff --git a/src/components/script/style/LICENSE b/src/components/script/style/LICENSE new file mode 100644 index 00000000000..14e2f777f6c --- /dev/null +++ b/src/components/script/style/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/src/components/script/style/Makefile.in b/src/components/script/style/Makefile.in new file mode 100644 index 00000000000..585705efd53 --- /dev/null +++ b/src/components/script/style/Makefile.in @@ -0,0 +1,39 @@ +VPATH=%VPATH% +CSSPARSER_DIR ?= ../rust-cssparser + +CC ?= gcc +CXX ?= g++ +CXXFLAGS ?= +AR ?= ar +RUSTC ?= rustc +RUSTFLAGS ?= -L $(CSSPARSER_DIR) + + +PROPERTIES_RS=$(VPATH)/properties/mod.rs +RUST_SRC=$(shell find $(VPATH)/. -type f -name '*.rs') $(CSSPARSER_DIR)/libcssparser.dummy $(PROPERTIES_RS) + +.PHONY: all +all: libservo-style.dummy + +libservo-style.dummy: servo-style.rc $(RUST_SRC) + $(RUSTC) $(RUSTFLAGS) $< + touch $@ + +servo-style-test: servo-style.rc $(RUST_SRC) + $(RUSTC) $(RUSTFLAGS) $< -o $@ --test + +.PHONY: check +check: servo-style-test + ./servo-style-test + +.PHONY: check-debug +check-debug: servo-style-tests + echo -e "start\n break upcall_fail\n continue\n where\n continue" | gdb -q ./servo-style-tests + +.PHONY: clean +clean: + rm -f *.o *.a *.so *.dylib *.dll *.dummy *-test + + +$(PROPERTIES_RS): $(PROPERTIES_RS).mako + PYTHONPATH=$(VPATH)/Mako-0.8.1.zip python -c "from mako.template import Template; print(Template(filename='$<').render())" > $@ diff --git a/src/components/script/style/Mako-0.8.1.zip b/src/components/script/style/Mako-0.8.1.zip new file mode 100644 index 00000000000..c324c72ef8a Binary files /dev/null and b/src/components/script/style/Mako-0.8.1.zip differ diff --git a/src/components/script/style/README.md b/src/components/script/style/README.md new file mode 100644 index 00000000000..fd3bb045639 --- /dev/null +++ b/src/components/script/style/README.md @@ -0,0 +1,10 @@ +servo-style +=========== + +Prototype replacement style system for Servo, +based on [rust-cssparser](https://github.com/mozilla-servo/rust-cssparser) +instead of [NetSurf’s libcss](https://github.com/mozilla-servo/libcss). + +This is meant to go into Servo’s `src/components/script/style` directory, +but is maintained here for now because it requires at least +Rust ecfc9a8 (2013-08-12). diff --git a/src/components/script/style/configure b/src/components/script/style/configure new file mode 100755 index 00000000000..62a0f4cd3e6 --- /dev/null +++ b/src/components/script/style/configure @@ -0,0 +1,4 @@ +#!/bin/bash + +SRCDIR="$(cd $(dirname $0) && pwd)" +sed "s#%VPATH%#${SRCDIR}#" ${SRCDIR}/Makefile.in > Makefile diff --git a/src/components/script/style/errors.rs b/src/components/script/style/errors.rs new file mode 100644 index 00000000000..2a60f66dca8 --- /dev/null +++ b/src/components/script/style/errors.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use cssparser::{SyntaxError, SourceLocation}; + + +pub struct ErrorLoggerIterator(I); + +impl>> Iterator for ErrorLoggerIterator { + fn next(&mut self) -> Option { + for result in **self { + match result { + Ok(v) => return Some(v), + Err(error) => log_css_error(error.location, fmt!("%?", error.reason)) + } + } + None + } +} + + +pub fn log_css_error(location: SourceLocation, message: &str) { + // TODO eventually this will got into a "web console" or something. + info!("%u:%u %s", location.line, location.column, message) +} diff --git a/src/components/script/style/media_queries.rs b/src/components/script/style/media_queries.rs new file mode 100644 index 00000000000..3266130fcf1 --- /dev/null +++ b/src/components/script/style/media_queries.rs @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::ascii::StrAsciiExt; +use cssparser::*; +use errors::{ErrorLoggerIterator, log_css_error}; +use stylesheets::{CSSRule, CSSMediaRule, parse_style_rule, parse_nested_at_rule}; +use namespaces::NamespaceMap; + + +pub struct MediaRule { + media_queries: MediaQueryList, + rules: ~[CSSRule], +} + + +pub struct MediaQueryList { + // "not all" is omitted from the list. + // An empty list never matches. + media_queries: ~[MediaQuery] +} + +// For now, this is a "Level 2 MQ", ie. a media type. +struct MediaQuery { + media_type: MediaQueryType, + // TODO: Level 3 MQ expressions +} + + +enum MediaQueryType { + All, // Always true + MediaType(MediaType), +} + +#[deriving(Eq)] +pub enum MediaType { + Screen, + Print, +} + +pub struct Device { + media_type: MediaType, + // TODO: Level 3 MQ data: viewport size, etc. +} + + +pub fn parse_media_rule(rule: AtRule, parent_rules: &mut ~[CSSRule], + namespaces: &NamespaceMap) { + let media_queries = parse_media_query_list(rule.prelude); + let block = match rule.block { + Some(block) => block, + None => { + log_css_error(rule.location, "Invalid @media rule"); + return + } + }; + let mut rules = ~[]; + for rule in ErrorLoggerIterator(parse_rule_list(block.move_iter())) { + match rule { + QualifiedRule(rule) => parse_style_rule(rule, &mut rules, namespaces), + AtRule(rule) => parse_nested_at_rule( + rule.name.to_ascii_lower(), rule, &mut rules, namespaces), + } + } + parent_rules.push(CSSMediaRule(MediaRule { + media_queries: media_queries, + rules: rules, + })) +} + + +pub fn parse_media_query_list(input: &[ComponentValue]) -> MediaQueryList { + let iter = &mut input.skip_whitespace(); + let mut next = iter.next(); + if next.is_none() { + return MediaQueryList{ media_queries: ~[MediaQuery{media_type: All}] } + } + let mut queries = ~[]; + loop { + let mq = match next { + Some(&Ident(ref value)) => { + match value.to_ascii_lower().as_slice() { + "screen" => Some(MediaQuery{ media_type: MediaType(Screen) }), + "print" => Some(MediaQuery{ media_type: MediaType(Print) }), + "all" => Some(MediaQuery{ media_type: All }), + _ => None + } + }, + _ => None + }; + match iter.next() { + None => { + mq.map_move(|mq| queries.push(mq)); + return MediaQueryList{ media_queries: queries } + }, + Some(&Comma) => { + mq.map_move(|mq| queries.push(mq)); + }, + // Ingnore this comma-separated part + _ => loop { + match iter.next() { + Some(&Comma) => break, + None => return MediaQueryList{ media_queries: queries }, + _ => (), + } + }, + } + next = iter.next(); + } +} + + +impl MediaQueryList { + pub fn evaluate(&self, device: &Device) -> bool { + do self.media_queries.iter().any |mq| { + match mq.media_type { + MediaType(media_type) => media_type == device.media_type, + All => true, + } + // TODO: match Level 3 expressions + } + } +} diff --git a/src/components/script/style/namespaces.rs b/src/components/script/style/namespaces.rs new file mode 100644 index 00000000000..3c94ec3b3ea --- /dev/null +++ b/src/components/script/style/namespaces.rs @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::hashmap::HashMap; +use cssparser::*; +use errors::log_css_error; + +pub struct NamespaceMap { + default: Option<~str>, // Optional URL + prefix_map: HashMap<~str, ~str>, // prefix -> URL +} + + +impl NamespaceMap { + pub fn new() -> NamespaceMap { + NamespaceMap { default: None, prefix_map: HashMap::new() } + } +} + + +pub fn parse_namespace_rule(rule: AtRule, namespaces: &mut NamespaceMap) { + let location = rule.location; + macro_rules! syntax_error( + () => {{ + log_css_error(location, "Invalid @namespace rule"); + return + }}; + ); + if rule.block.is_some() { syntax_error!() } + let mut prefix: Option<~str> = None; + let mut url: Option<~str> = None; + let mut iter = rule.prelude.move_skip_whitespace(); + for component_value in iter { + match component_value { + Ident(value) => { + if prefix.is_some() { syntax_error!() } + prefix = Some(value); + }, + URL(value) | String(value) => { + if url.is_some() { syntax_error!() } + url = Some(value); + break + }, + _ => syntax_error!(), + } + } + if iter.next().is_some() { syntax_error!() } + match (prefix, url) { + (Some(prefix), Some(url)) => { + if namespaces.prefix_map.swap(prefix, url).is_some() { + log_css_error(location, "Duplicate @namespace rule"); + } + }, + (None, Some(url)) => { + if namespaces.default.is_some() { + log_css_error(location, "Duplicate @namespace rule"); + } + namespaces.default = Some(url); + }, + _ => syntax_error!() + } +} diff --git a/src/components/script/style/parsing_utils.rs b/src/components/script/style/parsing_utils.rs new file mode 100644 index 00000000000..062e4f5a6ae --- /dev/null +++ b/src/components/script/style/parsing_utils.rs @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +use std::ascii::StrAsciiExt; +use cssparser::*; + + +pub fn one_component_value<'a>(input: &'a [ComponentValue]) -> Option<&'a ComponentValue> { + let mut iter = input.skip_whitespace(); + iter.next().filtered(|_| iter.next().is_none()) +} + + +pub fn get_ident_lower(component_value: &ComponentValue) -> Option<~str> { + match component_value { + &Ident(ref value) => Some(value.to_ascii_lower()), + _ => None, + } +} diff --git a/src/components/script/style/properties/common_types.rs b/src/components/script/style/properties/common_types.rs new file mode 100644 index 00000000000..8031df10479 --- /dev/null +++ b/src/components/script/style/properties/common_types.rs @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub type Float = f64; +pub type Integer = i64; + + +pub mod specified { + use std::ascii::StrAsciiExt; + use cssparser::*; + use super::{Integer, Float}; + pub use CSSColor = cssparser::Color; + + pub enum Length { + Au(Integer), // application units + Em(Float), + Ex(Float), + // XXX uncomment when supported: +// Ch(Float), +// Rem(Float), +// Vw(Float), +// Vh(Float), +// Vmin(Float), +// Vmax(Float), + } + static AU_PER_PX: Float = 60.; + static AU_PER_IN: Float = AU_PER_PX * 96.; + static AU_PER_CM: Float = AU_PER_IN / 2.54; + static AU_PER_MM: Float = AU_PER_IN / 25.4; + static AU_PER_PT: Float = AU_PER_IN / 72.; + static AU_PER_PC: Float = AU_PER_PT * 12.; + impl Length { + #[inline] + fn parse_internal(input: &ComponentValue, negative_ok: bool) -> Option { + match input { + &Dimension(ref value, ref unit) if negative_ok || value.value >= 0. + => Length::parse_dimension(value.value, unit.as_slice()), + &Number(ref value) if value.value == 0. => Some(Au(0)), + _ => None + } + } + pub fn parse(input: &ComponentValue) -> Option { + Length::parse_internal(input, /* negative_ok = */ true) + } + pub fn parse_non_negative(input: &ComponentValue) -> Option { + Length::parse_internal(input, /* negative_ok = */ false) + } + pub fn parse_dimension(value: Float, unit: &str) -> Option { + match unit.to_ascii_lower().as_slice() { + "px" => Some(Length::from_px(value)), + "in" => Some(Au((value * AU_PER_IN) as Integer)), + "cm" => Some(Au((value * AU_PER_CM) as Integer)), + "mm" => Some(Au((value * AU_PER_MM) as Integer)), + "pt" => Some(Au((value * AU_PER_PT) as Integer)), + "pc" => Some(Au((value * AU_PER_PC) as Integer)), + "em" => Some(Em(value)), + "ex" => Some(Ex(value)), + _ => None + } + } + #[inline] + pub fn from_px(px_value: Float) -> Length { + Au((px_value * AU_PER_PX) as Integer) + } + } + + pub enum LengthOrPercentage { + LP_Length(Length), + LP_Percentage(Float), + } + impl LengthOrPercentage { + fn parse_internal(input: &ComponentValue, negative_ok: bool) + -> Option { + match input { + &Dimension(ref value, ref unit) if negative_ok || value.value >= 0. + => Length::parse_dimension(value.value, unit.as_slice()).map_move(LP_Length), + &ast::Percentage(ref value) if negative_ok || value.value >= 0. + => Some(LP_Percentage(value.value)), + &Number(ref value) if value.value == 0. => Some(LP_Length(Au(0))), + _ => None + } + } + #[inline] + pub fn parse(input: &ComponentValue) -> Option { + LengthOrPercentage::parse_internal(input, /* negative_ok = */ true) + } + #[inline] + pub fn parse_non_negative(input: &ComponentValue) -> Option { + LengthOrPercentage::parse_internal(input, /* negative_ok = */ false) + } + } + + pub enum LengthOrPercentageOrAuto { + LPA_Length(Length), + LPA_Percentage(Float), + LPA_Auto, + } + impl LengthOrPercentageOrAuto { + fn parse_internal(input: &ComponentValue, negative_ok: bool) + -> Option { + match input { + &Dimension(ref value, ref unit) if negative_ok || value.value >= 0. + => Length::parse_dimension(value.value, unit.as_slice()).map_move(LPA_Length), + &ast::Percentage(ref value) if negative_ok || value.value >= 0. + => Some(LPA_Percentage(value.value)), + &Number(ref value) if value.value == 0. => Some(LPA_Length(Au(0))), + &Ident(ref value) if value.eq_ignore_ascii_case("auto") => Some(LPA_Auto), + _ => None + } + } + #[inline] + pub fn parse(input: &ComponentValue) -> Option { + LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ true) + } + #[inline] + pub fn parse_non_negative(input: &ComponentValue) -> Option { + LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ false) + } + } +} + +pub mod computed { + use cssparser; + pub use CSSColor = cssparser::Color; + pub use compute_CSSColor = std::util::id; + use super::*; + use super::super::longhands::font_weight; + pub struct Context { + current_color: cssparser::RGBA, + has_border_top: bool, + has_border_right: bool, + has_border_bottom: bool, + has_border_left: bool, + font_size: Length, + font_weight: font_weight::ComputedValue, + // TODO, as needed: root font size, viewport size, etc. + } + pub struct Length(Integer); // in application units + impl Length { + pub fn times(self, factor: Float) -> Length { + Length(((*self as Float) * factor) as Integer) + } + } + + pub fn compute_Length(value: specified::Length, context: &Context) -> Length { + match value { + specified::Au(value) => Length(value), + specified::Em(value) => context.font_size.times(value), + specified::Ex(value) => { + let x_height = 0.5; // TODO: find that from the font + context.font_size.times(value * x_height) + }, + } + } + + pub enum LengthOrPercentage { + LP_Length(Length), + LP_Percentage(Float), + } + pub fn compute_LengthOrPercentage(value: specified::LengthOrPercentage, context: &Context) + -> LengthOrPercentage { + match value { + specified::LP_Length(value) => LP_Length(compute_Length(value, context)), + specified::LP_Percentage(value) => LP_Percentage(value), + } + } + + pub enum LengthOrPercentageOrAuto { + LPA_Length(Length), + LPA_Percentage(Float), + LPA_Auto, + } + pub fn compute_LengthOrPercentageOrAuto(value: specified::LengthOrPercentageOrAuto, + context: &Context) -> LengthOrPercentageOrAuto { + match value { + specified::LPA_Length(value) => LPA_Length(compute_Length(value, context)), + specified::LPA_Percentage(value) => LPA_Percentage(value), + specified::LPA_Auto => LPA_Auto, + } + } +} diff --git a/src/components/script/style/properties/mod.rs.mako b/src/components/script/style/properties/mod.rs.mako new file mode 100644 index 00000000000..8b3417263df --- /dev/null +++ b/src/components/script/style/properties/mod.rs.mako @@ -0,0 +1,687 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is a Mako template: http://www.makotemplates.org/ + +use std::ascii::StrAsciiExt; +use errors::{ErrorLoggerIterator, log_css_error}; +pub use std::iterator; +pub use cssparser::*; +pub use parsing_utils::*; +pub use self::common_types::*; + +pub mod common_types; + + +<%! + +def to_rust_ident(name): + name = name.replace("-", "_") + if name in ["static"]: # Rust keywords + name += "_" + return name + +class Longhand(object): + def __init__(self, name): + self.name = name + self.ident = to_rust_ident(name) + + +class Shorthand(object): + def __init__(self, name, sub_properties): + self.name = name + self.ident = to_rust_ident(name) + self.sub_properties = [Longhand(s) for s in sub_properties] + +LONGHANDS = [] +SHORTHANDS = [] +INHERITED = set() + +%> + +pub mod longhands { + pub use super::*; + pub use std; + + <%def name="longhand(name, inherited=False, no_super=False)"> + <% + property = Longhand(name) + LONGHANDS.append(property) + if inherited: + INHERITED.add(name) + %> + pub mod ${property.ident} { + % if not no_super: + use super::*; + % endif + ${caller.body()} + } + + + <%def name="single_component_value(name, inherited=False)"> + <%self:longhand name="${name}" inherited="${inherited}"> + ${caller.body()} + pub fn parse(input: &[ComponentValue]) -> Option { + one_component_value(input).chain(from_component_value) + } + + + + <%def name="single_keyword(name, values, inherited=False)"> + <%self:single_component_value name="${name}" inherited="${inherited}"> + // The computed value is the same as the specified value. + pub use to_computed_value = std::util::id; + pub enum SpecifiedValue { + % for value in values.split(): + ${to_rust_ident(value)}, + % endfor + } + pub type ComputedValue = SpecifiedValue; + #[inline] pub fn get_initial_value() -> ComputedValue { + ${to_rust_ident(values.split()[0])} + } + pub fn from_component_value(v: &ComponentValue) -> Option { + do get_ident_lower(v).chain |keyword| { + match keyword.as_slice() { + % for value in values.split(): + "${value}" => Some(${to_rust_ident(value)}), + % endfor + _ => None, + } + } + } + + + + <%def name="predefined_type(name, type, initial_value, parse_method='parse', inherited=False)"> + <%self:longhand name="${name}" inherited="${inherited}"> + pub use to_computed_value = super::super::common_types::computed::compute_${type}; + pub type SpecifiedValue = specified::${type}; + pub type ComputedValue = computed::${type}; + #[inline] pub fn get_initial_value() -> ComputedValue { ${initial_value} } + pub fn parse(input: &[ComponentValue]) -> Option { + one_component_value(input).chain(specified::${type}::${parse_method}) + } + + + + + // CSS 2.1, Section 8 - Box model + + % for side in ["top", "right", "bottom", "left"]: + ${predefined_type("margin-" + side, "LengthOrPercentageOrAuto", + "computed::LPA_Length(computed::Length(0))")} + % endfor + + % for side in ["top", "right", "bottom", "left"]: + ${predefined_type("padding-" + side, "LengthOrPercentage", + "computed::LP_Length(computed::Length(0))", + "parse_non_negative")} + % endfor + + % for side in ["top", "right", "bottom", "left"]: + ${predefined_type("border-%s-color" % side, "CSSColor", "CurrentColor")} + % endfor + + // dotted dashed double groove ridge insed outset hidden + ${single_keyword("border-top-style", "none solid")} + % for side in ["right", "bottom", "left"]: + <%self:longhand name="border-${side}-style", no_super="True"> + pub use super::border_top_style::*; + pub type SpecifiedValue = super::border_top_style::SpecifiedValue; + pub type ComputedValue = super::border_top_style::ComputedValue; + + % endfor + + pub fn parse_border_width(component_value: &ComponentValue) -> Option { + match component_value { + &Ident(ref value) => match value.to_ascii_lower().as_slice() { + "thin" => Some(specified::Length::from_px(1.)), + "medium" => Some(specified::Length::from_px(3.)), + "thick" => Some(specified::Length::from_px(5.)), + _ => None + }, + _ => specified::Length::parse_non_negative(component_value) + } + } + % for side in ["top", "right", "bottom", "left"]: + <%self:longhand name="border-${side}-width"> + pub type SpecifiedValue = specified::Length; + pub type ComputedValue = computed::Length; + #[inline] pub fn get_initial_value() -> ComputedValue { + computed::Length(3 * 60) // medium + } + pub fn parse(input: &[ComponentValue]) -> Option { + one_component_value(input).chain(parse_border_width) + } + pub fn to_computed_value(value: SpecifiedValue, context: &computed::Context) + -> ComputedValue { + if context.has_border_${side} { computed::compute_Length(value, context) } + else { computed::Length(0) } + } + + % endfor + + // CSS 2.1, Section 9 - Visual formatting model + + // TODO: don't parse values we don't support + ${single_keyword("display", + "inline block list-item inline-block none " + )} +// "table inline-table table-row-group table-header-group table-footer-group " +// "table-row table-column-group table-column table-cell table-caption" + + ${single_keyword("position", "static absolute relative fixed")} + ${single_keyword("float", "none left right")} + ${single_keyword("clear", "none left right both")} + + // CSS 2.1, Section 10 - Visual formatting model details + + ${predefined_type("width", "LengthOrPercentageOrAuto", + "computed::LPA_Auto", + "parse_non_negative")} + ${predefined_type("height", "LengthOrPercentageOrAuto", + "computed::LPA_Auto", + "parse_non_negative")} + + <%self:single_component_value name="line-height"> + pub enum SpecifiedValue { + SpecifiedNormal, + SpecifiedLength(specified::Length), + SpecifiedNumber(Float), + // percentage are the same as em. + } + /// normal | | | + pub fn from_component_value(input: &ComponentValue) -> Option { + match input { + &ast::Number(ref value) if value.value >= 0. + => Some(SpecifiedNumber(value.value)), + &ast::Percentage(ref value) if value.value >= 0. + => Some(SpecifiedLength(specified::Em(value.value))), + &Dimension(ref value, ref unit) if value.value >= 0. + => specified::Length::parse_dimension(value.value, unit.as_slice()) + .map_move(SpecifiedLength), + &Ident(ref value) if value.eq_ignore_ascii_case("normal") + => Some(SpecifiedNormal), + _ => None, + } + } + pub enum ComputedValue { + Normal, + Length(computed::Length), + Number(Float), + } + #[inline] pub fn get_initial_value() -> ComputedValue { Normal } + pub fn to_computed_value(value: SpecifiedValue, context: &computed::Context) + -> ComputedValue { + match value { + SpecifiedNormal => Normal, + SpecifiedLength(value) => Length(computed::compute_Length(value, context)), + SpecifiedNumber(value) => Number(value), + } + } + + + // CSS 2.1, Section 11 - Visual effects + + // CSS 2.1, Section 12 - Generated content, automatic numbering, and lists + + // CSS 2.1, Section 13 - Paged media + + // CSS 2.1, Section 14 - Colors and Backgrounds + + ${predefined_type("background-color", "CSSColor", + "RGBA(RGBA { red: 0., green: 0., blue: 0., alpha: 0. }) /* transparent */")} + ${predefined_type("color", "CSSColor", + "RGBA(RGBA { red: 0., green: 0., blue: 0., alpha: 1. }) /* black */", + inherited=True)} + + // CSS 2.1, Section 15 - Fonts + + <%self:longhand name="font-family" inherited="True"> + pub use to_computed_value = std::util::id; + enum FontFamily { + FamilyName(~str), + // Generic +// Serif, +// SansSerif, +// Cursive, +// Fantasy, +// Monospace, + } + pub type SpecifiedValue = ~[FontFamily]; + pub type ComputedValue = SpecifiedValue; + #[inline] pub fn get_initial_value() -> ComputedValue { ~[FamilyName(~"serif")] } + /// # + /// = | [ + ] + /// TODO: + pub fn parse(input: &[ComponentValue]) -> Option { + from_iter(input.skip_whitespace()) + } + pub fn from_iter<'a>(mut iter: SkipWhitespaceIterator<'a>) -> Option { + let mut result = ~[]; + macro_rules! add( + ($value: expr) => { + { + result.push($value); + match iter.next() { + Some(&Comma) => (), + None => break 'outer, + _ => return None, + } + } + } + ) + 'outer: loop { + match iter.next() { + // TODO: avoid copying strings? + Some(&String(ref value)) => add!(FamilyName(value.to_owned())), + Some(&Ident(ref value)) => { + let value = value.as_slice(); + match value.to_ascii_lower().as_slice() { +// "serif" => add!(Serif), +// "sans-serif" => add!(SansSerif), +// "cursive" => add!(Cursive), +// "fantasy" => add!(Fantasy), +// "monospace" => add!(Monospace), + _ => { + let mut idents = ~[value]; + loop { + match iter.next() { + Some(&Ident(ref value)) => idents.push(value.as_slice()), + Some(&Comma) => { + result.push(FamilyName(idents.connect(" "))); + break + }, + None => { + result.push(FamilyName(idents.connect(" "))); + break 'outer + }, + _ => return None, + } + } + } + } + } + _ => return None, + } + } + Some(result) + } + + + + ${single_keyword("font-style", "normal italic oblique", inherited=True)} + ${single_keyword("font-variant", "normal", inherited=True)} // Add small-caps when supported + + <%self:single_component_value name="font-weight" inherited="True"> + pub enum SpecifiedValue { + Bolder, + Lighther, + % for weight in range(100, 901, 100): + SpecifiedWeight${weight}, + % endfor + } + /// normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 + pub fn from_component_value(input: &ComponentValue) -> Option { + match input { + &Ident(ref value) => match value.to_ascii_lower().as_slice() { + "bold" => Some(SpecifiedWeight700), + "normal" => Some(SpecifiedWeight400), + "bolder" => Some(Bolder), + "lighter" => Some(Lighther), + _ => None, + }, + &Number(ref value) => match value.int_value { + Some(100) => Some(SpecifiedWeight100), + Some(200) => Some(SpecifiedWeight200), + Some(300) => Some(SpecifiedWeight300), + Some(400) => Some(SpecifiedWeight400), + Some(500) => Some(SpecifiedWeight500), + Some(600) => Some(SpecifiedWeight600), + Some(700) => Some(SpecifiedWeight700), + Some(800) => Some(SpecifiedWeight800), + Some(900) => Some(SpecifiedWeight900), + _ => None, + }, + _ => None + } + } + pub enum ComputedValue { + % for weight in range(100, 901, 100): + Weight${weight}, + % endfor + } + #[inline] pub fn get_initial_value() -> ComputedValue { Weight400 } // normal + pub fn to_computed_value(value: SpecifiedValue, context: &computed::Context) + -> ComputedValue { + match value { + % for weight in range(100, 901, 100): + SpecifiedWeight${weight} => Weight${weight}, + % endfor + Bolder => match context.font_weight { + Weight100 => Weight400, + Weight200 => Weight400, + Weight300 => Weight400, + Weight400 => Weight700, + Weight500 => Weight700, + Weight600 => Weight900, + Weight700 => Weight900, + Weight800 => Weight900, + Weight900 => Weight900, + }, + Lighther => match context.font_weight { + Weight100 => Weight100, + Weight200 => Weight100, + Weight300 => Weight100, + Weight400 => Weight100, + Weight500 => Weight100, + Weight600 => Weight400, + Weight700 => Weight400, + Weight800 => Weight700, + Weight900 => Weight700, + }, + } + } + + + <%self:single_component_value name="font-size" inherited="True"> + pub use to_computed_value = super::super::common_types::computed::compute_Length; + pub type SpecifiedValue = specified::Length; // Percentages are the same as em. + pub type ComputedValue = computed::Length; + #[inline] pub fn get_initial_value() -> ComputedValue { + computed::Length(16 * 60) // medium + } + /// | + /// TODO: support and + pub fn from_component_value(input: &ComponentValue) -> Option { + do specified::LengthOrPercentage::parse_non_negative(input).map_move |value| { + match value { + specified::LP_Length(value) => value, + specified::LP_Percentage(value) => specified::Em(value), + } + } + } + + + // CSS 2.1, Section 16 - Text + + // TODO: initial value should be 'start' (CSS Text Level 3, direction-dependent.) + ${single_keyword("text-align", "left right center justify", inherited=True)} + + <%self:longhand name="text-decoration"> + pub use to_computed_value = std::util::id; + pub struct SpecifiedValue { + underline: bool, + overline: bool, + line_through: bool, + // 'blink' is accepted in the parser but ignored. + // Just not blinking the text is a conforming implementation per CSS 2.1. + } + pub type ComputedValue = SpecifiedValue; + #[inline] pub fn get_initial_value() -> ComputedValue { + SpecifiedValue { underline: false, overline: false, line_through: false } // none + } + /// none | [ underline || overline || line-through || blink ] + pub fn parse(input: &[ComponentValue]) -> Option { + let mut result = SpecifiedValue { + underline: false, overline: false, line_through: false, + }; + let mut blink = false; + let mut empty = true; + for component_value in input.skip_whitespace() { + match get_ident_lower(component_value) { + None => return None, + Some(keyword) => match keyword.as_slice() { + "underline" => if result.underline { return None } + else { empty = false; result.underline = true }, + "overline" => if result.overline { return None } + else { empty = false; result.overline = true }, + "line-through" => if result.line_through { return None } + else { empty = false; result.line_through = true }, + "blink" => if blink { return None } + else { empty = false; blink = true }, + "none" => return if empty { Some(result) } else { None }, + _ => return None, + } + } + } + if !empty { Some(result) } else { None } + } + + + // CSS 2.1, Section 17 - Tables + + // CSS 2.1, Section 18 - User interface +} + + +pub mod shorthands { + pub use super::*; + pub use super::longhands::*; + + <%def name="shorthand(name, sub_properties)"> + <% + shorthand = Shorthand(name, sub_properties.split()) + SHORTHANDS.append(shorthand) + %> + pub mod ${shorthand.ident} { + use super::*; + struct Longhands { + % for sub_property in shorthand.sub_properties: + ${sub_property.ident}: Option<${sub_property.ident}::SpecifiedValue>, + % endfor + } + pub fn parse(input: &[ComponentValue]) -> Option { + ${caller.body()} + } + } + + + <%def name="four_sides_shorthand(name, sub_property_pattern, parser_function)"> + <%self:shorthand name="${name}" sub_properties="${ + ' '.join(sub_property_pattern % side + for side in ['top', 'right', 'bottom', 'left'])}"> + let mut iter = input.skip_whitespace().map(${parser_function}); + // zero or more than four values is invalid. + // one value sets them all + // two values set (top, bottom) and (left, right) + // three values set top, (left, right) and bottom + // four values set them in order + let top = iter.next().unwrap_or_default(None); + let right = iter.next().unwrap_or_default(top); + let bottom = iter.next().unwrap_or_default(top); + let left = iter.next().unwrap_or_default(right); + if top.is_some() && right.is_some() && bottom.is_some() && left.is_some() + && iter.next().is_none() { + Some(Longhands { + % for side in ["top", "right", "bottom", "left"]: + ${to_rust_ident(sub_property_pattern % side)}: ${side}, + % endfor + }) + } else { + None + } + + + + + // TODO: other background-* properties + <%self:shorthand name="background" sub_properties="background-color"> + do one_component_value(input).chain(specified::CSSColor::parse).map_move |color| { + Longhands { background_color: Some(color) } + } + + + ${four_sides_shorthand("border-color", "border-%s-color", "specified::CSSColor::parse")} + ${four_sides_shorthand("border-style", "border-%s-style", + "border_top_style::from_component_value")} + ${four_sides_shorthand("border-width", "border-%s-width", "parse_border_width")} + + pub fn parse_border(input: &[ComponentValue]) + -> Option<(Option, + Option, + Option)> { + let mut color = None; + let mut style = None; + let mut width = None; + let mut any = false; + for component_value in input.skip_whitespace() { + if color.is_none() { + match specified::CSSColor::parse(component_value) { + Some(c) => { color = Some(c); any = true; loop }, + None => () + } + } + if style.is_none() { + match border_top_style::from_component_value(component_value) { + Some(s) => { style = Some(s); any = true; loop }, + None => () + } + } + if width.is_none() { + match parse_border_width(component_value) { + Some(w) => { width = Some(w); any = true; loop }, + None => () + } + } + return None + } + if any { Some((color, style, width)) } else { None } + } + + + % for side in ["top", "right", "bottom", "left"]: + <%self:shorthand name="border-${side}" sub_properties="${' '.join( + 'border-%s-%s' % (side, prop) + for prop in ['color', 'style', 'width'] + )}"> + do parse_border(input).map_move |(color, style, width)| { + Longhands { + % for prop in ["color", "style", "width"]: + ${"border_%s_%s: %s," % (side, prop, prop)} + % endfor + } + } + + % endfor + + <%self:shorthand name="border" sub_properties="${' '.join( + 'border-%s-%s' % (side, prop) + for side in ['top', 'right', 'bottom', 'left'] + for prop in ['color', 'style', 'width'] + )}"> + do parse_border(input).map_move |(color, style, width)| { + Longhands { + % for side in ["top", "right", "bottom", "left"]: + % for prop in ["color", "style", "width"]: + ${"border_%s_%s: %s," % (side, prop, prop)} + % endfor + % endfor + } + } + + +} + + +pub struct PropertyDeclarationBlock { + important: ~[PropertyDeclaration], + normal: ~[PropertyDeclaration], +} + + +pub fn parse_property_declaration_list(input: ~[Node]) -> PropertyDeclarationBlock { + let mut important = ~[]; + let mut normal = ~[]; + for item in ErrorLoggerIterator(parse_declaration_list(input.move_iter())) { + match item { + Decl_AtRule(rule) => log_css_error( + rule.location, fmt!("Unsupported at-rule in declaration list: @%s", rule.name)), + Declaration(Declaration{ location: l, name: n, value: v, important: i}) => { + let list = if i { &mut important } else { &mut normal }; + if !PropertyDeclaration::parse(n, v, list) { + log_css_error(l, "Invalid property declaration") + } + } + } + } + PropertyDeclarationBlock { important: important, normal: normal } +} + + +pub enum CSSWideKeyword { + Initial, + Inherit, + Unset, +} + +impl CSSWideKeyword { + pub fn parse(input: &[ComponentValue]) -> Option { + do one_component_value(input).chain(get_ident_lower).chain |keyword| { + match keyword.as_slice() { + "initial" => Some(Initial), + "inherit" => Some(Inherit), + "unset" => Some(Unset), + _ => None + } + } + } +} + +pub enum DeclaredValue { + SpecifiedValue(T), + CSSWideKeyword(CSSWideKeyword), +} + +pub enum PropertyDeclaration { + % for property in LONGHANDS: + ${property.ident}_declaration(DeclaredValue), + % endfor +} + +impl PropertyDeclaration { + pub fn parse(name: &str, value: &[ComponentValue], + result_list: &mut ~[PropertyDeclaration]) -> bool { + match name.to_ascii_lower().as_slice() { + % for property in LONGHANDS: + "${property.name}" => result_list.push(${property.ident}_declaration( + match CSSWideKeyword::parse(value) { + Some(keyword) => CSSWideKeyword(keyword), + None => match longhands::${property.ident}::parse(value) { + Some(value) => SpecifiedValue(value), + None => return false, + } + } + )), + % endfor + % for shorthand in SHORTHANDS: + "${shorthand.name}" => match CSSWideKeyword::parse(value) { + Some(keyword) => { + % for sub_property in shorthand.sub_properties: + result_list.push(${sub_property.ident}_declaration( + CSSWideKeyword(keyword) + )); + % endfor + }, + None => match shorthands::${shorthand.ident}::parse(value) { + Some(result) => { + % for sub_property in shorthand.sub_properties: + result_list.push(${sub_property.ident}_declaration( + match result.${sub_property.ident} { + Some(value) => SpecifiedValue(value), + None => CSSWideKeyword(Initial), + } + )); + % endfor + }, + None => return false, + } + }, + % endfor + _ => return false, // Unknown property + } + true + } +} diff --git a/src/components/script/style/selectors.rs b/src/components/script/style/selectors.rs new file mode 100644 index 00000000000..2cacb177b54 --- /dev/null +++ b/src/components/script/style/selectors.rs @@ -0,0 +1,482 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{vec, iterator}; +use std::ascii::StrAsciiExt; +use cssparser::*; +use namespaces::NamespaceMap; + + +pub struct Selector { + compound_selectors: CompoundSelector, + pseudo_element: Option, + specificity: u32, +} + +pub static STYLE_ATTRIBUTE_SPECIFICITY: u32 = 1 << 31; + + +pub enum PseudoElement { + Before, + After, + FirstLine, + FirstLetter, +} + + +pub struct CompoundSelector { + simple_selectors: ~[SimpleSelector], + next: Option<(~CompoundSelector, Combinator)>, // c.next is left of c +} + +pub enum Combinator { + Child, // > + Descendant, // space + NextSibling, // + + LaterSibling, // ~ +} + +pub enum SimpleSelector { + IDSelector(~str), + ClassSelector(~str), + LocalNameSelector{lowercase_name: ~str, cased_name: ~str}, + NamespaceSelector(~str), + + // Attribute selectors + AttrExists(AttrSelector), // [foo] + AttrEqual(AttrSelector, ~str), // [foo=bar] + AttrIncludes(AttrSelector, ~str), // [foo~=bar] + AttrDashMatch(AttrSelector, ~str), // [foo|=bar] + AttrPrefixMatch(AttrSelector, ~str), // [foo^=bar] + AttrSubstringMatch(AttrSelector, ~str), // [foo*=bar] + AttrSuffixMatch(AttrSelector, ~str), // [foo$=bar] + + // Pseudo-classes + Empty, + Root, + Lang(~str), + NthChild(i32, i32), + Negation(~[SimpleSelector]), + // ... +} + +pub struct AttrSelector { + lowercase_name: ~str, + cased_name: ~str, + namespace: Option<~str>, +} + + +type Iter = iterator::Peekable>; + + +// None means invalid selector +pub fn parse_selector_list(input: ~[ComponentValue], namespaces: &NamespaceMap) + -> Option<~[Selector]> { + let iter = &mut input.move_iter().peekable(); + let first = match parse_selector(iter, namespaces) { + None => return None, + Some(result) => result + }; + let mut results = ~[first]; + + loop { + skip_whitespace(iter); + match iter.peek() { + None => break, // EOF + Some(&Comma) => (), + _ => return None, + } + match parse_selector(iter, namespaces) { + Some(selector) => results.push(selector), + None => return None, + } + } + Some(results) +} + + +// None means invalid selector +fn parse_selector(iter: &mut Iter, namespaces: &NamespaceMap) + -> Option { + let (first, pseudo_element) = match parse_simple_selectors(iter, namespaces) { + None => return None, + Some(result) => result + }; + let mut compound = CompoundSelector{ simple_selectors: first, next: None }; + let mut pseudo_element = pseudo_element; + + while pseudo_element.is_none() { + let any_whitespace = skip_whitespace(iter); + let combinator = match iter.peek() { + None => break, // EOF + Some(&Delim('>')) => { iter.next(); Child }, + Some(&Delim('+')) => { iter.next(); NextSibling }, + Some(&Delim('~')) => { iter.next(); LaterSibling }, + Some(_) => { + if any_whitespace { Descendant } + else { return None } + } + }; + match parse_simple_selectors(iter, namespaces) { + None => return None, + Some((simple_selectors, pseudo)) => { + compound = CompoundSelector { + simple_selectors: simple_selectors, + next: Some((~compound, combinator)) + }; + pseudo_element = pseudo; + } + } + } + let selector = Selector{ + specificity: compute_specificity(&compound, &pseudo_element), + compound_selectors: compound, + pseudo_element: pseudo_element, + }; + Some(selector) +} + + +fn compute_specificity(mut selector: &CompoundSelector, + pseudo_element: &Option) -> u32 { + struct Specificity { + id_selectors: u32, + class_like_selectors: u32, + element_selectors: u32, + } + let mut specificity = Specificity { + id_selectors: 0, + class_like_selectors: 0, + element_selectors: 0, + }; + if pseudo_element.is_some() { specificity.element_selectors += 1 } + + simple_selectors_specificity(selector.simple_selectors, &mut specificity); + loop { + match selector.next { + None => break, + Some((ref next_selector, _)) => { + selector = &**next_selector; + simple_selectors_specificity(selector.simple_selectors, &mut specificity) + } + } + } + + fn simple_selectors_specificity(simple_selectors: &[SimpleSelector], + specificity: &mut Specificity) { + for simple_selector in simple_selectors.iter() { + match simple_selector { + &LocalNameSelector{_} => specificity.element_selectors += 1, + &IDSelector(*) => specificity.id_selectors += 1, + &ClassSelector(*) + | &AttrExists(*) | &AttrEqual(*) | &AttrIncludes(*) | &AttrDashMatch(*) + | &AttrPrefixMatch(*) | &AttrSubstringMatch(*) | &AttrSuffixMatch(*) + | &Empty | &Root | &Lang(*) | &NthChild(*) + => specificity.class_like_selectors += 1, + &NamespaceSelector(*) => (), + &Negation(ref negated) + => simple_selectors_specificity(negated.as_slice(), specificity), + } + } + } + + static MAX_10BIT: u32 = (1u32 << 10) - 1; + specificity.id_selectors.min(&MAX_10BIT) << 20 + | specificity.class_like_selectors.min(&MAX_10BIT) << 10 + | specificity.id_selectors.min(&MAX_10BIT) +} + + +// None means invalid selector +fn parse_simple_selectors(iter: &mut Iter, namespaces: &NamespaceMap) + -> Option<(~[SimpleSelector], Option)> { + let mut empty = true; + let mut simple_selectors = match parse_type_selector(iter, namespaces) { + None => return None, // invalid selector + Some(None) => ~[], + Some(Some(s)) => { empty = false; s } + }; + + let mut pseudo_element = None; + loop { + match parse_one_simple_selector(iter, namespaces, /* inside_negation = */ false) { + None => return None, // invalid selector + Some(None) => break, + Some(Some(Left(s))) => simple_selectors.push(s), + Some(Some(Right(p))) => { pseudo_element = Some(p); break }, + } + } + if empty { None } // An empty selector is invalid + else { Some((simple_selectors, pseudo_element)) } +} + + +// None means invalid selector +// Some(None) means no type selector +// Some(Some([...])) is a type selector. Might be empty for *|* +fn parse_type_selector(iter: &mut Iter, namespaces: &NamespaceMap) + -> Option> { + skip_whitespace(iter); + match parse_qualified_name(iter, /* allow_universal = */ true, namespaces) { + None => None, // invalid selector + Some(None) => Some(None), + Some(Some((namespace, local_name))) => { + let mut simple_selectors = ~[]; + match namespace { + Some(url) => simple_selectors.push(NamespaceSelector(url)), + None => (), + } + match local_name { + Some(name) => simple_selectors.push(LocalNameSelector{ + lowercase_name: name.to_ascii_lower(), + cased_name: name, + }), + None => (), + } + Some(Some(simple_selectors)) + } + } +} + + +// Parse a simple selector other than a type selector +fn parse_one_simple_selector(iter: &mut Iter, namespaces: &NamespaceMap, inside_negation: bool) + -> Option>> { + match iter.peek() { + Some(&IDHash(_)) => match iter.next() { + Some(IDHash(id)) => Some(Some(Left(IDSelector(id)))), + _ => fail!("Implementation error, this should not happen."), + }, + Some(&Delim('.')) => { + iter.next(); + match iter.next() { + Some(Ident(class)) => Some(Some(Left(ClassSelector(class)))), + _ => None, // invalid selector + } + } + Some(&SquareBracketBlock(_)) => match iter.next() { + Some(SquareBracketBlock(content)) + => match parse_attribute_selector(content, namespaces) { + None => None, + Some(simple_selector) => Some(Some(Left(simple_selector))), + }, + _ => fail!("Implementation error, this should not happen."), + }, + Some(&Delim(':')) => { + iter.next(); + match iter.next() { + Some(Ident(name)) => match parse_simple_pseudo_class(name) { + None => None, + Some(result) => Some(Some(result)), + }, + Some(Function(name, arguments)) => match parse_functional_pseudo_class( + name, arguments, namespaces, inside_negation) { + None => None, + Some(simple_selector) => Some(Some(Left(simple_selector))), + }, + Some(Delim(':')) => { + match iter.next() { + Some(Ident(name)) => match parse_pseudo_element(name) { + Some(pseudo_element) => Some(Some(Right(pseudo_element))), + _ => None, + }, + _ => None, + } + } + _ => None, + } + } + _ => Some(None), + } +} + +// None means invalid selector +// Some(None) means not a qualified name +// Some(Some((None, None)) means *|* +// Some(Some((Some(url), None)) means prefix|* +// Some(Some((None, Some(name)) means *|name +// Some(Some((Some(url), Some(name))) means prefix|name +// ... or equivalent +fn parse_qualified_name(iter: &mut Iter, allow_universal: bool, namespaces: &NamespaceMap) + -> Option, Option<~str>)>> { + #[inline] + fn default_namespace(namespaces: &NamespaceMap, local_name: Option<~str>) + -> Option, Option<~str>)>> { + match namespaces.default { + None => Some(Some((None, local_name))), + Some(ref url) => Some(Some((Some(url.to_owned()), local_name))), + } + } + + #[inline] + fn explicit_namespace(iter: &mut Iter, allow_universal: bool, namespace_url: Option<~str>) + -> Option, Option<~str>)>> { + assert!(iter.next() == Some(Delim('|'))); + match iter.peek() { + Some(&Delim('*')) if allow_universal => { + iter.next(); + Some(Some((namespace_url, None))) + }, + Some(&Ident(_)) => { + let local_name = get_next_ident(iter); + Some(Some((namespace_url, Some(local_name)))) + }, + _ => None, // invalid selector + } + } + + match iter.peek() { + Some(&Ident(_)) => { + let value = get_next_ident(iter); + match iter.peek() { + Some(&Delim('|')) => default_namespace(namespaces, Some(value)), + _ => { + let namespace_url = match namespaces.prefix_map.find(&value) { + None => return None, // Undeclared namespace prefix: invalid selector + Some(ref url) => url.to_owned(), + }; + explicit_namespace(iter, allow_universal, Some(namespace_url)) + }, + } + }, + Some(&Delim('*')) => { + iter.next(); // Consume '*' + match iter.peek() { + Some(&Delim('|')) => { + if allow_universal { default_namespace(namespaces, None) } + else { None } + }, + _ => explicit_namespace(iter, allow_universal, None), + } + }, + Some(&Delim('|')) => explicit_namespace(iter, allow_universal, Some(~"")), + _ => return None, + } +} + + +fn parse_attribute_selector(content: ~[ComponentValue], namespaces: &NamespaceMap) + -> Option { + let iter = &mut content.move_iter().peekable(); + let attr = match parse_qualified_name(iter, /* allow_universal = */ false, namespaces) { + None => return None, // invalid selector + Some(None) => return None, + Some(Some((_, None))) => fail!("Implementation error, this should not happen."), + Some(Some((namespace, Some(local_name)))) => AttrSelector { + namespace: namespace, + lowercase_name: local_name.to_ascii_lower(), + cased_name: local_name, + }, + }; + skip_whitespace(iter); + macro_rules! get_value( () => {{ + skip_whitespace(iter); + match iter.next() { + Some(Ident(value)) | Some(String(value)) => value, + _ => return None, + } + }};) + let result = match iter.next() { + None => AttrExists(attr), // [foo] + Some(Delim('=')) => AttrEqual(attr, get_value!()), // [foo=bar] + Some(IncludeMatch) => AttrIncludes(attr, get_value!()), // [foo~=bar] + Some(DashMatch) => AttrDashMatch(attr, get_value!()), // [foo|=bar] + Some(PrefixMatch) => AttrPrefixMatch(attr, get_value!()), // [foo^=bar] + Some(SubstringMatch) => AttrSubstringMatch(attr, get_value!()), // [foo*=bar] + Some(SuffixMatch) => AttrSuffixMatch(attr, get_value!()), // [foo$=bar] + _ => return None + }; + skip_whitespace(iter); + if iter.next().is_none() { Some(result) } else { None } +} + + +fn parse_simple_pseudo_class(name: ~str) -> Option> { + match name.to_ascii_lower().as_slice() { + "root" => Some(Left(Root)), + "empty" => Some(Left(Empty)), + + // Supported CSS 2.1 pseudo-elements only. + "before" => Some(Right(Before)), + "after" => Some(Right(After)), + "first-line" => Some(Right(FirstLine)), + "first-letter" => Some(Right(FirstLetter)), + _ => None + } +} + + +fn parse_functional_pseudo_class(name: ~str, arguments: ~[ComponentValue], + namespaces: &NamespaceMap, inside_negation: bool) + -> Option { + match name.to_ascii_lower().as_slice() { + "lang" => parse_lang(arguments), + "nth-child" => parse_nth(arguments).map(|&(a, b)| NthChild(a, b)), + "not" => if inside_negation { None } else { parse_negation(arguments, namespaces) }, + _ => None + } +} + + +fn parse_pseudo_element(name: ~str) -> Option { + match name.to_ascii_lower().as_slice() { + // All supported pseudo-elements + "before" => Some(Before), + "after" => Some(After), + "first-line" => Some(FirstLine), + "first-letter" => Some(FirstLetter), + _ => None + } +} + + +fn parse_lang(arguments: ~[ComponentValue]) -> Option { + let mut iter = arguments.move_skip_whitespace(); + match iter.next() { + Some(Ident(value)) => { + if "" == value || iter.next().is_some() { None } + else { Some(Lang(value)) } + }, + _ => None, + } +} + + +// Level 3: Parse ONE simple_selector +fn parse_negation(arguments: ~[ComponentValue], namespaces: &NamespaceMap) + -> Option { + let iter = &mut arguments.move_iter().peekable(); + Some(Negation(match parse_type_selector(iter, namespaces) { + None => return None, // invalid selector + Some(Some(s)) => s, + Some(None) => { + match parse_one_simple_selector(iter, namespaces, /* inside_negation = */ true) { + Some(Some(Left(s))) => ~[s], + _ => return None + } + }, + })) +} + + +/// Assuming the next token is an ident, consume it and return its value +#[inline] +fn get_next_ident(iter: &mut Iter) -> ~str { + match iter.next() { + Some(Ident(value)) => value, + _ => fail!("Implementation error, this should not happen."), + } +} + + +#[inline] +fn skip_whitespace(iter: &mut Iter) -> bool { + let mut any_whitespace = false; + loop { + if iter.peek() != Some(&WhiteSpace) { return any_whitespace } + any_whitespace = true; + iter.next(); + } +} diff --git a/src/components/script/style/servo-style.rc b/src/components/script/style/servo-style.rc new file mode 100644 index 00000000000..c4b891dd2d7 --- /dev/null +++ b/src/components/script/style/servo-style.rc @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[link(name = "servo-style", vers = "0.1")]; +#[crate_type = "lib"]; + +extern mod extra; +extern mod cssparser; + +pub mod stylesheets; +pub mod errors; +pub mod selectors; +pub mod properties; +pub mod namespaces; +pub mod media_queries; +pub mod parsing_utils; diff --git a/src/components/script/style/stylesheets.rs b/src/components/script/style/stylesheets.rs new file mode 100644 index 00000000000..5d9d5a2a9fa --- /dev/null +++ b/src/components/script/style/stylesheets.rs @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::iterator::Iterator; +use std::ascii::StrAsciiExt; +use cssparser::*; +use selectors; +use properties; +use errors::{ErrorLoggerIterator, log_css_error}; +use namespaces::{NamespaceMap, parse_namespace_rule}; +use media_queries::{MediaRule, parse_media_rule}; +use media_queries; + + +pub struct Stylesheet { + rules: ~[CSSRule], + namespaces: NamespaceMap, +} + + +pub enum CSSRule { + CSSStyleRule(StyleRule), + CSSMediaRule(MediaRule), +} + + +pub struct StyleRule { + selectors: ~[selectors::Selector], + declarations: properties::PropertyDeclarationBlock, +} + + +fn parse_stylesheet(css: &str) -> Stylesheet { + static STATE_CHARSET: uint = 1; + static STATE_IMPORTS: uint = 2; + static STATE_NAMESPACES: uint = 3; + static STATE_BODY: uint = 4; + let mut state: uint = STATE_CHARSET; + + let mut rules = ~[]; + let mut namespaces = NamespaceMap::new(); + + for rule in ErrorLoggerIterator(parse_stylesheet_rules(tokenize(css))) { + let next_state; // Unitialized to force each branch to set it. + match rule { + QualifiedRule(rule) => { + next_state = STATE_BODY; + parse_style_rule(rule, &mut rules, &namespaces) + }, + AtRule(rule) => { + let lower_name = rule.name.to_ascii_lower(); + match lower_name.as_slice() { + "charset" => { + if state > STATE_CHARSET { + log_css_error(rule.location, "@charset must be the first rule") + } + // Valid @charset rules are just ignored + next_state = STATE_IMPORTS; + }, + "import" => { + if state > STATE_IMPORTS { + next_state = state; + log_css_error(rule.location, + "@import must be before any rule but @charset") + } else { + next_state = STATE_IMPORTS; + log_css_error(rule.location, "@import is not supported yet") // TODO + } + }, + "namespace" => { + if state > STATE_NAMESPACES { + next_state = state; + log_css_error( + rule.location, + "@namespace must be before any rule but @charset and @import" + ) + } else { + next_state = STATE_NAMESPACES; + parse_namespace_rule(rule, &mut namespaces) + } + }, + _ => { + next_state = STATE_BODY; + parse_nested_at_rule(lower_name, rule, &mut rules, &namespaces) + }, + } + }, + } + state = next_state; + } + Stylesheet{ rules: rules, namespaces: namespaces } +} + + +pub fn parse_style_rule(rule: QualifiedRule, parent_rules: &mut ~[CSSRule], + namespaces: &NamespaceMap) { + let QualifiedRule{location: location, prelude: prelude, block: block} = rule; + match selectors::parse_selector_list(prelude, namespaces) { + Some(selectors) => parent_rules.push(CSSStyleRule(StyleRule{ + selectors: selectors, + declarations: properties::parse_property_declaration_list(block) + })), + None => log_css_error(location, "Unsupported CSS selector."), + } +} + + +// lower_name is passed explicitly to avoid computing it twice. +pub fn parse_nested_at_rule(lower_name: &str, rule: AtRule, + parent_rules: &mut ~[CSSRule], namespaces: &NamespaceMap) { + match lower_name { + "media" => parse_media_rule(rule, parent_rules, namespaces), + _ => log_css_error(rule.location, fmt!("Unsupported at-rule: @%s", lower_name)) + } +} + + +impl Stylesheet { + fn iter_style_rules<'a>(&'a self, device: &'a media_queries::Device) -> StyleRuleIterator<'a> { + StyleRuleIterator { device: device, stack: ~[(self.rules.as_slice(), 0)] } + } +} + +struct StyleRuleIterator<'self> { + device: &'self media_queries::Device, + // FIXME: I couldn’t get this to borrow-check with a stack of VecIterator + stack: ~[(&'self [CSSRule], uint)], +} + +impl<'self> Iterator<&'self StyleRule> for StyleRuleIterator<'self> { + fn next(&mut self) -> Option<&'self StyleRule> { + loop { + match self.stack.pop_opt() { + None => return None, + Some((rule_list, i)) => { + if i + 1 < rule_list.len() { + self.stack.push((rule_list, i + 1)) + } + match rule_list[i] { + CSSStyleRule(ref rule) => return Some(rule), + CSSMediaRule(ref rule) => { + if rule.media_queries.evaluate(self.device) { + self.stack.push((rule.rules.as_slice(), 0)) + } + } + } + } + } + } + } +}