cst
Version:
JavaScript CST Implementation
807 lines (671 loc) • 24.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _lines = require('../utils/lines');
var _ElementAssert = require('./ElementAssert');
var _ElementAssert2 = _interopRequireDefault(_ElementAssert);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Base class for Node, Token and Fragment.
*
* @name Element
* @class
* @abstract
*/
var Element = function () {
/**
* @param {String} type
* @param {Element[]} children
*/
function Element(type, children) {
(0, _classCallCheck3.default)(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);
}
this.isToken = false;
this.isWhitespace = false;
this.isCode = true;
this.isComment = false;
this.isNonCodeToken = this.isComment || this.isWhitespace;
this.isNode = false;
this.isStatement = false;
this.isPattern = false;
this.isAssignable = false;
this.isFragment = false;
}
(0, _createClass3.default)(Element, [{
key: 'getOwnerProgram',
// ==== Traversing =================================================================================================
/**
* Owner Program for this element or null if element does not have Program in its parent hierarchy.
*
* @returns {Program}
*/
value: function getOwnerProgram() {
var element = this;
while (element && !element._isProgram) {
element = element.parentElement;
}
return element;
}
}, {
key: 'getNextToken',
value: function getNextToken() {
var element = this;
while (element) {
if (element.nextSibling) {
return element.nextSibling.getFirstToken();
}
element = element.parentElement;
}
return null;
}
}, {
key: 'getPreviousToken',
value: function getPreviousToken() {
var element = this;
while (element) {
if (element.previousSibling) {
return element.previousSibling.getLastToken();
}
element = element.parentElement;
}
return null;
}
/**
* Next token (non-whitespace and non-comment). Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'getNextCodeToken',
value: function getNextCodeToken() {
var token = this.getNextToken();
while (token && !token.isCode) {
token = token.getNextToken();
}
return token;
}
/**
* Previous token (non-whitespace and non-comment). Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'getPreviousCodeToken',
value: function getPreviousCodeToken() {
var token = this.getPreviousToken();
while (token && !token.isCode) {
token = token.getPreviousToken();
}
return token;
}
/**
* Next non-whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'getNextNonWhitespaceToken',
value: function getNextNonWhitespaceToken() {
var token = this.getNextToken();
while (token && token.isWhitespace) {
token = token.getNextToken();
}
return token;
}
/**
* Previous non-whitespace token. Null if token was not found.
*
* @returns {Element|null}
*/
}, {
key: 'getPreviousNonWhitespaceToken',
value: function getPreviousNonWhitespaceToken() {
var token = this.getPreviousToken();
while (token && token.isWhitespace) {
token = token.getPreviousToken();
}
return token;
}
/**
* First token inside element child tree.
*
* @returns {Element|null}
*/
}, {
key: 'getFirstToken',
value: function getFirstToken() {
var element = this.firstChild;
while (element && !element.isToken) {
element = element.firstChild;
}
return element;
}
/**
* Last token inside element child tree.
*
* @returns {Element|null}
*/
}, {
key: 'getLastToken',
value: function getLastToken() {
var element = this.lastChild;
while (element && !element.isToken) {
element = element.lastChild;
}
return element;
}
/**
* Calculates and returns Element range.
*
* @returns {Number[]}
*/
}, {
key: 'getRange',
value: function getRange() {
var counter = 0;
var previous = this.getPreviousToken();
while (previous) {
counter += previous._sourceCodeLength;
previous = previous.getPreviousToken();
}
return [counter, counter + this.getSourceCodeLength()];
}
/**
* Calculates and returns Element loc.
*
* @returns {Object}
*/
}, {
key: 'getLoc',
value: function getLoc() {
var prevToken = this.getPreviousToken();
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.getNewlineCount();
prevToken = prevToken.getPreviousToken();
}
break;
}
prevToken = prevToken.getPreviousToken();
}
var elementLines = this.getSourceCodeLines();
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: 'getSourceCodeLength',
value: function getSourceCodeLength() {
var length = 0;
var child = this.firstChild;
while (child) {
length += child.getSourceCodeLength();
child = child.nextSibling;
}
return length;
}
/**
* Generated source code.
*
* @returns {String}
*/
}, {
key: 'getSourceCode',
value: function getSourceCode() {
var code = '';
var child = this.firstChild;
while (child) {
code += child.getSourceCode();
child = child.nextSibling;
}
return code;
}
/**
* Generated source code lines.
*
* @returns {String[]}
*/
}, {
key: 'getSourceCodeLines',
value: function getSourceCodeLines() {
return (0, _lines.getLines)(this.getSourceCode());
}
/**
* Generated source code line break count.
*
* @returns {Number}
*/
}, {
key: 'getNewlineCount',
value: function getNewlineCount() {
var count = 0;
var child = this.firstChild;
while (child) {
count += child.getNewlineCount();
child = child.nextSibling;
}
return count;
}
// ==== Child Element Manipulation =================================================================================
/**
* Removes specified element from the element child list.
*
* @param {Element} element
*
* @returns {Element}
*/
}, {
key: 'removeChild',
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.getOwnerProgram();
if (ownerProgram) {
ownerProgram._removeElementsFromProgram([element]);
}
setParentElement(element, 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 = void 0;
var newElements = void 0;
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.getOwnerProgram();
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 = void 0;
var newElements = void 0;
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.getOwnerProgram();
if (ownerProgram) {
ownerProgram._prependElementsToProgram(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 = void 0;
var newElements = void 0;
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.getOwnerProgram();
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 = void 0;
var newElements = void 0;
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.getOwnerProgram();
if (ownerProgram) {
ownerProgram._removeElementsFromProgram(replacedChildren);
}
for (var i = 0; i < replacedChildren.length; i++) {
var replacedChild = replacedChildren[i];
replacedChild.previousSibling = null;
replacedChild.nextSibling = null;
setParentElement(replacedChild, null);
}
if (ownerProgram && newElements) {
ownerProgram._addElementsToProgram(newElements);
}
}
/**
* Removes children from `firstRefChild` to `lastRefChild` with specified element.
*
* @param {Element} firstRefChild
* @param {Element} lastRefChild
*/
}, {
key: 'removeChildren',
value: function removeChildren(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 children = this.childElements.slice(0, firstIndex).concat(this.childElements.slice(lastIndex + 1));
var removedChildren = this.childElements.slice(firstIndex, lastIndex + 1);
this._setChildren(children);
var ownerProgram = this.getOwnerProgram();
if (ownerProgram) {
ownerProgram._removeElementsFromProgram(removedChildren);
}
for (var i = 0; i < removedChildren.length; i++) {
var removedChild = removedChildren[i];
removedChild.previousSibling = null;
removedChild.nextSibling = null;
setParentElement(removedChild, null);
}
}
/**
* 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.previousSibling = null;
setParentElement(previousChild, this);
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.previousSibling = previousChild;
setParentElement(child, this);
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);
}
}]);
return Element;
}();
/**
* Artificial class for correct flow behaviour.
*/
exports.default = Element;
var ConcreteElement = function (_Element) {
(0, _inherits3.default)(ConcreteElement, _Element);
function ConcreteElement(children) {
(0, _classCallCheck3.default)(this, ConcreteElement);
return (0, _possibleConstructorReturn3.default)(this, (ConcreteElement.__proto__ || (0, _getPrototypeOf2.default)(ConcreteElement)).call(this, 'ConcreteElement', children));
}
return ConcreteElement;
}(Element);
function setParentElement(element, parentElement) {
element.parentElement = parentElement;
if (element._onSetParentElement) {
element._onSetParentElement(parentElement);
}
}
//# sourceMappingURL=Element.js.map