Files
thalim-plugin-newsletter/includes/class-html-exporter.php
2026-05-12 23:34:00 +02:00

611 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* HTML Exporter — generates a complete, email-optimised HTML document
* from a saved newsletter draft post.
*
* Layout: table-based (600 px), inline styles throughout.
* Fonts : Gelasio (Google Fonts @import, fallback Georgia) for headings,
* Arial/Helvetica for body.
* Colors: taken directly from the THALIM theme (_variables.scss).
*/
if (!defined('ABSPATH')) {
exit;
}
class Thalim_NL_HTML_Exporter {
// ── Theme colours (from _variables.scss) ──────────────────────────────────
const COLOR_YELLOW = '#f7ff29';
const COLOR_BG = '#eeeeee'; // $light-gray
const COLOR_WHITE = '#ffffff';
const COLOR_DARK = '#1a1a1a'; // $dark-gray
const COLOR_MID = '#3e3e3e'; // $less-dark-gray
const COLOR_RULE = '#cccccc'; // $less-light-gray
// Category accent colours (from theme _variables.scss)
const COLOR_LABORATOIRE = '#e0775d'; // coral — parent 1
const COLOR_MANIFESTATIONS = '#7cc0c6'; // teal — parent 3
const COLOR_PUBLICATIONS = '#46ae51'; // green — parent 4
const COLOR_MEDIATIONS = '#e05680'; // pink — parent 5
const COLOR_RESSOURCES = '#bb8dd9'; // purple — parent 6
/** Parent category ID → accent colour */
private const PARENT_COLORS = [
1 => self::COLOR_LABORATOIRE,
3 => self::COLOR_MANIFESTATIONS,
4 => self::COLOR_PUBLICATIONS,
5 => self::COLOR_MEDIATIONS,
6 => self::COLOR_RESSOURCES,
];
// ── Public entry point ────────────────────────────────────────────────────
/**
* Generate a complete email-ready HTML document from a saved newsletter post.
*/
/** Month window timestamps — set by generate(), used by format_seminar_line() */
private int $month_start = 0;
private int $month_end = 0;
public function generate(WP_Post $post): string {
$sections_json = get_post_meta($post->ID, '_newsletter_sections', true);
$sections = json_decode($sections_json, true) ?: [];
$intro = get_post_meta($post->ID, '_newsletter_intro', true) ?: '';
$conclusion = get_post_meta($post->ID, '_newsletter_conclusion', true) ?: '';
$subscribe_url = get_post_meta($post->ID, '_newsletter_subscribe_url', true) ?: '';
$unsubscribe_url = get_post_meta($post->ID, '_newsletter_unsubscribe_url', true) ?: '';
// Compute month window from stored newsletter month
$year_month = get_post_meta($post->ID, '_newsletter_month', true) ?: '';
if ($year_month) {
$this->month_start = strtotime($year_month . '-01 00:00:00') ?: 0;
$this->month_end = strtotime('last day of ' . $year_month . ' 23:59:59') ?: 0;
}
// Build ordered section blocks following the category hierarchy
$groups = Thalim_NL_Post_Query::get_eligible_categories();
$section_blocks = [];
foreach ($groups as $parent_id => $group) {
$color = self::PARENT_COLORS[$parent_id] ?? self::COLOR_DARK;
// All cat IDs in this group: parent + children
$cats_in_group = [$parent_id => $group['name']];
foreach ($group['children'] as $cid => $cname) {
$cats_in_group[$cid] = $cname;
}
foreach ($cats_in_group as $cat_id => $label) {
$post_ids = $sections[$cat_id] ?? $sections[(string) $cat_id] ?? [];
if (empty($post_ids)) {
continue;
}
$items = [];
foreach ($post_ids as $pid) {
$line = $this->format_post_line((int) $pid, $cat_id);
if ($line) {
$items[] = $line;
}
}
if (!empty($items)) {
$section_blocks[] = [
'label' => $label,
'color' => $color,
'items' => $items,
];
}
}
}
return $this->render_email_document($intro, $section_blocks, $conclusion, $subscribe_url, $unsubscribe_url);
}
// ── Email document builder ─────────────────────────────────────────────────
private function render_email_document(string $intro, array $section_blocks, string $conclusion, string $subscribe_url = '', string $unsubscribe_url = ''): string {
$logo_url = get_theme_file_uri('assets/images/thalim-logo.png');
$site_url = get_bloginfo('url');
$site_name = get_bloginfo('name');
// Font stacks (Gelasio via Google Fonts for clients that support it)
$font_serif = "Gelasio, Georgia, 'Times New Roman', serif";
$font_sans = "Arial, Helvetica, sans-serif";
// Shared cell style
$cell_padding = 'padding:32px 40px;';
$body_style = "font-family:{$font_sans};font-size:15px;line-height:1.6;color:" . self::COLOR_DARK . ";";
ob_start();
?>
<!DOCTYPE html>
<html lang="fr" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="x-apple-disable-message-reformatting">
<title><?php echo esc_html($site_name); ?> — Newsletter</title>
<!--[if mso]>
<noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
<![endif]-->
<style type="text/css">
/* Google Fonts — Gelasio (supported by Apple Mail, Outlook Web, Gmail web) */
@import url('https://fonts.googleapis.com/css2?family=Gelasio&display=swap');
/* Email reset */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
box-sizing: border-box;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; display: block; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; }
/* Responsive */
@media only screen and (max-width: 620px) {
.email-wrapper { width: 100% !important; }
.email-content { padding: 24px 20px !important; }
.email-header { padding: 20px !important; }
.logo-img { width: 90px !important; }
}
</style>
</head>
<body style="margin:0;padding:0;background-color:<?php echo self::COLOR_BG; ?>;">
<!-- Preheader (hidden) -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">
<?php echo esc_html($site_name); ?> &mdash; Newsletter
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
</div>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
style="background-color:<?php echo self::COLOR_BG; ?>;">
<tr>
<td align="center" style="padding:24px 12px;">
<!-- ── Main wrapper: 600px ── -->
<table class="email-wrapper" role="presentation" border="0" cellpadding="0" cellspacing="0"
width="600" style="background-color:<?php echo self::COLOR_WHITE; ?>;">
<!-- ═══════════ HEADER ═══════════ -->
<tr>
<td class="email-header" bgcolor="<?php echo self::COLOR_BG; ?>"
style="background-color:<?php echo self::COLOR_BG; ?>;padding:28px 40px;text-align:center;">
<a href="<?php echo esc_url($site_url); ?>" style="text-decoration:none;">
<img class="logo-img" src="<?php echo esc_url($logo_url); ?>"
width="110" alt="<?php echo esc_attr($site_name); ?>"
style="display:block;margin:0 auto;width:110px;height:auto;">
</a>
</td>
</tr>
<!-- Yellow accent bar -->
<tr>
<td height="5" style="height:5px;font-size:0;line-height:0;"
bgcolor="<?php echo self::COLOR_YELLOW; ?>">
&nbsp;
</td>
</tr>
<?php if (!empty(trim($intro))): ?>
<!-- ═══════════ INTRO ═══════════ -->
<tr>
<td class="email-content"
style="<?php echo $cell_padding; ?><?php echo $body_style; ?>border-bottom:1px solid <?php echo self::COLOR_RULE; ?>;">
<?php echo $this->process_rich_text($intro); ?>
</td>
</tr>
<?php endif; ?>
<?php foreach ($section_blocks as $block): ?>
<!-- ═══════════ SECTION: <?php echo esc_html($block['label']); ?> ═══════════ -->
<tr>
<td style="padding:0 40px 24px 40px;">
<!-- Section heading with gradient underline -->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding-top:28px;padding-bottom:0;">
<span style="font-family:<?php echo $font_serif; ?>;font-size:20px;font-weight:normal;color:<?php echo self::COLOR_DARK; ?>;letter-spacing:0.01em;">
<?php echo esc_html($block['label']); ?>
</span>
</td>
</tr>
<tr>
<td style="padding-top:6px;padding-bottom:16px;">
<div style="height:3px;border-radius:2px;background:linear-gradient(to top, <?php echo esc_attr($block['color']); ?>, transparent);"></div>
</td>
</tr>
</table>
<!-- Post list -->
<?php foreach ($block['items'] as $item): ?>
<?php echo $item; ?>
<?php endforeach; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (!empty(trim($conclusion))): ?>
<!-- ═══════════ CONCLUSION ═══════════ -->
<tr>
<td class="email-content"
style="<?php echo $cell_padding; ?><?php echo $body_style; ?>border-top:1px solid <?php echo self::COLOR_RULE; ?>;">
<?php echo $this->process_rich_text($conclusion); ?>
</td>
</tr>
<?php endif; ?>
<!-- ═══════════ FOOTER ═══════════ -->
<tr>
<td bgcolor="<?php echo self::COLOR_BG; ?>"
style="background-color:<?php echo self::COLOR_BG; ?>;padding:20px 40px;text-align:center;">
<p style="margin:0 0 4px 0;font-family:<?php echo $font_sans; ?>;font-size:11px;
text-transform:uppercase;letter-spacing:0.08em;color:<?php echo self::COLOR_MID; ?>;">
<?php echo esc_html($site_name); ?>
</p>
<p style="margin:0;font-family:<?php echo $font_sans; ?>;font-size:11px;color:<?php echo self::COLOR_MID; ?>;">
Théorie et Histoire des Arts et des Littératures de la Modernité
</p>
<p style="margin:8px 0 0 0;">
<a href="<?php echo esc_url($site_url); ?>"
style="font-family:<?php echo $font_sans; ?>;font-size:11px;
color:<?php echo self::COLOR_MID; ?>;text-decoration:underline;">
<?php echo esc_url($site_url); ?>
</a>
</p>
<?php if ($subscribe_url || $unsubscribe_url): ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
style="margin:14px auto 0 auto;">
<tr>
<?php if ($subscribe_url): ?>
<td style="padding:0 10px;">
<a href="<?php echo esc_url($subscribe_url); ?>"
style="font-family:<?php echo $font_sans; ?>;font-size:12px;
color:<?php echo self::COLOR_MID; ?>;text-decoration:underline;">
S'abonner
</a>
</td>
<?php endif; ?>
<?php if ($unsubscribe_url): ?>
<td style="padding:0 10px;">
<a href="<?php echo esc_url($unsubscribe_url); ?>"
style="font-family:<?php echo $font_sans; ?>;font-size:12px;
color:<?php echo self::COLOR_MID; ?>;text-decoration:underline;">
Se désabonner
</a>
</td>
<?php endif; ?>
</tr>
</table>
<?php endif; ?>
</td>
</tr>
</table>
<!-- /Main wrapper -->
</td>
</tr>
</table>
</body>
</html>
<?php
return ob_get_clean();
}
// ── Per-post block renderer ────────────────────────────────────────────────
/**
* Format a single post as an HTML table block for the email.
* Optional image on the left, title in serif, meta below in sans-serif.
*/
public function format_post_line(int $post_id, int $cat_id): string {
// Seminars: special rendering with séances
if ($cat_id === THALIM_NL_CAT_SEMINAIRES) {
return $this->format_seminar_line($post_id);
}
$post = get_post($post_id);
if (!$post) {
return '';
}
$font_serif = "Gelasio, Georgia, 'Times New Roman', serif";
$font_sans = "Arial, Helvetica, sans-serif";
$url = get_permalink($post_id);
$title = get_the_title($post_id);
// Date
$date_str = $this->format_date_for_category($post_id, $cat_id);
// Authors
$author_parts = [];
foreach (get_post_meta($post_id, 'membres', false) as $uid) {
$user = get_userdata((int) $uid);
if ($user) {
$author_parts[] = $user->display_name;
}
}
$autrepersonnes = get_post_meta($post_id, 'autrepersonnes', true) ?: '';
if ($autrepersonnes) {
$author_parts[] = $autrepersonnes;
}
$authors_str = implode(', ', $author_parts);
// Image (first image from documents_joints)
$image_url = '';
$doc_ids = get_post_meta($post_id, 'documents_joints', false);
foreach ($doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id);
if ($mime && str_starts_with($mime, 'image/')) {
$src_data = wp_get_attachment_image_src($doc_id, 'thumbnail');
if ($src_data) {
$image_url = $src_data[0];
break;
}
}
}
// Meta line: date — authors
$meta_parts = [];
if ($date_str) {
$meta_parts[] = esc_html($date_str);
}
if ($authors_str) {
$meta_parts[] = esc_html($authors_str);
}
$meta_html = implode(' — ', $meta_parts);
// Build the post block as a table row
ob_start();
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
style="margin-bottom:16px;">
<tr>
<?php if ($image_url): ?>
<td width="70" valign="top" style="width:70px;padding-right:14px;">
<a href="<?php echo esc_url($url); ?>" style="text-decoration:none;">
<img src="<?php echo esc_url($image_url); ?>" width="70" height="70" alt=""
style="display:block;width:70px;height:70px;object-fit:cover;border-radius:4px;">
</a>
</td>
<?php endif; ?>
<td valign="top">
<a href="<?php echo esc_url($url); ?>"
style="font-family:<?php echo $font_serif; ?>;font-size:16px;line-height:1.35;color:<?php echo self::COLOR_DARK; ?>;text-decoration:none;">
<?php echo esc_html($title); ?>
</a>
<?php if ($meta_html): ?>
<div style="font-family:<?php echo $font_sans; ?>;font-size:13px;line-height:1.45;color:<?php echo self::COLOR_DARK; ?>;margin-top:4px;">
<?php echo $meta_html; ?>
</div>
<?php endif; ?>
</td>
</tr>
</table>
<?php
return ob_get_clean();
}
/**
* Format a seminar post: title (no dates) + list of séances with date, time, location.
* Each séance links to the seminar page.
*/
private function format_seminar_line(int $post_id): string {
$post = get_post($post_id);
if (!$post) {
return '';
}
$font_serif = "Gelasio, Georgia, 'Times New Roman', serif";
$font_sans = "Arial, Helvetica, sans-serif";
$url = get_permalink($post_id);
$title = get_the_title($post_id);
// Image
$image_url = '';
$doc_ids = get_post_meta($post_id, 'documents_joints', false);
foreach ($doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id);
if ($mime && str_starts_with($mime, 'image/')) {
$src_data = wp_get_attachment_image_src($doc_id, 'thumbnail');
if ($src_data) {
$image_url = $src_data[0];
break;
}
}
}
// Fetch séances within the newsletter month window
$seance_ids = get_post_meta($post_id, 'seances', false);
$seances = [];
foreach ($seance_ids as $sid) {
$sid = (int) $sid;
$s_post = get_post($sid);
if (!$s_post || $s_post->post_status !== 'publish') continue;
$raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: '';
$ts = $raw_debut ? strtotime($raw_debut) : false;
// Filter: only séances within the month window
if ($this->month_start && $this->month_end) {
if (!$ts || $ts < $this->month_start || $ts > $this->month_end) {
continue;
}
}
$seances[] = [
'id' => $sid,
'title' => get_the_title($sid),
'date_debut' => $raw_debut,
'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '',
'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5),
'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5),
'lieu' => get_post_meta($sid, 'lieu', true) ?: '',
];
}
usort($seances, fn($a, $b) => strcmp($a['date_debut'], $b['date_debut']));
ob_start();
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
style="margin-bottom:16px;">
<tr>
<?php if ($image_url): ?>
<td width="70" valign="top" style="width:70px;padding-right:14px;">
<a href="<?php echo esc_url($url); ?>" style="text-decoration:none;">
<img src="<?php echo esc_url($image_url); ?>" width="70" height="70" alt=""
style="display:block;width:70px;height:70px;object-fit:cover;border-radius:4px;">
</a>
</td>
<?php endif; ?>
<td valign="top">
<a href="<?php echo esc_url($url); ?>"
style="font-family:<?php echo $font_serif; ?>;font-size:16px;line-height:1.35;color:<?php echo self::COLOR_DARK; ?>;text-decoration:none;">
<?php echo esc_html($title); ?>
</a>
<?php if (!empty($seances)): ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
style="margin-top:8px;">
<?php foreach ($seances as $seance):
$seance_url = $url . '#seance-' . $seance['id'];
?>
<tr>
<td style="padding:3px 0;font-family:<?php echo $font_sans; ?>;font-size:13px;line-height:1.45;color:<?php echo self::COLOR_DARK; ?>;">
<?php
$parts = [];
if ($seance['date_debut']) {
$ts = strtotime($seance['date_debut']);
if ($ts) $parts[] = date_i18n('j F Y', $ts);
}
if ($seance['heure_de_debut']) {
$time = $seance['heure_de_debut'];
if ($seance['heure_de_fin']) $time .= '' . $seance['heure_de_fin'];
$parts[] = $time;
}
if ($seance['lieu']) {
$parts[] = esc_html($seance['lieu']);
}
?>
<a href="<?php echo esc_url($seance_url); ?>"
style="color:<?php echo self::COLOR_DARK; ?>;text-decoration:none;">
<?php echo esc_html($seance['title']); ?>
</a>
<?php if (!empty($parts)): ?>
<br>
<span style="color:<?php echo self::COLOR_MID; ?>;font-size:12px;">
<?php echo implode(' · ', $parts); ?>
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</td>
</tr>
</table>
<?php
return ob_get_clean();
}
// ── Date helpers ──────────────────────────────────────────────────────────
private function format_date_for_category(int $post_id, int $cat_id): string {
$window = Thalim_NL_Post_Query::get_window_type($cat_id);
if ($window === 'debut_minus35_to_fin') {
$raw_debut = get_post_meta($post_id, 'date_de_debut', true) ?: '';
$raw_fin = get_post_meta($post_id, 'date_de_fin', true) ?: '';
$debut_str = $this->format_raw_date($raw_debut);
$fin_str = $this->format_raw_date($raw_fin);
if ($debut_str && $fin_str && $debut_str !== $fin_str) {
return $debut_str . '' . $fin_str;
}
return $debut_str ?: $fin_str;
}
$raw_dt = get_post_meta($post_id, 'datetime', true) ?: '';
if ($raw_dt && !str_starts_with($raw_dt, '0000-00-00')) {
return $this->format_raw_date($raw_dt);
}
$post = get_post($post_id);
return $post ? $this->format_raw_date($post->post_date) : '';
}
private function format_raw_date(string $raw): string {
if (!$raw || str_starts_with($raw, '0000-00-00')) {
return '';
}
$ts = strtotime($raw);
return $ts ? date_i18n('j F Y', $ts) : '';
}
// ── Rich-text processor (intro / conclusion) ──────────────────────────────
/**
* Convert stored WYSIWYG content to email-safe inline-styled HTML.
* Applies wpautop then inlines font/colour styles on common tags.
*/
private function process_rich_text(string $content): string {
$font_serif = "Gelasio, Georgia, 'Times New Roman', serif";
$font_sans = "Arial, Helvetica, sans-serif";
$color_dark = self::COLOR_DARK;
$color_mid = self::COLOR_MID;
$color_rule = self::COLOR_RULE;
$html = wpautop($content);
// Style <p>
$html = preg_replace(
'/<p([ >])/',
'<p style="margin:0 0 14px 0;font-family:' . $font_sans . ';font-size:15px;line-height:1.6;color:' . $color_dark . ';"$1',
$html
);
// Style <a>
$html = preg_replace(
'/<a([ >])/',
'<a style="color:' . $color_dark . ';text-decoration:underline;"$1',
$html
);
// Style <ul>
$html = preg_replace(
'/<ul([ >])/',
'<ul style="margin:0 0 14px 0;padding-left:20px;font-family:' . $font_sans . ';font-size:15px;line-height:1.6;color:' . $color_dark . ';"$1',
$html
);
// Style <ol>
$html = preg_replace(
'/<ol([ >])/',
'<ol style="margin:0 0 14px 0;padding-left:20px;font-family:' . $font_sans . ';font-size:15px;line-height:1.6;color:' . $color_dark . ';"$1',
$html
);
// Style <li>
$html = preg_replace(
'/<li([ >])/',
'<li style="margin-bottom:6px;"$1',
$html
);
// Style <blockquote>
$html = preg_replace(
'/<blockquote([ >])/',
'<blockquote style="margin:0 0 14px 20px;padding-left:14px;border-left:3px solid ' . $color_rule . ';color:' . $color_mid . ';"$1',
$html
);
return $html;
}
}