Repository: projectfluent/fluent-rs (rust)

Issue:
Add FSI/PDI marks support
We should port the `withIsolation` part for the Resolver.

Pull request description:
Enable direction isolation
Fixes #96

Patch:
diff --git a/fluent-bundle/examples/functions.rs b/fluent-bundle/examples/functions.rs
index 765f008..845240a 100644
--- a/fluent-bundle/examples/functions.rs
+++ b/fluent-bundle/examples/functions.rs
@@ -57,7 +57,7 @@ fn main() {
     let (value, _) = bundle
         .format("hello-world", None)
         .expect("Failed to format a message.");
-    assert_eq!(&value, "Hey there! I'm a function!");
+    assert_eq!(&value, "Hey there! \u{2068}I'm a function!\u{2069}");
 
     let (value, _) = bundle
         .format("meaning-of-life", None)
diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs
index 806236e..3697365 100644
--- a/fluent-bundle/src/bundle.rs
+++ b/fluent-bundle/src/bundle.rs
@@ -47,7 +47,7 @@ pub struct Message<'m> {
 ///
 /// let (value, _) = bundle.format("intro", Some(&args))
 ///     .expect("Failed to format a message.");
-/// assert_eq!(&value, "Welcome, Rustacean.");
+/// assert_eq!(&value, "Welcome, \u{2068}Rustacean\u{2069}.");
 ///
 /// ```
 ///
@@ -89,6 +89,7 @@ pub struct FluentBundle<'bundle, R> {
     pub(crate) resources: Vec<R>,
     pub(crate) entries: HashMap<String, Entry<'bundle>>,
     pub(crate) plural_rules: IntlPluralRules,
+    pub(crate) use_isolating: bool,
 }
 
 impl<'bundle, R> FluentBundle<'bundle, R> {
@@ -128,6 +129,7 @@ impl<'bundle, R> FluentBundle<'bundle, R> {
             resources: vec![],
             entries: HashMap::new(),
             plural_rules: pr,
+            use_isolating: true,
         }
     }
 
@@ -217,6 +219,10 @@ impl<'bundle, R> FluentBundle<'bundle, R> {
         }
     }
 
+    pub fn set_use_isolating(&mut self, value: bool) {
+        self.use_isolating = value;
+    }
+
     /// Returns true if this bundle contains a message with the given id.
     ///
     /// # Examples
@@ -311,7 +317,7 @@ impl<'bundle, R> FluentBundle<'bundle, R> {
     ///
     /// let (value, _) = bundle.format("intro", Some(&args))
     ///     .expect("Failed to format a message.");
-    /// assert_eq!(&value, "Welcome, Rustacean.");
+    /// assert_eq!(&value, "Welcome, \u{2068}Rustacean\u{2069}.");
     ///
     /// ```
     ///
diff --git a/fluent-bundle/src/entry.rs b/fluent-bundle/src/entry.rs
index d953041..7e5bb55 100644
--- a/fluent-bundle/src/entry.rs
+++ b/fluent-bundle/src/entry.rs
@@ -9,8 +9,8 @@ use crate::bundle::FluentBundle;
 use crate::resource::FluentResource;
 use crate::types::FluentValue;
 
-pub type FluentFunction<'bundle> = Box<dyn
-    'bundle
+pub type FluentFunction<'bundle> = Box<
+    dyn 'bundle
         + for<'a> Fn(&[FluentValue<'a>], &HashMap<&str, FluentValue<'a>>) -> FluentValue<'a>
         + Send
         + Sync,
diff --git a/fluent-bundle/src/lib.rs b/fluent-bundle/src/lib.rs
index 5317b1d..841d46b 100644
--- a/fluent-bundle/src/lib.rs
+++ b/fluent-bundle/src/lib.rs
@@ -40,7 +40,7 @@
 //!     .format("intro", Some(&args))
 //!     .expect("Failed to format a message.");
 //!
-//! assert_eq!(value, "Welcome, John.");
+//! assert_eq!(value, "Welcome, \u{2068}John\u{2069}.");
 //! ```
 
 #[macro_use]
diff --git a/fluent-bundle/src/resolve.rs b/fluent-bundle/src/resolve.rs
index 4978132..8b83061 100644
--- a/fluent-bundle/src/resolve.rs
+++ b/fluent-bundle/src/resolve.rs
@@ -10,6 +10,7 @@ use std::borrow::Borrow;
 use std::cell::RefCell;
 use std::collections::hash_map::DefaultHasher;
 use std::collections::HashMap;
+use std::fmt::Write;
 use std::hash::{Hash, Hasher};
 
 use fluent_syntax::ast;
@@ -105,9 +106,7 @@ where
     if value.elements.len() == 1 {
         return match value.elements[0] {
             ast::PatternElement::TextElement(s) => FluentValue::String(s.into()),
-            ast::PatternElement::Placeable(ref p) => {
-                scope.track(entry, |scope| p.resolve(scope))
-            }
+            ast::PatternElement::Placeable(ref p) => scope.track(entry, |scope| p.resolve(scope)),
         };
     }
 
@@ -119,7 +118,24 @@ where
             }
             ast::PatternElement::Placeable(p) => {
                 let result = scope.track(entry, |scope| p.resolve(scope));
-                string.push_str(&result.to_string());
+                let needs_isolation = scope.bundle.use_isolating
+                    && match p {
+                        ast::Expression::InlineExpression(
+                            ast::InlineExpression::MessageReference { .. },
+                        )
+                        | ast::Expression::InlineExpression(
+                            ast::InlineExpression::TermReference { .. },
+                        )
+                        | ast::Expression::InlineExpression(
+                            ast::InlineExpression::StringLiteral { .. },
+                        ) => false,
+                        _ => true,
+                    };
+                if needs_isolation {
+                    write!(string, "\u{2068}{}\u{2069}", result).expect("Writing succeeded");
+                } else {
+                    write!(string, "{}", result).expect("Writing succeeded");
+                };
             }
         }
     }
@@ -175,7 +191,24 @@ impl<'source> ResolveValue<'source> for ast::Pattern<'source> {
                 }
                 ast::PatternElement::Placeable(p) => {
                     let result = p.resolve(scope).to_string();
-                    string.push_str(&result);
+                    let needs_isolation = scope.bundle.use_isolating
+                        && match p {
+                            ast::Expression::InlineExpression(
+                                ast::InlineExpression::MessageReference { .. },
+                            )
+                            | ast::Expression::InlineExpression(
+                                ast::InlineExpression::TermReference { .. },
+                            )
+                            | ast::Expression::InlineExpression(
+                                ast::InlineExpression::StringLiteral { .. },
+                            ) => false,
+                            _ => true,
+                        };
+                    if needs_isolation {
+                        write!(string, "\u{2068}{}\u{2069}", result).expect("Writing succeeded");
+                    } else {
+                        write!(string, "{}", result).expect("Writing succeeded");
+                    };
                 }
             }
         }


Test patch:
diff --git a/fluent-bundle/tests/bundle_test.rs b/fluent-bundle/tests/bundle_test.rs
index 476eb8f..30ee55e 100644
--- a/fluent-bundle/tests/bundle_test.rs
+++ b/fluent-bundle/tests/bundle_test.rs
@@ -1,5 +1,7 @@
 mod helpers;
 
+use std::collections::HashMap;
+
 use self::helpers::{
     assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors,
 };
@@ -66,3 +68,32 @@ bar =
     assert_eq!(bundle.has_message("missing"), false);
     assert_eq!(bundle.has_message("-missing"), false);
 }
+
+#[test]
+fn bundle_use_isolating() {
+    let res = assert_get_resource_from_str_no_errors(
+        "
+foo = Foo
+bar = Foo { $name } bar
+    ",
+    );
+    let mut bundle = assert_get_bundle_no_errors(&res, None);
+
+    // By default `assert_get_bundle_no_errors` sets isolating to false.
+    bundle.set_use_isolating(true);
+
+    assert_format_no_errors(bundle.format("foo", None), "Foo");
+    let mut args = HashMap::new();
+    args.insert("name", "John".into());
+    assert_format_no_errors(
+        bundle.format("bar", Some(&args)),
+        "Foo \u{2068}John\u{2069} bar",
+    );
+
+    bundle.set_use_isolating(false);
+
+    assert_format_no_errors(bundle.format("foo", None), "Foo");
+    let mut args = HashMap::new();
+    args.insert("name", "John".into());
+    assert_format_no_errors(bundle.format("bar", Some(&args)), "Foo John bar");
+}
diff --git a/fluent-bundle/tests/helpers/mod.rs b/fluent-bundle/tests/helpers/mod.rs
index 7b4c588..0b0f165 100644
--- a/fluent-bundle/tests/helpers/mod.rs
+++ b/fluent-bundle/tests/helpers/mod.rs
@@ -49,6 +49,7 @@ pub fn assert_get_bundle_no_errors<'a, R: Borrow<FluentResource>>(
     locale: Option<&str>,
 ) -> FluentBundle<R> {
     let mut bundle = FluentBundle::new(&[locale.unwrap_or("x-testing")]);
+    bundle.set_use_isolating(false);
     bundle
         .add_resource(res)
         .expect("Failed to add FluentResource to FluentBundle.");