libre
Version:
World's lightest CMS
150 lines (123 loc) • 5.06 kB
JavaScript
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;