UNPKG

@haxtheweb/haxcms-nodejs

Version:

HAXcms single and multisite nodejs server, api, and administration

360 lines (353 loc) 10.7 kB
"use strict"; const fs = require('fs-extra'); const { v4: uuidv4 } = require('uuid'); const JSONOutlineSchemaItem = require('./JSONOutlineSchemaItem.js'); const array_search = require('locutus/php/array/array_search'); const usort = require('locutus/php/array/usort'); /** * JSONOutlineSchema - An object for interfacing with the JSON Outline schema * specification. @see https://github.com/elmsln/json-outline-schema * for more details. This provides a simple way of loading outlines, parsing * and working with the items in them while writing back to the specification * accurately. */ class JSONOutlineSchema { /** * Establish defaults */ constructor() { this.file = null; this.id = uuidv4(); this.title = 'New site'; this.author = ''; this.description = ''; this.license = 'by-sa'; this.metadata = {}; this.items = []; } /** * Get a reasonable license name from the short hand */ getLicenseDetails() { list = { "by": { 'name': "Creative Commons: Attribution", 'link': "https://creativecommons.org/licenses/by/4.0/", 'image': "https://i.creativecommons.org/l/by/4.0/88x31.png" }, "by-sa": { 'name': "Creative Commons: Attribution Share a like", 'link': "https://creativecommons.org/licenses/by-sa/4.0/", 'image': "https://i.creativecommons.org/l/by-sa/4.0/88x31.png" }, "by-nd": { 'name': "Creative Commons: Attribution No derivatives", 'link': "https://creativecommons.org/licenses/by-nd/4.0/", 'image': "https://i.creativecommons.org/l/by-nd/4.0/88x31.png" }, "by-nc": { 'name': "Creative Commons: Attribution non-commercial", 'link': "https://creativecommons.org/licenses/by-nc/4.0/", 'image': "https://i.creativecommons.org/l/by-nc/4.0/88x31.png" }, "by-nc-sa": { 'name': "Creative Commons: Attribution non-commercial share a like", 'link': "https://creativecommons.org/licenses/by-nc-sa/4.0/", 'image': "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" }, "by-nc-nd": { 'name': "Creative Commons: Attribution Non-commercial No derivatives", 'link': "https://creativecommons.org/licenses/by-nc-nd/4.0/", 'image': "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png" } }; if (list[this.license]) { return list[this.license]; } return {}; } /** * Get a new item matching schema standards * @return new JSONOutlineSchemaItem Object */ newItem() { let item = new JSONOutlineSchemaItem(); return item; } /** * Add an item to the outline * @var item an array of values, keyed to match JSON Outline Schema * @return count of items in the array */ addItem(item) { let safeItem = this.validateItem(item); let count = this.items.push(safeItem); return count; } /** * Validate that an item matches JSONOutlineSchemaItem format * @var item JSONOutlineSchemaItem * @return JSONOutlineSchemaItem matching the specification */ validateItem(item) { // create a generic schema item let tmp = new JSONOutlineSchemaItem(); // crush the item given into a stdClass object let ary = item; for (var key in ary) { // only set what the element from spec allows into a new object if (tmp.hasOwnProperty(key)) { tmp[key] = ary[key]; } } return tmp; } /** * Remove an item from the outline if it exists * @var id an id that's in the array of items * @return JSONOutlineSchemaItem or false if not found */ removeItem(id) { for (var key in this.items) { if (this.items[key].id == id) { tmp = this.items[key]; delete this.items[key]; return tmp; } } return false; } /** * Update an item in the outline * @var id an id that's in the array of items * @return JSONOutlineSchemaItem or false if not found */ updateItem(item, save = false) { // verify this is a legit item let safeItem = this.validateItem(item); for (var key in this.items) { // match the current item's ID to our safeItem passed in if (this.items[key].id == safeItem.id) { // overwrite the item this.items[key] = safeItem; // if we save, then we let that return the whole file if (save) { return this.save(); } // this was successful return true; } } // we didn't find a match on the ID to bother saving an update return false; } /** * Load a schema from a file */ async load(location) { if (fs.lstatSync(location).isFile()) { this.file = location; let fileData = JSON.parse(await fs.readFileSync(location, { encoding: 'utf8', flag: 'r' })); let vars = fileData; for (var key in vars) { if (typeof this[key] !== 'undefined' && key != 'items') { this[key] = vars[key]; } } // check for items and escalate to full JSONOutlineSchemaItem object // also ensures data matches only what is supported if (vars['items']) { for (var key in vars['items']) { let item = vars['items'][key]; if (item) { let newItem = new JSONOutlineSchemaItem(); newItem.id = item.id; newItem.indent = item.indent; newItem.location = item.location; newItem.slug = item.slug; newItem.order = item.order; newItem.parent = item.parent; newItem.title = item.title; newItem.description = item.description; // metadata can be anything so whatever newItem.metadata = item.metadata; this.items[key] = newItem; } else { console.warn(`invalid item at ${key}`); } } } return true; } return false; } /** * Get an item by ID */ getItemById(id) { for (var i in this.items) { if (this.items[i].id === id) { return this.items[i]; } } return false; } /** * Get a key by ID, useful to find previous and next items quickly */ getItemKeyById(id) { for (var key in this.items) { if (this.items[key].id === id) { return key; } } return false; } /** * Get an item by property value */ getItemByProperty(propName, value) { for (var id in this.items) { if (this.items[id][propName] === value) { return this.items[id]; } } return false; } /** * Filter based on a set of parents built recursively */ findBranch(id) { const items = this.orderTree(this.items); let decendentIds = [id]; let children = []; children.push(this.getItemById(id)); // walk items and find things that have parent as present id for (var key in items) { if (decendentIds.includes(items[key].parent)) { children.push(items[key]); decendentIds.push(items[key].id); } } return children; } findChildenRecursively(items, activeIds = []) { for (var key in items) { // we found a kid if (activeIds.includes(items[key].parent)) {} let child = this.items[key2]; if (child.parent == item.id) { children.push(child); } } for (var key in currentItems) { let item = currentItems[key]; if (!idList.includes(item.id)) { idList.push(item.id); found.push(item); let children = []; for (var key2 in this.items) { let child = this.items[key2]; if (child.parent == item.id) { children.push(child); } } // sort the kids children.sort(function (a, b) { return a.order - b.order; }); // only walk deeper if there were children for this page if (children.length > 0) { this.orderRecurse(children, sorted, idList); } } } } /** * Get an item by ID */ async getContentById(id, cache = false) { const item = this.getItemById(id); // @todo something is up with our page cache request engine and not returning data in prod /*if (cache && process.env.OPEN_APIS_ENV !== 'development') { return await fetch(`https://${process.env.VERCEL_URL}/api/apps/haxcms/pageCache?site=${this.file}&uuid=${id}&type=link`, this.__fetchOptions).then((d) => d.ok ? d.text() : ''); } else {*/ let location = this.file.replace(this.__siteFileBase, item.location); if (this.__siteLocationPathName) { location = location.replace(this.__siteLocationPathName + '/', ''); } return await fetch(location, this.__fetchOptions).then(d => d.ok ? d.text() : ''); //} } /** * Save data back to the file system location */ async save(reorder = true) { // on every save we ensure it's sorted in the right order if (reorder) { this.items = await this.orderTree(this.items); } let schema = this; let file = schema['file']; // delete so it doesn't show up in the site.json file delete schema['file']; let output = JSON.stringify(schema, null, 2); // ensure we have valid json object if (output) { // reassign so we don't lose it in the transaction this.file = file; return await fs.writeFileSync(file, output); } } /** * Organize the items based on tree order. This makes front end navigation line up correctly */ orderTree(items) { let sorted = []; // do an initial by order usort(items, function (a, b) { return a.order > b.order; }); this.orderRecurse(items, sorted); // sanity check, should always be equal if (sorted.length == items.length) { return sorted; } // if it bombed just pass it through... again, sanity return items; } /** * Sort a JOS */ orderRecurse(currentItems, sorted = [], idList = []) { for (var key in currentItems) { let item = currentItems[key]; if (!array_search(item.id, idList)) { idList.push(item.id); sorted.push(item); let children = []; for (var key2 in this.items) { let child = this.items[key2]; if (child.parent == item.id) { children.push(child); } } // sort the kids usort(children, function (a, b) { return a.order > b.order; }); // only walk deeper if there were children for this page if (children.length > 0) { this.orderRecurse(children, sorted, idList); } } } } } module.exports = JSONOutlineSchema;