"MJML to HTML", "author" => "EPRC", "version" => "1.2.4", "summary" => "Allows you to write your Processwire template using MJML and get a converted HTML output using MJML API.", "href" => "https://github.com/eprcstudio/PageMjmlToHtml", "icon" => "code", "autoload" => true, "singular" => true, ]; } public function getDefaults() { return [ "apiId" => "", "apiKey" => "", "areCredentialsValid" => false, "noAppendFile" => 0, "noPrependFile" => 0, "allowedTemplates" => [], "superuserNoCache" => 0, "rolesNoCache" => [], "cachePerGet" => "", "disableMinify" => 0, "convertRelativeLinks" => 0, "relativeLinksHost" => "", "relativeLinksParams" => "", "relativeLinksIgnore" => "", ]; } public function init() { $this->addHookBefore("Modules::saveModuleConfigData", $this, "moduleConfigSaved"); if($this->apiId && $this->apiKey && $this->areCredentialsValid) { $this->addHookAfter("Pages::saved", $this, "clearPageCache"); $this->addHookBefore("PageRender::renderPage", $this, "readFromCache"); $this->addHookAfter("PageRender::renderPage", $this, "convertToHtml"); $this->addHookAfter("ProcessPageEdit::getViewActions", $this, "addViewRaw"); $this->addHookAfter("ProcessPageView::execute", $this, "addCopyRaw"); } } /** * Clear the cache after a Page::save() * * @param HookEvent $event * */ protected function clearPageCache(HookEvent $event) { $page = $event->arguments(0); $template = $page->template; if($template->name === "admin") return; if(!in_array($template->id, $this->allowedTemplates)) return; $this->clearCache($page); } /** * Check if a cache already exists in the user’s language and return it * * This will override the page’s `render` function if a cache is present * * If a `raw` GET parameter is present, display the content as text/plain * * If the template has a more recent modified timestamp than the cache’s * or if the role is set to bypass it, we regenerate the HTML from MJML * * If there is no cache, check first if we should remove prepended * / appended files * * @param HookEvent $event * */ protected function readFromCache(HookEvent $event) { $parentEvent = $event->arguments(0); $page = $parentEvent->object; $template = $page->template; if($template->name === "admin") return; if(!in_array($template->id, $this->allowedTemplates)) return; $bypassCache = false; if($this->user()->isSuperuser() && $this->superuserNoCache) { $bypassCache = true; } elseif(count($this->rolesNoCache)) { foreach($this->rolesNoCache as $role) { if($this->user()->hasRole($role)) { $bypassCache = true; break; } } } $cache = $this->cache()->getFor($this, $this->getCacheName($page)); if($cache && $cache["markup"] && $cache["timestamp"] > $template->modified && !$bypassCache) { if(!is_null($this->input("get", "raw"))) { header("Content-Type: text/plain"); } $parentEvent->return = $cache["markup"]; $event->cancelHooks = true; } else { $options = $parentEvent->arguments(0) ?: []; if($this->noAppendFile) { $options = array_merge([ "appendFile" => null, "appendFiles" => [], ], $options); } if($this->noPrependFile) { $options = array_merge([ "prependFile" => null, "prependFiles" => [], ], $options); } $parentEvent->arguments(0, $options); } } /** * Once the page has been rendered, convert the MJML to HTML and save in * the cache. Show as text/plain if the `raw` GET parameter is present * * A debug view will be rendered only for the superuser if there are errors. * However if the user can edit the page and there are errors the page will * display a message inviting to contact the superuser for more details * * @param HookEvent $event * */ protected function convertToHtml($event) { $parentEvent = $event->arguments(0); $page = $parentEvent->object; $template = $page->template; $rootPageUrl = $this->sanitizer("httpUrl", $this->relativeLinksHost) ?: $this->pages(1)->httpUrl; $rootPageUrl = rtrim($rootPageUrl, '/') . '/'; // force trailing slash if($template->name === "admin") return; if(!in_array($template->id, $this->allowedTemplates)) return; $mjml = $parentEvent->return; if($this->convertRelativeLinks) { $params = $this->getRelativeLinksParams($page); $ignore = explode("\n", $this->relativeLinksIgnore); $mjml = preg_replace_callback( '/href=(?:\"|\')(.+)(?:\"|\')/U', function($matches) use ($rootPageUrl, $params, $ignore) { if( strpos($matches[1], "http") === 0 || strpos($matches[1], "mailto") === 0 || strpos($matches[1], "tel") === 0 ) { return $matches[0]; } foreach($ignore as $i) { $i = trim($i); if(!$i) continue; if(strpos($i, "regex:") === 0) { $regex = "{" . trim(substr($i, 6)) . "}"; if(preg_match($regex, $matches[1])) { return $matches[0]; } } elseif(strpos($matches[1], $i) === 0) { return $matches[0]; } } if(strpos($matches[1], "/") === 0) { $matches[1] = substr($matches[1], 1); } $href = "href=\""; $href .= $rootPageUrl; $href .= $matches[1]; if($params) { if(strpos($matches[1], "?") === false) { $href .= "?"; } else { $href .= "&"; } $href .= $params; } $href .= "\""; return $href; }, $mjml ); } if(!$this->disableMinify) { $mjml = preg_replace("/(\w)\t(\w)/", '$1 $2', $mjml); $mjml = preg_replace("/[\r\t\f\v]+/", "", $mjml); $mjml = preg_replace("/ {2,}/", " ", $mjml); $mjml = preg_replace("/\n{2,}/", "\n", $mjml); } $mjml = $this->callToApi($mjml); $debugView = $this->renderDebugView($mjml); if($debugView === false) { if(!$this->disableMinify) { $mjml->html = preg_replace("/[\r\t\f\v]+/", "", $mjml->html); $mjml->html = preg_replace("/<(\n| )*/", "<", $mjml->html); $mjml->html = preg_replace("/(\n| )*>/", ">", $mjml->html); $mjml->html = preg_replace("/(\n| )*}(\n| )*/", "}", $mjml->html); $mjml->html = preg_replace("/(\n| )*{(\n| )*/", "{", $mjml->html); $mjml->html = preg_replace("/\n+/", " ", $mjml->html); $mjml->html = preg_replace("/ {2,}/", " ", $mjml->html); $mjml->html = preg_replace("/ style>/", ">", $mjml->html); $mjml->html = preg_replace("/> ", "><", $mjml->html); } $mjml->html = trim($mjml->html); $this->cache()->saveFor($this, $this->getCacheName($page), [ "markup" => $mjml->html, "timestamp" => time(), ], WireCache::expireNever); if(!is_null($this->input("get", "raw"))) { header("Content-Type: text/plain"); } $out = $mjml->html; } else { $this->clearCache($page); if($page->get("mailType")) { $this->error(sprintf($this->_('There was an issue generating this email. Check the %1$spage%2$s to get more details'), "httpUrl\" target=\"_blank\">", ""), Notice::allowMarkup); // ProMailer: return an empty string so the sending process gets cancelled $out = ""; } elseif($this->user()->isSuperuser()) { $out = $debugView; } elseif($page->editable()) { $out = "
"; $out .= $this->_("There was an issue generating this email. Please contact your administrator to get details"); $out .= "
"; } else { $out = "";
// Split into an array with one line of code per row
$code = explode("\n", $mjml->mjml);
// Array containing our formatted lines
$lines = [];
// We track the depth for indentation
$depth = 0;
foreach($code as $key => $c) {
$line = "";
$num = $key + 1;
// Check if there is any error for this line
$errors = [];
if(!empty($mjml->errors)) {
$debug = true;
$errors = array_filter($mjml->errors, function($item) use($num) {
return $item->line === $num;
});
}
// Wrap the line with an error message
if(!empty($errors)) {
$line .= "message . "\n";
return $carry;
}));
$line .= "\">";
}
$line .= "$num";
// Indentation
preg_match_all("/<(?!\/|\!)(?!area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)[^<]+?(?/", $c, $openingTags);
preg_match_all("/<\/.+?>/", $c, $closingTags);
if(count($openingTags[0]) < count($closingTags[0])) {
$depth--;
}
for($i = 0; $i < $depth; $i++) {
$line .= "\t";
}
if(count($openingTags[0]) > count($closingTags[0])) {
$depth++;
}
$line .= htmlentities($c);
if(!empty($errors)) {
$line .= "";
}
$lines[] = $line;
}
// Add the formatted lines in the debug view
$view .= implode("\n", $lines);
$view .= "
";
$view .= "