diff --git a/app.js b/app.js
index fa5c76d7..ba4e27bd 100644
--- a/app.js
+++ b/app.js
@@ -29,6 +29,7 @@ let templates = require('./routes/templates');
let campaigns = require('./routes/campaigns');
let links = require('./routes/links');
let fields = require('./routes/fields');
+let forms = require('./routes/forms');
let segments = require('./routes/segments');
let triggers = require('./routes/triggers');
let webhooks = require('./routes/webhooks');
@@ -54,6 +55,7 @@ if (config.www.proxy) {
app.disable('x-powered-by');
hbs.registerPartials(__dirname + '/views/partials');
+hbs.registerPartials(__dirname + '/views/subscription/partials/');
/**
* We need this helper to make sure that we consume flash messages only
@@ -80,7 +82,9 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer
let rows = [];
messages[key].forEach(message => {
- rows.push(hbs.handlebars.escapeExpression(message));
+ message = hbs.handlebars.escapeExpression(message);
+ message = message.replace(/(\r\n|\n|\r)/gm, '
');
+ rows.push(message);
});
if (rows.length > 1) {
@@ -205,6 +209,7 @@ app.use('/campaigns', campaigns);
app.use('/settings', settings);
app.use('/links', links);
app.use('/fields', fields);
+app.use('/forms', forms);
app.use('/segments', segments);
app.use('/triggers', triggers);
app.use('/webhooks', webhooks);
diff --git a/config/default.toml b/config/default.toml
index 93fbab01..2d8849ab 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -38,6 +38,9 @@ language="en"
# Inject custom scripts in layout.hbs
# customscripts=["/custom/hello-world.js"]
+# Inject custom scripts in subscription/layout.mjml.hbs
+# customsubscriptionscripts=["/custom/hello-world.js"]
+
# If you start out as a root user (eg. if you want to use ports lower than 1000)
# then you can downgrade the user once all services are up and running
#user="nobody"
diff --git a/languages/mailtrain.pot b/languages/mailtrain.pot
index 5da70af6..ae41f1d1 100644
--- a/languages/mailtrain.pot
+++ b/languages/mailtrain.pot
@@ -8,11 +8,10 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2017-03-12 00:22+0000\n"
+"POT-Creation-Date: 2017-03-19 12:36+0000\n"
#: views/archive/layout.hbs:1
#: views/layout.hbs:1
-#: views/subscription/layout.hbs:1
msgid "Self hosted email newsletter app"
msgstr ""
@@ -36,6 +35,9 @@ msgstr ""
#: views/lists/fields/create.hbs:1
#: views/lists/fields/edit.hbs:1
#: views/lists/fields/fields.hbs:1
+#: views/lists/forms/create.hbs:1
+#: views/lists/forms/edit.hbs:1
+#: views/lists/forms/forms.hbs:1
#: views/lists/lists.hbs:1
#: views/lists/segments/create.hbs:1
#: views/lists/segments/edit.hbs:1
@@ -64,7 +66,7 @@ msgstr ""
#: views/users/forgot.hbs:1
#: views/users/login.hbs:1
#: views/users/reset.hbs:1
-#: app.js:172
+#: app.js:176
msgid "Home"
msgstr ""
@@ -84,7 +86,7 @@ msgstr ""
#: views/campaigns/unsubscribed.hbs:2
#: views/campaigns/upload-attachment.hbs:2
#: views/campaigns/view.hbs:2
-#: lib/tools.js:119
+#: lib/tools.js:122
#: routes/campaigns.js:35
msgid "Campaigns"
msgstr ""
@@ -118,7 +120,7 @@ msgstr ""
#: views/campaigns/opened.hbs:7
#: views/campaigns/unsubscribed.hbs:7
#: views/lists/subscription/import-failed.hbs:8
-#: views/lists/view.hbs:18
+#: views/lists/view.hbs:19
#: views/triggers/triggered.hbs:6
msgid "Address"
msgstr ""
@@ -132,9 +134,8 @@ msgstr ""
#: views/lists/subscription/add.hbs:6
#: views/lists/subscription/edit.hbs:7
#: views/lists/subscription/import-preview.hbs:7
-#: views/lists/view.hbs:19
-#: views/subscription/manage.hbs:4
-#: views/subscription/subscribe.hbs:4
+#: views/lists/view.hbs:20
+#: views/subscription/partials/subscription-custom-fields.hbs:3
#: views/triggers/triggered.hbs:7
msgid "First Name"
msgstr ""
@@ -148,9 +149,8 @@ msgstr ""
#: views/lists/subscription/add.hbs:7
#: views/lists/subscription/edit.hbs:8
#: views/lists/subscription/import-preview.hbs:8
-#: views/lists/view.hbs:20
-#: views/subscription/manage.hbs:5
-#: views/subscription/subscribe.hbs:5
+#: views/lists/view.hbs:21
+#: views/subscription/partials/subscription-custom-fields.hbs:4
#: views/triggers/triggered.hbs:8
msgid "Last Name"
msgstr ""
@@ -197,6 +197,7 @@ msgstr ""
#: views/lists/create.hbs:5
#: views/lists/edit.hbs:6
#: views/lists/fields/fields.hbs:6
+#: views/lists/forms/forms.hbs:6
#: views/lists/lists.hbs:5
#: views/lists/segments/segments.hbs:6
#: views/templates/templates.hbs:5
@@ -214,6 +215,8 @@ msgstr ""
#: views/campaigns/view.hbs:72
#: views/lists/create.hbs:7
#: views/lists/edit.hbs:10
+#: views/lists/forms/edit.hbs:9
+#: views/lists/forms/forms.hbs:7
#: views/lists/lists.hbs:8
#: views/mosaico/editor.hbs:3
#: views/partials/merge-tag-reference.hbs:4
@@ -228,16 +231,16 @@ msgstr ""
#: views/campaigns/campaigns.hbs:10
#: views/campaigns/view.hbs:73
-#: views/lists/view.hbs:21
-#: views/lists/view.hbs:29
+#: views/lists/view.hbs:22
+#: views/lists/view.hbs:30
#: views/triggers/triggers.hbs:6
msgid "Status"
msgstr ""
#: views/campaigns/campaigns.hbs:11
#: views/campaigns/view.hbs:74
-#: views/lists/view.hbs:22
#: views/lists/view.hbs:23
+#: views/lists/view.hbs:24
msgid "Created"
msgstr ""
@@ -369,8 +372,7 @@ msgstr ""
#: views/lists/subscription/add.hbs:12
#: views/lists/subscription/edit.hbs:11
#: views/lists/subscription/import-preview.hbs:5
-#: views/subscription/manage.hbs:10
-#: views/subscription/subscribe.hbs:10
+#: views/subscription/partials/subscription-custom-fields.hbs:9
#: views/templates/create.hbs:8
#: views/triggers/create-select.hbs:7
#: views/triggers/create.hbs:17
@@ -569,8 +571,10 @@ msgstr ""
#: views/campaigns/edit-rss.hbs:24
#: views/campaigns/edit-triggered.hbs:27
#: views/campaigns/edit.hbs:35
-#: views/lists/edit.hbs:13
+#: views/lists/edit.hbs:16
#: views/lists/fields/edit.hbs:39
+#: views/lists/forms/edit.hbs:31
+#: views/lists/forms/forms.hbs:12
#: views/lists/segments/edit.hbs:14
#: views/lists/segments/rule-edit.hbs:38
#: views/lists/subscription/edit.hbs:17
@@ -625,7 +629,8 @@ msgstr ""
#: views/campaigns/edit.hbs:32
#: views/campaigns/view.hbs:66
#: views/lists/fields/fields.hbs:12
-#: views/lists/view.hbs:32
+#: views/lists/forms/forms.hbs:9
+#: views/lists/view.hbs:33
msgid "No data available in table"
msgstr ""
@@ -663,7 +668,7 @@ msgstr ""
#: views/campaigns/unsubscribed.hbs:11
#: views/campaigns/view.hbs:26
#: views/lists/subscription/import.hbs:10
-#: routes/lists.js:171
+#: routes/lists.js:187
msgid "Unsubscribed"
msgstr ""
@@ -725,7 +730,7 @@ msgid "List subscribers who received this message"
msgstr ""
#: views/campaigns/view.hbs:22
-#: routes/lists.js:171
+#: routes/lists.js:187
msgid "Bounced"
msgstr ""
@@ -888,55 +893,22 @@ msgid ""
"that entry and it will be listed here"
msgstr ""
-#: views/emails/confirm-html.hbs:1
-#: views/emails/confirm-html.hbs:2
-#: views/emails/confirm-text.hbs:1
-msgid "Please Confirm Subscription"
-msgstr ""
-
-#: views/emails/confirm-html.hbs:3
-#: views/emails/confirm-text.hbs:2
-msgid "Yes, subscribe me to this list"
-msgstr ""
-
-#: views/emails/confirm-html.hbs:4
-msgid ""
-"If you received this email by mistake, simply delete it. You won't be "
-"subscribed if you don't click the confirmation link above."
-msgstr ""
-
-#: views/emails/confirm-html.hbs:5
-#: views/emails/confirm-text.hbs:4
-#: views/emails/subscription-confirmed-html.hbs:7
-#: views/emails/subscription-confirmed-text.hbs:7
-#: views/emails/unsubscribe-confirmed-html.hbs:5
-#: views/emails/unsubscribe-confirmed-text.hbs:5
-msgid "For questions about this list, please contact:"
-msgstr ""
-
-#: views/emails/confirm-text.hbs:3
-msgid ""
-"If you received this email by mistake, simply delete it. You won't be "
-"subscribed unless you click the confirmation link above."
-msgstr ""
-
#: views/emails/password-reset-html.hbs:1
-#: views/emails/password-reset-html.hbs:2
#: views/emails/password-reset-text.hbs:1
msgid "Change your password"
msgstr ""
-#: views/emails/password-reset-html.hbs:3
+#: views/emails/password-reset-html.hbs:2
#: views/emails/password-reset-text.hbs:2
msgid "We have received a password change request for your Mailtrain account:"
msgstr ""
-#: views/emails/password-reset-html.hbs:4
+#: views/emails/password-reset-html.hbs:3
#: views/emails/password-reset-text.hbs:3
msgid "Reset password"
msgstr ""
-#: views/emails/password-reset-html.hbs:5
+#: views/emails/password-reset-html.hbs:4
#: views/emails/password-reset-text.hbs:4
msgid ""
"If you did not ask to change your password, then you can ignore this email "
@@ -953,10 +925,11 @@ msgstr ""
#: views/emails/stationery-html.hbs:4
#: views/emails/stationery-text.hbs:4
#: views/lists/subscription/edit.hbs:15
-#: views/subscription/manage.hbs:12
-#: views/subscription/unsubscribe.hbs:1
-#: views/subscription/unsubscribe.hbs:4
-#: routes/lists.js:253
+#: views/subscription/partials/subscription-unsubscribe-form.hbs:2
+#: views/subscription/web-manage.mjml.hbs:3
+#: views/subscription/web-unsubscribe.mjml.hbs:1
+#: views/subscription/web-unsubscribe.mjml.hbs:3
+#: routes/lists.js:269
msgid "Unsubscribe"
msgstr ""
@@ -976,63 +949,6 @@ msgstr ""
msgid "Cheers,"
msgstr ""
-#: views/emails/subscription-confirmed-html.hbs:1
-#: views/emails/subscription-confirmed-text.hbs:1
-#: views/subscription/subscribed.hbs:1
-msgid "Subscription Confirmed"
-msgstr ""
-
-#: views/emails/subscription-confirmed-html.hbs:2
-#: views/emails/subscription-confirmed-text.hbs:2
-#: views/subscription/subscribed.hbs:2
-msgid "Your subscription to our list has been confirmed."
-msgstr ""
-
-#: views/emails/subscription-confirmed-html.hbs:3
-#: views/emails/subscription-confirmed-text.hbs:3
-msgid "If you want to modify your subscription then you can:"
-msgstr ""
-
-#: views/emails/subscription-confirmed-html.hbs:4
-#: views/emails/subscription-confirmed-text.hbs:4
-#: views/subscription/subscribed.hbs:6
-msgid "manage your preferences"
-msgstr ""
-
-#: views/emails/subscription-confirmed-html.hbs:5
-#: views/emails/subscription-confirmed-text.hbs:5
-#: views/subscription/subscribed.hbs:5
-#: views/users/login.hbs:10
-msgid "or"
-msgstr ""
-
-#: views/emails/subscription-confirmed-html.hbs:6
-#: views/emails/subscription-confirmed-text.hbs:6
-msgid "unsubscribe here"
-msgstr ""
-
-#: views/emails/unsubscribe-confirmed-html.hbs:1
-#: views/emails/unsubscribe-confirmed-text.hbs:1
-msgid "You are now unsubscribed"
-msgstr ""
-
-#: views/emails/unsubscribe-confirmed-html.hbs:2
-#: views/emails/unsubscribe-confirmed-text.hbs:2
-msgid "We have removed your email address from our list."
-msgstr ""
-
-#: views/emails/unsubscribe-confirmed-html.hbs:3
-#: views/emails/unsubscribe-confirmed-text.hbs:3
-msgid "If you unsubscribed by mistake, you can re-subscribe at:"
-msgstr ""
-
-#: views/emails/unsubscribe-confirmed-html.hbs:4
-#: views/emails/unsubscribe-confirmed-text.hbs:4
-#: views/lists/subscription/add.hbs:16
-#: routes/lists.js:253
-msgid "Subscribe"
-msgstr ""
-
#: views/index.hbs:1
msgid "List Management"
msgstr ""
@@ -1062,7 +978,7 @@ msgstr ""
#: views/lists/fields/edit.hbs:3
#: views/lists/fields/fields.hbs:3
#: views/lists/fields/fields.hbs:5
-#: views/lists/view.hbs:5
+#: views/lists/view.hbs:6
msgid "Custom Fields"
msgstr ""
@@ -1140,7 +1056,7 @@ msgid ""
msgstr ""
#: views/index.hbs:25
-#: lib/tools.js:123
+#: lib/tools.js:126
msgid "Automation"
msgstr ""
@@ -1243,6 +1159,9 @@ msgstr ""
#: views/lists/fields/create.hbs:2
#: views/lists/fields/edit.hbs:2
#: views/lists/fields/fields.hbs:2
+#: views/lists/forms/create.hbs:2
+#: views/lists/forms/edit.hbs:2
+#: views/lists/forms/forms.hbs:2
#: views/lists/lists.hbs:2
#: views/lists/lists.hbs:4
#: views/lists/segments/create.hbs:2
@@ -1258,7 +1177,7 @@ msgstr ""
#: views/lists/subscription/import-preview.hbs:2
#: views/lists/subscription/import.hbs:2
#: views/lists/view.hbs:2
-#: lib/tools.js:111
+#: lib/tools.js:114
msgid "Lists"
msgstr ""
@@ -1276,7 +1195,7 @@ msgstr ""
#: views/lists/edit.hbs:3
#: views/lists/edit.hbs:4
-#: views/lists/view.hbs:7
+#: views/lists/view.hbs:8
msgid "Edit List"
msgstr ""
@@ -1293,6 +1212,21 @@ msgid "This is the list ID displayed to the subscribers"
msgstr ""
#: views/lists/edit.hbs:12
+msgid "Custom Form"
+msgstr ""
+
+#: views/lists/edit.hbs:13
+#: views/lists/forms/forms.hbs:11
+msgid "Default Mailtrain Form"
+msgstr ""
+
+#: views/lists/edit.hbs:14
+msgid ""
+"The custom form used for this list. You can create a form here."
+msgstr ""
+
+#: views/lists/edit.hbs:15
msgid "Delete List"
msgstr ""
@@ -1484,12 +1418,14 @@ msgid "Delete Field"
msgstr ""
#: views/lists/fields/fields.hbs:7
-#: views/lists/view.hbs:25
+#: views/lists/view.hbs:26
msgid "Type"
msgstr ""
#: views/lists/fields/fields.hbs:10
#: views/lists/fields/fields.hbs:11
+#: views/lists/forms/edit.hbs:23
+#: views/lists/forms/forms.hbs:8
#: views/lists/lists.hbs:9
#: views/lists/segments/segments.hbs:8
#: views/lists/segments/view.hbs:12
@@ -1498,11 +1434,150 @@ msgstr ""
#: routes/campaigns.js:287
#: routes/campaigns.js:576
#: routes/campaigns.js:626
-#: routes/lists.js:222
+#: routes/lists.js:238
#: routes/triggers.js:297
msgid "Edit"
msgstr ""
+#: views/lists/forms/create.hbs:3
+#: views/lists/forms/edit.hbs:3
+#: views/lists/forms/forms.hbs:3
+#: views/lists/forms/forms.hbs:5
+#: views/lists/view.hbs:5
+msgid "Custom Forms"
+msgstr ""
+
+#: views/lists/forms/create.hbs:4
+msgid "Create Form"
+msgstr ""
+
+#: views/lists/forms/create.hbs:5
+#: views/lists/forms/forms.hbs:4
+msgid "Create Custom Form"
+msgstr ""
+
+#: views/lists/forms/create.hbs:6
+#: views/lists/forms/create.hbs:7
+#: views/lists/forms/edit.hbs:7
+#: views/lists/forms/edit.hbs:8
+msgid "Form Name"
+msgstr ""
+
+#: views/lists/forms/create.hbs:8
+msgid "Add Form"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:4
+msgid "Edit Form"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:5
+msgid "Edit Custom Form"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:6
+msgid "Back to forms"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:10
+msgid "Optional comments about this form"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:11
+msgid "Form Preview"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:12
+msgid ""
+"Note: These links are solely for a quick preview. If you submit a preview "
+"form you'll get redirected to the list's default form."
+msgstr ""
+
+#: views/lists/forms/edit.hbs:13
+#: views/lists/subscription/add.hbs:16
+#: views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs:4
+#: views/subscription/mail-unsubscribe-confirmed-text.hbs:4
+#: routes/forms.js:150
+#: routes/lists.js:269
+msgid "Subscribe"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:14
+msgid "Confirm Notice"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:15
+msgid "Updated Notice"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:16
+msgid "Unsubscribed Notice"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:17
+msgid "Manage"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:18
+msgid "Manage Address"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:19
+msgid "Create a test user for additional options"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:20
+#: views/templates/create.hbs:2
+#: views/templates/edit.hbs:2
+#: views/templates/templates.hbs:2
+#: views/templates/templates.hbs:4
+#: lib/tools.js:118
+msgid "Templates"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:21
+msgid "Fields"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:22
+msgid "Help"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:24
+msgid "Form Fields"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:25
+msgid "Fields hidden on subscription page:"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:26
+msgid "Fields shown on subscription page:"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:27
+msgid "Fields hidden on preferences page:"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:28
+msgid "Fields shown on preferences page:"
+msgstr ""
+
+#: views/lists/forms/edit.hbs:29
+msgid ""
+"The MJML Documentation can be found here."
+msgstr ""
+
+#: views/lists/forms/edit.hbs:30
+msgid "Delete Form"
+msgstr ""
+
+#: views/lists/forms/forms.hbs:10
+msgid "The default form for this list is:"
+msgstr ""
+
#: views/lists/lists.hbs:6
msgid "ID"
msgstr ""
@@ -1519,8 +1594,8 @@ msgstr ""
#: views/lists/segments/segments.hbs:3
#: views/lists/segments/segments.hbs:5
#: views/lists/segments/view.hbs:3
-#: views/lists/view.hbs:6
-#: views/lists/view.hbs:13
+#: views/lists/view.hbs:7
+#: views/lists/view.hbs:14
msgid "Segments"
msgstr ""
@@ -1562,7 +1637,7 @@ msgstr ""
#: views/lists/segments/edit.hbs:4
#: views/lists/segments/edit.hbs:5
#: views/lists/segments/view.hbs:6
-#: views/lists/view.hbs:11
+#: views/lists/view.hbs:12
msgid "Edit Segment"
msgstr ""
@@ -1706,7 +1781,7 @@ msgid "Match"
msgstr ""
#: views/lists/segments/view.hbs:5
-#: views/lists/view.hbs:12
+#: views/lists/view.hbs:13
msgid "Segment"
msgstr ""
@@ -1728,8 +1803,7 @@ msgid "Add subscriber"
msgstr ""
#: views/lists/subscription/add.hbs:5
-#: views/subscription/manage.hbs:2
-#: views/subscription/subscribe.hbs:3
+#: views/subscription/partials/subscription-custom-fields.hbs:1
#: views/users/account.hbs:7
msgid "Email Address"
msgstr ""
@@ -1738,8 +1812,7 @@ msgstr ""
#: views/lists/subscription/edit.hbs:9
#: views/settings.hbs:82
#: views/settings.hbs:97
-#: views/subscription/manage.hbs:7
-#: views/subscription/subscribe.hbs:7
+#: views/subscription/partials/subscription-custom-fields.hbs:6
msgid "Begins with"
msgstr ""
@@ -1786,8 +1859,8 @@ msgstr ""
#: views/lists/subscription/edit.hbs:6
#: views/lists/subscription/import-preview.hbs:6
-#: views/subscription/unsubscribe.hbs:3
-#: lib/helpers.js:25
+#: views/subscription/partials/subscription-unsubscribe-form.hbs:1
+#: lib/helpers.js:38
#: lib/models/segments.js:11
msgid "Email address"
msgstr ""
@@ -1851,7 +1924,7 @@ msgid "Categorize the imported subscribers as"
msgstr ""
#: views/lists/subscription/import.hbs:8
-#: routes/lists.js:171
+#: routes/lists.js:187
msgid "Subscribed"
msgstr ""
@@ -1871,7 +1944,7 @@ msgstr ""
msgid "List Actions"
msgstr ""
-#: views/lists/view.hbs:8
+#: views/lists/view.hbs:9
#: views/triggers/create-select.hbs:3
#: views/triggers/create-select.hbs:4
#: views/triggers/create.hbs:3
@@ -1881,53 +1954,53 @@ msgstr ""
msgid "Create Trigger"
msgstr ""
-#: views/lists/view.hbs:9
+#: views/lists/view.hbs:10
msgid "Add Subscriber"
msgstr ""
-#: views/lists/view.hbs:10
+#: views/lists/view.hbs:11
msgid "Import Subscribers"
msgstr ""
-#: views/lists/view.hbs:14
+#: views/lists/view.hbs:15
msgid "Create New Segment"
msgstr ""
-#: views/lists/view.hbs:15
+#: views/lists/view.hbs:16
msgid "Filter"
msgstr ""
-#: views/lists/view.hbs:16
+#: views/lists/view.hbs:17
msgid "Subscriptions"
msgstr ""
-#: views/lists/view.hbs:17
+#: views/lists/view.hbs:18
msgid "Imports"
msgstr ""
-#: views/lists/view.hbs:24
+#: views/lists/view.hbs:25
#: routes/campaigns.js:266
-#: routes/lists.js:265
+#: routes/lists.js:281
msgid "Finished"
msgstr ""
-#: views/lists/view.hbs:26
+#: views/lists/view.hbs:27
msgid "Added"
msgstr ""
-#: views/lists/view.hbs:27
+#: views/lists/view.hbs:28
msgid "Updated"
msgstr ""
-#: views/lists/view.hbs:28
+#: views/lists/view.hbs:29
msgid "Failed"
msgstr ""
-#: views/lists/view.hbs:30
+#: views/lists/view.hbs:31
msgid "Are you sure? This action should only be called to resolve stalled imports"
msgstr ""
-#: views/lists/view.hbs:31
+#: views/lists/view.hbs:32
msgid "Restart"
msgstr ""
@@ -2423,128 +2496,210 @@ msgid ""
"are not signed."
msgstr ""
-#: views/subscription/confirm-notice.hbs:1
-#: views/subscription/subscribe.hbs:1
+#: views/subscription/mail-confirm-html.mjml.hbs:1
+#: views/subscription/mail-confirm-text.hbs:1
+msgid "Please Confirm Subscription"
+msgstr ""
+
+#: views/subscription/mail-confirm-html.mjml.hbs:2
+#: views/subscription/mail-confirm-text.hbs:2
+msgid "Yes, subscribe me to this list"
+msgstr ""
+
+#: views/subscription/mail-confirm-html.mjml.hbs:3
+msgid ""
+"If you received this email by mistake, simply delete it. You won't be "
+"subscribed if you don't click the confirmation link above."
+msgstr ""
+
+#: views/subscription/mail-confirm-html.mjml.hbs:4
+#: views/subscription/mail-confirm-text.hbs:4
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:8
+#: views/subscription/mail-subscription-confirmed-text.hbs:7
+#: views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs:5
+#: views/subscription/mail-unsubscribe-confirmed-text.hbs:5
+msgid "For questions about this list, please contact:"
+msgstr ""
+
+#: views/subscription/mail-confirm-text.hbs:3
+msgid ""
+"If you received this email by mistake, simply delete it. You won't be "
+"subscribed unless you click the confirmation link above."
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:1
+#: views/subscription/mail-subscription-confirmed-text.hbs:1
+#: views/subscription/web-subscribed.mjml.hbs:1
+msgid "Subscription Confirmed"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:2
+msgid "Your subscription to our list has been confirmed"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:3
+msgid "If you want to modify your subscription then you can "
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:4
+#: views/subscription/mail-subscription-confirmed-text.hbs:4
+msgid "manage your preferences"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:5
+#: views/subscription/mail-subscription-confirmed-text.hbs:5
+#: views/users/login.hbs:10
+msgid "or"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:6
+#: views/subscription/mail-subscription-confirmed-text.hbs:6
+msgid "unsubscribe here"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:7
+#: views/subscription/web-confirm-notice.mjml.hbs:3
+#: views/subscription/web-subscribed.mjml.hbs:4
+#: views/subscription/web-unsubscribe-notice.mjml.hbs:3
+#: views/subscription/web-updated-notice.mjml.hbs:3
+msgid "Return to our website"
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-text.hbs:2
+#: views/subscription/web-subscribed.mjml.hbs:2
+msgid "Your subscription to our list has been confirmed."
+msgstr ""
+
+#: views/subscription/mail-subscription-confirmed-text.hbs:3
+msgid "If you want to modify your subscription then you can:"
+msgstr ""
+
+#: views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs:1
+#: views/subscription/mail-unsubscribe-confirmed-text.hbs:1
+msgid "You Are Now Unsubscribed"
+msgstr ""
+
+#: views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs:2
+msgid "We have removed your email address from our list"
+msgstr ""
+
+#: views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs:3
+#: views/subscription/mail-unsubscribe-confirmed-text.hbs:3
+msgid "If you unsubscribed by mistake, you can re-subscribe at:"
+msgstr ""
+
+#: views/subscription/mail-unsubscribe-confirmed-text.hbs:2
+msgid "We have removed your email address from our list."
+msgstr ""
+
+#: views/subscription/partials/subscription-custom-fields.hbs:2
+msgid "want to change it?"
+msgstr ""
+
+#: views/subscription/partials/subscription-custom-fields.hbs:5
+msgid "Download signature verification key"
+msgstr ""
+
+#: views/subscription/partials/subscription-custom-fields.hbs:7
+msgid "Insert your GPG public key here to encrypt messages sent to your address"
+msgstr ""
+
+#: views/subscription/partials/subscription-custom-fields.hbs:8
+msgid "optional"
+msgstr ""
+
+#: views/subscription/partials/subscription-flash-messages.hbs:1
+#: views/subscription/partials/subscription-flash-messages.hbs:3
msgid "Warning!"
msgstr ""
-#: views/subscription/confirm-notice.hbs:2
+#: views/subscription/partials/subscription-flash-messages.hbs:2
msgid "If JavaScript was not enabled then no confirmation message was sent"
msgstr ""
-#: views/subscription/confirm-notice.hbs:3
-msgid "Almost finished."
+#: views/subscription/partials/subscription-flash-messages.hbs:4
+msgid "JavaScript must be enabled in order for this form to work"
msgstr ""
-#: views/subscription/confirm-notice.hbs:4
-msgid ""
-"We need to confirm your email address. To complete the subscription "
-"process, please click the link in the email we just sent you."
-msgstr ""
-
-#: views/subscription/confirm-notice.hbs:5
-#: views/subscription/unsubscribe-notice.hbs:3
-#: views/subscription/updated-notice.hbs:3
-msgid "return to our website"
-msgstr ""
-
-#: views/subscription/manage-address.hbs:1
-msgid "Update your Email Address"
-msgstr ""
-
-#: views/subscription/manage-address.hbs:2
+#: views/subscription/partials/subscription-manage-address-form.hbs:1
msgid "Existing Email Address"
msgstr ""
-#: views/subscription/manage-address.hbs:3
+#: views/subscription/partials/subscription-manage-address-form.hbs:2
msgid "New Email Address"
msgstr ""
-#: views/subscription/manage-address.hbs:4
+#: views/subscription/partials/subscription-manage-address-form.hbs:3
msgid "Your new email address"
msgstr ""
-#: views/subscription/manage-address.hbs:5
+#: views/subscription/partials/subscription-manage-address-form.hbs:4
msgid ""
"You will receive a confirmation request to your new email address that you "
"need to accept before your email is actually changed"
msgstr ""
-#: views/subscription/manage-address.hbs:6
+#: views/subscription/partials/subscription-manage-address-form.hbs:5
+#: views/subscription/web-manage-address.mjml.hbs:2
msgid "Update Email Address"
msgstr ""
-#: views/subscription/manage.hbs:1
-msgid "Update your preferences"
-msgstr ""
-
-#: views/subscription/manage.hbs:3
-msgid "want to change it?"
-msgstr ""
-
-#: views/subscription/manage.hbs:6
-#: views/subscription/subscribe.hbs:6
-msgid "Download signature verification key"
-msgstr ""
-
-#: views/subscription/manage.hbs:8
-#: views/subscription/subscribe.hbs:8
-msgid "Insert your GPG public key here to encrypt messages sent to your address"
-msgstr ""
-
-#: views/subscription/manage.hbs:9
-#: views/subscription/subscribe.hbs:9
-msgid "optional"
-msgstr ""
-
-#: views/subscription/manage.hbs:11
+#: views/subscription/partials/subscription-manage-form.hbs:1
+#: views/subscription/web-manage.mjml.hbs:2
msgid "Update Profile"
msgstr ""
-#: views/subscription/subscribe.hbs:2
-msgid "JavaScript must be enabled in order for the subscription form to work"
-msgstr ""
-
-#: views/subscription/subscribe.hbs:11
+#: views/subscription/partials/subscription-subscribe-form.hbs:1
+#: views/subscription/web-subscribe.mjml.hbs:2
msgid "Subscribe to list"
msgstr ""
-#: views/subscription/subscribed.hbs:3
+#: views/subscription/web-confirm-notice.mjml.hbs:1
+msgid "Almost Finished"
+msgstr ""
+
+#: views/subscription/web-confirm-notice.mjml.hbs:2
+msgid ""
+"We need to confirm your email address. To complete the subscription "
+"process, please click the link in the email we just sent you."
+msgstr ""
+
+#: views/subscription/web-manage-address.mjml.hbs:1
+msgid "Update Your Email Address"
+msgstr ""
+
+#: views/subscription/web-manage.mjml.hbs:1
+msgid "Update Your Preferences"
+msgstr ""
+
+#: views/subscription/web-subscribe.mjml.hbs:1
+msgid "Subscribe to List"
+msgstr ""
+
+#: views/subscription/web-subscribed.mjml.hbs:3
msgid "Thank you for subscribing!"
msgstr ""
-#: views/subscription/subscribed.hbs:4
-msgid "continue to our website"
-msgstr ""
-
-#: views/subscription/unsubscribe-notice.hbs:1
+#: views/subscription/web-unsubscribe-notice.mjml.hbs:1
msgid "Unsubscribe Successful"
msgstr ""
-#: views/subscription/unsubscribe-notice.hbs:2
+#: views/subscription/web-unsubscribe-notice.mjml.hbs:2
msgid "You have been removed from:"
msgstr ""
-#: views/subscription/unsubscribe.hbs:2
+#: views/subscription/web-unsubscribe.mjml.hbs:2
msgid "Enter your email address to unsubscribe from:"
msgstr ""
-#: views/subscription/updated-notice.hbs:1
+#: views/subscription/web-updated-notice.mjml.hbs:1
msgid "Profile Updated"
msgstr ""
-#: views/subscription/updated-notice.hbs:2
+#: views/subscription/web-updated-notice.mjml.hbs:2
msgid "Your profile information has been updated."
msgstr ""
-#: views/templates/create.hbs:2
-#: views/templates/edit.hbs:2
-#: views/templates/templates.hbs:2
-#: views/templates/templates.hbs:4
-#: lib/tools.js:115
-msgid "Templates"
-msgstr ""
-
#: views/templates/create.hbs:3
#: views/templates/create.hbs:4
#: views/templates/create.hbs:12
@@ -3011,45 +3166,45 @@ msgstr ""
msgid "Bad status code %s"
msgstr ""
-#: lib/helpers.js:16
+#: lib/helpers.js:29
msgid "URL that points to the unsubscribe page"
msgstr ""
-#: lib/helpers.js:19
+#: lib/helpers.js:32
msgid "URL that points to the preferences page of the subscriber"
msgstr ""
-#: lib/helpers.js:22
+#: lib/helpers.js:35
msgid "URL to preview the message in a browser"
msgstr ""
-#: lib/helpers.js:28
+#: lib/helpers.js:41
#: lib/models/segments.js:31
msgid "First name"
msgstr ""
-#: lib/helpers.js:31
+#: lib/helpers.js:44
#: lib/models/segments.js:35
msgid "Last name"
msgstr ""
-#: lib/helpers.js:34
+#: lib/helpers.js:47
msgid "Full name (first and last name combined)"
msgstr ""
-#: lib/helpers.js:37
+#: lib/helpers.js:50
msgid "Unique ID that identifies the recipient"
msgstr ""
-#: lib/helpers.js:40
+#: lib/helpers.js:53
msgid "Unique ID that identifies the list used for this campaign"
msgstr ""
-#: lib/helpers.js:43
+#: lib/helpers.js:56
msgid "Unique ID that identifies current campaign"
msgstr ""
-#: lib/mailer.js:215
+#: lib/mailer.js:245
msgid "Invalid mail transport"
msgstr ""
@@ -3102,19 +3257,20 @@ msgstr ""
#: lib/models/fields.js:53
#: lib/models/fields.js:98
#: lib/models/fields.js:123
+#: lib/models/forms.js:37
#: lib/models/lists.js:81
#: lib/models/lists.js:175
#: lib/models/lists.js:212
#: lib/models/segments.js:43
#: lib/models/segments.js:176
-#: lib/models/subscriptions.js:88
-#: lib/models/subscriptions.js:640
-#: lib/models/subscriptions.js:703
-#: lib/models/subscriptions.js:889
-#: lib/models/subscriptions.js:992
-#: lib/models/subscriptions.js:1046
-#: lib/models/subscriptions.js:1109
-#: lib/models/subscriptions.js:1152
+#: lib/models/subscriptions.js:89
+#: lib/models/subscriptions.js:661
+#: lib/models/subscriptions.js:724
+#: lib/models/subscriptions.js:910
+#: lib/models/subscriptions.js:1013
+#: lib/models/subscriptions.js:1067
+#: lib/models/subscriptions.js:1130
+#: lib/models/subscriptions.js:1173
msgid "Missing List ID"
msgstr ""
@@ -3153,6 +3309,22 @@ msgstr ""
msgid "Provided List ID not found"
msgstr ""
+#: lib/models/forms.js:62
+#: lib/models/forms.js:88
+#: lib/models/forms.js:136
+#: lib/models/forms.js:183
+msgid "Missing Form ID"
+msgstr ""
+
+#: lib/models/forms.js:96
+#: lib/models/forms.js:140
+msgid "Form Name must be set"
+msgstr ""
+
+#: lib/models/forms.js:200
+msgid "Custom form not found"
+msgstr ""
+
#: lib/models/links.js:328
#: routes/campaigns.js:541
#: routes/campaigns.js:590
@@ -3161,7 +3333,7 @@ msgid "Campaign not found"
msgstr ""
#: lib/models/links.js:336
-#: routes/lists.js:146
+#: routes/lists.js:162
#: services/sender.js:311
msgid "List not found"
msgstr ""
@@ -3270,48 +3442,48 @@ msgstr ""
msgid "Selected rule not found"
msgstr ""
-#: lib/models/subscriptions.js:233
+#: lib/models/subscriptions.js:235
msgid "%s: Please Confirm Subscription"
msgstr ""
-#: lib/models/subscriptions.js:324
+#: lib/models/subscriptions.js:345
msgid "Could not save subscription"
msgstr ""
-#: lib/models/subscriptions.js:507
-#: lib/models/subscriptions.js:537
+#: lib/models/subscriptions.js:528
+#: lib/models/subscriptions.js:558
msgid "Missing Subbscription ID"
msgstr ""
-#: lib/models/subscriptions.js:565
+#: lib/models/subscriptions.js:586
msgid "Missing Subbscription email address"
msgstr ""
-#: lib/models/subscriptions.js:644
-#: lib/models/subscriptions.js:893
-#: lib/models/subscriptions.js:1156
+#: lib/models/subscriptions.js:665
+#: lib/models/subscriptions.js:914
+#: lib/models/subscriptions.js:1177
msgid "Missing subscription ID"
msgstr ""
-#: lib/models/subscriptions.js:707
+#: lib/models/subscriptions.js:728
msgid "Missing email address"
msgstr ""
-#: lib/models/subscriptions.js:996
-#: lib/models/subscriptions.js:1050
-#: lib/models/subscriptions.js:1086
+#: lib/models/subscriptions.js:1017
+#: lib/models/subscriptions.js:1071
+#: lib/models/subscriptions.js:1107
msgid "Missing Import ID"
msgstr ""
-#: lib/models/subscriptions.js:1178
+#: lib/models/subscriptions.js:1199
msgid "Unknown subscription ID"
msgstr ""
-#: lib/models/subscriptions.js:1183
+#: lib/models/subscriptions.js:1204
msgid "Nothing seems to be changed"
msgstr ""
-#: lib/models/subscriptions.js:1197
+#: lib/models/subscriptions.js:1218
msgid "This address is already registered by someone else"
msgstr ""
@@ -3453,30 +3625,30 @@ msgstr ""
msgid "Incorrect username or password"
msgstr ""
-#: lib/tools.js:133
+#: lib/tools.js:136
msgid "Blocked email address \"%s\""
msgstr ""
-#: lib/tools.js:142
+#: lib/tools.js:145
msgid "Invalid email address \"%s\"."
msgstr ""
-#: lib/tools.js:145
+#: lib/tools.js:148
msgid "MX record not found for domain"
msgstr ""
-#: lib/tools.js:148
+#: lib/tools.js:151
msgid "Address domain not found"
msgstr ""
-#: lib/tools.js:151
+#: lib/tools.js:154
msgid "Address domain name is required"
msgstr ""
#: routes/archive.js:31
#: routes/archive.js:43
#: routes/archive.js:55
-#: app.js:220
+#: app.js:225
msgid "Not Found"
msgstr ""
@@ -3493,8 +3665,9 @@ msgstr ""
#: routes/campaigns.js:26
#: routes/editorapi.js:35
#: routes/fields.js:13
+#: routes/forms.js:16
#: routes/grapejs.js:13
-#: routes/lists.js:49
+#: routes/lists.js:50
#: routes/mosaico.js:14
#: routes/segments.js:13
#: routes/settings.js:23
@@ -3637,6 +3810,9 @@ msgstr ""
#: routes/fields.js:28
#: routes/fields.js:64
#: routes/fields.js:118
+#: routes/forms.js:31
+#: routes/forms.js:63
+#: routes/forms.js:94
#: routes/segments.js:28
#: routes/segments.js:59
#: routes/segments.js:102
@@ -3673,6 +3849,94 @@ msgstr ""
msgid "Could not delete specified field"
msgstr ""
+#: routes/forms.js:78
+msgid "Could not create custom form"
+msgstr ""
+
+#: routes/forms.js:105
+msgid "Selected form not found"
+msgstr ""
+
+#: routes/forms.js:141
+msgid "Layout"
+msgstr ""
+
+#: routes/forms.js:146
+msgid "Form Input Style"
+msgstr ""
+
+#: routes/forms.js:153
+msgid "Web - Subscribe"
+msgstr ""
+
+#: routes/forms.js:157
+msgid "Web - Confirm Notice"
+msgstr ""
+
+#: routes/forms.js:161
+msgid "Mail - Confirm Subscription (MJML)"
+msgstr ""
+
+#: routes/forms.js:165
+msgid "Mail - Confirm Subscription (Text)"
+msgstr ""
+
+#: routes/forms.js:169
+msgid "Web - Subscribed Notice"
+msgstr ""
+
+#: routes/forms.js:173
+msgid "Mail - Subscription Confirmed (MJML)"
+msgstr ""
+
+#: routes/forms.js:177
+msgid "Mail - Subscription Confirmed (Text)"
+msgstr ""
+
+#: routes/forms.js:184
+msgid "Web - Manage Preferences"
+msgstr ""
+
+#: routes/forms.js:188
+msgid "Web - Manage Address"
+msgstr ""
+
+#: routes/forms.js:192
+msgid "Web - Updated Notice"
+msgstr ""
+
+#: routes/forms.js:199
+msgid "Web - Unsubscribe"
+msgstr ""
+
+#: routes/forms.js:203
+msgid "Web - Unsubscribe Notice"
+msgstr ""
+
+#: routes/forms.js:207
+msgid "Mail - Unsubscribe Confirmed (MJML)"
+msgstr ""
+
+#: routes/forms.js:211
+msgid "Mail - Unsubscribe Confirmed (Text)"
+msgstr ""
+
+#: routes/forms.js:248
+msgid "Form settings updated"
+msgstr ""
+
+#: routes/forms.js:250
+msgid "Form settings not updated"
+msgstr ""
+
+#: routes/forms.js:266
+msgid "Custom form deleted"
+msgstr ""
+
+#: routes/forms.js:268
+msgid "Could not delete specified form"
+msgstr ""
+
#: routes/index.js:11
msgid "Self Hosted Newsletter App"
msgstr ""
@@ -3681,148 +3945,148 @@ msgstr ""
msgid "Oops, we couldn't find a link for the URL you clicked"
msgstr ""
-#: routes/lists.js:90
+#: routes/lists.js:91
msgid "Could not create list"
msgstr ""
-#: routes/lists.js:93
+#: routes/lists.js:94
msgid "List created"
msgstr ""
-#: routes/lists.js:101
-#: routes/lists.js:236
-#: routes/lists.js:301
-#: routes/lists.js:340
-#: routes/lists.js:409
-#: routes/lists.js:434
-#: routes/lists.js:479
-#: routes/lists.js:501
-#: routes/lists.js:530
-#: routes/lists.js:609
-#: routes/lists.js:666
-#: routes/lists.js:693
+#: routes/lists.js:102
+#: routes/lists.js:252
+#: routes/lists.js:317
+#: routes/lists.js:356
+#: routes/lists.js:425
+#: routes/lists.js:450
+#: routes/lists.js:495
+#: routes/lists.js:517
+#: routes/lists.js:546
+#: routes/lists.js:625
+#: routes/lists.js:682
+#: routes/lists.js:709
msgid "Could not find list with specified ID"
msgstr ""
-#: routes/lists.js:115
+#: routes/lists.js:129
msgid "List settings updated"
msgstr ""
-#: routes/lists.js:117
+#: routes/lists.js:131
msgid "List settings not updated"
msgstr ""
-#: routes/lists.js:133
+#: routes/lists.js:149
msgid "List deleted"
msgstr ""
-#: routes/lists.js:135
+#: routes/lists.js:151
msgid "Could not delete specified list"
msgstr ""
-#: routes/lists.js:171
+#: routes/lists.js:187
msgid "Unknown"
msgstr ""
-#: routes/lists.js:171
+#: routes/lists.js:187
msgid "Complained"
msgstr ""
-#: routes/lists.js:202
+#: routes/lists.js:218
msgid "Invalid key"
msgstr ""
-#: routes/lists.js:204
+#: routes/lists.js:220
msgid "Expired key"
msgstr ""
-#: routes/lists.js:206
+#: routes/lists.js:222
msgid "Revoked key"
msgstr ""
-#: routes/lists.js:256
+#: routes/lists.js:272
msgid "Initializing"
msgstr ""
-#: routes/lists.js:259
+#: routes/lists.js:275
msgid "Initialized"
msgstr ""
-#: routes/lists.js:262
+#: routes/lists.js:278
msgid "Importing"
msgstr ""
-#: routes/lists.js:268
+#: routes/lists.js:284
msgid "Errored"
msgstr ""
-#: routes/lists.js:346
-#: routes/lists.js:415
-#: routes/lists.js:440
+#: routes/lists.js:362
+#: routes/lists.js:431
+#: routes/lists.js:456
msgid "Could not find subscriber with specified ID"
msgstr ""
-#: routes/lists.js:392
+#: routes/lists.js:408
msgid "Could not add subscription"
msgstr ""
-#: routes/lists.js:397
+#: routes/lists.js:413
msgid "%s was successfully added to your list"
msgstr ""
-#: routes/lists.js:399
+#: routes/lists.js:415
msgid "%s was not added to your list"
msgstr ""
-#: routes/lists.js:421
+#: routes/lists.js:437
msgid "Could not unsubscribe user"
msgstr ""
-#: routes/lists.js:424
+#: routes/lists.js:440
msgid "%s was successfully unsubscribed from your list"
msgstr ""
-#: routes/lists.js:444
+#: routes/lists.js:460
msgid "%s was successfully removed from your list"
msgstr ""
-#: routes/lists.js:456
+#: routes/lists.js:472
msgid "Another subscriber with email address %s already exists"
msgstr ""
-#: routes/lists.js:463
+#: routes/lists.js:479
msgid "Subscription settings updated"
msgstr ""
-#: routes/lists.js:465
+#: routes/lists.js:481
msgid "Subscription settings not updated"
msgstr ""
-#: routes/lists.js:507
-#: routes/lists.js:615
-#: routes/lists.js:651
-#: routes/lists.js:679
-#: routes/lists.js:699
+#: routes/lists.js:523
+#: routes/lists.js:631
+#: routes/lists.js:667
+#: routes/lists.js:695
+#: routes/lists.js:715
msgid "Could not find import data with specified ID"
msgstr ""
-#: routes/lists.js:538
+#: routes/lists.js:554
msgid "Could not process CSV"
msgstr ""
-#: routes/lists.js:547
+#: routes/lists.js:563
msgid "Could not create importer"
msgstr ""
-#: routes/lists.js:598
+#: routes/lists.js:614
msgid "Empty file"
msgstr ""
-#: routes/lists.js:655
+#: routes/lists.js:671
msgid "Import started"
msgstr ""
-#: routes/lists.js:683
+#: routes/lists.js:699
msgid "Import restarted"
msgstr ""
@@ -3954,50 +4218,53 @@ msgstr ""
msgid "Mailer settings verified, ready to send some mail!"
msgstr ""
-#: routes/subscription.js:22
+#: routes/subscription.js:25
msgid "Selected subscription not found"
msgstr ""
-#: routes/subscription.js:32
-#: routes/subscription.js:103
-#: routes/subscription.js:141
-#: routes/subscription.js:166
-#: routes/subscription.js:191
-#: routes/subscription.js:232
-#: routes/subscription.js:270
-#: routes/subscription.js:317
-#: routes/subscription.js:339
-#: routes/subscription.js:368
-#: routes/subscription.js:392
-#: routes/subscription.js:424
+#: routes/subscription.js:35
+#: routes/subscription.js:155
+#: routes/subscription.js:222
+#: routes/subscription.js:275
+#: routes/subscription.js:327
+#: routes/subscription.js:396
+#: routes/subscription.js:434
+#: routes/subscription.js:510
+#: routes/subscription.js:532
+#: routes/subscription.js:592
+#: routes/subscription.js:616
+#: routes/subscription.js:681
msgid "Selected list not found"
msgstr ""
-#: routes/subscription.js:78
-#: routes/subscription.js:472
+#: routes/subscription.js:109
msgid "%s: Subscription Confirmed"
msgstr ""
-#: routes/subscription.js:217
+#: routes/subscription.js:381
msgid "Email address not set"
msgstr ""
-#: routes/subscription.js:255
+#: routes/subscription.js:419
msgid "Could not store confirmation data"
msgstr ""
-#: routes/subscription.js:284
-#: routes/subscription.js:349
-#: routes/subscription.js:402
+#: routes/subscription.js:448
+#: routes/subscription.js:547
+#: routes/subscription.js:631
msgid "Subscription not found from this list"
msgstr ""
-#: routes/subscription.js:383
+#: routes/subscription.js:607
msgid "Email address updated, check your mailbox for verification instructions"
msgstr ""
-#: routes/subscription.js:499
-#: routes/subscription.js:515
+#: routes/subscription.js:730
+msgid "%s: Unsubscribe Confirmed"
+msgstr ""
+
+#: routes/subscription.js:777
+#: routes/subscription.js:793
msgid "Public key is not set"
msgstr ""
diff --git a/lib/helpers.js b/lib/helpers.js
index 2366947f..2cac3dbb 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -1,12 +1,27 @@
'use strict';
+let config = require('config');
+let path = require('path');
+let fs = require('fs');
+let tools = require('./tools');
+let settings = require('./models/settings');
let lists = require('./models/lists');
let fields = require('./models/fields');
+let forms = require('./models/forms');
let _ = require('./translate')._;
+let objectHash = require('object-hash');
+let mjml = require('mjml');
+let mjmlTemplates = new Map();
+let hbs = require('hbs');
module.exports = {
getDefaultMergeTags,
- getListMergeTags
+ getListMergeTags,
+ captureFlashMessages,
+ injectCustomFormData,
+ injectCustomFormTemplates,
+ filterCustomFields,
+ getMjmlTemplate
};
function getDefaultMergeTags(callback) {
@@ -73,3 +88,173 @@ function getListMergeTags(listId, callback) {
});
});
}
+
+function filterCustomFields(customFieldsIn = [], fieldIds = [], method = 'include') {
+ let customFields = customFieldsIn.slice();
+ fieldIds = typeof fieldIds === 'string' ? fieldIds.split(',') : fieldIds;
+
+ customFields.unshift({
+ id: 'email',
+ name: 'Email Address',
+ type: 'Email',
+ typeSubsciptionEmail: true
+ }, {
+ id: 'firstname',
+ name: 'First Name',
+ type: 'Text',
+ typeFirstName: true
+ }, {
+ id: 'lastname',
+ name: 'Last Name',
+ type: 'Text',
+ typeLastName: true
+ });
+
+ let filtered = [];
+
+ if (method === 'include') {
+ fieldIds.forEach(id => {
+ let field = customFields.find(f => f.id.toString() === id);
+ field && filtered.push(field);
+ });
+ } else {
+ customFields.forEach(field => {
+ !fieldIds.includes(field.id.toString()) && filtered.push(field);
+ });
+ }
+
+ return filtered;
+}
+
+function injectCustomFormData(customFormId, viewPath, data, callback) {
+
+ let injectDefaultData = data => {
+ data.customFields = filterCustomFields(data.customFields, [], 'exclude');
+ data.formInputStyle = '@import url(/subscription/form-input-style.css);';
+ return data;
+ };
+
+ if (Number(customFormId) < 1) {
+ return callback(null, injectDefaultData(data));
+ }
+
+ forms.get(customFormId, (err, form) => {
+ if (err) {
+ return callback(null, injectDefaultData(data));
+ }
+
+ let view = viewPath.split('/')[1];
+
+ if (view === 'web-subscribe') {
+ data.customFields = form.fieldsShownOnSubscribe
+ ? filterCustomFields(data.customFields, form.fieldsShownOnSubscribe)
+ : filterCustomFields(data.customFields, [], 'exclude');
+ } else if (view === 'web-manage') {
+ data.customFields = form.fieldsShownOnManage
+ ? filterCustomFields(data.customFields, form.fieldsShownOnManage)
+ : filterCustomFields(data.customFields, [], 'exclude');
+ }
+
+ let key = tools.fromDbKey(view);
+ data.template.template = form[key] || data.template.template;
+ data.template.layout = form.layout || data.template.layout;
+ data.formInputStyle = form.formInputStyle || '@import url(/subscription/form-input-style.css);';
+
+ settings.list(['ua_code'], (err, configItems) => {
+ if (err) {
+ return callback(err);
+ }
+
+ data.uaCode = configItems.uaCode;
+ data.customSubscriptionScripts = config.customsubscriptionscripts || [];
+ callback(null, data);
+ });
+ });
+}
+
+function injectCustomFormTemplates(customFormId, templates, callback) {
+ if (Number(customFormId) < 1) {
+ return callback(null, templates);
+ }
+
+ forms.get(customFormId, (err, form) => {
+ if (err) {
+ return callback(null, templates);
+ }
+
+ let lookUp = name => {
+ let key = tools.fromDbKey(
+ /subscription\/([^.]*)/.exec(name)[1]
+ );
+ return form[key] || name;
+ };
+
+ Object.keys(templates).forEach(key => {
+ let value = templates[key];
+
+ if (typeof value === 'string') {
+ templates[key] = lookUp(value);
+ }
+ if (typeof value === 'object' && value.template) {
+ templates[key].template = lookUp(value.template);
+ }
+ if (typeof value === 'object' && value.layout) {
+ templates[key].layout = lookUp(value.layout);
+ }
+ });
+
+ callback(null, templates);
+ });
+}
+
+function getMjmlTemplate(template, callback) {
+ if (!template) {
+ return callback(null, false);
+ }
+
+ let key = (typeof template === 'object') ? objectHash(template) : template;
+
+ if (mjmlTemplates.has(key)) {
+ return callback(null, mjmlTemplates.get(key));
+ }
+
+ let done = source => {
+ let compiled;
+ try {
+ compiled = mjml.mjml2html(source);
+ } catch (err) {
+ return callback(err);
+ }
+ if (compiled.errors.length) {
+ return callback(compiled.errors[0].message || compiled.errors[0]);
+ }
+ let renderer = hbs.handlebars.compile(compiled.html);
+ mjmlTemplates.set(key, renderer);
+ callback(null, renderer);
+ };
+
+ if (typeof template === 'object') {
+ tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
+ if (err) {
+ return callback(err);
+ }
+ done(source);
+ });
+ } else {
+ fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
+ if (err) {
+ return callback(err);
+ }
+ done(source);
+ });
+ }
+}
+
+function captureFlashMessages(req, res, callback) {
+ res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
+ if (err) {
+ return callback(err);
+ }
+ callback(null, flash);
+ });
+}
diff --git a/lib/mailer.js b/lib/mailer.js
index 83f55e5a..8e84bf7d 100644
--- a/lib/mailer.js
+++ b/lib/mailer.js
@@ -13,6 +13,8 @@ let path = require('path');
let templates = new Map();
let htmlToText = require('html-to-text');
let aws = require('aws-sdk');
+let objectHash = require('object-hash');
+let mjml = require('mjml');
let _ = require('./translate')._;
let util = require('util');
@@ -124,18 +126,46 @@ function getTemplate(template, callback) {
return callback(null, false);
}
- if (templates.has(template)) {
- return callback(null, templates.get(template));
+ let key = (typeof template === 'object') ? objectHash(template) : template;
+
+ if (templates.has(key)) {
+ return callback(null, templates.get(key));
}
- fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
- if (err) {
- return callback(err);
+ let done = (source, isMjml = false) => {
+ if (isMjml) {
+ let compiled;
+ try {
+ compiled = mjml.mjml2html(source);
+ } catch (err) {
+ return callback(err);
+ }
+ if (compiled.errors.length) {
+ return callback(compiled.errors[0].message || compiled.errors[0]);
+ }
+ source = compiled.html;
}
let renderer = Handlebars.compile(source);
- templates.set(template, renderer);
- return callback(null, renderer);
- });
+ templates.set(key, renderer);
+ callback(null, renderer);
+ };
+
+ if (typeof template === 'object') {
+ tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
+ if (err) {
+ return callback(err);
+ }
+ let isMjml = template.type === 'mjml';
+ done(source, isMjml);
+ });
+ } else {
+ fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
+ if (err) {
+ return callback(err);
+ }
+ done(source);
+ });
+ }
}
function createMailer(callback) {
diff --git a/lib/models/fields.js b/lib/models/fields.js
index 2e10b322..8fe5d6e8 100644
--- a/lib/models/fields.js
+++ b/lib/models/fields.js
@@ -405,6 +405,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
case 'longtext':
{
let item = {
+ id: field.id,
type: field.type,
name: field.name,
column: field.column,
@@ -434,6 +435,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
}
let item = {
+ id: field.id,
type: field.type,
name: field.name,
column: field.column,
@@ -449,6 +451,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
case 'number':
{
let item = {
+ id: field.id,
type: field.type,
name: field.name,
column: field.column,
@@ -466,6 +469,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
case 'checkbox':
{
let item = {
+ id: field.id,
type: field.type,
name: field.name,
visible: !!field.visible,
@@ -556,6 +560,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
}
let item = {
+ id: field.id,
type: field.type,
name: field.name,
column: field.column,
diff --git a/lib/models/forms.js b/lib/models/forms.js
new file mode 100644
index 00000000..940f5483
--- /dev/null
+++ b/lib/models/forms.js
@@ -0,0 +1,409 @@
+'use strict';
+
+let db = require('../db');
+let fs = require('fs');
+let path = require('path');
+let tools = require('../tools');
+let mjml = require('mjml');
+let _ = require('../translate')._;
+
+let allowedKeys = [
+ 'name',
+ 'description',
+ 'fields_shown_on_subscribe',
+ 'fields_shown_on_manage',
+ 'layout',
+ 'form_input_style',
+ 'mail_confirm_html',
+ 'mail_confirm_text',
+ 'mail_subscription_confirmed_html',
+ 'mail_subscription_confirmed_text',
+ 'mail_unsubscribe_confirmed_html',
+ 'mail_unsubscribe_confirmed_text',
+ 'web_confirm_notice',
+ 'web_manage_address',
+ 'web_manage',
+ 'web_subscribe',
+ 'web_subscribed',
+ 'web_unsubscribe_notice',
+ 'web_unsubscribe',
+ 'web_updated_notice'
+];
+
+module.exports.list = (listId, callback) => {
+ listId = Number(listId) || 0;
+
+ if (listId < 1) {
+ return callback(new Error(_('Missing List ID')));
+ }
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('SELECT * FROM custom_forms WHERE list=? ORDER BY id', [listId], (err, rows) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ let formList = rows && rows.map(row => tools.convertKeys(row)) || [];
+ return callback(null, formList);
+ });
+ });
+};
+
+module.exports.get = (id, callback) => {
+ id = Number(id) || 0;
+
+ if (id < 1) {
+ return callback(new Error(_('Missing Form ID')));
+ }
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [id], (err, rows) => {
+ if (err) {
+ connection.release();
+ return callback(err);
+ }
+
+ let form = rows && rows[0] && tools.convertKeys(rows[0]) || false;
+
+ if (!form) {
+ connection.release();
+ return callback(new Error('Selected form not found'));
+ }
+
+ connection.query('SELECT * FROM custom_forms_data WHERE form=?', [id], (err, data_rows = []) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ data_rows.forEach(data_row => {
+ let modelKey = tools.fromDbKey(data_row.data_key);
+ form[modelKey] = data_row.data_value;
+ });
+
+ return callback(null, form);
+ });
+ });
+ });
+};
+
+
+module.exports.create = (listId, form, callback) => {
+ listId = Number(listId) || 0;
+
+ if (listId < 1) {
+ return callback(new Error(_('Missing Form ID')));
+ }
+
+ form = tools.convertKeys(form);
+ form = setDefaultValues(form);
+ form.name = (form.name || '').toString().trim();
+
+ if (!form.name) {
+ return callback(new Error(_('Form Name must be set')));
+ }
+
+ let keys = ['list'];
+ let values = [listId];
+
+ Object.keys(form).forEach(key => {
+ let value = form[key].trim();
+ key = tools.toDbKey(key);
+ if (key === 'description') {
+ value = tools.purifyHTML(value);
+ }
+ if (allowedKeys.indexOf(key) >= 0) {
+ keys.push(key);
+ values.push(value);
+ }
+ });
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
+ let query = 'INSERT INTO custom_forms (' + filtered.keys.join(', ') + ') VALUES (' + filtered.values.map(() => '?').join(',') + ')';
+
+ connection.query(query, filtered.values, (err, result) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ let formId = result && result.insertId;
+
+ if (!formId) {
+ return callback(new Error('Invalid custom_forms insertId'));
+ }
+
+ let jobs = 1;
+ let error = null;
+
+ let done = err => {
+ jobs--;
+ error = err ? err : error; // One's enough
+ jobs === 0 && callback(error, formId);
+ };
+
+ filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
+
+ filtered.keys.forEach((key, index) => {
+ jobs++;
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return done(err);
+ }
+
+ connection.query('INSERT INTO custom_forms_data (form, data_key, data_value) VALUES (?, ?, ?)', [formId, key, filtered.values[index]], err => {
+ connection.release();
+ if (err) {
+ return done(err);
+ }
+
+ return done(null);
+ });
+ });
+ });
+
+ done(null);
+ });
+ });
+};
+
+module.exports.update = (id, updates, callback) => {
+ updates = updates || {};
+ id = Number(id) || 0;
+
+ updates = tools.convertKeys(updates);
+
+ if (id < 1) {
+ return callback(new Error(_('Missing Form ID')));
+ }
+
+ if (!(updates.name || '').toString().trim()) {
+ return callback(new Error(_('Form Name must be set')));
+ }
+
+ let keys = [];
+ let values = [];
+
+ Object.keys(updates).forEach(key => {
+ let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
+ key = tools.toDbKey(key);
+ if (key === 'description') {
+ value = tools.purifyHTML(value);
+ }
+ if (allowedKeys.indexOf(key) >= 0) {
+ keys.push(key);
+ values.push(value);
+ }
+ });
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
+ let query = 'UPDATE custom_forms SET ' + filtered.keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1';
+
+ connection.query(query, filtered.values.concat(id), (err, result) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ let affectedRows = result && result.affectedRows;
+
+ let jobs = 1;
+ let error = null;
+
+ let done = err => {
+ jobs--;
+ error = err ? err : error; // One's enough
+
+ if (jobs === 0) {
+ if (error) {
+ return callback(error);
+ }
+
+ // Save then validate, as otherwise their work get's lost ...
+ err = testForMjmlErrors(keys, values);
+ if (err) {
+ return callback(err);
+ }
+
+ return callback(null, affectedRows);
+ }
+ };
+
+ filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
+
+ filtered.keys.forEach((key, index) => {
+ jobs++;
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return done(err);
+ }
+
+ connection.query('UPDATE custom_forms_data SET data_value=? WHERE data_key=? AND form=?', [filtered.values[index], key, id], err => {
+ connection.release();
+ if (err) {
+ return done(err);
+ }
+
+ return done(null);
+ });
+ });
+ });
+
+ done(null);
+ });
+ });
+};
+
+module.exports.delete = (formId, callback) => {
+ formId = Number(formId) || 0;
+
+ if (formId < 1) {
+ return callback(new Error(_('Missing Form ID')));
+ }
+
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [formId], (err, rows) => {
+ if (err) {
+ connection.release();
+ return callback(err);
+ }
+
+ if (!rows || !rows.length) {
+ connection.release();
+ return callback(new Error(_('Custom form not found')));
+ }
+
+ connection.query('DELETE FROM custom_forms WHERE id=? LIMIT 1', [formId], err => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, true);
+ });
+ });
+ });
+};
+
+function setDefaultValues(form) {
+ let getContents = fileName => {
+ try {
+ let basePath = path.join(__dirname, '..', '..');
+ let template = fs.readFileSync(path.join(basePath, fileName), 'utf8');
+ return template.replace(/\{\{#translate\}\}(.*?)\{\{\/translate\}\}/g, (m, s) => _(s));
+ } catch (err) {
+ return false;
+ }
+ };
+
+ allowedKeys.forEach(key => {
+ let modelKey = tools.fromDbKey(key);
+ let base = 'views/subscription/' + key.replace(/_/g, '-');
+
+ if (key.startsWith('mail') || key.startsWith('web')) {
+ form[modelKey] = getContents(base + '.mjml.hbs') || getContents(base + '.hbs') || '';
+ }
+ });
+
+ form.layout = getContents('views/subscription/layout.mjml.hbs') || '';
+ form.formInputStyle = getContents('public/subscription/form-input-style.css') || '@import url(/subscription/form-input-style.css);';
+
+ return form;
+}
+
+function filterKeysAndValues(keysIn, valuesIn, method = 'include', prefixes = []) {
+ let values = [];
+
+ let prefixMatch = key => (
+ prefixes.some(prefix => key.startsWith(prefix))
+ );
+
+ let keys = keysIn.filter((key, index) => {
+ if ((method === 'include' && prefixMatch(key)) || (method === 'exclude' && !prefixMatch(key))) {
+ values.push(valuesIn[index]);
+ return true;
+ }
+ return false;
+ });
+
+ return {
+ keys,
+ values
+ };
+}
+
+function testForMjmlErrors(keys, values) {
+
+ let errors = [];
+ let testLayout = '{{{body}}}';
+
+ let hasMjmlError = (template, layout = testLayout) => {
+ let source = layout.replace(/\{\{\{body\}\}\}/g, template);
+ let compiled;
+
+ try {
+ compiled = mjml.mjml2html(source);
+ } catch (err) {
+ return err;
+ }
+
+ if (compiled.errors.length) {
+ return compiled.errors[0].message || compiled.errors[0];
+ }
+
+ return null;
+ };
+
+ keys.forEach((key, index) => {
+ if (key.startsWith('mail_') || key.startsWith('web_')) {
+
+ let template = values[index];
+ let err = hasMjmlError(template);
+
+ err && errors.push(key + ': ' + (err.message || err));
+ key === 'mail_confirm_html' && !template.includes('{{confirmUrl}}') && errors.push(key + ': Missing {{confirmUrl}}');
+
+ } else if (key === 'layout') {
+
+ let layout = values[index];
+ let err = hasMjmlError('', layout);
+
+ err && errors.push('layout: ' + (err.message || err));
+ !layout.includes('{{{body}}}') && errors.push('layout: {{{body}}} not found');
+ }
+ });
+
+
+ if (errors.length) {
+ errors.forEach((err, index) => {
+ errors[index] = (index + 1) + ') ' + err;
+ });
+ return 'Please fix these MJML errors:\n\n' + errors.join('\n');
+ }
+
+ return null;
+}
diff --git a/lib/models/lists.js b/lib/models/lists.js
index 97db2210..02e7a4cd 100644
--- a/lib/models/lists.js
+++ b/lib/models/lists.js
@@ -6,7 +6,7 @@ let shortid = require('shortid');
let segments = require('./segments');
let _ = require('../translate')._;
-let allowedKeys = ['description'];
+let allowedKeys = ['description', 'default_form'];
module.exports.list = (start, limit, callback) => {
db.getConnection((err, connection) => {
diff --git a/lib/models/subscriptions.js b/lib/models/subscriptions.js
index 65731dac..a966e96f 100644
--- a/lib/models/subscriptions.js
+++ b/lib/models/subscriptions.js
@@ -3,6 +3,7 @@
let db = require('../db');
let shortid = require('shortid');
let tools = require('../tools');
+let helpers = require('../helpers');
let fields = require('./fields');
let geoip = require('geoip-ultralight');
let segments = require('./segments');
@@ -210,7 +211,7 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
}
});
- settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl'], (err, configItems) => {
+ settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl'], (err, configItems) => {
if (err) {
return callback(err);
}
@@ -221,35 +222,55 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
return;
}
- mailer.sendMail({
- from: {
- name: configItems.defaultFrom,
- address: configItems.defaultAddress
- },
- to: {
- name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
- address: email
- },
- subject: util.format(_('%s: Please Confirm Subscription'), list.name),
- encryptionKeys
- }, {
- html: 'emails/confirm-html.hbs',
- text: 'emails/confirm-text.hbs',
- data: {
- title: list.name,
- contactAddress: configItems.defaultAddress,
- confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
- }
- }, err => {
+ let sendMail = (html, text) => {
+ mailer.sendMail({
+ from: {
+ name: configItems.defaultFrom,
+ address: configItems.defaultAddress
+ },
+ to: {
+ name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
+ address: email
+ },
+ subject: util.format(_('%s: Please Confirm Subscription'), list.name),
+ encryptionKeys
+ }, {
+ html,
+ text,
+ data: {
+ title: list.name,
+ contactAddress: configItems.defaultAddress,
+ defaultPostaddress: configItems.defaultPostaddress,
+ confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
+ }
+ }, err => {
+ if (err) {
+ log.error('Subscription', err.stack);
+ }
+ });
+ };
+
+ let text = {
+ template: 'subscription/mail-confirm-text.hbs'
+ };
+
+ let html = {
+ template: 'subscription/mail-confirm-html.mjml.hbs',
+ layout: 'subscription/layout.mjml.hbs',
+ type: 'mjml'
+ };
+
+ helpers.injectCustomFormTemplates(list.defaultForm, { text, html }, (err, tmpl) => {
if (err) {
- log.error('Subscription', err.stack);
+ return sendMail(html, text);
}
+
+ sendMail(tmpl.html, tmpl.text);
});
});
return callback(null, cid);
});
});
-
});
});
});
diff --git a/lib/tools.js b/lib/tools.js
index e80b8d4a..e8e35f64 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -1,5 +1,7 @@
'use strict';
+let fs = require('fs');
+let path = require('path');
let db = require('./db');
let slugify = require('slugify');
let Isemail = require('isemail');
@@ -25,6 +27,7 @@ module.exports = {
getMessageLinks,
prepareHtml,
purifyHTML,
+ mergeTemplateIntoLayout,
workers: new Set()
};
@@ -245,3 +248,51 @@ function purifyHTML(html) {
let DOMPurify = createDOMPurify(win);
return DOMPurify.sanitize(html);
}
+
+// TODO Simplify!
+function mergeTemplateIntoLayout(template, layout, callback) {
+
+ layout = layout || '{{{body}}}';
+
+ let readFile = (relPath, callback) => {
+ fs.readFile(path.join(__dirname, '..', 'views', relPath), 'utf-8', (err, source) => {
+ if (err) {
+ return callback(err);
+ }
+ callback(null, source);
+ });
+ };
+
+ let done = (template, layout) => {
+ let source = layout.replace(/\{\{\{body\}\}\}/g, template);
+ return callback(null, source);
+ };
+
+ if (layout.endsWith('.hbs')) {
+ readFile(layout, (err, layout) => {
+ if (err) {
+ return callback(err);
+ }
+ // Please dont end your custom messages with .hbs ...
+ if (template.endsWith('.hbs')) {
+ readFile(template, (err, template) => {
+ if (err) {
+ return callback(err);
+ }
+ return done(template, layout);
+ });
+ } else {
+ return done(template, layout);
+ }
+ });
+ } else if (template.endsWith('.hbs')) {
+ readFile(template, (err, template) => {
+ if (err) {
+ return callback(err);
+ }
+ return done(template, layout);
+ });
+ } else {
+ return done(template, layout);
+ }
+}
\ No newline at end of file
diff --git a/meta.json b/meta.json
index 731dc8bf..7bb9c8c4 100644
--- a/meta.json
+++ b/meta.json
@@ -1,3 +1,3 @@
{
- "schemaVersion": 21
+ "schemaVersion": 22
}
diff --git a/package.json b/package.json
index 5c0eb349..f5c7c6d5 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"libmime": "^3.1.0",
"marked": "^0.3.6",
"memory-cache": "^0.1.6",
+ "mjml": "^3.2.2",
"mkdirp": "^0.5.1",
"moment-timezone": "^0.5.11",
"morgan": "^1.8.1",
@@ -81,6 +82,7 @@
"nodemailer": "^3.1.7",
"nodemailer-openpgp": "^1.0.2",
"npmlog": "^4.0.2",
+ "object-hash": "^1.1.7",
"openpgp": "^2.5.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
diff --git a/public/ace/mode-css.js b/public/ace/mode-css.js
new file mode 100644
index 00000000..84cd16c6
--- /dev/null
+++ b/public/ace/mode-css.js
@@ -0,0 +1 @@
+ace.define("ace/mode/css_highlight_rules",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/lang"),s=e("./text_highlight_rules").TextHighlightRules,o=t.supportType="align-content|align-items|align-self|all|animation|animation-delay|animation-direction|animation-duration|animation-fill-mode|animation-iteration-count|animation-name|animation-play-state|animation-timing-function|backface-visibility|background|background-attachment|background-blend-mode|background-clip|background-color|background-image|background-origin|background-position|background-repeat|background-size|border|border-bottom|border-bottom-color|border-bottom-left-radius|border-bottom-right-radius|border-bottom-style|border-bottom-width|border-collapse|border-color|border-image|border-image-outset|border-image-repeat|border-image-slice|border-image-source|border-image-width|border-left|border-left-color|border-left-style|border-left-width|border-radius|border-right|border-right-color|border-right-style|border-right-width|border-spacing|border-style|border-top|border-top-color|border-top-left-radius|border-top-right-radius|border-top-style|border-top-width|border-width|bottom|box-shadow|box-sizing|caption-side|clear|clip|color|column-count|column-fill|column-gap|column-rule|column-rule-color|column-rule-style|column-rule-width|column-span|column-width|columns|content|counter-increment|counter-reset|cursor|direction|display|empty-cells|filter|flex|flex-basis|flex-direction|flex-flow|flex-grow|flex-shrink|flex-wrap|float|font|font-family|font-size|font-size-adjust|font-stretch|font-style|font-variant|font-weight|hanging-punctuation|height|justify-content|left|letter-spacing|line-height|list-style|list-style-image|list-style-position|list-style-type|margin|margin-bottom|margin-left|margin-right|margin-top|max-height|max-width|min-height|min-width|nav-down|nav-index|nav-left|nav-right|nav-up|opacity|order|outline|outline-color|outline-offset|outline-style|outline-width|overflow|overflow-x|overflow-y|padding|padding-bottom|padding-left|padding-right|padding-top|page-break-after|page-break-before|page-break-inside|perspective|perspective-origin|position|quotes|resize|right|tab-size|table-layout|text-align|text-align-last|text-decoration|text-decoration-color|text-decoration-line|text-decoration-style|text-indent|text-justify|text-overflow|text-shadow|text-transform|top|transform|transform-origin|transform-style|transition|transition-delay|transition-duration|transition-property|transition-timing-function|unicode-bidi|vertical-align|visibility|white-space|width|word-break|word-spacing|word-wrap|z-index",u=t.supportFunction="rgb|rgba|url|attr|counter|counters",a=t.supportConstant="absolute|after-edge|after|all-scroll|all|alphabetic|always|antialiased|armenian|auto|avoid-column|avoid-page|avoid|balance|baseline|before-edge|before|below|bidi-override|block-line-height|block|bold|bolder|border-box|both|bottom|box|break-all|break-word|capitalize|caps-height|caption|center|central|char|circle|cjk-ideographic|clone|close-quote|col-resize|collapse|column|consider-shifts|contain|content-box|cover|crosshair|cubic-bezier|dashed|decimal-leading-zero|decimal|default|disabled|disc|disregard-shifts|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ease-in|ease-in-out|ease-out|ease|ellipsis|end|exclude-ruby|fill|fixed|georgian|glyphs|grid-height|groove|hand|hanging|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|icon|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|ideographic|inactive|include-ruby|inherit|initial|inline-block|inline-box|inline-line-height|inline-table|inline|inset|inside|inter-ideograph|inter-word|invert|italic|justify|katakana-iroha|katakana|keep-all|last|left|lighter|line-edge|line-through|line|linear|list-item|local|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|mathematical|max-height|max-size|medium|menu|message-box|middle|move|n-resize|ne-resize|newspaper|no-change|no-close-quote|no-drop|no-open-quote|no-repeat|none|normal|not-allowed|nowrap|nw-resize|oblique|open-quote|outset|outside|overline|padding-box|page|pointer|pre-line|pre-wrap|pre|preserve-3d|progress|relative|repeat-x|repeat-y|repeat|replaced|reset-size|ridge|right|round|row-resize|rtl|s-resize|scroll|se-resize|separate|slice|small-caps|small-caption|solid|space|square|start|static|status-bar|step-end|step-start|steps|stretch|strict|sub|super|sw-resize|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|tb-rl|text-after-edge|text-before-edge|text-bottom|text-size|text-top|text|thick|thin|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|use-script|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|z-index|zero",f=t.supportConstantColor="aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow",l=t.supportConstantFonts="arial|century|comic|courier|cursive|fantasy|garamond|georgia|helvetica|impact|lucida|symbol|system|tahoma|times|trebuchet|utopia|verdana|webdings|sans-serif|serif|monospace",c=t.numRe="\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))",h=t.pseudoElements="(\\:+)\\b(after|before|first-letter|first-line|moz-selection|selection)\\b",p=t.pseudoClasses="(:)\\b(active|checked|disabled|empty|enabled|first-child|first-of-type|focus|hover|indeterminate|invalid|last-child|last-of-type|link|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|only-child|only-of-type|required|root|target|valid|visited)\\b",d=function(){var e=this.createKeywordMapper({"support.function":u,"support.constant":a,"support.type":o,"support.constant.color":f,"support.constant.fonts":l},"text",!0);this.$rules={start:[{token:"comment",regex:"\\/\\*",push:"comment"},{token:"paren.lparen",regex:"\\{",push:"ruleset"},{token:"string",regex:"@.*?{",push:"media"},{token:"keyword",regex:"#[a-z0-9-_]+"},{token:"variable",regex:"\\.[a-z0-9-_]+"},{token:"string",regex:":[a-z0-9-_]+"},{token:"constant",regex:"[a-z0-9-_]+"},{caseInsensitive:!0}],media:[{token:"comment",regex:"\\/\\*",push:"comment"},{token:"paren.lparen",regex:"\\{",push:"ruleset"},{token:"string",regex:"\\}",next:"pop"},{token:"keyword",regex:"#[a-z0-9-_]+"},{token:"variable",regex:"\\.[a-z0-9-_]+"},{token:"string",regex:":[a-z0-9-_]+"},{token:"constant",regex:"[a-z0-9-_]+"},{caseInsensitive:!0}],comment:[{token:"comment",regex:"\\*\\/",next:"pop"},{defaultToken:"comment"}],ruleset:[{token:"paren.rparen",regex:"\\}",next:"pop"},{token:"comment",regex:"\\/\\*",push:"comment"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:["constant.numeric","keyword"],regex:"("+c+")(ch|cm|deg|em|ex|fr|gd|grad|Hz|in|kHz|mm|ms|pc|pt|px|rad|rem|s|turn|vh|vm|vw|%)"},{token:"constant.numeric",regex:c},{token:"constant.numeric",regex:"#[a-f0-9]{6}"},{token:"constant.numeric",regex:"#[a-f0-9]{3}"},{token:["punctuation","entity.other.attribute-name.pseudo-element.css"],regex:h},{token:["punctuation","entity.other.attribute-name.pseudo-class.css"],regex:p},{token:["support.function","string","support.function"],regex:"(url\\()(.*)(\\))"},{token:e,regex:"\\-?[a-zA-Z_][a-zA-Z0-9_\\-]*"},{caseInsensitive:!0}]},this.normalizeRules()};r.inherits(d,s),t.CssHighlightRules=d}),ace.define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),ace.define("ace/mode/css_completions",["require","exports","module"],function(e,t,n){"use strict";var r={background:{"#$0":1},"background-color":{"#$0":1,transparent:1,fixed:1},"background-image":{"url('/$0')":1},"background-repeat":{repeat:1,"repeat-x":1,"repeat-y":1,"no-repeat":1,inherit:1},"background-position":{bottom:2,center:2,left:2,right:2,top:2,inherit:2},"background-attachment":{scroll:1,fixed:1},"background-size":{cover:1,contain:1},"background-clip":{"border-box":1,"padding-box":1,"content-box":1},"background-origin":{"border-box":1,"padding-box":1,"content-box":1},border:{"solid $0":1,"dashed $0":1,"dotted $0":1,"#$0":1},"border-color":{"#$0":1},"border-style":{solid:2,dashed:2,dotted:2,"double":2,groove:2,hidden:2,inherit:2,inset:2,none:2,outset:2,ridged:2},"border-collapse":{collapse:1,separate:1},bottom:{px:1,em:1,"%":1},clear:{left:1,right:1,both:1,none:1},color:{"#$0":1,"rgb(#$00,0,0)":1},cursor:{"default":1,pointer:1,move:1,text:1,wait:1,help:1,progress:1,"n-resize":1,"ne-resize":1,"e-resize":1,"se-resize":1,"s-resize":1,"sw-resize":1,"w-resize":1,"nw-resize":1},display:{none:1,block:1,inline:1,"inline-block":1,"table-cell":1},"empty-cells":{show:1,hide:1},"float":{left:1,right:1,none:1},"font-family":{Arial:2,"Comic Sans MS":2,Consolas:2,"Courier New":2,Courier:2,Georgia:2,Monospace:2,"Sans-Serif":2,"Segoe UI":2,Tahoma:2,"Times New Roman":2,"Trebuchet MS":2,Verdana:1},"font-size":{px:1,em:1,"%":1},"font-weight":{bold:1,normal:1},"font-style":{italic:1,normal:1},"font-variant":{normal:1,"small-caps":1},height:{px:1,em:1,"%":1},left:{px:1,em:1,"%":1},"letter-spacing":{normal:1},"line-height":{normal:1},"list-style-type":{none:1,disc:1,circle:1,square:1,decimal:1,"decimal-leading-zero":1,"lower-roman":1,"upper-roman":1,"lower-greek":1,"lower-latin":1,"upper-latin":1,georgian:1,"lower-alpha":1,"upper-alpha":1},margin:{px:1,em:1,"%":1},"margin-right":{px:1,em:1,"%":1},"margin-left":{px:1,em:1,"%":1},"margin-top":{px:1,em:1,"%":1},"margin-bottom":{px:1,em:1,"%":1},"max-height":{px:1,em:1,"%":1},"max-width":{px:1,em:1,"%":1},"min-height":{px:1,em:1,"%":1},"min-width":{px:1,em:1,"%":1},overflow:{hidden:1,visible:1,auto:1,scroll:1},"overflow-x":{hidden:1,visible:1,auto:1,scroll:1},"overflow-y":{hidden:1,visible:1,auto:1,scroll:1},padding:{px:1,em:1,"%":1},"padding-top":{px:1,em:1,"%":1},"padding-right":{px:1,em:1,"%":1},"padding-bottom":{px:1,em:1,"%":1},"padding-left":{px:1,em:1,"%":1},"page-break-after":{auto:1,always:1,avoid:1,left:1,right:1},"page-break-before":{auto:1,always:1,avoid:1,left:1,right:1},position:{absolute:1,relative:1,fixed:1,"static":1},right:{px:1,em:1,"%":1},"table-layout":{fixed:1,auto:1},"text-decoration":{none:1,underline:1,"line-through":1,blink:1},"text-align":{left:1,right:1,center:1,justify:1},"text-transform":{capitalize:1,uppercase:1,lowercase:1,none:1},top:{px:1,em:1,"%":1},"vertical-align":{top:1,bottom:1},visibility:{hidden:1,visible:1},"white-space":{nowrap:1,normal:1,pre:1,"pre-line":1,"pre-wrap":1},width:{px:1,em:1,"%":1},"word-spacing":{normal:1},filter:{"alpha(opacity=$0100)":1},"text-shadow":{"$02px 2px 2px #777":1},"text-overflow":{"ellipsis-word":1,clip:1,ellipsis:1},"-moz-border-radius":1,"-moz-border-radius-topright":1,"-moz-border-radius-bottomright":1,"-moz-border-radius-topleft":1,"-moz-border-radius-bottomleft":1,"-webkit-border-radius":1,"-webkit-border-top-right-radius":1,"-webkit-border-top-left-radius":1,"-webkit-border-bottom-right-radius":1,"-webkit-border-bottom-left-radius":1,"-moz-box-shadow":1,"-webkit-box-shadow":1,transform:{"rotate($00deg)":1,"skew($00deg)":1},"-moz-transform":{"rotate($00deg)":1,"skew($00deg)":1},"-webkit-transform":{"rotate($00deg)":1,"skew($00deg)":1}},i=function(){};(function(){this.completionsDefined=!1,this.defineCompletions=function(){if(document){var e=document.createElement("c").style;for(var t in e){if(typeof e[t]!="string")continue;var n=t.replace(/[A-Z]/g,function(e){return"-"+e.toLowerCase()});r.hasOwnProperty(n)||(r[n]=1)}}this.completionsDefined=!0},this.getCompletions=function(e,t,n,r){this.completionsDefined||this.defineCompletions();var i=t.getTokenAt(n.row,n.column);if(!i)return[];if(e==="ruleset"){var s=t.getLine(n.row).substr(0,n.column);return/:[^;]+$/.test(s)?(/([\w\-]+):[^:]*$/.test(s),this.getPropertyValueCompletions(e,t,n,r)):this.getPropertyCompletions(e,t,n,r)}return[]},this.getPropertyCompletions=function(e,t,n,i){var s=Object.keys(r);return s.map(function(e){return{caption:e,snippet:e+": $0",meta:"property",score:Number.MAX_VALUE}})},this.getPropertyValueCompletions=function(e,t,n,i){var s=t.getLine(n.row).substr(0,n.column),o=(/([\w\-]+):[^:]*$/.exec(s)||{})[1];if(!o)return[];var u=[];return o in r&&typeof r[o]=="object"&&(u=Object.keys(r[o])),u.map(function(e){return{caption:e,snippet:e,meta:"property value",score:Number.MAX_VALUE}})}}).call(i.prototype),t.CssCompletions=i}),ace.define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},h=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},p=function(){this.add("braces","insertion",function(e,t,n,r,i){var s=n.getCursorPosition(),u=r.doc.getLine(s.row);if(i=="{"){c(n);var a=n.getSelectionRange(),l=r.doc.getTextRange(a);if(l!==""&&l!=="{"&&n.getWrapBehavioursEnabled())return h(a,l,"{","}");if(p.isSaneInsertion(n,r))return/[\]\}\)]/.test(u[s.column])||n.inMultiSelectMode?(p.recordAutoInsert(n,r,"}"),{text:"{}",selection:[1,1]}):(p.recordMaybeInsert(n,r,"{"),{text:"{",selection:[1,1]})}else if(i=="}"){c(n);var d=u.substring(s.column,s.column+1);if(d=="}"){var v=r.$findOpeningBracket("}",{column:s.column+1,row:s.row});if(v!==null&&p.isAutoInsertedClosing(s,u,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(i=="\n"||i=="\r\n"){c(n);var m="";p.isMaybeInsertedClosing(s,u)&&(m=o.stringRepeat("}",f.maybeInsertedBrackets),p.clearMaybeInsertedClosing());var d=u.substring(s.column,s.column+1);if(d==="}"){var g=r.findMatchingBracket({row:s.row,column:s.column+1},"}");if(!g)return null;var y=this.$getIndent(r.getLine(g.row))}else{if(!m){p.clearMaybeInsertedClosing();return}var y=this.$getIndent(u)}var b=y+r.getTabString();return{text:"\n"+b+"\n"+y+m,selection:[1,b.length,1,b.length]}}p.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"(",")");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"[","]");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){if(i=='"'||i=="'"){c(n);var s=i,o=n.getSelectionRange(),u=r.doc.getTextRange(o);if(u!==""&&u!=="'"&&u!='"'&&n.getWrapBehavioursEnabled())return h(o,u,s,s);if(!u){var a=n.getCursorPosition(),f=r.doc.getLine(a.row),l=f.substring(a.column-1,a.column),p=f.substring(a.column,a.column+1),d=r.getTokenAt(a.row,a.column),v=r.getTokenAt(a.row,a.column+1);if(l=="\\"&&d&&/escape/.test(d.type))return null;var m=d&&/string|escape/.test(d.type),g=!v||/string|escape/.test(v.type),y;if(p==s)y=m!==g;else{if(m&&!g)return null;if(m&&g)return null;var b=r.$mode.tokenRe;b.lastIndex=0;var w=b.test(l);b.lastIndex=0;var E=b.test(l);if(w||E)return null;if(p&&!/[\s;,.})\]\\]/.test(p))return null;y=!0}return{text:y?s+s:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&(s=='"'||s=="'")){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==s)return i.end.column++,i}})};p.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},p.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},p.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},p.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},p.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},p.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},p.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},p.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(p,i),t.CstyleBehaviour=p}),ace.define("ace/mode/behaviour/css",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle","ace/token_iterator"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("./cstyle").CstyleBehaviour,o=e("../../token_iterator").TokenIterator,u=function(){this.inherit(s),this.add("colon","insertion",function(e,t,n,r,i){if(i===":"){var s=n.getCursorPosition(),u=new o(r,s.row,s.column),a=u.getCurrentToken();a&&a.value.match(/\s+/)&&(a=u.stepBackward());if(a&&a.type==="support.type"){var f=r.doc.getLine(s.row),l=f.substring(s.column,s.column+1);if(l===":")return{text:"",selection:[1,1]};if(!f.substring(s.column).match(/^\s*;/))return{text:":;",selection:[1,1]}}}}),this.add("colon","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s===":"){var u=n.getCursorPosition(),a=new o(r,u.row,u.column),f=a.getCurrentToken();f&&f.value.match(/\s+/)&&(f=a.stepBackward());if(f&&f.type==="support.type"){var l=r.doc.getLine(i.start.row),c=l.substring(i.end.column,i.end.column+1);if(c===";")return i.end.column++,i}}}),this.add("semicolon","insertion",function(e,t,n,r,i){if(i===";"){var s=n.getCursorPosition(),o=r.doc.getLine(s.row),u=o.substring(s.column,s.column+1);if(u===";")return{text:"",selection:[1,1]}}})};r.inherits(u,s),t.CssBehaviour=u}),ace.define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),ace.define("ace/mode/css",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/css_highlight_rules","ace/mode/matching_brace_outdent","ace/worker/worker_client","ace/mode/css_completions","ace/mode/behaviour/css","ace/mode/folding/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./css_highlight_rules").CssHighlightRules,o=e("./matching_brace_outdent").MatchingBraceOutdent,u=e("../worker/worker_client").WorkerClient,a=e("./css_completions").CssCompletions,f=e("./behaviour/css").CssBehaviour,l=e("./folding/cstyle").FoldMode,c=function(){this.HighlightRules=s,this.$outdent=new o,this.$behaviour=new f,this.$completer=new a,this.foldingRules=new l};r.inherits(c,i),function(){this.foldingRules="cStyle",this.blockComment={start:"/*",end:"*/"},this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e).tokens;if(i.length&&i[i.length-1].type=="comment")return r;var s=t.match(/^.*\{\s*$/);return s&&(r+=n),r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.getCompletions=function(e,t,n,r){return this.$completer.getCompletions(e,t,n,r)},this.createWorker=function(e){var t=new u(["ace"],"ace/mode/css_worker","Worker");return t.attachToDocument(e.getDocument()),t.on("annotate",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/css"}.call(c.prototype),t.Mode=c})
\ No newline at end of file
diff --git a/public/ace/mode-plain_text.js b/public/ace/mode-plain_text.js
new file mode 100644
index 00000000..21cf21da
--- /dev/null
+++ b/public/ace/mode-plain_text.js
@@ -0,0 +1 @@
+ace.define("ace/mode/plain_text",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/text_highlight_rules","ace/mode/behaviour"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./text_highlight_rules").TextHighlightRules,o=e("./behaviour").Behaviour,u=function(){this.HighlightRules=s,this.$behaviour=new o};r.inherits(u,i),function(){this.type="text",this.getNextLineIndent=function(e,t,n){return""},this.$id="ace/mode/plain_text"}.call(u.prototype),t.Mode=u})
diff --git a/public/ace/worker-css.js b/public/ace/worker-css.js
new file mode 100644
index 00000000..408c6bd0
--- /dev/null
+++ b/public/ace/worker-css.js
@@ -0,0 +1 @@
+"no use strict";(function(e){function t(e,t){var n=e,r="";while(n){var i=t[n];if(typeof i=="string")return i+r;if(i)return i.location.replace(/\/*$/,"/")+(r||i.main||i.name);if(i===!1)return"";var s=n.lastIndexOf("/");if(s===-1)break;r=n.substr(s)+r,n=n.slice(0,s)}return e}if(typeof e.window!="undefined"&&e.document)return;if(e.require&&e.define)return;e.console||(e.console=function(){var e=Array.prototype.slice.call(arguments,0);postMessage({type:"log",data:e})},e.console.error=e.console.warn=e.console.log=e.console.trace=e.console),e.window=e,e.ace=e,e.onerror=function(e,t,n,r,i){postMessage({type:"error",data:{message:e,data:i.data,file:t,line:n,col:r,stack:i.stack}})},e.normalizeModule=function(t,n){if(n.indexOf("!")!==-1){var r=n.split("!");return e.normalizeModule(t,r[0])+"!"+e.normalizeModule(t,r[1])}if(n.charAt(0)=="."){var i=t.split("/").slice(0,-1).join("/");n=(i?i+"/":"")+n;while(n.indexOf(".")!==-1&&s!=n){var s=n;n=n.replace(/^\.\//,"").replace(/\/\.\//,"/").replace(/[^\/]+\/\.\.\//,"")}}return n},e.require=function(r,i){i||(i=r,r=null);if(!i.charAt)throw new Error("worker.js require() accepts only (parentId, id) as arguments");i=e.normalizeModule(r,i);var s=e.require.modules[i];if(s)return s.initialized||(s.initialized=!0,s.exports=s.factory().exports),s.exports;if(!e.require.tlns)return console.log("unable to load "+i);var o=t(i,e.require.tlns);return o.slice(-3)!=".js"&&(o+=".js"),e.require.id=i,e.require.modules[i]={},importScripts(o),e.require(r,i)},e.require.modules={},e.require.tlns={},e.define=function(t,n,r){arguments.length==2?(r=n,typeof t!="string"&&(n=t,t=e.require.id)):arguments.length==1&&(r=t,n=[],t=e.require.id);if(typeof r!="function"){e.require.modules[t]={exports:r,initialized:!0};return}n.length||(n=["require","exports","module"]);var i=function(n){return e.require(t,n)};e.require.modules[t]={exports:{},factory:function(){var e=this,t=r.apply(this,n.map(function(t){switch(t){case"require":return i;case"exports":return e.exports;case"module":return e;default:return i(t)}}));return t&&(e.exports=t),e}}},e.define.amd={},require.tlns={},e.initBaseUrls=function(t){for(var n in t)require.tlns[n]=t[n]},e.initSender=function(){var n=e.require("ace/lib/event_emitter").EventEmitter,r=e.require("ace/lib/oop"),i=function(){};return function(){r.implement(this,n),this.callback=function(e,t){postMessage({type:"call",id:t,data:e})},this.emit=function(e,t){postMessage({type:"event",name:e,data:t})}}.call(i.prototype),new i};var n=e.main=null,r=e.sender=null;e.onmessage=function(t){var i=t.data;if(i.event&&r)r._signal(i.event,i.data);else if(i.command)if(n[i.command])n[i.command].apply(n,i.args);else{if(!e[i.command])throw new Error("Unknown command:"+i.command);e[i.command].apply(e,i.args)}else if(i.init){e.initBaseUrls(i.tlns),require("ace/lib/es5-shim"),r=e.sender=e.initSender();var s=require(i.module)[i.classname];n=e.main=new s(r)}}})(this),ace.define("ace/lib/oop",["require","exports","module"],function(e,t,n){"use strict";t.inherits=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},t.mixin=function(e,t){for(var n in t)e[n]=t[n];return e},t.implement=function(e,n){t.mixin(e,n)}}),ace.define("ace/lib/lang",["require","exports","module"],function(e,t,n){"use strict";t.last=function(e){return e[e.length-1]},t.stringReverse=function(e){return e.split("").reverse().join("")},t.stringRepeat=function(e,t){var n="";while(t>0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?tthis.end.column?1:0:ethis.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.rowt)var r={row:t+1,column:0};else if(this.start.row=0&&t.row=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),ace.define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;othis.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e0,r=t=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4&&this.$splitAndapplyLargeDelta(e,2e4),i(this.$lines,e,t),this._signal("change",e)},this.$splitAndapplyLargeDelta=function(e,t){var n=e.lines,r=n.length,i=e.start.row,s=e.start.column,o=0,u=0;do{o=u,u+=t-1;var a=n.slice(o,u);if(u>r){e.lines=a,e.start.row=i+o,e.start.column=s;break}a.push(""),this.applyDelta({start:this.pos(i+o,s),end:this.pos(i+u,s=0),action:e.action,lines:a},!0)}while(!0)},this.revertDelta=function(e){this.applyDelta({start:this.clonePos(e.start),end:this.clonePos(e.end),action:e.action=="insert"?"remove":"insert",lines:e.lines.slice()})},this.indexToPosition=function(e,t){var n=this.$lines||this.getAllLines(),r=this.getNewLineCharacter().length;for(var i=t||0,s=n.length;i=0&&this._ltIndex-1&&!t[u.type].hide&&(u.channel=t[u.type].channel,this._token=u,this._lt.push(u),this._ltIndexCache.push(this._lt.length-this._ltIndex+i),this._lt.length>5&&this._lt.shift(),this._ltIndexCache.length>5&&this._ltIndexCache.shift(),this._ltIndex=this._lt.length),a=t[u.type],a&&(a.hide||a.channel!==undefined&&e!==a.channel)?this.get(e):u.type},LA:function(e){var t=e,n;if(e>0){if(e>5)throw new Error("Too much lookahead.");while(t)n=this.get(),t--;while(tthis._tokenData.length?"UNKNOWN_TOKEN":this._tokenData[e].name},tokenType:function(e){return this._tokenData[e]||-1},unget:function(){if(!this._ltIndexCache.length)throw new Error("Too much lookahead.");this._ltIndex-=this._ltIndexCache.pop(),this._token=this._lt[this._ltIndex-1]}},parserlib.util={StringReader:t,SyntaxError:n,SyntaxUnit:r,EventTarget:e,TokenStreamBase:i}})(),function(){function Combinator(e,t,n){SyntaxUnit.call(this,e,t,n,Parser.COMBINATOR_TYPE),this.type="unknown",/^\s+$/.test(e)?this.type="descendant":e==">"?this.type="child":e=="+"?this.type="adjacent-sibling":e=="~"&&(this.type="sibling")}function MediaFeature(e,t){SyntaxUnit.call(this,"("+e+(t!==null?":"+t:"")+")",e.startLine,e.startCol,Parser.MEDIA_FEATURE_TYPE),this.name=e,this.value=t}function MediaQuery(e,t,n,r,i){SyntaxUnit.call(this,(e?e+" ":"")+(t?t:"")+(t&&n.length>0?" and ":"")+n.join(" and "),r,i,Parser.MEDIA_QUERY_TYPE),this.modifier=e,this.mediaType=t,this.features=n}function Parser(e){EventTarget.call(this),this.options=e||{},this._tokenStream=null}function PropertyName(e,t,n,r){SyntaxUnit.call(this,e,n,r,Parser.PROPERTY_NAME_TYPE),this.hack=t}function PropertyValue(e,t,n){SyntaxUnit.call(this,e.join(" "),t,n,Parser.PROPERTY_VALUE_TYPE),this.parts=e}function PropertyValueIterator(e){this._i=0,this._parts=e.parts,this._marks=[],this.value=e}function PropertyValuePart(text,line,col){SyntaxUnit.call(this,text,line,col,Parser.PROPERTY_VALUE_PART_TYPE),this.type="unknown";var temp;if(/^([+\-]?[\d\.]+)([a-z]+)$/i.test(text)){this.type="dimension",this.value=+RegExp.$1,this.units=RegExp.$2;switch(this.units.toLowerCase()){case"em":case"rem":case"ex":case"px":case"cm":case"mm":case"in":case"pt":case"pc":case"ch":case"vh":case"vw":case"vmax":case"vmin":this.type="length";break;case"deg":case"rad":case"grad":this.type="angle";break;case"ms":case"s":this.type="time";break;case"hz":case"khz":this.type="frequency";break;case"dpi":case"dpcm":this.type="resolution"}}else/^([+\-]?[\d\.]+)%$/i.test(text)?(this.type="percentage",this.value=+RegExp.$1):/^([+\-]?\d+)$/i.test(text)?(this.type="integer",this.value=+RegExp.$1):/^([+\-]?[\d\.]+)$/i.test(text)?(this.type="number",this.value=+RegExp.$1):/^#([a-f0-9]{3,6})/i.test(text)?(this.type="color",temp=RegExp.$1,temp.length==3?(this.red=parseInt(temp.charAt(0)+temp.charAt(0),16),this.green=parseInt(temp.charAt(1)+temp.charAt(1),16),this.blue=parseInt(temp.charAt(2)+temp.charAt(2),16)):(this.red=parseInt(temp.substring(0,2),16),this.green=parseInt(temp.substring(2,4),16),this.blue=parseInt(temp.substring(4,6),16))):/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1,this.green=+RegExp.$2,this.blue=+RegExp.$3):/^rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1*255/100,this.green=+RegExp.$2*255/100,this.blue=+RegExp.$3*255/100):/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d\.]+)\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1,this.green=+RegExp.$2,this.blue=+RegExp.$3,this.alpha=+RegExp.$4):/^rgba\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1*255/100,this.green=+RegExp.$2*255/100,this.blue=+RegExp.$3*255/100,this.alpha=+RegExp.$4):/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)?(this.type="color",this.hue=+RegExp.$1,this.saturation=+RegExp.$2/100,this.lightness=+RegExp.$3/100):/^hsla\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)?(this.type="color",this.hue=+RegExp.$1,this.saturation=+RegExp.$2/100,this.lightness=+RegExp.$3/100,this.alpha=+RegExp.$4):/^url\(["']?([^\)"']+)["']?\)/i.test(text)?(this.type="uri",this.uri=RegExp.$1):/^([^\(]+)\(/i.test(text)?(this.type="function",this.name=RegExp.$1,this.value=text):/^["'][^"']*["']/.test(text)?(this.type="string",this.value=eval(text)):Colors[text.toLowerCase()]?(this.type="color",temp=Colors[text.toLowerCase()].substring(1),this.red=parseInt(temp.substring(0,2),16),this.green=parseInt(temp.substring(2,4),16),this.blue=parseInt(temp.substring(4,6),16)):/^[\,\/]$/.test(text)?(this.type="operator",this.value=text):/^[a-z\-_\u0080-\uFFFF][a-z0-9\-_\u0080-\uFFFF]*$/i.test(text)&&(this.type="identifier",this.value=text)}function Selector(e,t,n){SyntaxUnit.call(this,e.join(" "),t,n,Parser.SELECTOR_TYPE),this.parts=e,this.specificity=Specificity.calculate(this)}function SelectorPart(e,t,n,r,i){SyntaxUnit.call(this,n,r,i,Parser.SELECTOR_PART_TYPE),this.elementName=e,this.modifiers=t}function SelectorSubPart(e,t,n,r){SyntaxUnit.call(this,e,n,r,Parser.SELECTOR_SUB_PART_TYPE),this.type=t,this.args=[]}function Specificity(e,t,n,r){this.a=e,this.b=t,this.c=n,this.d=r}function isHexDigit(e){return e!==null&&h.test(e)}function isDigit(e){return e!==null&&/\d/.test(e)}function isWhitespace(e){return e!==null&&/\s/.test(e)}function isNewLine(e){return e!==null&&nl.test(e)}function isNameStart(e){return e!==null&&/[a-z_\u0080-\uFFFF\\]/i.test(e)}function isNameChar(e){return e!==null&&(isNameStart(e)||/[0-9\-\\]/.test(e))}function isIdentStart(e){return e!==null&&(isNameStart(e)||/\-\\/.test(e))}function mix(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function TokenStream(e){TokenStreamBase.call(this,e,Tokens)}function ValidationError(e,t,n){this.col=n,this.line=t,this.message=e}var EventTarget=parserlib.util.EventTarget,TokenStreamBase=parserlib.util.TokenStreamBase,StringReader=parserlib.util.StringReader,SyntaxError=parserlib.util.SyntaxError,SyntaxUnit=parserlib.util.SyntaxUnit,Colors={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgrey:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",grey:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32",activeBorder:"Active window border.",activecaption:"Active window caption.",appworkspace:"Background color of multiple document interface.",background:"Desktop background.",buttonface:"The face background color for 3-D elements that appear 3-D due to one layer of surrounding border.",buttonhighlight:"The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border.",buttonshadow:"The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border.",buttontext:"Text on push buttons.",captiontext:"Text in caption, size box, and scrollbar arrow box.",graytext:"Grayed (disabled) text. This color is set to #000 if the current display driver does not support a solid gray color.",greytext:"Greyed (disabled) text. This color is set to #000 if the current display driver does not support a solid grey color.",highlight:"Item(s) selected in a control.",highlighttext:"Text of item(s) selected in a control.",inactiveborder:"Inactive window border.",inactivecaption:"Inactive window caption.",inactivecaptiontext:"Color of text in an inactive caption.",infobackground:"Background color for tooltip controls.",infotext:"Text color for tooltip controls.",menu:"Menu background.",menutext:"Text in menus.",scrollbar:"Scroll bar gray area.",threeddarkshadow:"The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",threedface:"The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",threedhighlight:"The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",threedlightshadow:"The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",threedshadow:"The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",window:"Window background.",windowframe:"Window frame.",windowtext:"Text in windows."};Combinator.prototype=new SyntaxUnit,Combinator.prototype.constructor=Combinator,MediaFeature.prototype=new SyntaxUnit,MediaFeature.prototype.constructor=MediaFeature,MediaQuery.prototype=new SyntaxUnit,MediaQuery.prototype.constructor=MediaQuery,Parser.DEFAULT_TYPE=0,Parser.COMBINATOR_TYPE=1,Parser.MEDIA_FEATURE_TYPE=2,Parser.MEDIA_QUERY_TYPE=3,Parser.PROPERTY_NAME_TYPE=4,Parser.PROPERTY_VALUE_TYPE=5,Parser.PROPERTY_VALUE_PART_TYPE=6,Parser.SELECTOR_TYPE=7,Parser.SELECTOR_PART_TYPE=8,Parser.SELECTOR_SUB_PART_TYPE=9,Parser.prototype=function(){var e=new EventTarget,t,n={constructor:Parser,DEFAULT_TYPE:0,COMBINATOR_TYPE:1,MEDIA_FEATURE_TYPE:2,MEDIA_QUERY_TYPE:3,PROPERTY_NAME_TYPE:4,PROPERTY_VALUE_TYPE:5,PROPERTY_VALUE_PART_TYPE:6,SELECTOR_TYPE:7,SELECTOR_PART_TYPE:8,SELECTOR_SUB_PART_TYPE:9,_stylesheet:function(){var e=this._tokenStream,t=null,n,r,i;this.fire("startstylesheet"),this._charset(),this._skipCruft();while(e.peek()==Tokens.IMPORT_SYM)this._import(),this._skipCruft();while(e.peek()==Tokens.NAMESPACE_SYM)this._namespace(),this._skipCruft();i=e.peek();while(i>Tokens.EOF){try{switch(i){case Tokens.MEDIA_SYM:this._media(),this._skipCruft();break;case Tokens.PAGE_SYM:this._page(),this._skipCruft();break;case Tokens.FONT_FACE_SYM:this._font_face(),this._skipCruft();break;case Tokens.KEYFRAMES_SYM:this._keyframes(),this._skipCruft();break;case Tokens.VIEWPORT_SYM:this._viewport(),this._skipCruft();break;case Tokens.UNKNOWN_SYM:e.get();if(!!this.options.strict)throw new SyntaxError("Unknown @ rule.",e.LT(0).startLine,e.LT(0).startCol);this.fire({type:"error",error:null,message:"Unknown @ rule: "+e.LT(0).value+".",line:e.LT(0).startLine,col:e.LT(0).startCol}),n=0;while(e.advance([Tokens.LBRACE,Tokens.RBRACE])==Tokens.LBRACE)n++;while(n)e.advance([Tokens.RBRACE]),n--;break;case Tokens.S:this._readWhitespace();break;default:if(!this._ruleset())switch(i){case Tokens.CHARSET_SYM:throw r=e.LT(1),this._charset(!1),new SyntaxError("@charset not allowed here.",r.startLine,r.startCol);case Tokens.IMPORT_SYM:throw r=e.LT(1),this._import(!1),new SyntaxError("@import not allowed here.",r.startLine,r.startCol);case Tokens.NAMESPACE_SYM:throw r=e.LT(1),this._namespace(!1),new SyntaxError("@namespace not allowed here.",r.startLine,r.startCol);default:e.get(),this._unexpectedToken(e.token())}}}catch(s){if(!(s instanceof SyntaxError&&!this.options.strict))throw s;this.fire({type:"error",error:s,message:s.message,line:s.line,col:s.col})}i=e.peek()}i!=Tokens.EOF&&this._unexpectedToken(e.token()),this.fire("endstylesheet")},_charset:function(e){var t=this._tokenStream,n,r,i,s;t.match(Tokens.CHARSET_SYM)&&(i=t.token().startLine,s=t.token().startCol,this._readWhitespace(),t.mustMatch(Tokens.STRING),r=t.token(),n=r.value,this._readWhitespace(),t.mustMatch(Tokens.SEMICOLON),e!==!1&&this.fire({type:"charset",charset:n,line:i,col:s}))},_import:function(e){var t=this._tokenStream,n,r,i,s=[];t.mustMatch(Tokens.IMPORT_SYM),i=t.token(),this._readWhitespace(),t.mustMatch([Tokens.STRING,Tokens.URI]),r=t.token().value.replace(/^(?:url\()?["']?([^"']+?)["']?\)?$/,"$1"),this._readWhitespace(),s=this._media_query_list(),t.mustMatch(Tokens.SEMICOLON),this._readWhitespace(),e!==!1&&this.fire({type:"import",uri:r,media:s,line:i.startLine,col:i.startCol})},_namespace:function(e){var t=this._tokenStream,n,r,i,s;t.mustMatch(Tokens.NAMESPACE_SYM),n=t.token().startLine,r=t.token().startCol,this._readWhitespace(),t.match(Tokens.IDENT)&&(i=t.token().value,this._readWhitespace()),t.mustMatch([Tokens.STRING,Tokens.URI]),s=t.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/,"$1"),this._readWhitespace(),t.mustMatch(Tokens.SEMICOLON),this._readWhitespace(),e!==!1&&this.fire({type:"namespace",prefix:i,uri:s,line:n,col:r})},_media:function(){var e=this._tokenStream,t,n,r;e.mustMatch(Tokens.MEDIA_SYM),t=e.token().startLine,n=e.token().startCol,this._readWhitespace(),r=this._media_query_list(),e.mustMatch(Tokens.LBRACE),this._readWhitespace(),this.fire({type:"startmedia",media:r,line:t,col:n});for(;;)if(e.peek()==Tokens.PAGE_SYM)this._page();else if(e.peek()==Tokens.FONT_FACE_SYM)this._font_face();else if(e.peek()==Tokens.VIEWPORT_SYM)this._viewport();else if(!this._ruleset())break;e.mustMatch(Tokens.RBRACE),this._readWhitespace(),this.fire({type:"endmedia",media:r,line:t,col:n})},_media_query_list:function(){var e=this._tokenStream,t=[];this._readWhitespace(),(e.peek()==Tokens.IDENT||e.peek()==Tokens.LPAREN)&&t.push(this._media_query());while(e.match(Tokens.COMMA))this._readWhitespace(),t.push(this._media_query());return t},_media_query:function(){var e=this._tokenStream,t=null,n=null,r=null,i=[];e.match(Tokens.IDENT)&&(n=e.token().value.toLowerCase(),n!="only"&&n!="not"?(e.unget(),n=null):r=e.token()),this._readWhitespace(),e.peek()==Tokens.IDENT?(t=this._media_type(),r===null&&(r=e.token())):e.peek()==Tokens.LPAREN&&(r===null&&(r=e.LT(1)),i.push(this._media_expression()));if(t===null&&i.length===0)return null;this._readWhitespace();while(e.match(Tokens.IDENT))e.token().value.toLowerCase()!="and"&&this._unexpectedToken(e.token()),this._readWhitespace(),i.push(this._media_expression());return new MediaQuery(n,t,i,r.startLine,r.startCol)},_media_type:function(){return this._media_feature()},_media_expression:function(){var e=this._tokenStream,t=null,n,r=null;return e.mustMatch(Tokens.LPAREN),t=this._media_feature(),this._readWhitespace(),e.match(Tokens.COLON)&&(this._readWhitespace(),n=e.LT(1),r=this._expression()),e.mustMatch(Tokens.RPAREN),this._readWhitespace(),new MediaFeature(t,r?new SyntaxUnit(r,n.startLine,n.startCol):null)},_media_feature:function(){var e=this._tokenStream;return e.mustMatch(Tokens.IDENT),SyntaxUnit.fromToken(e.token())},_page:function(){var e=this._tokenStream,t,n,r=null,i=null;e.mustMatch(Tokens.PAGE_SYM),t=e.token().startLine,n=e.token().startCol,this._readWhitespace(),e.match(Tokens.IDENT)&&(r=e.token().value,r.toLowerCase()==="auto"&&this._unexpectedToken(e.token())),e.peek()==Tokens.COLON&&(i=this._pseudo_page()),this._readWhitespace(),this.fire({type:"startpage",id:r,pseudo:i,line:t,col:n}),this._readDeclarations(!0,!0),this.fire({type:"endpage",id:r,pseudo:i,line:t,col:n})},_margin:function(){var e=this._tokenStream,t,n,r=this._margin_sym();return r?(t=e.token().startLine,n=e.token().startCol,this.fire({type:"startpagemargin",margin:r,line:t,col:n}),this._readDeclarations(!0),this.fire({type:"endpagemargin",margin:r,line:t,col:n}),!0):!1},_margin_sym:function(){var e=this._tokenStream;return e.match([Tokens.TOPLEFTCORNER_SYM,Tokens.TOPLEFT_SYM,Tokens.TOPCENTER_SYM,Tokens.TOPRIGHT_SYM,Tokens.TOPRIGHTCORNER_SYM,Tokens.BOTTOMLEFTCORNER_SYM,Tokens.BOTTOMLEFT_SYM,Tokens.BOTTOMCENTER_SYM,Tokens.BOTTOMRIGHT_SYM,Tokens.BOTTOMRIGHTCORNER_SYM,Tokens.LEFTTOP_SYM,Tokens.LEFTMIDDLE_SYM,Tokens.LEFTBOTTOM_SYM,Tokens.RIGHTTOP_SYM,Tokens.RIGHTMIDDLE_SYM,Tokens.RIGHTBOTTOM_SYM])?SyntaxUnit.fromToken(e.token()):null},_pseudo_page:function(){var e=this._tokenStream;return e.mustMatch(Tokens.COLON),e.mustMatch(Tokens.IDENT),e.token().value},_font_face:function(){var e=this._tokenStream,t,n;e.mustMatch(Tokens.FONT_FACE_SYM),t=e.token().startLine,n=e.token().startCol,this._readWhitespace(),this.fire({type:"startfontface",line:t,col:n}),this._readDeclarations(!0),this.fire({type:"endfontface",line:t,col:n})},_viewport:function(){var e=this._tokenStream,t,n;e.mustMatch(Tokens.VIEWPORT_SYM),t=e.token().startLine,n=e.token().startCol,this._readWhitespace(),this.fire({type:"startviewport",line:t,col:n}),this._readDeclarations(!0),this.fire({type:"endviewport",line:t,col:n})},_operator:function(e){var t=this._tokenStream,n=null;if(t.match([Tokens.SLASH,Tokens.COMMA])||e&&t.match([Tokens.PLUS,Tokens.STAR,Tokens.MINUS]))n=t.token(),this._readWhitespace();return n?PropertyValuePart.fromToken(n):null},_combinator:function(){var e=this._tokenStream,t=null,n;return e.match([Tokens.PLUS,Tokens.GREATER,Tokens.TILDE])&&(n=e.token(),t=new Combinator(n.value,n.startLine,n.startCol),this._readWhitespace()),t},_unary_operator:function(){var e=this._tokenStream;return e.match([Tokens.MINUS,Tokens.PLUS])?e.token().value:null},_property:function(){var e=this._tokenStream,t=null,n=null,r,i,s,o;return e.peek()==Tokens.STAR&&this.options.starHack&&(e.get(),i=e.token(),n=i.value,s=i.startLine,o=i.startCol),e.match(Tokens.IDENT)&&(i=e.token(),r=i.value,r.charAt(0)=="_"&&this.options.underscoreHack&&(n="_",r=r.substring(1)),t=new PropertyName(r,n,s||i.startLine,o||i.startCol),this._readWhitespace()),t},_ruleset:function(){var e=this._tokenStream,t,n;try{n=this._selectors_group()}catch(r){if(r instanceof SyntaxError&&!this.options.strict){this.fire({type:"error",error:r,message:r.message,line:r.line,col:r.col}),t=e.advance([Tokens.RBRACE]);if(t!=Tokens.RBRACE)throw r;return!0}throw r}return n&&(this.fire({type:"startrule",selectors:n,line:n[0].line,col:n[0].col}),this._readDeclarations(!0),this.fire({type:"endrule",selectors:n,line:n[0].line,col:n[0].col})),n},_selectors_group:function(){var e=this._tokenStream,t=[],n;n=this._selector();if(n!==null){t.push(n);while(e.match(Tokens.COMMA))this._readWhitespace(),n=this._selector(),n!==null?t.push(n):this._unexpectedToken(e.LT(1))}return t.length?t:null},_selector:function(){var e=this._tokenStream,t=[],n=null,r=null,i=null;n=this._simple_selector_sequence();if(n===null)return null;t.push(n);do{r=this._combinator();if(r!==null)t.push(r),n=this._simple_selector_sequence(),n===null?this._unexpectedToken(e.LT(1)):t.push(n);else{if(!this._readWhitespace())break;i=new Combinator(e.token().value,e.token().startLine,e.token().startCol),r=this._combinator(),n=this._simple_selector_sequence(),n===null?r!==null&&this._unexpectedToken(e.LT(1)):(r!==null?t.push(r):t.push(i),t.push(n))}}while(!0);return new Selector(t,t[0].line,t[0].col)},_simple_selector_sequence:function(){var e=this._tokenStream,t=null,n=[],r="",i=[function(){return e.match(Tokens.HASH)?new SelectorSubPart(e.token().value,"id",e.token().startLine,e.token().startCol):null},this._class,this._attrib,this._pseudo,this._negation],s=0,o=i.length,u=null,a=!1,f,l;f=e.LT(1).startLine,l=e.LT(1).startCol,t=this._type_selector(),t||(t=this._universal()),t!==null&&(r+=t);for(;;){if(e.peek()===Tokens.S)break;while(s1&&e.unget()),null)},_class:function(){var e=this._tokenStream,t;return e.match(Tokens.DOT)?(e.mustMatch(Tokens.IDENT),t=e.token(),new SelectorSubPart("."+t.value,"class",t.startLine,t.startCol-1)):null},_element_name:function(){var e=this._tokenStream,t;return e.match(Tokens.IDENT)?(t=e.token(),new SelectorSubPart(t.value,"elementName",t.startLine,t.startCol)):null},_namespace_prefix:function(){var e=this._tokenStream,t="";if(e.LA(1)===Tokens.PIPE||e.LA(2)===Tokens.PIPE)e.match([Tokens.IDENT,Tokens.STAR])&&(t+=e.token().value),e.mustMatch(Tokens.PIPE),t+="|";return t.length?t:null},_universal:function(){var e=this._tokenStream,t="",n;return n=this._namespace_prefix(),n&&(t+=n),e.match(Tokens.STAR)&&(t+="*"),t.length?t:null},_attrib:function(){var e=this._tokenStream,t=null,n,r;return e.match(Tokens.LBRACKET)?(r=e.token(),t=r.value,t+=this._readWhitespace(),n=this._namespace_prefix(),n&&(t+=n),e.mustMatch(Tokens.IDENT),t+=e.token().value,t+=this._readWhitespace(),e.match([Tokens.PREFIXMATCH,Tokens.SUFFIXMATCH,Tokens.SUBSTRINGMATCH,Tokens.EQUALS,Tokens.INCLUDES,Tokens.DASHMATCH])&&(t+=e.token().value,t+=this._readWhitespace(),e.mustMatch([Tokens.IDENT,Tokens.STRING]),t+=e.token().value,t+=this._readWhitespace()),e.mustMatch(Tokens.RBRACKET),new SelectorSubPart(t+"]","attribute",r.startLine,r.startCol)):null},_pseudo:function(){var e=this._tokenStream,t=null,n=":",r,i;return e.match(Tokens.COLON)&&(e.match(Tokens.COLON)&&(n+=":"),e.match(Tokens.IDENT)?(t=e.token().value,r=e.token().startLine,i=e.token().startCol-n.length):e.peek()==Tokens.FUNCTION&&(r=e.LT(1).startLine,i=e.LT(1).startCol-n.length,t=this._functional_pseudo()),t&&(t=new SelectorSubPart(n+t,"pseudo",r,i))),t},_functional_pseudo:function(){var e=this._tokenStream,t=null;return e.match(Tokens.FUNCTION)&&(t=e.token().value,t+=this._readWhitespace(),t+=this._expression(),e.mustMatch(Tokens.RPAREN),t+=")"),t},_expression:function(){var e=this._tokenStream,t="";while(e.match([Tokens.PLUS,Tokens.MINUS,Tokens.DIMENSION,Tokens.NUMBER,Tokens.STRING,Tokens.IDENT,Tokens.LENGTH,Tokens.FREQ,Tokens.ANGLE,Tokens.TIME,Tokens.RESOLUTION,Tokens.SLASH]))t+=e.token().value,t+=this._readWhitespace();return t.length?t:null},_negation:function(){var e=this._tokenStream,t,n,r="",i,s=null;return e.match(Tokens.NOT)&&(r=e.token().value,t=e.token().startLine,n=e.token().startCol,r+=this._readWhitespace(),i=this._negation_arg(),r+=i,r+=this._readWhitespace(),e.match(Tokens.RPAREN),r+=e.token().value,s=new SelectorSubPart(r,"not",t,n),s.args.push(i)),s},_negation_arg:function(){var e=this._tokenStream,t=[this._type_selector,this._universal,function(){return e.match(Tokens.HASH)?new SelectorSubPart(e.token().value,"id",e.token().startLine,e.token().startCol):null},this._class,this._attrib,this._pseudo],n=null,r=0,i=t.length,s,o,u,a;o=e.LT(1).startLine,u=e.LT(1).startCol;while(r0?new PropertyValue(n,n[0].line,n[0].col):null},_term:function(e){var t=this._tokenStream,n=null,r=null,i=null,s,o,u;return n=this._unary_operator(),n!==null&&(o=t.token().startLine,u=t.token().startCol),t.peek()==Tokens.IE_FUNCTION&&this.options.ieFilters?(r=this._ie_function(),n===null&&(o=t.token().startLine,u=t.token().startCol)):e&&t.match([Tokens.LPAREN,Tokens.LBRACE,Tokens.LBRACKET])?(s=t.token(),i=s.endChar,r=s.value+this._expr(e).text,n===null&&(o=t.token().startLine,u=t.token().startCol),t.mustMatch(Tokens.type(i)),r+=i,this._readWhitespace()):t.match([Tokens.NUMBER,Tokens.PERCENTAGE,Tokens.LENGTH,Tokens.ANGLE,Tokens.TIME,Tokens.FREQ,Tokens.STRING,Tokens.IDENT,Tokens.URI,Tokens.UNICODE_RANGE])?(r=t.token().value,n===null&&(o=t.token().startLine,u=t.token().startCol),this._readWhitespace()):(s=this._hexcolor(),s===null?(n===null&&(o=t.LT(1).startLine,u=t.LT(1).startCol),r===null&&(t.LA(3)==Tokens.EQUALS&&this.options.ieFilters?r=this._ie_function():r=this._function())):(r=s.value,n===null&&(o=s.startLine,u=s.startCol))),r!==null?new PropertyValuePart(n!==null?n+r:r,o,u):null},_function:function(){var e=this._tokenStream,t=null,n=null,r;if(e.match(Tokens.FUNCTION)){t=e.token().value,this._readWhitespace(),n=this._expr(!0),t+=n;if(this.options.ieFilters&&e.peek()==Tokens.EQUALS)do{this._readWhitespace()&&(t+=e.token().value),e.LA(0)==Tokens.COMMA&&(t+=e.token().value),e.match(Tokens.IDENT),t+=e.token().value,e.match(Tokens.EQUALS),t+=e.token().value,r=e.peek();while(r!=Tokens.COMMA&&r!=Tokens.S&&r!=Tokens.RPAREN)e.get(),t+=e.token().value,r=e.peek()}while(e.match([Tokens.COMMA,Tokens.S]));e.match(Tokens.RPAREN),t+=")",this._readWhitespace()}return t},_ie_function:function(){var e=this._tokenStream,t=null,n=null,r;if(e.match([Tokens.IE_FUNCTION,Tokens.FUNCTION])){t=e.token().value;do{this._readWhitespace()&&(t+=e.token().value),e.LA(0)==Tokens.COMMA&&(t+=e.token().value),e.match(Tokens.IDENT),t+=e.token().value,e.match(Tokens.EQUALS),t+=e.token().value,r=e.peek();while(r!=Tokens.COMMA&&r!=Tokens.S&&r!=Tokens.RPAREN)e.get(),t+=e.token().value,r=e.peek()}while(e.match([Tokens.COMMA,Tokens.S]));e.match(Tokens.RPAREN),t+=")",this._readWhitespace()}return t},_hexcolor:function(){var e=this._tokenStream,t=null,n;if(e.match(Tokens.HASH)){t=e.token(),n=t.value;if(!/#[a-f0-9]{3,6}/i.test(n))throw new SyntaxError("Expected a hex color but found '"+n+"' at line "+t.startLine+", col "+t.startCol+".",t.startLine,t.startCol);this._readWhitespace()}return t},_keyframes:function(){var e=this._tokenStream,t,n,r,i="";e.mustMatch(Tokens.KEYFRAMES_SYM),t=e.token(),/^@\-([^\-]+)\-/.test(t.value)&&(i=RegExp.$1),this._readWhitespace(),r=this._keyframe_name(),this._readWhitespace(),e.mustMatch(Tokens.LBRACE),this.fire({type:"startkeyframes",name:r,prefix:i,line:t.startLine,col:t.startCol}),this._readWhitespace(),n=e.peek();while(n==Tokens.IDENT||n==Tokens.PERCENTAGE)this._keyframe_rule(),this._readWhitespace(),n=e.peek();this.fire({type:"endkeyframes",name:r,prefix:i,line:t.startLine,col:t.startCol}),this._readWhitespace(),e.mustMatch(Tokens.RBRACE)},_keyframe_name:function(){var e=this._tokenStream,t;return e.mustMatch([Tokens.IDENT,Tokens.STRING]),SyntaxUnit.fromToken(e.token())},_keyframe_rule:function(){var e=this._tokenStream,t,n=this._key_list();this.fire({type:"startkeyframerule",keys:n,line:n[0].line,col:n[0].col}),this._readDeclarations(!0),this.fire({type:"endkeyframerule",keys:n,line:n[0].line,col:n[0].col})},_key_list:function(){var e=this._tokenStream,t,n,r=[];r.push(this._key()),this._readWhitespace();while(e.match(Tokens.COMMA))this._readWhitespace(),r.push(this._key()),this._readWhitespace();return r},_key:function(){var e=this._tokenStream,t;if(e.match(Tokens.PERCENTAGE))return SyntaxUnit.fromToken(e.token());if(e.match(Tokens.IDENT)){t=e.token();if(/from|to/i.test(t.value))return SyntaxUnit.fromToken(t);e.unget()}this._unexpectedToken(e.LT(1))},_skipCruft:function(){while(this._tokenStream.match([Tokens.S,Tokens.CDO,Tokens.CDC]));},_readDeclarations:function(e,t){var n=this._tokenStream,r;this._readWhitespace(),e&&n.mustMatch(Tokens.LBRACE),this._readWhitespace();try{for(;;){if(!(n.match(Tokens.SEMICOLON)||t&&this._margin())){if(!this._declaration())break;if(!n.match(Tokens.SEMICOLON))break}this._readWhitespace()}n.mustMatch(Tokens.RBRACE),this._readWhitespace()}catch(i){if(!(i instanceof SyntaxError&&!this.options.strict))throw i;this.fire({type:"error",error:i,message:i.message,line:i.line,col:i.col}),r=n.advance([Tokens.SEMICOLON,Tokens.RBRACE]);if(r==Tokens.SEMICOLON)this._readDeclarations(!1,t);else if(r!=Tokens.RBRACE)throw i}},_readWhitespace:function(){var e=this._tokenStream,t="";while(e.match(Tokens.S))t+=e.token().value;return t},_unexpectedToken:function(e){throw new SyntaxError("Unexpected token '"+e.value+"' at line "+e.startLine+", col "+e.startCol+".",e.startLine,e.startCol)},_verifyEnd:function(){this._tokenStream.LA(1)!=Tokens.EOF&&this._unexpectedToken(this._tokenStream.LT(1))},_validateProperty:function(e,t){Validation.validate(e,t)},parse:function(e){this._tokenStream=new TokenStream(e,Tokens),this._stylesheet()},parseStyleSheet:function(e){return this.parse(e)},parseMediaQuery:function(e){this._tokenStream=new TokenStream(e,Tokens);var t=this._media_query();return this._verifyEnd(),t},parsePropertyValue:function(e){this._tokenStream=new TokenStream(e,Tokens),this._readWhitespace();var t=this._expr();return this._readWhitespace(),this._verifyEnd(),t},parseRule:function(e){this._tokenStream=new TokenStream(e,Tokens),this._readWhitespace();var t=this._ruleset();return this._readWhitespace(),this._verifyEnd(),t},parseSelector:function(e){this._tokenStream=new TokenStream(e,Tokens),this._readWhitespace();var t=this._selector();return this._readWhitespace(),this._verifyEnd(),t},parseStyleAttribute:function(e){e+="}",this._tokenStream=new TokenStream(e,Tokens),this._readDeclarations()}};for(t in n)n.hasOwnProperty(t)&&(e[t]=n[t]);return e}();var Properties={"align-items":"flex-start | flex-end | center | baseline | stretch","align-content":"flex-start | flex-end | center | space-between | space-around | stretch","align-self":"auto | flex-start | flex-end | center | baseline | stretch","-webkit-align-items":"flex-start | flex-end | center | baseline | stretch","-webkit-align-content":"flex-start | flex-end | center | space-between | space-around | stretch","-webkit-align-self":"auto | flex-start | flex-end | center | baseline | stretch","alignment-adjust":"auto | baseline | before-edge | text-before-edge | middle | central | after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | | ","alignment-baseline":"baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",animation:1,"animation-delay":{multi:"