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=>`
| ${h} | `).join("")}
|---|