Files
2021-12-21 01:32:37 +11:00

261 lines
9.6 KiB
JavaScript

import { gid, leftPad } from "./";
export function extractTodos(text) {
const headlines = parse(text);
const todos = [];
for (let i=0; i < headlines.length; i++) {
const todo = formatTodo(headlines[i]);
if (todo.status) {
todos.push(todo);
}
}
return todos
.sort((a, b) => {
if (a.status === "NEXT" && b.status !== "NEXT") return -1;
else if (b.status === "NEXT" && a.status !== "NEXT") return +1;
else if (a.status === "TODO" && b.status !== "TODO") return -1;
else if (b.status === "TODO" && a.status !== "TODO") return +1;
else if (a.status === "DONE" && b.status !== "DONE" &&
b.todo_status === "done") return -1;
else if (b.status === "DONE" && a.status !== "DONE" &&
a.todo_status === "done") return +1;
else if (a.todo_status === "todo" && b.todo_status !== "todo") return -1;
else if (a.todo_status === "done" && b.todo_status !== "done") return +1;
else if (a.priority !== null && b.priority === null) return -1;
else if (a.priority === null && b.priority !== null) return +1;
else if (a.priority !== null && b.priority !== null &&
a.priority !== b.priority) return a.priority > b.priority? +1 : -1;
else if (a.is_overdue === true && b.is_overdue === false) return -1;
else if (a.is_overdue === false && b.is_overdue === true) return +1;
else if (a.status === b.status) return a.id < b.id ? -1 : +1;
});
function formatTodo(thing) {
const todo_status = ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(
thing.header.todo_keyword,
) !== -1 ? "todo" : "done";
return {
key: thing.header.todo_keyword,
id: thing.id,
line: thing.header.line,
title: thing.header.title,
status: thing.header.todo_keyword,
todo_status: todo_status,
is_overdue: _is_overdue(todo_status, thing.timestamps),
priority: thing.header.priority,
scheduled: _find_scheduled(thing.timestamps),
deadline: _find_deadline(thing.timestamps),
tasks: thing.subtasks,
tags: thing.header.tags,
};
}
}
export function extractEvents(text) {
const headlines = parse(text);
let events = [];
for (let i=0; i < headlines.length; i++) {
events = events.concat(
formatEvents(headlines[i]),
);
}
return events.sort((a, b) => a.date - b.date);
function formatEvents(thing) {
const events = [];
for (let i=0; i < thing.timestamps.length; i++) {
const timestamp = thing.timestamps[i];
if (timestamp.active === false) continue;
const todo_status = function(keyword) {
if (!keyword) return null;
return ["TODO", "NEXT", "DOING", "WAITING", "PENDING"].indexOf(
keyword,
) !== -1 ? "todo" : "done";
}(thing.header.todo_keyword);
const event = {
id: thing.id,
line: thing.header.line,
title: thing.header.title,
status: thing.header.todo_keyword,
todo_status: todo_status,
scheduled: _find_scheduled(thing.timestamps),
deadline: _find_deadline(thing.timestamps),
is_overdue: _is_overdue(todo_status, thing.timestamps),
priority: thing.header.priority,
tasks: [],
tags: thing.header.tags,
};
if (event.todo_status === "done") continue;
event.date = new Date(timestamp.timestamp);
const today = new Date();
today.setHours(23);
today.setMinutes(59);
today.setSeconds(59);
today.setMilliseconds(999);
if (event.date < today) {
event.date = today;
}
event.key = Intl.DateTimeFormat().format(event.date);
event.date = event.date.toISOString();
if (timestamp.repeat) {
if (timestamp.repeat.interval === "m") {
events.push(event);
} else {
if (timestamp.repeat.interval === "y") {
timestamp.repeat.n *= 365;
} else if (timestamp.repeat.interval === "w") {
timestamp.repeat.n *= 7;
}
const n_days = timestamp.repeat.n;
const today = _normalise(new Date());
for (let j=0; j<30; j++) {
if (((today - _normalise(new Date(timestamp.timestamp))) / 1000*60*60*24) % n_days === 0) {/* eslint-disable-line max-len */
event.date = today.getTime();
event.key = Intl.DateTimeFormat().format(today);
events.push(JSON.parse(JSON.stringify((event))));
}
today.setDate(today.getDate() + 1);
}
}
} else {
events.push(event);
}
}
return events;
}
}
export function parse(content) {
const todos = [];
let todo = reset(0);
let data;
let text;
let tags = [];
const lines = content.split("\n");
for (let i = 0; i<lines.length; i++) {
text = lines[i];
if (data = parse_header(text, i)) {
tags = tags.filter((e) => e.level < data.level);
tags.push({ level: data.level, tags: data.tags });
data.tags = tags.reduce((acc, el) => {
return acc.concat(el.tags);
}, []);
if (todo.header) {
todos.push(todo);
todo = reset(i);
}
todo.header = data;
} else if (data = parse_timestamp(text, i)) {
todo.timestamps = todo.timestamps.concat(data);
} else if (data = parse_subtask(text, i)) {
todo.subtasks.push(data);
}
if (i === lines.length - 1 && todo.header) {
todos.push(todo);
}
}
return todos;
function reset(i) {
return { id: leftPad(i.toString(), 5) + gid(i), timestamps: [], subtasks: [] };
}
}
function parse_header(text, line) {
const match = text.match(/^(\*+)\s(?:([A-Z]{4,})\s){0,1}(?:\[\#([A-C])\]\s){0,1}(.*?)(?:\s+\:((?:[a-z]+\:){1,})){0,1}$/); /* eslint-disable-line max-len */
if (!match) return null;
return {
line: line,
level: RegExp.$1.length,
todo_keyword: RegExp.$2 || null,
priority: RegExp.$3 || null,
title: RegExp.$4 || "Empty Heading",
tags: RegExp.$5
.replace(/:/g, " ")
.trim()
.split(" ")
.filter((e) => e),
};
}
function parse_subtask(text, line) {
const match = text.match(/(?:-|\d+[\.\)])\s\[([X\s-])\]\s(.*)/);
if (!match) return null;
return {
line: line,
status: function(state) {
if (state === "X") return "DONE";
else if (state === " ") return "TODO";
return null;
}(match[1]),
title: match[2] || "Empty task",
};
}
function parse_timestamp(text, line, _memory) {
const reg = /(?:([A-Z]+)\:\s){0,1}([<\[])(\d{4}-\d{2}-\d{2})[^>](?:[A-Z][a-z]{1,2})(?:\s([0-9]{2}\:[0-9]{2})){0,1}(?:\-([0-9]{2}\:[0-9]{2})){0,1}(?:\s(\+{1,2}[0-9]+[dwmy])){0,1}[\>\]](?:--[<\[](\d{4}-\d{2}-\d{2})\s[A-Z][a-z]{1,2}\s(\d{2}:\d{2}){0,1}[>\]]){0,1}/; /* eslint-disable-line max-len */
const match = text.match(reg);
if (!match) return _memory || null;
// https://orgmode.org/manual/Timestamps.html
const timestamp = {
line: line,
keyword: match[1],
active: match[2] === "<" ? true : false,
timestamp: new Date(match[3] + (match[4] ? " "+match[4] : "")).toISOString(),
range: function(start_date, start_time = "", start_time_end, end_date = "", end_time = "") {
if (start_time_end && !end_date) {
return new Date(start_date+" "+start_time_end) -
new Date(start_date+" "+start_time);
}
if (end_date) {
return new Date(end_date+" "+end_time) - new Date(start_date+" "+start_time);
}
return null;
}(match[3], match[4], match[5], match[7], match[8]),
repeat: function(keyword) {
if (!keyword) return null;
return {
n: parseInt(keyword.replace(/^.*([0-9]+).*$/, "$1")),
interval: keyword.replace(/^.*([dwmy])$/, "$1"),
};
}(match[6]),
};
if (!_memory) _memory = [];
_memory.push(timestamp);
return parse_timestamp(text.replace(reg, ""), line, _memory);
}
function _find_deadline(timestamps) {
return timestamps.filter((e) => e.keyword === "DEADLINE")[0] || null;
}
function _find_scheduled(timestamps) {
return timestamps.filter((e) => e.keyword === "SCHEDULED")[0] || null;
}
function _is_overdue(status, timestamp) {
if (status !== "todo") return false;
return timestamp.filter((timeObj) => {
if (_normalise(new Date()) < _normalise(new Date(timeObj.timestamp))) return false;
if (timeObj.keyword === "DEADLINE" || timeObj.keyword === "SCHEDULED") return true;
return false;
}).length > 0 ? true : false;
}
function _normalise(date) {
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}