-
-
Save psych0der/4e9e83273577c86dea72cf6d3179995b to your computer and use it in GitHub Desktop.
StylusSDK: Writing platform helper for Juggernaut writing platform
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* SSDK: Stylus SDK | |
* Wraps around editor instance used in app and manages interaction with API. This is basically | |
* taking care of persisting/caching changes made and communicating with API. | |
* | |
* Exposes 2 interfaces for the editor | |
* | |
* HTML ==> VDOM | |
* API specific JSON ==> HTML | |
* | |
* Assumptions for the editor: | |
* - Should be able to spit out HTML of the text entered | |
* - Be able to load pre-generated HTML | |
*/ | |
/* ES6 imports */ | |
import { | |
copyProps, hasAttr, getAttr | |
} | |
from '../../utils'; | |
/* Requires Virtual DOM for interacting with HTMl changes*/ | |
let VNode = require('virtual-dom/vnode/vnode'), | |
VText = require('virtual-dom/vnode/vtext'), | |
diff = require('virtual-dom/diff'), | |
createElement = require('virtual-dom/create-element'), | |
md5 = require('blueimp-md5/js/md5.min.js'); | |
class StylusSDK { | |
constructor(bookId = null, facade = null, localStorageInterface = null, | |
Plugins = {}, modifiers = {}) { | |
if (bookId === null || facade === null || localStorageInterface === null) { | |
/* Return error */ | |
throw new Error('Required parameters are missing ....'); | |
} | |
/* html-to-vdom is a bridge to directly convert html to virtual-dom object*/ | |
this._convertHTMLToVDOM = require('html-to-vdom')({ | |
VNode: VNode, | |
VText: VText | |
}); | |
/* book_id of book being edited */ | |
this._bookId = bookId; | |
/* facade isntance to be used */ | |
this._facade = facade; | |
/* localStorage instance for persistence*/ | |
this._localStorage = localStorageInterface; | |
/* counter of last version pushed */ | |
this._last_version_uploaded = null; | |
/* resolve method reference for internal state promise */ | |
this._resolve = null; | |
/* reject method reference for internal state promise */ | |
this._reject = null; | |
/* internal promise to keep track of loaded state */ | |
this._promise = new Promise((resolve, reject) => { | |
this._resolve = resolve; | |
this._reject = reject; | |
}); | |
/* VDOM to html converter */ | |
this._convertVDOMToHTML = createElement; | |
/* Declare private instance of html */ | |
this._HTML = ''; | |
/* current VDOM version */ | |
this._current_StylusOBJ_version = -1; | |
/* StylusOBJ prototype to create new instances */ | |
this._StylusOBJPrototype = null; | |
/* current Stylus object*/ | |
this._current_SylusOBJ = null; | |
/* current serialized StylusOBJ */ | |
this._serializedStylusOBJ = ''; | |
/* Dictionary of stylusOBJ objects with meta data such as creation time */ | |
this._stylusOBJContainer = {}; | |
/* Boolean switch to single freezing of plugins */ | |
this._pluginsLoaded = false; | |
/* Plugin registry of SSDK */ | |
this._plugins = {}; | |
/* Boolean for signifying init status */ | |
this._loaded = false; | |
/* apply modifiers only in case of development */ | |
this._modifiers = (__PRODUCTION__) ? {} : modifiers; | |
/* load plugins */ | |
this._loadPlugins(Plugins); | |
/* initialize plugin */ | |
this._init(); | |
} | |
/* Utility to create and persist stylus index */ | |
_persistStylusIndex() { | |
let newStylusIndex = Object.assign({}, { | |
currentVersion: this._current_StylusOBJ_version, | |
stylusOBJContainer: this._stylusOBJContainer | |
}); | |
return this._localStorage.set('STYLUS_INDEX_' + this._bookId, | |
newStylusIndex); | |
} | |
/** | |
* Removes previous stylusOBJ versions from localstorage | |
*/ | |
removePreviousStylusOBJVersions(version) { | |
let targetVersion = parseInt(version); | |
Object.keys(this._stylusOBJContainer).forEach((key) => { | |
let sourceVersion = parseInt(key); | |
if(sourceVersion <= targetVersion && sourceVersion != parseInt(this._current_StylusOBJ_version)){ | |
delete this._stylusOBJContainer[key]; | |
} | |
}); | |
this._persistStylusIndex(); | |
} | |
/** | |
* Pushes the unsynced versions present on localmachine to server | |
*/ | |
pushPendingStylusOBJVersions(upstream_version) { | |
let targetVersion = parseInt(upstream_version); | |
Object.keys(this._stylusOBJContainer).forEach((key) => { | |
let sourceVersion = parseInt(key); | |
if(sourceVersion > targetVersion) { | |
/* Pusing previous data to blotter*/ | |
this._facade({ | |
eventName: 'pushToSocket', | |
arguments: { | |
'event': 'upload', | |
'eventPayload': { | |
requestId: key, | |
payload: { | |
bookId: this._bookId, | |
version: key, | |
md5: this._stylusOBJContainer[key]['md5'], | |
timestamp: this._stylusOBJContainer[key]['timestamp'], | |
stylusOBJ: this._stylusOBJContainer[key]['stylusOBJ'] | |
} | |
} | |
} | |
}); | |
} | |
}) | |
} | |
/** | |
* Loads plugins specifid in Plugins object | |
* @param {object} Plugins object containing individual plugins | |
*/ | |
_loadPlugins(Plugins) { | |
if (this._pluginsLoaded) { | |
throw 'Plugins already loaded. SSDK instance can\'t be modified'; | |
} | |
/* typechecking for plugins */ | |
if (typeof Plugins !== 'object') { | |
console.error('Invalid object provided for plugins') | |
return false; | |
} | |
this._pluginsLoaded = true; | |
/* load plugins */ | |
for (let indentifier in Plugins) { | |
let plugin = Plugins[indentifier]; | |
/* TODO: add type checking for plugin object */ | |
/* creating type if doesn't exist */ | |
if (!(plugin.type in this._plugins)) { | |
this._plugins[plugin.type] = []; | |
} | |
/* registering plugins in SDK*/ | |
this._plugins[plugin.type].push(plugin.plugin); | |
} | |
} | |
/** | |
* StylusOBJ Initializer | |
* Tries to fetch data from localstorage and validates it. | |
* If the data from localStorage passes all tests, It'll be assigned to private instances | |
*/ | |
_init() { | |
/* attach event handler to socket events */ | |
this._facade({ | |
'eventName': 'attachSocketEventHandler', | |
arguments: { | |
'event': 'bookVersionStatus', | |
'callback': { | |
'key': 'bookVersionStatus', | |
'function': (payload) => { | |
payload = JSON.parse(payload); | |
if('status' in payload){ | |
if(payload['status'] === 'success') { | |
this.removePreviousStylusOBJVersions(payload.version); | |
} | |
} | |
} | |
} | |
} | |
}); | |
`` | |
let StylusIndex = this._localStorage.get('STYLUS_INDEX_' + this._bookId); | |
let state = null; | |
let recoveredHash = null; | |
let recoveredTimestamp = null; | |
if (StylusIndex === null || StylusIndex === {} || | |
!(hasAttr(StylusIndex, 'currentVersion')) || | |
!(hasAttr(StylusIndex, 'stylusOBJContainer')) | |
) { | |
/* No Stylus state found. Assuming fresh start */ | |
state = false; | |
} else { | |
let currentVersion = StylusIndex.currentVersion; | |
let stylusOBJContainer = StylusIndex.stylusOBJContainer; | |
/* extracting current version from stylusOBJContainer to currentStylusOBJ */ | |
if (currentVersion > -1) { | |
/* if current version is not available in localstorage, reset localstorage */ | |
if (!(currentVersion in stylusOBJContainer)) { | |
/* no meaningful data contained */ | |
state = false; | |
} | |
let currentStylusOBJ = ('stylusOBJ' in stylusOBJContainer[ | |
currentVersion]) ? stylusOBJContainer[currentVersion].stylusOBJ : | |
null; | |
let response = this._validateStylusOBJ(currentStylusOBJ); | |
if (!response.status) { | |
/* Stylus OBJ recovered is corrupted */ | |
state = false; | |
} | |
recoveredHash = ('md5' in stylusOBJContainer[currentVersion]) ? | |
StylusIndex.stylusOBJContainer[currentVersion].md5 : null; | |
recoveredTimestamp = ('timestamp' in stylusOBJContainer[ | |
currentVersion]) ? StylusIndex.stylusOBJContainer[currentVersion] | |
.timestamp : null; | |
if (recoveredTimestamp === null) { | |
/* couldn't fetch meta data from localstorage data. Reset the localstorage */ | |
state = false; | |
} | |
if (recoveredHash === null) { | |
/* couldn't fetch meta data from localstorage data. Reset the localstorage */ | |
state = false; | |
} | |
let hash = md5(JSON.stringify(currentStylusOBJ)); | |
/* check whether targetHash matches hash of stored stylus */ | |
if (hash != recoveredHash) { | |
/* corrupted state found. Reset it */ | |
state = false; | |
} else { | |
state = true; | |
this._current_StylusOBJ_version = currentVersion; | |
this._stylusOBJContainer = stylusOBJContainer; | |
this._current_SylusOBJ = currentStylusOBJ; | |
} | |
} else { | |
/* No meaningful data can be recovered from localstorage */ | |
state = false; | |
} | |
} | |
/* if state is corrupted, reinitalize the state */ | |
this._persistStylusIndex(); | |
/* Don't connect with server if no reconcile is passed [DEVELOPMENT mode only] */ | |
if (('NO_RECONCILE' in this._modifiers && this._modifiers['NO_RECONCILE'] === | |
true)) { | |
this._loaded = true; | |
return this._resolve({ | |
'status': 'success', | |
'message': 'SSDK is ready to serve' | |
}); | |
} | |
/* fire the call to check for the latest version */ | |
this._facade({ | |
eventName: 'fetch', | |
arguments: { | |
reqName: 'getLatestState', | |
urlContext: { | |
'bookId': this._bookId | |
}, | |
options: {}, | |
successCallback: (response) => { | |
let responseData = response.data; | |
let fetchLatestVersion = false; | |
if (!(responseData && responseData.payload)) { | |
// todo : no data found | |
fetchLatestVersion = false; | |
} else { | |
let current_version_data = responseData.payload[ | |
'current_version']; | |
let upstream_timestamp = current_version_data['timestamp']; | |
let upstream_md5 = current_version_data['md5'] | |
let upstream_version = current_version_data['version']; | |
/* checking in case of legitimate data has been recovered from localstorage */ | |
if (state) { | |
if (upstream_version > this._current_StylusOBJ_version) { | |
fetchLatestVersion = true; | |
} else { | |
if (upstream_version < this._current_StylusOBJ_version) { | |
this.pushPendingStylusOBJVersions(upstream_version); | |
} else { | |
if ((upstream_md5 !== recoveredHash) || ( | |
recoveredTimestamp !== upstream_timestamp)) { | |
fetchLatestVersion = true; | |
} | |
} | |
} | |
} else { | |
if (upstream_version > -1) | |
fetchLatestVersion = true; | |
} | |
} | |
if (fetchLatestVersion) { | |
this._facade({ | |
'eventName': 'fetch', | |
'arguments': { | |
reqName: 'getLatestVersion', | |
urlContext: { | |
'bookId': this._bookId | |
}, | |
options: {}, | |
successCallback: (response) => { | |
let responseData = response.data; | |
if (!(responseData && responseData.payload)) { | |
// todo : no data found | |
fetchLatestVersion = false; | |
} else { | |
let data = responseData.payload; | |
this._current_StylusOBJ_version = data[ | |
'version']; | |
this._current_SylusOBJ = data['stylusOBJ']; | |
/* pushing fetched data to stylusOBJ container */ | |
this._stylusOBJContainer[this._current_StylusOBJ_version] = { | |
version: this._current_StylusOBJ_version, | |
stylusOBJ: Object.assign({}, this._current_SylusOBJ), | |
timestamp: data.timestamp, | |
md5: data.md5 | |
}; | |
/* saving state in localstorage */ | |
this._persistStylusIndex(); | |
this._loaded = true; | |
this._resolve({ | |
'status': 'success', | |
'message': 'SSDK is ready to serve' | |
}); | |
} | |
} | |
} | |
}) | |
} else { | |
this._loaded = true; | |
this._resolve({ | |
'status': 'success', | |
'message': 'SSDK is ready to serve' | |
}); | |
} | |
}, | |
errorCallback: (err) => { | |
console.error('error while communicating with server '); | |
console.error(err); | |
this._reject({ | |
'status': 'failed', | |
'message': 'unable to reconcile with server', | |
'error': err | |
}); | |
} | |
} | |
}); | |
} | |
/** | |
* Validates VDOM to custom rules/requirements for StylusOBJ | |
* Current criteria for checking: | |
* - every node should have: | |
* - data-type attribute | |
* VirtualText should have parent with data-leaf attribute | |
* | |
*/ | |
_validateVDOM(VDOM, parent = null) { | |
let errors = []; | |
let required_attrs = ['properties.attributes.data-type', | |
'properties.attributes.data-id' | |
]; | |
let leafAttr = 'properties.attributes.data-leaf'; | |
/* return error if individual elements are returned */ | |
if (Array.isArray(VDOM)) { | |
return { | |
status: false, | |
errors: ["Sections aren't contained in parent element"] | |
}; | |
} | |
/* check for all required attrs */ | |
for (let attr of required_attrs) { | |
if ((getAttr(VDOM, attr) === undefined)) { | |
return { | |
status: false, | |
errors: ["doesn't contain required attr: " + attr] | |
}; | |
} | |
} | |
if (VDOM.type == 'VirtualText') { | |
if (getAttr(parent, leafAttr) !== "true") { | |
return { | |
status: false, | |
errors: ['Text node is not bounded by data leaf parent'] | |
}; | |
} | |
} else { | |
if (getAttr(VDOM, leafAttr) === "true") { | |
/* break loop and return if node is data-leaf */ | |
return { | |
status: true, | |
errors: [] | |
}; | |
} else { | |
let childrenStatus = true; | |
/* validate each child */ | |
VDOM.children.forEach((child) => { | |
let response = this._validateVDOM(child, VDOM); | |
childrenStatus = childrenStatus && response.status; | |
errors = errors.concat(response.errors); | |
}); | |
return { | |
status: childrenStatus, | |
errors: errors | |
}; | |
} | |
} | |
} | |
/** | |
* Validates stylusOBJ | |
* Current criteria for checking: | |
* - every object should have: | |
* - data-type attribute in props | |
* data-leaf attributes in bottom elements | |
* data-leaf elements should have html elements | |
* | |
*/ | |
_validateStylusOBJ(stylusOBJ, parent = null) { | |
let errors = []; | |
let required_attrs = ['type', 'tagName', 'isLeaf', 'meta', | |
'properties.attributes.data-type', 'properties.attributes.data-id' | |
]; | |
let leafAttr = 'properties.attributes.data-leaf'; | |
/* check for all required attrs */ | |
for (let attr of required_attrs) { | |
if ((getAttr(stylusOBJ, attr) === undefined)) { | |
return { | |
status: false, | |
errors: ["doesn't contain required attr: " + attr] | |
}; | |
} | |
} | |
/* check if both isLeaf and data-leaf are not consistent */ | |
if ((stylusOBJ.isLeaf && getAttr(stylusOBJ, leafAttr) == undefined) || (! | |
stylusOBJ.isLeaf && getAttr(stylusOBJ, leafAttr) == "true")) { | |
return { | |
status: false, | |
errors: ['Leaf attribute is not consistent'] | |
}; | |
} | |
/* Check for HTML attribute in leaf elements */ | |
if (stylusOBJ.isLeaf) { | |
if (getAttr(stylusOBJ, 'HTML') === undefined || getAttr(stylusOBJ, | |
'HTML') === "") { | |
return { | |
status: false, | |
errors: ['Leaf node doesn\'nt contain HTML attribute'] | |
}; | |
} | |
/* break loop and return if node is leaf */ | |
return { | |
status: true, | |
errors: [] | |
}; | |
} else { | |
if (stylusOBJ.children.length == 0) { | |
/* terminal obect is not labeled as leaf node */ | |
return { | |
status: false, | |
errors: ['Terminal node is not labeled as leaf'] | |
}; | |
} | |
let childrenStatus = true; | |
/* validate each child */ | |
stylusOBJ.children.forEach((child) => { | |
let response = this._validateStylusOBJ(child, stylusOBJ); | |
childrenStatus = childrenStatus && response.status; | |
errors = errors.concat(response.errors); | |
}); | |
return { | |
status: childrenStatus, | |
errors: errors | |
}; | |
} | |
} | |
/** | |
* Prepare VDOM from current HTML | |
*/ | |
_prepareVDOM(HTML) { | |
let VDOM = this._convertHTMLToVDOM(HTML); | |
let response = this._validateVDOM(VDOM); | |
if (!response.status) { | |
return { | |
'status': response.status, | |
'message': 'Error in parsing HTML', | |
errors: response.errors | |
} | |
} | |
return { | |
'status': response.status, | |
'VDOM': VDOM | |
}; | |
} | |
/** | |
* Returns private instance of current VDOM | |
*/ | |
getVDOM() { | |
return this._current_VDOM; | |
} | |
/** | |
* Load StylusOBJ from JSON | |
*/ | |
loadStylusOBJ(serializedStylusOBJ) { | |
this._setSerializedStylusOBJ(serializedStylusOBJ); | |
/* TODO: VALIDATE StylusOBJ */ | |
this._current_SylusOBJ = JSON.parse(this._serializedStylusOBJ); | |
} | |
/** | |
* set serialized StylusOBJ string | |
*/ | |
_setSerializedStylusOBJ(StylusOBJString) { | |
this._serializedStylusOBJ = StylusOBJString; | |
} | |
/** | |
* Factory function for StylusOBJ | |
*/ | |
_getNewStylusOBJ() { | |
if (this._StylusOBJPrototype === null) { | |
let StylusOBJ = {}; | |
/* creating attributes for Stylus OBJ */ | |
StylusOBJ.type = ""; | |
StylusOBJ.properties = null; | |
StylusOBJ.children = []; | |
StylusOBJ.meta = {}; | |
/* setting StylusOBJ to prototype */ | |
this._StylusOBJPrototype = StylusOBJ; | |
} | |
return Object.assign({}, this._StylusOBJPrototype); | |
} | |
_getMapperObject(mapPair) { | |
/* TODO: add type checking and index count checking [should have 2 attributes] */ | |
return { | |
'from': mapPair[0], | |
'to': mapPair[1] | |
} | |
} | |
_getMapperRegistryFromTupples(Tupples) { | |
/* TODO: add type checking for type [array of array] */ | |
let mapperRegistry = Tupples.map(Tupple => this._getMapperObject(Tupple)); | |
return mapperRegistry; | |
} | |
/** | |
* Maps attributes from VDOM to StylusOBJ | |
*/ | |
_assignStylusOBJAttrs(StylusOBJ, VDOM) { | |
/* This syntax will be used to transfer property from source object to target object */ | |
let mapperTupples = [ | |
['properties', 'properties'], | |
['tagName', 'tagName'] | |
]; | |
let mapperRegistry = this._getMapperRegistryFromTupples(mapperTupples); | |
/* assigning attributes to StylusOBJ */ | |
StylusOBJ = copyProps(VDOM, StylusOBJ, mapperRegistry); | |
} | |
/** | |
* Attached correct type to StylusOBJ based on VDOM | |
*/ | |
_assignStylusOBJType(StylusOBJ, VDOM) { | |
/* copy attrs to base level */ | |
StylusOBJ.type = VDOM.properties.attributes['data-type']; | |
StylusOBJ.section_id = VDOM.properties.attributes['data-id']; | |
/* assign isLeaf attribute to StylusOBJ if */ | |
if ('data-leaf' in StylusOBJ.properties.attributes && StylusOBJ.properties | |
.attributes['data-leaf'] == "true") { | |
StylusOBJ.isLeaf = true; | |
} else { | |
StylusOBJ.isLeaf = false; | |
} | |
} | |
/** | |
* Apply plugins on StylusOBJ to add meta information on objects | |
*/ | |
_postProcessStylusOBJ(StylusOBJ) { | |
/* apply plugins to StylusOBJ */ | |
if (StylusOBJ.type in this._plugins) { | |
this._plugins[StylusOBJ.type].forEach((plugin) => { | |
let so = JSON.parse(JSON.stringify(StylusOBJ)); // clone StylusOBJ to prevent side effects | |
Object.freeze(so); | |
let {assets, ...extraMeta} = plugin(so); | |
/* merging meta with StylusOBJ */ | |
StylusOBJ.meta = Object.assign({}, StylusOBJ.meta, extraMeta); | |
StylusOBJ.meta.assets = Object.assign({}, StylusOBJ.meta.assets, assets); | |
}) | |
} | |
} | |
/** | |
* Function to decodes html string. | |
* Currently removes `amp;` to fix image urls | |
*/ | |
_scrubHTMLText(html) { | |
let entitiesToRemove = ['amp;'] | |
let processString = html; | |
entitiesToRemove.forEach(token => { | |
html = html.replace(new RegExp(token, 'g'), ''); | |
}); | |
return html; | |
} | |
/** | |
* converts VDOM to proprietary StylusOBJ | |
*/ | |
_convertVDOMToStylusOBJ(VDOM) { | |
let StylusOBJ = this._getNewStylusOBJ(); | |
this._assignStylusOBJAttrs(StylusOBJ, VDOM); | |
this._assignStylusOBJType(StylusOBJ, VDOM); | |
/* prepare children StylusObJ if this is not a LEAF object */ | |
if (!StylusOBJ.isLeaf) { | |
StylusOBJ.children = VDOM.children.map(child => this._convertVDOMToStylusOBJ( | |
child)) | |
} else { | |
/* add html to StylusOBJ if it is a leaf */ | |
let DOMElement = this._convertVDOMToHTML(VDOM); | |
StylusOBJ.HTML = this._scrubHTMLText(DOMElement.outerHTML); | |
} | |
/* execute plugins on StylusOBJ */ | |
this._postProcessStylusOBJ(StylusOBJ); | |
return StylusOBJ; | |
} | |
/** | |
* Prepares StylusOBJ by converting specifid VDOM to the proprietary format | |
*/ | |
_prepareStylusObject(VDOM) { | |
/* exit if current VDOM is null */ | |
if (VDOM === null) { | |
return false; | |
} | |
let ParentStylusOBJ = {}; | |
/* Convert root VDOM to StylusOBJ */ | |
ParentStylusOBJ = this._convertVDOMToStylusOBJ(VDOM); | |
return ParentStylusOBJ; | |
} | |
/** | |
* generate inline styles from StylusOBJ | |
*/ | |
_getInlineStyleFromStylusObject(StylusOBJ) { | |
let styles = []; | |
let StylusOBJStyles = StylusOBJ.properties.styles; | |
for (let key in StylusOBJStyles) { | |
if (StylusOBJStyles.hasOwnProperty(key)) { | |
styles.push(key + ":" + StylusOBJStyles[key]); | |
} | |
} | |
return "style=" + '"' + styles.join(';') + '"'; | |
} | |
/** | |
* Generate attributes from StylusOBJ | |
*/ | |
_getHTMLAttributesFromStylusObject(StylusOBJ) { | |
let attributes = []; // container to store attributes | |
let attrs = StylusOBJ.properties.attributes; | |
for (let key in attrs) { | |
if (attrs.hasOwnProperty(key)) { | |
attributes.push(key + '="' + attrs[key] + '"'); | |
} | |
} | |
/* push style attributes */ | |
let styles = this._getInlineStyleFromStylusObject(StylusOBJ); | |
attributes.push(styles); | |
return attributes.join(' '); | |
} | |
/** | |
* Convert StylusOBJ to representative HTML string | |
*/ | |
_convertStylusOBJToHTML(StylusOBJ) { | |
let HTMLContainer = []; /* array containing html tags which will be concatenated later */ | |
/* If object is leaf then return the stored html */ | |
if (StylusOBJ.isLeaf) { | |
return StylusOBJ.HTML; | |
} | |
/* In case of non leaf element generate html */ | |
let attributes = this._getHTMLAttributesFromStylusObject(StylusOBJ); | |
/* create opening tag of this element */ | |
let HTMLString = '<' + StylusOBJ.tagName + ' ' + attributes + ' >' | |
HTMLContainer.push(HTMLString); | |
/* Generate html of children */ | |
StylusOBJ.children.forEach(child => { | |
HTMLContainer.push(this._convertStylusOBJToHTML(child)); | |
}); | |
/* add closing tag of this object */ | |
HTMLContainer.push('</' + StylusOBJ.tagName + '>'); | |
return HTMLContainer.join(''); | |
} | |
/** | |
* Set html | |
*/ | |
setHTML(HTML) { | |
if (!this._loaded) { | |
return { | |
'status': 'false', | |
'message': 'SSDK has not finished loading data ....', | |
'error': 'SSDK_NOT_READY' | |
}; | |
} | |
if (!HTML) { | |
return { | |
'status': 'failed', | |
'message': 'empty html string provided', | |
'error': 'EMPTY_HTML' | |
}; | |
} | |
/* Removing whitespace from between the tags */ | |
HTML = HTML.trim().replace(/>\s+</g, '><'); | |
let response = this._prepareVDOM(HTML); | |
/* if html is valid only then set it to current html */ | |
if (!response.status) { | |
return { | |
'status': 'failed', | |
'message': 'invalid html sent', | |
errors: response.errors, | |
'error': 'INVALID_HTML' | |
} | |
} | |
/* setting valid HTML to current HTML */ | |
this._HTML = HTML; | |
this._current_VDOM = response.VDOM; | |
/* convert HTML to vdom */ | |
let stylusOBJ = this._prepareStylusObject(this._current_VDOM); | |
/* setting current stylusOBJ */ | |
this._current_SylusOBJ = stylusOBJ; | |
/* increment current StylusOBJ version and push to localstorage */ | |
this._current_StylusOBJ_version++; | |
let now = new Date().getTime(); | |
let hash = md5(JSON.stringify(this._current_SylusOBJ)); | |
this._stylusOBJContainer[this._current_StylusOBJ_version] = { | |
version: this._current_StylusOBJ_version, | |
stylusOBJ: Object.assign({}, this._current_SylusOBJ), | |
timestamp: now, | |
md5: hash | |
}; | |
let result = this._persistStylusIndex(); | |
if (result.status !== 'success') { | |
/* notify error */ | |
return { | |
'status': 'failed', | |
'message': 'unable to save in localstorage', | |
'error': 'LOCALSTORAGE_FAILED' | |
} | |
console.error('unable to save current version'); | |
} else { | |
/* upload changes to server in case of socket available and reconciliation is enabled */ | |
if (!(('NO_RECONCILE' in this._modifiers) && this._modifiers[ | |
'NO_RECONCILE'] === true)) { | |
this._facade({ | |
eventName: 'pushToSocket', | |
arguments: { | |
'event': 'upload', | |
'eventPayload': { | |
requestId: this._current_StylusOBJ_version, | |
payload: { | |
bookId: this._bookId, | |
version: this._current_StylusOBJ_version, | |
md5: hash, | |
timestamp: now, | |
stylusOBJ: Object.assign({}, this._current_SylusOBJ) | |
} | |
} | |
} | |
}); | |
} | |
return { | |
'status': 'success', | |
'message': 'saved successfully' | |
} | |
} | |
} | |
/** | |
* Generate html from StylusOBJ | |
*/ | |
getHTML() { | |
if (!this._loaded) { | |
return { | |
'status': 'false', | |
'message': 'SSDK has not finished loading data ....', | |
'error': 'SSDK_NOT_READY' | |
}; | |
} | |
/* return null if current stylus object is not defined or is malformed */ | |
if (this._current_SylusOBJ == null || (!(this._current_SylusOBJ instanceof Object))) { | |
return { | |
'status': 'error', | |
'message': 'Stylus object is not defined', | |
'error': 'UNDEFINED_STYLUS_OBJ' | |
}; | |
} | |
/* TODO: add validation for current Stylus OBJ formed */ | |
if (!this._validateStylusOBJ(this._current_SylusOBJ).status) { | |
return { | |
'status': 'error', | |
'message': 'Stylus object is corrupted', | |
'error': 'CORRUPTED_STYLUS_OBJ' | |
}; | |
} | |
let StylusOBJHTML = this._convertStylusOBJToHTML(this._current_SylusOBJ); | |
return { | |
status: 'success', | |
data: StylusOBJHTML | |
}; | |
} | |
/** | |
* Check manually if SSDK is ready | |
*/ | |
isReady() { | |
return this._loaded === true; | |
} | |
/* attaches callback to promise to report status of loading */ | |
ready(callback = null) { | |
/* attach callback to promise */ | |
if (callback) { | |
this._promise.then(callback, callback); | |
} | |
} | |
} | |
module.exports = StylusSDK; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Object related utils to be used project wide | |
*/ | |
/** | |
* Checks if specified object has nested attribute | |
* @param Object obj Object to be tested for presence of attribute | |
* @param String path Nested attribute selector in dot notation | |
* @return Boolean Returns true if object has this nested attribute | |
*/ | |
function hasAttr(obj, path='' /*, 'level1.level2 ... levelN' */) { | |
if (path == '') { | |
return false; | |
} | |
let selectors = path.split('.'); | |
for (let selector of selectors) { | |
if (!obj || !obj.hasOwnProperty(selector)) { | |
return false; | |
} | |
obj = obj[selector]; | |
} | |
return true; | |
} | |
/** | |
* Gets value of nessted attribute if it exists | |
* @param Object obj Object to be tested for presence of attribute | |
* @param String path Nested attribute selector in dot notation | |
* @return Boolean Returns true if object has this nested attribute | |
*/ | |
function getAttr(obj, path='' /*, 'level1.level2 ... levelN' */) { | |
if (path == '') { | |
return false; | |
} | |
let selectors = path.split('.'); | |
for (let selector of selectors) { | |
if (!obj || !obj.hasOwnProperty(selector)) { | |
return undefined; | |
} | |
obj = obj[selector]; | |
} | |
return obj; | |
} | |
/** | |
* Sets deeply nested attribute of the specified object | |
* @param Object obj Target object | |
* @param String path deeply nested path of attr in DOT notation | |
* @param Mixed value Value to be set to the path in object | |
*/ | |
function setAttr(obj, path, value) { | |
let schema = obj; // a moving reference to internal objects within obj | |
let pList = path.split('.'); | |
for(let path of pList.slice(0,-1)) { | |
if( !schema[path] ) schema[path] = {} | |
schema = schema[path]; | |
} | |
schema[pList[pList.length-1]] = value; | |
} | |
/** | |
* Copies properties from source object to target object based on mapperRegistry object | |
* mapperRegistry is a container of mapper objects | |
* mapper object contains from and to keys specfying source anc location paths to be transfered | |
* | |
* @param Object source Object from which propties will be copied | |
* @param Object target Object to which properties will be copied | |
* @param Object mapperRegistry container of mapper objects | |
*/ | |
function copyProps(source, target, mapperRegistry) { | |
mapperRegistry.forEach(mapping => { | |
if (mapping.from in source) { | |
if (typeof source[mapping.from] === 'object') { | |
setAttr(target, mapping.to, Object.assign({}, source[mapping.from])); | |
} | |
if ( ['number', 'string', 'boolean'].includes(typeof source[mapping.from]) ) { | |
setAttr(target, mapping.to, source[mapping.from]); | |
} | |
} | |
}); | |
return Object.assign({}, source); | |
} | |
export {hasAttr, copyProps, getAttr} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment