Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into master
  • Loading branch information
alexprey committed Jan 25, 2021
2 parents 31ea048 + 660d246 commit 70a308c
Show file tree
Hide file tree
Showing 10 changed files with 460 additions and 36 deletions.
14 changes: 14 additions & 0 deletions examples/Alert.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@
"type": "any"
}
},
{
"visibility": "private",
"description": null,
"keywords": [],
"name": "EVENT",
"kind": "const",
"static": false,
"readonly": true,
"type": {
"kind": "type",
"text": "any",
"type": "any"
}
},
{
"visibility": "public",
"description": null,
Expand Down
6 changes: 5 additions & 1 deletion examples/Alert.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
*/
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const EVENT = {
CLOSE: "close"
}
export let closable = false;
</script>
Expand All @@ -27,7 +31,7 @@
The `close` event fired when user click to X button in the panel.
@event CloseEvent#close
-->
<button on:click={() => dispatch('close')}>&times;</button>
<button on:click={() => dispatch(EVENT.CLOSE)}>&times;</button>
</div>
{/if}
</div>
28 changes: 20 additions & 8 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,17 @@ class Parser extends EventEmitter {
const token = tokens[i];

if (token.type === 'Identifier' && token.value === 'fire') {
if (!tokens[i + 2]) {
const nextIndex = i + 2;

if (!tokens[nextIndex]) {
break;
}

if (!this.features.includes('events')) {
continue;
}

const next = tokens[i + 2];
const next = tokens[nextIndex];
const event = {
name: null,
parent: null,
Expand Down Expand Up @@ -524,20 +526,30 @@ class Parser extends EventEmitter {
break;

case 'Identifier':
event.name = utils.getIdentifierValue(
tokens, next.value, next.range[0]);
if (next.value in this.identifiers) {
const startingAtFirstArg = tokens.slice(nextIndex);

const chain = utils.buildPropertyAccessorChainFromTokens(startingAtFirstArg);

event.name = utils.getValueForPropertyAccessorChain(this.identifiers, chain);
}

if (!event.name) {
event.name = utils.getIdentifierValue(
tokens, next.value, next.range[0]);

if (typeof event.name === 'object') {
event.name = utils.getIdentifierValueFromStart(
this.ast.tokens, event.name.notFoundIdentifier);
if (typeof event.name === 'object') {
event.name = utils.getIdentifierValueFromStart(
this.ast.tokens, event.name.notFoundIdentifier);
}
}

break;
}
}

if (!event.name) {
event.name = '****unhandled-event-name****';
event.name = utils.UNHANDLED_EVENT_NAME;
} else {
if (hasOwnProperty(this.eventsEmitted, event.name)) {
const emitedEvent = this.eventsEmitted[event.name];
Expand Down
189 changes: 181 additions & 8 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const RE_VISIBILITY = new RegExp(`^(${VISIBILITIES.join('|')})$`);
const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig;

const DEFAULT_VISIBILITY = 'public';
const UNHANDLED_EVENT_NAME = '****unhandled-event-name****';

const isVisibilitySupported = (v) => RE_VISIBILITY.test(v);

Expand Down Expand Up @@ -112,30 +113,30 @@ class NodeFunction {
}

const value = (property) => {
if (property.key.type === 'Literal') {
property.key.name = property.key.value;
}
const keyName = property.key.type === 'Literal'
? property.key.value
: property.key.name;

switch (property.value.type) {
case 'Literal':
return { [property.key.name]: property.value.value };
return { [keyName]: property.value.value };

case 'Identifier':
return {
[property.key.name]: property.value.name === 'undefined'
[keyName]: property.value.name === 'undefined'
? undefined
: property.value.name
};

case 'ObjectExpression':
return { [property.key.name]: values(property) };
return { [keyName]: values(property) };

case 'FunctionExpression':
case 'ArrowFunctionExpression':
return { [property.key.name]: new NodeFunction(property.value) };
return { [keyName]: new NodeFunction(property.value) };
}

return { [property.key.name]: property.value };
return { [keyName]: property.value };
};

const values = (entry) => {
Expand All @@ -153,6 +154,173 @@ const values = (entry) => {

return values;
};
/**
* @param {{ type: string; value: string }} token the Node token that needs to be tested
* @param {string} which a punctuator value to compare the token's value against
* @returns true if token is a punctuator with the correct value (if provided)
*/
const isPunctuatorToken = (token, which = undefined) => {
if (!token) {
return false;
}

const isPunctuator = token.type === 'Punctuator';
const isSpecific = which === undefined || token.value === which;

return isPunctuator && isSpecific;
};

/**
* The array of tokens provided must start at the first identifier of the
* chain, but it can go beyond the last identifier. Only the chained
* identifiers will be parsed.
*
* See {@link buildPropertyAccessorChainFromAst} examples for
* expected returned values.
*
* @param {{ type: string; value: string }[]} tokens
*/
const buildPropertyAccessorChainFromTokens = (tokens) => {
const next = tokens[0];

if (!next) {
return [];
}

if (!next.type === 'Identifier') {
return [];
}

const chain = [next.value];

let punctIndex = 1;
let isChained = isPunctuatorToken(tokens[punctIndex], '.');
let chained = tokens[punctIndex + 1];

while (isChained && chained && chained.type === 'Identifier') {
chain.push(chained.value);
punctIndex += 2;
isChained = isPunctuatorToken(tokens[punctIndex], '.');
chained = tokens[punctIndex + 1];
}

return chain;
};

/**
* Builds an array of property names from a 'MemberExpression' node.
* - Supports nested 'MemberExpression' and 'Identifier' nodes
* - Does not support bracket notation (computed === true).
*
* If the ast contains unsupported nodes, an empty array is returned.
*
* @example
* dispatch(PLAIN.NESTED.INNER);
* // Parsing the 'MemberExpression' node
* // corresponding to 'PLAIN.NESTED.INNER'
* // would return:
* ['PLAIN', 'NESTED', 'INNER']
*
* @example
* dispatch(PLAIN['NESTED'].INNER);
* // Parsing the 'MemberExpression' node
* // corresponding to 'PLAIN['NESTED'].INNER'
* // would return:
* []
*
* @param {{ type: string; object: any, property: any, computed: boolean }} node
* @returns an array of property names built from the ast
*/
const buildPropertyAccessorChainFromAst = (node) => {
if (node.type === 'Identifier') {
return [node.name];
}

if (node.type !== 'MemberExpression') {
return [];
}

const chain = [];

if (!node.computed) {
// Dot notation
chain.push(...buildPropertyAccessorChainFromAst(node.object));
chain.push(node.property.name);
} else {
// TODO: Support bracket notation
chain.push(undefined);
}

return chain.includes(undefined) ? [] : chain;
};

/**
* Builds an object expression (i.e. { ... }) from an 'ObjectExpression' node.
* Supports a limited range of property types:
* - 'ObjectExpression' (nested)
* - 'Literal' (string, int, boolean, etc)
*
* @param {{ type: 'ObjectExpression'; properties: any[] }} node
*/
const buildObjectFromObjectExpression = (node) => {
if (node.type !== 'ObjectExpression') {
throw new TypeError("Node must be of type 'ObjectExpression' but is", node.type);
}

const obj = {};

node.properties.forEach((property) => {
if (property.value.type === 'ObjectExpression') {
obj[property.key.name] = buildObjectFromObjectExpression(property.value);
} else if (property.value.type === 'Literal') {
obj[property.key.name] = property.value.value;
}
});

return obj;
};

/**
* Supports a limited range of property types:
* - 'ObjectExpression' (nested)
* - 'Literal' (string, int, boolean, etc)
*
* If the `chain` visits an unsupported node type or tries to access a
* non-existing node, a default value is returned instead.
*
* @throws TypeError when argument `chain` is not an array of strings
* @param {Record<string, { type: string; value?: any }>} record identifier keys mapped to ast node values
* @param {string[]} chain an array of string used to access a value in `record`
* @returns the value found in `record` for the provided accessor `chain`
*/
const getValueForPropertyAccessorChain = (record, chain) => {
if (!chain.every(s => typeof s === 'string')) {
throw new TypeError('Unsupported PropertyAccessorChain:' +
`Expected 'chain' to be an array of strings but it was ${chain}`);
}

const rootExpression = record[chain[0]];

if (rootExpression.type === 'Literal') {
return rootExpression.value;
}

if (rootExpression.type !== 'ObjectExpression') {
return UNHANDLED_EVENT_NAME;
}

let current = buildObjectFromObjectExpression(rootExpression);

for (const identifier of chain.slice(1)) {
current = current[identifier];

if (!current) {
return UNHANDLED_EVENT_NAME;
}
}

return current;
};

const tokensInterval = (tokens, range) => {
return tokens.filter((item) => {
Expand Down Expand Up @@ -299,13 +467,18 @@ const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj,

module.exports.VISIBILITIES = VISIBILITIES;
module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY;
module.exports.UNHANDLED_EVENT_NAME = UNHANDLED_EVENT_NAME;
module.exports.isVisibilitySupported = isVisibilitySupported;
module.exports.getVisibility = getVisibility;
module.exports.parseComment = parseComment;
module.exports.getCommentFromSourceCode = getCommentFromSourceCode;
module.exports.NodeFunction = NodeFunction;
module.exports.value = value;
module.exports.values = values;
module.exports.buildObjectFromObjectExpression = buildObjectFromObjectExpression;
module.exports.buildPropertyAccessorChainFromAst = buildPropertyAccessorChainFromAst;
module.exports.buildPropertyAccessorChainFromTokens = buildPropertyAccessorChainFromTokens;
module.exports.getValueForPropertyAccessorChain = getValueForPropertyAccessorChain;
module.exports.tokensInterval = tokensInterval;
module.exports.getIdentifierValue = getIdentifierValue;
module.exports.getIdentifierValueFromStart = getIdentifierValueFromStart;
Expand Down
Loading

0 comments on commit 70a308c

Please sign in to comment.