(function (app) { 'use strict'; var dom = app.dom; var util = app.util; var filesModule = app.modules.files; var ARCHIVE_DIR_NAME = '.archive'; function sanitizeUrlSegment(value, fallback) { var str = (value || '').toString().trim(); if (!str) { return fallback; } var cleaned = str .replace(/[^A-Za-z0-9._~-]+/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, ''); return cleaned || fallback; } function encodeRelativePath(path) { return path.split('/').map(function (segment) { return encodeURIComponent(segment); }).join('/'); } function buildRedirectHtml(targetHref, displayName) { var safeHrefAttr = util.escapeHtmlAttribute(targetHref); var safeLabel = util.escapeHtml(displayName || targetHref); return '\n' + '\n' + '
\n' + '\n' + '\n' + 'Redirecting to ' + safeLabel + '.
\n' + '\n' + '\n'; } async function ensureArchiveDirectory(rootHandle) { return rootHandle.getDirectoryHandle(ARCHIVE_DIR_NAME, { create: true }); } async function writeRedirectFile(directoryHandle, filename, html) { var fileHandle = await directoryHandle.getFileHandle(filename, { create: true }); var writable = await fileHandle.createWritable(); await writable.write(new Blob([html], { type: 'text/html' })); await writable.close(); } function groupFilesByTracking(files) { var map = new Map(); files.forEach(function (file) { var key = file.trackingNumber || ''; if (!map.has(key)) { map.set(key, []); } map.get(key).push(file); }); return map; } function determineLatestByTracking(grouped) { var latest = new Map(); grouped.forEach(function (files, tracking) { if (!files || !files.length) { return; } var sorted = files.slice().sort(function (a, b) { var revisionCompare = util.compareRevisionPriority(a.revision, b.revision); if (revisionCompare !== 0) { return revisionCompare; } var pathCompare = (a.path || '').localeCompare(b.path || ''); if (pathCompare !== 0) { return pathCompare; } return (a.name || '').localeCompare(b.name || ''); }); latest.set(tracking, sorted[sorted.length - 1]); }); return latest; } function buildRedirectFilenames(file) { var trackingSegment = sanitizeUrlSegment(file.trackingNumber || '', 'tracking'); var rawRevision = (file.revision || '').trim(); var revisionSegment = rawRevision ? sanitizeUrlSegment(rawRevision, 'rev') : ''; var rawHash = (file.sha256 || '').trim(); var hashSegment = rawHash ? sanitizeUrlSegment(rawHash, 'hash') : ''; var htmlSuffix = '.html'; var revisionFilename = revisionSegment ? (trackingSegment + '_' + revisionSegment + htmlSuffix) : ''; var hashFilename = hashSegment ? (hashSegment + htmlSuffix) : ''; var latestFilename = trackingSegment + htmlSuffix; return { latestFilename: latestFilename, revisionFilename: revisionFilename, hashFilename: hashFilename }; } async function resolveFileMetadata(entry) { var parsed = zddc.parseFilename(entry.name || entry.path || '') || {}; if (!parsed.trackingNumber) { return null; } var file = await entry.handle.getFile(); var sha256 = await util.hashFile(file); return { path: entry.path, name: file.name, extension: parsed.extension || zddc.splitExtension(file.name).extension, trackingNumber: parsed.trackingNumber, revision: parsed.revision, status: parsed.status, title: parsed.title, sha256: sha256, size: file.size }; } function buildRelativeHref(relativePath) { var encoded = encodeRelativePath(relativePath); return '../' + encoded; } async function createRedirectFiles(archiveHandle, file, isLatest) { var filenames = buildRedirectFilenames(file); var href = buildRelativeHref(file.path); var html = buildRedirectHtml(href, file.name); var writes = []; if (filenames.revisionFilename) { writes.push(writeRedirectFile(archiveHandle, filenames.revisionFilename, html)); } if (filenames.hashFilename) { writes.push(writeRedirectFile(archiveHandle, filenames.hashFilename, html)); } if (isLatest) { writes.push(writeRedirectFile(archiveHandle, filenames.latestFilename, html)); } await Promise.all(writes); return true; } async function pickIndexDirectory() { if (typeof window.showDirectoryPicker !== 'function') { throw new Error('File System Access API showDirectoryPicker is required'); } return window.showDirectoryPicker(); } filesModule.generateArchiveRedirects = async function generateArchiveRedirects() { var rootHandle; try { rootHandle = await pickIndexDirectory(); } catch (err) { if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) { console.log('[transmittal] Create Index cancelled.'); return; } throw err; } var overallStart = filesModule.nowMs(); var entries = await filesModule.collectDirectoryEntries(rootHandle); if (!entries.length) { console.log('[transmittal] Create Index: No files found.'); return; } var archiveHandle = await ensureArchiveDirectory(rootHandle); var files = []; for (var i = 0; i < entries.length; i++) { try { var metadata = await resolveFileMetadata(entries[i]); if (metadata) { files.push(metadata); } } catch (err) { console.warn('[transmittal] create-index skip', entries[i].path, err); } } if (!files.length) { console.log('[transmittal] Create Index: No ZDDC files found.'); return; } var grouped = groupFilesByTracking(files); var latest = determineLatestByTracking(grouped); var writePromises = files.map(function (file) { var isLatest = latest.get(file.trackingNumber) === file; return createRedirectFiles(archiveHandle, file, isLatest) .catch(function (err) { console.error('[transmittal] create-index write failed', file.path, err); return null; }); }); var results = await Promise.all(writePromises); var created = results.filter(function (result) { return result !== null; }).length; var totalElapsed = filesModule.nowMs() - overallStart; console.log('[transmittal] Create Index: Generated redirects for ' + created + ' files in ' + filesModule.formatDuration(totalElapsed) + '.'); }; })(window.transmittalApp);