UNPKG

@onereach/webform

Version:

Content Builder includes several views for: - Content builder view itself; - Web Form view; - Slack block-kit builder;

1,021 lines (932 loc) 35.5 kB
/* eslint-disable no-template-curly-in-string */ /* eslint-disable camelcase */ import * as _ from 'lodash'; import espree from 'espree'; import Slimdown from './slimdown.js'; import slackdown from './slackdown.js'; import beautify from 'js-beautify'; export function previewMergeValues (str) { str = str && str.trim(); if (!str || str === '``') return ''; const mf_re = /^await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\((?:\{path:\s+'([\w\.]+)'\})?\)$/; const sg_re = /^await\s+this\.get(?:Shared|Global)?\((?:'([^']*?)'|"([^"]*?)"|`([^`]*?)`)\)$/; const hc_re = /^this\.(helpers|config|error)(\.[a-zA-Z_\$][\w\$]*)?$/; if ( ((!str.startsWith('`') || !str.endsWith('`')) && mf_re.test(str)) || sg_re.test(str) || hc_re.test(str) ) { str = '`${' + str + '}`'; } const escChars = {}; const previewString = str .replace(/\\./g, (match, offset) => { const key = `__escaped__${offset}__`; escChars[key] = match; return key; }) .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace( /("[\s\S]+?")|(`[\s\S]+?`)|('[\s\S]+?')/g, match => `<#<${match.slice(1, -1)}>#>` ) .replace(/>#>[\s\S]+?<#</g, '{code}') .replace(/(.+?<#<)|(>#>.+?)/g, '{code}') .replace(/<#<|>#>/g, String()) .replace( /\${this\.((helpers|config|error)(\.[a-zA-Z_\$][\w\$]*)?)}/g, (match, value) => `{${value}}` ) .replace( /\${await\s+this\.get(?:Shared|Global)?\((?:'([^']*?)'|"([^"]*?)"|`([^`]*?)`)\)}/g, (match, value) => `{${value}}` ) .replace( /\${await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\((?:\{path:\s+'([\w\.]+)'\})?\)}/g, (match, ...rest) => `{${rest[0] + (rest[1] ? `.${rest[1]}` : '')}}` ) .replace(/{.+?}/g, ' <span style="color: #64b2da">$&</span> ') .replace(/\n/g, '<br>') .replace(/^(<br>)+/, '') .replace(/__escaped__[0-9]+__/g, match => { return escChars[match][1] === 'n' ? '<br>' : escChars[match][1]; }); return previewString; } export function previewMergeValuesInInputs (str) { str = str && str.trim(); if (!str || str === '``') return ''; const mf_re = /^await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\((?:\{path:\s+'([\w\.]+)'\})?\)$/; const sg_re = /^await\s+this\.get(?:Shared|Global)?\((?:'([^']*?)'|"([^"]*?)"|`([^`]*?)`)\)$/; const hc_re = /^this\.(helpers|config|error)(\.[a-zA-Z_\$][\w\$]*)?$/; if ( ((!str.startsWith('`') || !str.endsWith('`')) && mf_re.test(str)) || sg_re.test(str) || hc_re.test(str) ) { str = '`${' + str + '}`'; } const escChars = {}; const previewString = str .replace(/\\./g, (match, offset) => { const key = `__escaped__${offset}__`; escChars[key] = match; return key; }) .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace( /("[\s\S]+?")|(`[\s\S]+?`)|('[\s\S]+?')/g, match => `<#<${match.slice(1, -1)}>#>` ) .replace(/>#>[\s\S]+?<#</g, '{code}') .replace(/(.+?<#<)|(>#>.+?)/g, '{code}') .replace(/<#<|>#>/g, String()) .replace( /\${this\.((helpers|config|error)(\.[a-zA-Z_\$][\w\$]*)?)}/g, (match, value) => `{${value}}` ) .replace( /\${await\s+this\.get(?:Shared|Global)?\((?:'([^']*?)'|"([^"]*?)"|`([^`]*?)`)\)}/g, (match, value) => `{${value}}` ) .replace( /\${await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\((?:\{path:\s+'([\w\.]+)'\})?\)}/g, (match, ...rest) => `{${rest[0] + (rest[1] ? `.${rest[1]}` : '')}}` ) // .replace(/{.+?}/g, ` <span style="color: #64b2da">$&</span> `) .replace(/\n/g, '<br>') .replace(/^(<br>)+/, '') .replace(/__escaped__[0-9]+__/g, match => { return escChars[match][1] === 'n' ? '<br>' : escChars[match][1]; }); return previewString; } export function sliceTicks (data) { if (_.isArray(data)) { return _.map(data, sliceTicks); } if (_.isObject(data)) { return _.mapValues(data, sliceTicks); } if (typeof data === 'string' && data.startsWith('`') && data.endsWith('`')) { return data.slice(1, -1); } return data; } export function prepareEmptyValue (label) { return ''; // return `<span class="empty-value">{{${label}}}</span>` } export function prepareRegularValue (value = '') { value = ' ' + value + ' '; const newLineRep = '__new_line_temp_replacement'; return _.chain(value) .replace(/\\`/g, '`') .replace(/\n/g, newLineRep) .replace(new RegExp('\\`{3}(.+?)\\`{3}', 'g'), (match, p1) => new RegExp(newLineRep, 'g').test(p1) ? `<span class=\"monospace-value block\">${p1}</span>` : `<span class=\"monospace-value\">${p1}</span>` ) .replace(new RegExp(newLineRep, 'g'), '\n') .replace( new RegExp('\\`{1}(.+?)\\`{1}', 'g'), '<span class="monospace-value text--red">$1</span>' ) .replace( new RegExp("\\${this(.session)?\\.get\\(\\'(.+?)\\'\\)}", 'g'), '<span class="merge-tag">{$2}</span>' ) .replace( /\${await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\((?:\{path:\s+'([\w\.]+)'\})?\)}/g, (match, ...rest) => `<span class=\"merge-tag\">{${rest[0] + (rest[1] ? `.${rest[1]}` : '')}}</span>` ) .replace( new RegExp('((^|\\s|~|\\*){1,})_(.+?)_((\\s|~|\\*|$){1,})', 'g'), '$1<span class="italized-value">$3</span>$4' ) .replace( new RegExp('((^|\\s|_|~){1,})\\*(.+?)\\*((\\s|_|~|$){1,})', 'g'), '$1<span class="bold-value">$3</span>$4' ) .replace( new RegExp('((^|\\s|_|\\*){1,})~(.+?)~((\\s|_|\\*|$){1,})', 'g'), '$1<span class="strinkethrough-value">$3</span>$4' ) .replace(/\n/g, '<br />') .value(); } export function prepareCompiledValue (previewString) { if (_.isEmpty(previewString)) return prepareEmptyValue(previewString); return prepareRegularValue(previewString); } export function getFormatedStringForWebForm (value = '') { if (!value || !value.trim()) return ''; const newLineRep = '__new_line_temp_replacement'; return _.chain(value) .replace(/\\`/g, '`') .replace(/\n/g, newLineRep) .replace(new RegExp('\\`{3}(.+?)\\`{3}', 'g'), (match, p1) => new RegExp(newLineRep, 'g').test(p1) ? `<span class=\"monospace-value block\">${p1}</span>` : `<span class=\"monospace-value\">${p1}</span>` ) .replace(new RegExp(newLineRep, 'g'), '\n') .replace( new RegExp('\\`{1}(.+?)\\`{1}', 'g'), '<span class="monospace-value text--red">$1</span>' ) .replace( new RegExp('((^|\\s|~|\\*){1,})_(.+?)_((\\s|~|\\*|$){1,})', 'g'), '$1<span class="italized-value">$3</span>$4' ) .replace( new RegExp('((^|\\s|_|~){1,})\\*(.+?)\\*((\\s|_|~|$){1,})', 'g'), '$1<span class="bold-value">$3</span>$4' ) .replace( new RegExp('((^|\\s|_|\\*){1,})~(.+?)~((\\s|_|\\*|$){1,})', 'g'), '$1<span class="strinkethrough-value">$3</span>$4' ) .replace(/\n/g, '<br />') .value(); } // unused. left for the future purpose export function parseMarkdown (str = '') { str = str.replace(/\\`/g, '`'); const sd = new Slimdown(); return sd.render(str); } // unused. left for the future purpose export function parseSlack (str = '') { str = str.replace(/\\`/g, '`'); const sd = slackdown(); return sd.parse(str); } export function getSlackFormattedDate (date) { if (!Date.parse(date)) { return; } date = new Date(date); const year = date.getFullYear(); const month = (1 + date.getMonth()).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return month + '/' + day + '/' + year; } export const capitalize = (s) => { if (typeof s !== 'string') return ''; return s.charAt(0).toUpperCase() + s.slice(1); }; const restrictFocusDisabledClass = 'restrict-focus-disabled'; const SYSTEM_PREFIX = '#'; // eslint-disable-next-line valid-jsdoc /** * Present merge-tag value in readable form * @param {string} [value=''] value to present * @param {{mergeTagLeftWrapper: string, mergeTagRightWrapper: string}} [options={}] * @returns {string} readable presentation for merge-tag */ export function getReadableMergeTag (value = '', options) { const mergeTagLeftWrapper = _.get(options, 'mergeTagLeftWrapper', '{'); const mergeTagRightWrapper = _.get(options, 'mergeTagRightWrapper', '}'); return _.chain(value) .replace(/^[`'"](.*)[`'"]$/g, '$1') .replace( /this\.get\([`'"](.*)[`'"]\)/g, `${mergeTagLeftWrapper}$1${mergeTagRightWrapper}` ) .replace( /this\.session\.get\([\`'"](.*)[\`'"]\)/g, `${mergeTagLeftWrapper}#session.$1${mergeTagRightWrapper}` ) .replace( /await this\.getShared\([\`'"](.*)[\`'"]\)/g, `${mergeTagLeftWrapper}#shared.$1${mergeTagRightWrapper}` ) .replace( /await this\.getGlobal\([\`'"](.*)[\`'"]\)/g, `${mergeTagLeftWrapper}#global.$1${mergeTagRightWrapper}` ) .replace(/this\.(.+)/, `${mergeTagLeftWrapper}#$1${mergeTagRightWrapper}`) .value(); } /** * Convert all merge-tags in a string to a readable form * @param {string} [value=''] value to present * @param {{mergeTagLeftWrapper: string, mergeTagRightWrapper: string}} [options={}] * @returns {string} readable presentation for merge-tag */ export function convertMergeTagsToReadableForm (value = '', options) { const mergeTagLeftWrapper = _.get(options, 'mergeTagLeftWrapper', '{'); const mergeTagRightWrapper = _.get(options, 'mergeTagRightWrapper', '}'); return _.chain(value) .replace(/^[`'"](.*)[`'"]$/g, '$1') .replace( /\$\{this\.get\([`'"](.*?)[`'"]\)\}/g, `${mergeTagLeftWrapper}$1${mergeTagRightWrapper}` ) .replace( /\$\{this\.session\.get\([\`'"](.*?)[\`'"]\)\}/g, `${mergeTagLeftWrapper}#session.$1${mergeTagRightWrapper}` ) .replace( /\$\{await this\.getShared\([\`'"](.*?)[\`'"]\)\}/g, `${mergeTagLeftWrapper}#shared.$1${mergeTagRightWrapper}` ) .replace( /\$\{await this\.getGlobal\([\`'"](.*?)[\`'"]\)\}/g, `${mergeTagLeftWrapper}#global.$1${mergeTagRightWrapper}` ) .replace( /\$\{this\.(.+?)\}/g, `${mergeTagLeftWrapper}#$1${mergeTagRightWrapper}` ) .value(); } /** * Present text-expression value in readable form * @param {string} [value=''] value to present * @param {{mergeTagLeftWrapper: string, mergeTagRightWrapper: string}} [options={}] * @returns {string} readable presentation for text-expression */ export function getReadableTextExpression (value = '', options) { const mergeTagValue = _.replace(value, /\$\{(.*)\}/g, '$1'); return getReadableMergeTag(mergeTagValue, options); } /** * converts tags with merge fields to actual code * @param {string} full path to get value from * @returns {string} readable presentation for text-expression */ function _convertMergeFieldTag (fullPath) { const keyPaths = _.split(fullPath, '.'); const dataOutName = keyPaths.splice(0, 1); const path = keyPaths.join('.'); return `await this.mergeFields['${dataOutName}'].get(${ path ? `{path: '${path}'}` : '' })`; } // eslint-disable-next-line valid-jsdoc /** * Convert readable form of expression to actual code * @param {string} [value=''] value to present * @returns {string} actual expression value */ export function convertMergeTagToCode (tag) { if (_.isEqual(tag, `${SYSTEM_PREFIX}error`)) { return 'this.error'; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}error.`)) { return `this.error.${_.replace( tag, new RegExp(`^${SYSTEM_PREFIX}error\.`), '' )}`; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}session.`)) { return `this.session.get('${_.replace( tag, new RegExp(`^${SYSTEM_PREFIX}session\.`), '' )}')`; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}helpers.`)) { return `this.helpers.${_.replace( tag, new RegExp(`^${SYSTEM_PREFIX}helpers\.`), '' )}`; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}config.`)) { return `this.config.${_.replace( tag, new RegExp(`^${SYSTEM_PREFIX}config\.`), '' )}`; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}request.`)) { return `this.request.${_.replace( tag, new RegExp(`^${SYSTEM_PREFIX}request\.`), '' )}`; } else if (_.startsWith(tag, `${SYSTEM_PREFIX}shared.`)) { const fullPath = _.replace( tag, new RegExp(`^${SYSTEM_PREFIX}shared\.`), '' ); return _convertMergeFieldTag(fullPath); } else if (_.startsWith(tag, `${SYSTEM_PREFIX}global.`)) { const fullPath = _.replace( tag, new RegExp(`^${SYSTEM_PREFIX}global\.`), '' ); return _convertMergeFieldTag(fullPath); } else { return _convertMergeFieldTag(tag); } } /** * Get middle part of the string representing parts of template string like * `string${, `string`, }string`, }string${ and `string` * @param {Object} token token to parse * @returns {string} extracted string from text expression */ function _extractStringFromTemplateToken (token) { if (token.type !== 'Template') { return ''; } if (token.value.substr(-2, 2) === '${') { return token.value.substring(1, token.value.length - 2); } return token.value.substring(1, token.value.length - 1); } /** * Get string value from '' and "" strings or from string part of template string * (e.g. `string${, `string`, }string`, }string${ and `string`) * @param {Object} token token to parse * @returns {string} properly formatted value */ function _extractStringValue (token) { const value = token.value; const slashSubstitute = '#sweet_slash_of_mine_823cd925_slash#'; if (_.head(value) === "'") { return _.chain(value.substring(1, value.length - 1)) .replace(/\\\\/g, slashSubstitute) .replace(/\\/g, '') .replace(new RegExp(slashSubstitute, 'g'), '\\') .value(); } if (_.head(value) === '"') { return JSON.parse(value); } return _.chain(token) .thru(_extractStringFromTemplateToken) .replace(/\\\\/g, slashSubstitute) .replace(/\\/g, '') .replace(new RegExp(slashSubstitute, 'g'), '\\') .value(); } /** * Does a given token represent beginning of template string (e.g. `string ${ or `string`) * @param {Object} token token to parse * @returns {Boolean} true if token is in a start of template token */ function _isStartTemplateToken (token) { return token.type === 'Template' && _.head(token.value) === '`'; } /** * Does a given token represent ending of template string (e.g. }string` or `string`) * @param {Object} token token to parse * @returns {boolean} true if token is last token in a template */ function _isEndTemplateToken (token) { return token.type === 'Template' && _.last(token.value) === '`'; } /** * Does a given token represent beginning of template string expression (e.g. "something${") * @param {Object} token token to parse * @returns {boolean} true if token is first token in a string template expression ${ */ function _isStartTemplateExprToken (token) { return token.type === 'Template' && token.value.substr(-2, 2) === '${'; // eslint-disable-line no-magic-numbers } /** * Does a given token represent ending of template string expression (e.g. "}something") * @param {Object} token token to parse * @returns {boolean} true if token is last token in a string template expression } */ function _isEndTemplateExprToken (token) { return token.type === 'Template' && _.head(token.value) === '}'; } /** * From a given array of token extact all tokens part of a template string ${} expression * @param {Array} tokens - tokens array to look in * @param {Number} startWith - a position in array to start search from * @returns {Array} - token belonging to template string ${} expression */ function _getAllTemplateExprTokens (tokens, startWith) { const exprTokens = [tokens[startWith]]; let endReached = false; /* eslint-disable */ for (let i = startWith + 1; i < tokens.length && !endReached; i++) { exprTokens.push(tokens[i]); if (_isEndTemplateExprToken(tokens[i])) { endReached = true; } } /* eslint-enable */ return exprTokens; } /** * Figure out if a given tokens array represent ${this.get('stuff')} js expression * @param {Array} tokens list of token to parse * @returns {boolean} returns true if tokens represent get expression */ function _isGetExpressionTokens (tokens) { return ( tokens.length === 8 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'this' && tokens[2].value === '.' && tokens[3].value === 'get' && tokens[4].value === '(' && tokens[5].type === 'String' && tokens[6].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${this.session.get('stuff')} js expression * @param {Array} tokens list of token to parse * @returns {boolean} returns true if tokens represent session get expression */ function _isSessionGetExpressionTokens (tokens) { return ( tokens.length === 10 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'this' && tokens[2].value === '.' && tokens[3].value === 'session' && tokens[4].value === '.' && tokens[5].value === 'get' && tokens[6].value === '(' && tokens[7].type === 'String' && tokens[8].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${this.get('stuff', 'default')} js expression * @param {Array} tokens tokens to parse * @returns {boolean} returns true if token represent get expression with default value */ function _isGetDefaultExpressionTokens (tokens) { return ( tokens.length === 10 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'this' && tokens[2].value === '.' && tokens[3].value === 'get' && tokens[4].value === '(' && tokens[5].type === 'String' && tokens[6].value === ',' && tokens[7].type === 'String' && tokens[8].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${this.session.get('stuff', 'default')} js expression * @param {Array} tokens tokens to parse * @returns {boolean} returns true if token represent session get expression with default value */ function _isSessionGetDefaultExpressionTokens (tokens) { return ( tokens.length === 12 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'this' && tokens[2].value === '.' && tokens[3].value === 'session' && tokens[4].value === '.' && tokens[5].value === 'get' && tokens[6].value === '(' && tokens[7].type === 'String' && tokens[8].value === ',' && tokens[9].type === 'String' && tokens[10].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${await this.[functionName]('stuff')} js expression * @param {Array} tokens list of token to parse * @param {String} functionName that should be parsed as getter * @returns {boolean} returns true if tokens represent session get expression */ function _isAsyncFunctionExpressionTokens (tokens, functionName) { return ( tokens.length === 9 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'await' && tokens[2].value === 'this' && tokens[3].value === '.' && tokens[4].value === functionName && tokens[5].value === '(' && tokens[6].type === 'String' && tokens[7].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${await this.[functionName]('stuff', 'default')} js expression * @param {Array} tokens tokens to parse * @param {String} functionName that should be parsed as getter * @returns {boolean} returns true if token represent session get expression with default value */ function _isAsyncFunctionDefaultExpressionTokens (tokens, functionName) { return ( tokens.length === 11 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'await' && tokens[2].value === 'this' && tokens[3].value === '.' && tokens[4].value === functionName && tokens[5].value === '(' && tokens[6].type === 'String' && tokens[7].value === ',' && tokens[8].type === 'String' && tokens[9].value === ')' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${await this.mergeFields[mergeFieldName].get(...)} js expression * @param {Array} tokens tokens to parse * @param {String} functionName that should be parsed as getter * @returns {boolean} returns true if token represent session get expression with default value */ function _isAsyncFunctionMergeFieldExpressionTokens (tokens) { return ( tokens.length >= 13 && _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'await' && tokens[2].value === 'this' && tokens[3].value === '.' && tokens[4].value === 'mergeFields' && tokens[5].value === '[' && tokens[6].type === 'String' && tokens[7].value === ']' && tokens[8].value === '.' && tokens[9].value === 'get' && tokens[10].value === '(' && _isEndTemplateExprToken(_.last(tokens)) ); } /** * Figure out if a given tokens array represent ${this.helpers.stuff} (or other allowed services) js expression * @param {Array} tokens tokens to parse * @returns {Boolean} returns true if token represent allowed expression */ function _isAllowedExpressionTokens (tokens) { const allowedConditionStart = _isStartTemplateExprToken(_.head(tokens)) && tokens[1].value === 'this' && tokens[2].value === '.' && _.includes(['helpers', 'request', 'config', 'error'], tokens[3].value); return ( (tokens.length === 5 && allowedConditionStart) || (tokens.length > 5 && allowedConditionStart && _isEndTemplateExprToken(_.last(tokens))) ); } /** * Extracts path part from ${this.get('path')} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|string} returns '' if there're no expression tokens or object with path instead */ function _extractGetExpressionFromTokens (tokens) { if (!_isGetExpressionTokens(tokens)) { return ''; } return { path: _extractStringValue(tokens[5]) }; } /** * Extracts 'path' and 'default' parts from ${this.get('path', default)} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractGetDefaultExpressionFromTokens (tokens) { if (!_isGetDefaultExpressionTokens(tokens)) { return ''; } return { path: _extractStringValue(tokens[5]), defaultValue: _extractStringValue(tokens[7]) }; } /** * Extracts path part from ${this.get('path')} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|string} returns '' if there're no expression tokens or object with path instead */ function _extractSessionGetExpressionFromTokens (tokens) { if (!_isSessionGetExpressionTokens(tokens)) { return ''; } return { path: `${SYSTEM_PREFIX}session.${_extractStringValue(tokens[7])}` }; } /** * Extracts 'path' and 'default' parts from ${this.get('path', default)} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractSessionGetDefaultExpressionFromTokens (tokens) { if (!_isSessionGetDefaultExpressionTokens(tokens)) { return ''; } return { path: `${SYSTEM_PREFIX}session.${_extractStringValue(tokens[7])}`, defaultValue: _extractStringValue(tokens[9]) }; } /** * Extracts path part from ${await this.getShared('path')} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|string} returns '' if there're no expression tokens or object with path instead */ function _extractSharedGetExpressionFromTokens (tokens) { if (!_isAsyncFunctionExpressionTokens(tokens, 'getShared')) { return ''; } return { path: `${SYSTEM_PREFIX}shared.${_extractStringValue(tokens[6])}` }; } /** * Extracts 'path' and 'default' parts from ${await this.getShared('path', default)} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractSharedGetDefaultExpressionFromTokens (tokens) { if (!_isAsyncFunctionDefaultExpressionTokens(tokens, 'getShared')) { return ''; } return { path: `${SYSTEM_PREFIX}shared.${_extractStringValue(tokens[6])}`, defaultValue: _extractStringValue(tokens[8]) }; } /** * Extracts path part from ${await this.getGlobal('path')} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|string} returns '' if there're no expression tokens or object with path instead */ function _extractGlobalGetExpressionFromTokens (tokens) { if (!_isAsyncFunctionExpressionTokens(tokens, 'getGlobal')) { return ''; } return { path: `${SYSTEM_PREFIX}global.${_extractStringValue(tokens[6])}` }; } /** * Extracts 'path' and 'default' parts from ${await this.getGlobal('path', default)} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractGlobalGetDefaultExpressionFromTokens (tokens) { if (!_isAsyncFunctionDefaultExpressionTokens(tokens, 'getGlobal')) { return ''; } return { path: `${SYSTEM_PREFIX}global.${_extractStringValue(tokens[6])}`, defaultValue: _extractStringValue(tokens[8]) }; } /** * Extracts 'path' and 'default' parts from ${await this.getGlobal('path', default)} expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractMergeFieldExpressionFromTokens (tokens) { const path = _.get( _.map(tokens, 'value') .join('') .match(/\bpath: *'(.*?)'/), '[1]' ); const dafalutValue = _.get( _.map(tokens, 'value') .join('') .match(/\bdefaultValue: *'(.*?)'/), '[1]' ); /* console.log('path', { path, dafalutValue }); */ return { path: `${_extractStringValue(tokens[6])}${path ? `.${path}` : ''}`, defaultValue: dafalutValue }; } /** * Extracts 'path' part from ${this.helpers.path} (or another allowed service) expression * and returns object containing this info * @param {Array} tokens list of token * @returns {Object|String} returns '' if there're no expression tokens or object with path instead */ function _extractAllowedExpressionFromTokens (tokens) { if (!_isAllowedExpressionTokens(tokens)) { return ''; } const path = _.reduce( tokens, (memo, token, index) => { if (index > 4 && index < tokens.length - 1) { memo += token.value; } return memo; }, '' ); return { path: `${SYSTEM_PREFIX}${tokens[3].value}${path ? `.${path}` : ''}` }; } /** * Figure out if a given expression is 'string', "string", `string ${this.get('stuff', 'default')}`, * `string ${this.session.get('stuff', 'default')}`, `string ${this.helpers.stuff}` * @param {string} expression expression to extract from * @returns {Array|undefined} list of expressions */ export function stringifyExpression (expression) { // eslint-disable-line complexity let tokens; try { const espreeOptions = { ecmaVersion: 9, ecmaFeatures: { globalReturn: true, impliedStrict: true } }; tokens = espree.tokenize(expression, espreeOptions); } catch (e) { console.error('catched error -> ', e); return; } const firstToken = _.head(tokens); // expression is a simple single- or double-quoted string if (tokens.length === 1 && firstToken.type === 'String') { return [_extractStringValue(firstToken)]; } // expression is a "plain" string expression/template (e.g. `plain string`) if (tokens.length === 1 && _isStartTemplateToken(firstToken) && _isEndTemplateToken(firstToken)) { return [_extractStringValue(firstToken)]; } // expression is a string expression/template (e.g. `string ${template}`) if it starts and ends with ` if (tokens.length > 1 && _isStartTemplateToken(firstToken) && _isEndTemplateToken(_.last(tokens))) { // fill in first "string" part of the text even if it's empty const result = [_extractStringValue(firstToken)]; let i = 0; // check ${} parts of string template while (i < tokens.length - 1) { const templateExprTokens = _getAllTemplateExprTokens(tokens, i); const templateExprTokensLength = templateExprTokens.length; // if next tokens represent ${this.get('stuff')} expression extract them and look further if (_isGetExpressionTokens(templateExprTokens)) { result.push(_extractGetExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.get('stuff', 'default')} expression extract them and look further } else if (_isGetDefaultExpressionTokens(templateExprTokens)) { result.push(_extractGetDefaultExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.session.get('stuff')} expression extract them and look further } else if (_isSessionGetExpressionTokens(templateExprTokens)) { result.push(_extractSessionGetExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.session.get('stuff', 'default')} expression extract them and look further } else if (_isSessionGetDefaultExpressionTokens(templateExprTokens)) { result.push(_extractSessionGetDefaultExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.shared.get('stuff')} (or other "supported" service call) expression extract them and look further } else if (_isAsyncFunctionExpressionTokens(templateExprTokens, 'getShared')) { result.push(_extractSharedGetExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.shared.get('stuff', 'default')} expression extract them and look further } else if (_isAsyncFunctionDefaultExpressionTokens(templateExprTokens, 'getShared')) { result.push(_extractSharedGetDefaultExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.helpers.stuff} (or other "supported" service call) expression extract them and look further } else if (_isAsyncFunctionExpressionTokens(templateExprTokens, 'getGlobal')) { result.push(_extractGlobalGetExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.shared.get('stuff', 'default')} expression extract them and look further } else if (_isAsyncFunctionDefaultExpressionTokens(templateExprTokens, 'getGlobal')) { result.push(_extractGlobalGetDefaultExpressionFromTokens(templateExprTokens)); // if next tokens represent ${this.helpers.stuff} (or other "supported" service call) expression extract them and look further } else if (_isAsyncFunctionMergeFieldExpressionTokens(templateExprTokens)) { result.push(_extractMergeFieldExpressionFromTokens(templateExprTokens)); } else if (_isAllowedExpressionTokens(templateExprTokens)) { result.push(_extractAllowedExpressionFromTokens(templateExprTokens)); // otherwise it's not convertible } else { return; } // extract string value that goes after get expression result.push(_extractStringValue(tokens[i + templateExprTokensLength - 1])); i += templateExprTokensLength - 1; } return _.compact(result); } } export const hasMergeField = (value) => { const mergeFieldRegex = /await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\(.*?\)|\$\{[^\}]*\}|this(\.session)?\.get(Shared|Global)?\(.*?\)|this\.(helpers|config|error)(\.[\w\$]+)?/g; return mergeFieldRegex.test(value); }; export const wait = (fn, time = 1000) => { return new Promise(resolve => { setTimeout(() => { fn(); resolve(); }, time); }); }; export function beautifyCode (source, options) { const opts = options || { indent_size: 2 }; return beautify(source, opts); } export function stringifyValue (value) { const strValue = JSON.stringify(value, null, 2); // RegExp: all kinds of merge fields const mergeFieldRegex = /"(await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\(.*?\)|this(\.session)?\.get(Shared|Global)?\(.*?\)|this\.(helpers|config|error)(\.[\w\$]+)?)"/g; // RegExp: "`...`" const templateRegex = /"(`[^\$`]*(\$\{[^\}]*\})?[^`]*?`)"/g; // RegExp: "[`..`]: ..." const keyTemplateRegexp = /"(\[`[^\$`]*(\$\{[^\}]*\})?[^`]*?`\])":/g; // RegExp: "await (async () => ...)()" const codeRegExp = /"(await\s*\(async\s+\([^)]*?\)\s*=>\s*([\s\S])*?\)\([^)]*?\))"/g; const replaced = strValue .replace(codeRegExp, (match, p1) => { const bodyReg = /await\s*\(async\s+\([^)]*?\)\s*=>\s*([\s\S]*?)\)\([^)]*?\)/; const bodyMatch = p1.match(bodyReg); if (bodyMatch) { const body = bodyMatch[1]; return p1.replace(body, JSON.parse(`"${body}"`)); } }) .replace(keyTemplateRegexp, '$1:') .replace(templateRegex, '$1') .replace(mergeFieldRegex, '$1'); return beautifyCode(replaced); } // remove beautify identation function removeIdentation (str) { return str.replace(/\n[\s]*/g, ''); } export function parseCodeInput (value) { const wrapInAsyncFn = /await\s*\(async\s+\([^)]*?\)\s*=>\s*([\s\S]*?)\)\([^)]*?\)/; const keyTemplateRegexp = /(\[`[^\$`]*(\$\{[^\}]*\})?[^`]*?`\]):/; const codeRegExp = /(\[`[^\$`]*(\$\{[^\}]*\})?[^`]*?`\]):|await\s*\(async\s+\([^)]*?\)\s*=>\s*([\s\S]*?)\)\([^)]*?\)|`[^`\$]*(\$\{[^\}]*\})?[^`]*?`|await\s+this\.mergeFields\['(#?[\w\d]*?)'\]\.get\([\s\S]*?\)|this(\.session)?\.get(Shared|Global)?\(.*?\)|this\.(helpers|config|error)(\.[\w\$]+)?/g; const replaced = value .replace(codeRegExp, (match, p1) => { if (keyTemplateRegexp.test(match)) return `"${p1}":`; if (wrapInAsyncFn.test(match)) { const asyncWrapMatch = match.match(wrapInAsyncFn); const body = asyncWrapMatch[1]; return `"${match.replace(body, JSON.stringify(removeIdentation(body)).slice(1, -1))}"`; } const expr = removeIdentation(match); return `"${expr}"`; }); let parsed; try { parsed = JSON.parse(replaced); } finally { return parsed; } } export const isCodeValue = (value) => { const codeRegExp = /await\s*\(async\s+\([^)]*?\)\s*=>\s*([\s\S]*?)\)\([^)]*?\)/g; return codeRegExp.test(value); }; export const clearLSByVuexPersistedKey = () => { window.localStorage.removeItem('_secure__ls__metadata'); _.forIn(window.localStorage, (value, objKey) => { if (_.startsWith(objKey, 'vuex_') === true) { window.localStorage.removeItem(objKey); } }); };