[geary/mjog/mail-merge-plugin: 1/10] MailMerge.Processor: Add field parser object, use in `contains_field`




commit 27dbd523175bf94822471d54e7ab91ff5e04bd97
Author: Michael Gratton <mike vee net>
Date:   Thu Aug 6 13:21:26 2020 +1000

    MailMerge.Processor: Add field parser object, use in `contains_field`
    
    Add a reusable private parser struct, reimplement `contains_field` using
    it. Add unit tests for that and `is_mail_merge_template`.

 .../plugin/mail-merge/mail-merge-processor.vala    | 106 +++++++++++---
 .../mail-merge/mail-merge-test-processor.vala      | 153 +++++++++++++++++++++
 src/client/plugin/mail-merge/mail-merge-test.vala  |   1 +
 src/client/plugin/mail-merge/mail-merge.vala       |  40 ++----
 src/client/plugin/mail-merge/meson.build           |   1 +
 5 files changed, 255 insertions(+), 46 deletions(-)
---
diff --git a/src/client/plugin/mail-merge/mail-merge-processor.vala 
b/src/client/plugin/mail-merge/mail-merge-processor.vala
index da0295f97..90c4fe010 100644
--- a/src/client/plugin/mail-merge/mail-merge-processor.vala
+++ b/src/client/plugin/mail-merge/mail-merge-processor.vala
@@ -19,6 +19,81 @@ public class MailMerge.Processor : GLib.Object {
     private const string FIELD_END = "}}";
 
 
+    private struct Parser {
+
+        public unowned string text;
+        public int index;
+        public bool spent;
+        public bool at_field_start;
+        public bool at_field_end;
+
+        public Parser(string text) {
+            this.text = text;
+            this.index = 0;
+            this.spent = (text.length == 0);
+            this.at_field_start = text.has_prefix(FIELD_START);
+            this.at_field_end = false;
+        }
+
+        public string read_text() {
+            this.at_field_end = false;
+
+            int start = this.index;
+            char c = this.text[this.index];
+            while (c != 0) {
+                this.index++;
+                if (c == FIELD_START[0] &&
+                    this.text[this.index] == FIELD_START[1]) {
+                    this.index--;
+                    this.at_field_start = true;
+                    break;
+                }
+                c = this.text[this.index];
+            }
+            if (c == 0) {
+                this.spent = true;
+            }
+            return this.text.slice(start, this.index);
+        }
+
+        public string read_field() {
+            this.at_field_start = false;
+
+            // Skip the opening field separator
+            this.index += FIELD_START.length;
+
+            int start = this.index;
+            char c = this.text[this.index];
+            while (c != 0) {
+                this.index++;
+                if (c == FIELD_END[0]) {
+                    if (this.text[this.index] == FIELD_END[1]) {
+                        this.index++;
+                        this.at_field_end = true;
+                        break;
+                    }
+                }
+                c = this.text[this.index];
+            }
+            var end = this.index;
+            if (this.at_field_end) {
+                // Don't include the closing field separator
+                end -= FIELD_END.length;
+            } else {
+                // No closing field separator found, so not a valid
+                // field. Move start back so it includes the opening
+                // field separator
+                start -= FIELD_START.length;
+            }
+            if (c == 0 || this.index == this.text.length) {
+                this.spent = true;
+            }
+            return this.text.slice(start, end);
+        }
+
+    }
+
+
     public static string to_field(string name) {
         return FIELD_START + name + FIELD_END;
     }
@@ -51,30 +126,21 @@ public class MailMerge.Processor : GLib.Object {
         return found;
     }
 
-    private static bool contains_field(string value) {
+    public static bool contains_field(string text) {
+        var parser = Parser(text);
         var found = false;
-        var index = 0;
-        while (!found) {
-            var field_start = value.index_of(FIELD_START, index);
-            if (field_start < 0) {
-                break;
+        while (!parser.spent) {
+            if (parser.at_field_start) {
+                parser.read_field();
+                if (parser.at_field_end) {
+                    found = true;
+                    break;
+                }
+            } else {
+                parser.read_text();
             }
-            found = parse_field((string) value.data[field_start:-1]) != null;
-            index = field_start + 1;
         }
         return found;
     }
 
-    private static string? parse_field(string value) {
-        string? field = null;
-        if (value.has_prefix(FIELD_START)) {
-            int start = FIELD_START.length;
-            int end = value.index_of(FIELD_END, start);
-            if (end >= 0) {
-                field = value.substring(start, end - FIELD_END.length).strip();
-            }
-        }
-        return field;
-    }
-
 }
diff --git a/src/client/plugin/mail-merge/mail-merge-test-processor.vala 
b/src/client/plugin/mail-merge/mail-merge-test-processor.vala
new file mode 100644
index 000000000..d1c34f93e
--- /dev/null
+++ b/src/client/plugin/mail-merge/mail-merge-test-processor.vala
@@ -0,0 +1,153 @@
+/*
+ * Copyright © 2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class MailMerge.TestProcessor : ValaUnit.TestCase {
+
+
+    private class MockEmailIdentifier : Geary.EmailIdentifier {
+
+
+        private int id;
+
+
+        public MockEmailIdentifier(int id) {
+            this.id = id;
+        }
+
+        public override uint hash() {
+            return GLib.int_hash(this.id);
+        }
+
+        public override bool equal_to(Geary.EmailIdentifier other) {
+            return (
+                this.get_type() == other.get_type() &&
+                this.id == ((MockEmailIdentifier) other).id
+            );
+        }
+
+
+        public override string to_string() {
+            return "%s(%d)".printf(
+                this.get_type().name(),
+                this.id
+            );
+        }
+
+        public override GLib.Variant to_variant() {
+            return new GLib.Variant.int32(id);
+        }
+
+        public override int natural_sort_comparator(Geary.EmailIdentifier other) {
+            MockEmailIdentifier? other_mock = other as MockEmailIdentifier;
+            return (other_mock == null) ? 1 : this.id - other_mock.id;
+        }
+
+    }
+
+
+    public TestProcessor() {
+        base("MailMerge.TestProcessor");
+        add_test("contains_field", contains_field);
+        add_test("is_mail_merge_template", is_mail_merge_template);
+    }
+
+    public void contains_field() throws GLib.Error {
+        assert_true(Processor.contains_field("{{test}}"), "{{test}}");
+        assert_true(Processor.contains_field("test {{test}}"), "test {{test}}");
+        assert_true(Processor.contains_field("test {{test}} test"), "test {{test}} test");
+        assert_true(Processor.contains_field("test {{test}}"), "test {{test}}");
+
+        assert_false(Processor.contains_field("{{test"), "{{test");
+        assert_false(Processor.contains_field("{{test}"), "{{test}");
+        assert_false(Processor.contains_field("{test}"), "{test}");
+        assert_false(Processor.contains_field("test}}"), "test}}");
+        assert_false(Processor.contains_field("test {test"), "test {test");
+        assert_false(Processor.contains_field("test {"), "test {");
+        assert_false(Processor.contains_field("test {{"), "test {{");
+    }
+
+    public void is_mail_merge_template() throws GLib.Error {
+        assert_false(
+            Processor.is_mail_merge_template(string_to_email(EMPTY_MESSAGE)),
+            "empty message"
+        );
+        assert_true(
+            Processor.is_mail_merge_template(string_to_email(TEMPLATE_SUBJECT)),
+            "subject"
+        );
+        assert_true(
+            Processor.is_mail_merge_template(string_to_email(TEMPLATE_BODY)),
+            "body"
+        );
+    }
+
+    private Geary.Email string_to_email(string message_text)
+        throws GLib.Error {
+        var message = new Geary.RFC822.Message.from_buffer(
+            new Geary.Memory.StringBuffer(message_text.replace("\n","\r\n"))
+        );
+        return new Geary.Email.from_message(
+            new MockEmailIdentifier(0), message
+        );
+    }
+
+    private const string EMPTY_MESSAGE = """From: Alice <alice example net>
+Sender: Bob <bob example net>
+To: Charlie <charlie example net>
+CC: Dave <dave example net>
+BCC: Eve <eve example net>
+Reply-To: "Alice: Personal Account" <alice example org>
+Subject: Re: Basic text/plain message
+Date: Fri, 21 Nov 1997 10:01:10 -0600
+Message-ID: <3456 example net>
+In-Reply-To: <1234@local.machine.example>
+References: <1234@local.machine.example>
+X-Mailer: Geary Test Suite 1.0
+
+This is the first line.
+
+This is the second line.
+
+""";
+
+    private const string TEMPLATE_SUBJECT = """From: Alice <alice example net>
+Sender: Bob <bob example net>
+To: Charlie <charlie example net>
+CC: Dave <dave example net>
+BCC: Eve <eve example net>
+Reply-To: "Alice: Personal Account" <alice example org>
+Subject: {{hello}}
+Date: Fri, 21 Nov 1997 10:01:10 -0600
+Message-ID: <3456 example net>
+In-Reply-To: <1234@local.machine.example>
+References: <1234@local.machine.example>
+X-Mailer: Geary Test Suite 1.0
+
+This is the first line.
+
+This is the second line.
+
+""";
+
+    private const string TEMPLATE_BODY = """From: Alice <alice example net>
+Sender: Bob <bob example net>
+To: Charlie <charlie example net>
+CC: Dave <dave example net>
+BCC: Eve <eve example net>
+Reply-To: "Alice: Personal Account" <alice example org>
+Subject: Re: Basic text/plain message
+Date: Fri, 21 Nov 1997 10:01:10 -0600
+Message-ID: <3456 example net>
+In-Reply-To: <1234@local.machine.example>
+References: <1234@local.machine.example>
+X-Mailer: Geary Test Suite 1.0
+
+Hello {{name}}!
+
+""";
+
+}
diff --git a/src/client/plugin/mail-merge/mail-merge-test.vala 
b/src/client/plugin/mail-merge/mail-merge-test.vala
index 22fab9ee0..39cc6d10d 100644
--- a/src/client/plugin/mail-merge/mail-merge-test.vala
+++ b/src/client/plugin/mail-merge/mail-merge-test.vala
@@ -18,6 +18,7 @@ int main(string[] args) {
 
     GLib.TestSuite root = GLib.TestSuite.get_root();
     root.add_suite(new MailMerge.TestReader().suite);
+    root.add_suite(new MailMerge.TestProcessor().suite);
 
     GLib.MainLoop loop = new GLib.MainLoop();
     int ret = -1;
diff --git a/src/client/plugin/mail-merge/mail-merge.vala b/src/client/plugin/mail-merge/mail-merge.vala
index de9317a85..798bbed1e 100644
--- a/src/client/plugin/mail-merge/mail-merge.vala
+++ b/src/client/plugin/mail-merge/mail-merge.vala
@@ -21,10 +21,6 @@ public class Plugin.MailMerge :
     PluginBase, FolderExtension, EmailExtension, TrustedExtension {
 
 
-    private const string FIELD_START = "{{";
-    private const string FIELD_END = "}}";
-
-
     // Translators: Templates folder name alternatives. Separate names
     // using a vertical bar and put the most common localized name to
     // the front for the default. English names do not need to be
@@ -125,21 +121,6 @@ public class Plugin.MailMerge :
         this.folder_names.clear();
     }
 
-    private async bool is_mail_merge_template(Email plugin) {
-        bool is_merge = false;
-        try {
-            Geary.Email? email = yield load_merge_email(plugin);
-            if (email != null) {
-                is_merge = global::MailMerge.Processor.is_mail_merge_template(
-                    email
-                );
-            }
-        } catch (GLib.Error err) {
-            warning("Unable to load merge template: %s", err.message);
-        }
-        return is_merge;
-    }
-
     private async void edit_email(EmailIdentifier id) {
         try {
             var composer = yield this.plugin_application.compose_with_context(
@@ -213,13 +194,20 @@ public class Plugin.MailMerge :
         } catch (GLib.Error err) {
             warning("Could not load folders for email: %s", err.message);
         }
-        if (containing.any_match((f) => f.display_name in this.folder_names) &&
-            yield is_mail_merge_template(target)) {
-            this.email.add_email_info_bar(
-                target.identifier,
-                new_template_email_info_bar(target.identifier),
-                INFO_BAR_PRIORITY
-            );
+        if (containing.any_match((f) => f.display_name in this.folder_names)) {
+            try {
+                var email = yield load_merge_email(target);
+                if (global::MailMerge.Processor.is_mail_merge_template(email)) {
+                    this.email.add_email_info_bar(
+                        target.identifier,
+                        new_template_email_info_bar(target.identifier),
+                        INFO_BAR_PRIORITY
+                    );
+                }
+            } catch (GLib.Error err) {
+                warning("Error checking email for merge templates: %s",
+                        err.message);
+            }
         }
     }
 
diff --git a/src/client/plugin/mail-merge/meson.build b/src/client/plugin/mail-merge/meson.build
index dcdc67e7f..fd3e9f0ec 100644
--- a/src/client/plugin/mail-merge/meson.build
+++ b/src/client/plugin/mail-merge/meson.build
@@ -14,6 +14,7 @@ lib_src = files(
 )
 
 test_src = files(
+  'mail-merge-test-processor.vala',
   'mail-merge-test-reader.vala',
   'mail-merge-test.vala'
 )


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]