UNPKG

libre

Version:
150 lines (123 loc) 5.06 kB
import axios from 'axios'; import _ from 'lodash'; class Libre { constructor() { this.path = '/libre'; this.raw = {}; this.typed = {}; this.grouped = {}; this.typeMap = {}; this.plugins = {}; } setup({typeMap, plugins}) { if (typeMap) this.typeMap = typeMap; if (plugins) this.plugins = plugins; } //retrieve a simple piece of content based on unique id get(key, applyPlugins) { const c = this.raw[key]; if (c == null) return []; if (!applyPlugins || !c.content) return c.content || []; //if applyPlugins, also call process to run string interpolation plugins //content is a special field and is an automatic array of lines (split on \n) from the source Sheet //so we also use spread operator to flatten the output into a single array of pieces const processed = []; c.content.map(line => processed.push(...this.process(line))); return processed; } //retrieve a model for structured content based on a path getTyped(path, type) { const items = _.filter(this.typed, t => t.path == path && t.constructor.name == type.name); return items.length > 0 ? items[0] : {}; } //create array of typed objects corresponding to a tab in the spreadsheet and given model class model(sheet, type) { const raw = _.filter(this.raw, {sheet}); const items = []; raw.map(json => { const typed = new type(json, this); this.typed[typed.id] = typed; items.push(typed); }); this.grouped[sheet] = items; return items; } load(success) { axios.get(this.path).then(({data}) => { this.init(data); if (typeof(success) == 'function') success(this); }); } push(tab, row, success) { axios.post(`${this.path}/push`, {tab, row}).then(result => { if (typeof(success) == 'function') success(result); }); } init(data) { const loaded = []; //add the sheet name to the source data _.forOwn(data, (tab, sheet) => { _.forOwn(tab, item => item.sheet = sheet); loaded.push(sheet); //merge results into overall content store _.assign(this.raw, tab); }); console.log(`[LIBRE] Content loaded for [${loaded.join(', ')}]`); //after all raw content is loaded //create typed model classes of all content based on map in config.js //TODO: add properties to routes because this is overly rigid (route key needs to match sheet name) //typeMap config object tells us what model types to create for each sheet _.forOwn(this.typeMap, (type, sheet) => this.model(sheet, type)); } //force sever to re-ingest static content from google doc refresh(success) { //collect all sheets from what is currently loaded const tabs = []; _.forOwn(this.raw, c => { if (tabs.indexOf(c.sheet) < 0) tabs.push(c.sheet) }); console.log('[LIBRE] Refreshing cached content on server...'); axios.post('/libre/refresh', {tabs}).then(({data}) => { console.log('[LIBRE] Content refresh complete on server.'); this.init(data); success(this); }); } //for given text, apply interpolation plugins and return array of pieces for rendering process(text) { const plugins = this.plugins; if (!text || !plugins) return []; let pieces = [], ranges = []; //1. run through all plugins to find matches to be replaced _.forOwn(plugins, (plugin, type) => { let pattern = plugin.pattern; let match, start, end; //each plugin can have multiple matches -- keep going to find them all while ((match = pattern.exec(text)) !== null) { start = match.index; end = start + match[0].length; ranges.push({type, start, end, match}) } }); //if we find nothing, just convert the whole thing to 1 plain piece if (ranges.length == 0) return [{type:'plain', text}]; //2. sort by starting position ranges = _.sortBy(ranges, 'start'); //3. fill in the gaps with plain text for (var i = 0; i < ranges.length; i++) { //if we're looking at a plain (added) piece or the last piece, skip if (ranges[i].type == 'plain' || i + 1 == ranges.length) continue; const plain = {type:'plain', start: ranges[i].end, end: ranges[i+1].start}; if (plain.end > plain.start) ranges.push(plain); } //4. sort again -- now we have everything from the first matched range to the last ranges = _.sortBy(ranges, 'start'); //add plain piece at beginning and end if necessary (the above loop ignores these edge cases) if (ranges[0].start > 0) ranges.splice(0, 0, {type: 'plain', start: 0, end: ranges[0].start}); if (ranges[ranges.length-1].end < text.length) ranges.push({type:'plain', start: ranges[ranges.length-1].end, end: text.length}); //5. now populate with the actual text content ranges.map(r => pieces.push({type:r.type, text: text.substring(r.start, r.end), match: r.match})); return pieces; } } const libre = new Libre(); if (typeof(window) == 'object') window.Libre = libre; export default libre;