From 569a1e700d363937857f6a645c106c6e06763e14 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 19 Aug 2024 14:17:50 +0200 Subject: [PATCH] Add MJML support This commit adds MJML support to Keila. Huge thanks to @jdrouet for his Rust implementation of MJML and @paulgoetze for the Elixir wrapper --- assets/js/campaign-editors/mjml/helpers.js | 18 + assets/js/campaign-editors/mjml/index.js | 51 ++ assets/js/campaign-editors/mjml/tags.js | 537 ++++++++++++++++ assets/js/campaign-editors/mjml/theme.js | 37 ++ assets/js/hooks/campaign-edit-live.js | 11 +- assets/package-lock.json | 597 +++++++++++++++++- assets/package.json | 3 + lib/keila/mailings/builder.ex | 6 + lib/keila/mailings/builder/mjml.ex | 40 ++ lib/keila/mailings/schemas/campaign.ex | 2 + .../mailings/schemas/campaign_settings.ex | 2 +- .../api/schemas/mailings_campaign.ex | 17 +- .../controllers/campaign_controller.ex | 2 + lib/keila_web/live/campaign_edit_live.ex | 2 + .../templates/campaign/_mjml_editor.html.heex | 96 +++ .../campaign/_settings_dialog.html.heex | 5 +- .../templates/campaign/edit_live.html.heex | 4 + .../templates/campaign/new.html.heex | 17 +- mix.exs | 3 +- mix.lock | 2 + .../email_templates/default-mjml-content.mjml | 121 ++++ .../20240524220616_add_mjml_body.exs | 9 + priv/vendor/keila-icons/mjml.svg | 9 + 23 files changed, 1583 insertions(+), 8 deletions(-) create mode 100644 assets/js/campaign-editors/mjml/helpers.js create mode 100644 assets/js/campaign-editors/mjml/index.js create mode 100644 assets/js/campaign-editors/mjml/tags.js create mode 100644 assets/js/campaign-editors/mjml/theme.js create mode 100644 lib/keila/mailings/builder/mjml.ex create mode 100644 lib/keila_web/templates/campaign/_mjml_editor.html.heex create mode 100644 priv/email_templates/default-mjml-content.mjml create mode 100644 priv/repo/migrations/20240524220616_add_mjml_body.exs create mode 100644 priv/vendor/keila-icons/mjml.svg diff --git a/assets/js/campaign-editors/mjml/helpers.js b/assets/js/campaign-editors/mjml/helpers.js new file mode 100644 index 00000000..1b0fd01f --- /dev/null +++ b/assets/js/campaign-editors/mjml/helpers.js @@ -0,0 +1,18 @@ +export const indentAndAutocompleteWithTab = { + key: "Tab", + preventDefault: true, + shift: indentLess, + run: (e) => { + if (!completionStatus(e.state)) return indentMore(e) + return acceptCompletion(e) + } +} + +export const saveUpdates = (source) => { + return EditorView.updateListener.of((e) => { + if (e.docChanged) { + source.value = e.state.doc.toString() + source.dispatchEvent(new Event("change", { bubbles: true })) + } + }) +} diff --git a/assets/js/campaign-editors/mjml/index.js b/assets/js/campaign-editors/mjml/index.js new file mode 100644 index 00000000..912cef55 --- /dev/null +++ b/assets/js/campaign-editors/mjml/index.js @@ -0,0 +1,51 @@ +import { acceptCompletion, completionStatus } from "@codemirror/autocomplete" +import { defaultKeymap, indentLess, indentMore } from "@codemirror/commands" +import { html } from "@codemirror/lang-html" +import { EditorState } from "@codemirror/state" +import { EditorView, keymap } from "@codemirror/view" +import { basicSetup } from "codemirror" + +import tags from "./autocomplete.js" +import { indentAndAutocompleteWithTab, saveUpdates } from "./helpers.js" +import theme from "./theme.js" + +export default class MjmlEditor { + constructor(place, source) { + this.source = source + this.place = place + + let state = EditorState.create({ + doc: source.value, + extensions: [ + basicSetup, + html({ extraTags: tags, selfClosingTags: true }), + keymap.of([...defaultKeymap, indentAndAutocompleteWithTab]), + theme, + saveUpdates(source) + ] + }) + + this.view = new EditorView({ + state: state, + parent: place + }) + + place.parentNode.parentNode.addEventListener("x-show-image-dialog", () => { + document + .querySelector("[data-dialog-for=image]") + .dispatchEvent(new CustomEvent("x-show", { detail: {} })) + window.addEventListener( + "update-image", + (e) => { + const { src } = e.detail + if (!src) { + this.view.focus() + return + } + this.view.dispatch(this.view.state.replaceSelection(src)) + }, + { once: true } + ) + }) + } +} diff --git a/assets/js/campaign-editors/mjml/tags.js b/assets/js/campaign-editors/mjml/tags.js new file mode 100644 index 00000000..0f94912d --- /dev/null +++ b/assets/js/campaign-editors/mjml/tags.js @@ -0,0 +1,537 @@ +export default = { + mjml: { + children: ["mj-head", "mj-body"], + globalAttrs: false + }, + "mj-head": { + children: [ + "mj-attributes", + "mj-preview", + "mj-title", + "mj-font", + "mj-breakpoint", + "mj-raw" + ], + globalAttrs: false + }, + "mj-body": { + children: ["mj-section", "mj-wrapper", "mj-raw"], + globalAttrs: false + }, + "mj-accordion": { + attrs: { + "css-class": null, + "container-background-color": null, + border: null, + "font-family": null, + "icon-align": null, + "icon-wrapped-url": null, + "icon-wrapped-alt": null, + "icon-unwrapped-url": null, + "icon-unwrapped-alt": null, + "icon-position": null, + "icon-height": null, + "icon-width": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null + }, + globalAttrs: false + }, + "mj-accordion-element": { + attrs: { + "background-color": null, + "font-family": null, + "icon-align": null, + "icon-wrapped-url": null, + "icon-wrapped-alt": null, + "icon-unwrapped-url": null, + "icon-unwrapped-alt": null, + "icon-position": null, + "icon-height": null, + "icon-width": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-accordion-title": { + attrs: { + "background-color": null, + color: null, + "font-family": null, + "font-size": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-accordion-text": { + attrs: { + "background-color": null, + color: null, + "font-family": null, + "font-size": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-breakpoint": { + attrs: { + width: null + }, + globalAttrs: false + }, + "mj-attributes": { + children: ["mj-section", "mj-column", "mj-text", "mj-button", "mj-image"], + globalAttrs: false + }, + "mj-button": { + attrs: { + "background-color": null, + "container-background-color": null, + border: null, + "border-bottom": null, + "border-left": null, + "border-right": null, + "border-top": null, + "border-radius": null, + "font-style": null, + "font-size": null, + "font-weight": null, + "font-family": null, + color: null, + "text-decoration": null, + "text-transform": null, + align: null, + "vertical-align": null, + "line-height": null, + href: null, + rel: null, + "inner-padding": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + width: null, + height: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-carousel": { + attrs: { + align: null, + "border-radius": null, + "background-color": null, + thumbnails: null, + "tb-border": null, + "tb-border-radius": null, + "tb-hover-border-color": null, + "tb-selected-border-color": null, + "tb-width": null, + "left-icon": null, + "right-icon": null, + "icon-width": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-carousel-image": { + attrs: { + src: null, + "thumbnails-src": null, + href: null, + rel: null, + alt: null, + title: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-class": { + attrs: { + name: null + }, + globalAttrs: false + }, + "mj-column": { + attrs: { + "background-color": null, + border: null, + "border-bottom": null, + "border-left": null, + "border-right": null, + "border-top": null, + "border-radius": null, + width: null, + "vertical-align": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-divider": { + attrs: { + "border-color": null, + "border-style": null, + "border-width": null, + width: null, + "container-background-color": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-group": { + attrs: { + width: null, + "vertical-align": null, + "background-color": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-font": { + attrs: { + href: null, + name: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-hero": { + attrs: { + width: null, + mode: null, + height: null, + "background-width": null, + "background-height": null, + "background-url": null, + "background-color": null, + "background-position": null, + padding: null, + "padding-top": null, + "padding-right": null, + "padding-left": null, + "padding-bottom": null, + "vertical-align": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-image": { + attrs: { + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "container-background-color": null, + border: null, + "border-radius": null, + width: null, + height: null, + src: null, + href: null, + rel: null, + alt: null, + align: null, + title: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-invoice": { + attrs: { + align: null, + color: null, + "font-family": null, + "font-size": null, + "line-height": null, + border: null, + "container-background-color": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + intl: null, + format: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-invoice-item": { + attrs: { + color: null, + "font-family": null, + "font-size": null, + "line-height": null, + border: null, + "text-align": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + name: null, + price: null, + quantity: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-list": { + attrs: { + color: null, + "font-family": null, + "font-size": null, + "line-height": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-location": { + attrs: { + color: null, + "font-family": null, + "font-size": null, + "font-weight": null, + href: null, + rel: null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "img-src": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-navbar": { + attrs: { + hamburger: null, + align: null, + "ico-open": null, + "ico-close": null, + "ico-padding": null, + "ico-padding-top": null, + "ico-padding-right": null, + "ico-padding-bottom": null, + "ico-padding-left": null, + "ico-align": null, + "ico-color": null, + "ico-font-size": null, + "ico-font-family": null, + "ico-text-transform": null, + "ico-text-decoration": null, + "ico-line-height": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-navbar-link": { + attrs: { + color: null, + "font-family": null, + "font-size": null, + "font-style": null, + "font-weight": null, + "line-height": null, + "text-decoration": null, + "text-transform": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + rel: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-preview": { + globalAttrs: false + }, + "mj-raw": { + globalAttrs: false + }, + "mj-section": { + attrs: { + "full-width": null, + border: null, + "border-bottom": null, + "border-left": null, + "border-right": null, + "border-top": null, + "border-radius": null, + "background-color": null, + "background-url": null, + "background-repeat": null, + "background-size": null, + "vertical-align": null, + "text-align": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + direction: null, + "css-class": null + }, + children: ["mj-column"], + globalAttrs: false + }, + "mj-social": { + attrs: { + align: null, + "border-radius": null, + "container-background-color": null, + color: null, + "font-family": null, + "font-size": null, + "font-style": null, + "font-weight": null, + "icon-size": null, + "inner-padding": null, + "line-height": null, + mode: null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null, + "table-layout": null, + "vertical-align": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-social-element": { + attrs: { + align: null, + "background-color": null, + color: null, + "border-radius": null, + "font-family": null, + "font-size": null, + "font-style": null, + "font-weight": null, + href: null, + "icon-color": null, + "icon-size": null, + "line-height": null, + name: null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null, + src: null, + target: null, + "text-decoration": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-spacer": { + attrs: { + height: null, + "css-class": null + }, + globalAttrs: false + }, + "mj-table": { + attrs: { + color: null, + cellpadding: null, + cellspacing: null, + "font-family": null, + "font-size": null, + "line-height": null, + "container-background-color": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + width: null, + "table-layout": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-text": { + attrs: { + align: null, + "background-color": null, + color: null, + "container-background-color": null, + "font-family": null, + "font-size": null, + "font-style": null, + "font-weight": null, + height: null, + "letter-spacing": null, + "line-height": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "padding-top": null, + padding: null, + "text-decoration": null, + "text-transform": null, + "vertical-align": null, + "css-class": null + }, + globalAttrs: false + }, + "mj-title": { globalAttrs: false }, + "mj-wrapper": { + attrs: { + "full-width": null, + border: null, + "border-bottom": null, + "border-left": null, + "border-right": null, + "border-top": null, + "border-radius": null, + "background-color": null, + "background-url": null, + "background-repeat": null, + "background-size": null, + "vertical-align": null, + "text-align": null, + padding: null, + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "css-class": null + }, + globalAttrs: false + }, + children: ["mj-section"], + globalAttrs: false +} diff --git a/assets/js/campaign-editors/mjml/theme.js b/assets/js/campaign-editors/mjml/theme.js new file mode 100644 index 00000000..9e6c578c --- /dev/null +++ b/assets/js/campaign-editors/mjml/theme.js @@ -0,0 +1,37 @@ +import { EditorView } from "prosemirror-view" + +export default EditorView.theme( + { + "&": { + color: "#f4f4f5", + backgroundColor: "#030712" + }, + ".cm-content": { + caretColor: "#f9fafb" + }, + "&.cm-focused .cm-cursor": { + borderLeftColor: "#f9fafb" + }, + "&.cm-focused .cm-selectionBackground, ::selection": { + backgroundColor: "#083344 !important" + }, + ".cm-selectionBackground, ::selection": { + backgroundColor: "#083344" + }, + ".cm-selectionMatch": { + backgroundColor: "#155e75" + }, + ".cm-gutters": { + backgroundColor: "#111827", + color: "#6b7280", + border: "none" + }, + ".ͼe": { + color: "#93c5fd" + }, + ".ͼi": { + color: "#4ade80" + } + }, + { dark: true } +) diff --git a/assets/js/hooks/campaign-edit-live.js b/assets/js/hooks/campaign-edit-live.js index a5a947bf..39afe4b9 100644 --- a/assets/js/hooks/campaign-edit-live.js +++ b/assets/js/hooks/campaign-edit-live.js @@ -1,6 +1,7 @@ import BlockEditor from "../campaign-editors/block" import { MarkdownEditor } from "../campaign-editors/markdown" import MarkdownSimpleEditor from "../campaign-editors/markdown-simple" +import MjmlEditor from "../campaign-editors/mjml" const putHtmlPreview = (el) => { const content = el.innerText @@ -38,6 +39,13 @@ const BlockEditorHook = { } } +const MjmlEditorHook = { + mounted() { + let place = this.el.querySelector(".editor") + new MjmlEditor(place, document.querySelector("#campaign_mjml_body")) + } +} + const HtmlPreviewHook = { mounted() { putHtmlPreview(this.el) @@ -51,5 +59,6 @@ export { BlockEditorHook as BlockEditor, HtmlPreviewHook as HtmlPreview, MarkdownEditorHook as MarkdownEditor, - MarkdownSimpleEditorHook as MarkdownSimpleEditor + MarkdownSimpleEditorHook as MarkdownSimpleEditor, + MjmlEditorHook as MjmlEditor } diff --git a/assets/package-lock.json b/assets/package-lock.json index e4a5fe37..f5f9d9c7 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -4,9 +4,10 @@ "requires": true, "packages": { "": { - "name": "assets", "license": "agpl-3.0", "dependencies": { + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/lang-html": "^6.4.9", "@editorjs/editorjs": "git+https://github.com/pentacent/editor.js.git#ff6e6ac3272f29e02229cd97ebffa5acf6c0e0fe", "@editorjs/header": "^2.7.0", "@editorjs/image": "^2.8.1", @@ -16,6 +17,7 @@ "@editorjs/quote": "^2.5.0", "alpinejs": "^2.8.0", "chart.js": "^4.1.1", + "codemirror": "^6.0.1", "nprogress": "^0.2.0", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", @@ -99,6 +101,230 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.0.tgz", + "integrity": "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'", + "dependencies": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", + "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz", + "integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz", + "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/view": { + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.32.0.tgz", + "integrity": "sha512-AgVNvED2QTsZp5e3syoHLsrWtwJFYWdx1Vr/m3f4h1ATQz0ax60CfXF3Htdmk69k2MlYZw8gXesnQdHtzyVmAw==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@codexteam/icons": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.1.0.tgz", @@ -280,6 +506,57 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.1.tgz", "integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw==" }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/css": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz", + "integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz", + "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -874,6 +1151,20 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/codex-notifier": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/codex-notifier/-/codex-notifier-1.1.2.tgz", @@ -915,6 +1206,11 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2393,6 +2689,11 @@ "node": ">=8" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2898,6 +3199,225 @@ "regenerator-runtime": "^0.13.11" } }, + "@codemirror/autocomplete": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.0.tgz", + "integrity": "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "requires": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + }, + "dependencies": { + "@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "requires": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "requires": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "requires": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "requires": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "requires": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + }, + "@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "requires": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + }, + "@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "requires": { + "@lezer/common": "^0.16.0" + } + }, + "@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "requires": { + "@lezer/common": "^0.16.0" + } + } + } + }, + "@codemirror/commands": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", + "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "@codemirror/lang-css": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz", + "integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz", + "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "@codemirror/view": { + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.32.0.tgz", + "integrity": "sha512-AgVNvED2QTsZp5e3syoHLsrWtwJFYWdx1Vr/m3f4h1ATQz0ax60CfXF3Htdmk69k2MlYZw8gXesnQdHtzyVmAw==", + "requires": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "@codexteam/icons": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.1.0.tgz", @@ -3083,6 +3603,57 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.1.tgz", "integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw==" }, + "@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "@lezer/css": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz", + "integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz", + "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3555,6 +4126,20 @@ "wrap-ansi": "^7.0.0" } }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "codex-notifier": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/codex-notifier/-/codex-notifier-1.1.2.tgz", @@ -3593,6 +4178,11 @@ "yaml": "^1.10.0" } }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4646,6 +5236,11 @@ "ansi-regex": "^5.0.1" } }, + "style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/assets/package.json b/assets/package.json index d906c02f..6811952e 100644 --- a/assets/package.json +++ b/assets/package.json @@ -6,6 +6,8 @@ "deploy": "NODE_ENV=production tailwindcss --input=css/app.scss --output=../priv/static/css/app.css --postcss --minify" }, "dependencies": { + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/lang-html": "^6.4.9", "@editorjs/editorjs": "git+https://github.com/pentacent/editor.js.git#ff6e6ac3272f29e02229cd97ebffa5acf6c0e0fe", "@editorjs/header": "^2.7.0", "@editorjs/image": "^2.8.1", @@ -15,6 +17,7 @@ "@editorjs/quote": "^2.5.0", "alpinejs": "^2.8.0", "chart.js": "^4.1.1", + "codemirror": "^6.0.1", "nprogress": "^0.2.0", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", diff --git a/lib/keila/mailings/builder.ex b/lib/keila/mailings/builder.ex index 1bf02559..b1fcc912 100644 --- a/lib/keila/mailings/builder.ex +++ b/lib/keila/mailings/builder.ex @@ -201,6 +201,12 @@ defmodule Keila.Mailings.Builder do end end + defp put_body(email, campaign = %{settings: %{type: :mjml}}, assigns) do + mjml_content = campaign.mjml_body || "" + + __MODULE__.MJML.put_body(email, mjml_content, assigns) + end + defp fetch_styles(campaign) defp fetch_styles(%Campaign{template: %Template{styles: styles}}) when is_list(styles) do diff --git a/lib/keila/mailings/builder/mjml.ex b/lib/keila/mailings/builder/mjml.ex new file mode 100644 index 00000000..f88ad552 --- /dev/null +++ b/lib/keila/mailings/builder/mjml.ex @@ -0,0 +1,40 @@ +defmodule Keila.Mailings.Builder.MJML do + @moduledoc """ + Builder for MJML emails. + """ + require KeilaWeb.Gettext + + import Swoosh.Email + import KeilaWeb.Gettext + + @spec put_body(Swoosh.Email.t(), String.t(), map()) :: Swoosh.Email.t() + def put_body(email, mjml_content, assigns \\ %{}) do + with {:ok, rendered_mjml} <- render_mjml(mjml_content), + {:ok, html_body} <- render_liquid(rendered_mjml, assigns) do + html_body(email, html_body) + else + {:error, reason} -> + email |> text_body(reason) |> header("X-Keila-Invalid", reason) + end + end + + defp render_mjml(input) do + case Mjml.to_html(input) do + {:ok, output} -> + {:ok, output} + + {:error, reason} -> + {:error, gettext("Error compiling MJML: %{reason}", reason: reason)} + end + end + + defp render_liquid(input, assigns) do + case Keila.Mailings.Builder.LiquidRenderer.render_liquid(input, assigns) do + {:ok, output} -> + {:ok, output} + + {:error, reason} -> + {:error, gettext("Error compiling Liquid: %{reason}", reason: reason)} + end + end +end diff --git a/lib/keila/mailings/schemas/campaign.ex b/lib/keila/mailings/schemas/campaign.ex index e498a348..b3d3d982 100644 --- a/lib/keila/mailings/schemas/campaign.ex +++ b/lib/keila/mailings/schemas/campaign.ex @@ -10,6 +10,7 @@ defmodule Keila.Mailings.Campaign do :text_body, :html_body, :json_body, + :mjml_body, :preview_text, :sender_id, :template_id, @@ -23,6 +24,7 @@ defmodule Keila.Mailings.Campaign do field(:text_body, :string) field(:html_body, :string) field(:json_body, :map) + field(:mjml_body, :string) field(:preview_text, :string) field(:data, Keila.Repo.JsonField) field(:sent_at, :utc_datetime) diff --git a/lib/keila/mailings/schemas/campaign_settings.ex b/lib/keila/mailings/schemas/campaign_settings.ex index 96dd878a..8e0c8e5c 100644 --- a/lib/keila/mailings/schemas/campaign_settings.ex +++ b/lib/keila/mailings/schemas/campaign_settings.ex @@ -3,7 +3,7 @@ defmodule Keila.Mailings.Campaign.Settings do import Ecto.Changeset embedded_schema do - field(:type, Ecto.Enum, values: [:text, :markdown, :block]) + field(:type, Ecto.Enum, values: [:text, :markdown, :block, :mjml]) field(:enable_wysiwyg, :boolean, default: true) field(:do_not_track, :boolean, default: false) end diff --git a/lib/keila_web/api/schemas/mailings_campaign.ex b/lib/keila_web/api/schemas/mailings_campaign.ex index 769b3b74..6becd25b 100644 --- a/lib/keila_web/api/schemas/mailings_campaign.ex +++ b/lib/keila_web/api/schemas/mailings_campaign.ex @@ -35,6 +35,20 @@ defmodule KeilaWeb.Api.Schemas.MailingsCampaign do } """ }, + mjml_body: %{ + type: :string, + example: """ + + + + + Hello I’m an MJML campaign! + + + + + """ + }, data: %{ type: :map, example: %{ @@ -50,7 +64,7 @@ defmodule KeilaWeb.Api.Schemas.MailingsCampaign do type: %{ type: :string, required: true, - enum: ["markdown", "text", "block"], + enum: ["markdown", "text", "block", "mjml"], example: "markdown" } } @@ -114,6 +128,7 @@ defmodule KeilaWeb.Api.Schemas.MailingsCampaign.Params do :subject, :text_body, :json_body, + :mjml_body, :settings, :template_id, :sender_id, diff --git a/lib/keila_web/controllers/campaign_controller.ex b/lib/keila_web/controllers/campaign_controller.ex index 0c488c52..0e26ba7a 100644 --- a/lib/keila_web/controllers/campaign_controller.ex +++ b/lib/keila_web/controllers/campaign_controller.ex @@ -8,6 +8,7 @@ defmodule KeilaWeb.CampaignController do @default_text_body File.read!("priv/email_templates/default-text-content.txt") @default_markdown_body File.read!("priv/email_templates/default-markdown-content.md") + @default_mjml_body File.read!("priv/email_templates/default-mjml-content.mjml") @spec index(Plug.Conn.t(), map()) :: Plug.Conn.t() def index(conn, _params) do @@ -70,6 +71,7 @@ defmodule KeilaWeb.CampaignController do # TODO Maybe this would be better implemented as a Context module function case get_in(params, ["settings", "type"]) do "markdown" -> Map.put(params, "text_body", @default_markdown_body) + "mjml" -> Map.put(params, "mjml_body", @default_mjml_body) _ -> Map.put(params, "text_body", @default_text_body) end end diff --git a/lib/keila_web/live/campaign_edit_live.ex b/lib/keila_web/live/campaign_edit_live.ex index 3ae0c8b1..7ca10439 100644 --- a/lib/keila_web/live/campaign_edit_live.ex +++ b/lib/keila_web/live/campaign_edit_live.ex @@ -215,6 +215,8 @@ defmodule KeilaWeb.CampaignEditLive do defp transform_style_selector(selector, :text), do: selector + defp transform_style_selector(selector, :mjml), do: selector + @markdown_editor_selector "#wysiwyg .editor" @markdown_editor_content_selector "#wysiwyg .editor .ProseMirror" defp transform_style_selector(selector, :markdown) do diff --git a/lib/keila_web/templates/campaign/_mjml_editor.html.heex b/lib/keila_web/templates/campaign/_mjml_editor.html.heex new file mode 100644 index 00000000..cd222375 --- /dev/null +++ b/lib/keila_web/templates/campaign/_mjml_editor.html.heex @@ -0,0 +1,96 @@ +
+
+ + + +
+ + +
+
+ +<% f = form_for(@changeset, "#") %> +
+ <%= textarea(f, :mjml_body, + rows: 20, + class: "hidden", + phx_debounce: "1000" + ) %> +
+
+
+
+
+
+ + +
+ +
+
+ +
+ +
diff --git a/lib/keila_web/templates/campaign/_settings_dialog.html.heex b/lib/keila_web/templates/campaign/_settings_dialog.html.heex index 9d2212d9..75b7c3e1 100644 --- a/lib/keila_web/templates/campaign/_settings_dialog.html.heex +++ b/lib/keila_web/templates/campaign/_settings_dialog.html.heex @@ -74,7 +74,8 @@ [ {gettext("Markdown"), "markdown"}, {gettext("Text only"), "text"}, - {gettext("Block Editor"), "block"} + {gettext("Block Editor"), "block"}, + {gettext("MJML"), "mjml"} ], x_model: "type" ) %> @@ -105,7 +106,7 @@ -