Initial commit

This commit is contained in:
2026-05-12 23:34:00 +02:00
commit f3970480c4
8 changed files with 2151 additions and 0 deletions

View File

@@ -0,0 +1,610 @@
<?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;
}
}