['message' => 'Unsupported format.']]);
exit;
}
if (!$events) {
http_response_code(422);
echo json_encode(['error' => ['message' => 'No events provided.']]);
exit;
}
if (!class_exists('ZipArchive')) {
http_response_code(500);
echo json_encode(['error' => ['message' => 'ZipArchive extension not available.']]);
exit;
}
$text = buildTimelineText($events, $whatFound);
$docx = buildTimelineDocx($text);
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
header('Content-Disposition: attachment; filename="timeline.docx"');
header('Content-Length: ' . strlen($docx));
header('Cache-Control: no-store');
echo $docx;
exit;
function buildTimelineText(array $events, string $whatFound): string
{
$lines = [];
if ($whatFound !== '') {
$lines[] = $whatFound;
$lines[] = '';
$lines[] = str_repeat("\u{2500}", 40);
$lines[] = '';
}
foreach ($events as $ev) {
$date = (string)($ev['date'] ?? 'unknown date');
$time = (string)($ev['time'] ?? '');
$actor = (string)($ev['actor'] ?? '');
$event = (string)($ev['event'] ?? '');
$excerpt = (string)($ev['source_excerpt'] ?? '');
$conf = (string)($ev['confidence'] ?? '');
$dateStr = $time !== '' ? "{$date} {$time}" : $date;
$header = $actor !== '' ? "[{$dateStr}] {$actor}" : "[{$dateStr}]";
if ($conf !== '' && $conf !== 'high') {
$header .= " ({$conf} confidence)";
}
$lines[] = $header;
if ($event !== '') $lines[] = $event;
if ($excerpt !== '') $lines[] = "Source: \u{201C}{$excerpt}\u{201D}";
$lines[] = '';
$lines[] = str_repeat("\u{2500}", 40);
$lines[] = '';
}
return implode("\n", $lines);
}
function buildTimelineDocx(string $text): string
{
$tmp = tempnam(sys_get_temp_dir(), 'dbn_tl_');
@unlink($tmp);
$tmp .= '.docx';
$zip = new ZipArchive();
$zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$zip->addFromString('[Content_Types].xml', tlContentTypesXml());
$zip->addFromString('_rels/.rels', tlRelsXml());
$zip->addFromString('word/document.xml', tlDocumentXml($text));
$zip->addFromString('word/_rels/document.xml.rels', tlWordRelsXml());
$zip->addFromString('docProps/app.xml', tlAppXml());
$zip->addFromString('docProps/core.xml', tlCoreXml());
$zip->close();
$bytes = file_get_contents($tmp);
@unlink($tmp);
return $bytes;
}
function tlDocumentXml(string $text): string
{
$lines = explode("\n", str_replace("\r\n", "\n", str_replace("\r", "\n", $text)));
$paras = [];
foreach ($lines as $line) {
$safe = htmlspecialchars($line, ENT_XML1 | ENT_COMPAT, 'UTF-8');
if ($safe === '') {
$paras[] = '';
} elseif (preg_match('/^\[.+?\]/', $line)) {
// Event header line — bold
$paras[] = ''
. '' . $safe . '';
} elseif (str_starts_with($line, str_repeat("\u{2500}", 5))) {
// Divider line — light grey
$paras[] = ''
. '' . $safe . '';
} else {
$paras[] = ''
. '' . $safe . '';
}
}
return ''
. ''
. '' . implode('', $paras)
. ''
. ''
. '';
}
function tlContentTypesXml(): string
{
return ''
. ''
. ''
. ''
. ''
. ''
. ''
. '';
}
function tlRelsXml(): string
{
return ''
. ''
. ''
. ''
. ''
. '';
}
function tlWordRelsXml(): string
{
return ''
. '';
}
function tlAppXml(): string
{
return ''
. ''
. 'DoBetterNorge Timeline'
. '';
}
function tlCoreXml(): string
{
$date = date('Y-m-d\TH:i:s\Z');
return ''
. ''
. 'DoBetterNorge'
. '' . $date . ''
. '';
}