aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordavidpkj <davidpenkow1@gmail.com>2024-04-23 21:45:37 +0200
committerdavidpkj <davidpenkow1@gmail.com>2024-04-23 21:45:37 +0200
commitc533d2c1d579906924237e41b6d71e1601deecae (patch)
tree667a9c455703672a4bd1ce282cda25d78a33412a
parentd09f40a732d65c222aefb2c9df4ddad3d0b66ed7 (diff)
mega commit: ics, readme updates, easier usage
-rw-r--r--.gitignore1
-rw-r--r--README.adoc33
-rw-r--r--package-lock.json70
-rw-r--r--package.json4
-rw-r--r--public/config.yaml10
-rw-r--r--src/files.js27
-rw-r--r--src/ics.js62
-rw-r--r--src/main.js101
-rw-r--r--src/pdf.js96
9 files changed, 295 insertions, 109 deletions
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 `<THIS>` 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 `<THIS>` 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: <YOUR NAME>
-semester: <THE SEMESTER>
-filename: <THE RESULT FILENAME> # i.e. Stundenplan_SS24.pdf
+semester: <SEMESTER>
+filename: <RESULT FILENAME> # i.e. Stundenplan_SS24
+
+# Is only needed for ICS export
+vorlesungszeit:
+ - <LECTURES START>
+ - <LECTURES END>
# 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 = `<style>${readStyle()}</style>`; \ No newline at end of file
+c.style = `<style>${readStyle()}</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 = "<ul>";
-for (let hinweis of c.hinweise) {
- hinweise += `<li>${hinweis}</li>`;
-}
-hinweise += `</ul>`;
-
-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 += `<tr class="${mensa}">` + 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 += `<td rowspan="${el.runtime}" class="${styleclass}"></td>`;
- } else {
- let name = el.name == "BUFFER" ? "" : `<div><span>${el.name}</span>`;
- let raum = el.raum == "BUFFER" ? "" : `<span>${el.raum}</span>`;
-
- res += `<td rowspan="${el.runtime}">${name}${raum}</td>`;
- }
-
- timeout[day] = el.runtime - 1;
- tage_runtimes[day].splice(0, 1);
- }
-
- res += "</tr>"
- }
-
- return res
+ return runtimes(tage, tage_runtimes)
}
-
-let html = `
-${c.style}
-<p>Persönlicher Stundenplan von ${c.student} für das ${c.semester}. Stand: ${date}.</p>
-<table>
- <tr>
- <th>Uhrzeit</th>
- <th class="tag">Montag</th>
- <th class="tag">Dienstag</th>
- <th class="tag">Mittwoch</th>
- <th class="tag">Donnerstag</th>
- <th class="tag">Freitag</th>
- </tr>
- ${main()}
-</table>
-<br>
-<em>Hinweise:</em>
-${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 = "<ul>";
+for (let hinweis of c.hinweise) {
+ hinweise += `<li>${hinweis}</li>`;
+}
+hinweise += `</ul>`;
+
+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 += `<tr class="${mensa}">` + 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 += `<td rowspan="${el.runtime}" class="${styleclass}"></td>`;
+ } else {
+ let name = el.name == "BUFFER" ? "" : `<div><span>${el.name}</span>`;
+ let raum = el.raum == "BUFFER" ? "" : `<span>${el.raum}</span>`;
+
+ res += `<td rowspan="${el.runtime}">${name}${raum}</td>`;
+ }
+
+ timeout[day] = el.runtime - 1;
+ tage_runtimes[day].splice(0, 1);
+ }
+
+ res += "</tr>"
+ }
+
+ return res
+}
+
+function generatePDF() {
+ let html = `
+ ${c.style}
+ <p>Persönlicher Stundenplan von ${c.student} für das ${c.semester}. Stand: ${date}.</p>
+ <table>
+ <tr>
+ <th>Uhrzeit</th>
+ <th class="tag">Montag</th>
+ <th class="tag">Dienstag</th>
+ <th class="tag">Mittwoch</th>
+ <th class="tag">Donnerstag</th>
+ <th class="tag">Freitag</th>
+ </tr>
+ ${format()}
+ </table>
+ <br>
+ <em>Hinweise:</em>
+ ${hinweise}
+ `;
+
+ html_to_pdf.generatePdf({content: html}, options).then(pdfBuffer => {writePDF(pdfBuffer)});
+}
+
+generatePDF()