cst
Version:
JavaScript CST Implementation
963 lines (844 loc) • 29.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
var _utilsLines = require('../utils/lines');
var _ElementAssert = require('./ElementAssert');
var _ElementAssert2 = _interopRequireDefault(_ElementAssert);
/**
* Base class for Node, Token and Fragment.
*
* @name Element
* @class
* @abstract
*/
var Element = (function () {
/**
* @param {String} type
* @param {Element[]} children
*/
function Element(type, children) {
_classCallCheck(this, Element);
this._type = type;
this._firstChild = null;
this._lastChild = null;
this._parentElement = null;
this._nextSibling = null;
this._previousSibling = null;
this._childElements = [];
if (children) {
for (var i = 0; i < children.length; i++) {
if (children[i]._parentElement) {
throw new Error('Cannot add element to several parents');
}
}
this._setChildren(children);
}
}
/**
* Artificial class for correct flow behaviour.
*/
_createClass(Element, [{
key: 'removeChild',
// ==== Child Element Manipulation =================================================================================
/**
* Removes specified element from the element child list.
*
* @param {Element} element
*
* @returns {Element}
*/
value: function removeChild(element) {
if (element._parentElement !== this) {
throw new Error('The element to be removed is not a child of this element.');
}
var children = this._childElements.concat();
var elementIndex = children.indexOf(element);
children.splice(elementIndex, 1);
this._setChildren(children);
var ownerProgram = this.ownerProgram;
if (ownerProgram) {
ownerProgram._removeElementsFromProgram([element]);
}
element._parentElement = null;
return element;
}
/**
* Removes element.
*/
}, {
key: 'remove',
value: function remove() {
if (!this.parentElement) {
return;
}
this.parentElement.removeChild(this);
}
/**
* Appends specified element to the end of the child list.
* Accepts multiple nodes using `Fragment`.
*
* @param {Element} newElement
*/
}, {
key: 'appendChild',
value: function appendChild(newElement) {
var children = undefined;
var newElements = undefined;
if (newElement.isFragment) {
this._ensureCanAdoptFragment(newElement);
newElements = newElement._childElements;
children = this._childElements.concat(newElement._childElements);
} else {
if (newElement._parentElement) {
throw new Error('Remove element before adding again');
}
this._ensureCanAdopt(newElement);
newElements = [newElement];
children = this._childElements.concat(newElement);
}
this._setChildren(children);
if (newElements) {
var ownerProgram = this.ownerProgram;
if (ownerProgram) {
ownerProgram._addElementsToProgram(newElements);
}
}
}
/**
* Prepends specified element to the beginning of the child list.
* Accepts multiple nodes using `Fragment`.
*
* @param {Element} newElement
*/
}, {
key: 'prependChild',
value: function prependChild(newElement) {
var children = undefined;
var newElements = undefined;
if (newElement.isFragment) {
this._ensureCanAdoptFragment(newElement);
newElements = newElement._childElements;
children = newElement._childElements.concat(this._childElements);
} else {
if (newElement._parentElement) {
throw new Error('Remove element before adding again');
}
this._ensureCanAdopt(newElement);
newElements = [newElement];
children = [newElement].concat(this._childElements);
}
this._setChildren(children);
if (newElements) {
var ownerProgram = this.ownerProgram;
if (ownerProgram) {
ownerProgram._addElementsToProgram(newElements);
}
}
}
/**
* Inserts specified element before specified reference child.
* Accepts multiple nodes using `Fragment`.
*
* @param {Element} newElement
* @param {Element} referenceChild
*/
}, {
key: 'insertChildBefore',
value: function insertChildBefore(newElement, referenceChild) {
if (referenceChild._parentElement !== this) {
throw new Error('Invalid reference child');
}
var index = this._childElements.indexOf(referenceChild);
var childrenBefore = this._childElements.slice(0, index);
var childrenAfter = this._childElements.slice(index);
var children = undefined;
var newElements = undefined;
if (newElement.isFragment) {
this._ensureCanAdoptFragment(newElement);
newElements = newElement._childElements;
children = childrenBefore.concat(newElement._childElements, childrenAfter);
} else {
if (newElement._parentElement) {
throw new Error('Remove element before adding again');
}
this._ensureCanAdopt(newElement);
children = childrenBefore.concat(newElement, childrenAfter);
newElements = [newElement];
}
this._setChildren(children);
if (newElements) {
var ownerProgram = this.ownerProgram;
if (ownerProgram) {
ownerProgram._addElementsToProgram(newElements);
}
}
}
/**
* Replaces children from `firstRefChild` to `lastRefChild` with specified element.
* Accepts multiple replacement nodes using `Fragment`.
*
* @param {Element} newElement
* @param {Element} firstRefChild
* @param {Element} lastRefChild
*/
}, {
key: 'replaceChildren',
value: function replaceChildren(newElement, firstRefChild, lastRefChild) {
if (!firstRefChild || firstRefChild._parentElement !== this) {
throw new Error('Invalid first reference child');
}
if (!lastRefChild || lastRefChild._parentElement !== this) {
throw new Error('Invalid last reference child');
}
var firstIndex = this._childElements.indexOf(firstRefChild);
var lastIndex = this._childElements.indexOf(lastRefChild);
if (firstIndex > lastIndex) {
throw new Error('Invalid reference children order');
}
var childrenBefore = this._childElements.slice(0, firstIndex);
var childrenAfter = this._childElements.slice(lastIndex + 1);
var replacedChildren = this._childElements.slice(firstIndex, lastIndex + 1);
var children = undefined;
var newElements = undefined;
if (newElement.isFragment) {
this._ensureCanAdoptFragment(newElement);
children = childrenBefore.concat(newElement._childElements, childrenAfter);
newElements = newElement._childElements;
} else {
if (newElement._parentElement) {
throw new Error('Remove element before adding again');
}
this._ensureCanAdopt(newElement);
children = childrenBefore.concat(newElement, childrenAfter);
newElements = [newElement];
}
this._setChildren(children);
var ownerProgram = this.ownerProgram;
if (ownerProgram) {
ownerProgram._removeElementsFromProgram(replacedChildren);
}
for (var i = 0; i < replacedChildren.length; i++) {
var replacedChild = replacedChildren[i];
replacedChild._parentElement = null;
replacedChild._previousSibling = null;
replacedChild._nextSibling = null;
}
if (ownerProgram && newElements) {
ownerProgram._addElementsToProgram(newElements);
}
}
/**
* Replaces child with specified element.
* Accepts multiple replacement nodes using `Fragment`.
*
* @param {Element} newElement
* @param {Element} oldElement
*/
}, {
key: 'replaceChild',
value: function replaceChild(newElement, oldElement) {
this.replaceChildren(newElement, oldElement, oldElement);
}
/**
* Returns array of child element from firstRefChild to lastRefChild (including reference children).
*
* @param {Element} firstRefChild
* @param {Element} lastRefChild
* @returns {Array}
*/
}, {
key: 'getChildrenBetween',
value: function getChildrenBetween(firstRefChild, lastRefChild) {
if (!firstRefChild || firstRefChild._parentElement !== this) {
throw new Error('Invalid first reference child');
}
if (!lastRefChild || lastRefChild._parentElement !== this) {
throw new Error('Invalid last reference child');
}
var firstIndex = this._childElements.indexOf(firstRefChild);
var lastIndex = this._childElements.indexOf(lastRefChild);
if (firstIndex > lastIndex) {
throw new Error('Invalid reference children order');
}
return this._childElements.slice(firstIndex, lastIndex + 1);
}
/**
* Makes sure specified child is not already one of the parents of this element.
* Throws error on failure.
*
* @param {Element} child
* @private
*/
}, {
key: '_ensureCanAdopt',
value: function _ensureCanAdopt(child) {
var element = this;
while (element) {
if (element === child) {
throw new Error('The new child element contains the parent.');
}
element = element._parentElement;
}
}
/**
* Calls _ensureCanAdopt for each fragment element.
*
* @param {Element} fragment
* @private
*/
}, {
key: '_ensureCanAdoptFragment',
value: function _ensureCanAdoptFragment(fragment) {
var fragmentChild = fragment._firstChild;
while (fragmentChild) {
this._ensureCanAdopt(fragmentChild);
fragmentChild = fragmentChild._nextSibling;
}
}
/**
* Assigns new children. Runs element syntax assertions.
*
* @param {Element[]} newChildren
* @private
*/
}, {
key: '_setChildren',
value: function _setChildren(newChildren) {
this._acceptChildren(new _ElementAssert2['default'](newChildren));
if (newChildren.length > 0) {
var previousChild = newChildren[0];
this._firstChild = previousChild;
previousChild._parentElement = this;
previousChild._previousSibling = null;
if (newChildren.length > 1) {
// TODO(flow): error with only `let child;`
var child = newChildren[1];
for (var i = 1; i < newChildren.length; i++) {
child = newChildren[i];
child._parentElement = this;
child._previousSibling = previousChild;
previousChild._nextSibling = child;
previousChild = child;
}
child._nextSibling = null;
this._lastChild = child;
} else {
previousChild._nextSibling = null;
this._lastChild = previousChild;
}
} else {
this._firstChild = this._lastChild = null;
}
this._childElements = newChildren;
}
/**
* Runs element syntax assertions. Should be implemented for every Node.
*
* @param {Object} children
* @abstract
*/
}, {
key: '_acceptChildren',
value: function _acceptChildren(children) {}
// Override
/**
* Clones current Element structure.
*
* @returns {Element}
*/
}, {
key: 'cloneElement',
value: function cloneElement() {
var clonedChildren = new Array(this._childElements.length);
for (var i = 0; i < clonedChildren.length; i++) {
clonedChildren[i] = this._childElements[i].cloneElement();
}
var objectToClone = this;
return new objectToClone.constructor(clonedChildren);
}
}, {
key: 'type',
/**
* Element type.
* Important: some Node types also exist in Tokens. Do not rely on type only, use isToken, isNode.
*
* @returns {String}
*/
get: function get() {
return this._type;
}
/**
* True if Element is a Token.
*
* @returns {Boolean}
*/
}, {
key: 'isToken',
get: function get() {
return false;
}
/**
* True if Element is a whitespace Token.
*
* @returns {Boolean}
*/
}, {
key: 'isWhitespace',
get: function get() {
return false;
}
/**
* True if Element is a code Token.
*
* @returns {Boolean}
*/
}, {
key: 'isCode',
get: function get() {
return true;
}
/**
* True if Element is a comment Token.
*
* @returns {Boolean}
*/
}, {
key: 'isComment',
get: function get() {
return false;
}
/**
* True if Element is Comment or Whitespace.
*
* @returns {Boolean}
*/
}, {
key: 'isNonCodeToken',
get: function get() {
return this.isComment || this.isWhitespace;
}
/**
* True if Element is a Node.
*
* @returns {Boolean}
*/
}, {
key: 'isNode',
get: function get() {
return false;
}
/**
* True if Element is a Node which can be used as Expression.
*
* @returns {Boolean}
*/
}, {
key: 'isExpression',
get: function get() {
return false;
}
/**
* True if Element is a Node which can be used as Statement.
*
* @returns {Boolean}
*/
}, {
key: 'isStatement',
get: function get() {
return false;
}
/**
* True if Element is a Node which can be used as Pattern.
*
* @returns {Boolean}
*/
}, {
key: 'isPattern',
get: function get() {
return false;
}
/**
* True if Element is a Node which can be used as left part of Assignment.
*
* @returns {Boolean}
*/
}, {
key: 'isAssignable',
get: function get() {
return false;
}
/**
* True if Element is a Fragment.
*
* @returns {Boolean}
*/
}, {
key: 'isFragment',
get: function get() {
return false;
}
// ==== Traversing =================================================================================================
/**
* Parent element or null if parent element does not exist.
*
* @returns {Element|null}
*/
}, {
key: 'parentElement',
get: function get() {
return this._parentElement;
}
/**
* Owner Program for this element or null if element does not have Program in its parent hierarchy.
*
* @returns {Program}
*/
}, {
key: 'ownerProgram',
get: function get() {
var element = this;
while (element && !element._isProgram) {
element = element._parentElement;
}
return element;
}
/**
* Next sibling within parent.
* Null if element is the last child in parent or if element does not have parent.
*
* @returns {Element|null}
*/
}, {
key: 'nextSibling',
get: function get() {
return this._nextSibling;
}
/**
* Previous sibling within parent.
* Null if element is the first child in parent or if element does not have parent.
*
* @returns {Element|null}
*/
}, {
key: 'previousSibling',
get: function get() {
return this._previousSibling;
}
/**
* Next token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'nextToken',
get: function get() {
if (this._nextSibling) {
return this._nextSibling.firstToken;
}
if (this._parentElement) {
return this._parentElement.nextToken;
}
return null;
}
/**
* Previous token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'previousToken',
get: function get() {
if (this._previousSibling) {
return this._previousSibling.lastToken;
}
if (this._parentElement) {
return this._parentElement.previousToken;
}
return null;
}
/**
* Next token (non-whitespace and non-comment). Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'nextCodeToken',
get: function get() {
var token = this.nextToken;
while (token && !token.isCode) {
token = token.nextToken;
}
return token;
}
/**
* Previous token (non-whitespace and non-comment). Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'previousCodeToken',
get: function get() {
var token = this.previousToken;
while (token && !token.isCode) {
token = token.previousToken;
}
return token;
}
/**
* Next non-whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'nextNonWhitespaceToken',
get: function get() {
var token = this.nextToken;
while (token && token.isWhitespace) {
token = token.nextToken;
}
return token;
}
/**
* Previous non-whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'previousNonWhitespaceToken',
get: function get() {
var token = this.previousToken;
while (token && token.isWhitespace) {
token = token.previousToken;
}
return token;
}
/**
* Next whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'nextWhitespaceToken',
get: function get() {
var token = this.nextToken;
while (token && !token.isWhitespace) {
token = token.nextToken;
}
return token;
}
/**
* Previous whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'previousWhitespaceToken',
get: function get() {
var token = this.previousToken;
while (token && !token.isWhitespace) {
token = token.previousToken;
}
return token;
}
/**
* First token inside element child tree.
*
* @returns {Element|null}
*/
}, {
key: 'firstToken',
get: function get() {
var element = this._firstChild;
while (element && !element.isToken) {
element = element._firstChild;
}
return element;
}
/**
* Last token inside element child tree.
*
* @returns {Element|null}
*/
}, {
key: 'lastToken',
get: function get() {
var element = this._lastChild;
while (element && !element.isToken) {
element = element._lastChild;
}
return element;
}
/**
* First child element. Null if element does not have children.
*
* @returns {Element|null}
*/
}, {
key: 'firstChild',
get: function get() {
return this._firstChild;
}
/**
* Last child element. Null if element does not have children.
*
* @returns {Element|null}
*/
}, {
key: 'lastChild',
get: function get() {
return this._lastChild;
}
/**
* Direct children of the element.
*
* @returns {ElementList}
*/
}, {
key: 'childElements',
get: function get() {
return this._childElements.concat();
}
/**
* Direct child count.
*
* @returns {Number}
*/
}, {
key: 'childCount',
get: function get() {
return this._childElements.length;
}
/**
* Calculates and returns Element range.
*
* @returns {Number[]}
*/
}, {
key: 'range',
get: function get() {
var counter = 0;
var previous = this.previousToken;
while (previous) {
counter += previous.sourceCodeLength;
previous = previous.previousToken;
}
return [counter, counter + this.sourceCodeLength];
}
/**
* Calculates and returns Element loc.
*
* @returns {Object}
*/
}, {
key: 'loc',
get: function get() {
var prevToken = this.previousToken;
var startColumn = 0;
var startLine = 1;
while (prevToken) {
var lines = prevToken.sourceCodeLines;
startColumn += lines[lines.length - 1].length;
if (lines.length > 1) {
while (prevToken) {
startLine += prevToken.newlineCount;
prevToken = prevToken.previousToken;
}
break;
}
prevToken = prevToken.previousToken;
}
var elementLines = this.sourceCodeLines;
var endLine = startLine + elementLines.length - 1;
var endColumn = elementLines[elementLines.length - 1].length;
if (startLine === endLine) {
endColumn += startColumn;
}
return {
start: {
line: startLine,
column: startColumn
},
end: {
line: endLine,
column: endColumn
}
};
}
// ==== Source Code ================================================================================================
/**
* Generated source code length.
*
* @returns {Number}
*/
}, {
key: 'sourceCodeLength',
get: function get() {
var length = 0;
var child = this._firstChild;
while (child) {
length += child.sourceCodeLength;
child = child._nextSibling;
}
return length;
}
/**
* Generated source code.
*
* @returns {String}
*/
}, {
key: 'sourceCode',
get: function get() {
var code = '';
var child = this._firstChild;
while (child) {
code += child.sourceCode;
child = child._nextSibling;
}
return code;
}
/**
* Generated source code lines.
*
* @returns {String[]}
*/
}, {
key: 'sourceCodeLines',
get: function get() {
return (0, _utilsLines.getLines)(this.sourceCode);
}
/**
* Generated source code line break count.
*
* @returns {Number}
*/
}, {
key: 'newlineCount',
get: function get() {
var count = 0;
var child = this._firstChild;
while (child) {
count += child.newlineCount;
child = child._nextSibling;
}
return count;
}
}]);
return Element;
})();
exports['default'] = Element;
var ConcreteElement = (function (_Element) {
_inherits(ConcreteElement, _Element);
function ConcreteElement(children) {
_classCallCheck(this, ConcreteElement);
_get(Object.getPrototypeOf(ConcreteElement.prototype), 'constructor', this).call(this, 'ConcreteElement', children);
}
return ConcreteElement;
})(Element);
module.exports = exports['default'];