mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-04 05:27:04 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			261 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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;
 | 
						|
}
 |