UNPKG

conf-cal

Version:
397 lines (375 loc) 12.3 kB
const CalError = require('./CalError') const getTimezone = require('./getTimezone') const slotsForRooms = require('./slotsForRooms') const renderSlots = require('./renderSlots') const moment = require('moment') const MD_INDENT = 4 function applyAutoIds (entries, entriesList) { for (const entry of entriesList) { if (!entry.id) { let id = entry.auto_id // This works because the parent // is always before the child in the list if (entry.parent) { id = `${entry.parent.id}${entry.auto_id}` entry.parentId = entry.parent.id delete entry.parent } while (entries[id]) { id = `$${id}` } entry.id = id entries[id] = entry } else { entry.hasCustomId = true } delete entry.auto_id } } function extractPerson (roomEntry) { let personParts = /\s+by\s+(.*)$/ig.exec(roomEntry.summary) if (personParts) { roomEntry.summary = roomEntry.summary.substr(0, personParts.index) roomEntry.person = personParts[1] } else { roomEntry.person = null } } function processInput (options, string) { const lines = string.split('\n') const isEmptyLine = (line) => /^\s*$/.test(line) if (lines.filter(isEmptyLine).length === lines.length) { throw new CalError('empty', 'Input is empty') } const rooms = {} const persons = {} function addToPersons (roomEntry) { if (roomEntry.person) { let roomEntries = persons[roomEntry.person] if (!roomEntries) { roomEntries = [] persons[roomEntry.person] = roomEntries } roomEntries.push(roomEntry) } } const entries = {} const entriesList = [] const doc = { rooms } const checkMissing = (lineIndex) => { if (!doc.date) { throw new CalError('missing-data', 'Date not specified in header, use "on YYYY/MM/DD" as a date format', lineIndex) } if (!doc.location) { throw new CalError('missing-data', 'Location not specified in header, use "at <location>#<google-place-id>" to specify one', lineIndex) } if (!doc.title) { throw new CalError('missing-data', 'No title specified, before the first room, enter any title in the first line', lineIndex) } } let room = null let roomIndex = 0 let roomData = null let continueLine = false let docIndent = -1 let entryIndent = -1 let subEntryIndent = -1 let restrictIndent = -1 let continueDescription = false let wasEmpty = false function extractId (roomEntry, lineIndex, columOffset) { let idParts = /\s+#([a-zA-Z0-9-._~:@/?!$&'()*+,;=]*)/ig.exec(roomEntry.summary) if (idParts) { roomEntry.summary = roomEntry.summary.substr(0, idParts.index) const id = idParts[1] if (entries[id]) { throw new CalError('duplicate-id', `There are two or more entries with the id: ${id}`, lineIndex, columOffset + idParts.index) } roomEntry.id = id } } function extractLang (roomEntry, lineIndex, columOffset) { let langParts = /\s+in\s+([a-z]{2}(-[a-z]{2})?)$/ig.exec(roomEntry.summary) if (langParts) { roomEntry.summary = roomEntry.summary.substr(0, langParts.index) roomEntry.lang = langParts[1] } else { roomEntry.lang = null } } function extractEntryMeta (roomEntry, lineIndex, columOffset) { extractId(roomEntry, lineIndex, columOffset) extractLang(roomEntry, lineIndex, columOffset) extractPerson(roomEntry) } function processFirstEntryLine (roomEntry, lineIndex, columOffset) { extractEntryMeta(roomEntry, lineIndex, columOffset) const continueLine = /\\$/ig.test(roomEntry.summary) if (continueLine) { roomEntry.summary = roomEntry.summary.substr(0, roomEntry.summary.length - 1) } return continueLine } function processRoom (line, lineIndex) { const r = /^\[(.*)\]\s*$/ig.exec(line) if (r) { room = r[1] roomData = [] rooms[room] = roomData roomIndex += 1 checkMissing(lineIndex) return true } } function assertStrictIndent (lineIndex, lineIndent) { if (lineIndent !== docIndent) { throw new CalError('invalid-indent', `The document's indent is derminded in the first line to be ${docIndent} spaces, it is ${lineIndent} spaces at line ${lineIndex}`, lineIndex, lineIndent) } } function processHeader (line, lineIndex, lineIndent) { assertStrictIndent(lineIndex, lineIndent) if (processRoom(line, lineIndex)) { return true } const loc = /^at ([^#]*)#(.*)\s*$/ig.exec(line) if (loc) { doc.location = loc[1] doc.googleObjectId = loc[2] return true } const time = /^on ([0-9]{4})\/([0-9]{2})\/([0-9]{2})\s*$/ig.exec(line) if (time) { doc.date = `${time[1]}${time[2]}${time[3]}` return true } if (!doc.title) { doc.title = line.trim() return true } throw new CalError('invalid-data', `Unknown header "${line}"`, lineIndex, lineIndent) } function processDateLine (line, lineIndex, columnOffset) { const parts = /^(([0-9]{2}):([0-9]{2})-([0-9]{2}):([0-9]{2})\s*)(.*)\s*$/ig.exec(line) if (parts) { if (continueLine) { throw new CalError('invalid-data', 'Line tries to extend over entry boundaries', lineIndex - 1, columnOffset) } const roomEntry = { auto_id: `${roomIndex}-${roomData.length + 1}`, start: `${doc.date}T${parts[2]}${parts[3]}00`, end: `${doc.date}T${parts[4]}${parts[5]}00`, summary: parts[6], room } continueDescription = false continueLine = processFirstEntryLine(roomEntry, lineIndex, columnOffset + parts[1].length) if (continueLine) { restrictIndent = entryIndent } else { finishSummary(roomEntry) } addToPersons(roomEntry) if (roomEntry.id) { entries[roomEntry.id] = roomEntry } entriesList.push(roomEntry) roomData.push(roomEntry) return true } } function finishSummary (roomEntry) { roomEntry.summary = roomEntry.summary.trim() } function processBody (roomEntry, formerRoom, line, lineIndex, lineIndent, allowSubentries) { let nextLine = line.replace(/(\s*)$/, '') if (!continueLine) { const listParts = /^(-\s+)(.*)$/g.exec(nextLine) if (listParts && allowSubentries) { if (!formerRoom.entries) { formerRoom.entries = [] } roomEntry = { auto_id: `-${formerRoom.entries.length + 1}`, summary: listParts[2], room } if (roomEntry.id) { entries[roomEntry.id] = roomEntry } entriesList.push(roomEntry) continueDescription = false continueLine = processFirstEntryLine(roomEntry, lineIndex, listParts[1].length + lineIndent) if (!continueLine) { finishSummary(roomEntry) } addToPersons(roomEntry) roomEntry.parent = formerRoom formerRoom.entries.push(roomEntry) return true } nextLine = '\n' + nextLine } continueLine = /\\$/ig.test(nextLine) if (continueLine) { nextLine = nextLine.substr(0, nextLine.length - 1) restrictIndent = lineIndent } else { restrictIndent = -1 } if (continueDescription) { if (wasEmpty) { nextLine = '\n' + nextLine } roomEntry.description += nextLine return true } if (wasEmpty) { roomEntry.description = nextLine.substr(1) continueDescription = true return true } roomEntry.summary += nextLine if (!continueLine) { finishSummary(roomEntry) } return true } function processLine (line, lineIndex) { const lineParts = /^([ ]*)(.*)/g.exec(line) const lineIndent = lineParts[1].length line = lineParts[2] if (docIndent === -1) { docIndent = lineIndent entryIndent = docIndent + MD_INDENT subEntryIndent = docIndent + MD_INDENT * 2 } if (room === null) { if (processHeader(line, lineIndex, lineIndent)) { return } } else { const formerRoom = roomData[roomData.length - 1] if (restrictIndent !== -1) { if (lineIndent !== restrictIndent) { throw new CalError('invalid-indent', `Line is a continuation of the former line and as such is expected to have ${restrictIndent} spaces, but it has ${lineIndent}`, lineIndex, lineIndent) } } if (!formerRoom) { assertStrictIndent(lineIndex, lineIndent) } if (!formerRoom || lineIndent === docIndent) { if (processRoom(line, lineIndex)) { return true } if (processDateLine(line, lineIndex, lineIndent)) { return true } } else { let roomEntry = formerRoom.entries ? formerRoom.entries[formerRoom.entries.length - 1] : formerRoom let expectedIndent = entryIndent if (roomEntry.parent) { if (lineIndent < subEntryIndent && !continueLine) { roomEntry = formerRoom continueDescription = false } else { expectedIndent = subEntryIndent } } const allowSubentries = (!roomEntry.parent) && !roomEntry.description if (lineIndent < expectedIndent) { throw new CalError('invalid-indent', `The indentation of line needs to be at least ${expectedIndent} as it is content of the entry but is ${lineIndent}`, lineIndex, lineIndent) } if (lineIndent > expectedIndent) { line = spaces(lineIndent - expectedIndent) + line } if (processBody(roomEntry, formerRoom, line, lineIndex, expectedIndent, allowSubentries)) { return true } } } throw new CalError('invalid-data', `Unprocessable line.`, lineIndex, lineIndent) } try { lines.forEach((line, lineIndex) => { if (isEmptyLine(line)) { wasEmpty = true return // empty lines } processLine(line, lineIndex) wasEmpty = false }) } catch (err) { if (err instanceof CalError) { let line = lines[err.line - 1] err.message += '\n\nL' + err.line + ':' + line + '\n' + spaces(err.column - 1 + err.line.toString().length + 2) + '↑' } throw err } checkMissing(lines.length - 1) return getTimezone(options, doc.googleObjectId) .then(googleObject => { applyTimeZone(rooms, googleObject.timeZone) doc.googleObject = googleObject doc.persons = persons applyAutoIds(entries, entriesList) doc.entries = entries return doc }) } function spaces (count) { return new Array(count + 1).join(' ') } function applyTimeZone (rooms, timeZone) { Object.keys(rooms).forEach(room => { rooms[room].forEach(roomEntry => { roomEntry.start = moment.tz(roomEntry.start, timeZone).toISOString() roomEntry.end = moment.tz(roomEntry.end, timeZone).toISOString() }) }) } function toPromise (input) { if (input instanceof Promise || (input && input.then)) { return input } return Promise.resolve(input) } const confCal = (options, input) => { if (input === null || input === undefined) { return Promise.reject(new CalError('empty', 'input is missing')) } if (!options || typeof options !== 'object') { return Promise.reject(new CalError('missing-option', 'options are missing')) } return toPromise(input) .then(stringOrBuffer => { if (!stringOrBuffer) { throw new CalError('empty', 'Input not given') } return String(stringOrBuffer) }) .then(string => processInput(options, string)) .then(rawData => { return Object.assign( rawData, { toSlots: function () { return slotsForRooms(this.googleObject.timeZone, this.rooms) }, render: function (options) { const slots = this.toSlots() options = Object.assign({ header: `## ${this.title}\nat [${this.location}](${this.googleObject.url})\n` }, options) return renderSlots(options, slots) }, toMarkdown: function () { return this.render({}) } } ) }) } confCal.CalError = CalError module.exports = confCal