diff --git a/package.json b/package.json index cce5337..2f8fb39 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ }, "scripts": { "start": "node app.js" + }, + "devDependencies": { + "moment": "^2.29.4" } } diff --git a/parse_orgmode_to_json.mjs b/parse_orgmode_to_json.mjs index a858317..1329881 100644 --- a/parse_orgmode_to_json.mjs +++ b/parse_orgmode_to_json.mjs @@ -2,20 +2,30 @@ * convertir un fichier .org vers des données structurées en json * @type {*} */ -// const fileToParse = process.argv[0] import fs from 'node-fs'; -const sourceFileName = 'demo.org' -const sourceFilePath = './sources/'+sourceFileName; +import moment from 'moment'; + + +/********************** + * initialize configs + **********************/ + +const sourceFileName = 'demo_more.org' +const sourceFilePath = './sources/' + sourceFileName; + +let headers = [] +let tasksObjectsForJsonExport = [] +let headersByKind = {} +let writeJsonAfterParse = false; + +/************************************************************** + * fetch the source orgmode file to read its contents + *************************************************************/ console.log('parse some org file', sourceFilePath) if (!sourceFilePath) { console.error('pas de fichier à ouvrir') } -let headers = [] -let tasksObjectsForJsonExport = [] -let headersByKind = {} - - fs.stat(sourceFilePath, function (err, stat) { if (err == null) { console.log(`File ${sourceFilePath} exists`); @@ -28,26 +38,41 @@ fs.stat(sourceFilePath, function (err, stat) { } }); +/********************** + * search elements + *********************/ +let stateKeywordList = ['SOMEDAY', 'NEXT', 'TODO', 'CANCELLED', 'DONE', 'WAITING']; +let dateKeywordList = ['CREATED', 'SCHEDULED', 'DEADLINE', 'CLOSED','Refiled']; +let sectionKeywordList = ['PROPERTIES', 'LOGBOOK', 'END']; +let propertiesSection = {} +let logBookSection = {} -let keyword = 'SOMEDAY'; -let keywordList = ['SOMEDAY', 'NEXT','TODO', 'CANCELLED','DONE', 'WAITING']; - +let headerKeywordSearch = '[' + stateKeywordList.join('|') + ']' +/** + * task object example + * @type {{level: string, header: string, dates: {CREATED: string, DONE: string, REFILED: string}, state: string, content: string, properties: {}, tags: [], tagsInherited: []}} + */ let task = { - header : "", - level : "", - content : "", - state : "", - tags : [], - tagsInherited : [], - dates : { - 'CREATED':'', - 'REFILED':'', - 'DONE':'', - }, - properties : {}, + header: "", + level: "", + content: "", + state: "", + tags: [], + tagsInherited: [], + dates: {}, + logbook: {}, + properties: {}, } -let currentTask = Object.create(task); +// init first task object as empty clone +let currentTask = {...task}; +let isHeader = false; +let isProperty = false; +let isLogbook = false; +let isFirst = true; +/********************** + * loop to parse all + *********************/ fs.readFile(sourceFilePath, 'utf8', function (err, data) { if (err) { return console.log(err); @@ -61,54 +86,125 @@ fs.readFile(sourceFilePath, 'utf8', function (err, data) { everyline.forEach((line) => { + // gérer la création d'objets définissant les tâches et leurs propriétés if (line.match(/^\*+? /)) { // add last task to export list - tasksObjectsForJsonExport.push(currentTask) + if (!isFirst) { + tasksObjectsForJsonExport.push(currentTask) + + console.log('currentTask.dates', currentTask.dates) + currentTask = {...task}; + } else { + isFirst = false; + } + + isHeader = true; + // compter les étoiles pour trouver le niveau du header + currentTask.level = line.match(/\*/g)?.length + // create a new task - currentTask = Object.create(task); + + + line = line.replace('*', '') + line = line.replace(stateKeywordList, [].fill('', 0, stateKeywordList.length)) + headers.push(line) currentTask.header = line; - keywordList.forEach(keyword => { - lookForKeywordInLine(line, keyword) + stateKeywordList.forEach(keyword => { + let keywordIsFound = lineHasKeyword(line, keyword) - if(line.indexOf('* '+keyword) !== -1){ + if (keywordIsFound) { currentTask.state = keyword } - // compter les étoiles pour trouver le niveau du header - currentTask.level = line.match(/\*/g).length - let tagsFound = line.match(/\:(.*)\:/g) - console.log('tagsFound', tagsFound) - if(tagsFound){ - tagsFound = tagsFound[0]; - console.log('tagsFound', tagsFound) - tagsFound = tagsFound.split(':').filter(item => item.length ) - currentTask.tags = tagsFound; - } - }) - // TODO gérer la création d'objets définissant les tâches et leurs propriétés + + // trouver les tags + let tagsFound = line.match(/\:(.*)\:/g) + if (tagsFound) { + tagsFound = tagsFound[0]; + console.log('tagsFound', tagsFound) + tagsFound = tagsFound.split(':').filter(item => item.length) + currentTask.tags = tagsFound; + } + + + // fin des recherches dans la ligne de Header + } else { + isHeader = false; } + // examen des lignes de corps de tâche, ou de corps de section suite au header. + + // classer les dates de création, cloture, et de logbook + let dateFound = searchDate(line) + if(dateFound){ + + dateKeywordList.forEach(keyword => { + if (lineHasSubstring(line, keyword)) { + if (!currentTask.dates[keyword]) { + currentTask.dates[keyword] = ''; + } + currentTask.dates[keyword] = new Date(dateFound[0]); + } else { + // console.log('keyword', keyword) + } + }) + } + + // ajouter le corps complet de la section après le header + if (line.length && !isHeader) { + + let cleanedLine = line.replace(/\s\s/g, ' ') + cleanedLine = line.replace(/ {2,}/g, ' ') + console.log('line', cleanedLine) + currentTask.corpus += ` + ` + cleanedLine; + } + }) + // ajouter la dernière tâche parsée + tasksObjectsForJsonExport.push(currentTask) + console.log('headers', headers) console.log(" parsing fini") - console.log('nombre de lignes', everyline.length) - console.log('nombre de headers', headers.length) - keywordList.forEach(keyword => console.log('nombre de headers',keyword, headersByKind[keyword]?.length)) + stateKeywordList.forEach(keyword => console.log('nombre de headers', keyword, headersByKind[keyword]?.length)) - console.log('tasksObjectsForJsonExport', tasksObjectsForJsonExport) - // writeJsonFile('export_'+sourceFileName +'.json' , JSON.stringify(tasksObjectsForJsonExport)); + + const jsonContent = { + statistics: { + lines_count: everyline.length, + headers_count: headers.length, + }, + meta_data: { + author: '@tykayn@mastodon.Cipherbliss.com', + generated_at: new Date(), + generated_from_file: sourceFilePath + sourceFileName, + sources: 'https://forge.chapril.org/tykayn/org-report-stats.git' + }, + tasks_list: tasksObjectsForJsonExport + } + // console.log('tasksObjectsForJsonExport', jsonContent) + + if (writeJsonAfterParse) { + writeJsonFile('export_' + sourceFileName + '.json', JSON.stringify(jsonContent)); + } return; }) -function lookForKeywordInLine(line, keyword='TODO') { +function lineHasKeyword(line, keyword = 'TODO') { - - if ( line.indexOf('* '+keyword) !== -1) { - createNewHeaderKind(keyword) + let isFound = (line.indexOf('* ' + keyword) !== -1) + if (isFound) { + createNewHeaderKind(keyword) headersByKind[keyword].push(line); } + return isFound; +} + +function lineHasSubstring(line, keyword) { + + return (line.indexOf(keyword) !== -1) } function createNewHeaderKind(keyword) { @@ -117,9 +213,45 @@ function createNewHeaderKind(keyword) { } } +/** + * chercher des dates et heures au format + * YYYY-MM-DD HH:II:SS + * + * @param line + * @returns {*} + */ +function searchDate(line) { + // return line.match(/[(\d{4}\-\d{2}\-\d{2} ?\d{2}?\:?\d{2}?\:?\d{2}?)(\d{4}\-\d{2}\-\d{2})]/) + let simpleDay = line.match(/\d{4}\-\d{2}\-\d{2} \w{3}?\.?/) + let simpleDayHour = line.match(/\d{4}\-\d{2}\-\d{2} \w{3}?\.? \d{2}\:\d{2}/) + let simpleDayHourSec = line.match(/\d{4}\-\d{2}\-\d{2} \w{3}?\.? \d{2}\:\d{2}\:\d{2}/) + + if(simpleDayHourSec){ + return simpleDayHourSec; + } + + if(simpleDayHour){ + return simpleDayHour; + } + if(simpleDay){ + return simpleDay; + } + +} + +/** + * afin de trouver la première date liée à une tâche parmi celles mentionnées, il faut comparer les dates + * @param date1 + * @param date2 + */ +function compareDatesAndKeepOldest(date1, date2) { + date1 = moment(date1) + date2 = moment(date2) +} function writeJsonFile(fileName, fileContent) { console.log('write file ', fileName); + return fs.writeFile( `./output/${fileName}`, fileContent, diff --git a/sources/demo.org b/sources/demo.org index 4a357c4..9c79fe1 100644 --- a/sources/demo.org +++ b/sources/demo.org @@ -1,10 +1,4 @@ -* coucou la démo de fichier org -* voilà un 2e header * TODO faire une démo -* NEXT écrire le fichier de démo [1/3] :demo: -** SOMEDAY oh un sous header, niveau 2 :subtil: -** CANCELLED un truc pas fait au final -** DONE et voilà ça c'est fait :projet_fini:ARCHIVE: -CLOSED : [2023-03-04 15:00] -* pas mal hein -c'est un texte de description \ No newline at end of file +DEADLINE: <2023-02-28 mar. 12:30> +** NEXT écrire le fichier de démo [1/3] :demo: +CREATED: <2023-02-28 mar.> \ No newline at end of file diff --git a/sources/demo_more.org b/sources/demo_more.org new file mode 100644 index 0000000..29a0b7f --- /dev/null +++ b/sources/demo_more.org @@ -0,0 +1,23 @@ +* coucou la démo de fichier org +* voilà un 2e header +* TODO faire une démo +DEADLINE: <2023-02-28 mar.> +* NEXT écrire le fichier de démo [1/3] :demo: +CREATED: <2023-02-28 mar.> +SCHEDULED: <2023-02-28 mar.> +DEADLINE: <2023-02-28 mar.> +** SOMEDAY oh un sous header, niveau 2 avec logbook et propriétés :subtil: +CREATED: <2023-02-28 mar.> + +:PROPERTIES: +:CREATED: [2023-02-28 11:36:23] + :END: + :LOGBOOK: + - State "TODO" from "SOMEDAY" [2023-02-28 mar. 12:12] + - Refiled on [2023-02-28 mar. 11:50] + :END: +** CANCELLED un truc pas fait au final +** DONE et voilà ça c'est fait :projet_fini:ARCHIVE: +CLOSED : [2023-03-04 15:00] +* pas mal hein +c'est un texte de description \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 58fb375..55e1197 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1052,6 +1052,13 @@ __metadata: languageName: node linkType: hard +"moment@npm:^2.29.4": + version: 2.29.4 + resolution: "moment@npm:2.29.4" + checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -1268,6 +1275,7 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: d3: ^7.8.2 + moment: ^2.29.4 node-fs: ^0.1.7 nodemon: ^2.0.19 languageName: unknown