Fra Markdown til JSON
Denne teksten inngår i serien node.
Andre artikler i samme serie:
Det er enklere og mer oversiktlig å arbeide med strukturert data fremfor ustrukturert tekst. Derfor valgte jeg å konvertere Node-applikasjonen min fra å bruke Markdown til bransjestandarden JSON.
I forrige
Node-innlegg gjorde jeg meg kjent med fs-metoden i Node, for filbehandling. Jeg endte opp med fire filer til sammen:
- Ferdige oppgaver
- Backup av ferdige oppgaver
- Uferdige oppgaver
- Backup av uferdige oppgaver
Med JSON halverte jeg antallet ved å konvertere oppgavene fra enkle tekststrenger til JavaScript-objekter med utfyllende informasjon.
Så jeg gikk fra fire Markdown-filer som dette…:
- [ ] Lage app for bokregistrering
- [ ] Lage app for musikk-registrering
- [ ] Lage app for bok, musikk og CD-registrering
… til to JSON-filer som dette:
[
{
"createdDate": "2026-02-26",
"done": false,
"id": 1772116260687,
"task": "dette er en test2"
},
{
"createdDate": "2026-02-27",
"done": false,
"id": 1772192130897,
"task": "dette er en test7"
}
]
JSON
JavaScript Object Notation (JSON) er en metode for å transportere data som strukturert tekst, og brukes mye i web-applikasjoner som å sende informasjon mellom en server og en klient. Slik sett passer det perfekt for min lille to-do-app.
Det er et standardformat som er basert på JavaScript, men er i seg selv et språk-uavhengig format som de fleste programmeringsspråk støtter.
JSON bygger på to datastrukturer som er “nøkkelpar” og lister («arrayer»). Det er ganske enkelt sammenlignet med for eksempel XML, og resulterer i raskere overføringer og mindre filstørrelser.
Formatet ble opprinnelig spesifisert av Douglas Crockford tidlig på 2000-tallet, og støtter følgende datatyper:
- Tall
- Tekst (String)
- Bolske verdier («true/false»)
- Lister («arrays»)
- Objekter (nøkkel:verdi-par)
- Null (tom verdi)
JSON gir langt bedre fleksibilitet enn tekststrenger basert på indeks. Jeg kan lagre mer data per oppgave, og det blir mye enklere å filtrere mellom utførte og uferdige oppgaver i én og samme fil
Så første steg var å bestemme hva to-do-objektet skulle inneholde. Jeg landet på følgende:
{
"createdDate": new Date().toISOString().split("T")[0],
"id": Date.now(),
"task": task,
"done": false
}
createdDate: Dato for opprettelse. Jeg valgte å fjerne klokkeslettet med split, slik at jeg kun satt igjen med datoen.id: Unik ID basert på dagens dato ned til millisekundertask: Oppgavendone: Status
Jeg erfarte også at JSON er litt strengere med syntaksen. Jeg hadde nemlig komma bak siste nøkkelpar, og da feilet koden:
[
{
"createdDate": "2026-02-26",
"done": false,
"id": 1772116260687,
"task": "dette er en test2",
}
]
JSON støtter heller ikke kommentarer i koden.
En liste med objekter
En JSON-fil kan enten starte med et objekt eller en liste på toppnivå. Det betydde at jeg måtte legge opp til en ny fil tasks.json i koden, dersom den ikke allerede eksisterer. Og jeg måtte sørge for at den alltid starter med en liste [].
const fs = require("fs");
const tasksFileName = "tasks.json";
let taskObjects;
// Validate file
function validateFile() {
// create file if it doesnt exist
if (!fs.existsSync(`./${tasksFileName}`) || fs.readFileSync(tasksFileName, "utf8") === "") {fs.writeFileSync(tasksFileName, "[]")};
taskObjects = JSON.parse(fs.readFileSync(tasksFileName, "utf8"));
}
Jeg kunne kvitte meg med trim og split som jeg hadde tidligere, fordi Nodes innebygde JSON-metode JSON.parse fikser det for oss. Den konverterer tekst til objekter, og omvendt med JSON.stringify(). Disse metodene er for øvrig også innebygd i JavaScript, og til og med i nettleseren.
Her er et eksempel på hvor mye kode som kan fjernes ved å konvertere til JSON:
// ------ GAMMEL KODE ------- //
// Validate file
function validateFile(...files) {
files.forEach((file) => {
// create file if it doesnt exist
if (!fs.existsSync(`./${file}`)) {fs.writeFileSync(file, "")};
const readFile = fs.readFileSync(file, "utf8").trim("").split("\n");
// avoid empty line on top of document
if(readFile.length === 1 && readFile[0] == "") {readFile.length = 0};
switch(file) {
case tasksFileName:
fileContentTasks = readFile;
break;
case completedFileName:
fileContentCompleted = readFile;
break;
}
});
// ------ SLUTT GAMMEL KODE ------ //
// ------ NY KODE ...... //
function validateFile() {
// create file if it doesnt exist
if (!fs.existsSync(`./${tasksFileName}`)) {fs.writeFileSync(tasksFileName, "[]")};
taskObjects = JSON.parse(fs.readFileSync(tasksFileName, "utf8"));
Objekt-generator
Neste steg var å lage en funksjon som konstruerte objekter basert på innkommende oppgave via add-argumentet:
function taskObjectGenerator(task) {
const object = {
"createdDate": new Date().toISOString().split("T")[0],
"id": Date.now(),
"task": task,
"done": false
}
taskObjects.push(object);
writeFile();
};
Jeg oppdaterte også writeFile-funksjonen til å konvertere tilbake fra JSON-objekt til streng:
// write files
function writeFile() {
fs.writeFileSync(tasksFileName, JSON.stringify(taskObjects, null, 2));
}
Opprinnelig la jeg JSON.stringify i selve objekt-generatoren, før kallet på writefile:
// taskObjectGenerator
function taskObjectGenerator(task) {
const object = {
"id": Date.now(),
"task": task,
"done": false
}
taskObjects.push(object);
taskObjects = JSON.stringify(taskObjects, null, 2);
writeFile()
};
// write files
function writeFile() {
fs.writeFileSync(tasksFileName, taskObjects);
}
Det var en dårlig løsning, for hva om man trenger taskObjects som array senere i programmet? Det tryggeste er derfor å holde den som en array hele veien, helt til filen skal skrives. Jeg flyttet derfor JSON.stringify inn i writeFile-funksjonen som vist lenger opp.
Presentere oppgaver
Jeg fortsatte med listTasks-funkjsonen som også kunne forenkles en del. Også her fjernet jeg trim og split til fordel for filter.
// list tasks
function listTasks() {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
console.log("Oppgaver som venter:")
if (!unfinishedTasks.length) {
console.log("0")
} else {
unfinishedTasks.forEach(({id, task}, index) => {
console.log(${index + 1}. [ ] ${task});
})
}
console.log("\nOppgaver som er gjort:")
if (!finishedTasks.length) {
console.log("0")
} else {
finishedTasks.forEach(({id, task}, index) => {
console.log(${(index + 1) + unfinishedTasks.length}. [x] ${task});
})
}
}
Endre status på oppgaver
Så kom jeg til funksjonen for å endre status på en oppgave. Også denne kunne forkortes betraktelig ettersom jeg hadde gått over til objekter:
// check or uncheck task
function checkTask(status) {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
(status ? unfinishedTasks : finishedTasks).forEach((obj, index) => {
if ((status ? secondArg : (secondArg - unfinishedTasks.length)) == index + 1) {
obj.done = !obj.done;
}
});
console.log(`Marker oppgave ${secondArg} som ${status ? "ferdig" : "uferdig"}`);
writeFile();
}
INFO Det kan være nyttig å minne om referanser vs. verdier i JavaScript. Primitive verdier som tall, strenger og boolske verdier kopieres når de tilordnes en variabel. Det gjelder ikke objekter og lister. Vi får en referanse til minnet hvor originalen befinner seg. Så en variabel med en filtrert liste inneholder ingen “ekte” verdier selv, men kun henvisninger til originallisten.
Slette oppgaver
deleteTask-funksjonen kunne jeg også vesentlig forkorte, samtidig som jeg forbedret den.
Jeg opprettet to variabler for henholdsvis uferdige og fullførte oppgaver. Deretter brukte jeg spread og filter for å slå de sammen igjen. Dette var for å sikre riktig sortering: uferdige først, ferdige etterpå, slik at nummereringen stemmer overens med det listTasks viser.
// delete a task
function deleteTask() {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
taskObjects = [...unfinishedTasks, ...finishedTasks].filter((obj, index) => index + 1 != secondArg);
writeFile();
console.log(`Sletter oppgave ${secondArg}`)
return true;
}
Endelig resultat
Etter flere optimaliseringer og refaktoreringer satt jeg igjen med følgende kode:
"use strict"
const fs = require("fs");
const args = process.argv.slice(2);
const firstArg = args[0];
const secondArg = args[1];
const tasksFileName = "tasks.json";
let taskObjects;
// Check for valid argument
function checkForValidArgument() {
if (secondArg && secondArg.trim().length > 0) {
return true;
} else {
console.log("Må angi et argument")
return false;
}
}
// Validate file
function validateFile() {
// create file if it doesnt exist
if (!fs.existsSync(`./${tasksFileName}`)) {fs.writeFileSync(tasksFileName, "[]")};
taskObjects = JSON.parse(fs.readFileSync(tasksFileName, "utf8"));
}
// Validate tasks
function checkForValidTask() {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
if (isNaN(secondArg) || !Number.isInteger(Number(secondArg)) || secondArg.trim().length == 0) {
console.log("Må være et nummer uten desimaler")
return false;
}
if (args.length > 2) {
console.log("Angi kun ett tall uten mellomrom");
return false;
}
if (secondArg < 1 || secondArg > taskObjects.length) {
console.log("Nummer utenfor rekkevidde.")
return false;
}
return true;
}
// write files
function writeFile() {
fs.writeFileSync(tasksFileName, JSON.stringify(taskObjects, null, 2));
}
// create a backup
function backup() {
fs.copyFileSync(tasksFileName, `${tasksFileName}.bak`);
}
// list tasks
function listTasks() {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
console.log("Oppgaver som venter:")
if (!unfinishedTasks.length) {
console.log("0")
} else {
unfinishedTasks.forEach(({id, task}, index) => {
console.log(`${index + 1}. [ ] ${task}`);
})
}
console.log("\nOppgaver som er gjort:")
if (!finishedTasks.length) {
console.log("0")
} else {
finishedTasks.forEach(({id, task}, index) => {
console.log(`${(index + 1) + unfinishedTasks.length}. [x] ${task}`);
})
}
}
// check or uncheck task
function checkTask(status) {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
(status ? unfinishedTasks : finishedTasks).forEach((obj, index) => {
if ((status ? secondArg : (secondArg - unfinishedTasks.length)) == index + 1) {
obj.done = !obj.done;
}
});
console.log(`Marker oppgave ${secondArg} som ${status ? "ferdig" : "uferdig"}`);
writeFile();
}
// delete a task
function deleteTask() {
const unfinishedTasks = taskObjects.filter(obj => !obj.done);
const finishedTasks = taskObjects.filter(obj => obj.done);
taskObjects = [...unfinishedTasks, ...finishedTasks].filter((obj, index) => index + 1 != secondArg);
writeFile();
console.log(`Sletter oppgave ${secondArg}`)
return true;
}
function taskObjectGenerator(task) {
const object = {
"createdDate": new Date().toISOString().split("T")[0],
"done": false,
"id": Date.now(),
"task": task,
}
taskObjects.push(object);
writeFile();
};
switch(firstArg) {
case "add":
if ( !checkForValidArgument() ) { return };
validateFile();
backup();
const task = args.slice(1).join(" ");
taskObjectGenerator(task)
console.log("La til oppgave", task);
break;
case "check":
validateFile()
if ( !checkForValidTask() ) { return };
backup();
checkTask(true);
break;
case "uncheck":
validateFile()
if ( !checkForValidTask() ) { return };
backup();
checkTask(false);
break;
case "del":
validateFile()
if ( !checkForValidTask()) { return };
backup();
deleteTask();
break;
case "list":
validateFile()
listTasks();
break;
default:
console.log("Ugyldig kommando");
};
Neste steg på reisen blir å implementere Express.