calendar-layout-engine / src /layout-engine.js
jwcalendar's picture
Add layout calculations and preview exports
1d7f640 verified
Raw History Blame Contribute Delete
6.41 kB
export const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
export const WEEKDAYS = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
export const PAPER = { A4: { width: 210, height: 297 }, Letter: { width: 215.9, height: 279.4 } };
function utcDate(year, month, day) { const d=new Date(0);d.setUTCHours(0,0,0,0);d.setUTCFullYear(year,month-1,day);return d; }
function monthGrid(year,month,weekStart,adjacentMonths) {
const first=utcDate(year,month,1).getUTCDay(),offset=(first-weekStart+7)%7,count=utcDate(year,month+1,0).getUTCDate(),rows=Math.ceil((offset+count)/7);
return {year,month,weekStart,firstWeekday:first,daysInMonth:count,rows,leadingEmptyCells:offset,trailingEmptyCells:rows*7-offset-count,cells:Array.from({length:rows*7},(_,i)=>{const d=utcDate(year,month,i-offset+1);return{date:`${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,"0")}-${String(d.getUTCDate()).padStart(2,"0")}`,day:d.getUTCDate(),inMonth:d.getUTCMonth()===month-1,visible:adjacentMonths||d.getUTCMonth()===month-1,weekday:d.getUTCDay()};})};
}
function isoWeek(dateString) {const [y,m,d]=dateString.split("-").map(Number),date=utcDate(y,m,d),thursday=new Date(date);thursday.setUTCDate(thursday.getUTCDate()+(4-(thursday.getUTCDay()||7)));const isoYear=thursday.getUTCFullYear(),jan4=utcDate(isoYear,1,4);jan4.setUTCDate(jan4.getUTCDate()+(4-(jan4.getUTCDay()||7)));return 1+Math.round((thursday-jan4)/604800000);}
export function calculateLayout(options) {
const { year, month, weekStart, paper, orientation, margin, headerHeight, cellPadding, isoWeekColumn } = options;
const base = PAPER[paper];
const pageWidth = orientation === "landscape" ? base.height : base.width;
const pageHeight = orientation === "landscape" ? base.width : base.height;
const printableWidth = pageWidth - 2 * margin;
const printableHeight = pageHeight - 2 * margin - headerHeight;
if (printableWidth <= 0 || printableHeight <= 0) throw new RangeError("Margins and header exceed the paper dimensions.");
const grid = monthGrid(year, month, weekStart, options.adjacentMonths);
const columns = 7 + Number(isoWeekColumn);
const cellWidth = printableWidth / columns;
const cellHeight = printableHeight / (grid.rows + 1);
return { ...grid, paper, orientation, pageWidth, pageHeight, margin, headerHeight, printableWidth, printableHeight, rows: grid.rows, columns, cellWidth, cellHeight, cellPadding, safeTextWidth: Math.max(0, cellWidth - 2 * cellPadding), safeTextHeight: Math.max(0, cellHeight - 2 * cellPadding), isoWeekColumn, adjacentMonths: options.adjacentMonths };
}
export function makeLayoutCss(spec) {
return `@page { size: ${spec.paper} ${spec.orientation}; margin: ${spec.margin}mm; }\n@media print { body { margin: 0; } .calendar-page { width: ${spec.pageWidth - 2*spec.margin}mm; height: ${spec.pageHeight - 2*spec.margin}mm; } }\n.calendar-page { font: 10pt/1.2 system-ui, sans-serif; color: #111; }\n.calendar-page table { width: 100%; height: ${spec.printableHeight}mm; border-collapse: collapse; table-layout: fixed; }\n.calendar-page th, .calendar-page td { border: 0.2mm solid #777; padding: ${spec.cellPadding}mm; vertical-align: top; }\n.calendar-page td { height: ${spec.cellHeight}mm; }`;
}
export function makeHtml(spec, title = `${MONTH_NAMES[spec.month-1]} ${spec.year}`) {
const weekdays = Array.from({length:7},(_,i)=>WEEKDAYS[(spec.weekStart+i)%7]);
const headings = [...(spec.isoWeekColumn ? ["ISO"] : []), ...weekdays];
const rows = Array.from({length:spec.rows},(_,r)=>{
const week = spec.cells.slice(r*7,r*7+7);
const days = week.map(cell=>`<td>${cell.inMonth || spec.adjacentMonths ? cell.day : ""}</td>`).join("");
const thursday=week.find(cell=>cell.weekday===4) ?? week[0];
return `<tr>${spec.isoWeekColumn ? `<td>W${isoWeek(thursday.date)}</td>` : ""}${days}</tr>`;
}).join("\n");
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title><style>${makeLayoutCss(spec)}</style></head><body><section class="calendar-page"><h1>${title}</h1><table><thead><tr>${headings.map(h=>`<th>${h}</th>`).join("")}</tr></thead><tbody>${rows}</tbody></table></section></body></html>`;
}
export function makeSvg(spec, title = `${MONTH_NAMES[spec.month-1]} ${spec.year}`) {
const mm = value => value * 3.7795275591;
const margin=mm(spec.margin), width=mm(spec.pageWidth),height=mm(spec.pageHeight), gridTop=margin+mm(spec.headerHeight), gridWidth=mm(spec.printableWidth),gridHeight=mm(spec.printableHeight),cellW=gridWidth/spec.columns,cellH=gridHeight/(spec.rows+1);
const parts=[`<svg xmlns="http://www.w3.org/2000/svg" width="${spec.pageWidth}mm" height="${spec.pageHeight}mm" viewBox="0 0 ${width} ${height}">`,`<rect width="100%" height="100%" fill="white"/>`,`<text x="${margin}" y="${margin+mm(8)}" font-family="system-ui,sans-serif" font-size="${mm(6)}" font-weight="700">${title}</text>`];
const names=[...(spec.isoWeekColumn?["ISO"]:[]),...Array.from({length:7},(_,i)=>WEEKDAYS[(spec.weekStart+i)%7])];
names.forEach((name,c)=>{const x=margin+c*cellW;parts.push(`<rect x="${x}" y="${gridTop}" width="${cellW}" height="${cellH}" fill="#f2f5f3" stroke="#52615b" stroke-width="0.5"/>`,`<text x="${x+mm(2)}" y="${gridTop+cellH/2}" font-family="system-ui,sans-serif" font-size="${mm(3.4)}">${name.slice(0,3)}</text>`);});
for(let r=0;r<spec.rows;r++){
if(spec.isoWeekColumn){const x=margin;const y=gridTop+(r+1)*cellH;const week=spec.cells.slice(r*7,r*7+7);const thursday=week.find(cell=>cell.weekday===4)??week[0];parts.push(`<rect x="${x}" y="${y}" width="${cellW}" height="${cellH}" fill="white" stroke="#52615b" stroke-width="0.5"/><text x="${x+mm(2)}" y="${y+mm(5)}" font-family="system-ui,sans-serif" font-size="${mm(3)}">W${isoWeek(thursday.date)}</text>`);}
spec.cells.slice(r*7,r*7+7).forEach((cell,c)=>{const col=c+Number(spec.isoWeekColumn),x=margin+col*cellW,y=gridTop+(r+1)*cellH;parts.push(`<rect x="${x}" y="${y}" width="${cellW}" height="${cellH}" fill="white" stroke="#52615b" stroke-width="0.5"/>`);if(cell.inMonth||spec.adjacentMonths)parts.push(`<text x="${x+mm(spec.cellPadding)}" y="${y+mm(spec.cellPadding+3)}" font-family="system-ui,sans-serif" font-size="${mm(3.4)}" fill="${cell.inMonth?"#18231f":"#99a39e"}">${cell.day}</text>`);});
}
parts.push("</svg>");return parts.join("\n");
}