From c533d2c1d579906924237e41b6d71e1601deecae Mon Sep 17 00:00:00 2001 From: davidpkj Date: Tue, 23 Apr 2024 21:45:37 +0200 Subject: mega commit: ics, readme updates, easier usage --- .gitignore | 1 + README.adoc | 33 +++++++++++++---- package-lock.json | 70 +++++++++++++++++++++++++++++++++++++ package.json | 4 ++- public/config.yaml | 10 +++--- src/files.js | 27 +++++++++++--- src/ics.js | 62 ++++++++++++++++++++++++++++++++ src/main.js | 101 ++++------------------------------------------------- src/pdf.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 295 insertions(+), 109 deletions(-) create mode 100644 src/ics.js create mode 100644 src/pdf.js diff --git a/.gitignore b/.gitignore index e373c23..9c7c6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ *.xml +*.ics *.pdf diff --git a/README.adoc b/README.adoc index aff7c15..e1bd72c 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,10 @@ = Stundenplan -A small JavaScript project to generate beautiful PDF schedules. Takes information from `public/config.yaml`. +A small JavaScript project to generate beautiful PDF schedules and actually working Calendar Files. Takes information from `public/config.yaml`. + +"But why would anyone want this? UnivIS already does that!" + +In short: This does it better. You have precise control over how the end product looks, what it displays (on a very granular level) and technically even what format it should have. Though you would need to edit the source at `src/pdf.js` for that. == Usage @@ -8,7 +12,15 @@ Requires the `npm` tool. ```bash npm i # installes packages -npm run main # runs the software +npm run pdf # runs the software and generates a .pdf +``` + +=== Optional: Generate ICS file + +You have the option to generate a calendar file, which you should be able to import into most calendar software. Works with moodle. + +```bash +npm run ics # runs the software and generates a .ics ``` === Optional: Parse UnivIS XML @@ -25,15 +37,20 @@ npm run parse Generally, you may edit anything in the `public` directory. The default are how I like it. -To configure the actual displayed information you shoudl edit the `.yaml` file. It should look something like the following, where things like `` should be replaced by your text. +To configure the actual displayed information you shoudl edit the `.yaml` file. It should look something like the following, where things like `` should be replaced by your text. An example is preconfigured at `public/config.yaml`. Comments (things that are ignored by the software) is everything after a `#` sign. More information on the file format and syntax can be found https://yaml.org/[here]. ```yaml # General Information student: -semester: -filename: # i.e. Stundenplan_SS24.pdf +semester: +filename: # i.e. Stundenplan_SS24 + +# Is only needed for ICS export +vorlesungszeit: + - + - # Each of these lines will be listed at the bottom of the PDF hinweise: @@ -50,6 +67,10 @@ eintraege: .Please note the following exceptions: ==== * `START TIME` and `END TIME` should be given in the format `HHhMM` so to say "1:02 PM", you would actually write `13h02`. +* `LECTURES START` and `LECTURES END` are given in the format `YYYY-MM-DD` so to say "Apr. 23rd, 2024", you would actually write `2024-04-23`. +* `LECTURES START` has to be a (the first) Monday. +* `LECTURES END` has to be a the day after the last day of lectures. +* `RESULT FILENAME` does not need an extension, as that is determined upon file creation. * `WEEKDAY` should be given in abbreviated form without a dot. I.e. "Wednesday" becomes `Mi`. * The Module `STYLECLASS` is reserved for special stylized cases. @@ -73,5 +94,5 @@ This is then picked up by `public/style.css` (were I have defined what `.block` - Have to use the exact yaml structure - Margin on the left not easily configurable - Mensa times are not actually displayed -- All config.yaml items are required +- Most config.yaml items are required (no defaults) - Code is german spaghetti diff --git a/package-lock.json b/package-lock.json index d3e4a15..fdf1db1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "html-pdf-node": "^1.0.8", + "ics": "^3.7.2", "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "jspdf": "^2.5.1", @@ -705,6 +706,16 @@ "node": ">=0.10.0" } }, + "node_modules/ics": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/ics/-/ics-3.7.2.tgz", + "integrity": "sha512-UC5bBJYKyzkYZv4/AoIV9dsZw+rwFkUOtHHhnxmb0HTUEpfDmfl5sB9DlePKrTxx6SGMXiIQbiElf66viSTB0A==", + "dependencies": { + "nanoid": "^3.1.23", + "runes2": "^1.1.2", + "yup": "^1.2.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -969,6 +980,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -1101,6 +1129,11 @@ "node": ">=0.4.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -1285,6 +1318,11 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" }, + "node_modules/runes2": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", + "integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1430,6 +1468,16 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -1455,6 +1503,17 @@ "node": ">=18" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -1625,6 +1684,17 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/package.json b/package.json index 7bbb1a8..86704f7 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,15 @@ "type": "module", "dependencies": { "html-pdf-node": "^1.0.8", + "ics": "^3.7.2", "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "jspdf": "^2.5.1", "xml-js": "^1.6.11" }, "scripts": { - "main": "node src/main.js", + "pdf": "node src/pdf.js", + "ics": "node src/ics.js", "parse": "node src/univis.js" }, "author": "David Penkowoj", diff --git a/public/config.yaml b/public/config.yaml index 8285dcb..92f78e1 100644 --- a/public/config.yaml +++ b/public/config.yaml @@ -1,7 +1,12 @@ # Allgemeine Informationen student: David Penkowoj semester: Sommersemester 2024 -filename: Stundenplan_SS24.pdf +filename: Stundenplan_SS24 + +# Wird nur für den ICS export benötigt +vorlesungszeit: + - "2024-04-08" + - "2024-07-27" # Jeder hinweis wird unter dem Stundenplan aufgelistet hinweise: @@ -15,7 +20,6 @@ eintraege: - STYLECLASS: BLOCK: - [Mo, 13h00, 19h00, ""] - - [Do, 10h00, 15h00, ""] MENSA: - [Di, 12h00, 12h30, ""] - [Di, 14h00, 14h30, ""] @@ -27,8 +31,6 @@ eintraege: - [Di, 08h45, 09h45, H 1] U: - [Fr, 12h00, 13h00, AM S4] - V: - - [Do, 08h30, 10h00, Z 1/2] - TGI 1: V: - [Mi, 08h15, 09h45, AM 1] diff --git a/src/files.js b/src/files.js index 970d28d..e5ac76a 100644 --- a/src/files.js +++ b/src/files.js @@ -3,18 +3,37 @@ import * as fs from "fs" const dir = "./public"; +function read(file) { + console.log(`Reading ${file}`); + return fs.readFileSync(file, 'utf8'); +} + +function write(file, content) { + let r = fs.writeFileSync(file, content); + console.log("Done!"); + return r; +} + function readConfig() { - return yml.load(fs.readFileSync(`${dir}/config.yaml`, 'utf8')); + return yml.load(read(`${dir}/config.yaml`)); } function readStyle() { - return fs.readFileSync(`${dir}/style.css`, 'utf8'); + return read(`${dir}/style.css`); +} + +export function writePDF(buffer) { + return write(c.filename + ".pdf", buffer) +} + +export function writeICS(ics) { + return write(c.filename + ".ics", ics); } export function readXML() { - return fs.readFileSync(`${dir}/data.xml`, 'utf8'); + return read(`${dir}/data.xml`); } export const c = readConfig(); -c.style = ``; \ No newline at end of file +c.style = ``; diff --git a/src/ics.js b/src/ics.js new file mode 100644 index 0000000..ebf6480 --- /dev/null +++ b/src/ics.js @@ -0,0 +1,62 @@ +import * as ics from "ics" + +import { prepare } from "./main.js" +import { c, writeICS } from "./files.js" + +const daymap = { + "Mo": "MO", + "Di": "TU", + "Mi": "WE", + "Do": "TH", + "Fr": "FR", +} + +let start = new Date(c.vorlesungszeit[0]) +let end = new Date(c.vorlesungszeit[1]) + +function getDateList(date, time) { + let year = date.getFullYear() - 0 + let month = date.getMonth() - 0 + let day = date.getDate() - 0 + + let timelist = time.split(":") + + let hour = timelist[0] - 0 + let minute = timelist[1] - 0 + + return [year, month, day, hour, minute] +} + +function generateICS() { + let tage = prepare(true); + let veranstaltungen = [] + + for (let tag in tage) { + for (let eintrag of tage[tag]) { + let until = end.toISOString().replace(/-|\.|:/g, "").replace("000000000", "000000"); + let day = daymap[tag]; + + if (!eintrag.name.includes("STYLECLASS")) { + veranstaltungen.push({ + title: eintrag.name, + location: eintrag.raum, + start: getDateList(start, eintrag.von), + end: getDateList(start, eintrag.bis), // yes this is correct + recurrenceRule: `FREQ=WEEKLY;INTERVAL=1;BYDAY=${day};UNTIL=${until}` + }) + } + } + + start.setDate(start.getDate() + 1); + } + + const { error, value } = ics.createEvents(veranstaltungen) + + if (error) { + console.log(error) + } else { + writeICS(value) + } +} + +generateICS() diff --git a/src/main.js b/src/main.js index 31183b2..4792319 100644 --- a/src/main.js +++ b/src/main.js @@ -1,22 +1,5 @@ -import * as fs from "fs" -import * as html_to_pdf from "html-pdf-node" - import { c } from './files.js' -let hinweise = "
    "; -for (let hinweis of c.hinweise) { - hinweise += `
  • ${hinweis}
  • `; -} -hinweise += `
`; - -let date = new Date(); -date = date.toLocaleDateString("de-DE", { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', -}); - const ctage = { "Mo": [], "Di": [], @@ -25,24 +8,13 @@ const ctage = { "Fr": [] } -const options = { - format: 'A4', - landscape: true, - margin: { - top: "0.5cm", - right: "1cm", - bottom: "1cm", - left: "2cm", - }, -}; - // returns time difference in 15-minute intervalls function timediff(a, b) { const time = "1970-01-01 "; return (new Date(time + a) - new Date(time + b)) / 1000 / 60 / 15; } -function gettimefromintervalls(x) { +export function gettimefromintervalls(x) { let y = 15 * x / 60 + 8 let z = y.toString().padStart(2, '0'); @@ -127,9 +99,7 @@ function bufferUp(tage) { return tage; } -function main() { - let res = ""; - +export function prepare(forICS = false) { let tage = structuredClone(ctage); let tage_runtimes = structuredClone(ctage); @@ -149,69 +119,12 @@ function main() { } } + if (forICS) { + return tage = sortTage(tage); + } + tage = bufferUp(tage, ctage); tage = sortTage(tage); - tage_runtimes = runtimes(tage, tage_runtimes) - - let timeout = {} - - // 1h has 4 15 minute intervalls: 08 to 19 means 44 intervalls - for (let i = 0; i < 44; i++) { - let mensa = ""; - - if (11 < i && i < 25) mensa = "mensa1"; - if (25 < i && i < 27) mensa = "mensa2"; - - res += `` + gettimefromintervalls(i) - for (let day in tage_runtimes) { - if (timeout[day] > 0) { - timeout[day]--; - continue; - } - - let el = tage_runtimes[day][0]; - - if (el.name.includes("STYLECLASS")) { - let styleclass = el.name.split("/")[1].trim().toLowerCase(); - res += ``; - } else { - let name = el.name == "BUFFER" ? "" : `
${el.name}`; - let raum = el.raum == "BUFFER" ? "" : `${el.raum}`; - - res += `${name}${raum}`; - } - - timeout[day] = el.runtime - 1; - tage_runtimes[day].splice(0, 1); - } - - res += "" - } - - return res + return runtimes(tage, tage_runtimes) } - -let html = ` -${c.style} -

Persönlicher Stundenplan von ${c.student} für das ${c.semester}. Stand: ${date}.

- - - - - - - - - - ${main()} -
UhrzeitMontagDienstagMittwochDonnerstagFreitag
-
-Hinweise: -${hinweise} -`; - -html_to_pdf.generatePdf({content: html}, options).then(pdfBuffer => { - // fs.writeFileSync("test.html", html) - fs.writeFileSync(c.filename, pdfBuffer); -}); diff --git a/src/pdf.js b/src/pdf.js new file mode 100644 index 0000000..a943781 --- /dev/null +++ b/src/pdf.js @@ -0,0 +1,96 @@ +import * as html_to_pdf from "html-pdf-node" + +import { prepare, gettimefromintervalls } from "./main.js" +import { c, writePDF } from './files.js' + +let date = new Date(); +date = date.toLocaleDateString("de-DE", { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', +}); + +let hinweise = "
    "; +for (let hinweis of c.hinweise) { + hinweise += `
  • ${hinweis}
  • `; +} +hinweise += `
`; + +const options = { + format: 'A4', + landscape: true, + margin: { + top: "0.5cm", + right: "1cm", + bottom: "1cm", + left: "2cm", + }, +}; + +function format() { + let tage_runtimes = prepare() + let timeout = {} + let res = ""; + + // 1h has 4 15 minute intervalls: 08 to 19 means 44 intervalls + for (let i = 0; i < 44; i++) { + let mensa = ""; + + if (11 < i && i < 25) mensa = "mensa1"; + if (25 < i && i < 27) mensa = "mensa2"; + + res += `` + gettimefromintervalls(i) + + for (let day in tage_runtimes) { + if (timeout[day] > 0) { + timeout[day]--; + continue; + } + + let el = tage_runtimes[day][0]; + + if (el.name.includes("STYLECLASS")) { + let styleclass = el.name.split("/")[1].trim().toLowerCase(); + res += ``; + } else { + let name = el.name == "BUFFER" ? "" : `
${el.name}`; + let raum = el.raum == "BUFFER" ? "" : `${el.raum}`; + + res += `${name}${raum}`; + } + + timeout[day] = el.runtime - 1; + tage_runtimes[day].splice(0, 1); + } + + res += "" + } + + return res +} + +function generatePDF() { + let html = ` + ${c.style} +

Persönlicher Stundenplan von ${c.student} für das ${c.semester}. Stand: ${date}.

+ + + + + + + + + + ${format()} +
UhrzeitMontagDienstagMittwochDonnerstagFreitag
+
+ Hinweise: + ${hinweise} + `; + + html_to_pdf.generatePdf({content: html}, options).then(pdfBuffer => {writePDF(pdfBuffer)}); +} + +generatePDF() -- cgit v1.2.3