v1.4.0: CSV/Excel/JSON/NextUp exports, units-first smart year standing, live export progress bar, AES-GCM at-rest password encryption.
230 lines
11 KiB
JavaScript
230 lines
11 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
|
|
}
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
// ---- Minimal ZIP writer (STORED, no compression) ----
|
|
// Produces a valid ZIP containing the XLSX parts. Stored entries are used
|
|
// because we have no deflate available (browser + zero-dependency Node).
|
|
function crc32(buf) {
|
|
let table = [];
|
|
for (let n = 0; n < 256; n++) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
table[n] = c >>> 0;
|
|
}
|
|
let crc = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function zipStore(entries) {
|
|
// entries: [{ name, data: Uint8Array }]
|
|
const chunks = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
const enc = new TextEncoder();
|
|
for (const e of entries) {
|
|
const nameBytes = enc.encode(e.name);
|
|
const crc = crc32(e.data);
|
|
const local = new Uint8Array(30 + nameBytes.length);
|
|
const dv = new DataView(local.buffer);
|
|
dv.setUint32(0, 0x04034b50, true); // local file header sig
|
|
dv.setUint16(4, 20, true); // version needed
|
|
dv.setUint16(6, 0, true); // flags
|
|
dv.setUint16(8, 0, true); // method: stored
|
|
dv.setUint16(10, 0, true); // mod time
|
|
dv.setUint16(12, 0x21, true); // mod date
|
|
dv.setUint32(14, crc, true);
|
|
dv.setUint32(18, e.data.length, true); // compressed size
|
|
dv.setUint32(22, e.data.length, true); // uncompressed size
|
|
dv.setUint16(26, nameBytes.length, true); // name len
|
|
dv.setUint16(28, 0, true); // extra len
|
|
local.set(nameBytes, 30);
|
|
chunks.push(local, e.data);
|
|
|
|
const cd = new Uint8Array(46 + nameBytes.length);
|
|
const cdv = new DataView(cd.buffer);
|
|
cdv.setUint32(0, 0x02014b50, true); // central dir sig
|
|
cdv.setUint16(4, 20, true); // version made by
|
|
cdv.setUint16(6, 20, true); // version needed
|
|
cdv.setUint16(8, 0, true); // flags
|
|
cdv.setUint16(10, 0, true); // method stored
|
|
cdv.setUint16(12, 0, true); // mod time
|
|
cdv.setUint16(14, 0x21, true); // mod date
|
|
cdv.setUint32(16, crc, true);
|
|
cdv.setUint32(20, e.data.length, true);
|
|
cdv.setUint32(24, e.data.length, true);
|
|
cdv.setUint16(28, nameBytes.length, true);
|
|
cdv.setUint16(30, 0, true); // extra len
|
|
cdv.setUint16(32, 0, true); // comment len
|
|
cdv.setUint16(34, 0, true); // disk start
|
|
cdv.setUint16(36, 0, true); // internal attrs
|
|
cdv.setUint32(38, 0, true); // external attrs
|
|
cdv.setUint32(42, offset, true); // local header offset
|
|
cd.set(nameBytes, 46);
|
|
central.push(cd);
|
|
|
|
offset += local.length + e.data.length;
|
|
}
|
|
|
|
const cdStart = offset;
|
|
let cdLen = 0;
|
|
for (const c of central) cdLen += c.length;
|
|
const eocd = new Uint8Array(22);
|
|
const eocdv = new DataView(eocd.buffer);
|
|
eocdv.setUint32(0, 0x06054b50, true);
|
|
eocdv.setUint16(8, entries.length, true); // total entries (this disk)
|
|
eocdv.setUint16(10, entries.length, true);
|
|
eocdv.setUint32(12, cdLen, true);
|
|
eocdv.setUint32(16, cdStart, true);
|
|
|
|
chunks.push(...central, eocd);
|
|
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
const out = new Uint8Array(total);
|
|
let pos = 0;
|
|
for (const c of chunks) { out.set(c, pos); pos += c.length; }
|
|
return out;
|
|
}
|
|
|
|
// ---- XML helpers ----
|
|
function esc(s) {
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
// ---- XLSX builder ----
|
|
// Builds a single-sheet workbook: a title/header block + a course table
|
|
// + a totals row, mirroring the CSV layout.
|
|
function buildStudyLoadXlsx(data) {
|
|
const header = data.header || {};
|
|
const enc = new TextEncoder();
|
|
// Gather all strings into a `sharedStrings` table; cells reference them.
|
|
const strings = [];
|
|
const sIndex = new Map();
|
|
function addS(str) {
|
|
const key = String(str == null ? '' : str);
|
|
if (!sIndex.has(key)) { sIndex.set(key, strings.length); strings.push(key); }
|
|
return sIndex.get(key);
|
|
}
|
|
|
|
// rows: array of arrays of { text } (all text cells for simplicity)
|
|
const rows = [];
|
|
rows.push([{ text: 'ISMIS HELPER - STUDY LOAD' }]);
|
|
rows.push([{ text: 'Program' }, { text: header.program || '' }]);
|
|
rows.push([{ text: 'Student Name' }, { text: header.fullName || header.studentName || '' }]);
|
|
rows.push([{ text: 'Student ID' }, { text: header.studentId || '' }]);
|
|
rows.push([{ text: 'Semester' }, { text: header.semester || '' }]);
|
|
rows.push([{ text: 'Year Level' }, { text: header.yearLevel || '' }]);
|
|
rows.push([{ text: 'Total Units' }, { text: data.totalUnits || '' }]);
|
|
rows.push([]); // blank
|
|
rows.push([{ text: 'Group' }, { text: 'Course Code' }, { text: 'Description' },
|
|
{ text: 'Schedule' }, { text: 'Faculty' }, { text: 'Units' }, { text: 'Status' }]);
|
|
for (const c of (data.courses || [])) {
|
|
rows.push([
|
|
{ text: c.group }, { text: c.courseCode }, { text: c.description },
|
|
{ text: c.schedule }, { text: c.faculty || c.teacher }, { text: c.units },
|
|
{ text: c.status }
|
|
]);
|
|
}
|
|
rows.push([{ text: 'Total' }, { text: '' }, { text: '' }, { text: '' },
|
|
{ text: '' }, { text: data.totalUnits || '' }, { text: '' }]);
|
|
|
|
// Pre-compute shared string refs so sheet1.xml can reference them.
|
|
const cellRefs = rows.map((row) => row.map((c) => c.text === '' ? -1 : addS(c.text)));
|
|
|
|
// ---- sheet1.xml ----
|
|
let sheetXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
sheetXml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
|
|
sheetXml += '<cols>' +
|
|
'<col min="1" max="1" width="8" customWidth="1"/>' +
|
|
'<col min="2" max="2" width="14" customWidth="1"/>' +
|
|
'<col min="3" max="3" width="42" customWidth="1"/>' +
|
|
'<col min="4" max="4" width="38" customWidth="1"/>' +
|
|
'<col min="5" max="5" width="24" customWidth="1"/>' +
|
|
'<col min="6" max="6" width="10" customWidth="1"/>' +
|
|
'<col min="7" max="7" width="12" customWidth="1"/>' +
|
|
'</cols>';
|
|
sheetXml += '<sheetData>';
|
|
const colNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
|
|
cellRefs.forEach((refs, ri) => {
|
|
sheetXml += '<row r="' + (ri + 1) + '">';
|
|
refs.forEach((s, ci) => {
|
|
if (s < 0) return;
|
|
sheetXml += '<c r="' + colNames[ci] + (ri + 1) + '" t="s"><v>' + s + '</v></c>';
|
|
});
|
|
sheetXml += '</row>';
|
|
});
|
|
sheetXml += '</sheetData></worksheet>';
|
|
|
|
// ---- sharedStrings.xml ----
|
|
let ssXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
ssXml += '<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="' +
|
|
cellRefs.reduce((s, r) => s + r.filter((x) => x >= 0).length, 0) +
|
|
'" uniqueCount="' + strings.length + '">';
|
|
for (const s of strings) ssXml += '<si><t>' + esc(s) + '</t></si>';
|
|
ssXml += '</sst>';
|
|
|
|
// ---- other parts ----
|
|
const ct = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' +
|
|
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>' +
|
|
'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' +
|
|
'</Types>';
|
|
|
|
const rels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
|
|
'</Relationships>';
|
|
|
|
const wb = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ' +
|
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' +
|
|
'<sheets><sheet name="Study Load" sheetId="1" r:id="rId1"/></sheets></workbook>';
|
|
|
|
const wbRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>' +
|
|
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>' +
|
|
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' +
|
|
'</Relationships>';
|
|
|
|
const xlRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
|
|
'</Relationships>';
|
|
|
|
const styles = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' +
|
|
'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>' +
|
|
'<fills count="1"><fill><patternFill patternType="none"/></fill></fills>' +
|
|
'<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>' +
|
|
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' +
|
|
'<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>' +
|
|
'<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>' +
|
|
'</styleSheet>';
|
|
|
|
const parts = [
|
|
{ name: '[Content_Types].xml', data: enc.encode(ct) },
|
|
{ name: '_rels/.rels', data: enc.encode(rels) },
|
|
{ name: 'xl/workbook.xml', data: enc.encode(wb) },
|
|
{ name: 'xl/_rels/workbook.xml.rels', data: enc.encode(wbRels) },
|
|
{ name: 'xl/worksheets/sheet1.xml', data: enc.encode(sheetXml) },
|
|
{ name: 'xl/sharedStrings.xml', data: enc.encode(ssXml) },
|
|
{ name: 'xl/styles.xml', data: enc.encode(styles) }
|
|
];
|
|
return zipStore(parts);
|
|
}
|
|
|
|
return { buildStudyLoadXlsx };
|
|
});
|