Skip to content
Snippets Groups Projects
Commit 6fe94c07 authored by Tiaan Louw's avatar Tiaan Louw
Browse files

Bug 1813481 - Use abstract color parser r=emilio,supply-chain-reviewers

In stead of having the css parser construct a color in it's own format
and then converting it to what Gecko needs to perform operations, we now
construct a Gecko friendly color type directly.

Differential Revision: https://phabricator.services.mozilla.com/D170187
parent f7fe2aaf
No related branches found
No related tags found
No related merge requests found
......@@ -122,7 +122,7 @@ replace-with = "vendored-sources"
[source."https://github.com/servo/rust-cssparser"]
git = "https://github.com/servo/rust-cssparser"
rev = "d3670a89bae26ba3a8db4758eb7976616113987d"
rev = "b196a164dcbb317016d4aa6c58c13147e6045ebb"
replace-with = "vendored-sources"
......
......@@ -1104,7 +1104,7 @@ dependencies = [
[[package]]
name = "cssparser"
version = "0.30.0"
source = "git+https://github.com/servo/rust-cssparser?rev=d3670a89bae26ba3a8db4758eb7976616113987d#d3670a89bae26ba3a8db4758eb7976616113987d"
source = "git+https://github.com/servo/rust-cssparser?rev=b196a164dcbb317016d4aa6c58c13147e6045ebb#b196a164dcbb317016d4aa6c58c13147e6045ebb"
dependencies = [
"cssparser-macros",
"dtoa-short",
......@@ -1119,7 +1119,7 @@ dependencies = [
[[package]]
name = "cssparser-macros"
version = "0.6.0"
source = "git+https://github.com/servo/rust-cssparser?rev=d3670a89bae26ba3a8db4758eb7976616113987d#d3670a89bae26ba3a8db4758eb7976616113987d"
source = "git+https://github.com/servo/rust-cssparser?rev=b196a164dcbb317016d4aa6c58c13147e6045ebb#b196a164dcbb317016d4aa6c58c13147e6045ebb"
dependencies = [
"quote",
"syn",
......
......@@ -167,7 +167,7 @@ minidump-writer = { git = "https://github.com/rust-minidump/minidump-writer.git"
# warp 0.3.3 + https://github.com/seanmonstar/warp/pull/1007
warp = { git = "https://github.com/glandium/warp", rev = "4af45fae95bc98b0eba1ef0db17e1dac471bb23d" }
cssparser = { git = "https://github.com/servo/rust-cssparser", rev = "d3670a89bae26ba3a8db4758eb7976616113987d" }
cssparser = { git = "https://github.com/servo/rust-cssparser", rev = "b196a164dcbb317016d4aa6c58c13147e6045ebb" }
# application-services overrides to make updating them all simpler.
interrupt-support = { git = "https://github.com/bendk/application-services", rev = "ecb35df5fc40357c49922f90e86bf4147fa52953" }
......
......@@ -274,52 +274,6 @@ impl From<cssparser::PredefinedColorSpace> for ColorSpace {
}
}
impl From<cssparser::AbsoluteColor> for AbsoluteColor {
fn from(f: cssparser::AbsoluteColor) -> Self {
match f {
cssparser::AbsoluteColor::Rgba(rgba) => Self::from_rgba(rgba),
cssparser::AbsoluteColor::Lab(lab) => Self::new(
ColorSpace::Lab,
ColorComponents(lab.lightness, lab.a, lab.b),
lab.alpha,
),
cssparser::AbsoluteColor::Lch(lch) => Self::new(
ColorSpace::Lch,
ColorComponents(lch.lightness, lch.chroma, lch.hue),
lch.alpha,
),
cssparser::AbsoluteColor::Oklab(oklab) => Self::new(
ColorSpace::Oklab,
ColorComponents(oklab.lightness, oklab.a, oklab.b),
oklab.alpha,
),
cssparser::AbsoluteColor::Oklch(oklch) => Self::new(
ColorSpace::Oklch,
ColorComponents(oklch.lightness, oklch.chroma, oklch.hue),
oklch.alpha,
),
cssparser::AbsoluteColor::ColorFunction(c) => {
let mut result = AbsoluteColor::new(
c.color_space.into(),
ColorComponents(c.c1, c.c2, c.c3),
c.alpha,
);
if matches!(c.color_space, cssparser::PredefinedColorSpace::Srgb) {
result.flags |= SerializationFlags::AS_COLOR_FUNCTION;
}
result
},
}
}
}
impl ToCss for AbsoluteColor {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
......@@ -394,7 +348,7 @@ impl ToCss for AbsoluteColor {
c3: self.components.2,
alpha: self.alpha,
};
let color = cssparser::AbsoluteColor::ColorFunction(color_function);
let color = cssparser::Color::ColorFunction(color_function);
cssparser::ToCss::to_css(&color, dest)
},
}
......
......@@ -6,7 +6,7 @@
use super::AllowQuirks;
use crate::color::mix::ColorInterpolationMethod;
use crate::color::{AbsoluteColor, ColorComponents, ColorSpace};
use crate::color::{AbsoluteColor, ColorComponents, ColorSpace, SerializationFlags};
use crate::media_queries::Device;
use crate::parser::{Parse, ParserContext};
use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue};
......@@ -417,8 +417,65 @@ impl From<RGBA> for Color {
}
}
struct ColorComponentParser<'a, 'b: 'a>(&'a ParserContext<'b>);
impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponentParser<'a, 'b> {
#[inline]
fn new_absolute(color_space: ColorSpace, c1: f32, c2: f32, c3: f32, alpha: f32) -> Color {
Color::Absolute(Box::new(Absolute {
color: AbsoluteColor::new(color_space, ColorComponents(c1, c2, c3), alpha),
authored: None,
}))
}
impl cssparser::FromParsedColor for Color {
fn from_current_color() -> Self {
Color::CurrentColor
}
fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
new_absolute(
ColorSpace::Srgb,
red as f32 / 255.0,
green as f32 / 255.0,
blue as f32 / 255.0,
alpha,
)
}
fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
new_absolute(ColorSpace::Lab, lightness, a, b, alpha)
}
fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
new_absolute(ColorSpace::Lch, lightness, chroma, hue, alpha)
}
fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
new_absolute(ColorSpace::Oklab, lightness, a, b, alpha)
}
fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
new_absolute(ColorSpace::Oklch, lightness, chroma, hue, alpha)
}
fn from_color_function(
color_space: cssparser::PredefinedColorSpace,
c1: f32,
c2: f32,
c3: f32,
alpha: f32,
) -> Self {
let mut result = new_absolute(color_space.into(), c1, c2, c3, alpha);
if let Color::Absolute(ref mut absolute) = result {
if matches!(absolute.color.color_space, ColorSpace::Srgb) {
absolute.color.flags |= SerializationFlags::AS_COLOR_FUNCTION;
}
}
result
}
}
struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>);
impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorParser<'i> for ColorParser<'a, 'b> {
type Output = Color;
type Error = StyleParseErrorKind<'i>;
fn parse_angle_or_number<'t>(
......@@ -516,23 +573,30 @@ impl Color {
},
};
let compontent_parser = ColorComponentParser(&*context);
match input.try_parse(|i| CSSParserColor::parse_with(&compontent_parser, i)) {
Ok(value) => Ok(match value {
CSSParserColor::CurrentColor => Color::CurrentColor,
CSSParserColor::Absolute(absolute) => {
let enabled = matches!(absolute, cssparser::AbsoluteColor::Rgba(_)) ||
static_prefs::pref!("layout.css.more_color_4.enabled");
let color_parser = ColorParser(&*context);
match input.try_parse(|i| cssparser::parse_color_with(&color_parser, i)) {
Ok(mut color) => {
if let Color::Absolute(ref mut absolute) = color {
let enabled = {
let is_srgb = matches!(absolute.color.color_space, ColorSpace::Srgb);
let is_color_function = absolute
.color
.flags
.contains(SerializationFlags::AS_COLOR_FUNCTION);
let pref_enabled = static_prefs::pref!("layout.css.more_color_4.enabled");
(is_srgb && !is_color_function) || pref_enabled
};
if !enabled {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Color::Absolute(Box::new(Absolute {
color: absolute.into(),
authored: authored.map(|s| s.to_ascii_lowercase().into_boxed_str()),
}))
},
}),
// Because we can't set the `authored` value at construction time, we have to set it
// here.
absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str());
}
Ok(color)
},
Err(e) => {
#[cfg(feature = "gecko")]
{
......
......@@ -428,6 +428,11 @@ criteria = "safe-to-deploy"
delta = "0.6.0 -> 0.6.0@git:722b30d2f1634714befab967ecae627813fa4cf0"
notes = "We are pulling this package from a non crates.io source until the changes are published. No changes were made to the code."
[[audits.cssparser-macros]]
who = "Tiaan Louw <tlouw@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.6.0 -> 0.6.0@git:b196a164dcbb317016d4aa6c58c13147e6045ebb"
[[audits.cssparser-macros]]
who = "Tiaan Louw <tlouw@mozilla.com>"
criteria = "safe-to-deploy"
......
{"files":{".github/workflows/main.yml":"d66f2aac0764ebb09540737931fe2b9311e7033a2bf9a116c072cae6bec5e187","Cargo.toml":"03677b7dd7609f355cdeff66b2034647e2e553b282aa9fe7d0aca93a3ab04299","LICENSE":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85","README.md":"53a6805edd80f642473514cb93f1f4197e17a911d66a2dfcefc3dc5e82bac206","build.rs":"b30f35bfbd713943822a19ce6ebe5c99017f603cb001ed37354020549aec71fc","build/match_byte.rs":"f57faf0597cb7b3e32999c5fb1215a43a5603121588c67d5031f720362171e1c","docs/.nojekyll":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","docs/404.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","docs/index.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","src/color.rs":"c23e56c9b7ba6e229b8f56015acd0b01919779ace6c2b17db22da9c651b0c584","src/cow_rc_str.rs":"89b5dff5cf80eef3fcff0c11799e54a978d02d8b8963a621fbb999d35e7c03a3","src/from_bytes.rs":"b1cf15c4e975523fef46b575598737a39f3c63e5ce0b2bfd6ec627c69c6ea54a","src/lib.rs":"67646b82ac4e673880801fbed6f7517cffbe901675c12f8484cbc8212b873adc","src/macros.rs":"0d4c3d27a22677d9eb3616d7f7af604dc3de2932ca04fd1c036102884cd6f079","src/nth.rs":"2fc26915f0a36cb22ac45dd9a7ecbdc64c327b2ec135370258ec3db9f9985460","src/parser.rs":"f9985187ede4361a29b3bf22d248903343d58e5cf369a9b5e046961356a4faf9","src/rules_and_declarations.rs":"d826f82f8c179fc13756b92336556e3ee40a273314ef774f95af71e687745f2a","src/serializer.rs":"3a0155521676deea9a6327c2ed00af6d5dabb29a97e2341d0f565f8c2b66d0a3","src/size_of_tests.rs":"da0cbcaa304f7800e9122e2bce0a11d42a70b9012e646a723cb23ee74a6b858c","src/tests.rs":"5cd338d9e9d77342010bc7f42350c0cf0b505e4bf499fcbd142a2480e717a080","src/tokenizer.rs":"71600903284f1d68a7da6b69c938b31f9d641f8d981c7adfd06a3c8b783541f2","src/unicode_range.rs":"20d96f06fbb73921e308cc340c9fe065e27f19843005689fb259007a6a372bcc"},"package":null}
\ No newline at end of file
{"files":{".github/workflows/main.yml":"d66f2aac0764ebb09540737931fe2b9311e7033a2bf9a116c072cae6bec5e187","Cargo.toml":"03677b7dd7609f355cdeff66b2034647e2e553b282aa9fe7d0aca93a3ab04299","LICENSE":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85","README.md":"53a6805edd80f642473514cb93f1f4197e17a911d66a2dfcefc3dc5e82bac206","build.rs":"b30f35bfbd713943822a19ce6ebe5c99017f603cb001ed37354020549aec71fc","build/match_byte.rs":"f57faf0597cb7b3e32999c5fb1215a43a5603121588c67d5031f720362171e1c","docs/.nojekyll":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","docs/404.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","docs/index.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","src/color.rs":"541087948795b2e6e605fb9947db94956c1d0d6956444c6ff4860a430bb421ca","src/cow_rc_str.rs":"89b5dff5cf80eef3fcff0c11799e54a978d02d8b8963a621fbb999d35e7c03a3","src/from_bytes.rs":"b1cf15c4e975523fef46b575598737a39f3c63e5ce0b2bfd6ec627c69c6ea54a","src/lib.rs":"a708572027ee6a21795c2bc2652ecda2871e620b69241120491a6117a8a1eec6","src/macros.rs":"0d4c3d27a22677d9eb3616d7f7af604dc3de2932ca04fd1c036102884cd6f079","src/nth.rs":"2fc26915f0a36cb22ac45dd9a7ecbdc64c327b2ec135370258ec3db9f9985460","src/parser.rs":"f9985187ede4361a29b3bf22d248903343d58e5cf369a9b5e046961356a4faf9","src/rules_and_declarations.rs":"d826f82f8c179fc13756b92336556e3ee40a273314ef774f95af71e687745f2a","src/serializer.rs":"3a0155521676deea9a6327c2ed00af6d5dabb29a97e2341d0f565f8c2b66d0a3","src/size_of_tests.rs":"da0cbcaa304f7800e9122e2bce0a11d42a70b9012e646a723cb23ee74a6b858c","src/tests.rs":"20e2369301229c541955eb7417196419c5f676e1c0dd141b191ba7ddbb51cae3","src/tokenizer.rs":"71600903284f1d68a7da6b69c938b31f9d641f8d981c7adfd06a3c8b783541f2","src/unicode_range.rs":"20d96f06fbb73921e308cc340c9fe065e27f19843005689fb259007a6a372bcc"},"package":null}
\ No newline at end of file
This diff is collapsed.
......@@ -68,8 +68,8 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser)
#![recursion_limit = "200"] // For color::parse_color_keyword
pub use crate::color::{
hsl_to_rgb, hwb_to_rgb, parse_color_keyword, AbsoluteColor, AngleOrNumber, Color,
ColorComponentParser, ColorFunction, Lab, Lch, NumberOrPercentage, Oklab, Oklch,
hsl_to_rgb, hwb_to_rgb, parse_color_keyword, parse_color_with, AngleOrNumber, Color,
ColorFunction, ColorParser, FromParsedColor, Lab, Lch, NumberOrPercentage, Oklab, Oklch,
PredefinedColorSpace, RGBA,
};
pub use crate::cow_rc_str::CowRcStr;
......
......@@ -8,16 +8,18 @@ extern crate test;
use encoding_rs;
use serde_json::{self, json, Map, Value};
use crate::color::{parse_color_with, FromParsedColor};
use crate::{ColorParser, PredefinedColorSpace};
#[cfg(feature = "bench")]
use self::test::Bencher;
use super::{
color::{rgb, rgba},
parse_important, parse_nth, parse_one_declaration, parse_one_rule, stylesheet_encoding,
AbsoluteColor, AtRuleParser, BasicParseError, BasicParseErrorKind, Color, CowRcStr,
DeclarationListParser, DeclarationParser, Delimiter, EncodingSupport, ParseError,
ParseErrorKind, Parser, ParserInput, ParserState, QualifiedRuleParser, RuleListParser,
SourceLocation, ToCss, Token, TokenSerializationType, UnicodeRange, RGBA,
AtRuleParser, BasicParseError, BasicParseErrorKind, Color, CowRcStr, DeclarationListParser,
DeclarationParser, Delimiter, EncodingSupport, ParseError, ParseErrorKind, Parser, ParserInput,
ParserState, QualifiedRuleParser, RuleListParser, SourceLocation, ToCss, Token,
TokenSerializationType, UnicodeRange, RGBA,
};
macro_rules! JArray {
......@@ -590,19 +592,19 @@ fn serialize_current_color() {
#[test]
fn serialize_rgb_full_alpha() {
let c = rgb(255, 230, 204);
let c = Color::Rgba(RGBA::new(255, 230, 204, 1.0));
assert_eq!(c.to_css_string(), "rgb(255, 230, 204)");
}
#[test]
fn serialize_rgba() {
let c = rgba(26, 51, 77, 0.125);
let c = Color::Rgba(RGBA::new(26, 51, 77, 0.125));
assert_eq!(c.to_css_string(), "rgba(26, 51, 77, 0.125)");
}
#[test]
fn serialize_rgba_two_digit_float_if_roundtrips() {
let c = Color::Absolute(AbsoluteColor::Rgba(RGBA::from_floats(0., 0., 0., 0.5)));
let c = Color::Rgba(RGBA::from_floats(0., 0., 0., 0.5));
assert_eq!(c.to_css_string(), "rgba(0, 0, 0, 0.5)");
}
......@@ -895,18 +897,16 @@ impl ToJson for Color {
fn to_json(&self) -> Value {
match *self {
Color::CurrentColor => "currentcolor".to_json(),
Color::Absolute(absolute) => match absolute {
AbsoluteColor::Rgba(ref rgba) => {
json!([rgba.red, rgba.green, rgba.blue, rgba.alpha])
}
AbsoluteColor::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
AbsoluteColor::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]),
AbsoluteColor::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
AbsoluteColor::Oklch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]),
AbsoluteColor::ColorFunction(ref c) => {
json!([c.color_space.as_str(), c.c1, c.c2, c.c3, c.alpha])
}
},
Color::Rgba(ref rgba) => {
json!([rgba.red, rgba.green, rgba.blue, rgba.alpha])
}
Color::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
Color::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]),
Color::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
Color::Oklch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]),
Color::ColorFunction(ref c) => {
json!([c.color_space.as_str(), c.c1, c.c2, c.c3, c.alpha])
}
}
}
}
......@@ -1510,3 +1510,121 @@ fn servo_define_css_keyword_enum() {
assert_eq!(UserZoom::from_ident("fixed"), Ok(UserZoom::Fixed));
}
#[test]
fn generic_parser() {
#[derive(Debug, PartialEq)]
enum OutputType {
CurrentColor,
Rgba(u8, u8, u8, f32),
Lab(f32, f32, f32, f32),
Lch(f32, f32, f32, f32),
Oklab(f32, f32, f32, f32),
Oklch(f32, f32, f32, f32),
ColorFunction(PredefinedColorSpace, f32, f32, f32, f32),
}
impl FromParsedColor for OutputType {
fn from_current_color() -> Self {
OutputType::CurrentColor
}
fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
OutputType::Rgba(red, green, blue, alpha)
}
fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
OutputType::Lab(lightness, a, b, alpha)
}
fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
OutputType::Lch(lightness, chroma, hue, alpha)
}
fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
OutputType::Oklab(lightness, a, b, alpha)
}
fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
OutputType::Oklch(lightness, chroma, hue, alpha)
}
fn from_color_function(
color_space: PredefinedColorSpace,
c1: f32,
c2: f32,
c3: f32,
alpha: f32,
) -> Self {
OutputType::ColorFunction(color_space, c1, c2, c3, alpha)
}
}
struct ComponentParser;
impl<'i> ColorParser<'i> for ComponentParser {
type Output = OutputType;
type Error = ();
}
const TESTS: &[(&str, OutputType)] = &[
("currentColor", OutputType::CurrentColor),
("rgb(1, 2, 3)", OutputType::Rgba(1, 2, 3, 1.0)),
("rgba(1, 2, 3, 0.4)", OutputType::Rgba(1, 2, 3, 0.4)),
(
"lab(100 20 30 / 0.4)",
OutputType::Lab(100.0, 20.0, 30.0, 0.4),
),
(
"lch(100 20 30 / 0.4)",
OutputType::Lch(100.0, 20.0, 30.0, 0.4),
),
(
"oklab(100 20 30 / 0.4)",
OutputType::Oklab(100.0, 20.0, 30.0, 0.4),
),
(
"oklch(100 20 30 / 0.4)",
OutputType::Oklch(100.0, 20.0, 30.0, 0.4),
),
(
"color(srgb 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::Srgb, 0.1, 0.2, 0.3, 0.4),
),
(
"color(srgb-linear 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::SrgbLinear, 0.1, 0.2, 0.3, 0.4),
),
(
"color(display-p3 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::DisplayP3, 0.1, 0.2, 0.3, 0.4),
),
(
"color(a98-rgb 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::A98Rgb, 0.1, 0.2, 0.3, 0.4),
),
(
"color(prophoto-rgb 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::ProphotoRgb, 0.1, 0.2, 0.3, 0.4),
),
(
"color(rec2020 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::Rec2020, 0.1, 0.2, 0.3, 0.4),
),
(
"color(xyz-d50 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::XyzD50, 0.1, 0.2, 0.3, 0.4),
),
(
"color(xyz-d65 0.1 0.2 0.3 / 0.4)",
OutputType::ColorFunction(PredefinedColorSpace::XyzD65, 0.1, 0.2, 0.3, 0.4),
),
];
for (input, expected) in TESTS {
let mut input = ParserInput::new(*input);
let mut input = Parser::new(&mut input);
let actual: OutputType = parse_color_with(&ComponentParser, &mut input).unwrap();
assert_eq!(actual, *expected);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment