diff --git a/Cargo.lock b/Cargo.lock index 9149cf9f6..47b86a635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,12 @@ dependencies = [ "simd-abstraction", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "better_scoped_tls" version = "1.0.0" @@ -279,6 +285,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bstr" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +dependencies = [ + "memchr", + "regex-automata 0.4.6", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -443,6 +460,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + [[package]] name = "const-str" version = "0.3.2" @@ -757,6 +786,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "equivalent" version = "1.0.1" @@ -1046,6 +1081,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "langtag" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1978,6 +2019,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2037,6 +2089,26 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe85670573cd6f0fa97940f26e7e6601213c3b0555246c24234131f88c5709e" +dependencies = [ + "console", + "similar", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2798,6 +2870,7 @@ checksum = "642c58202491c273ea984e0d7e923319afe0f94195d2985b3e7f71f7d8232e06" dependencies = [ "new_debug_unreachable", "num-bigint", + "serde", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -2868,6 +2941,36 @@ dependencies = [ "swc_common", ] +[[package]] +name = "swc_formatjs_transform" +version = "1.0.0" +dependencies = [ + "base64ct", + "once_cell", + "pretty_assertions", + "regex", + "serde", + "serde_json", + "sha2 0.10.8", + "swc_core", + "swc_icu_messageformat_parser", +] + +[[package]] +name = "swc_icu_messageformat_parser" +version = "1.0.0" +dependencies = [ + "langtag", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_repr", + "similar-asserts", + "testing", + "widestring", +] + [[package]] name = "swc_macros_common" version = "1.0.0" @@ -2921,6 +3024,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "swc_plugin_formatjs" +version = "1.0.0" +dependencies = [ + "serde", + "serde_json", + "swc_core", + "swc_formatjs_transform", +] + [[package]] name = "swc_plugin_jest" version = "0.31.4" @@ -3634,6 +3747,12 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 8cd2e2f90..796cc0b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["xtask", "packages/*"] +members = ["xtask", "crates/*", "packages/*"] resolver = "2" @@ -17,6 +17,7 @@ rust-version = "1.70" Inflector = "0.11.4" anyhow = "1.0.83" base64 = "0.22" +base64ct = "1.5.2" byteorder = "1" cargo_metadata = "0.18.1" cipher = "0.4.4" @@ -26,16 +27,21 @@ default-from-serde = "0.1" fxhash = "0.2.1" handlebars = "5.1.2" hex = "0.4.3" +langtag = "0.3.2" lightningcss = "1.0.0-alpha.60" magic-crypt = "3.1.13" once_cell = "1.19.0" parcel_selectors = "0.28.0" phf = "0.11.2" preset_env_base = "1.0.0" +pretty_assertions = "1.3.0" radix_fmt = "1" regex = { version = "1.10.4", default-features = false } serde = "1.0.203" serde_json = "1.0.117" +serde_repr = "0.1" +sha2 = "0.10" +similar-asserts = "1.4.2" sourcemap = "9.0.0" swc_atoms = "2.0.0" swc_cached = "1.0.0" @@ -63,6 +69,9 @@ swc_plugin_proxy = "4.0.0" swc_trace_macro = "2.0.0" testing = "4.0.0" tracing = "0.1.40" +widestring = "1.0.2" + +swc_icu_messageformat_parser = { version = "1.0.0", path = "./crates/swc_icu_messageformat_parser" } [profile.release] diff --git a/crates/swc_icu_messageformat_parser/Cargo.toml b/crates/swc_icu_messageformat_parser/Cargo.toml new file mode 100644 index 000000000..24cbefa1e --- /dev/null +++ b/crates/swc_icu_messageformat_parser/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = [ + "OJ Kwon ", + "DongYoon Kang ", +] +description = "ICU MessageFormat Parser" +edition.workspace = true +license.workspace = true +name = "swc_icu_messageformat_parser" +repository.workspace = true +version = "1.0.0" + + +[features] +utf16 = ["widestring"] + +[dependencies] +langtag = { workspace = true } +once_cell = { workspace = true } +regex = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_repr = { workspace = true } +widestring = { workspace = true, optional = true } + +[dev-dependencies] +serde_json = { workspace = true } +similar-asserts = { workspace = true } +testing = { workspace = true } diff --git a/crates/swc_icu_messageformat_parser/src/ast.rs b/crates/swc_icu_messageformat_parser/src/ast.rs new file mode 100644 index 000000000..2a2d095e5 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/ast.rs @@ -0,0 +1,549 @@ +use std::fmt; + +use serde::{ + ser::{SerializeMap, SerializeStruct}, + Serialize, Serializer, +}; +use serde_repr::Serialize_repr; +#[cfg(feature = "utf16")] +use widestring::Utf16Str; + +use crate::intl::{ + date_time_format_options::JsIntlDateTimeFormatOptions, + number_format_options::JsIntlNumberFormatOptions, +}; + +/// The type of an error that occurred while building an AST. +#[derive(Clone, Debug, Eq, PartialEq, Serialize_repr)] +#[repr(u8)] +pub enum ErrorKind { + /// Argument is unclosed (e.g. `{0`) + ExpectArgumentClosingBrace = 1, + /// Argument is empty (e.g. `{}`). + EmptyArgument = 2, + /// Argument is malformed (e.g. `{foo!}``) + MalformedArgument = 3, + /// Expect an argument type (e.g. `{foo,}`) + ExpectArgumentType = 4, + /// Unsupported argument type (e.g. `{foo,foo}`) + InvalidArgumentType = 5, + /// Expect an argument style (e.g. `{foo, number, }`) + ExpectArgumentStyle = 6, + /// The number skeleton is invalid. + InvalidNumberSkeleton = 7, + /// The date time skeleton is invalid. + InvalidDateTimeSkeleton = 8, + /// Exepct a number skeleton following the `::` (e.g. `{foo, number, ::}`) + ExpectNumberSkeleton = 9, + /// Exepct a date time skeleton following the `::` (e.g. `{foo, date, ::}`) + ExpectDateTimeSkeleton = 10, + /// Unmatched apostrophes in the argument style (e.g. `{foo, number, 'test`) + UnclosedQuoteInArgumentStyle = 11, + /// Missing select argument options (e.g. `{foo, select}`) + ExpectSelectArgumentOptions = 12, + + /// Expecting an offset value in `plural` or `selectordinal` argument (e.g + /// `{foo, plural, offset}`) + ExpectPluralArgumentOffsetValue = 13, + /// Offset value in `plural` or `selectordinal` is invalid (e.g. `{foo, + /// plural, offset: x}`) + InvalidPluralArgumentOffsetValue = 14, + + /// Expecting a selector in `select` argument (e.g `{foo, select}`) + ExpectSelectArgumentSelector = 15, + /// Expecting a selector in `plural` or `selectordinal` argument (e.g `{foo, + /// plural}`) + ExpectPluralArgumentSelector = 16, + + /// Expecting a message fragment after the `select` selector (e.g. `{foo, + /// select, apple}`) + ExpectSelectArgumentSelectorFragment = 17, + /// Expecting a message fragment after the `plural` or `selectordinal` + /// selector (e.g. `{foo, plural, one}`) + ExpectPluralArgumentSelectorFragment = 18, + + /// Selector in `plural` or `selectordinal` is malformed (e.g. `{foo, + /// plural, =x {#}}`) + InvalidPluralArgumentSelector = 19, + + /// Duplicate selectors in `plural` or `selectordinal` argument. + /// (e.g. {foo, plural, one {#} one {#}}) + DuplicatePluralArgumentSelector = 20, + /// Duplicate selectors in `select` argument. + /// (e.g. {foo, select, apple {apple} apple {apple}}) + DuplicateSelectArgumentSelector = 21, + + /// Plural or select argument option must have `other` clause. + MissingOtherClause = 22, + + /// The tag is malformed. (e.g. `foo) + InvalidTag = 23, + /// The tag name is invalid. (e.g. `<123>foo`) + InvalidTagName = 25, + /// The closing tag does not match the opening tag. (e.g. + /// `foo`) + UnmatchedClosingTag = 26, + /// The opening tag has unmatched closing tag. (e.g. `foo`) + UnclosedTag = 27, +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ErrorKind::ExpectArgumentClosingBrace => write!(f, "EXPECT_ARGUMENT_CLOSING_BRACE"), + ErrorKind::EmptyArgument => write!(f, "EMPTY_ARGUMENT"), + ErrorKind::MalformedArgument => write!(f, "MALFORMED_ARGUMENT"), + ErrorKind::ExpectArgumentType => write!(f, "EXPECT_ARGUMENT_TYPE"), + ErrorKind::InvalidArgumentType => write!(f, "INVALID_ARGUMENT_TYPE"), + ErrorKind::ExpectArgumentStyle => write!(f, "EXPECT_ARGUMENT_STYLE"), + ErrorKind::InvalidNumberSkeleton => write!(f, "INVALID_NUMBER_SKELETON"), + ErrorKind::InvalidDateTimeSkeleton => write!(f, "INVALID_DATE_TIME_SKELETON"), + ErrorKind::ExpectNumberSkeleton => write!(f, "EXPECT_NUMBER_SKELETON"), + ErrorKind::ExpectDateTimeSkeleton => write!(f, "EXPECT_DATE_TIME_SKELETON"), + ErrorKind::UnclosedQuoteInArgumentStyle => { + write!(f, "UNCLOSED_QUOTE_IN_ARGUMENT_STYLE") + } + ErrorKind::ExpectSelectArgumentOptions => write!(f, "EXPECT_SELECT_ARGUMENT_OPTIONS"), + ErrorKind::ExpectPluralArgumentOffsetValue => { + write!(f, "EXPECT_PLURAL_ARGUMENT_OFFSET_VALUE") + } + ErrorKind::InvalidPluralArgumentOffsetValue => { + write!(f, "INVALID_PLURAL_ARGUMENT_OFFSET_VALUE") + } + ErrorKind::ExpectSelectArgumentSelector => write!(f, "EXPECT_SELECT_ARGUMENT_SELECTOR"), + ErrorKind::ExpectPluralArgumentSelector => write!(f, "EXPECT_PLURAL_ARGUMENT_SELECTOR"), + ErrorKind::ExpectSelectArgumentSelectorFragment => { + write!(f, "EXPECT_SELECT_ARGUMENT_SELECTOR_FRAGMENT") + } + ErrorKind::ExpectPluralArgumentSelectorFragment => { + write!(f, "EXPECT_PLURAL_ARGUMENT_SELECTOR_FRAGMENT") + } + ErrorKind::InvalidPluralArgumentSelector => { + write!(f, "INVALID_PLURAL_ARGUMENT_SELECTOR") + } + ErrorKind::DuplicatePluralArgumentSelector => { + write!(f, "DUPLICATE_PLURAL_ARGUMENT_SELECTOR") + } + ErrorKind::DuplicateSelectArgumentSelector => { + write!(f, "DUPLICATE_SELECT_ARGUMENT_SELECTOR") + } + ErrorKind::MissingOtherClause => write!(f, "MISSING_OTHER_CLAUSE"), + ErrorKind::InvalidTag => write!(f, "INVALID_TAG"), + ErrorKind::InvalidTagName => write!(f, "INVALID_TAG_NAME"), + ErrorKind::UnmatchedClosingTag => write!(f, "UNMATCHED_CLOSING_TAG"), + ErrorKind::UnclosedTag => write!(f, "UNCLOSED_TAG"), + } + } +} + +/// A single position in an ICU message. +/// +/// A position encodes one half of a span, and include the code unit offset, +/// line number and column number. +#[derive(Clone, Copy, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub offset: usize, + pub line: usize, + pub column: usize, +} + +impl Position { + pub fn new(offset: usize, line: usize, column: usize) -> Position { + Position { + offset, + line, + column, + } + } +} + +impl fmt::Debug for Position { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Position::new({:?}, {:?}, {:?})", + self.offset, self.line, self.column + ) + } +} + +/// Span represents the position information of a single AST item. +/// +/// All span positions are absolute byte offsets that can be used on the +/// original regular expression that was parsed. +#[derive(Clone, Copy, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Span { + /// The start byte offset. + pub start: Position, + /// The end byte offset. + pub end: Position, +} + +impl fmt::Debug for Span { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Span::new({:?}, {:?})", self.start, self.end) + } +} + +impl Span { + /// Create a new span with the given positions. + pub fn new(start: Position, end: Position) -> Span { + Span { start, end } + } +} + +/// An error that occurred while parsing an ICU message into an abstract +/// syntax tree. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct Error { + /// The kind of error. + pub kind: ErrorKind, + /// The original message that the parser generated the error from. Every + /// span in an error is a valid range into this string. + pub message: String, + /// The span of this error. + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +/// An abstract syntax tree for a ICU message. Adapted from: +/// https://github.com/formatjs/formatjs/blob/c03d4989323a33765798acdd74fb4f5b01f0bdcd/packages/intl-messageformat-parser/src/types.ts +pub type Ast<'s> = Vec>; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PluralType { + Cardinal, + Ordinal, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AstElement<'s> { + /// Raw text + Literal { value: String, span: Option }, + /// Variable w/o any format, e.g `var` in `this is a {var}` + Argument { value: String, span: Option }, + /// Variable w/ number format + Number { + value: String, + span: Option, + style: Option>, + }, + /// Variable w/ date format + Date { + value: String, + span: Option, + style: Option>, + }, + /// Variable w/ time format + Time { + value: String, + span: Option, + style: Option>, + }, + /// Variable w/ select format + Select { + value: String, + span: Option, + options: PluralOrSelectOptions<'s>, + }, + /// Variable w/ plural format + Plural { + value: String, + plural_type: PluralType, + span: Option, + // TODO: want to use double here but it does not implement Eq trait. + offset: i64, + options: PluralOrSelectOptions<'s>, + }, + /// Only possible within plural argument. + /// This is the `#` symbol that will be substituted with the count. + Pound(Span), + /// XML-like tag + Tag { + value: &'s str, + span: Option, + children: Box>, + }, +} + +// Until this is resolved, we have to roll our own serialization: https://github.com/serde-rs/serde/issues/745 +impl Serialize for AstElement<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + AstElement::Literal { + ref value, + ref span, + } => { + let mut state = serializer.serialize_struct("Literal", 3)?; + state.serialize_field("type", &0)?; + state.serialize_field("value", value)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.end() + } + AstElement::Argument { + ref value, + ref span, + } => { + let mut state = serializer.serialize_struct("Argument", 3)?; + state.serialize_field("type", &1)?; + state.serialize_field("value", value)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.end() + } + AstElement::Number { + ref value, + ref span, + ref style, + } => { + let mut state = serializer.serialize_struct("Number", 4)?; + state.serialize_field("type", &2)?; + state.serialize_field("value", value)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.serialize_field("style", style)?; + state.end() + } + AstElement::Date { + ref value, + ref span, + ref style, + } => { + let mut state = serializer.serialize_struct("Date", 4)?; + state.serialize_field("type", &3)?; + state.serialize_field("value", value)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.serialize_field("style", style)?; + state.end() + } + AstElement::Time { + ref value, + ref span, + ref style, + } => { + let mut state = serializer.serialize_struct("Time", 4)?; + state.serialize_field("type", &4)?; + state.serialize_field("value", value)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.serialize_field("style", style)?; + state.end() + } + AstElement::Select { + ref value, + ref span, + ref options, + } => { + let mut state = serializer.serialize_struct("Select", 4)?; + state.serialize_field("type", &5)?; + state.serialize_field("value", value)?; + state.serialize_field("options", options)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.end() + } + AstElement::Plural { + ref value, + ref span, + ref plural_type, + ref offset, + ref options, + } => { + let mut state = serializer.serialize_struct("Plural", 6)?; + state.serialize_field("type", &6)?; + state.serialize_field("value", value)?; + state.serialize_field("options", options)?; + state.serialize_field("offset", offset)?; + state.serialize_field("pluralType", plural_type)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.end() + } + AstElement::Pound(ref span) => { + let mut state = serializer.serialize_struct("Pound", 2)?; + state.serialize_field("type", &7)?; + state.serialize_field("location", span)?; + state.end() + } + AstElement::Tag { + ref value, + ref span, + ref children, + } => { + let mut state = serializer.serialize_struct("Pound", 2)?; + state.serialize_field("type", &8)?; + state.serialize_field("value", value)?; + state.serialize_field("children", children)?; + if span.is_some() { + state.serialize_field("location", span)?; + } + state.end() + } + } + } +} + +#[cfg(feature = "utf16")] +#[derive(Clone, Debug, PartialEq)] +pub struct PluralOrSelectOptions<'s>(pub Vec<(&'s Utf16Str, PluralOrSelectOption<'s>)>); + +/// Workaround of Rust's orphan impl rule +#[cfg(not(feature = "utf16"))] +#[derive(Clone, Debug, PartialEq)] +pub struct PluralOrSelectOptions<'s>(pub Vec<(&'s str, PluralOrSelectOption<'s>)>); + +impl Serialize for PluralOrSelectOptions<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let options = &self.0; + let mut state = serializer.serialize_map(Some(options.len()))?; + for (selector, fragment) in options { + #[cfg(feature = "utf16")] + let s = selector.to_string(); + #[cfg(feature = "utf16")] + let s = s.as_str(); + #[cfg(not(feature = "utf16"))] + let s = selector; + state.serialize_entry(s, fragment)?; + } + state.end() + } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum NumberArgStyle<'s> { + Style(&'s str), + Skeleton(Box>), +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberSkeleton<'s> { + #[serde(rename = "type")] + pub skeleton_type: SkeletonType, + pub tokens: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + pub parsed_options: JsIntlNumberFormatOptions, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberSkeletonToken<'s> { + pub stem: &'s str, + pub options: Vec<&'s str>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum DateTimeArgStyle<'s> { + Style(&'s str), + Skeleton(DateTimeSkeleton), +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize_repr)] +#[repr(u8)] +pub enum SkeletonType { + Number, + DateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DateTimeSkeleton { + #[serde(rename = "type")] + pub skeleton_type: SkeletonType, + pub pattern: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + pub parsed_options: JsIntlDateTimeFormatOptions, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PluralOrSelectOption<'s> { + pub value: Ast<'s>, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::intl::number_format_options::JsIntlNumberFormatOptions; + + #[test] + fn serialize_number_arg_style_with_skeleton() { + similar_asserts::assert_eq!( + serde_json::to_value(NumberArgStyle::Skeleton(Box::new(NumberSkeleton { + skeleton_type: SkeletonType::Number, + tokens: vec![NumberSkeletonToken { + stem: "foo", + options: vec!["bar", "baz"] + }], + location: Some(Span::new(Position::new(0, 1, 1), Position::new(11, 1, 12))), + parsed_options: JsIntlNumberFormatOptions::default(), + }))) + .unwrap(), + json!({ + "tokens": [{ + "stem": "foo", + "options": [ + "bar", + "baz" + ] + }], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1, + }, + "end": { + "offset": 11, + "line": 1, + "column": 12, + } + }, + "type": 0, + "parsedOptions": {}, + }) + ); + } + + #[test] + fn serialize_number_arg_style_string() { + similar_asserts::assert_eq!( + serde_json::to_value(NumberArgStyle::Style("percent")).unwrap(), + json!("percent") + ) + } + + #[test] + fn serialize_plural_type() { + similar_asserts::assert_eq!( + serde_json::to_value(PluralType::Cardinal).unwrap(), + json!("cardinal") + ) + } +} diff --git a/crates/swc_icu_messageformat_parser/src/intl/date_time_format_options.rs b/crates/swc_icu_messageformat_parser/src/intl/date_time_format_options.rs new file mode 100644 index 000000000..55bb77e9a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/intl/date_time_format_options.rs @@ -0,0 +1,49 @@ +use serde::Serialize; + +use super::options::{ + DateTimeDisplayFormat, DateTimeFormatMatcher, DateTimeFormatStyle, DateTimeMonthDisplayFormat, + HourCycle, LocaleMatcherFormatOptions, TimeZoneNameFormat, UnitDisplay, +}; + +/// Subset of options that will be parsed from the ICU message daet or time +/// skeleton. +#[derive(Default, Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JsIntlDateTimeFormatOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub locale_matcher: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub weekday: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub era: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub year: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub month: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub day: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hour: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub minute: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub second: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_zone_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hour12: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hour_cycle: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_zone: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format_matcher: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub date_style: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_style: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub day_period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fractional_second_digits: Option, +} diff --git a/crates/swc_icu_messageformat_parser/src/intl/mod.rs b/crates/swc_icu_messageformat_parser/src/intl/mod.rs new file mode 100644 index 000000000..685c7ce34 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/intl/mod.rs @@ -0,0 +1,3 @@ +pub mod date_time_format_options; +pub mod number_format_options; +pub mod options; diff --git a/crates/swc_icu_messageformat_parser/src/intl/number_format_options.rs b/crates/swc_icu_messageformat_parser/src/intl/number_format_options.rs new file mode 100644 index 000000000..4f8ca0019 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/intl/number_format_options.rs @@ -0,0 +1,56 @@ +use serde::Serialize; + +use super::options::{ + CompactDisplay, LocaleMatcherFormatOptions, Notation, NumberFormatOptionsCurrencyDisplay, + NumberFormatOptionsCurrencySign, NumberFormatOptionsRoundingPriority, + NumberFormatOptionsSignDisplay, NumberFormatOptionsStyle, + NumberFormatOptionsTrailingZeroDisplay, UnitDisplay, +}; + +/// Subset of options that will be parsed from the ICU message number skeleton. +#[derive(Default, Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JsIntlNumberFormatOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub notation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compact_display: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub locale_matcher: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub currency_sign: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sign_display: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub numbering_system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trailing_zero_display: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rounding_priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scale: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub use_grouping: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub minimum_integer_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub minimum_fraction_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub maximum_fraction_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub minimum_significant_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub maximum_significant_digits: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub currency_display: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unit_display: Option, +} diff --git a/crates/swc_icu_messageformat_parser/src/intl/options.rs b/crates/swc_icu_messageformat_parser/src/intl/options.rs new file mode 100644 index 000000000..cb376b2b0 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/intl/options.rs @@ -0,0 +1,138 @@ +use serde::Serialize; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CompactDisplay { + Short, + Long, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Notation { + Standard, + Scientific, + Engineering, + Compact, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum UnitDisplay { + Short, + Long, + Narrow, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsTrailingZeroDisplay { + Auto, + StripIfInteger, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsRoundingPriority { + Auto, + MorePrecision, + LessPrecision, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum LocaleMatcherFormatOptions { + Lookup, + #[serde(rename = "best fit")] + BestFit, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsStyle { + Decimal, + Percent, + Currency, + Unit, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsCurrencyDisplay { + Symbol, + Code, + Name, + NarrowSymbol, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsCurrencySign { + Standard, + Accounting, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum NumberFormatOptionsSignDisplay { + Auto, + Always, + Never, + ExceptZero, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DateTimeFormatMatcher { + Basic, + #[serde(rename = "best fit")] + BestFit, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DateTimeFormatStyle { + Full, + Long, + Medium, + Short, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DateTimeDisplayFormat { + Numeric, + #[serde(rename = "2-digit")] + TwoDigit, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DateTimeMonthDisplayFormat { + Numeric, + #[serde(rename = "2-digit")] + TwoDigit, + Long, + Short, + Narrow, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TimeZoneNameFormat { + Short, + Long, + ShortOffset, + LongOffset, + ShortGeneric, + LongGeneric, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum HourCycle { + H11, + H12, + H23, + H24, +} diff --git a/crates/swc_icu_messageformat_parser/src/lib.rs b/crates/swc_icu_messageformat_parser/src/lib.rs new file mode 100644 index 000000000..d960e1a88 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/lib.rs @@ -0,0 +1,7 @@ +mod ast; +mod intl; +mod parser; +mod pattern_syntax; + +pub use ast::{Ast, AstElement, Error, Position, Span}; +pub use parser::{Parser, ParserOptions}; diff --git a/crates/swc_icu_messageformat_parser/src/parser.rs b/crates/swc_icu_messageformat_parser/src/parser.rs new file mode 100644 index 000000000..c7d3feec5 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/parser.rs @@ -0,0 +1,1829 @@ +use std::{cell::Cell, cmp, collections::HashSet, result}; + +use langtag::LanguageTag; +use once_cell::sync::Lazy; +use regex::Regex as Regexp; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "utf16")] +use widestring::{Utf16Str, Utf16String}; + +use crate::{ + ast::{self, *}, + intl::{ + date_time_format_options::JsIntlDateTimeFormatOptions, + number_format_options::JsIntlNumberFormatOptions, + options::{ + CompactDisplay, DateTimeDisplayFormat, DateTimeMonthDisplayFormat, HourCycle, Notation, + NumberFormatOptionsCurrencyDisplay, NumberFormatOptionsCurrencySign, + NumberFormatOptionsRoundingPriority, NumberFormatOptionsSignDisplay, + NumberFormatOptionsStyle, NumberFormatOptionsTrailingZeroDisplay, TimeZoneNameFormat, + UnitDisplay, + }, + }, + pattern_syntax::is_pattern_syntax, +}; + +type Result = result::Result; + +pub static FRACTION_PRECISION_REGEX: Lazy = + Lazy::new(|| Regexp::new(r"^\.(?:(0+)(\*)?|(#+)|(0+)(#+))$").unwrap()); +pub static SIGNIFICANT_PRECISION_REGEX: Lazy = + Lazy::new(|| Regexp::new(r"^(@+)?(\+|#+)?[rs]?$").unwrap()); +pub static INTEGER_WIDTH_REGEX: Lazy = + Lazy::new(|| Regexp::new(r"(\*)(0+)|(#+)(0+)|(0+)").unwrap()); +pub static CONCISE_INTEGER_WIDTH_REGEX: Lazy = + Lazy::new(|| Regexp::new(r"^(0+)$").unwrap()); + +/// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table +/// Credit: https://github.com/caridy/intl-datetimeformat-pattern/blob/master/index.js +/// with some tweaks +/// TODO: This is incomplete +pub static DATE_TIME_REGEX: Lazy = Lazy::new(|| { + Regexp::new(r"(?:[Eec]{1,6}|G{1,5}|[Qq]{1,5}|(?:[yYur]+|U{1,5})|[ML]{1,5}|d{1,2}|D{1,3}|F{1}|[abB]{1,5}|[hkHK]{1,2}|w{1,2}|W{1}|m{1,2}|s{1,2}|[zZOvVxX]{1,4})").unwrap() +}); + +#[derive(Clone, Debug)] +pub struct Parser<'s> { + position: Cell, + message: &'s str, + options: ParserOptions, + #[cfg(feature = "utf16")] + message_utf16: Utf16String, +} + +#[derive(Default, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ParserOptions { + /// Whether to treat HTML/XML tags as string literal + /// instead of parsing them as tag token. + /// When this is false we only allow simple tags without + /// any attributes + #[serde(default)] + pub ignore_tag: bool, + + /// Should `select`, `selectordinal`, and `plural` arguments always include + /// the `other` case clause. + #[serde(default)] + pub requires_other_clause: bool, + + /// Whether to parse number/datetime skeleton + /// into Intl.NumberFormatOptions and Intl.DateTimeFormatOptions, + /// respectively + #[serde(default)] + pub should_parse_skeletons: bool, + + /// Capture location info in AST + /// Default is false + #[serde(default)] + pub capture_location: bool, + + /// Instance of Intl.Locale to resolve locale-dependent skeleton + #[serde(default)] + pub locale: Option, +} + +impl ParserOptions { + pub fn new( + ignore_tag: bool, + requires_other_clause: bool, + should_parse_skeletons: bool, + capture_location: bool, + locale: Option, + ) -> Self { + ParserOptions { + ignore_tag, + requires_other_clause, + should_parse_skeletons, + capture_location, + locale, + } + } +} + +fn is_whitespace(ch: char) -> bool { + ch.is_whitespace() || ch == '\u{200e}' || ch == '\u{200f}' +} + +fn is_alpha(ch: Option) -> bool { + if let Some(ch) = ch { + ch.is_ascii_alphabetic() + } else { + false + } +} + +fn get_default_hour_symbol_from_locale(locale: &str) -> char { + let language_tag = LanguageTag::parse(locale).expect("Should able to parse locale tag"); + + // There's no built in Intl.Locale, manually read through extensions for the + // values we need to read + for extension in language_tag.extensions() { + //TODO: locale.hourCycles support is missing + + let hour_cycle = if extension.singleton() as char == 'u' { + let mut ret = None; + let mut ext_iter = extension.iter(); + loop { + let ext = ext_iter.next(); + + if let Some(ext) = ext { + if ext == "hc" { + let hour_cycle = ext_iter.next().expect("Should have hour cycle"); + ret = match hour_cycle.as_str() { + "h11" => Some(HourCycle::H11), + "h12" => Some(HourCycle::H12), + "h23" => Some(HourCycle::H23), + "h24" => Some(HourCycle::H24), + _ => None, + }; + } + } else { + break; + } + } + ret + } else { + None + }; + + if let Some(hour_cycle) = hour_cycle { + return match hour_cycle { + HourCycle::H11 => 'K', + HourCycle::H12 => 'h', + HourCycle::H23 => 'H', + HourCycle::H24 => 'k', + }; + } + + //TODO: locale.language data generation + } + + panic!("Should have hour cycle"); +} + +fn get_best_pattern(skeleton: &str, locale: &str) -> String { + let mut ret = "".to_string(); + + let skeleton_chars: Vec<_> = skeleton.chars().collect(); + let skeleton_char_len = skeleton_chars.len(); + let mut extra_len = 0; + + for (pattern_pos, pattern_char) in skeleton.chars().enumerate() { + if pattern_char == 'j' { + if pattern_pos + 1 < skeleton_char_len + && skeleton_chars[pattern_pos + 1] == pattern_char + { + extra_len += 1; + continue; + } else { + let mut hour_len = 1 + (extra_len & 1); + let mut day_period_len = if extra_len < 2 { + 1 + } else { + 3 + (extra_len >> 1) + }; + let day_period_char = 'a'; + let hour_char = get_default_hour_symbol_from_locale(locale); + + if hour_char == 'H' || hour_char == 'k' { + day_period_len = 0; + } + + while day_period_len > 0 { + ret = format!("{}{}", ret, day_period_char); + day_period_len -= 1; + } + + while hour_len > 0 { + ret = format!("{}{}", hour_char, ret); + hour_len -= 1; + } + } + } else if pattern_char == 'J' { + ret = format!("{}H", ret); + } else { + ret = format!("{}{}", ret, pattern_char); + } + } + + ret +} + +fn parse_date_time_skeleton(skeleton: &str) -> JsIntlDateTimeFormatOptions { + let mut ret = JsIntlDateTimeFormatOptions::default(); + + for caps in DATE_TIME_REGEX.captures_iter(skeleton) { + let match_str = caps.get(0).map(|m| m.as_str()).unwrap_or_default(); + let match_len = match_str.len(); + + match &match_str.chars().next().unwrap_or_default() { + // Era + 'G' => { + if match_len == 4 { + ret.era = Some(UnitDisplay::Long); + } else if match_len == 5 { + ret.era = Some(UnitDisplay::Narrow); + } else { + ret.era = Some(UnitDisplay::Short); + } + } + // Year + 'y' => { + if match_len == 2 { + ret.year = Some(DateTimeDisplayFormat::TwoDigit); + } else { + ret.year = Some(DateTimeDisplayFormat::Numeric); + } + } + 'Y' | 'u' | 'U' | 'r' => { + panic!("`Y/u/U/r` (year) patterns are not supported, use `y` instead"); + } + // Quarter + 'q' | 'Q' => { + panic!("`q/Q` (quarter) patterns are not supported"); + } + // Month + 'M' | 'L' => { + if match_len == 1 { + ret.month = Some(DateTimeMonthDisplayFormat::Numeric); + } else if match_len == 2 { + ret.month = Some(DateTimeMonthDisplayFormat::TwoDigit); + } else if match_len == 3 { + ret.month = Some(DateTimeMonthDisplayFormat::Short); + } else if match_len == 4 { + ret.month = Some(DateTimeMonthDisplayFormat::Long); + } else if match_len == 5 { + ret.month = Some(DateTimeMonthDisplayFormat::Narrow); + } + } + // Week + 'w' | 'W' => { + panic!("`w/W` (week) patterns are not supported"); + } + 'd' => { + if match_len == 1 { + ret.day = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.day = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'D' | 'F' | 'g' => { + panic!("`D/F/g` (day) patterns are not supported, use `d` instead"); + } + 'E' => { + if match_len == 4 { + ret.weekday = Some(UnitDisplay::Short); + } else if match_len == 5 { + ret.weekday = Some(UnitDisplay::Narrow); + } else { + ret.weekday = Some(UnitDisplay::Short); + } + } + 'e' => { + if match_len < 4 { + panic!("`e..eee` (weekday) patterns are not supported"); + } + + if match_len == 4 { + ret.weekday = Some(UnitDisplay::Short); + } else if match_len == 5 { + ret.weekday = Some(UnitDisplay::Long); + } else if match_len == 6 { + ret.weekday = Some(UnitDisplay::Narrow); + } else if match_len == 7 { + ret.weekday = Some(UnitDisplay::Short); + } + } + 'c' => { + if match_len < 4 { + panic!("`c..ccc` (weekday) patterns are not supported"); + } + + if match_len == 4 { + ret.weekday = Some(UnitDisplay::Short); + } else if match_len == 5 { + ret.weekday = Some(UnitDisplay::Long); + } else if match_len == 6 { + ret.weekday = Some(UnitDisplay::Narrow); + } else if match_len == 7 { + ret.weekday = Some(UnitDisplay::Short); + } + } + // Period + 'a' => { + // AM, PM + ret.hour12 = Some(true) + } + 'b' /* am, pm, noon, midnight*/ | 'B' /* flexible day periods */ => { + panic!("`b/B` (period) patterns are not supported, use `a` instead"); + } + //Hour + 'h' => { + ret.hour_cycle = Some(HourCycle::H12); + if match_len == 1 { + ret.hour = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.hour = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'H' => { + ret.hour_cycle = Some(HourCycle::H23); + if match_len == 1 { + ret.hour = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.hour = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'K' => { + ret.hour_cycle = Some(HourCycle::H11); + if match_len == 1 { + ret.hour = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.hour = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'k' => { + ret.hour_cycle = Some(HourCycle::H24); + if match_len == 1 { + ret.hour = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.hour = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'j' | 'J' | 'C' => { + panic!("`j/J/C` (hour) patterns are not supported, use `h/H/K/k` instead"); + } + // Minute + 'm' => { + if match_len == 1 { + ret.minute = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.minute = Some(DateTimeDisplayFormat::TwoDigit); + } + } + // Second + 's' => { + if match_len == 1 { + ret.second = Some(DateTimeDisplayFormat::Numeric); + } else if match_len == 2 { + ret.second = Some(DateTimeDisplayFormat::TwoDigit); + } + } + 'S' | 'A' => { panic!("`S/A` (second) patterns are not supported, use `s` instead'"); } + // Zone + 'z' => { + // 1..3, 4: specific non-location format + ret.time_zone_name = if match_len < 4 { Some(TimeZoneNameFormat::Short) } else { + Some(TimeZoneNameFormat::Long) + }; + } + 'Z' /* 1..3, 4, 5: The ISO8601 varios formats */ | + 'O' /* 1, 4: miliseconds in day short, long */ | + 'v' /* 1, 4: generic non-location format */ | + 'V' /* 1, 2, 3, 4: time zone ID or city */ | + 'X' /* 1, 2, 3, 4: The ISO8601 varios formats */ | + 'x' /* 1, 2, 3, 4: The ISO8601 varios formats */ => { + panic!("`Z/O/v/V/X/x` (timeZone) patterns are not supported, use `z` instead'"); + } + _ => {} + } + } + + ret +} + +fn icu_unit_to_ecma(value: &str) -> Option { + Some( + Regexp::new(r"^(.*?)-") + .unwrap() + .replace(value, "") + .to_string(), + ) +} + +fn parse_significant_precision(ret: &mut JsIntlNumberFormatOptions, value: &str) { + if let Some(l) = value.chars().last() { + if l == 'r' { + ret.rounding_priority = Some(NumberFormatOptionsRoundingPriority::MorePrecision); + } else if l == 's' { + ret.rounding_priority = Some(NumberFormatOptionsRoundingPriority::LessPrecision); + } + } + + let cap = SIGNIFICANT_PRECISION_REGEX.captures(value); + if let Some(cap) = cap { + let g1 = cap.get(1); + let g2 = cap.get(2); + + let g1_len = g1.map(|g| g.as_str().len() as u32); + let is_g2_non_str = g2.is_none() + || g2 + .map(|g| g.as_str().parse::().is_ok()) + .unwrap_or(false); + + // @@@ case + if is_g2_non_str { + ret.minimum_significant_digits = g1_len; + ret.maximum_significant_digits = g1_len; + } + // @@@+ case + else if g2.map(|g| g.as_str() == "+").unwrap_or(false) { + ret.minimum_significant_digits = g1_len; + } + // .### case + else if g1.map(|g| g.as_str().starts_with("#")).unwrap_or(false) { + ret.maximum_significant_digits = g1_len; + } + // .@@## or .@@@ case + else { + ret.minimum_significant_digits = g1_len; + ret.maximum_significant_digits = + g1_len.map(|l| l + g2.map(|g| g.as_str().len() as u32).unwrap_or(0)); + } + } +} + +fn parse_sign(ret: &mut JsIntlNumberFormatOptions, value: &str) { + match value { + "sign-auto" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::Auto); + } + "sign-accounting" | "()" => { + ret.currency_sign = Some(NumberFormatOptionsCurrencySign::Accounting); + } + "sign-always" | "+!" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::Always); + } + "sign-accounting-always" | "()!" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::Always); + ret.currency_sign = Some(NumberFormatOptionsCurrencySign::Accounting); + } + "sign-except-zero" | "+?" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::ExceptZero); + } + "sign-accounting-except-zero" | "()?" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::ExceptZero); + ret.currency_sign = Some(NumberFormatOptionsCurrencySign::Accounting); + } + "sign-never" | "+_" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::Never); + } + _ => {} + } +} + +fn parse_concise_scientific_and_engineering_stem(ret: &mut JsIntlNumberFormatOptions, stem: &str) { + let mut stem = stem; + let mut has_sign = false; + if stem.starts_with("EE") { + ret.notation = Some(Notation::Engineering); + stem = &stem[2..]; + has_sign = true; + } else if stem.starts_with("E") { + ret.notation = Some(Notation::Scientific); + stem = &stem[1..]; + has_sign = true; + } + + if has_sign { + let sign_display = &stem[0..2]; + match sign_display { + "+!" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::Always); + stem = &stem[2..]; + } + "+?" => { + ret.sign_display = Some(NumberFormatOptionsSignDisplay::ExceptZero); + stem = &stem[2..]; + } + _ => {} + } + + if !CONCISE_INTEGER_WIDTH_REGEX.is_match(stem) { + panic!("Malformed concise eng/scientific notation"); + } + + ret.minimum_integer_digits = Some(stem.len() as u32); + } +} + +fn parse_number_skeleton(skeleton: &Vec) -> JsIntlNumberFormatOptions { + let mut ret = JsIntlNumberFormatOptions::default(); + for token in skeleton { + match token.stem { + "percent" | "%" => { + ret.style = Some(NumberFormatOptionsStyle::Percent); + continue; + } + "%x100" => { + ret.style = Some(NumberFormatOptionsStyle::Percent); + ret.scale = Some(100.0); + continue; + } + "currency" => { + ret.style = Some(NumberFormatOptionsStyle::Currency); + ret.currency = Some(token.options[0].to_string()); + continue; + } + "group-off" | ",_" => { + ret.use_grouping = Some(false); + continue; + } + "precision-integer" | "." => { + ret.maximum_fraction_digits = Some(0); + continue; + } + "measure-unit" | "unit" => { + ret.style = Some(NumberFormatOptionsStyle::Unit); + ret.unit = icu_unit_to_ecma(token.options[0]); + continue; + } + "compact-short" | "K" => { + ret.notation = Some(Notation::Compact); + ret.compact_display = Some(CompactDisplay::Short); + continue; + } + "compact-long" | "KK" => { + ret.notation = Some(Notation::Compact); + ret.compact_display = Some(CompactDisplay::Long); + continue; + } + "scientific" => { + ret.notation = Some(Notation::Scientific); + for opt in &token.options { + parse_sign(&mut ret, opt); + } + continue; + } + "engineering" => { + ret.notation = Some(Notation::Engineering); + for opt in &token.options { + parse_sign(&mut ret, opt); + } + continue; + } + "notation-simple" => { + ret.notation = Some(Notation::Standard); + continue; + } + // https://github.com/unicode-org/icu/blob/master/icu4c/source/i18n/unicode/unumberformatter.h + "unit-width-narrow" => { + ret.currency_display = Some(NumberFormatOptionsCurrencyDisplay::NarrowSymbol); + ret.unit_display = Some(UnitDisplay::Narrow); + continue; + } + "unit-width-short" => { + ret.currency_display = Some(NumberFormatOptionsCurrencyDisplay::Code); + ret.unit_display = Some(UnitDisplay::Short); + continue; + } + "unit-width-full-name" => { + ret.currency_display = Some(NumberFormatOptionsCurrencyDisplay::Name); + ret.unit_display = Some(UnitDisplay::Long); + continue; + } + "unit-width-iso-code" => { + ret.currency_display = Some(NumberFormatOptionsCurrencyDisplay::Symbol); + continue; + } + "scale" => { + ret.scale = token.options[0].parse().ok(); + continue; + } + "integer-width" => { + let cap = INTEGER_WIDTH_REGEX.captures(token.options[0]); + if let Some(cap) = cap { + if cap.get(1).is_some() { + ret.minimum_integer_digits = cap.get(2).map(|c| c.as_str().len() as u32); + } else if cap.get(3).is_some() && cap.get(4).is_some() { + panic!("We currently do not support maximum integer digits"); + } else if cap.get(5).is_some() { + panic!("We currently do not support exact integer digits"); + } + } + continue; + } + _ => { + //noop + } + } + + // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#integer-width + if CONCISE_INTEGER_WIDTH_REGEX.is_match(token.stem) { + ret.minimum_integer_digits = Some(token.stem.len() as u32); + continue; + } + + if FRACTION_PRECISION_REGEX.is_match(token.stem) { + // Precision + // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#fraction-precision + // precision-integer case + let caps = FRACTION_PRECISION_REGEX.captures(token.stem); + if let Some(caps) = caps { + let g1_len = caps.get(1).map(|g| g.as_str().len() as u32); + let g2 = caps.get(2); + let g3 = caps.get(3); + let g4 = caps.get(4); + let g5 = caps.get(5); + + // .000* case (before ICU67 it was .000+) + if g2.map(|g| g.as_str() == "*").unwrap_or(false) { + ret.minimum_fraction_digits = g1_len; + } + // .### case + else if g3.map(|g| g.as_str().starts_with("#")).unwrap_or(false) { + ret.maximum_fraction_digits = g3.map(|g| g.as_str().len() as u32); + } + // .00## case + else if g4.is_some() && g5.is_some() { + ret.minimum_fraction_digits = g4.map(|g| g.as_str().len() as u32); + ret.maximum_fraction_digits = + Some(g4.unwrap().as_str().len() as u32 + g5.unwrap().as_str().len() as u32); + } else { + ret.minimum_fraction_digits = g1_len; + ret.maximum_fraction_digits = g1_len; + } + + let opt = token.options.first(); + // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#trailing-zero-display + if let Some(opt) = opt { + if *opt == "w" { + ret.trailing_zero_display = + Some(NumberFormatOptionsTrailingZeroDisplay::StripIfInteger); + } else { + parse_significant_precision(&mut ret, opt); + } + } + } + continue; + } + + // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#significant-digits-precision + if SIGNIFICANT_PRECISION_REGEX.is_match(token.stem) { + parse_significant_precision(&mut ret, token.stem); + continue; + } + + parse_sign(&mut ret, token.stem); + parse_concise_scientific_and_engineering_stem(&mut ret, token.stem); + } + ret +} + +impl<'s> Parser<'s> { + pub fn new(message: &'s str, options: &ParserOptions) -> Parser<'s> { + Parser { + message, + #[cfg(feature = "utf16")] + message_utf16: Utf16String::from(message), + position: Cell::new(Position { + offset: 0, + line: 1, + column: 1, + }), + options: options.clone(), + } + } + + pub fn parse(&mut self) -> Result { + assert_eq!(self.offset(), 0, "parser can only be used once"); + self.parse_message(0, "", false) + } + + /// # Arguments + /// + /// * `nesting_level` - The nesting level of the message. This can be + /// positive if the message is nested inside the plural or select + /// argument's selector clause. + /// * `parent_arg_type` - If nested, this is the parent plural or selector's + /// argument type. Otherwise this should just be an empty string. + /// * `expecting_close_tag` - If true, this message is directly or + /// indirectly nested inside between a pair of opening and closing tags. + /// The nested message will not parse beyond the closing tag boundary. + fn parse_message( + &self, + nesting_level: usize, + parent_arg_type: &str, + expecting_close_tag: bool, + ) -> Result { + let mut elements: Vec = vec![]; + + while !self.is_eof() { + elements.push(match self.char() { + '{' => self.parse_argument(nesting_level, expecting_close_tag)?, + '}' if nesting_level > 0 => break, + '#' if matches!(parent_arg_type, "plural" | "selectordinal") => { + let position = self.position(); + self.bump(); + AstElement::Pound(Span::new(position, self.position())) + } + '<' if !self.options.ignore_tag && self.peek() == Some('/') => { + if expecting_close_tag { + break; + } else { + return Err(self.error( + ErrorKind::UnmatchedClosingTag, + Span::new(self.position(), self.position()), + )); + } + } + '<' if !self.options.ignore_tag && is_alpha(self.peek()) => { + self.parse_tag(nesting_level, parent_arg_type)? + } + _ => self.parse_literal(nesting_level, parent_arg_type)?, + }) + } + + Ok(elements) + } + + fn position(&self) -> Position { + self.position.get() + } + + /// A tag name must start with an ASCII lower case letter. The grammar is + /// based on the [custom element name][] except that a dash is NOT + /// always mandatory and uppercase letters are accepted: + /// + /// ```ignore + /// tag ::= "<" tagName (whitespace)* "/>" | "<" tagName (whitespace)* ">" message "" + /// tagName ::= [a-z] (PENChar)* + /// PENChar ::= + /// "-" | "." | [0-9] | "_" | [a-z] | [A-Z] | #xB7 | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x37D] | + /// [#x37F-#x1FFF] | [#x200C-#x200D] | [#x203F-#x2040] | [#x2070-#x218F] | [#x2C00-#x2FEF] | + /// [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + /// ``` + /// + /// [custom element name]: https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + fn parse_tag(&self, nesting_level: usize, parent_arg_type: &str) -> Result { + let start_position = self.position(); + self.bump(); // '<' + + let tag_name = self.parse_tag_name(); + self.bump_space(); + + if self.bump_if("/>") { + // Self closing tag + Ok(AstElement::Literal { + value: format!("<{}/>", tag_name), + span: if self.options.capture_location { + Some(Span::new(start_position, self.position())) + } else { + None + }, + }) + } else if self.bump_if(">") { + let children = self.parse_message(nesting_level + 1, parent_arg_type, true)?; + + // Expecting a close tag + let end_tag_start_position = self.position(); + + if self.bump_if("") { + let span = Span::new(end_tag_start_position, self.position()); + return Err(self.error(ErrorKind::InvalidTag, span)); + } + + Ok(AstElement::Tag { + value: tag_name, + span: if self.options.capture_location { + Some(Span::new(start_position, self.position())) + } else { + None + }, + children: Box::new(children), + }) + } else { + Err(self.error( + ErrorKind::UnclosedTag, + Span::new(start_position, self.position()), + )) + } + } else { + Err(self.error( + ErrorKind::InvalidTag, + Span::new(start_position, self.position()), + )) + } + } + + fn parse_tag_name(&self) -> &str { + let start_offset = self.offset(); + + self.bump(); // the first tag name character + while !self.is_eof() && is_potential_element_name_char(self.char()) { + self.bump(); + } + + &self.message[start_offset..self.offset()] + } + + fn parse_literal(&self, nesting_level: usize, parent_arg_type: &str) -> Result { + let start = self.position(); + + let mut value = String::new(); + loop { + if self.bump_if("''") { + value.push('\''); + } else if let Some(fragment) = self.try_parse_quote(parent_arg_type) { + value.push_str(&fragment); + } else if let Some(fragment) = self.try_parse_unquoted(nesting_level, parent_arg_type) { + value.push(fragment); + } else if let Some(fragment) = self.try_parse_left_angle_bracket() { + value.push(fragment); + } else { + break; + } + } + + let span = Span::new(start, self.position()); + Ok(AstElement::Literal { + span: if self.options.capture_location { + Some(span) + } else { + None + }, + value, + }) + } + + /// Starting with ICU 4.8, an ASCII apostrophe only starts quoted text if it + /// immediately precedes a character that requires quoting (that is, + /// "only where needed"), and works the same in nested messages as on + /// the top level of the pattern. The new behavior is otherwise compatible. + fn try_parse_quote(&self, parent_arg_type: &str) -> Option { + if self.is_eof() || self.char() != '\'' { + return None; + } + + // Parse escaped char following the apostrophe, or early return if there is no + // escaped char. Check if is valid escaped character + match self.peek() { + Some('{') | Some('<') | Some('>') | Some('}') => (), + Some('#') if matches!(parent_arg_type, "plural" | "selectordinal") => (), + _ => { + return None; + } + } + + self.bump(); // apostrophe + let mut value = self.char().to_string(); // escaped char + self.bump(); + + // read chars until the optional closing apostrophe is found + loop { + if self.is_eof() { + break; + } + match self.char() { + '\'' if self.peek() == Some('\'') => { + value.push('\''); + // Bump one more time because we need to skip 2 characters. + self.bump(); + } + '\'' => { + // Optional closing apostrophe. + self.bump(); + break; + } + c => value.push(c), + } + self.bump(); + } + + Some(value) + } + + fn try_parse_unquoted(&self, nesting_level: usize, parent_arg_type: &str) -> Option { + if self.is_eof() { + return None; + } + match self.char() { + '<' | '{' => None, + '#' if parent_arg_type == "plural" || parent_arg_type == "selectordinal" => None, + '}' if nesting_level > 0 => None, + c => { + self.bump(); + Some(c) + } + } + } + + fn try_parse_left_angle_bracket(&self) -> Option { + if !self.is_eof() + && self.char() == '<' + && (self.options.ignore_tag + // If at the opening tag or closing tag position, bail. + || !(matches!(self.peek(), Some(c) if is_alpha(Some(c)) || c == '/'))) + { + self.bump(); // `<` + Some('<') + } else { + None + } + } + + fn parse_argument( + &self, + nesting_level: usize, + expecting_close_tag: bool, + ) -> Result { + let opening_brace_position = self.position(); + self.bump(); // `{` + + self.bump_space(); + + if self.is_eof() { + return Err(self.error( + ErrorKind::ExpectArgumentClosingBrace, + Span::new(opening_brace_position, self.position()), + )); + } + + if self.char() == '}' { + self.bump(); + return Err(self.error( + ErrorKind::EmptyArgument, + Span::new(opening_brace_position, self.position()), + )); + } + + // argument name + let value = self.parse_identifier_if_possible().0.to_string(); + if value.is_empty() { + return Err(self.error( + ErrorKind::MalformedArgument, + Span::new(opening_brace_position, self.position()), + )); + } + + self.bump_space(); + + if self.is_eof() { + return Err(self.error( + ErrorKind::ExpectArgumentClosingBrace, + Span::new(opening_brace_position, self.position()), + )); + } + + match self.char() { + // Simple argument: `{name}` + '}' => { + self.bump(); // `}` + + Ok(AstElement::Argument { + // value does not include the opening and closing braces. + value, + span: if self.options.capture_location { + Some(Span::new(opening_brace_position, self.position())) + } else { + None + }, + }) + } + + // Argument with options: `{name, format, ...}` + ',' => { + self.bump(); // ',' + self.bump_space(); + + if self.is_eof() { + return Err(self.error( + ErrorKind::ExpectArgumentClosingBrace, + Span::new(opening_brace_position, self.position()), + )); + } + + self.parse_argument_options( + nesting_level, + expecting_close_tag, + value, + opening_brace_position, + ) + } + + _ => Err(self.error( + ErrorKind::MalformedArgument, + Span::new(opening_brace_position, self.position()), + )), + } + } + + fn parse_argument_options( + &'s self, + nesting_level: usize, + expecting_close_tag: bool, + value: String, + opening_brace_position: Position, + ) -> Result> { + // Parse this range: + // {name, type, style} + // ^---^ + let type_starting_position = self.position(); + #[cfg(feature = "utf16")] + let arg_type_utf16 = self.parse_identifier_if_possible().0; + #[cfg(feature = "utf16")] + let arg_type = arg_type_utf16.to_string(); + #[cfg(feature = "utf16")] + let arg_type = arg_type.as_str(); + + #[cfg(not(feature = "utf16"))] + let arg_type = self.parse_identifier_if_possible().0; + let type_end_position = self.position(); + + match arg_type { + "" => { + // Expecting a style string number, date, time, plural, selectordinal, or + // select. + Err(self.error( + ErrorKind::ExpectArgumentType, + Span::new(type_starting_position, type_end_position), + )) + } + + "number" | "date" | "time" => { + // Parse this range: + // {name, number, style} + // ^-------^ + self.bump_space(); + + let style_and_span = if self.bump_if(",") { + self.bump_space(); + + let style_start_position = self.position(); + let style = self.parse_simple_arg_style_if_possible()?.trim_end(); + if style.is_empty() { + return Err(self.error( + ErrorKind::ExpectArgumentStyle, + Span::new(self.position(), self.position()), + )); + } + + let style_span = Span::new(style_start_position, self.position()); + Some((style, style_span)) + } else { + None + }; + + self.try_parse_argument_close(opening_brace_position)?; + let span = Span::new(opening_brace_position, self.position()); + + // Extract style or skeleton + if let Some((style, style_span)) = style_and_span { + if let Some(skeleton) = style.strip_prefix("::") { + // Skeleton starts with `::`. + let skeleton = skeleton.trim_start(); + + Ok(match arg_type { + "number" => { + let skeleton = parse_number_skeleton_from_string( + skeleton, + style_span, + self.options.should_parse_skeletons, + self.options.capture_location, + ) + .map_err(|kind| self.error(kind, style_span))?; + + AstElement::Number { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: Some(NumberArgStyle::Skeleton(Box::new(skeleton))), + } + } + _ => { + if skeleton.is_empty() { + return Err(self.error(ErrorKind::ExpectDateTimeSkeleton, span)); + } + + let pattern = if let Some(locale) = &self.options.locale { + get_best_pattern(skeleton, locale) + } else { + skeleton.to_string() + }; + + let parsed_options = if self.options.should_parse_skeletons { + parse_date_time_skeleton(&pattern) + } else { + Default::default() + }; + + let style = Some(DateTimeArgStyle::Skeleton(DateTimeSkeleton { + skeleton_type: SkeletonType::DateTime, + pattern, + location: if self.options.capture_location { + Some(style_span) + } else { + None + }, + parsed_options, + })); + if arg_type == "date" { + AstElement::Date { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style, + } + } else { + AstElement::Time { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style, + } + } + } + }) + } else { + // Regular style + Ok(match arg_type { + "number" => AstElement::Number { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: Some(NumberArgStyle::Style(style)), + }, + "date" => AstElement::Date { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: Some(DateTimeArgStyle::Style(style)), + }, + _ => AstElement::Time { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: Some(DateTimeArgStyle::Style(style)), + }, + }) + } + } else { + // No style + Ok(match arg_type { + "number" => AstElement::Number { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: None, + }, + "date" => AstElement::Date { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: None, + }, + _ => AstElement::Time { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + style: None, + }, + }) + } + } + + "plural" | "selectordinal" | "select" => { + // Parse this range: + // {name, plural, options} + // ^---------^ + let type_end_position = self.position(); + + self.bump_space(); + if !self.bump_if(",") { + return Err(self.error( + ErrorKind::ExpectSelectArgumentOptions, + Span::new(type_end_position, type_end_position), + )); + } + self.bump_space(); + + // Parse offset: + // {name, plural, offset:1, options} + // ^-----^ + // + // or the first option: + // + // {name, plural, one {...} other {...}} + // ^--^ + let mut identifier_and_span = self.parse_identifier_if_possible(); + + let plural_offset = if arg_type != "select" && identifier_and_span.0 == "offset" { + if !self.bump_if(":") { + return Err(self.error( + ErrorKind::ExpectPluralArgumentOffsetValue, + Span::new(self.position(), self.position()), + )); + } + self.bump_space(); + let offset = self.try_parse_decimal_integer( + ErrorKind::ExpectPluralArgumentOffsetValue, + ErrorKind::InvalidPluralArgumentOffsetValue, + )?; + + // Parse another identifier for option parsing + self.bump_space(); + identifier_and_span = self.parse_identifier_if_possible(); + + offset + } else { + 0 + }; + + #[cfg(feature = "utf16")] + let options = self.try_parse_plural_or_select_options( + nesting_level, + arg_type_utf16, + expecting_close_tag, + identifier_and_span, + )?; + #[cfg(not(feature = "utf16"))] + let options = self.try_parse_plural_or_select_options( + nesting_level, + arg_type, + expecting_close_tag, + identifier_and_span, + )?; + self.try_parse_argument_close(opening_brace_position)?; + + let span = Span::new(opening_brace_position, self.position()); + match arg_type { + "select" => Ok(AstElement::Select { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + options, + }), + _ => Ok(AstElement::Plural { + value, + span: if self.options.capture_location { + Some(span) + } else { + None + }, + options, + offset: plural_offset, + plural_type: if arg_type == "plural" { + PluralType::Cardinal + } else { + PluralType::Ordinal + }, + }), + } + } + + _ => Err(self.error( + ErrorKind::InvalidArgumentType, + Span::new(type_starting_position, type_end_position), + )), + } + } + + /// * `nesting_level` - the current nesting level of messages. This can be + /// positive when parsing message fragment in select or plural argument + /// options. + /// * `parent_arg_type` - the parent argument's type. + /// * `parsed_first_identifier` - if provided, this is the first + /// identifier-like selector of the argument. It is a by-product of a + /// previous parsing attempt. + /// * `expecting_close_tag` - If true, this message is directly or + /// indirectly nested inside between a pair of opening and closing tags. + /// The nested message will not parse beyond the closing tag boundary. /// + fn try_parse_plural_or_select_options( + &'s self, + nesting_level: usize, + #[cfg(feature = "utf16")] parent_arg_type: &'s Utf16Str, + #[cfg(not(feature = "utf16"))] parent_arg_type: &'s str, + expecting_close_tag: bool, + #[cfg(feature = "utf16")] parsed_first_identifier: (&'s Utf16Str, Span), + #[cfg(not(feature = "utf16"))] parsed_first_identifier: (&'s str, Span), + ) -> Result> { + let mut has_other_clause = false; + + let mut options = vec![]; + let mut selectors_parsed = HashSet::new(); + let (mut selector, mut selector_span) = parsed_first_identifier; + // Parse: + // one {one apple} + // ^--^ + loop { + if selector.is_empty() { + let start_position = self.position(); + if parent_arg_type != "select" && self.bump_if("=") { + // Try parse `={number}` selector + self.try_parse_decimal_integer( + ErrorKind::ExpectPluralArgumentSelector, + ErrorKind::InvalidPluralArgumentSelector, + )?; + selector_span = Span::new(start_position, self.position()); + #[cfg(feature = "utf16")] + { + selector = &self.message_utf16[start_position.offset..self.offset()]; + } + #[cfg(not(feature = "utf16"))] + { + selector = &self.message[start_position.offset..self.offset()]; + } + } else { + // TODO: check to make sure that the plural category is valid. + break; + } + } + + // Duplicate selector clauses + if selectors_parsed.contains(selector) { + return Err(self.error( + if parent_arg_type == "select" { + ErrorKind::DuplicateSelectArgumentSelector + } else { + ErrorKind::DuplicatePluralArgumentSelector + }, + selector_span, + )); + } + + if selector == "other" { + has_other_clause = true; + } + + // Parse: + // one {one apple} + // ^----------^ + self.bump_space(); + let opening_brace_position = self.position(); + if !self.bump_if("{") { + return Err(self.error( + if parent_arg_type == "select" { + ErrorKind::ExpectSelectArgumentSelectorFragment + } else { + ErrorKind::ExpectPluralArgumentSelectorFragment + }, + Span::new(self.position(), self.position()), + )); + } + + let fragment = self.parse_message( + nesting_level + 1, + parent_arg_type.to_string().as_str(), + expecting_close_tag, + )?; + self.try_parse_argument_close(opening_brace_position)?; + + options.push(( + selector, + PluralOrSelectOption { + value: fragment, + location: if self.options.capture_location { + Some(Span::new(opening_brace_position, self.position())) + } else { + None + }, + }, + )); + // Keep track of the existing selectors + selectors_parsed.insert(selector); + + // Prep next selector clause. + self.bump_space(); + // 🤷‍♂️ Destructure assignment is NOT yet supported by Rust. + let _identifier_and_span = self.parse_identifier_if_possible(); + selector = _identifier_and_span.0; + selector_span = _identifier_and_span.1; + } + + if options.is_empty() { + return Err(self.error( + match parent_arg_type.to_string().as_str() { + "select" => ErrorKind::ExpectSelectArgumentSelector, + _ => ErrorKind::ExpectPluralArgumentSelector, + }, + Span::new(self.position(), self.position()), + )); + } + + if self.options.requires_other_clause && !has_other_clause { + return Err(self.error( + ErrorKind::MissingOtherClause, + Span::new(self.position(), self.position()), + )); + } + + Ok(PluralOrSelectOptions(options)) + } + + fn try_parse_decimal_integer( + &self, + expect_number_error: ErrorKind, + invalid_number_error: ErrorKind, + ) -> Result { + let mut sign = 1; + let start_position = self.position(); + + if self.bump_if("+") { + } else if self.bump_if("-") { + sign = -1; + } + + let mut digits = String::new(); + while !self.is_eof() && self.char().is_ascii_digit() { + digits.push(self.char()); + self.bump(); + } + + let span = Span::new(start_position, self.position()); + + if self.is_eof() { + return Err(self.error(expect_number_error, span)); + } + + digits + .parse::() + .map(|x| x * sign) + .map_err(|_| self.error(invalid_number_error, span)) + } + + /// See: https://github.com/unicode-org/icu/blob/af7ed1f6d2298013dc303628438ec4abe1f16479/icu4c/source/common/messagepattern.cpp#L659 + fn parse_simple_arg_style_if_possible(&self) -> Result<&str> { + let mut nested_braces = 0; + + let start_position = self.position(); + while !self.is_eof() { + match self.char() { + '\'' => { + // Treat apostrophe as quoting but include it in the style part. + // Find the end of the quoted literal text. + self.bump(); + let apostrophe_position = self.position(); + if !self.bump_until('\'') { + return Err(self.error( + ErrorKind::UnclosedQuoteInArgumentStyle, + Span::new(apostrophe_position, self.position()), + )); + } + self.bump(); + } + '{' => { + nested_braces += 1; + self.bump(); + } + '}' => { + if nested_braces > 0 { + nested_braces -= 1; + } else { + break; + } + } + _ => { + self.bump(); + } + } + } + + Ok(&self.message[start_position.offset..self.offset()]) + } + + fn try_parse_argument_close(&self, opening_brace_position: Position) -> Result<()> { + // Parse: {value, number, ::currency/GBP } + // ^^ + if self.is_eof() { + return Err(self.error( + ErrorKind::ExpectArgumentClosingBrace, + Span::new(opening_brace_position, self.position()), + )); + } + + if self.char() != '}' { + return Err(self.error( + ErrorKind::ExpectArgumentClosingBrace, + Span::new(opening_brace_position, self.position()), + )); + } + self.bump(); // `}` + + Ok(()) + } + + fn parse_identifier_if_possible_inner(&self) -> Span { + let starting_position = self.position(); + + while !self.is_eof() && !is_whitespace(self.char()) && !is_pattern_syntax(self.char()) { + self.bump(); + } + + let end_position = self.position(); + Span::new(starting_position, end_position) + } + + /// Advance the parser until the end of the identifier, if it is currently + /// on an identifier character. Return an empty string otherwise. + #[cfg(feature = "utf16")] + fn parse_identifier_if_possible(&self) -> (&Utf16Str, Span) { + let span = self.parse_identifier_if_possible_inner(); + ( + &self.message_utf16[span.start.offset..span.end.offset], + span, + ) + } + + #[cfg(not(feature = "utf16"))] + fn parse_identifier_if_possible(&self) -> (&str, Span) { + let span = self.parse_identifier_if_possible_inner(); + (&self.message[span.start.offset..span.end.offset], span) + } + + fn error(&self, kind: ErrorKind, span: Span) -> ast::Error { + ast::Error { + kind, + message: self.message.to_string(), + location: if self.options.capture_location { + Some(span) + } else { + None + }, + } + } + + fn offset(&self) -> usize { + self.position().offset + } + + /// Return the character at the current position of the parser. + /// + /// This panics if the current position does not point to a valid char. + fn char(&self) -> char { + self.char_at(self.offset()) + } + + /// Return the character at the given position. + /// + /// This panics if the given position does not point to a valid char. + fn char_at(&self, i: usize) -> char { + #[cfg(feature = "utf16")] + let message = &self.message_utf16[i..].to_string(); + #[cfg(feature = "utf16")] + let message = message.as_str(); + + #[cfg(not(feature = "utf16"))] + let message = &self.message[i..]; + + message + .chars() + .next() + .unwrap_or_else(|| panic!("expected char at offset {}", i)) + } + + /// Bump the parser to the next Unicode scalar value. + fn bump(&self) { + if self.is_eof() { + return; + } + let Position { + mut offset, + mut line, + mut column, + } = self.position(); + let ch = self.char(); + if ch == '\n' { + line = line.checked_add(1).unwrap(); + column = 1; + } else { + column = column.checked_add(1).unwrap(); + } + + #[cfg(feature = "utf16")] + { + offset += ch.len_utf16(); + } + #[cfg(not(feature = "utf16"))] + { + offset += ch.len_utf8(); + } + self.position.set(Position { + offset, + line, + column, + }); + } + + /// Bump the parser to the target offset. + /// + /// If target offset is beyond the end of the input, bump the parser to the + /// end of the input. + fn bump_to(&self, target_offset: usize) { + assert!( + self.offset() <= target_offset, + "target_offset {} must be greater than the current offset {})", + target_offset, + self.offset() + ); + + let target_offset = cmp::min(target_offset, self.message.len()); + loop { + let offset = self.offset(); + + if self.offset() == target_offset { + break; + } + assert!( + offset < target_offset, + "target_offset is at invalid unicode byte boundary: {}", + target_offset + ); + + self.bump(); + if self.is_eof() { + break; + } + } + } + + /// If the substring starting at the current position of the parser has + /// the given prefix, then bump the parser to the character immediately + /// following the prefix and return true. Otherwise, don't bump the parser + /// and return false. + fn bump_if(&self, prefix: &str) -> bool { + #[cfg(feature = "utf16")] + let message = &self.message_utf16[self.offset()..].to_string(); + #[cfg(feature = "utf16")] + let message = message.as_str(); + + #[cfg(not(feature = "utf16"))] + let message = &self.message[self.offset()..]; + + if message.starts_with(prefix) { + for _ in 0..prefix.chars().count() { + self.bump(); + } + true + } else { + false + } + } + + /// Bump the parser until the pattern character is found and return `true`. + /// Otherwise bump to the end of the file and return `false`. + fn bump_until(&self, pattern: char) -> bool { + let current_offset = self.offset(); + if let Some(delta) = self.message[current_offset..].find(pattern) { + self.bump_to(current_offset + delta); + true + } else { + self.bump_to(self.message.len()); + false + } + } + + /// advance the parser through all whitespace to the next non-whitespace + /// byte. + fn bump_space(&self) { + while !self.is_eof() && is_whitespace(self.char()) { + self.bump(); + } + } + + /// Peek at the *next* character in the input without advancing the parser. + /// + /// If the input has been exhausted, then this returns `None`. + fn peek(&self) -> Option { + if self.is_eof() { + return None; + } + self.message[self.offset() + self.char().len_utf8()..] + .chars() + .next() + } + + /// Returns true if the next call to `bump` would return false. + fn is_eof(&self) -> bool { + #[cfg(feature = "utf16")] + return self.offset() == self.message_utf16.len(); + + #[cfg(not(feature = "utf16"))] + return self.offset() == self.message.len(); + } +} + +fn parse_number_skeleton_from_string( + skeleton: &str, + span: Span, + should_parse_skeleton: bool, + should_capture_location: bool, +) -> std::result::Result { + if skeleton.is_empty() { + return Err(ErrorKind::InvalidNumberSkeleton); + } + // Parse the skeleton + let tokens: std::result::Result, _> = skeleton + .split(char::is_whitespace) + .filter(|x| !x.is_empty()) + .map(|token| { + let mut stem_and_options = token.split('/'); + if let Some(stem) = stem_and_options.next() { + let options: std::result::Result, _> = stem_and_options + .map(|option| { + // Token option cannot be empty + if option.is_empty() { + Err(ErrorKind::InvalidNumberSkeleton) + } else { + Ok(option) + } + }) + .collect(); + Ok(NumberSkeletonToken { + stem, + options: options?, + }) + } else { + Err(ErrorKind::InvalidNumberSkeleton) + } + }) + .collect(); + + let tokens = tokens?; + let parsed_options = if should_parse_skeleton { + parse_number_skeleton(&tokens) + } else { + Default::default() + }; + + Ok(NumberSkeleton { + skeleton_type: SkeletonType::Number, + tokens, + // TODO: use trimmed end position + location: if should_capture_location { + Some(span) + } else { + None + }, + parsed_options, + }) +} + +fn is_potential_element_name_char(ch: char) -> bool { + matches!(ch, '-' + | '.' + | '0'..='9' + | '_' + | 'a'..='z' + | 'A'..='Z' + | '\u{B7}' + | '\u{C0}'..='\u{D6}' + | '\u{D8}'..='\u{F6}' + | '\u{F8}'..='\u{37D}' + | '\u{37F}'..='\u{1FFF}' + | '\u{200C}'..='\u{200D}' + | '\u{203F}'..='\u{2040}' + | '\u{2070}'..='\u{218F}' + | '\u{2C00}'..='\u{2FEF}' + | '\u{3001}'..='\u{D7FF}' + | '\u{F900}'..='\u{FDCF}' + | '\u{FDF0}'..='\u{FFFD}' + | '\u{10000}'..='\u{EFFFF}') +} diff --git a/crates/swc_icu_messageformat_parser/src/pattern_syntax.rs b/crates/swc_icu_messageformat_parser/src/pattern_syntax.rs new file mode 100644 index 000000000..d95219744 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/src/pattern_syntax.rs @@ -0,0 +1,217 @@ +/// See https://github.com/unicode-org/icu/blob/d1dcb6931884dcf4b8b9a88fa17d19159a95a04c/icu4c/source/common/patternprops.cpp#L119 +pub fn is_pattern_syntax(c: char) -> bool { + PATTERN_SYNTAX_CODE_POINTS + .binary_search(&(c as u32)) + .is_ok() +} + +/// See https://github.com/node-unicode/unicode-13.0.0/blob/dbbbf9d0b97b5181cdad6c928dec838df387b794/Binary_Property/Pattern_Syntax/code-points.js +const PATTERN_SYNTAX_CODE_POINTS: &[u32] = &[ + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, + 93, 94, 96, 123, 124, 125, 126, 161, 162, 163, 164, 165, 166, 167, 169, 171, 172, 174, 176, + 177, 182, 187, 191, 215, 247, 8208, 8209, 8210, 8211, 8212, 8213, 8214, 8215, 8216, 8217, 8218, + 8219, 8220, 8221, 8222, 8223, 8224, 8225, 8226, 8227, 8228, 8229, 8230, 8231, 8240, 8241, 8242, + 8243, 8244, 8245, 8246, 8247, 8248, 8249, 8250, 8251, 8252, 8253, 8254, 8257, 8258, 8259, 8260, + 8261, 8262, 8263, 8264, 8265, 8266, 8267, 8268, 8269, 8270, 8271, 8272, 8273, 8274, 8275, 8277, + 8278, 8279, 8280, 8281, 8282, 8283, 8284, 8285, 8286, 8592, 8593, 8594, 8595, 8596, 8597, 8598, + 8599, 8600, 8601, 8602, 8603, 8604, 8605, 8606, 8607, 8608, 8609, 8610, 8611, 8612, 8613, 8614, + 8615, 8616, 8617, 8618, 8619, 8620, 8621, 8622, 8623, 8624, 8625, 8626, 8627, 8628, 8629, 8630, + 8631, 8632, 8633, 8634, 8635, 8636, 8637, 8638, 8639, 8640, 8641, 8642, 8643, 8644, 8645, 8646, + 8647, 8648, 8649, 8650, 8651, 8652, 8653, 8654, 8655, 8656, 8657, 8658, 8659, 8660, 8661, 8662, + 8663, 8664, 8665, 8666, 8667, 8668, 8669, 8670, 8671, 8672, 8673, 8674, 8675, 8676, 8677, 8678, + 8679, 8680, 8681, 8682, 8683, 8684, 8685, 8686, 8687, 8688, 8689, 8690, 8691, 8692, 8693, 8694, + 8695, 8696, 8697, 8698, 8699, 8700, 8701, 8702, 8703, 8704, 8705, 8706, 8707, 8708, 8709, 8710, + 8711, 8712, 8713, 8714, 8715, 8716, 8717, 8718, 8719, 8720, 8721, 8722, 8723, 8724, 8725, 8726, + 8727, 8728, 8729, 8730, 8731, 8732, 8733, 8734, 8735, 8736, 8737, 8738, 8739, 8740, 8741, 8742, + 8743, 8744, 8745, 8746, 8747, 8748, 8749, 8750, 8751, 8752, 8753, 8754, 8755, 8756, 8757, 8758, + 8759, 8760, 8761, 8762, 8763, 8764, 8765, 8766, 8767, 8768, 8769, 8770, 8771, 8772, 8773, 8774, + 8775, 8776, 8777, 8778, 8779, 8780, 8781, 8782, 8783, 8784, 8785, 8786, 8787, 8788, 8789, 8790, + 8791, 8792, 8793, 8794, 8795, 8796, 8797, 8798, 8799, 8800, 8801, 8802, 8803, 8804, 8805, 8806, + 8807, 8808, 8809, 8810, 8811, 8812, 8813, 8814, 8815, 8816, 8817, 8818, 8819, 8820, 8821, 8822, + 8823, 8824, 8825, 8826, 8827, 8828, 8829, 8830, 8831, 8832, 8833, 8834, 8835, 8836, 8837, 8838, + 8839, 8840, 8841, 8842, 8843, 8844, 8845, 8846, 8847, 8848, 8849, 8850, 8851, 8852, 8853, 8854, + 8855, 8856, 8857, 8858, 8859, 8860, 8861, 8862, 8863, 8864, 8865, 8866, 8867, 8868, 8869, 8870, + 8871, 8872, 8873, 8874, 8875, 8876, 8877, 8878, 8879, 8880, 8881, 8882, 8883, 8884, 8885, 8886, + 8887, 8888, 8889, 8890, 8891, 8892, 8893, 8894, 8895, 8896, 8897, 8898, 8899, 8900, 8901, 8902, + 8903, 8904, 8905, 8906, 8907, 8908, 8909, 8910, 8911, 8912, 8913, 8914, 8915, 8916, 8917, 8918, + 8919, 8920, 8921, 8922, 8923, 8924, 8925, 8926, 8927, 8928, 8929, 8930, 8931, 8932, 8933, 8934, + 8935, 8936, 8937, 8938, 8939, 8940, 8941, 8942, 8943, 8944, 8945, 8946, 8947, 8948, 8949, 8950, + 8951, 8952, 8953, 8954, 8955, 8956, 8957, 8958, 8959, 8960, 8961, 8962, 8963, 8964, 8965, 8966, + 8967, 8968, 8969, 8970, 8971, 8972, 8973, 8974, 8975, 8976, 8977, 8978, 8979, 8980, 8981, 8982, + 8983, 8984, 8985, 8986, 8987, 8988, 8989, 8990, 8991, 8992, 8993, 8994, 8995, 8996, 8997, 8998, + 8999, 9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, + 9015, 9016, 9017, 9018, 9019, 9020, 9021, 9022, 9023, 9024, 9025, 9026, 9027, 9028, 9029, 9030, + 9031, 9032, 9033, 9034, 9035, 9036, 9037, 9038, 9039, 9040, 9041, 9042, 9043, 9044, 9045, 9046, + 9047, 9048, 9049, 9050, 9051, 9052, 9053, 9054, 9055, 9056, 9057, 9058, 9059, 9060, 9061, 9062, + 9063, 9064, 9065, 9066, 9067, 9068, 9069, 9070, 9071, 9072, 9073, 9074, 9075, 9076, 9077, 9078, + 9079, 9080, 9081, 9082, 9083, 9084, 9085, 9086, 9087, 9088, 9089, 9090, 9091, 9092, 9093, 9094, + 9095, 9096, 9097, 9098, 9099, 9100, 9101, 9102, 9103, 9104, 9105, 9106, 9107, 9108, 9109, 9110, + 9111, 9112, 9113, 9114, 9115, 9116, 9117, 9118, 9119, 9120, 9121, 9122, 9123, 9124, 9125, 9126, + 9127, 9128, 9129, 9130, 9131, 9132, 9133, 9134, 9135, 9136, 9137, 9138, 9139, 9140, 9141, 9142, + 9143, 9144, 9145, 9146, 9147, 9148, 9149, 9150, 9151, 9152, 9153, 9154, 9155, 9156, 9157, 9158, + 9159, 9160, 9161, 9162, 9163, 9164, 9165, 9166, 9167, 9168, 9169, 9170, 9171, 9172, 9173, 9174, + 9175, 9176, 9177, 9178, 9179, 9180, 9181, 9182, 9183, 9184, 9185, 9186, 9187, 9188, 9189, 9190, + 9191, 9192, 9193, 9194, 9195, 9196, 9197, 9198, 9199, 9200, 9201, 9202, 9203, 9204, 9205, 9206, + 9207, 9208, 9209, 9210, 9211, 9212, 9213, 9214, 9215, 9216, 9217, 9218, 9219, 9220, 9221, 9222, + 9223, 9224, 9225, 9226, 9227, 9228, 9229, 9230, 9231, 9232, 9233, 9234, 9235, 9236, 9237, 9238, + 9239, 9240, 9241, 9242, 9243, 9244, 9245, 9246, 9247, 9248, 9249, 9250, 9251, 9252, 9253, 9254, + 9255, 9256, 9257, 9258, 9259, 9260, 9261, 9262, 9263, 9264, 9265, 9266, 9267, 9268, 9269, 9270, + 9271, 9272, 9273, 9274, 9275, 9276, 9277, 9278, 9279, 9280, 9281, 9282, 9283, 9284, 9285, 9286, + 9287, 9288, 9289, 9290, 9291, 9292, 9293, 9294, 9295, 9296, 9297, 9298, 9299, 9300, 9301, 9302, + 9303, 9304, 9305, 9306, 9307, 9308, 9309, 9310, 9311, 9472, 9473, 9474, 9475, 9476, 9477, 9478, + 9479, 9480, 9481, 9482, 9483, 9484, 9485, 9486, 9487, 9488, 9489, 9490, 9491, 9492, 9493, 9494, + 9495, 9496, 9497, 9498, 9499, 9500, 9501, 9502, 9503, 9504, 9505, 9506, 9507, 9508, 9509, 9510, + 9511, 9512, 9513, 9514, 9515, 9516, 9517, 9518, 9519, 9520, 9521, 9522, 9523, 9524, 9525, 9526, + 9527, 9528, 9529, 9530, 9531, 9532, 9533, 9534, 9535, 9536, 9537, 9538, 9539, 9540, 9541, 9542, + 9543, 9544, 9545, 9546, 9547, 9548, 9549, 9550, 9551, 9552, 9553, 9554, 9555, 9556, 9557, 9558, + 9559, 9560, 9561, 9562, 9563, 9564, 9565, 9566, 9567, 9568, 9569, 9570, 9571, 9572, 9573, 9574, + 9575, 9576, 9577, 9578, 9579, 9580, 9581, 9582, 9583, 9584, 9585, 9586, 9587, 9588, 9589, 9590, + 9591, 9592, 9593, 9594, 9595, 9596, 9597, 9598, 9599, 9600, 9601, 9602, 9603, 9604, 9605, 9606, + 9607, 9608, 9609, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, + 9623, 9624, 9625, 9626, 9627, 9628, 9629, 9630, 9631, 9632, 9633, 9634, 9635, 9636, 9637, 9638, + 9639, 9640, 9641, 9642, 9643, 9644, 9645, 9646, 9647, 9648, 9649, 9650, 9651, 9652, 9653, 9654, + 9655, 9656, 9657, 9658, 9659, 9660, 9661, 9662, 9663, 9664, 9665, 9666, 9667, 9668, 9669, 9670, + 9671, 9672, 9673, 9674, 9675, 9676, 9677, 9678, 9679, 9680, 9681, 9682, 9683, 9684, 9685, 9686, + 9687, 9688, 9689, 9690, 9691, 9692, 9693, 9694, 9695, 9696, 9697, 9698, 9699, 9700, 9701, 9702, + 9703, 9704, 9705, 9706, 9707, 9708, 9709, 9710, 9711, 9712, 9713, 9714, 9715, 9716, 9717, 9718, + 9719, 9720, 9721, 9722, 9723, 9724, 9725, 9726, 9727, 9728, 9729, 9730, 9731, 9732, 9733, 9734, + 9735, 9736, 9737, 9738, 9739, 9740, 9741, 9742, 9743, 9744, 9745, 9746, 9747, 9748, 9749, 9750, + 9751, 9752, 9753, 9754, 9755, 9756, 9757, 9758, 9759, 9760, 9761, 9762, 9763, 9764, 9765, 9766, + 9767, 9768, 9769, 9770, 9771, 9772, 9773, 9774, 9775, 9776, 9777, 9778, 9779, 9780, 9781, 9782, + 9783, 9784, 9785, 9786, 9787, 9788, 9789, 9790, 9791, 9792, 9793, 9794, 9795, 9796, 9797, 9798, + 9799, 9800, 9801, 9802, 9803, 9804, 9805, 9806, 9807, 9808, 9809, 9810, 9811, 9812, 9813, 9814, + 9815, 9816, 9817, 9818, 9819, 9820, 9821, 9822, 9823, 9824, 9825, 9826, 9827, 9828, 9829, 9830, + 9831, 9832, 9833, 9834, 9835, 9836, 9837, 9838, 9839, 9840, 9841, 9842, 9843, 9844, 9845, 9846, + 9847, 9848, 9849, 9850, 9851, 9852, 9853, 9854, 9855, 9856, 9857, 9858, 9859, 9860, 9861, 9862, + 9863, 9864, 9865, 9866, 9867, 9868, 9869, 9870, 9871, 9872, 9873, 9874, 9875, 9876, 9877, 9878, + 9879, 9880, 9881, 9882, 9883, 9884, 9885, 9886, 9887, 9888, 9889, 9890, 9891, 9892, 9893, 9894, + 9895, 9896, 9897, 9898, 9899, 9900, 9901, 9902, 9903, 9904, 9905, 9906, 9907, 9908, 9909, 9910, + 9911, 9912, 9913, 9914, 9915, 9916, 9917, 9918, 9919, 9920, 9921, 9922, 9923, 9924, 9925, 9926, + 9927, 9928, 9929, 9930, 9931, 9932, 9933, 9934, 9935, 9936, 9937, 9938, 9939, 9940, 9941, 9942, + 9943, 9944, 9945, 9946, 9947, 9948, 9949, 9950, 9951, 9952, 9953, 9954, 9955, 9956, 9957, 9958, + 9959, 9960, 9961, 9962, 9963, 9964, 9965, 9966, 9967, 9968, 9969, 9970, 9971, 9972, 9973, 9974, + 9975, 9976, 9977, 9978, 9979, 9980, 9981, 9982, 9983, 9984, 9985, 9986, 9987, 9988, 9989, 9990, + 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999, 10000, 10001, 10002, 10003, 10004, 10005, + 10006, 10007, 10008, 10009, 10010, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10018, + 10019, 10020, 10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029, 10030, 10031, + 10032, 10033, 10034, 10035, 10036, 10037, 10038, 10039, 10040, 10041, 10042, 10043, 10044, + 10045, 10046, 10047, 10048, 10049, 10050, 10051, 10052, 10053, 10054, 10055, 10056, 10057, + 10058, 10059, 10060, 10061, 10062, 10063, 10064, 10065, 10066, 10067, 10068, 10069, 10070, + 10071, 10072, 10073, 10074, 10075, 10076, 10077, 10078, 10079, 10080, 10081, 10082, 10083, + 10084, 10085, 10086, 10087, 10088, 10089, 10090, 10091, 10092, 10093, 10094, 10095, 10096, + 10097, 10098, 10099, 10100, 10101, 10132, 10133, 10134, 10135, 10136, 10137, 10138, 10139, + 10140, 10141, 10142, 10143, 10144, 10145, 10146, 10147, 10148, 10149, 10150, 10151, 10152, + 10153, 10154, 10155, 10156, 10157, 10158, 10159, 10160, 10161, 10162, 10163, 10164, 10165, + 10166, 10167, 10168, 10169, 10170, 10171, 10172, 10173, 10174, 10175, 10176, 10177, 10178, + 10179, 10180, 10181, 10182, 10183, 10184, 10185, 10186, 10187, 10188, 10189, 10190, 10191, + 10192, 10193, 10194, 10195, 10196, 10197, 10198, 10199, 10200, 10201, 10202, 10203, 10204, + 10205, 10206, 10207, 10208, 10209, 10210, 10211, 10212, 10213, 10214, 10215, 10216, 10217, + 10218, 10219, 10220, 10221, 10222, 10223, 10224, 10225, 10226, 10227, 10228, 10229, 10230, + 10231, 10232, 10233, 10234, 10235, 10236, 10237, 10238, 10239, 10240, 10241, 10242, 10243, + 10244, 10245, 10246, 10247, 10248, 10249, 10250, 10251, 10252, 10253, 10254, 10255, 10256, + 10257, 10258, 10259, 10260, 10261, 10262, 10263, 10264, 10265, 10266, 10267, 10268, 10269, + 10270, 10271, 10272, 10273, 10274, 10275, 10276, 10277, 10278, 10279, 10280, 10281, 10282, + 10283, 10284, 10285, 10286, 10287, 10288, 10289, 10290, 10291, 10292, 10293, 10294, 10295, + 10296, 10297, 10298, 10299, 10300, 10301, 10302, 10303, 10304, 10305, 10306, 10307, 10308, + 10309, 10310, 10311, 10312, 10313, 10314, 10315, 10316, 10317, 10318, 10319, 10320, 10321, + 10322, 10323, 10324, 10325, 10326, 10327, 10328, 10329, 10330, 10331, 10332, 10333, 10334, + 10335, 10336, 10337, 10338, 10339, 10340, 10341, 10342, 10343, 10344, 10345, 10346, 10347, + 10348, 10349, 10350, 10351, 10352, 10353, 10354, 10355, 10356, 10357, 10358, 10359, 10360, + 10361, 10362, 10363, 10364, 10365, 10366, 10367, 10368, 10369, 10370, 10371, 10372, 10373, + 10374, 10375, 10376, 10377, 10378, 10379, 10380, 10381, 10382, 10383, 10384, 10385, 10386, + 10387, 10388, 10389, 10390, 10391, 10392, 10393, 10394, 10395, 10396, 10397, 10398, 10399, + 10400, 10401, 10402, 10403, 10404, 10405, 10406, 10407, 10408, 10409, 10410, 10411, 10412, + 10413, 10414, 10415, 10416, 10417, 10418, 10419, 10420, 10421, 10422, 10423, 10424, 10425, + 10426, 10427, 10428, 10429, 10430, 10431, 10432, 10433, 10434, 10435, 10436, 10437, 10438, + 10439, 10440, 10441, 10442, 10443, 10444, 10445, 10446, 10447, 10448, 10449, 10450, 10451, + 10452, 10453, 10454, 10455, 10456, 10457, 10458, 10459, 10460, 10461, 10462, 10463, 10464, + 10465, 10466, 10467, 10468, 10469, 10470, 10471, 10472, 10473, 10474, 10475, 10476, 10477, + 10478, 10479, 10480, 10481, 10482, 10483, 10484, 10485, 10486, 10487, 10488, 10489, 10490, + 10491, 10492, 10493, 10494, 10495, 10496, 10497, 10498, 10499, 10500, 10501, 10502, 10503, + 10504, 10505, 10506, 10507, 10508, 10509, 10510, 10511, 10512, 10513, 10514, 10515, 10516, + 10517, 10518, 10519, 10520, 10521, 10522, 10523, 10524, 10525, 10526, 10527, 10528, 10529, + 10530, 10531, 10532, 10533, 10534, 10535, 10536, 10537, 10538, 10539, 10540, 10541, 10542, + 10543, 10544, 10545, 10546, 10547, 10548, 10549, 10550, 10551, 10552, 10553, 10554, 10555, + 10556, 10557, 10558, 10559, 10560, 10561, 10562, 10563, 10564, 10565, 10566, 10567, 10568, + 10569, 10570, 10571, 10572, 10573, 10574, 10575, 10576, 10577, 10578, 10579, 10580, 10581, + 10582, 10583, 10584, 10585, 10586, 10587, 10588, 10589, 10590, 10591, 10592, 10593, 10594, + 10595, 10596, 10597, 10598, 10599, 10600, 10601, 10602, 10603, 10604, 10605, 10606, 10607, + 10608, 10609, 10610, 10611, 10612, 10613, 10614, 10615, 10616, 10617, 10618, 10619, 10620, + 10621, 10622, 10623, 10624, 10625, 10626, 10627, 10628, 10629, 10630, 10631, 10632, 10633, + 10634, 10635, 10636, 10637, 10638, 10639, 10640, 10641, 10642, 10643, 10644, 10645, 10646, + 10647, 10648, 10649, 10650, 10651, 10652, 10653, 10654, 10655, 10656, 10657, 10658, 10659, + 10660, 10661, 10662, 10663, 10664, 10665, 10666, 10667, 10668, 10669, 10670, 10671, 10672, + 10673, 10674, 10675, 10676, 10677, 10678, 10679, 10680, 10681, 10682, 10683, 10684, 10685, + 10686, 10687, 10688, 10689, 10690, 10691, 10692, 10693, 10694, 10695, 10696, 10697, 10698, + 10699, 10700, 10701, 10702, 10703, 10704, 10705, 10706, 10707, 10708, 10709, 10710, 10711, + 10712, 10713, 10714, 10715, 10716, 10717, 10718, 10719, 10720, 10721, 10722, 10723, 10724, + 10725, 10726, 10727, 10728, 10729, 10730, 10731, 10732, 10733, 10734, 10735, 10736, 10737, + 10738, 10739, 10740, 10741, 10742, 10743, 10744, 10745, 10746, 10747, 10748, 10749, 10750, + 10751, 10752, 10753, 10754, 10755, 10756, 10757, 10758, 10759, 10760, 10761, 10762, 10763, + 10764, 10765, 10766, 10767, 10768, 10769, 10770, 10771, 10772, 10773, 10774, 10775, 10776, + 10777, 10778, 10779, 10780, 10781, 10782, 10783, 10784, 10785, 10786, 10787, 10788, 10789, + 10790, 10791, 10792, 10793, 10794, 10795, 10796, 10797, 10798, 10799, 10800, 10801, 10802, + 10803, 10804, 10805, 10806, 10807, 10808, 10809, 10810, 10811, 10812, 10813, 10814, 10815, + 10816, 10817, 10818, 10819, 10820, 10821, 10822, 10823, 10824, 10825, 10826, 10827, 10828, + 10829, 10830, 10831, 10832, 10833, 10834, 10835, 10836, 10837, 10838, 10839, 10840, 10841, + 10842, 10843, 10844, 10845, 10846, 10847, 10848, 10849, 10850, 10851, 10852, 10853, 10854, + 10855, 10856, 10857, 10858, 10859, 10860, 10861, 10862, 10863, 10864, 10865, 10866, 10867, + 10868, 10869, 10870, 10871, 10872, 10873, 10874, 10875, 10876, 10877, 10878, 10879, 10880, + 10881, 10882, 10883, 10884, 10885, 10886, 10887, 10888, 10889, 10890, 10891, 10892, 10893, + 10894, 10895, 10896, 10897, 10898, 10899, 10900, 10901, 10902, 10903, 10904, 10905, 10906, + 10907, 10908, 10909, 10910, 10911, 10912, 10913, 10914, 10915, 10916, 10917, 10918, 10919, + 10920, 10921, 10922, 10923, 10924, 10925, 10926, 10927, 10928, 10929, 10930, 10931, 10932, + 10933, 10934, 10935, 10936, 10937, 10938, 10939, 10940, 10941, 10942, 10943, 10944, 10945, + 10946, 10947, 10948, 10949, 10950, 10951, 10952, 10953, 10954, 10955, 10956, 10957, 10958, + 10959, 10960, 10961, 10962, 10963, 10964, 10965, 10966, 10967, 10968, 10969, 10970, 10971, + 10972, 10973, 10974, 10975, 10976, 10977, 10978, 10979, 10980, 10981, 10982, 10983, 10984, + 10985, 10986, 10987, 10988, 10989, 10990, 10991, 10992, 10993, 10994, 10995, 10996, 10997, + 10998, 10999, 11000, 11001, 11002, 11003, 11004, 11005, 11006, 11007, 11008, 11009, 11010, + 11011, 11012, 11013, 11014, 11015, 11016, 11017, 11018, 11019, 11020, 11021, 11022, 11023, + 11024, 11025, 11026, 11027, 11028, 11029, 11030, 11031, 11032, 11033, 11034, 11035, 11036, + 11037, 11038, 11039, 11040, 11041, 11042, 11043, 11044, 11045, 11046, 11047, 11048, 11049, + 11050, 11051, 11052, 11053, 11054, 11055, 11056, 11057, 11058, 11059, 11060, 11061, 11062, + 11063, 11064, 11065, 11066, 11067, 11068, 11069, 11070, 11071, 11072, 11073, 11074, 11075, + 11076, 11077, 11078, 11079, 11080, 11081, 11082, 11083, 11084, 11085, 11086, 11087, 11088, + 11089, 11090, 11091, 11092, 11093, 11094, 11095, 11096, 11097, 11098, 11099, 11100, 11101, + 11102, 11103, 11104, 11105, 11106, 11107, 11108, 11109, 11110, 11111, 11112, 11113, 11114, + 11115, 11116, 11117, 11118, 11119, 11120, 11121, 11122, 11123, 11124, 11125, 11126, 11127, + 11128, 11129, 11130, 11131, 11132, 11133, 11134, 11135, 11136, 11137, 11138, 11139, 11140, + 11141, 11142, 11143, 11144, 11145, 11146, 11147, 11148, 11149, 11150, 11151, 11152, 11153, + 11154, 11155, 11156, 11157, 11158, 11159, 11160, 11161, 11162, 11163, 11164, 11165, 11166, + 11167, 11168, 11169, 11170, 11171, 11172, 11173, 11174, 11175, 11176, 11177, 11178, 11179, + 11180, 11181, 11182, 11183, 11184, 11185, 11186, 11187, 11188, 11189, 11190, 11191, 11192, + 11193, 11194, 11195, 11196, 11197, 11198, 11199, 11200, 11201, 11202, 11203, 11204, 11205, + 11206, 11207, 11208, 11209, 11210, 11211, 11212, 11213, 11214, 11215, 11216, 11217, 11218, + 11219, 11220, 11221, 11222, 11223, 11224, 11225, 11226, 11227, 11228, 11229, 11230, 11231, + 11232, 11233, 11234, 11235, 11236, 11237, 11238, 11239, 11240, 11241, 11242, 11243, 11244, + 11245, 11246, 11247, 11248, 11249, 11250, 11251, 11252, 11253, 11254, 11255, 11256, 11257, + 11258, 11259, 11260, 11261, 11262, 11263, 11776, 11777, 11778, 11779, 11780, 11781, 11782, + 11783, 11784, 11785, 11786, 11787, 11788, 11789, 11790, 11791, 11792, 11793, 11794, 11795, + 11796, 11797, 11798, 11799, 11800, 11801, 11802, 11803, 11804, 11805, 11806, 11807, 11808, + 11809, 11810, 11811, 11812, 11813, 11814, 11815, 11816, 11817, 11818, 11819, 11820, 11821, + 11822, 11823, 11824, 11825, 11826, 11827, 11828, 11829, 11830, 11831, 11832, 11833, 11834, + 11835, 11836, 11837, 11838, 11839, 11840, 11841, 11842, 11843, 11844, 11845, 11846, 11847, + 11848, 11849, 11850, 11851, 11852, 11853, 11854, 11855, 11856, 11857, 11858, 11859, 11860, + 11861, 11862, 11863, 11864, 11865, 11866, 11867, 11868, 11869, 11870, 11871, 11872, 11873, + 11874, 11875, 11876, 11877, 11878, 11879, 11880, 11881, 11882, 11883, 11884, 11885, 11886, + 11887, 11888, 11889, 11890, 11891, 11892, 11893, 11894, 11895, 11896, 11897, 11898, 11899, + 11900, 11901, 11902, 11903, 12289, 12290, 12291, 12296, 12297, 12298, 12299, 12300, 12301, + 12302, 12303, 12304, 12305, 12306, 12307, 12308, 12309, 12310, 12311, 12312, 12313, 12314, + 12315, 12316, 12317, 12318, 12319, 12320, 12336, 64830, 64831, 65093, 65094, +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_pattern_syntax_1() { + assert!(is_pattern_syntax('.')); + } + + #[test] + fn test_is_pattern_syntax_2() { + assert!(!is_pattern_syntax('a')); + } +} diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_1 new file mode 100644 index 000000000..e41a88d0c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_1 @@ -0,0 +1,25 @@ +{a} +--- +{} +--- +{ + "val": [ + { + "type": 1, + "value": "a", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 3, + "line": 1, + "column": 4 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_2 new file mode 100644 index 000000000..c24596488 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/basic_argument_2 @@ -0,0 +1,58 @@ +a {b} +c +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "a ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 2, + "line": 1, + "column": 3 + } + } + }, + { + "type": 1, + "value": "b", + "location": { + "start": { + "offset": 2, + "line": 1, + "column": 3 + }, + "end": { + "offset": 5, + "line": 1, + "column": 6 + } + } + }, + { + "type": 0, + "value": " \nc", + "location": { + "start": { + "offset": 5, + "line": 1, + "column": 6 + }, + "end": { + "offset": 8, + "line": 2, + "column": 2 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_1 new file mode 100644 index 000000000..64f85839a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_1 @@ -0,0 +1,42 @@ +{0, date, ::yyyy.MM.dd G 'at' HH:mm:ss vvvv} +--- +{} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 44, + "line": 1, + "column": 45 + } + }, + "style": { + "type": 1, + "pattern": "yyyy.MM.dd G 'at' HH:mm:ss vvvv", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 43, + "line": 1, + "column": 44 + } + }, + "parsedOptions": {} + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_2 new file mode 100644 index 000000000..fd0ac57ba --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_2 @@ -0,0 +1,42 @@ +{0, date, ::EEE, MMM d, ''yy} +--- +{} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 29, + "line": 1, + "column": 30 + } + }, + "style": { + "type": 1, + "pattern": "EEE, MMM d, ''yy", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + }, + "parsedOptions": {} + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_3 new file mode 100644 index 000000000..4fb726803 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_3 @@ -0,0 +1,42 @@ +{0, date, ::h:mm a} +--- +{} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 19, + "line": 1, + "column": 20 + } + }, + "style": { + "type": 1, + "pattern": "h:mm a", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + }, + "parsedOptions": {} + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_J b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_J new file mode 100644 index 000000000..983bf8eb7 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_J @@ -0,0 +1,49 @@ +{0, date, ::J} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + }, + "style": { + "type": 1, + "pattern": "H", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + }, + "parsedOptions": { + "hourCycle": "h23", + "hour": "numeric" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_JJ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_JJ new file mode 100644 index 000000000..5a0ed9948 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_capital_JJ @@ -0,0 +1,49 @@ +{0, date, ::JJ} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 15, + "line": 1, + "column": 16 + } + }, + "style": { + "type": 1, + "pattern": "HH", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + }, + "parsedOptions": { + "hourCycle": "h23", + "hour": "2-digit" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_j b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_j new file mode 100644 index 000000000..1c06a5236 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_j @@ -0,0 +1,50 @@ +{0, date, ::j} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + }, + "style": { + "type": 1, + "pattern": "ha", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "numeric", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jj b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jj new file mode 100644 index 000000000..76222ca20 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jj @@ -0,0 +1,50 @@ +{0, date, ::jj} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 15, + "line": 1, + "column": 16 + } + }, + "style": { + "type": 1, + "pattern": "hha", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "2-digit", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjj b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjj new file mode 100644 index 000000000..a24828b55 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjj @@ -0,0 +1,50 @@ +{0, date, ::jjj} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 16, + "line": 1, + "column": 17 + } + }, + "style": { + "type": 1, + "pattern": "haaaa", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 15, + "line": 1, + "column": 16 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "numeric", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjj b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjj new file mode 100644 index 000000000..27b2ac7e7 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjj @@ -0,0 +1,50 @@ +{0, date, ::jjjj} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + }, + "style": { + "type": 1, + "pattern": "hhaaaa", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 16, + "line": 1, + "column": 17 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "2-digit", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjj b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjj new file mode 100644 index 000000000..7dd0e25bb --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjj @@ -0,0 +1,50 @@ +{0, date, ::jjjjj} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + }, + "style": { + "type": 1, + "pattern": "haaaaa", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "numeric", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjjj b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjjj new file mode 100644 index 000000000..19d48d4e8 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/date_arg_skeleton_with_jjjjjj @@ -0,0 +1,50 @@ +{0, date, ::jjjjjj} +--- +{ + "locale": "und-u-hc-h12", + "shouldParseSkeletons": true, + "requiresOtherClause": true +} +--- +{ + "val": [ + { + "type": 3, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 19, + "line": 1, + "column": 20 + } + }, + "style": { + "type": 1, + "pattern": "hhaaaaa", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + }, + "parsedOptions": { + "hourCycle": "h12", + "hour": "2-digit", + "hour12": true + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/double_apostrophes_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/double_apostrophes_1 new file mode 100644 index 000000000..99dbb6b9e --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/double_apostrophes_1 @@ -0,0 +1,25 @@ +a''b +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "a'b", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 4, + "line": 1, + "column": 5 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_plural_selectors b/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_plural_selectors new file mode 100644 index 000000000..b40d41f5c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_plural_selectors @@ -0,0 +1,23 @@ +You have {count, plural, one {# hot dog} one {# hamburger} one {# sandwich} other {# snacks}} in your lunch bag. +--- +{} +--- +{ + "val": null, + "err": { + "kind": 20, + "message": "You have {count, plural, one {# hot dog} one {# hamburger} one {# sandwich} other {# snacks}} in your lunch bag.", + "location": { + "start": { + "offset": 41, + "line": 1, + "column": 42 + }, + "end": { + "offset": 44, + "line": 1, + "column": 45 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_select_selectors b/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_select_selectors new file mode 100644 index 000000000..e939491f2 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/duplicate_select_selectors @@ -0,0 +1,23 @@ +You have {count, select, one {# hot dog} one {# hamburger} one {# sandwich} other {# snacks}} in your lunch bag. +--- +{} +--- +{ + "val": null, + "err": { + "kind": 21, + "message": "You have {count, select, one {# hot dog} one {# hamburger} one {# sandwich} other {# snacks}} in your lunch bag.", + "location": { + "start": { + "offset": 41, + "line": 1, + "column": 42 + }, + "end": { + "offset": 44, + "line": 1, + "column": 45 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_1 new file mode 100644 index 000000000..e4449dfe0 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_1 @@ -0,0 +1,23 @@ +My name is { } +--- +{} +--- +{ + "val": null, + "err": { + "kind": 2, + "message": "My name is { }", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_2 new file mode 100644 index 000000000..5d3527a42 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/empty_argument_2 @@ -0,0 +1,24 @@ +My name is { +} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 2, + "message": "My name is {\n}", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 14, + "line": 2, + "column": 2 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_multiple_tags_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_multiple_tags_1 new file mode 100644 index 000000000..865eeb786 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_multiple_tags_1 @@ -0,0 +1,25 @@ +I '<'3 cats. 'foo' 'bar' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "I <3 cats. foo bar", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 38, + "line": 1, + "column": 39 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_pound_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_pound_1 new file mode 100644 index 000000000..6affcf21a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/escaped_pound_1 @@ -0,0 +1,125 @@ +{numPhotos, plural, =0{no photos} =1{one photo} other{'#' photos}} +--- +{} +--- +{ + "val": [ + { + "type": 6, + "value": "numPhotos", + "options": { + "=0": { + "value": [ + { + "type": 0, + "value": "no photos", + "location": { + "start": { + "offset": 23, + "line": 1, + "column": 24 + }, + "end": { + "offset": 32, + "line": 1, + "column": 33 + } + } + } + ], + "location": { + "start": { + "offset": 22, + "line": 1, + "column": 23 + }, + "end": { + "offset": 33, + "line": 1, + "column": 34 + } + } + }, + "=1": { + "value": [ + { + "type": 0, + "value": "one photo", + "location": { + "start": { + "offset": 37, + "line": 1, + "column": 38 + }, + "end": { + "offset": 46, + "line": 1, + "column": 47 + } + } + } + ], + "location": { + "start": { + "offset": 36, + "line": 1, + "column": 37 + }, + "end": { + "offset": 47, + "line": 1, + "column": 48 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "# photos", + "location": { + "start": { + "offset": 54, + "line": 1, + "column": 55 + }, + "end": { + "offset": 64, + "line": 1, + "column": 65 + } + } + } + ], + "location": { + "start": { + "offset": 53, + "line": 1, + "column": 54 + }, + "end": { + "offset": 65, + "line": 1, + "column": 66 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 66, + "line": 1, + "column": 67 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/expect_arg_format_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_arg_format_1 new file mode 100644 index 000000000..0ae97bf70 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_arg_format_1 @@ -0,0 +1,23 @@ +My name is {0, } +--- +{} +--- +{ + "val": null, + "err": { + "kind": 4, + "message": "My name is {0, }", + "location": { + "start": { + "offset": 15, + "line": 1, + "column": 16 + }, + "end": { + "offset": 15, + "line": 1, + "column": 16 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_1 new file mode 100644 index 000000000..4458a62bb --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_1 @@ -0,0 +1,23 @@ +{0, number, ::} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 7, + "message": "{0, number, ::}", + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_option_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_option_1 new file mode 100644 index 000000000..d9b7a959d --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_skeleton_token_option_1 @@ -0,0 +1,23 @@ +{0, number, ::currency/} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 7, + "message": "{0, number, ::currency/}", + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 23, + "line": 1, + "column": 24 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_style_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_style_1 new file mode 100644 index 000000000..7eb381b08 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/expect_number_arg_style_1 @@ -0,0 +1,23 @@ +{0, number, } +--- +{} +--- +{ + "val": null, + "err": { + "kind": 6, + "message": "{0, number, }", + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 12, + "line": 1, + "column": 13 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tag_number_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tag_number_arg_1 new file mode 100644 index 000000000..250525e15 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tag_number_arg_1 @@ -0,0 +1,60 @@ +I have {numCats, number} cats. +--- +{ + "ignoreTag": true +} +--- +{ + "val": [ + { + "type": 0, + "value": "I have ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 12, + "line": 1, + "column": 13 + } + } + }, + { + "type": 2, + "value": "numCats", + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 29, + "line": 1, + "column": 30 + } + }, + "style": null + }, + { + "type": 0, + "value": " cats.", + "location": { + "start": { + "offset": 29, + "line": 1, + "column": 30 + }, + "end": { + "offset": 41, + "line": 1, + "column": 42 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tags_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tags_1 new file mode 100644 index 000000000..f5d9d02ea --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/ignore_tags_1 @@ -0,0 +1,27 @@ + +--- +{ + "ignoreTag": true +} +--- +{ + "val": [ + { + "type": 0, + "value": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 21, + "line": 1, + "column": 22 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/incomplete_nested_message_in_tag b/crates/swc_icu_messageformat_parser/tests/fixtures/incomplete_nested_message_in_tag new file mode 100644 index 000000000..3cf7ddcfe --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/incomplete_nested_message_in_tag @@ -0,0 +1,23 @@ +{a, plural, other {}} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "{a, plural, other {}}", + "location": { + "start": { + "offset": 21, + "line": 1, + "column": 22 + }, + "end": { + "offset": 22, + "line": 1, + "column": 23 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_arg_format_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_arg_format_1 new file mode 100644 index 000000000..df2c2c63f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_arg_format_1 @@ -0,0 +1,23 @@ +My name is {0, foo} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 5, + "message": "My name is {0, foo}", + "location": { + "start": { + "offset": 15, + "line": 1, + "column": 16 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_close_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_close_tag_1 new file mode 100644 index 000000000..0f2e91de4 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_close_tag_1 @@ -0,0 +1,23 @@ + +--- +{} +--- +{ + "val": null, + "err": { + "kind": 23, + "message": "", + "location": { + "start": { + "offset": 3, + "line": 1, + "column": 4 + }, + "end": { + "offset": 5, + "line": 1, + "column": 6 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_1 new file mode 100644 index 000000000..29a6443aa --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_1 @@ -0,0 +1,23 @@ +a +--- +{} +--- +{ + "val": null, + "err": { + "kind": 23, + "message": "a", + "location": { + "start": { + "offset": 7, + "line": 1, + "column": 8 + }, + "end": { + "offset": 9, + "line": 1, + "column": 10 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_2 new file mode 100644 index 000000000..9dede7369 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_closing_tag_2 @@ -0,0 +1,23 @@ +aa +--- +{} +--- +{ + "val": null, + "err": { + "kind": 23, + "message": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 1, + "column": 6 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_2 new file mode 100644 index 000000000..5aaf72941 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_2 @@ -0,0 +1,23 @@ + +--- +{} +--- +{ + "val": null, + "err": { + "kind": 23, + "message": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 6, + "line": 1, + "column": 7 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_3 new file mode 100644 index 000000000..64d5ad3d9 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/invalid_tag_3 @@ -0,0 +1,23 @@ + +--- +{} +--- +{ + "val": null, + "err": { + "kind": 23, + "message": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 6, + "line": 1, + "column": 7 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/left_angle_bracket_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/left_angle_bracket_1 new file mode 100644 index 000000000..e567750e3 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/left_angle_bracket_1 @@ -0,0 +1,25 @@ +I <3 cats. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "I <3 cats.", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 10, + "line": 1, + "column": 11 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/less_than_sign_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/less_than_sign_1 new file mode 100644 index 000000000..51069657f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/less_than_sign_1 @@ -0,0 +1,219 @@ +< {level, select, A {1} 4 {2} 3 {3} 2{6} 1{12}} hours +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "< ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 2, + "line": 1, + "column": 3 + } + } + }, + { + "type": 5, + "value": "level", + "options": { + "1": { + "value": [ + { + "type": 0, + "value": "12", + "location": { + "start": { + "offset": 43, + "line": 1, + "column": 44 + }, + "end": { + "offset": 45, + "line": 1, + "column": 46 + } + } + } + ], + "location": { + "start": { + "offset": 42, + "line": 1, + "column": 43 + }, + "end": { + "offset": 46, + "line": 1, + "column": 47 + } + } + }, + "2": { + "value": [ + { + "type": 0, + "value": "6", + "location": { + "start": { + "offset": 38, + "line": 1, + "column": 39 + }, + "end": { + "offset": 39, + "line": 1, + "column": 40 + } + } + } + ], + "location": { + "start": { + "offset": 37, + "line": 1, + "column": 38 + }, + "end": { + "offset": 40, + "line": 1, + "column": 41 + } + } + }, + "3": { + "value": [ + { + "type": 0, + "value": "3", + "location": { + "start": { + "offset": 33, + "line": 1, + "column": 34 + }, + "end": { + "offset": 34, + "line": 1, + "column": 35 + } + } + } + ], + "location": { + "start": { + "offset": 32, + "line": 1, + "column": 33 + }, + "end": { + "offset": 35, + "line": 1, + "column": 36 + } + } + }, + "4": { + "value": [ + { + "type": 0, + "value": "2", + "location": { + "start": { + "offset": 27, + "line": 1, + "column": 28 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + } + } + ], + "location": { + "start": { + "offset": 26, + "line": 1, + "column": 27 + }, + "end": { + "offset": 29, + "line": 1, + "column": 30 + } + } + }, + "A": { + "value": [ + { + "type": 0, + "value": "1", + "location": { + "start": { + "offset": 21, + "line": 1, + "column": 22 + }, + "end": { + "offset": 22, + "line": 1, + "column": 23 + } + } + } + ], + "location": { + "start": { + "offset": 20, + "line": 1, + "column": 21 + }, + "end": { + "offset": 23, + "line": 1, + "column": 24 + } + } + } + }, + "location": { + "start": { + "offset": 2, + "line": 1, + "column": 3 + }, + "end": { + "offset": 47, + "line": 1, + "column": 48 + } + } + }, + { + "type": 0, + "value": " hours", + "location": { + "start": { + "offset": 47, + "line": 1, + "column": 48 + }, + "end": { + "offset": 53, + "line": 1, + "column": 54 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/malformed_argument_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/malformed_argument_1 new file mode 100644 index 000000000..175969028 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/malformed_argument_1 @@ -0,0 +1,23 @@ +My name is {0!} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 3, + "message": "My name is {0!}", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/negative_offset_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/negative_offset_1 new file mode 100644 index 000000000..fd824686a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/negative_offset_1 @@ -0,0 +1,157 @@ +{c, plural, offset:-2 =-1 { {text} project} other { {text} projects}} +--- +{} +--- +{ + "val": [ + { + "type": 6, + "value": "c", + "options": { + "=-1": { + "value": [ + { + "type": 0, + "value": " ", + "location": { + "start": { + "offset": 27, + "line": 1, + "column": 28 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + } + }, + { + "type": 1, + "value": "text", + "location": { + "start": { + "offset": 28, + "line": 1, + "column": 29 + }, + "end": { + "offset": 34, + "line": 1, + "column": 35 + } + } + }, + { + "type": 0, + "value": " project", + "location": { + "start": { + "offset": 34, + "line": 1, + "column": 35 + }, + "end": { + "offset": 42, + "line": 1, + "column": 43 + } + } + } + ], + "location": { + "start": { + "offset": 26, + "line": 1, + "column": 27 + }, + "end": { + "offset": 43, + "line": 1, + "column": 44 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": " ", + "location": { + "start": { + "offset": 51, + "line": 1, + "column": 52 + }, + "end": { + "offset": 52, + "line": 1, + "column": 53 + } + } + }, + { + "type": 1, + "value": "text", + "location": { + "start": { + "offset": 52, + "line": 1, + "column": 53 + }, + "end": { + "offset": 58, + "line": 1, + "column": 59 + } + } + }, + { + "type": 0, + "value": " projects", + "location": { + "start": { + "offset": 58, + "line": 1, + "column": 59 + }, + "end": { + "offset": 67, + "line": 1, + "column": 68 + } + } + } + ], + "location": { + "start": { + "offset": 50, + "line": 1, + "column": 51 + }, + "end": { + "offset": 68, + "line": 1, + "column": 69 + } + } + } + }, + "offset": -2, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 69, + "line": 1, + "column": 70 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/nested_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/nested_1 new file mode 100644 index 000000000..1e708d03e --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/nested_1 @@ -0,0 +1,1192 @@ + + {gender_of_host, select, + female { + {num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.}}} + male { + {num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.}}} + other { + {num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.}}}} + +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 2, + "column": 5 + } + } + }, + { + "type": 5, + "value": "gender_of_host", + "options": { + "female": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 44, + "line": 3, + "column": 15 + }, + "end": { + "offset": 53, + "line": 4, + "column": 9 + } + } + }, + { + "type": 6, + "value": "num_guests", + "options": { + "=0": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 97, + "line": 5, + "column": 15 + }, + "end": { + "offset": 103, + "line": 5, + "column": 21 + } + } + }, + { + "type": 0, + "value": " does not give a party.", + "location": { + "start": { + "offset": 103, + "line": 5, + "column": 21 + }, + "end": { + "offset": 126, + "line": 5, + "column": 44 + } + } + } + ], + "location": { + "start": { + "offset": 96, + "line": 5, + "column": 14 + }, + "end": { + "offset": 127, + "line": 5, + "column": 45 + } + } + }, + "=1": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 142, + "line": 6, + "column": 15 + }, + "end": { + "offset": 148, + "line": 6, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 148, + "line": 6, + "column": 21 + }, + "end": { + "offset": 157, + "line": 6, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 157, + "line": 6, + "column": 30 + }, + "end": { + "offset": 164, + "line": 6, + "column": 37 + } + } + }, + { + "type": 0, + "value": " to her party.", + "location": { + "start": { + "offset": 164, + "line": 6, + "column": 37 + }, + "end": { + "offset": 178, + "line": 6, + "column": 51 + } + } + } + ], + "location": { + "start": { + "offset": 141, + "line": 6, + "column": 14 + }, + "end": { + "offset": 179, + "line": 6, + "column": 52 + } + } + }, + "=2": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 194, + "line": 7, + "column": 15 + }, + "end": { + "offset": 200, + "line": 7, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 200, + "line": 7, + "column": 21 + }, + "end": { + "offset": 209, + "line": 7, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 209, + "line": 7, + "column": 30 + }, + "end": { + "offset": 216, + "line": 7, + "column": 37 + } + } + }, + { + "type": 0, + "value": " and one other person to her party.", + "location": { + "start": { + "offset": 216, + "line": 7, + "column": 37 + }, + "end": { + "offset": 251, + "line": 7, + "column": 72 + } + } + } + ], + "location": { + "start": { + "offset": 193, + "line": 7, + "column": 14 + }, + "end": { + "offset": 252, + "line": 7, + "column": 73 + } + } + }, + "other": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 270, + "line": 8, + "column": 18 + }, + "end": { + "offset": 276, + "line": 8, + "column": 24 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 276, + "line": 8, + "column": 24 + }, + "end": { + "offset": 285, + "line": 8, + "column": 33 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 285, + "line": 8, + "column": 33 + }, + "end": { + "offset": 292, + "line": 8, + "column": 40 + } + } + }, + { + "type": 0, + "value": " and ", + "location": { + "start": { + "offset": 292, + "line": 8, + "column": 40 + }, + "end": { + "offset": 297, + "line": 8, + "column": 45 + } + } + }, + { + "type": 7, + "location": { + "start": { + "offset": 297, + "line": 8, + "column": 45 + }, + "end": { + "offset": 298, + "line": 8, + "column": 46 + } + } + }, + { + "type": 0, + "value": " other people to her party.", + "location": { + "start": { + "offset": 298, + "line": 8, + "column": 46 + }, + "end": { + "offset": 325, + "line": 8, + "column": 73 + } + } + } + ], + "location": { + "start": { + "offset": 269, + "line": 8, + "column": 17 + }, + "end": { + "offset": 326, + "line": 8, + "column": 74 + } + } + } + }, + "offset": 1, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 53, + "line": 4, + "column": 9 + }, + "end": { + "offset": 327, + "line": 8, + "column": 75 + } + } + } + ], + "location": { + "start": { + "offset": 43, + "line": 3, + "column": 14 + }, + "end": { + "offset": 328, + "line": 8, + "column": 76 + } + } + }, + "male": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 341, + "line": 9, + "column": 13 + }, + "end": { + "offset": 350, + "line": 10, + "column": 9 + } + } + }, + { + "type": 6, + "value": "num_guests", + "options": { + "=0": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 394, + "line": 11, + "column": 15 + }, + "end": { + "offset": 400, + "line": 11, + "column": 21 + } + } + }, + { + "type": 0, + "value": " does not give a party.", + "location": { + "start": { + "offset": 400, + "line": 11, + "column": 21 + }, + "end": { + "offset": 423, + "line": 11, + "column": 44 + } + } + } + ], + "location": { + "start": { + "offset": 393, + "line": 11, + "column": 14 + }, + "end": { + "offset": 424, + "line": 11, + "column": 45 + } + } + }, + "=1": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 439, + "line": 12, + "column": 15 + }, + "end": { + "offset": 445, + "line": 12, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 445, + "line": 12, + "column": 21 + }, + "end": { + "offset": 454, + "line": 12, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 454, + "line": 12, + "column": 30 + }, + "end": { + "offset": 461, + "line": 12, + "column": 37 + } + } + }, + { + "type": 0, + "value": " to his party.", + "location": { + "start": { + "offset": 461, + "line": 12, + "column": 37 + }, + "end": { + "offset": 475, + "line": 12, + "column": 51 + } + } + } + ], + "location": { + "start": { + "offset": 438, + "line": 12, + "column": 14 + }, + "end": { + "offset": 476, + "line": 12, + "column": 52 + } + } + }, + "=2": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 491, + "line": 13, + "column": 15 + }, + "end": { + "offset": 497, + "line": 13, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 497, + "line": 13, + "column": 21 + }, + "end": { + "offset": 506, + "line": 13, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 506, + "line": 13, + "column": 30 + }, + "end": { + "offset": 513, + "line": 13, + "column": 37 + } + } + }, + { + "type": 0, + "value": " and one other person to his party.", + "location": { + "start": { + "offset": 513, + "line": 13, + "column": 37 + }, + "end": { + "offset": 548, + "line": 13, + "column": 72 + } + } + } + ], + "location": { + "start": { + "offset": 490, + "line": 13, + "column": 14 + }, + "end": { + "offset": 549, + "line": 13, + "column": 73 + } + } + }, + "other": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 567, + "line": 14, + "column": 18 + }, + "end": { + "offset": 573, + "line": 14, + "column": 24 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 573, + "line": 14, + "column": 24 + }, + "end": { + "offset": 582, + "line": 14, + "column": 33 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 582, + "line": 14, + "column": 33 + }, + "end": { + "offset": 589, + "line": 14, + "column": 40 + } + } + }, + { + "type": 0, + "value": " and ", + "location": { + "start": { + "offset": 589, + "line": 14, + "column": 40 + }, + "end": { + "offset": 594, + "line": 14, + "column": 45 + } + } + }, + { + "type": 7, + "location": { + "start": { + "offset": 594, + "line": 14, + "column": 45 + }, + "end": { + "offset": 595, + "line": 14, + "column": 46 + } + } + }, + { + "type": 0, + "value": " other people to his party.", + "location": { + "start": { + "offset": 595, + "line": 14, + "column": 46 + }, + "end": { + "offset": 622, + "line": 14, + "column": 73 + } + } + } + ], + "location": { + "start": { + "offset": 566, + "line": 14, + "column": 17 + }, + "end": { + "offset": 623, + "line": 14, + "column": 74 + } + } + } + }, + "offset": 1, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 350, + "line": 10, + "column": 9 + }, + "end": { + "offset": 624, + "line": 14, + "column": 75 + } + } + } + ], + "location": { + "start": { + "offset": 340, + "line": 9, + "column": 12 + }, + "end": { + "offset": 625, + "line": 14, + "column": 76 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 639, + "line": 15, + "column": 14 + }, + "end": { + "offset": 648, + "line": 16, + "column": 9 + } + } + }, + { + "type": 6, + "value": "num_guests", + "options": { + "=0": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 692, + "line": 17, + "column": 15 + }, + "end": { + "offset": 698, + "line": 17, + "column": 21 + } + } + }, + { + "type": 0, + "value": " does not give a party.", + "location": { + "start": { + "offset": 698, + "line": 17, + "column": 21 + }, + "end": { + "offset": 721, + "line": 17, + "column": 44 + } + } + } + ], + "location": { + "start": { + "offset": 691, + "line": 17, + "column": 14 + }, + "end": { + "offset": 722, + "line": 17, + "column": 45 + } + } + }, + "=1": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 737, + "line": 18, + "column": 15 + }, + "end": { + "offset": 743, + "line": 18, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 743, + "line": 18, + "column": 21 + }, + "end": { + "offset": 752, + "line": 18, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 752, + "line": 18, + "column": 30 + }, + "end": { + "offset": 759, + "line": 18, + "column": 37 + } + } + }, + { + "type": 0, + "value": " to their party.", + "location": { + "start": { + "offset": 759, + "line": 18, + "column": 37 + }, + "end": { + "offset": 775, + "line": 18, + "column": 53 + } + } + } + ], + "location": { + "start": { + "offset": 736, + "line": 18, + "column": 14 + }, + "end": { + "offset": 776, + "line": 18, + "column": 54 + } + } + }, + "=2": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 791, + "line": 19, + "column": 15 + }, + "end": { + "offset": 797, + "line": 19, + "column": 21 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 797, + "line": 19, + "column": 21 + }, + "end": { + "offset": 806, + "line": 19, + "column": 30 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 806, + "line": 19, + "column": 30 + }, + "end": { + "offset": 813, + "line": 19, + "column": 37 + } + } + }, + { + "type": 0, + "value": " and one other person to their party.", + "location": { + "start": { + "offset": 813, + "line": 19, + "column": 37 + }, + "end": { + "offset": 850, + "line": 19, + "column": 74 + } + } + } + ], + "location": { + "start": { + "offset": 790, + "line": 19, + "column": 14 + }, + "end": { + "offset": 851, + "line": 19, + "column": 75 + } + } + }, + "other": { + "value": [ + { + "type": 1, + "value": "host", + "location": { + "start": { + "offset": 869, + "line": 20, + "column": 18 + }, + "end": { + "offset": 875, + "line": 20, + "column": 24 + } + } + }, + { + "type": 0, + "value": " invites ", + "location": { + "start": { + "offset": 875, + "line": 20, + "column": 24 + }, + "end": { + "offset": 884, + "line": 20, + "column": 33 + } + } + }, + { + "type": 1, + "value": "guest", + "location": { + "start": { + "offset": 884, + "line": 20, + "column": 33 + }, + "end": { + "offset": 891, + "line": 20, + "column": 40 + } + } + }, + { + "type": 0, + "value": " and ", + "location": { + "start": { + "offset": 891, + "line": 20, + "column": 40 + }, + "end": { + "offset": 896, + "line": 20, + "column": 45 + } + } + }, + { + "type": 7, + "location": { + "start": { + "offset": 896, + "line": 20, + "column": 45 + }, + "end": { + "offset": 897, + "line": 20, + "column": 46 + } + } + }, + { + "type": 0, + "value": " other people to their party.", + "location": { + "start": { + "offset": 897, + "line": 20, + "column": 46 + }, + "end": { + "offset": 926, + "line": 20, + "column": 75 + } + } + } + ], + "location": { + "start": { + "offset": 868, + "line": 20, + "column": 17 + }, + "end": { + "offset": 927, + "line": 20, + "column": 76 + } + } + } + }, + "offset": 1, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 648, + "line": 16, + "column": 9 + }, + "end": { + "offset": 928, + "line": 20, + "column": 77 + } + } + } + ], + "location": { + "start": { + "offset": 638, + "line": 15, + "column": 13 + }, + "end": { + "offset": 929, + "line": 20, + "column": 78 + } + } + } + }, + "location": { + "start": { + "offset": 5, + "line": 2, + "column": 5 + }, + "end": { + "offset": 930, + "line": 20, + "column": 79 + } + } + }, + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 930, + "line": 20, + "column": 79 + }, + "end": { + "offset": 935, + "line": 21, + "column": 5 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/nested_tags_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/nested_tags_1 new file mode 100644 index 000000000..32a1146bb --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/nested_tags_1 @@ -0,0 +1,93 @@ +this is nested {placeholder} +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "this is ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 8, + "line": 1, + "column": 9 + } + } + }, + { + "type": 8, + "value": "a", + "children": [ + { + "type": 0, + "value": "nested ", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + } + }, + { + "type": 8, + "value": "b", + "children": [ + { + "type": 1, + "value": "placeholder", + "location": { + "start": { + "offset": 21, + "line": 1, + "column": 22 + }, + "end": { + "offset": 34, + "line": 1, + "column": 35 + } + } + } + ], + "location": { + "start": { + "offset": 18, + "line": 1, + "column": 19 + }, + "end": { + "offset": 38, + "line": 1, + "column": 39 + } + } + } + ], + "location": { + "start": { + "offset": 8, + "line": 1, + "column": 9 + }, + "end": { + "offset": 42, + "line": 1, + "column": 43 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/not_escaped_pound_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/not_escaped_pound_1 new file mode 100644 index 000000000..01752f461 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/not_escaped_pound_1 @@ -0,0 +1,25 @@ +'#' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "'#'", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 3, + "line": 1, + "column": 4 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_1 new file mode 100644 index 000000000..0eeaf4ac4 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_1 @@ -0,0 +1,25 @@ +'aa''b' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "'aa'b'", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 7, + "line": 1, + "column": 8 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_2 new file mode 100644 index 000000000..ea3e2456f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/not_quoted_string_2 @@ -0,0 +1,25 @@ +I don't know +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "I don't know", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 12, + "line": 1, + "column": 13 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/not_self_closing_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/not_self_closing_tag_1 new file mode 100644 index 000000000..3df733a96 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/not_self_closing_tag_1 @@ -0,0 +1,25 @@ +< test-tag /> +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "< test-tag />", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_2 new file mode 100644 index 000000000..c532e9caa --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_2 @@ -0,0 +1,49 @@ +{0, number, :: currency/GBP} +--- +{} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "currency", + "options": [ + "GBP" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 27, + "line": 1, + "column": 28 + } + }, + "parsedOptions": {} + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_3 new file mode 100644 index 000000000..221960fcd --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_skeleton_3 @@ -0,0 +1,53 @@ +{0, number, ::currency/GBP compact-short} +--- +{} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 41, + "line": 1, + "column": 42 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "currency", + "options": [ + "GBP" + ] + }, + { + "stem": "compact-short", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 40, + "line": 1, + "column": 41 + } + }, + "parsedOptions": {} + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_style_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_style_1 new file mode 100644 index 000000000..9b89973e5 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_arg_style_1 @@ -0,0 +1,26 @@ +{0, number, percent} +--- +{} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 20, + "line": 1, + "column": 21 + } + }, + "style": "percent" + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_1 new file mode 100644 index 000000000..1408172cc --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_1 @@ -0,0 +1,60 @@ +{0, number, ::compact-short currency/GBP} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 41, + "line": 1, + "column": 42 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "compact-short", + "options": [] + }, + { + "stem": "currency", + "options": [ + "GBP" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 40, + "line": 1, + "column": 41 + } + }, + "parsedOptions": { + "notation": "compact", + "compactDisplay": "short", + "style": "currency", + "currency": "GBP" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_10 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_10 new file mode 100644 index 000000000..87da6edff --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_10 @@ -0,0 +1,70 @@ +{0, number, ::currency/GBP .00##/@@@ unit-width-full-name} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 58, + "line": 1, + "column": 59 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "currency", + "options": [ + "GBP" + ] + }, + { + "stem": ".00##", + "options": [ + "@@@" + ] + }, + { + "stem": "unit-width-full-name", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 57, + "line": 1, + "column": 58 + } + }, + "parsedOptions": { + "style": "currency", + "currency": "GBP", + "minimumFractionDigits": 2, + "maximumFractionDigits": 4, + "minimumSignificantDigits": 3, + "maximumSignificantDigits": 3, + "currencyDisplay": "name", + "unitDisplay": "long" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_11 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_11 new file mode 100644 index 000000000..7f40fe098 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_11 @@ -0,0 +1,70 @@ +{0, number, ::measure-unit/length-meter .00##/@@@ unit-width-full-name} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 71, + "line": 1, + "column": 72 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "measure-unit", + "options": [ + "length-meter" + ] + }, + { + "stem": ".00##", + "options": [ + "@@@" + ] + }, + { + "stem": "unit-width-full-name", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 70, + "line": 1, + "column": 71 + } + }, + "parsedOptions": { + "style": "unit", + "unit": "meter", + "minimumFractionDigits": 2, + "maximumFractionDigits": 4, + "minimumSignificantDigits": 3, + "maximumSignificantDigits": 3, + "currencyDisplay": "name", + "unitDisplay": "long" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_12 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_12 new file mode 100644 index 000000000..c66bb3e1a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_12 @@ -0,0 +1,55 @@ +{0, number, ::scientific/+ee/sign-always} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 41, + "line": 1, + "column": 42 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "scientific", + "options": [ + "+ee", + "sign-always" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 40, + "line": 1, + "column": 41 + } + }, + "parsedOptions": { + "notation": "scientific", + "signDisplay": "always" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_2 new file mode 100644 index 000000000..ffd48c847 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_2 @@ -0,0 +1,52 @@ +{0, number, ::@@#} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "@@#", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + }, + "parsedOptions": { + "minimumSignificantDigits": 2, + "maximumSignificantDigits": 3 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_3 new file mode 100644 index 000000000..7b404fe88 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_3 @@ -0,0 +1,60 @@ +{0, number, ::currency/CAD unit-width-narrow} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 45, + "line": 1, + "column": 46 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "currency", + "options": [ + "CAD" + ] + }, + { + "stem": "unit-width-narrow", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 44, + "line": 1, + "column": 45 + } + }, + "parsedOptions": { + "style": "currency", + "currency": "CAD", + "currencyDisplay": "narrowSymbol", + "unitDisplay": "narrow" + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_4 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_4 new file mode 100644 index 000000000..a82cd8d0f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_4 @@ -0,0 +1,56 @@ +{0, number, ::percent .##} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 26, + "line": 1, + "column": 27 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".##", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 25, + "line": 1, + "column": 26 + } + }, + "parsedOptions": { + "style": "percent", + "maximumFractionDigits": 2 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_5 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_5 new file mode 100644 index 000000000..17b9c20b3 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_5 @@ -0,0 +1,56 @@ +{0, number, ::percent .000*} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".000*", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 27, + "line": 1, + "column": 28 + } + }, + "parsedOptions": { + "style": "percent", + "minimumFractionDigits": 3 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_6 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_6 new file mode 100644 index 000000000..d30244850 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_6 @@ -0,0 +1,57 @@ +{0, number, ::percent .0###} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 28, + "line": 1, + "column": 29 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".0###", + "options": [] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 27, + "line": 1, + "column": 28 + } + }, + "parsedOptions": { + "style": "percent", + "minimumFractionDigits": 1, + "maximumFractionDigits": 4 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_7 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_7 new file mode 100644 index 000000000..a2b78b7d2 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_7 @@ -0,0 +1,61 @@ +{0, number, ::percent .00/@##} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 30, + "line": 1, + "column": 31 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".00", + "options": [ + "@##" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 29, + "line": 1, + "column": 30 + } + }, + "parsedOptions": { + "style": "percent", + "minimumFractionDigits": 2, + "maximumFractionDigits": 2, + "minimumSignificantDigits": 1, + "maximumSignificantDigits": 3 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_8 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_8 new file mode 100644 index 000000000..ee0b8da00 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_8 @@ -0,0 +1,61 @@ +{0, number, ::percent .00/@@@} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 30, + "line": 1, + "column": 31 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".00", + "options": [ + "@@@" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 29, + "line": 1, + "column": 30 + } + }, + "parsedOptions": { + "style": "percent", + "minimumFractionDigits": 2, + "maximumFractionDigits": 2, + "minimumSignificantDigits": 3, + "maximumSignificantDigits": 3 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_9 b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_9 new file mode 100644 index 000000000..a0cf331ac --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/number_skeleton_9 @@ -0,0 +1,59 @@ +{0, number, ::percent .00/@@@@*} +--- +{ + "shouldParseSkeletons": true +} +--- +{ + "val": [ + { + "type": 2, + "value": "0", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 32, + "line": 1, + "column": 33 + } + }, + "style": { + "type": 0, + "tokens": [ + { + "stem": "percent", + "options": [] + }, + { + "stem": ".00", + "options": [ + "@@@@*" + ] + } + ], + "location": { + "start": { + "offset": 12, + "line": 1, + "column": 13 + }, + "end": { + "offset": 31, + "line": 1, + "column": 32 + } + }, + "parsedOptions": { + "style": "percent", + "minimumFractionDigits": 2, + "maximumFractionDigits": 2 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/numeric_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/numeric_tag_1 new file mode 100644 index 000000000..3913ae1fd --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/numeric_tag_1 @@ -0,0 +1,43 @@ +foo +--- +{} +--- +{ + "val": [ + { + "type": 8, + "value": "i0", + "children": [ + { + "type": 0, + "value": "foo", + "location": { + "start": { + "offset": 4, + "line": 1, + "column": 5 + }, + "end": { + "offset": 7, + "line": 1, + "column": 8 + } + } + } + ], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 12, + "line": 1, + "column": 13 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_1 new file mode 100644 index 000000000..b9e582901 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_1 @@ -0,0 +1,26 @@ + +--- +{} +--- +{ + "val": [ + { + "type": 8, + "value": "test-tag", + "children": [], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 21, + "line": 1, + "column": 22 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_2 new file mode 100644 index 000000000..b0473dc39 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_2 @@ -0,0 +1,43 @@ +foo +--- +{} +--- +{ + "val": [ + { + "type": 8, + "value": "test-tag", + "children": [ + { + "type": 0, + "value": "foo", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + } + } + ], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 24, + "line": 1, + "column": 25 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_3 new file mode 100644 index 000000000..62deff79f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_3 @@ -0,0 +1,75 @@ +foo {0} bar +--- +{} +--- +{ + "val": [ + { + "type": 8, + "value": "test-tag", + "children": [ + { + "type": 0, + "value": "foo ", + "location": { + "start": { + "offset": 10, + "line": 1, + "column": 11 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + }, + { + "type": 1, + "value": "0", + "location": { + "start": { + "offset": 14, + "line": 1, + "column": 15 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + } + }, + { + "type": 0, + "value": " bar", + "location": { + "start": { + "offset": 17, + "line": 1, + "column": 18 + }, + "end": { + "offset": 21, + "line": 1, + "column": 22 + } + } + } + ], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 32, + "line": 1, + "column": 33 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_args b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_args new file mode 100644 index 000000000..f3495e7c6 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_args @@ -0,0 +1,158 @@ +I have {numCats, number} some string {placeholder} cats. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "I ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 2, + "line": 1, + "column": 3 + } + } + }, + { + "type": 8, + "value": "b", + "children": [ + { + "type": 0, + "value": "have", + "location": { + "start": { + "offset": 5, + "line": 1, + "column": 6 + }, + "end": { + "offset": 9, + "line": 1, + "column": 10 + } + } + } + ], + "location": { + "start": { + "offset": 2, + "line": 1, + "column": 3 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + } + }, + { + "type": 0, + "value": " ", + "location": { + "start": { + "offset": 13, + "line": 1, + "column": 14 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + }, + { + "type": 8, + "value": "foo", + "children": [ + { + "type": 2, + "value": "numCats", + "location": { + "start": { + "offset": 19, + "line": 1, + "column": 20 + }, + "end": { + "offset": 36, + "line": 1, + "column": 37 + } + }, + "style": null + }, + { + "type": 0, + "value": " some string ", + "location": { + "start": { + "offset": 36, + "line": 1, + "column": 37 + }, + "end": { + "offset": 49, + "line": 1, + "column": 50 + } + } + }, + { + "type": 1, + "value": "placeholder", + "location": { + "start": { + "offset": 49, + "line": 1, + "column": 50 + }, + "end": { + "offset": 62, + "line": 1, + "column": 63 + } + } + } + ], + "location": { + "start": { + "offset": 14, + "line": 1, + "column": 15 + }, + "end": { + "offset": 68, + "line": 1, + "column": 69 + } + } + }, + { + "type": 0, + "value": " cats.", + "location": { + "start": { + "offset": 68, + "line": 1, + "column": 69 + }, + "end": { + "offset": 74, + "line": 1, + "column": 75 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_nested_arg b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_nested_arg new file mode 100644 index 000000000..0b85f9f75 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/open_close_tag_with_nested_arg @@ -0,0 +1,213 @@ +You have { + count, plural, + one {# apple} + other {# apples} + }. +--- +{} +--- +{ + "val": [ + { + "type": 8, + "value": "bold", + "children": [ + { + "type": 0, + "value": "You have ", + "location": { + "start": { + "offset": 6, + "line": 1, + "column": 7 + }, + "end": { + "offset": 15, + "line": 1, + "column": 16 + } + } + }, + { + "type": 6, + "value": "count", + "options": { + "one": { + "value": [ + { + "type": 8, + "value": "italic", + "children": [ + { + "type": 7, + "location": { + "start": { + "offset": 61, + "line": 3, + "column": 22 + }, + "end": { + "offset": 62, + "line": 3, + "column": 23 + } + } + } + ], + "location": { + "start": { + "offset": 53, + "line": 3, + "column": 14 + }, + "end": { + "offset": 71, + "line": 3, + "column": 32 + } + } + }, + { + "type": 0, + "value": " apple", + "location": { + "start": { + "offset": 71, + "line": 3, + "column": 32 + }, + "end": { + "offset": 77, + "line": 3, + "column": 38 + } + } + } + ], + "location": { + "start": { + "offset": 52, + "line": 3, + "column": 13 + }, + "end": { + "offset": 78, + "line": 3, + "column": 39 + } + } + }, + "other": { + "value": [ + { + "type": 8, + "value": "italic", + "children": [ + { + "type": 7, + "location": { + "start": { + "offset": 102, + "line": 4, + "column": 24 + }, + "end": { + "offset": 103, + "line": 4, + "column": 25 + } + } + } + ], + "location": { + "start": { + "offset": 94, + "line": 4, + "column": 16 + }, + "end": { + "offset": 112, + "line": 4, + "column": 34 + } + } + }, + { + "type": 0, + "value": " apples", + "location": { + "start": { + "offset": 112, + "line": 4, + "column": 34 + }, + "end": { + "offset": 119, + "line": 4, + "column": 41 + } + } + } + ], + "location": { + "start": { + "offset": 93, + "line": 4, + "column": 15 + }, + "end": { + "offset": 120, + "line": 4, + "column": 42 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 15, + "line": 1, + "column": 16 + }, + "end": { + "offset": 126, + "line": 5, + "column": 6 + } + } + }, + { + "type": 0, + "value": ".", + "location": { + "start": { + "offset": 126, + "line": 5, + "column": 6 + }, + "end": { + "offset": 127, + "line": 5, + "column": 7 + } + } + } + ], + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 134, + "line": 5, + "column": 14 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_1 new file mode 100644 index 000000000..70a1f0638 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_1 @@ -0,0 +1,145 @@ + + Cart: {itemCount} {itemCount, plural, + one {item} + other {items} + } +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n Cart: ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 11, + "line": 2, + "column": 11 + } + } + }, + { + "type": 1, + "value": "itemCount", + "location": { + "start": { + "offset": 11, + "line": 2, + "column": 11 + }, + "end": { + "offset": 22, + "line": 2, + "column": 22 + } + } + }, + { + "type": 0, + "value": " ", + "location": { + "start": { + "offset": 22, + "line": 2, + "column": 22 + }, + "end": { + "offset": 23, + "line": 2, + "column": 23 + } + } + }, + { + "type": 6, + "value": "itemCount", + "options": { + "one": { + "value": [ + { + "type": 0, + "value": "item", + "location": { + "start": { + "offset": 56, + "line": 3, + "column": 14 + }, + "end": { + "offset": 60, + "line": 3, + "column": 18 + } + } + } + ], + "location": { + "start": { + "offset": 55, + "line": 3, + "column": 13 + }, + "end": { + "offset": 61, + "line": 3, + "column": 19 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "items", + "location": { + "start": { + "offset": 77, + "line": 4, + "column": 16 + }, + "end": { + "offset": 82, + "line": 4, + "column": 21 + } + } + } + ], + "location": { + "start": { + "offset": 76, + "line": 4, + "column": 15 + }, + "end": { + "offset": 83, + "line": 4, + "column": 22 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 23, + "line": 2, + "column": 23 + }, + "end": { + "offset": 89, + "line": 5, + "column": 6 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_2 new file mode 100644 index 000000000..a7528e2fa --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_2 @@ -0,0 +1,178 @@ + + You have {itemCount, plural, + =0 {no items} + one {1 item} + other {{itemCount} items} + }. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n You have ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 14, + "line": 2, + "column": 14 + } + } + }, + { + "type": 6, + "value": "itemCount", + "options": { + "=0": { + "value": [ + { + "type": 0, + "value": "no items", + "location": { + "start": { + "offset": 46, + "line": 3, + "column": 13 + }, + "end": { + "offset": 54, + "line": 3, + "column": 21 + } + } + } + ], + "location": { + "start": { + "offset": 45, + "line": 3, + "column": 12 + }, + "end": { + "offset": 55, + "line": 3, + "column": 22 + } + } + }, + "one": { + "value": [ + { + "type": 0, + "value": "1 item", + "location": { + "start": { + "offset": 69, + "line": 4, + "column": 14 + }, + "end": { + "offset": 75, + "line": 4, + "column": 20 + } + } + } + ], + "location": { + "start": { + "offset": 68, + "line": 4, + "column": 13 + }, + "end": { + "offset": 76, + "line": 4, + "column": 21 + } + } + }, + "other": { + "value": [ + { + "type": 1, + "value": "itemCount", + "location": { + "start": { + "offset": 92, + "line": 5, + "column": 16 + }, + "end": { + "offset": 103, + "line": 5, + "column": 27 + } + } + }, + { + "type": 0, + "value": " items", + "location": { + "start": { + "offset": 103, + "line": 5, + "column": 27 + }, + "end": { + "offset": 109, + "line": 5, + "column": 33 + } + } + } + ], + "location": { + "start": { + "offset": 91, + "line": 5, + "column": 15 + }, + "end": { + "offset": 110, + "line": 5, + "column": 34 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 14, + "line": 2, + "column": 14 + }, + "end": { + "offset": 116, + "line": 6, + "column": 6 + } + } + }, + { + "type": 0, + "value": ".", + "location": { + "start": { + "offset": 116, + "line": 6, + "column": 6 + }, + "end": { + "offset": 117, + "line": 6, + "column": 7 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_escaped_nested_message b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_escaped_nested_message new file mode 100644 index 000000000..a537b9c4c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_escaped_nested_message @@ -0,0 +1,113 @@ + + {itemCount, plural, + one {item'}'} + other {items'}'} + } +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 2, + "column": 5 + } + } + }, + { + "type": 6, + "value": "itemCount", + "options": { + "one": { + "value": [ + { + "type": 0, + "value": "item}", + "location": { + "start": { + "offset": 38, + "line": 3, + "column": 14 + }, + "end": { + "offset": 45, + "line": 3, + "column": 21 + } + } + } + ], + "location": { + "start": { + "offset": 37, + "line": 3, + "column": 13 + }, + "end": { + "offset": 46, + "line": 3, + "column": 22 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "items}", + "location": { + "start": { + "offset": 62, + "line": 4, + "column": 16 + }, + "end": { + "offset": 70, + "line": 4, + "column": 24 + } + } + } + ], + "location": { + "start": { + "offset": 61, + "line": 4, + "column": 15 + }, + "end": { + "offset": 71, + "line": 4, + "column": 25 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 5, + "line": 2, + "column": 5 + }, + "end": { + "offset": 77, + "line": 5, + "column": 6 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_offset_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_offset_1 new file mode 100644 index 000000000..feeddfd7d --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/plural_arg_with_offset_1 @@ -0,0 +1,177 @@ +You have {itemCount, plural, offset: 2 + =0 {no items} + one {1 item} + other {{itemCount} items} + }. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You have ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 9, + "line": 1, + "column": 10 + } + } + }, + { + "type": 6, + "value": "itemCount", + "options": { + "=0": { + "value": [ + { + "type": 0, + "value": "no items", + "location": { + "start": { + "offset": 51, + "line": 2, + "column": 13 + }, + "end": { + "offset": 59, + "line": 2, + "column": 21 + } + } + } + ], + "location": { + "start": { + "offset": 50, + "line": 2, + "column": 12 + }, + "end": { + "offset": 60, + "line": 2, + "column": 22 + } + } + }, + "one": { + "value": [ + { + "type": 0, + "value": "1 item", + "location": { + "start": { + "offset": 74, + "line": 3, + "column": 14 + }, + "end": { + "offset": 80, + "line": 3, + "column": 20 + } + } + } + ], + "location": { + "start": { + "offset": 73, + "line": 3, + "column": 13 + }, + "end": { + "offset": 81, + "line": 3, + "column": 21 + } + } + }, + "other": { + "value": [ + { + "type": 1, + "value": "itemCount", + "location": { + "start": { + "offset": 97, + "line": 4, + "column": 16 + }, + "end": { + "offset": 108, + "line": 4, + "column": 27 + } + } + }, + { + "type": 0, + "value": " items", + "location": { + "start": { + "offset": 108, + "line": 4, + "column": 27 + }, + "end": { + "offset": 114, + "line": 4, + "column": 33 + } + } + } + ], + "location": { + "start": { + "offset": 96, + "line": 4, + "column": 15 + }, + "end": { + "offset": 115, + "line": 4, + "column": 34 + } + } + } + }, + "offset": 2, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 9, + "line": 1, + "column": 10 + }, + "end": { + "offset": 121, + "line": 5, + "column": 6 + } + } + }, + { + "type": 0, + "value": ".", + "location": { + "start": { + "offset": 121, + "line": 5, + "column": 6 + }, + "end": { + "offset": 122, + "line": 5, + "column": 7 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_1 new file mode 100644 index 000000000..d9cb51ef2 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_1 @@ -0,0 +1,125 @@ +You {count, plural, one {worked for '#' hour} other {worked for '#' hours}} today. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 4, + "line": 1, + "column": 5 + } + } + }, + { + "type": 6, + "value": "count", + "options": { + "one": { + "value": [ + { + "type": 0, + "value": "worked for # hour", + "location": { + "start": { + "offset": 25, + "line": 1, + "column": 26 + }, + "end": { + "offset": 44, + "line": 1, + "column": 45 + } + } + } + ], + "location": { + "start": { + "offset": 24, + "line": 1, + "column": 25 + }, + "end": { + "offset": 45, + "line": 1, + "column": 46 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "worked for # hours", + "location": { + "start": { + "offset": 53, + "line": 1, + "column": 54 + }, + "end": { + "offset": 73, + "line": 1, + "column": 74 + } + } + } + ], + "location": { + "start": { + "offset": 52, + "line": 1, + "column": 53 + }, + "end": { + "offset": 74, + "line": 1, + "column": 75 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 4, + "line": 1, + "column": 5 + }, + "end": { + "offset": 75, + "line": 1, + "column": 76 + } + } + }, + { + "type": 0, + "value": " today.", + "location": { + "start": { + "offset": 75, + "line": 1, + "column": 76 + }, + "end": { + "offset": 82, + "line": 1, + "column": 83 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_2 new file mode 100644 index 000000000..a7c964209 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_pound_sign_2 @@ -0,0 +1,124 @@ +You {count, plural, one {worked for '# hour} other {worked for '# hours}} today. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 4, + "line": 1, + "column": 5 + } + } + }, + { + "type": 6, + "value": "count", + "options": { + "one": { + "value": [ + { + "type": 0, + "value": "worked for # hour} other {worked for ", + "location": { + "start": { + "offset": 25, + "line": 1, + "column": 26 + }, + "end": { + "offset": 64, + "line": 1, + "column": 65 + } + } + }, + { + "type": 7, + "location": { + "start": { + "offset": 64, + "line": 1, + "column": 65 + }, + "end": { + "offset": 65, + "line": 1, + "column": 66 + } + } + }, + { + "type": 0, + "value": " hours", + "location": { + "start": { + "offset": 65, + "line": 1, + "column": 66 + }, + "end": { + "offset": 71, + "line": 1, + "column": 72 + } + } + } + ], + "location": { + "start": { + "offset": 24, + "line": 1, + "column": 25 + }, + "end": { + "offset": 72, + "line": 1, + "column": 73 + } + } + } + }, + "offset": 0, + "pluralType": "cardinal", + "location": { + "start": { + "offset": 4, + "line": 1, + "column": 5 + }, + "end": { + "offset": 73, + "line": 1, + "column": 74 + } + } + }, + { + "type": 0, + "value": " today.", + "location": { + "start": { + "offset": 73, + "line": 1, + "column": 74 + }, + "end": { + "offset": 80, + "line": 1, + "column": 81 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_1 new file mode 100644 index 000000000..2d18954eb --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_1 @@ -0,0 +1,25 @@ +'{a''b}' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "{a'b}", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 8, + "line": 1, + "column": 9 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_2 new file mode 100644 index 000000000..0cd5d5a6e --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_2 @@ -0,0 +1,25 @@ +'}a''b{' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "}a'b{", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 8, + "line": 1, + "column": 9 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_3 new file mode 100644 index 000000000..bf1bb1de1 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_3 @@ -0,0 +1,25 @@ +aaa'{' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "aaa{", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 6, + "line": 1, + "column": 7 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_4 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_4 new file mode 100644 index 000000000..7ef63e8a3 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_4 @@ -0,0 +1,25 @@ +aaa'}' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "aaa}", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 6, + "line": 1, + "column": 7 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_5 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_5 new file mode 100644 index 000000000..17963f34b --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_string_5 @@ -0,0 +1,25 @@ +This '{isn''t}' obvious +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "This {isn't} obvious", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 23, + "line": 1, + "column": 24 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_tag_1 new file mode 100644 index 000000000..5f7f87753 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/quoted_tag_1 @@ -0,0 +1,25 @@ +' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 4, + "line": 1, + "column": 5 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_1 new file mode 100644 index 000000000..1b9318d32 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_1 @@ -0,0 +1,160 @@ + + {gender, select, + male {He} + female {She} + other {They} + } will respond shortly. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 2, + "column": 5 + } + } + }, + { + "type": 5, + "value": "gender", + "options": { + "male": { + "value": [ + { + "type": 0, + "value": "He", + "location": { + "start": { + "offset": 36, + "line": 3, + "column": 15 + }, + "end": { + "offset": 38, + "line": 3, + "column": 17 + } + } + } + ], + "location": { + "start": { + "offset": 35, + "line": 3, + "column": 14 + }, + "end": { + "offset": 39, + "line": 3, + "column": 18 + } + } + }, + "female": { + "value": [ + { + "type": 0, + "value": "She", + "location": { + "start": { + "offset": 56, + "line": 4, + "column": 17 + }, + "end": { + "offset": 59, + "line": 4, + "column": 20 + } + } + } + ], + "location": { + "start": { + "offset": 55, + "line": 4, + "column": 16 + }, + "end": { + "offset": 60, + "line": 4, + "column": 21 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "They", + "location": { + "start": { + "offset": 76, + "line": 5, + "column": 16 + }, + "end": { + "offset": 80, + "line": 5, + "column": 20 + } + } + } + ], + "location": { + "start": { + "offset": 75, + "line": 5, + "column": 15 + }, + "end": { + "offset": 81, + "line": 5, + "column": 21 + } + } + } + }, + "location": { + "start": { + "offset": 5, + "line": 2, + "column": 5 + }, + "end": { + "offset": 87, + "line": 6, + "column": 6 + } + } + }, + { + "type": 0, + "value": " will respond shortly.", + "location": { + "start": { + "offset": 87, + "line": 6, + "column": 6 + }, + "end": { + "offset": 109, + "line": 6, + "column": 28 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_with_nested_arguments b/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_with_nested_arguments new file mode 100644 index 000000000..24aa2bc95 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/select_arg_with_nested_arguments @@ -0,0 +1,161 @@ + + {taxableArea, select, + yes {An additional {taxRate, number, percent} tax will be collected.} + other {No taxes apply.} + } + +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 2, + "column": 5 + } + } + }, + { + "type": 5, + "value": "taxableArea", + "options": { + "yes": { + "value": [ + { + "type": 0, + "value": "An additional ", + "location": { + "start": { + "offset": 40, + "line": 3, + "column": 14 + }, + "end": { + "offset": 54, + "line": 3, + "column": 28 + } + } + }, + { + "type": 2, + "value": "taxRate", + "location": { + "start": { + "offset": 54, + "line": 3, + "column": 28 + }, + "end": { + "offset": 80, + "line": 3, + "column": 54 + } + }, + "style": "percent" + }, + { + "type": 0, + "value": " tax will be collected.", + "location": { + "start": { + "offset": 80, + "line": 3, + "column": 54 + }, + "end": { + "offset": 103, + "line": 3, + "column": 77 + } + } + } + ], + "location": { + "start": { + "offset": 39, + "line": 3, + "column": 13 + }, + "end": { + "offset": 104, + "line": 3, + "column": 78 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "No taxes apply.", + "location": { + "start": { + "offset": 120, + "line": 4, + "column": 16 + }, + "end": { + "offset": 135, + "line": 4, + "column": 31 + } + } + } + ], + "location": { + "start": { + "offset": 119, + "line": 4, + "column": 15 + }, + "end": { + "offset": 136, + "line": 4, + "column": 32 + } + } + } + }, + "location": { + "start": { + "offset": 5, + "line": 2, + "column": 5 + }, + "end": { + "offset": 142, + "line": 5, + "column": 6 + } + } + }, + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 142, + "line": 5, + "column": 6 + }, + "end": { + "offset": 147, + "line": 6, + "column": 5 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/selectordinal_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/selectordinal_1 new file mode 100644 index 000000000..a1507f80a --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/selectordinal_1 @@ -0,0 +1,265 @@ +{floor, selectordinal, =0{ground} one{#st} two{#nd} few{#rd} other{#th}} floor +--- +{} +--- +{ + "val": [ + { + "type": 6, + "value": "floor", + "options": { + "=0": { + "value": [ + { + "type": 0, + "value": "ground", + "location": { + "start": { + "offset": 26, + "line": 1, + "column": 27 + }, + "end": { + "offset": 32, + "line": 1, + "column": 33 + } + } + } + ], + "location": { + "start": { + "offset": 25, + "line": 1, + "column": 26 + }, + "end": { + "offset": 33, + "line": 1, + "column": 34 + } + } + }, + "one": { + "value": [ + { + "type": 7, + "location": { + "start": { + "offset": 38, + "line": 1, + "column": 39 + }, + "end": { + "offset": 39, + "line": 1, + "column": 40 + } + } + }, + { + "type": 0, + "value": "st", + "location": { + "start": { + "offset": 39, + "line": 1, + "column": 40 + }, + "end": { + "offset": 41, + "line": 1, + "column": 42 + } + } + } + ], + "location": { + "start": { + "offset": 37, + "line": 1, + "column": 38 + }, + "end": { + "offset": 42, + "line": 1, + "column": 43 + } + } + }, + "two": { + "value": [ + { + "type": 7, + "location": { + "start": { + "offset": 47, + "line": 1, + "column": 48 + }, + "end": { + "offset": 48, + "line": 1, + "column": 49 + } + } + }, + { + "type": 0, + "value": "nd", + "location": { + "start": { + "offset": 48, + "line": 1, + "column": 49 + }, + "end": { + "offset": 50, + "line": 1, + "column": 51 + } + } + } + ], + "location": { + "start": { + "offset": 46, + "line": 1, + "column": 47 + }, + "end": { + "offset": 51, + "line": 1, + "column": 52 + } + } + }, + "few": { + "value": [ + { + "type": 7, + "location": { + "start": { + "offset": 56, + "line": 1, + "column": 57 + }, + "end": { + "offset": 57, + "line": 1, + "column": 58 + } + } + }, + { + "type": 0, + "value": "rd", + "location": { + "start": { + "offset": 57, + "line": 1, + "column": 58 + }, + "end": { + "offset": 59, + "line": 1, + "column": 60 + } + } + } + ], + "location": { + "start": { + "offset": 55, + "line": 1, + "column": 56 + }, + "end": { + "offset": 60, + "line": 1, + "column": 61 + } + } + }, + "other": { + "value": [ + { + "type": 7, + "location": { + "start": { + "offset": 67, + "line": 1, + "column": 68 + }, + "end": { + "offset": 68, + "line": 1, + "column": 69 + } + } + }, + { + "type": 0, + "value": "th", + "location": { + "start": { + "offset": 68, + "line": 1, + "column": 69 + }, + "end": { + "offset": 70, + "line": 1, + "column": 71 + } + } + } + ], + "location": { + "start": { + "offset": 66, + "line": 1, + "column": 67 + }, + "end": { + "offset": 71, + "line": 1, + "column": 72 + } + } + } + }, + "offset": 0, + "pluralType": "ordinal", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 72, + "line": 1, + "column": 73 + } + } + }, + { + "type": 0, + "value": " floor", + "location": { + "start": { + "offset": 72, + "line": 1, + "column": 73 + }, + "end": { + "offset": 78, + "line": 1, + "column": 79 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_1 new file mode 100644 index 000000000..eba487e62 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_1 @@ -0,0 +1,25 @@ + +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 12, + "line": 1, + "column": 13 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_2 new file mode 100644 index 000000000..c8a36b4bc --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/self_closing_tag_2 @@ -0,0 +1,25 @@ + +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 11, + "line": 1, + "column": 12 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_1 new file mode 100644 index 000000000..c1ac5734d --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_1 @@ -0,0 +1,41 @@ +My name is {0} +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "My name is ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 11, + "line": 1, + "column": 12 + } + } + }, + { + "type": 1, + "value": "0", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_2 new file mode 100644 index 000000000..9c3c39a65 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_argument_2 @@ -0,0 +1,41 @@ +My name is { name } +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "My name is ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 11, + "line": 1, + "column": 12 + } + } + }, + { + "type": 1, + "value": "name", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 19, + "line": 1, + "column": 20 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/simple_date_and_time_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_date_and_time_arg_1 new file mode 100644 index 000000000..24de95eff --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_date_and_time_arg_1 @@ -0,0 +1,75 @@ +Your meeting is scheduled for the {dateVal, date} at {timeVal, time} +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "Your meeting is scheduled for the ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 34, + "line": 1, + "column": 35 + } + } + }, + { + "type": 3, + "value": "dateVal", + "location": { + "start": { + "offset": 34, + "line": 1, + "column": 35 + }, + "end": { + "offset": 49, + "line": 1, + "column": 50 + } + }, + "style": null + }, + { + "type": 0, + "value": " at ", + "location": { + "start": { + "offset": 49, + "line": 1, + "column": 50 + }, + "end": { + "offset": 53, + "line": 1, + "column": 54 + } + } + }, + { + "type": 4, + "value": "timeVal", + "location": { + "start": { + "offset": 53, + "line": 1, + "column": 54 + }, + "end": { + "offset": 68, + "line": 1, + "column": 69 + } + }, + "style": null + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/simple_number_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_number_arg_1 new file mode 100644 index 000000000..33c982e5e --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/simple_number_arg_1 @@ -0,0 +1,58 @@ +I have {numCats, number} cats. +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "I have ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 7, + "line": 1, + "column": 8 + } + } + }, + { + "type": 2, + "value": "numCats", + "location": { + "start": { + "offset": 7, + "line": 1, + "column": 8 + }, + "end": { + "offset": 24, + "line": 1, + "column": 25 + } + }, + "style": null + }, + { + "type": 0, + "value": " cats.", + "location": { + "start": { + "offset": 24, + "line": 1, + "column": 25 + }, + "end": { + "offset": 30, + "line": 1, + "column": 31 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/treat_unicode_nbsp_as_whitespace b/crates/swc_icu_messageformat_parser/tests/fixtures/treat_unicode_nbsp_as_whitespace new file mode 100644 index 000000000..c94b9132c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/treat_unicode_nbsp_as_whitespace @@ -0,0 +1,211 @@ + + {gender, select, + ‎male { + {He}} + ‎female { + {She}} + ‎other{ + {They}}} + +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 5, + "line": 2, + "column": 5 + } + } + }, + { + "type": 5, + "value": "gender", + "options": { + "male": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 33, + "line": 3, + "column": 12 + }, + "end": { + "offset": 42, + "line": 4, + "column": 9 + } + } + }, + { + "type": 1, + "value": "He", + "location": { + "start": { + "offset": 42, + "line": 4, + "column": 9 + }, + "end": { + "offset": 46, + "line": 4, + "column": 13 + } + } + } + ], + "location": { + "start": { + "offset": 32, + "line": 3, + "column": 11 + }, + "end": { + "offset": 47, + "line": 4, + "column": 14 + } + } + }, + "female": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 61, + "line": 5, + "column": 14 + }, + "end": { + "offset": 70, + "line": 6, + "column": 9 + } + } + }, + { + "type": 1, + "value": "She", + "location": { + "start": { + "offset": 70, + "line": 6, + "column": 9 + }, + "end": { + "offset": 75, + "line": 6, + "column": 14 + } + } + } + ], + "location": { + "start": { + "offset": 60, + "line": 5, + "column": 13 + }, + "end": { + "offset": 76, + "line": 6, + "column": 15 + } + } + }, + "other": { + "value": [ + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 88, + "line": 7, + "column": 12 + }, + "end": { + "offset": 97, + "line": 8, + "column": 9 + } + } + }, + { + "type": 1, + "value": "They", + "location": { + "start": { + "offset": 97, + "line": 8, + "column": 9 + }, + "end": { + "offset": 103, + "line": 8, + "column": 15 + } + } + } + ], + "location": { + "start": { + "offset": 87, + "line": 7, + "column": 11 + }, + "end": { + "offset": 104, + "line": 8, + "column": 16 + } + } + } + }, + "location": { + "start": { + "offset": 5, + "line": 2, + "column": 5 + }, + "end": { + "offset": 105, + "line": 8, + "column": 17 + } + } + }, + { + "type": 0, + "value": "\n ", + "location": { + "start": { + "offset": 105, + "line": 8, + "column": 17 + }, + "end": { + "offset": 110, + "line": 9, + "column": 5 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_1 new file mode 100644 index 000000000..b4906af37 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_1 @@ -0,0 +1,25 @@ +a +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "a", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 1, + "line": 1, + "column": 2 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_2 new file mode 100644 index 000000000..a3377d735 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/trivial_2 @@ -0,0 +1,25 @@ +中文 +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "中文", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 2, + "line": 1, + "column": 3 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_1 new file mode 100644 index 000000000..6cd76570f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_1 @@ -0,0 +1,23 @@ +My name is { 0 +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "My name is { 0", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 14, + "line": 1, + "column": 15 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_2 new file mode 100644 index 000000000..bd061f809 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_argument_2 @@ -0,0 +1,23 @@ +My name is { +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "My name is { ", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 13, + "line": 1, + "column": 14 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_1 new file mode 100644 index 000000000..3f87cd519 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_1 @@ -0,0 +1,23 @@ +{0, number +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "{0, number", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 10, + "line": 1, + "column": 11 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_2 new file mode 100644 index 000000000..a32caa15f --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_2 @@ -0,0 +1,23 @@ +{0, number, percent +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "{0, number, percent", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 19, + "line": 1, + "column": 20 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_3 new file mode 100644 index 000000000..a55f53471 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_number_arg_3 @@ -0,0 +1,23 @@ +{0, number, ::percent +--- +{} +--- +{ + "val": null, + "err": { + "kind": 1, + "message": "{0, number, ::percent", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 21, + "line": 1, + "column": 22 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_1 new file mode 100644 index 000000000..6f5a3a37c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_1 @@ -0,0 +1,25 @@ +a '{a{ {}{}{} ''bb +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "a {a{ {}{}{} 'bb", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_2 new file mode 100644 index 000000000..a12931cc9 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_2 @@ -0,0 +1,23 @@ +a 'a {}{} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 2, + "message": "a 'a {}{}", + "location": { + "start": { + "offset": 5, + "line": 1, + "column": 6 + }, + "end": { + "offset": 7, + "line": 1, + "column": 8 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_3 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_3 new file mode 100644 index 000000000..b9794b678 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_3 @@ -0,0 +1,24 @@ +a '{a{ {}{}{}}}''' + {} +--- +{} +--- +{ + "val": null, + "err": { + "kind": 2, + "message": "a '{a{ {}{}{}}}''' \n {}", + "location": { + "start": { + "offset": 21, + "line": 2, + "column": 2 + }, + "end": { + "offset": 23, + "line": 2, + "column": 4 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_4 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_4 new file mode 100644 index 000000000..0103a1ee9 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_4 @@ -0,0 +1,25 @@ +You have '{count' +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You have {count", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_5 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_5 new file mode 100644 index 000000000..3f79c30ae --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_5 @@ -0,0 +1,25 @@ +You have '{count +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You have {count", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 16, + "line": 1, + "column": 17 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_6 b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_6 new file mode 100644 index 000000000..64423a4e6 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unclosed_quoted_string_6 @@ -0,0 +1,25 @@ +You have '{count} +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "You have {count}", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 17, + "line": 1, + "column": 18 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unescaped_string_literal_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/unescaped_string_literal_1 new file mode 100644 index 000000000..85f306734 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unescaped_string_literal_1 @@ -0,0 +1,25 @@ +} +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "}", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 1, + "line": 1, + "column": 2 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_1 new file mode 100644 index 000000000..2ca891833 --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_1 @@ -0,0 +1,23 @@ + +--- +{} +--- +{ + "val": null, + "err": { + "kind": 26, + "message": "", + "location": { + "start": { + "offset": 5, + "line": 1, + "column": 6 + }, + "end": { + "offset": 6, + "line": 1, + "column": 7 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_2 b/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_2 new file mode 100644 index 000000000..dac08103b --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/unmatched_open_close_tag_2 @@ -0,0 +1,23 @@ + +--- +{} +--- +{ + "val": null, + "err": { + "kind": 26, + "message": "", + "location": { + "start": { + "offset": 5, + "line": 1, + "column": 6 + }, + "end": { + "offset": 7, + "line": 1, + "column": 8 + } + } + } +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/fixtures/uppercase_tag_1 b/crates/swc_icu_messageformat_parser/tests/fixtures/uppercase_tag_1 new file mode 100644 index 000000000..9f1f9a74c --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/fixtures/uppercase_tag_1 @@ -0,0 +1,93 @@ +this is nested +--- +{} +--- +{ + "val": [ + { + "type": 0, + "value": "this is ", + "location": { + "start": { + "offset": 0, + "line": 1, + "column": 1 + }, + "end": { + "offset": 8, + "line": 1, + "column": 9 + } + } + }, + { + "type": 8, + "value": "a", + "children": [ + { + "type": 0, + "value": "nested ", + "location": { + "start": { + "offset": 11, + "line": 1, + "column": 12 + }, + "end": { + "offset": 18, + "line": 1, + "column": 19 + } + } + }, + { + "type": 8, + "value": "Button", + "children": [ + { + "type": 1, + "value": "placeholder", + "location": { + "start": { + "offset": 26, + "line": 1, + "column": 27 + }, + "end": { + "offset": 39, + "line": 1, + "column": 40 + } + } + } + ], + "location": { + "start": { + "offset": 18, + "line": 1, + "column": 19 + }, + "end": { + "offset": 48, + "line": 1, + "column": 49 + } + } + } + ], + "location": { + "start": { + "offset": 8, + "line": 1, + "column": 9 + }, + "end": { + "offset": 52, + "line": 1, + "column": 53 + } + } + } + ], + "err": null +} \ No newline at end of file diff --git a/crates/swc_icu_messageformat_parser/tests/run_parser_e2e.rs b/crates/swc_icu_messageformat_parser/tests/run_parser_e2e.rs new file mode 100644 index 000000000..ac3e0fefd --- /dev/null +++ b/crates/swc_icu_messageformat_parser/tests/run_parser_e2e.rs @@ -0,0 +1,171 @@ +#![allow(non_snake_case)] +use std::{fs, path::PathBuf}; + +use serde::Serialize; +use serde_json::Value; +use swc_icu_messageformat_parser::{AstElement, Error, Parser, ParserOptions}; +use testing::fixture; + +#[derive(Debug)] +struct TestFixtureSections { + message: String, + snapshot_options: ParserOptions, + expected: String, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct Snapshot<'a> { + val: Option>>, + err: Option, +} + +fn read_sections(file: PathBuf) -> TestFixtureSections { + let input = fs::read_to_string(file).expect("Should able to read fixture"); + + let input: Vec<&str> = input.split("\n---\n").collect(); + + TestFixtureSections { + message: input.first().expect("").to_string(), + snapshot_options: serde_json::from_str(input.get(1).expect("")) + .expect("Should able to deserialize options"), + expected: input.get(2).expect("").to_string(), + } +} + +#[cfg_attr( + feature = "utf16", + fixture("tests/fixtures/treat_unicode_nbsp_as_whitespace") +)] +#[cfg_attr(feature = "utf16", fixture("tests/fixtures/trivial_2"))] +#[fixture("tests/fixtures/uppercase_tag_1")] +#[fixture("tests/fixtures/expect_number_arg_skeleton_token_1")] +#[fixture("tests/fixtures/self_closing_tag_1")] +#[fixture("tests/fixtures/self_closing_tag_2")] +#[fixture("tests/fixtures/date_arg_skeleton_with_j")] +#[fixture("tests/fixtures/date_arg_skeleton_with_jj")] +#[fixture("tests/fixtures/date_arg_skeleton_with_jjj")] +#[fixture("tests/fixtures/date_arg_skeleton_with_jjjj")] +#[fixture("tests/fixtures/date_arg_skeleton_with_jjjjj")] +#[fixture("tests/fixtures/date_arg_skeleton_with_jjjjjj")] +#[fixture("tests/fixtures/date_arg_skeleton_with_capital_J")] +#[fixture("tests/fixtures/date_arg_skeleton_with_capital_JJ")] +#[fixture("tests/fixtures/negative_offset_1")] +#[fixture("tests/fixtures/simple_date_and_time_arg_1")] +#[fixture("tests/fixtures/select_arg_with_nested_arguments")] +#[fixture("tests/fixtures/expect_number_arg_skeleton_token_option_1")] +#[fixture("tests/fixtures/less_than_sign_1")] +#[fixture("tests/fixtures/unmatched_open_close_tag_1")] +#[fixture("tests/fixtures/unmatched_open_close_tag_2")] +#[fixture("tests/fixtures/basic_argument_1")] +#[fixture("tests/fixtures/basic_argument_2")] +#[fixture("tests/fixtures/date_arg_skeleton_1")] +#[fixture("tests/fixtures/date_arg_skeleton_2")] +#[fixture("tests/fixtures/date_arg_skeleton_3")] +#[fixture("tests/fixtures/number_arg_skeleton_2")] +#[fixture("tests/fixtures/number_arg_skeleton_3")] +#[fixture("tests/fixtures/number_arg_style_1")] +#[fixture("tests/fixtures/expect_number_arg_style_1")] +#[fixture("tests/fixtures/expect_arg_format_1")] +#[fixture("tests/fixtures/trivial_1")] +#[fixture("tests/fixtures/simple_number_arg_1")] +#[fixture("tests/fixtures/simple_argument_1")] +#[fixture("tests/fixtures/simple_argument_2")] +#[fixture("tests/fixtures/ignore_tags_1")] +#[fixture("tests/fixtures/ignore_tag_number_arg_1")] +#[fixture("tests/fixtures/unclosed_argument_1")] +#[fixture("tests/fixtures/unclosed_argument_2")] +#[fixture("tests/fixtures/unclosed_number_arg_1")] +#[fixture("tests/fixtures/unclosed_number_arg_2")] +#[fixture("tests/fixtures/unclosed_number_arg_3")] +#[fixture("tests/fixtures/unclosed_quoted_string_1")] +#[fixture("tests/fixtures/unclosed_quoted_string_2")] +#[fixture("tests/fixtures/unclosed_quoted_string_3")] +#[fixture("tests/fixtures/unclosed_quoted_string_4")] +#[fixture("tests/fixtures/unclosed_quoted_string_5")] +#[fixture("tests/fixtures/unclosed_quoted_string_6")] +#[fixture("tests/fixtures/unescaped_string_literal_1")] +#[fixture("tests/fixtures/not_quoted_string_1")] +#[fixture("tests/fixtures/not_quoted_string_2")] +#[fixture("tests/fixtures/left_angle_bracket_1")] +#[fixture("tests/fixtures/malformed_argument_1")] +#[fixture("tests/fixtures/invalid_close_tag_1")] +#[fixture("tests/fixtures/invalid_closing_tag_1")] +#[fixture("tests/fixtures/invalid_closing_tag_2")] +#[fixture("tests/fixtures/invalid_tag_1")] +#[fixture("tests/fixtures/invalid_tag_2")] +#[fixture("tests/fixtures/invalid_tag_3")] +#[fixture("tests/fixtures/double_apostrophes_1")] +#[fixture("tests/fixtures/quoted_string_1")] +#[fixture("tests/fixtures/quoted_string_2")] +#[fixture("tests/fixtures/quoted_string_3")] +#[fixture("tests/fixtures/quoted_string_4")] +#[fixture("tests/fixtures/quoted_string_5")] +#[fixture("tests/fixtures/number_skeleton_1")] +#[fixture("tests/fixtures/number_skeleton_2")] +#[fixture("tests/fixtures/number_skeleton_3")] +#[fixture("tests/fixtures/number_skeleton_4")] +#[fixture("tests/fixtures/number_skeleton_5")] +#[fixture("tests/fixtures/number_skeleton_6")] +#[fixture("tests/fixtures/number_skeleton_7")] +#[fixture("tests/fixtures/number_skeleton_8")] +#[fixture("tests/fixtures/number_skeleton_9")] +#[fixture("tests/fixtures/number_skeleton_10")] +#[fixture("tests/fixtures/number_skeleton_11")] +#[fixture("tests/fixtures/number_skeleton_12")] +#[fixture("tests/fixtures/empty_argument_1")] +#[fixture("tests/fixtures/empty_argument_2")] +#[fixture("tests/fixtures/duplicate_select_selectors")] +#[fixture("tests/fixtures/duplicate_plural_selectors")] +#[fixture("tests/fixtures/plural_arg_1")] +#[fixture("tests/fixtures/plural_arg_2")] +#[fixture("tests/fixtures/plural_arg_with_escaped_nested_message")] +#[fixture("tests/fixtures/plural_arg_with_offset_1")] +#[fixture("tests/fixtures/open_close_tag_1")] +#[fixture("tests/fixtures/open_close_tag_2")] +#[fixture("tests/fixtures/open_close_tag_3")] +#[fixture("tests/fixtures/open_close_tag_with_args")] +#[fixture("tests/fixtures/open_close_tag_with_nested_arg")] +#[fixture("tests/fixtures/escaped_pound_1")] +#[fixture("tests/fixtures/escaped_multiple_tags_1")] +#[fixture("tests/fixtures/invalid_arg_format_1")] +#[fixture("tests/fixtures/incomplete_nested_message_in_tag")] +#[fixture("tests/fixtures/not_escaped_pound_1")] +#[fixture("tests/fixtures/not_self_closing_tag_1")] +#[fixture("tests/fixtures/nested_1")] +#[fixture("tests/fixtures/nested_tags_1")] +#[fixture("tests/fixtures/numeric_tag_1")] +#[fixture("tests/fixtures/quoted_pound_sign_1")] +#[fixture("tests/fixtures/quoted_pound_sign_2")] +#[fixture("tests/fixtures/quoted_tag_1")] +#[fixture("tests/fixtures/select_arg_1")] +#[fixture("tests/fixtures/selectordinal_1")] +fn parser_tests(file: PathBuf) { + let fixture_sections = read_sections(file); + let options = ParserOptions { + capture_location: true, + ..fixture_sections.snapshot_options + }; + + let mut parser = Parser::new(&fixture_sections.message, &options); + + let parsed_result = parser.parse(); + let parsed_result_snapshot = match parsed_result { + Ok(parsed_result) => Snapshot { + val: Some(parsed_result), + err: None, + }, + Err(err) => Snapshot { + val: None, + err: Some(err), + }, + }; + + let parsed_result_str = serde_json::to_string_pretty(&parsed_result_snapshot) + .expect("Should able to serialize parsed result"); + + let input: Value = serde_json::from_str(&parsed_result_str).unwrap(); + let expected: Value = serde_json::from_str(&fixture_sections.expected).unwrap(); + + similar_asserts::assert_eq!(input, expected); +} diff --git a/packages/emotion/CHANGELOG.md b/packages/emotion/CHANGELOG.md index 94216bd86..35625a720 100644 --- a/packages/emotion/CHANGELOG.md +++ b/packages/emotion/CHANGELOG.md @@ -1,5 +1,11 @@ # @swc/plugin-emotion +## 7.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 7.0.0 ### Major Changes diff --git a/packages/emotion/README.md b/packages/emotion/README.md index 584de1ea2..74889583a 100644 --- a/packages/emotion/README.md +++ b/packages/emotion/README.md @@ -34,6 +34,12 @@ Source code for plugin itself (not transforms) are copied from https://github.co # @swc/plugin-emotion +## 7.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 7.0.0 ### Major Changes diff --git a/packages/emotion/package.json b/packages/emotion/package.json index 568d6f1dc..ce245bf1e 100644 --- a/packages/emotion/package.json +++ b/packages/emotion/package.json @@ -1,6 +1,6 @@ { "name": "@swc/plugin-emotion", - "version": "7.0.0", + "version": "7.0.1", "description": "SWC plugin for emotion css-in-js library", "main": "swc_plugin_emotion.wasm", "scripts": { diff --git a/packages/emotion/transform/src/lib.rs b/packages/emotion/transform/src/lib.rs index f90df45f8..b89b5ca1c 100644 --- a/packages/emotion/transform/src/lib.rs +++ b/packages/emotion/transform/src/lib.rs @@ -179,6 +179,7 @@ pub fn emotion( pub struct EmotionTransformer { pub options: EmotionOptions, + #[allow(unused)] filepath_hash: Option, filepath: PathBuf, dirname: Option, diff --git a/packages/formatjs/.npmignore b/packages/formatjs/.npmignore new file mode 100644 index 000000000..1ed674f50 --- /dev/null +++ b/packages/formatjs/.npmignore @@ -0,0 +1,2 @@ +transform/ +tests/ \ No newline at end of file diff --git a/packages/formatjs/CHANGELOG.md b/packages/formatjs/CHANGELOG.md new file mode 100644 index 000000000..b144bda9e --- /dev/null +++ b/packages/formatjs/CHANGELOG.md @@ -0,0 +1,7 @@ +# @swc/plugin-formatjs + +## 1.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository diff --git a/packages/formatjs/Cargo.toml b/packages/formatjs/Cargo.toml new file mode 100644 index 000000000..ad3d34173 --- /dev/null +++ b/packages/formatjs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +authors = [ + "OJ Kwon ", + "DongYoon Kang ", +] +description = "formatjs plugin for SWC" +edition.workspace = true +license.workspace = true +name = "swc_plugin_formatjs" +publish = false +repository.workspace = true +version = "1.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +swc_core = { workspace = true, features = [ + "ecma_plugin_transform", + "ecma_ast_serde", +] } +swc_formatjs_transform = { path = "./transform", version = "1.0.0" } diff --git a/packages/formatjs/README.md b/packages/formatjs/README.md new file mode 100644 index 000000000..e5de8ffbe --- /dev/null +++ b/packages/formatjs/README.md @@ -0,0 +1,11 @@ +# @swc/plugin-formatjs + +FormatJS SWC plugin, maintained by SWC team. + +# @swc/plugin-formatjs + +## 1.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository diff --git a/packages/formatjs/README.tmpl.md b/packages/formatjs/README.tmpl.md new file mode 100644 index 000000000..0172cf32c --- /dev/null +++ b/packages/formatjs/README.tmpl.md @@ -0,0 +1,5 @@ +# @swc/plugin-formatjs + +FormatJS SWC plugin, maintained by SWC team. + +${CHANGELOG} diff --git a/packages/formatjs/package.json b/packages/formatjs/package.json new file mode 100644 index 000000000..f78867daa --- /dev/null +++ b/packages/formatjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@swc/plugin-formatjs", + "version": "1.0.1", + "description": "FormatJS SWC plugin", + "main": "swc_plugin_formatjs.wasm", + "scripts": { + "prepack": "cargo build --release -p swc_plugin_formatjs --target wasm32-wasi && cp ../../target/wasm32-wasi/release/swc_plugin_formatjs.wasm ." + }, + "homepage": "https://swc.rs", + "repository": { + "type": "git", + "url": "+https://github.com/swc-project/plugins.git" + }, + "bugs": { + "url": "https://github.com/swc-project/plugins/issues" + }, + "author": "강동윤 ", + "keywords": [], + "license": "Apache-2.0", + "preferUnplugged": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } +} diff --git a/packages/formatjs/src/lib.rs b/packages/formatjs/src/lib.rs new file mode 100644 index 000000000..f1dec3d45 --- /dev/null +++ b/packages/formatjs/src/lib.rs @@ -0,0 +1,36 @@ +use swc_core::{ + ecma::{ast::Program, visit::*}, + plugin::{ + metadata::TransformPluginMetadataContextKind, plugin_transform, + proxies::TransformPluginProgramMetadata, + }, +}; +use swc_formatjs_transform::{create_formatjs_visitor, FormatJSPluginOptions}; + +#[plugin_transform] +pub fn process(mut program: Program, metadata: TransformPluginProgramMetadata) -> Program { + let filename = metadata.get_context(&TransformPluginMetadataContextKind::Filename); + let filename = filename.as_deref().unwrap_or("unknown.js"); + + let plugin_config = metadata.get_transform_plugin_config(); + let plugin_options: FormatJSPluginOptions = if let Some(plugin_config) = plugin_config { + serde_json::from_str(&plugin_config).unwrap_or_else(|f| { + println!("Could not deserialize instrumentation option"); + println!("{:#?}", f); + Default::default() + }) + } else { + Default::default() + }; + + let mut visitor = create_formatjs_visitor( + std::sync::Arc::new(metadata.source_map), + metadata.comments.as_ref(), + plugin_options, + filename, + ); + + program.visit_mut_with(&mut visitor); + + program +} diff --git a/packages/formatjs/transform/Cargo.toml b/packages/formatjs/transform/Cargo.toml new file mode 100644 index 000000000..888881486 --- /dev/null +++ b/packages/formatjs/transform/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = [ + "OJ Kwon ", + "DongYoon Kang ", +] +description = "formatjs custom transform visitor for SWC" +edition.workspace = true +license.workspace = true +name = "swc_formatjs_transform" +repository.workspace = true +version = "1.0.0" + +[features] +custom_transform = [] + +[dependencies] +base64ct = { workspace = true, features = ["alloc"] } +once_cell = { workspace = true } +regex = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +swc_core = { workspace = true, features = ["common", "ecma_visit", "ecma_ast"] } +swc_icu_messageformat_parser = { workspace = true, features = ["utf16"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/packages/formatjs/transform/src/lib.rs b/packages/formatjs/transform/src/lib.rs new file mode 100644 index 000000000..38361ee42 --- /dev/null +++ b/packages/formatjs/transform/src/lib.rs @@ -0,0 +1,1234 @@ +use std::collections::{HashMap, HashSet}; + +use base64ct::{Base64, Encoding}; +use once_cell::sync::Lazy; +use regex::{Captures, Regex as Regexp}; +use serde::{ser::SerializeMap, Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use swc_core::{ + common::{ + comments::{Comment, CommentKind, Comments}, + source_map::SmallPos, + BytePos, Loc, SourceMapper, Span, Spanned, DUMMY_SP, + }, + ecma::{ + ast::{ + ArrayLit, Bool, CallExpr, Callee, Expr, ExprOrSpread, IdentName, JSXAttr, JSXAttrName, + JSXAttrOrSpread, JSXAttrValue, JSXElementName, JSXExpr, JSXNamespacedName, + JSXOpeningElement, KeyValueProp, Lit, MemberProp, ModuleItem, Number, ObjectLit, Prop, + PropName, PropOrSpread, Str, + }, + visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, + }, +}; +use swc_icu_messageformat_parser::{Parser, ParserOptions}; + +pub static WHITESPACE_REGEX: Lazy = Lazy::new(|| Regexp::new(r"\s+").unwrap()); + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct FormatJSPluginOptions { + pub pragma: Option, + pub remove_default_message: bool, + pub id_interpolate_pattern: Option, + pub ast: bool, + pub extract_source_location: bool, + pub preserve_whitespace: bool, + pub __debug_extracted_messages_comment: bool, + pub additional_function_names: Vec, + pub additional_component_names: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct JSXMessageDescriptorPath { + id: Option, + default_message: Option, + description: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct CallExprMessageDescriptorPath { + id: Option, + default_message: Option, + description: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct MessageDescriptor { + id: Option, + default_message: Option, + description: Option, +} + +// TODO: consolidate with get_message_descriptor_key_from_call_expr? +fn get_message_descriptor_key_from_jsx(name: &JSXAttrName) -> &str { + match name { + JSXAttrName::Ident(name) + | JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => &name.sym, + } + + // NOTE: Do not support evaluatePath() +} + +fn get_message_descriptor_key_from_call_expr(name: &PropName) -> Option<&str> { + match name { + PropName::Ident(name) => Some(&*name.sym), + PropName::Str(name) => Some(&*name.value), + _ => None, + } + + // NOTE: Do not support evaluatePath() +} + +// TODO: Consolidate with create_message_descriptor_from_call_expr +fn create_message_descriptor_from_jsx_attr( + attrs: &Vec, +) -> JSXMessageDescriptorPath { + let mut ret = JSXMessageDescriptorPath::default(); + for attr in attrs { + if let JSXAttrOrSpread::JSXAttr(JSXAttr { name, value, .. }) = attr { + let key = get_message_descriptor_key_from_jsx(name); + + match key { + "id" => { + ret.id = value.clone(); + } + "defaultMessage" => { + ret.default_message = value.clone(); + } + "description" => { + ret.description = value.clone(); + } + _ => { + //unexpected + } + } + } + } + + ret +} + +fn create_message_descriptor_from_call_expr( + props: &Vec, +) -> CallExprMessageDescriptorPath { + let mut ret = CallExprMessageDescriptorPath::default(); + for prop in props { + if let PropOrSpread::Prop(prop) = prop { + if let Prop::KeyValue(KeyValueProp { key, value }) = &**prop { + if let Some(key) = get_message_descriptor_key_from_call_expr(key) { + match key { + "id" => { + ret.id = Some(*value.clone()); + } + "defaultMessage" => { + ret.default_message = Some(*value.clone()); + } + "description" => { + ret.description = Some(*value.clone()); + } + _ => { + //unexpected + } + } + }; + } + } + } + + ret +} + +fn get_jsx_message_descriptor_value( + value: &Option, + is_message_node: Option, +) -> Option { + if value.is_none() { + return None; + } + let value = value.as_ref().expect("Should be available"); + + // NOTE: do not support evaluatePath + match value { + JSXAttrValue::JSXExprContainer(container) => { + if is_message_node.unwrap_or(false) { + if let JSXExpr::Expr(expr) = &container.expr { + // If this is already compiled, no need to recompiled it + if let Expr::Array(..) = &**expr { + return None; + } + } + } + + match &container.expr { + JSXExpr::Expr(expr) => match &**expr { + Expr::Lit(Lit::Str(s)) => Some(s.value.to_string()), + Expr::Tpl(tpl) => { + //NOTE: This doesn't fully evaluate templates + Some( + tpl.quasis + .iter() + .map(|q| { + q.cooked + .as_ref() + .map(|v| v.to_string()) + .unwrap_or("".to_string()) + }) + .collect::>() + .join(""), + ) + } + _ => None, + }, + _ => None, + } + } + JSXAttrValue::Lit(Lit::Str(s)) => Some(s.value.to_string()), + _ => None, + } +} + +fn get_call_expr_message_descriptor_value( + value: &Option, + _is_message_node: Option, +) -> Option { + if value.is_none() { + return None; + } + + let value = value.as_ref().expect("Should be available"); + + // NOTE: do not support evaluatePath + match value { + Expr::Ident(ident) => Some(ident.sym.to_string()), + Expr::Lit(Lit::Str(s)) => Some(s.value.to_string()), + Expr::Tpl(tpl) => { + //NOTE: This doesn't fully evaluate templates + Some( + tpl.quasis + .iter() + .map(|q| { + q.cooked + .as_ref() + .map(|v| v.to_string()) + .unwrap_or("".to_string()) + }) + .collect::>() + .join(""), + ) + } + _ => None, + } +} + +#[derive(Debug, Clone, Deserialize)] +pub enum MessageDescriptionValue { + Str(String), + Obj(ObjectLit), +} + +impl Serialize for MessageDescriptionValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + MessageDescriptionValue::Str(s) => serializer.serialize_str(s), + // NOTE: this is good enough to barely pass key-value object serialization. Not a + // complete implementation. + MessageDescriptionValue::Obj(obj) => { + let mut state = serializer.serialize_map(Some(obj.props.len()))?; + for prop in &obj.props { + match prop { + PropOrSpread::Prop(prop) => { + match &**prop { + Prop::KeyValue(key_value) => { + let key = match &key_value.key { + PropName::Ident(ident) => ident.sym.to_string(), + PropName::Str(s) => s.value.to_string(), + _ => { + //unexpected + continue; + } + }; + let value = match &*key_value.value { + Expr::Lit(Lit::Str(s)) => s.value.to_string(), + _ => { + //unexpected + continue; + } + }; + state.serialize_entry(&key, &value)?; + } + _ => { + //unexpected + continue; + } + } + } + _ => { + //unexpected + continue; + } + } + } + state.end() + } + } + } +} + +// NOTE: due to not able to support static evaluation, this +// fn manually expands possible values for the description values +// from string to object. +//TODO: Consolidate with get_call_expr_message_descriptor_value_maybe_object +fn get_jsx_message_descriptor_value_maybe_object( + value: &Option, + is_message_node: Option, +) -> Option { + if value.is_none() { + return None; + } + let value = value.as_ref().expect("Should be available"); + + // NOTE: do not support evaluatePath + match value { + JSXAttrValue::JSXExprContainer(container) => { + if is_message_node.unwrap_or(false) { + if let JSXExpr::Expr(expr) = &container.expr { + // If this is already compiled, no need to recompiled it + if let Expr::Array(..) = &**expr { + return None; + } + } + } + + match &container.expr { + JSXExpr::Expr(expr) => match &**expr { + Expr::Lit(Lit::Str(s)) => { + Some(MessageDescriptionValue::Str(s.value.to_string())) + } + Expr::Object(object_lit) => { + Some(MessageDescriptionValue::Obj(object_lit.clone())) + } + _ => None, + }, + _ => None, + } + } + JSXAttrValue::Lit(Lit::Str(s)) => Some(MessageDescriptionValue::Str(s.value.to_string())), + _ => None, + } +} + +fn get_call_expr_message_descriptor_value_maybe_object( + value: &Option, + _is_message_node: Option, +) -> Option { + if value.is_none() { + return None; + } + + let value = value.as_ref().expect("Should be available"); + // NOTE: do not support evaluatePath + match value { + Expr::Ident(ident) => Some(MessageDescriptionValue::Str(ident.sym.to_string())), + Expr::Lit(Lit::Str(s)) => Some(MessageDescriptionValue::Str(s.value.to_string())), + Expr::Object(object_lit) => Some(MessageDescriptionValue::Obj(object_lit.clone())), + _ => None, + } +} + +// TODO: Consolidate with get_call_expr_icu_message_value +fn get_jsx_icu_message_value( + message_path: &Option, + preserve_whitespace: bool, +) -> String { + if message_path.is_none() { + return "".to_string(); + } + + let message = + get_jsx_message_descriptor_value(message_path, Some(true)).unwrap_or("".to_string()); + + let message = if !preserve_whitespace { + let message = WHITESPACE_REGEX.replace_all(&message, " "); + message.trim().to_string() + } else { + message + }; + + let mut parser = Parser::new(message.as_str(), &ParserOptions::default()); + + if let Err(e) = parser.parse() { + let is_literal_err = if let Some(JSXAttrValue::Lit(..)) = message_path { + message.contains("\\\\") + } else { + false + }; + + let handler = &swc_core::plugin::errors::HANDLER; + + if is_literal_err { + { + handler.with(|handler| { + handler + .struct_err( + r#" + [React Intl] Message failed to parse. + It looks like `\\`s were used for escaping, + this won't work with JSX string literals. + Wrap with `{{}}`. + See: http://facebook.github.io/react/docs/jsx-gotchas.html + "#, + ) + .emit() + }); + } + } else { + { + handler.with(|handler| { + handler + .struct_warn( + r#" + [React Intl] Message failed to parse. + See: https://formatjs.io/docs/core-concepts/icu-syntax + \n {:#?} + "#, + ) + .emit(); + handler + .struct_err(&format!("SyntaxError: {}", e.kind)) + .emit() + }); + } + } + } + + message +} + +fn get_call_expr_icu_message_value( + message_path: &Option, + preserve_whitespace: bool, +) -> String { + if message_path.is_none() { + return "".to_string(); + } + + let message = + get_call_expr_message_descriptor_value(message_path, Some(true)).unwrap_or("".to_string()); + + let message = if !preserve_whitespace { + let message = WHITESPACE_REGEX.replace_all(&message, " "); + message.trim().to_string() + } else { + message + }; + + let mut parser = Parser::new(message.as_str(), &ParserOptions::default()); + + if let Err(e) = parser.parse() { + let handler = &swc_core::plugin::errors::HANDLER; + + { + handler.with(|handler| { + handler + .struct_warn( + r#" + [React Intl] Message failed to parse. + See: https://formatjs.io/docs/core-concepts/icu-syntax + \n {:#?} + "#, + ) + .emit(); + handler + .struct_err(&format!("SyntaxError: {}", e.kind)) + .emit() + }); + } + } + + message +} + +fn interpolate_name(_resource_path: &str, name: &str, content: &str) -> Option { + let filename = name; + + // let ext = "bin"; + // let basename = "file"; + // let directory = ""; + // let folder = ""; + // let query = ""; + + /* + if (resource_path) { + const parsed = path.parse(loaderContext.resourcePath) + let resourcePath = loaderContext.resourcePath + + if (parsed.ext) { + ext = parsed.ext.slice(1) + } + + if (parsed.dir) { + basename = parsed.name + resourcePath = parsed.dir + path.sep + } + + if (typeof context !== 'undefined') { + directory = path + .relative(context, resourcePath + '_') + .replace(/\\/g, '/') + .replace(/\.\.(\/)?/g, '_$1') + directory = directory.slice(0, -1) + } else { + directory = resourcePath.replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1') + } + + if (directory.length === 1) { + directory = '' + } else if (directory.length > 1) { + folder = path.basename(directory) + } + } + */ + + let mut url = filename.to_string(); + let r = Regexp::new(r#"\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]"#) + .unwrap(); + + url = r + .replace(url.as_str(), |cap: &Captures| { + // let hash_type = cap.get(1); + // let digest_type = cap.get(2); + let max_length = cap.get(3); + + // TODO: support hashtype + let mut hasher = Sha512::new(); + hasher.update(content.as_bytes()); + let hash = hasher.finalize(); + let base64_hash = Base64::encode_string(&hash); + + if let Some(max_length) = max_length { + base64_hash[0..max_length.as_str().parse::().unwrap()].to_string() + } else { + base64_hash + } + }) + .to_string(); + + /* + url = url + .replace(/\[ext\]/gi, () => ext) + .replace(/\[name\]/gi, () => basename) + .replace(/\[path\]/gi, () => directory) + .replace(/\[folder\]/gi, () => folder) + .replace(/\[query\]/gi, () => query) + */ + + Some(url.to_string()) +} + +// TODO: Consolidate with evaluate_call_expr_message_descriptor +fn evaluate_jsx_message_descriptor( + descriptor_path: &JSXMessageDescriptorPath, + options: &FormatJSPluginOptions, + filename: &str, +) -> MessageDescriptor { + let id = get_jsx_message_descriptor_value(&descriptor_path.id, None); + let default_message = get_jsx_icu_message_value( + &descriptor_path.default_message, + options.preserve_whitespace, + ); + + let description = + get_jsx_message_descriptor_value_maybe_object(&descriptor_path.description, None); + + // Note: do not support override fn + let id = if id.is_none() && !default_message.is_empty() { + let interpolate_pattern = if let Some(interpolate_pattern) = &options.id_interpolate_pattern + { + interpolate_pattern.as_str() + } else { + "[sha512:contenthash:base64:6]" + }; + + let content = if let Some(MessageDescriptionValue::Str(description)) = &description { + format!("{}#{}", default_message, description) + } else { + default_message.clone() + }; + + interpolate_name(filename, interpolate_pattern, &content) + } else { + id + }; + + MessageDescriptor { + id, + default_message: Some(default_message), + description, + } +} + +fn evaluate_call_expr_message_descriptor( + descriptor_path: &CallExprMessageDescriptorPath, + options: &FormatJSPluginOptions, + filename: &str, +) -> MessageDescriptor { + let id = get_call_expr_message_descriptor_value(&descriptor_path.id, None); + let default_message = get_call_expr_icu_message_value( + &descriptor_path.default_message, + options.preserve_whitespace, + ); + + let description = + get_call_expr_message_descriptor_value_maybe_object(&descriptor_path.description, None); + + let id = if id.is_none() && !default_message.is_empty() { + let interpolate_pattern = if let Some(interpolate_pattern) = &options.id_interpolate_pattern + { + interpolate_pattern.as_str() + } else { + "[sha512:contenthash:base64:6]" + }; + + let content = if let Some(MessageDescriptionValue::Str(description)) = &description { + format!("{}#{}", default_message, description) + } else { + default_message.clone() + }; + interpolate_name(filename, interpolate_pattern, &content) + } else { + id + }; + + MessageDescriptor { + id, + default_message: Some(default_message), + description, + } +} + +fn store_message( + messages: &mut Vec, + descriptor: &MessageDescriptor, + filename: &str, + location: Option<(Loc, Loc)>, +) { + if descriptor.id.is_none() && descriptor.default_message.is_none() { + let handler = &swc_core::plugin::errors::HANDLER; + + handler.with(|handler| { + handler + .struct_err("[React Intl] Message Descriptors require an `id` or `defaultMessage`.") + .emit() + }); + } + + let source_location = if let Some(location) = location { + let (start, end) = location; + + // NOTE: this is not fully identical to babel's test snapshot output + Some(SourceLocation { + file: filename.to_string(), + start: Location { + line: start.line, + col: start.col.to_usize(), + }, + end: Location { + line: end.line, + col: end.col.to_usize(), + }, + }) + } else { + None + }; + + messages.push(ExtractedMessage { + id: descriptor + .id + .as_ref() + .unwrap_or(&"".to_string()) + .to_string(), + default_message: descriptor + .default_message + .as_ref() + .expect("Should be available") + .clone(), + description: descriptor.description.clone(), + loc: source_location, + }); +} + +fn get_message_object_from_expression(expr: Option<&mut ExprOrSpread>) -> Option<&mut Expr> { + if let Some(expr) = expr { + let expr = &mut *expr.expr; + Some(expr) + } else { + None + } +} + +fn assert_object_expression(expr: &Option<&mut Expr>, callee: &Callee) { + let assert_fail = match expr { + Some(expr) => !expr.is_object(), + _ => true, + }; + + if assert_fail { + let prop = if let Callee::Expr(expr) = callee { + if let Expr::Ident(ident) = &**expr { + Some(ident.sym.to_string()) + } else { + None + } + } else { + None + }; + + let handler = &swc_core::plugin::errors::HANDLER; + + handler.with(|handler| { + handler + .struct_err( + &(format!( + r#"[React Intl] `{}` must be called with an object expression + with values that are React Intl Message Descriptors, + also defined as object expressions."#, + prop.unwrap_or_default() + )), + ) + .emit() + }); + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ExtractedMessage { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub default_message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub loc: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceLocation { + pub file: String, + pub start: Location, + pub end: Location, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Location { + pub line: usize, + pub col: usize, +} + +pub struct FormatJSVisitor { + // We may not need Arc in the plugin context - this is only to preserve isomorphic interface + // between plugin & custom transform pass. + source_map: std::sync::Arc, + comments: C, + options: FormatJSPluginOptions, + filename: String, + messages: Vec, + meta: HashMap, + component_names: HashSet, + function_names: HashSet, +} + +impl FormatJSVisitor { + fn new( + source_map: std::sync::Arc, + comments: C, + plugin_options: FormatJSPluginOptions, + filename: &str, + ) -> Self { + let mut function_names: HashSet = Default::default(); + plugin_options + .additional_function_names + .iter() + .for_each(|name| { + function_names.insert(name.to_string()); + }); + function_names.insert("formatMessage".to_string()); + function_names.insert("$formatMessage".to_string()); + + let mut component_names: HashSet = Default::default(); + component_names.insert("FormattedMessage".to_string()); + plugin_options + .additional_component_names + .iter() + .for_each(|name| { + component_names.insert(name.to_string()); + }); + + FormatJSVisitor { + source_map, + comments, + options: plugin_options, + filename: filename.to_string(), + messages: Default::default(), + meta: Default::default(), + component_names, + function_names, + } + } + + fn read_pragma(&mut self, span_lo: BytePos, span_hi: BytePos) { + if let Some(pragma) = &self.options.pragma { + let mut comments = self.comments.get_leading(span_lo).unwrap_or_default(); + comments.append(&mut self.comments.get_leading(span_hi).unwrap_or_default()); + + let pragma = pragma.as_str(); + + for comment in comments { + let comment_text = &*comment.text; + if comment_text.contains(pragma) { + let value = comment_text.split(pragma).nth(1); + if let Some(value) = value { + let value = WHITESPACE_REGEX.split(value.trim()); + for kv in value { + let mut kv = kv.split(":"); + if let Some(k) = kv.next() { + if let Some(v) = kv.next() { + self.meta.insert(k.to_string(), v.to_string()); + } + } + } + } + } + } + } + } + + fn process_message_object(&mut self, message_descriptor: &mut Option<&mut Expr>) { + if let Some(message_obj) = &mut *message_descriptor { + let (lo, hi) = (message_obj.span().lo, message_obj.span().hi); + + if let Expr::Object(obj) = *message_obj { + let properties = &obj.props; + + let descriptor_path = create_message_descriptor_from_call_expr(properties); + + // If the message is already compiled, don't re-compile it + if let Some(default_message) = &descriptor_path.default_message { + if default_message.is_array() { + return; + } + } + + let descriptor = evaluate_call_expr_message_descriptor( + &descriptor_path, + &self.options, + &self.filename, + ); + + let source_location = if self.options.extract_source_location { + Some(( + self.source_map.lookup_char_pos(lo), + self.source_map.lookup_char_pos(hi), + )) + } else { + None + }; + + store_message( + &mut self.messages, + &descriptor, + &self.filename, + source_location, + ); + + // let first_prop = properties.first().is_some(); + + // Insert ID potentially 1st before removing nodes + let id_prop = obj.props.iter().find(|prop| { + if let PropOrSpread::Prop(prop) = prop { + if let Prop::KeyValue(kv) = &**prop { + return match &kv.key { + PropName::Ident(ident) => &*ident.sym == "id", + PropName::Str(str_) => &*str_.value == "id", + _ => false, + }; + } + } + false + }); + + if let Some(descriptor_id) = descriptor.id { + if let Some(id_prop) = id_prop { + let prop = id_prop.as_prop().unwrap(); + let kv = &mut prop.as_key_value().unwrap(); + kv.to_owned().value = Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: descriptor_id.into(), + raw: None, + }))); + } else { + obj.props.insert( + 0, + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new("id".into(), DUMMY_SP)), + value: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: descriptor_id.into(), + raw: None, + }))), + }))), + ) + } + } + + let mut props = vec![]; + for prop in obj.props.drain(..) { + match prop { + PropOrSpread::Prop(mut prop) => { + if let Prop::KeyValue(keyvalue) = &mut *prop { + let key = get_message_descriptor_key_from_call_expr(&keyvalue.key); + if let Some(key) = key { + match key { + "description" => { + // remove description + if descriptor.description.is_some() { + self.comments.take_leading(prop.span().lo); + } else { + props.push(PropOrSpread::Prop(prop)); + } + } + // Pre-parse or remove defaultMessage + "defaultMessage" => { + if self.options.remove_default_message { + // remove defaultMessage + } else { + if let Some(descriptor_default_message) = + descriptor.default_message.as_ref() + { + if self.options.ast { + let mut parser = Parser::new( + descriptor_default_message, + &ParserOptions::new( + false, false, false, false, None, + ), + ); + if let Ok(parsed) = parser.parse() { + let v = serde_json::to_value(&parsed) + .unwrap(); + keyvalue.value = json_value_to_expr(&v); + } + } else { + keyvalue.value = + Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: descriptor_default_message + .as_str() + .into(), + raw: None, + }))); + } + } + + props.push(PropOrSpread::Prop(prop)); + } + } + _ => props.push(PropOrSpread::Prop(prop)), + } + } else { + props.push(PropOrSpread::Prop(prop)); + } + } else { + props.push(PropOrSpread::Prop(prop)); + } + } + _ => props.push(prop), + } + } + + obj.props = props; + } + } + } +} + +impl VisitMut for FormatJSVisitor { + noop_visit_mut_type!(); + + fn visit_mut_jsx_opening_element(&mut self, jsx_opening_elem: &mut JSXOpeningElement) { + jsx_opening_elem.visit_mut_children_with(self); + + let name = &jsx_opening_elem.name; + + if let JSXElementName::Ident(ident) = name { + if !self.component_names.contains(&*ident.sym) { + return; + } + } + + let descriptor_path = create_message_descriptor_from_jsx_attr(&jsx_opening_elem.attrs); + + // In order for a default message to be extracted when + // declaring a JSX element, it must be done with standard + // `key=value` attributes. But it's completely valid to + // write ``, because it will be + // skipped here and extracted elsewhere. The descriptor will + // be extracted only (storeMessage) if a `defaultMessage` prop. + if descriptor_path.default_message.is_none() { + return; + } + + // Evaluate the Message Descriptor values in a JSX + // context, then store it. + let descriptor = + evaluate_jsx_message_descriptor(&descriptor_path, &self.options, &self.filename); + + let source_location = if self.options.extract_source_location { + Some(( + self.source_map.lookup_char_pos(jsx_opening_elem.span().lo), + self.source_map.lookup_char_pos(jsx_opening_elem.span().hi), + )) + } else { + None + }; + + store_message( + &mut self.messages, + &descriptor, + &self.filename, + source_location, + ); + + let id_attr = jsx_opening_elem.attrs.iter().find(|attr| match attr { + JSXAttrOrSpread::JSXAttr(attr) => { + if let JSXAttrName::Ident(ident) = &attr.name { + &*ident.sym == "id" + } else { + false + } + } + _ => false, + }); + + let first_attr = !jsx_opening_elem.attrs.is_empty(); + + // Do not support overrideIdFn, only support idInterpolatePattern + if descriptor.id.is_some() { + if let Some(id_attr) = id_attr { + if let JSXAttrOrSpread::JSXAttr(attr) = id_attr { + attr.to_owned().value = Some(JSXAttrValue::Lit(Lit::Str(Str::from( + descriptor.id.unwrap(), + )))); + } + } else if first_attr { + jsx_opening_elem.attrs.insert( + 0, + JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(IdentName::new("id".into(), DUMMY_SP)), + value: Some(JSXAttrValue::Lit(Lit::Str(Str::from( + descriptor.id.unwrap(), + )))), + }), + ) + } + } + + let mut attrs = vec![]; + for attr in jsx_opening_elem.attrs.drain(..) { + match attr { + JSXAttrOrSpread::JSXAttr(attr) => { + let key = get_message_descriptor_key_from_jsx(&attr.name); + match key { + "description" => { + // remove description + if descriptor.description.is_some() { + self.comments.take_leading(attr.span.lo); + } else { + attrs.push(JSXAttrOrSpread::JSXAttr(attr)); + } + } + "defaultMessage" => { + if self.options.remove_default_message { + // remove defaultMessage + } else { + /* + if (ast && descriptor.defaultMessage) { + defaultMessageAttr + .get('value') + .replaceWith(t.jsxExpressionContainer(t.nullLiteral())) + const valueAttr = defaultMessageAttr.get( + 'value' + ) as NodePath + valueAttr + .get('expression') + .replaceWithSourceString( + JSON.stringify(parse(descriptor.defaultMessage)) + ) + } + */ + attrs.push(JSXAttrOrSpread::JSXAttr(attr)) + } + } + _ => attrs.push(JSXAttrOrSpread::JSXAttr(attr)), + } + } + _ => attrs.push(attr), + } + } + + jsx_opening_elem.attrs = attrs.to_vec(); + + // tag_as_extracted(); + } + + fn visit_mut_call_expr(&mut self, call_expr: &mut CallExpr) { + call_expr.visit_mut_children_with(self); + + let callee = &call_expr.callee; + let args = &mut call_expr.args; + + if let Callee::Expr(callee_expr) = callee { + if let Expr::Ident(ident) = &**callee_expr { + if &*ident.sym == "defineMessage" || &*ident.sym == "defineMessages" { + let first_arg = args.get_mut(0); + let mut message_obj = get_message_object_from_expression(first_arg); + + assert_object_expression(&message_obj, callee); + + if &*ident.sym == "defineMessage" { + self.process_message_object(&mut message_obj); + } else if let Some(Expr::Object(obj)) = message_obj { + for prop in obj.props.iter_mut() { + if let PropOrSpread::Prop(prop) = &mut *prop { + if let Prop::KeyValue(kv) = &mut **prop { + self.process_message_object(&mut Some(&mut *kv.value)); + } + } + } + } + } + } + } + + // Check that this is `intl.formatMessage` call + if let Callee::Expr(expr) = &callee { + let is_format_message_call = match &**expr { + Expr::Ident(ident) if self.function_names.contains(&*ident.sym) => true, + Expr::Member(member_expr) => { + if let MemberProp::Ident(ident) = &member_expr.prop { + self.function_names.contains(&*ident.sym) + } else { + false + } + } + _ => false, + }; + + if is_format_message_call { + let message_descriptor = args.get_mut(0); + if let Some(message_descriptor) = message_descriptor { + if message_descriptor.expr.is_object() { + self.process_message_object(&mut Some(message_descriptor.expr.as_mut())); + } + } + } + } + } + + fn visit_mut_module_items(&mut self, items: &mut Vec) { + /* + if self.is_instrumented_already() { + return; + } + */ + + for item in items { + self.read_pragma(item.span().lo, item.span().hi); + item.visit_mut_children_with(self); + } + + if self.options.__debug_extracted_messages_comment { + let messages_json_str = + serde_json::to_string(&self.messages).expect("Should be serializable"); + let meta_json_str = serde_json::to_string(&self.meta).expect("Should be serializable"); + + // Append extracted messages to the end of the file as stringified JSON + // comments. SWC's plugin does not support to return aribitary data + // other than transformed codes, There's no way to pass extracted + // messages after transform. This is not a public interface; + // currently for debugging / testing purpose only. + self.comments.add_trailing( + Span::dummy_with_cmt().hi, + Comment { + kind: CommentKind::Block, + span: Span::dummy_with_cmt(), + text: format!( + "__formatjs__messages_extracted__::{{\"messages\":{}, \"meta\":{}}}", + messages_json_str, meta_json_str + ) + .into(), + }, + ); + } + } +} + +fn json_value_to_expr(json_value: &serde_json::Value) -> Box { + Box::new(match json_value { + serde_json::Value::Null => { + Expr::Lit(Lit::Null(swc_core::ecma::ast::Null { span: DUMMY_SP })) + } + serde_json::Value::Bool(v) => Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: *v, + })), + serde_json::Value::Number(v) => Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + raw: None, + value: v.as_f64().unwrap(), + })), + serde_json::Value::String(v) => Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + raw: None, + value: v.as_str().into(), + })), + serde_json::Value::Array(v) => Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: v + .iter() + .map(|elem| { + Some(ExprOrSpread { + spread: None, + expr: json_value_to_expr(elem), + }) + }) + .collect(), + }), + serde_json::Value::Object(v) => Expr::Object(ObjectLit { + span: DUMMY_SP, + props: v + .iter() + .map(|(key, value)| { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new(key.to_string().into(), DUMMY_SP)), + value: json_value_to_expr(value), + }))) + }) + .collect(), + }), + }) +} + +pub fn create_formatjs_visitor( + source_map: std::sync::Arc, + comments: C, + plugin_options: FormatJSPluginOptions, + filename: &str, +) -> FormatJSVisitor { + FormatJSVisitor::new(source_map, comments, plugin_options, filename) +} diff --git a/packages/loadable-components/CHANGELOG.md b/packages/loadable-components/CHANGELOG.md index d3b0505e0..988af0fba 100644 --- a/packages/loadable-components/CHANGELOG.md +++ b/packages/loadable-components/CHANGELOG.md @@ -1,5 +1,11 @@ # @swc/plugin-loadable-components +## 4.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 4.0.0 ### Major Changes diff --git a/packages/loadable-components/README.md b/packages/loadable-components/README.md index 66a1ee4eb..6f9b1ea68 100644 --- a/packages/loadable-components/README.md +++ b/packages/loadable-components/README.md @@ -23,6 +23,12 @@ By default `loadable-components` are configured to transform dynamic imports use # @swc/plugin-loadable-components +## 4.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 4.0.0 ### Major Changes diff --git a/packages/loadable-components/package.json b/packages/loadable-components/package.json index 8e58c77a6..2a689461f 100644 --- a/packages/loadable-components/package.json +++ b/packages/loadable-components/package.json @@ -1,6 +1,6 @@ { "name": "@swc/plugin-loadable-components", - "version": "4.0.0", + "version": "4.0.1", "description": "SWC plugin for `@loadable/components`", "main": "swc_plugin_loadable_components.wasm", "scripts": { diff --git a/packages/loadable-components/src/lib.rs b/packages/loadable-components/src/lib.rs index efdfc8bc7..5a2dda235 100644 --- a/packages/loadable-components/src/lib.rs +++ b/packages/loadable-components/src/lib.rs @@ -97,6 +97,7 @@ where } } + #[allow(clippy::only_used_in_recursion)] fn is_supported(&self, e: &Expr) -> bool { match e { Expr::Paren(e) => self.is_supported(&e.expr), diff --git a/packages/relay/CHANGELOG.md b/packages/relay/CHANGELOG.md index 22b4c117f..c82c332b0 100644 --- a/packages/relay/CHANGELOG.md +++ b/packages/relay/CHANGELOG.md @@ -1,5 +1,11 @@ # @swc/plugin-relay +## 5.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 5.0.0 ### Major Changes diff --git a/packages/relay/README.md b/packages/relay/README.md index 947b8dea8..d2451543a 100644 --- a/packages/relay/README.md +++ b/packages/relay/README.md @@ -104,6 +104,12 @@ In this example typescript graphql files will output transpiled import path of ` # @swc/plugin-relay +## 5.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 5.0.0 ### Major Changes diff --git a/packages/relay/package.json b/packages/relay/package.json index 90ff96648..b640ddd42 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@swc/plugin-relay", - "version": "5.0.0", + "version": "5.0.1", "description": "SWC plugin for relay", "main": "swc_plugin_relay.wasm", "types": "./types.d.ts", diff --git a/packages/relay/transform/src/lib.rs b/packages/relay/transform/src/lib.rs index 1cd365ab6..f8b567c53 100644 --- a/packages/relay/transform/src/lib.rs +++ b/packages/relay/transform/src/lib.rs @@ -227,7 +227,10 @@ fn unique_ident_name_from_operation_name(operation_name: &str) -> String { #[derive(Debug)] enum BuildRequirePathError { FileNameNotReal, - ArtifactDirectoryExpected { file_name: String }, + ArtifactDirectoryExpected { + #[allow(unused)] + file_name: String, + }, } impl Relay { diff --git a/packages/transform-imports/CHANGELOG.md b/packages/transform-imports/CHANGELOG.md index 35195ed05..7402721a4 100644 --- a/packages/transform-imports/CHANGELOG.md +++ b/packages/transform-imports/CHANGELOG.md @@ -1,5 +1,11 @@ # @swc/plugin-transform-imports +## 5.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 5.0.0 ### Major Changes diff --git a/packages/transform-imports/README.md b/packages/transform-imports/README.md index 567886fc8..93054f312 100644 --- a/packages/transform-imports/README.md +++ b/packages/transform-imports/README.md @@ -18,6 +18,12 @@ # @swc/plugin-transform-imports +## 5.0.1 + +### Patch Changes + +- 4ff3b22: Move formatjs plugin to official plugin repository + ## 5.0.0 ### Major Changes diff --git a/packages/transform-imports/package.json b/packages/transform-imports/package.json index b40a2ec19..10c2bc30c 100644 --- a/packages/transform-imports/package.json +++ b/packages/transform-imports/package.json @@ -1,6 +1,6 @@ { "name": "@swc/plugin-transform-imports", - "version": "5.0.0", + "version": "5.0.1", "description": "SWC plugin for https://www.npmjs.com/package/babel-plugin-transform-imports", "main": "swc_plugin_transform_imports.wasm", "scripts": { diff --git a/packages/transform-imports/transform/src/lib.rs b/packages/transform-imports/transform/src/lib.rs index e62526c02..9fbdf1d87 100644 --- a/packages/transform-imports/transform/src/lib.rs +++ b/packages/transform-imports/transform/src/lib.rs @@ -70,7 +70,7 @@ enum CtxData<'a> { Array(&'a [&'a str]), } -impl<'a> Rewriter<'a> { +impl Rewriter<'_> { fn new_path(&self, name_str: Option<&str>) -> Atom { let mut ctx: HashMap<&str, CtxData> = HashMap::new(); ctx.insert("matches", CtxData::Array(&self.group[..]));