import type { ShardInfo, UrlEntry } from './types';
const XML_HEADER = '';
const SITEMAP_SCHEMA = 'http://www.sitemaps.org/schemas/sitemap/0.9';
const TANGLED_EPOCH = new Date('2025-01-01T00:00:00Z');
export const PROTOCOL_MAX_URLS = 50000;
export const PROTOCOL_MAX_BYTES = 50 * 1024 * 1024; // 50 MB uncompressed
export function escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
export function unescapeXml(str: string): string {
return str
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/&/g, '&');
}
export function sanitizeLastmod(rawDate?: string, referenceTime = new Date()): string {
if (!rawDate) {
return referenceTime.toISOString().split('T')[0];
}
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime()) || parsed > referenceTime || parsed < TANGLED_EPOCH) {
return referenceTime.toISOString().split('T')[0];
}
return parsed.toISOString().split('T')[0];
}
export function measureXmlBytes(xml: string): number {
return new TextEncoder().encode(xml).byteLength;
}
export function buildUrlsetXml(urls: UrlEntry[]): string {
const body = urls
.map((u) => {
const parts = [` ${escapeXml(u.loc)}`];
if (u.lastmod) parts.push(` ${escapeXml(u.lastmod)}`);
if (u.changefreq) parts.push(` ${u.changefreq}`);
if (u.priority !== undefined) parts.push(` ${u.priority.toFixed(1)}`);
return ` \n${parts.join('\n')}\n `;
})
.join('\n');
return `${XML_HEADER}\n\n${body}\n\n`;
}
export function buildSitemapIndexXml(shards: ShardInfo[], baseUrl: string): string {
const body = shards
.map((shard) => {
const loc = `${baseUrl.replace(/\/+$/, '')}/sitemaps/${shard.filename}`;
return ` \n ${escapeXml(loc)}\n ${escapeXml(shard.lastmod)}\n `;
})
.join('\n');
return `${XML_HEADER}\n\n${body}\n\n`;
}
export async function gzipCompress(text: string): Promise {
const blob = new Blob([text]);
const stream = blob.stream().pipeThrough(new CompressionStream('gzip'));
const buffer = await new Response(stream).arrayBuffer();
return new Uint8Array(buffer);
}
export async function gzipDecompress(data: ArrayBuffer | Uint8Array): Promise {
const blob = new Blob([data as any]);
const stream = blob.stream().pipeThrough(new DecompressionStream('gzip'));
return await new Response(stream).text();
}