diff --git a/client/calendar.coffee b/client/calendar.coffee deleted file mode 100644 index 68788e0..0000000 --- a/client/calendar.coffee +++ /dev/null @@ -1,130 +0,0 @@ -### - * Federated Wiki : Calendar Plugin - * - * Licensed under the MIT license. - * https://github.com/fedwiki/wiki-plugin-calendar/blob/master/LICENSE.txt -### - -months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] -spans = ['EARLY', 'LATE', 'DECADE', 'DAY', 'MONTH', 'YEAR'] - -span = (result, span) -> - if (m = spans.indexOf result.span) < 0 - result.span = span - else if (spans.indexOf span) < m - result.span = span - -parse = (text) -> - rows = [] - for line in text.split /\n/ - result = {} - words = line.match /\S+/g - for word, i in words - if word.match /^\d\d\d\d$/ - result.year = +word - span result, 'YEAR' - else if m = word.match /^(\d0)S$/ - result.year = +m[1]+1900 - span result, 'DECADE' - else if (m = spans.indexOf word) >= 0 - result.span = spans[m] - else if (m = months.indexOf word[0..2]) >= 0 - result.month = m+1 - span result, 'MONTH' - else if m = word.match /^([1-3]?[0-9])$/ - result.day = +m[1] - span result, 'DAY' - else - result.label = words[i..999].join ' ' - break - rows.push result - rows - -apply = (input, output, date, rows) -> - result = [] - for row in rows - if input[row.label]?.date? - date = input[row.label].date - if output[row.label]?.date? - date = output[row.label].date - if row.year? - date = new Date row.year, 1-1 - if row.month? - date = new Date date.getYear()+1900, row.month-1 - if row.day? - date = new Date date.getYear()+1900, date.getMonth(), row.day - if row.label? - output[row.label] = {date} - output[row.label].span = row.span if row.span? - row.date = date - radarValue = dateAsValue(row.date, row.span) - row.units = radarValue.units - row.value = radarValue.value - row.precision = radarValue.precision - result.push row - result - -show = (date, span) -> - switch span - when 'YEAR' then date.getFullYear() - when 'DECADE' then "#{date.getFullYear()}'S" - when 'EARLY' then "Early #{date.getFullYear()}'S" - when 'LATE' then "Late #{date.getFullYear()}'S" - when 'MONTH' then "#{months[date.getMonth()]} #{date.getFullYear()}" - else "#{date.getDate()} #{months[date.getMonth()]} #{date.getFullYear()}" - - -format = (rows) -> - for row in rows - """#{show row.date, row.span}#{row.label}""" - -precisionFor = - DAY: 1000 * 60 * 60 * 24 - MONTH: 1000 * 60 * 60 * 24 * 365.25 / 12 - YEAR: 1000 * 60 * 60 * 24 * 365.25 - DECADE: 1000 * 60 * 60 * 24 * 365.25 * 10 - EARLY: 1000 * 60 * 60 * 24 * 365.25 * 10 - LATE: 1000 * 60 * 60 * 24 * 365.25 * 10 - -unitsFor = - DAY: 'day' - MONTH: 'month' - YEAR: 'year' - DECADE: 'decade' - EARLY: 'decade' - LATE: 'decade' - -dateAsValue = (date, span) -> - precisionInMilliseconds = precisionFor[span] ? precisionFor.DAY - units : [unitsFor[span] ? unitsFor.DAY] - value : (Math.floor(date.getTime() / precisionInMilliseconds)) - precision : precisionInMilliseconds - -radarSource = ($item, results) -> - data = {} - for row in results - data[row.label] = - units: row.units - value: row.value - precision: row.precision - - $item.addClass 'radar-source' - $item.get(0).radarData = -> data - -module.exports = {parse, apply, format, radarSource} if module? - - -emit = (div, item) -> - rows = parse item.text - # wiki.log 'calendar rows', rows - results = apply {}, {}, new Date(), rows - # wiki.log 'calendar results', results - radarSource div, results - div.append """ - #{format(results).join ''}
- """ - -bind = (div, item) -> - div.on 'dblclick', () -> wiki.textEditor div, item - -window.plugins.calendar = {emit, bind} if window? diff --git a/src/client/calendar.js b/src/client/calendar.js new file mode 100644 index 0000000..d6fcc2d --- /dev/null +++ b/src/client/calendar.js @@ -0,0 +1,163 @@ +/* + * Federated Wiki : Calendar Plugin + * + * Licensed under the MIT license. + * https://github.com/fedwiki/wiki-plugin-calendar/blob/master/LICENSE.txt + */ + +const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] +const spans = ['EARLY', 'LATE', 'DECADE', 'DAY', 'MONTH', 'YEAR'] + +const span = function (result, span) { + let m + if ((m = spans.indexOf(result.span)) < 0) { + return (result.span = span) + } else if (spans.indexOf(span) < m) { + return (result.span = span) + } +} + +const parse = function (text) { + const rows = [] + for (var line of text.split(/\n/)) { + var result = {} + var words = line.match(/\S+/g) + for (var i = 0; i < words.length; i++) { + var m + var word = words[i] + if (word.match(/^\d\d\d\d$/)) { + result.year = +word + span(result, 'YEAR') + } else if ((m = word.match(/^(\d0)S$/))) { + result.year = +m[1] + 1900 + span(result, 'DECADE') + } else if ((m = spans.indexOf(word)) >= 0) { + result.span = spans[m] + } else if ((m = months.indexOf(word.slice(0, 3))) >= 0) { + result.month = m + 1 + span(result, 'MONTH') + } else if ((m = word.match(/^([1-3]?[0-9])$/))) { + result.day = +m[1] + span(result, 'DAY') + } else { + result.label = words.slice(i, 1000).join(' ') + break + } + } + rows.push(result) + } + return rows +} + +const apply = function (input, output, date, rows) { + const result = [] + for (var row of rows) { + if (input[row.label]?.date) { + date = input[row.label].date + } + if (output[row.label]?.date) { + date = output[row.label].date + } + if (row.year) { + date = new Date(row.year, 1 - 1) + } + if (row.month) { + date = new Date(date.getYear() + 1900, row.month - 1) + } + if (row.day) { + date = new Date(date.getYear() + 1900, date.getMonth(), row.day) + } + if (row.label) { + output[row.label] = { date } + if (row.span) { + output[row.label].span = row.span + } + } + row.date = date + var radarValue = dateAsValue(row.date, row.span) + row.units = radarValue.units + row.value = radarValue.value + row.precision = radarValue.precision + result.push(row) + } + return result +} + +const show = function (date, span) { + switch (span) { + case 'YEAR': + return date.getFullYear() + case 'DECADE': + return `${date.getFullYear()}'S` + case 'EARLY': + return `Early ${date.getFullYear()}'S` + case 'LATE': + return `Late ${date.getFullYear()}'S` + case 'MONTH': + return `${months[date.getMonth()]} ${date.getFullYear()}` + default: + return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}` + } +} + +const format = rows => rows.map(row => `${show(row.date, row.span)}${row.label}`) + +const precisionFor = { + DAY: 1000 * 60 * 60 * 24, + MONTH: (1000 * 60 * 60 * 24 * 365.25) / 12, + YEAR: 1000 * 60 * 60 * 24 * 365.25, + DECADE: 1000 * 60 * 60 * 24 * 365.25 * 10, + EARLY: 1000 * 60 * 60 * 24 * 365.25 * 10, + LATE: 1000 * 60 * 60 * 24 * 365.25 * 10, +} + +const unitsFor = { + DAY: 'day', + MONTH: 'month', + YEAR: 'year', + DECADE: 'decade', + EARLY: 'decade', + LATE: 'decade', +} + +var dateAsValue = function (date, span) { + const precisionInMilliseconds = precisionFor[span] != null ? precisionFor[span] : precisionFor.DAY + return { + units: [unitsFor[span] != null ? unitsFor[span] : unitsFor.DAY], + value: Math.floor(date.getTime() / precisionInMilliseconds), + precision: precisionInMilliseconds, + } +} + +const radarSource = function ($item, results) { + const data = {} + for (var row of results) { + data[row.label] = { + units: row.units, + value: row.value, + precision: row.precision, + } + } + + $item.addClass('radar-source') + return ($item.get(0).radarData = () => data) +} + +const emit = function (div, item) { + const rows = parse(item.text) + // wiki.log 'calendar rows', rows + const results = apply({}, {}, new Date(), rows) + // wiki.log 'calendar results', results + radarSource(div, results) + div.append(`\ +${format(results).join('')}
\ +`) +} + +const bind = (div, item) => div.on('dblclick', () => wiki.textEditor(div, item)) + +if (typeof window !== 'undefined' && window !== null) { + window.plugins.calendar = { emit, bind } +} + +export const calendar = typeof window == 'undefined' ? { parse, apply, format, radarSource } : undefined diff --git a/test/test.coffee b/test/test.coffee deleted file mode 100644 index d23b737..0000000 --- a/test/test.coffee +++ /dev/null @@ -1,118 +0,0 @@ -report = require '../client/calendar' -expect = require 'expect.js' - -describe 'calendar plugin', -> - - describe 'parsing', -> - - it 'recognizes decades', -> - expect(report.parse "1960 DECADE").to.eql [{year: 1960, span:'DECADE'}] - expect(report.parse "DECADE 1960").to.eql [{year: 1960, span:'DECADE'}] - expect(report.parse "60S").to.eql [{year: 1960, span:'DECADE'}] - - it 'recognizes half decades', -> - expect(report.parse "60S EARLY").to.eql [{year: 1960, span:'EARLY'}] - expect(report.parse "EARLY 60S").to.eql [{year: 1960, span:'EARLY'}] - expect(report.parse "LATE 60S").to.eql [{year: 1960, span:'LATE'}] - - it 'recognizes years', -> - expect(report.parse "1960").to.eql [{year: 1960, span:'YEAR'}] - - it 'recognizes months', -> - expect(report.parse "1960 MAR").to.eql [{year: 1960, month:3, span:'MONTH'}] - expect(report.parse "MAR 1960").to.eql [{year: 1960, month:3, span:'MONTH'}] - expect(report.parse "MARCH 1960").to.eql [{year: 1960, month:3, span:'MONTH'}] - - it 'recognizes days', -> - expect(report.parse "MAR 5 1960").to.eql [{year: 1960, month:3, day: 5, span:'DAY'}] - expect(report.parse "1960 MAR 5").to.eql [{year: 1960, month:3, day: 5, span:'DAY'}] - expect(report.parse "5 MAR 1960").to.eql [{year: 1960, month:3, day: 5, span:'DAY'}] - - it 'recognizes labels', -> - expect(report.parse "Ward's CHM Interview").to.eql [{label: "Ward's CHM Interview"}] - expect(report.parse "APRIL 24 2006 Ward's CHM Interview").to.eql [{year: 2006, month:4, day: 24, span:'DAY', label: "Ward's CHM Interview"}] - expect(report.parse " APRIL 24 2006\tWard's CHM Interview ").to.eql [{year: 2006, month:4, day: 24, span:'DAY', label: "Ward's CHM Interview"}] - - describe 'applying', -> - - today = new Date 2013, 2-1, 3 - interview = new Date 2006, 4-1, 24 - oneDayInMS = 24*60*60*1000 - - it 'recalls input', -> - input = {interview: {date: interview}} - output = {} - rows = report.parse "interview" - expect(report.apply input, output, today, rows).to.eql [ - date: interview - label:'interview' - units: ['day'] - value: Math.floor(interview.getTime() / oneDayInMS) - precision: oneDayInMS - ] - - it 'extends today', -> - input = {} - output = {} - rows = report.parse "APRIL 1 April Fools Day" - results = report.apply input, output, today, rows - expect(results).to.eql [ - date: new Date(2013, 4-1, 1) - month: 4 - day: 1 - span:'DAY' - label: 'April Fools Day' - units: ['day'] - value: Math.floor(new Date(2013, 4-1, 1).getTime() / oneDayInMS) - precision: oneDayInMS - ] - expect(output).to.eql {'April Fools Day': {date: new Date(2013, 4-1, 1), span:'DAY'}} - - describe 'radarSource', -> - mock = {} - beforeEach -> - mock.el = {} - mock.$el = - addClass : (c) -> mock.actualClass = c - get : (n) -> mock.el - results = report.apply {}, {}, new Date(), report.parse(''' - 2015 SEP 1 Starts Now - LATE 90S Some languages were born - ''') - report.radarSource(mock.$el, results) - - it 'calls addClass with "radar-source"', -> - expect(mock.actualClass).to.be 'radar-source' - - it 'adds radarData() to the DOM element', -> - expect(mock.el).to.have.key 'radarData' - - it 'uses the labels as keys in the radarData', -> - expect(mock.el.radarData()).to.have.key 'Starts Now' - - it 'puts the distance from the Epoch into the values in the radarData', -> - data = mock.el.radarData() - daysSinceEpoch = Math.floor(new Date(2015, 8, 1).getTime() / (24 * 60 * 60 * 1000) ) # 16,679 - expect(data['Starts Now']).to.have.key 'value' - expect(data['Starts Now'].value).to.eql daysSinceEpoch - - it 'specifies units & precision with values in the radarData', -> - data = mock.el.radarData() - oneDayInMS = 24 * 60 * 60 * 1000 # 86,400,000 - expect(data['Starts Now']).to.have.key 'units' - expect(data['Starts Now'].units).to.eql ['day'] - expect(data['Starts Now']).to.have.key 'precision' - expect(data['Starts Now'].precision).to.eql oneDayInMS - - it 'chooses units & precision to match the parsed span of the date', -> - data = mock.el.radarData() - oneDecadeInMS = 10 * 365.25 * 24 * 60 * 60 * 1000 # 315,576,000,000 - expect(data['Some languages were born']).to.have.key 'units' - expect(data['Some languages were born'].units).to.eql ['decade'] - expect(data['Some languages were born']).to.have.key 'precision' - expect(data['Some languages were born'].precision).to.eql oneDecadeInMS - - # describe 'formatting', -> - # it 'returns an array of strings', -> - # rows = report.format report.parse "" - # expect(rows).to.eql [] diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..dab2c8d --- /dev/null +++ b/test/test.js @@ -0,0 +1,144 @@ +import { calendar as report } from '../src/client/calendar.js' +import expect from 'expect.js' + +describe('calendar plugin', function () { + describe('parsing', function () { + it('recognizes decades', function () { + expect(report.parse('1960 DECADE')).to.eql([{ year: 1960, span: 'DECADE' }]) + expect(report.parse('DECADE 1960')).to.eql([{ year: 1960, span: 'DECADE' }]) + expect(report.parse('60S')).to.eql([{ year: 1960, span: 'DECADE' }]) + }) + + it('recognizes half decades', function () { + expect(report.parse('60S EARLY')).to.eql([{ year: 1960, span: 'EARLY' }]) + expect(report.parse('EARLY 60S')).to.eql([{ year: 1960, span: 'EARLY' }]) + expect(report.parse('LATE 60S')).to.eql([{ year: 1960, span: 'LATE' }]) + }) + + it('recognizes years', () => expect(report.parse('1960')).to.eql([{ year: 1960, span: 'YEAR' }])) + + it('recognizes months', function () { + expect(report.parse('1960 MAR')).to.eql([{ year: 1960, month: 3, span: 'MONTH' }]) + expect(report.parse('MAR 1960')).to.eql([{ year: 1960, month: 3, span: 'MONTH' }]) + expect(report.parse('MARCH 1960')).to.eql([{ year: 1960, month: 3, span: 'MONTH' }]) + }) + + it('recognizes days', function () { + expect(report.parse('MAR 5 1960')).to.eql([{ year: 1960, month: 3, day: 5, span: 'DAY' }]) + expect(report.parse('1960 MAR 5')).to.eql([{ year: 1960, month: 3, day: 5, span: 'DAY' }]) + expect(report.parse('5 MAR 1960')).to.eql([{ year: 1960, month: 3, day: 5, span: 'DAY' }]) + }) + + return it('recognizes labels', function () { + expect(report.parse("Ward's CHM Interview")).to.eql([{ label: "Ward's CHM Interview" }]) + expect(report.parse("APRIL 24 2006 Ward's CHM Interview")).to.eql([ + { year: 2006, month: 4, day: 24, span: 'DAY', label: "Ward's CHM Interview" }, + ]) + expect(report.parse(" APRIL 24 2006\tWard's CHM Interview ")).to.eql([ + { year: 2006, month: 4, day: 24, span: 'DAY', label: "Ward's CHM Interview" }, + ]) + }) + }) + + describe('applying', function () { + const today = new Date(2013, 2 - 1, 3) + const interview = new Date(2006, 4 - 1, 24) + const oneDayInMS = 24 * 60 * 60 * 1000 + + it('recalls input', function () { + const input = { interview: { date: interview } } + const output = {} + const rows = report.parse('interview') + expect(report.apply(input, output, today, rows)).to.eql([ + { + date: interview, + label: 'interview', + units: ['day'], + value: Math.floor(interview.getTime() / oneDayInMS), + precision: oneDayInMS, + }, + ]) + }) + + return it('extends today', function () { + const input = {} + const output = {} + const rows = report.parse('APRIL 1 April Fools Day') + const results = report.apply(input, output, today, rows) + expect(results).to.eql([ + { + date: new Date(2013, 4 - 1, 1), + month: 4, + day: 1, + span: 'DAY', + label: 'April Fools Day', + units: ['day'], + value: Math.floor(new Date(2013, 4 - 1, 1).getTime() / oneDayInMS), + precision: oneDayInMS, + }, + ]) + expect(output).to.eql({ 'April Fools Day': { date: new Date(2013, 4 - 1, 1), span: 'DAY' } }) + }) + }) + + return describe('radarSource', function () { + const mock = {} + beforeEach(function () { + mock.el = {} + mock.$el = { + addClass(c) { + return (mock.actualClass = c) + }, + get(n) { + return mock.el + }, + } + const results = report.apply( + {}, + {}, + new Date(), + report.parse(`\ + 2015 SEP 1 Starts Now + LATE 90S Some languages were born\ +`), + ) + return report.radarSource(mock.$el, results) + }) + + it('calls addClass with "radar-source"', () => expect(mock.actualClass).to.be('radar-source')) + + it('adds radarData() to the DOM element', () => expect(mock.el).to.have.key('radarData')) + + it('uses the labels as keys in the radarData', () => expect(mock.el.radarData()).to.have.key('Starts Now')) + + it('puts the distance from the Epoch into the values in the radarData', function () { + const data = mock.el.radarData() + const daysSinceEpoch = Math.floor(new Date(2015, 8, 1).getTime() / (24 * 60 * 60 * 1000)) // 16,679 + expect(data['Starts Now']).to.have.key('value') + return expect(data['Starts Now'].value).to.eql(daysSinceEpoch) + }) + + it('specifies units & precision with values in the radarData', function () { + const data = mock.el.radarData() + const oneDayInMS = 24 * 60 * 60 * 1000 // 86,400,000 + expect(data['Starts Now']).to.have.key('units') + expect(data['Starts Now'].units).to.eql(['day']) + expect(data['Starts Now']).to.have.key('precision') + expect(data['Starts Now'].precision).to.eql(oneDayInMS) + }) + + return it('chooses units & precision to match the report.parsed span of the date', function () { + const data = mock.el.radarData() + const oneDecadeInMS = 10 * 365.25 * 24 * 60 * 60 * 1000 // 315,576,000,000 + expect(data['Some languages were born']).to.have.key('units') + expect(data['Some languages were born'].units).to.eql(['decade']) + expect(data['Some languages were born']).to.have.key('precision') + expect(data['Some languages were born'].precision).to.eql(oneDecadeInMS) + }) + }) +}) + +// describe 'formatting', -> +// it 'returns an array of strings', -> +// rows = report.format report.parse "" +// expect(rows).to.eql []