lezer-tree
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -0,1 +1,17 @@ | ||
## 0.2.0 (2019-08-02) | ||
### Bug fixes | ||
Fix incorrect node length calculation in `Tree.build`. | ||
### New features | ||
Tree nodes are now identified with tags. | ||
New `Tag` data structure to represent node tags. | ||
### Breaking changes | ||
Drop support for grammar ids and node types. | ||
## 0.1.1 (2019-07-09) | ||
@@ -2,0 +18,0 @@ |
export declare const DefaultBufferLength = 1024; | ||
export declare function isTagged(type: number): boolean; | ||
export declare function grammarID(type: number): number; | ||
export declare function termID(type: number): number; | ||
export declare function allocateGrammarID(): number; | ||
export interface ChangedRange { | ||
@@ -12,4 +8,4 @@ fromA: number; | ||
} | ||
export declare type EnterFunc<T> = (type: number, start: number, end: number) => T | false | undefined; | ||
export declare type LeaveFunc = (type: number, start: number, end: number) => void; | ||
export declare type EnterFunc<T> = (tag: Tag, start: number, end: number) => T | false | undefined; | ||
export declare type LeaveFunc = (tag: Tag, start: number, end: number) => void; | ||
declare class Iteration<T> { | ||
@@ -21,7 +17,7 @@ readonly enter: EnterFunc<T>; | ||
readonly done: boolean; | ||
doEnter(type: number, start: number, end: number): boolean; | ||
doEnter(tag: Tag, start: number, end: number): boolean; | ||
} | ||
export declare abstract class Subtree { | ||
abstract parent: Subtree | null; | ||
abstract type: number; | ||
abstract tag: Tag; | ||
abstract start: number; | ||
@@ -31,3 +27,3 @@ abstract end: number; | ||
readonly root: Tree; | ||
abstract toString(tags?: TagMap<any>): string; | ||
abstract toString(): string; | ||
abstract iterate<T = any>(from: number, to: number, enter: EnterFunc<T>, leave?: LeaveFunc): T | undefined; | ||
@@ -45,8 +41,11 @@ resolve(pos: number, side?: -1 | 0 | 1): Subtree; | ||
readonly length: number; | ||
readonly tags: readonly Tag[]; | ||
readonly type: number; | ||
parent: null; | ||
constructor(children: (Tree | TreeBuffer)[], positions: number[], length: number, type?: number); | ||
constructor(children: (Tree | TreeBuffer)[], positions: number[], length: number, tags: readonly Tag[], type?: number); | ||
readonly start: number; | ||
toString(tags?: TagMap<any>): string; | ||
readonly end: number; | ||
readonly tag: Tag; | ||
isPartOf(tags: readonly Tag[]): boolean; | ||
toString(): string; | ||
private partial; | ||
@@ -65,3 +64,3 @@ applyChanges(changes: readonly ChangedRange[]): Tree; | ||
balance(maxBufferLength?: number): Tree; | ||
static build(buffer: BufferCursor | readonly number[], grammarID: number, maxBufferLength?: number, reused?: Tree[]): Tree; | ||
static build(buffer: BufferCursor | readonly number[], tags: readonly Tag[], maxBufferLength?: number, reused?: Tree[]): Tree; | ||
} | ||
@@ -71,6 +70,6 @@ export declare class TreeBuffer { | ||
readonly length: number; | ||
readonly grammarID: number; | ||
constructor(buffer: Uint16Array, length: number, grammarID: number); | ||
toString(tags?: TagMap<any>): string; | ||
childToString(index: number, parts: string[], tags?: TagMap<any>): number; | ||
readonly tags: readonly Tag[]; | ||
constructor(buffer: Uint16Array, length: number, tags: readonly Tag[]); | ||
toString(): string; | ||
childToString(index: number, parts: string[]): number; | ||
cut(at: number): TreeBuffer; | ||
@@ -92,14 +91,11 @@ iterInner<T>(from: number, to: number, offset: number, iter: Iteration<T>): void; | ||
} | ||
export declare class TagMap<T> { | ||
readonly grammars: { | ||
readonly [id: number]: readonly (T | null)[]; | ||
}; | ||
constructor(grammars: { | ||
readonly [id: number]: readonly (T | null)[]; | ||
}); | ||
get(type: number): T | null; | ||
static single<T>(grammarID: number, values: readonly (T | null)[]): TagMap<T>; | ||
static combine<T>(maps: TagMap<T>[]): TagMap<T>; | ||
static empty: TagMap<any>; | ||
export declare class Tag { | ||
readonly tag: string; | ||
private parts; | ||
constructor(tag: string); | ||
private find; | ||
has(name: string, value?: string): boolean; | ||
matches(tag: Tag): number; | ||
static empty: Tag; | ||
} | ||
export {}; |
263
dist/tree.js
@@ -18,16 +18,4 @@ "use strict"; | ||
exports.DefaultBufferLength = 1024; | ||
/// Checks whether the given node type is a tagged node. | ||
// Checks whether the given node type is a tagged node. | ||
function isTagged(type) { return (type & 1) > 0; } | ||
exports.isTagged = isTagged; | ||
var GRAMMAR_ID_MASK = (Math.pow(2, 14) - 1) << 16, TERM_ID_MASK = Math.pow(2, 16) - 1; | ||
/// Get the ID of the grammar that a node type is part of. | ||
function grammarID(type) { return type & GRAMMAR_ID_MASK; } | ||
exports.grammarID = grammarID; | ||
/// Get the term ID for the given node type. | ||
function termID(type) { return type & TERM_ID_MASK; } | ||
exports.termID = termID; | ||
var nextGrammarID = 0; | ||
/// Allocate a new unique grammar ID. | ||
function allocateGrammarID() { return (nextGrammarID++) << 16; } | ||
exports.allocateGrammarID = allocateGrammarID; | ||
var Iteration = /** @class */ (function () { | ||
@@ -44,4 +32,4 @@ function Iteration(enter, leave) { | ||
}); | ||
Iteration.prototype.doEnter = function (type, start, end) { | ||
var value = this.enter(type, start, end); | ||
Iteration.prototype.doEnter = function (tag, start, end) { | ||
var value = this.enter(tag, start, end); | ||
if (value === undefined) | ||
@@ -133,3 +121,3 @@ return true; | ||
__extends(Tree, _super); | ||
/// Construct a tree. | ||
/// @internal | ||
function Tree( | ||
@@ -145,3 +133,5 @@ /// The tree's child nodes. Children small enough to fit in a | ||
length, | ||
/// This tree's node type. The root node has type 0. | ||
/// Mapping from node types to tags for this grammar @internal | ||
tags, | ||
/// This tree's node type. The root node has type 0. @internal | ||
type) { | ||
@@ -153,2 +143,3 @@ if (type === void 0) { type = 0; } | ||
_this.length = length; | ||
_this.tags = tags; | ||
_this.type = type; | ||
@@ -158,2 +149,3 @@ return _this; | ||
Object.defineProperty(Tree.prototype, "start", { | ||
/// @internal | ||
get: function () { return 0; }, | ||
@@ -163,8 +155,4 @@ enumerable: true, | ||
}); | ||
Tree.prototype.toString = function (tags) { | ||
var name = !isTagged(this.type) ? null : tags ? tags.get(this.type) : this.type; | ||
var children = this.children.map(function (c) { return c.toString(tags); }).join(); | ||
return !name ? children : name + (children.length ? "(" + children + ")" : ""); | ||
}; | ||
Object.defineProperty(Tree.prototype, "end", { | ||
/// @internal | ||
get: function () { return this.length; }, | ||
@@ -174,2 +162,18 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(Tree.prototype, "tag", { | ||
/// @internal | ||
get: function () { return isTagged(this.type) ? this.tags[this.type >> 1] : Tag.empty; }, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/// Check whether this tree's tag belongs to a given set of tags. | ||
/// Can be used to determine that a node belongs to the grammar | ||
/// defined by a specific parser. | ||
Tree.prototype.isPartOf = function (tags) { return this.tags == tags; }; | ||
/// @internal | ||
Tree.prototype.toString = function () { | ||
var name = !isTagged(this.type) ? null : this.tag.tag; | ||
var children = this.children.map(function (c) { return c.toString(); }).join(); | ||
return !name ? children : name + (children.length ? "(" + children + ")" : ""); | ||
}; | ||
Tree.prototype.partial = function (start, end, offset, children, positions) { | ||
@@ -225,3 +229,3 @@ for (var i = 0; i < this.children.length; i++) { | ||
} | ||
return new Tree(children, positions, this.length + off); | ||
return new Tree(children, positions, this.length + off, this.tags); | ||
}; | ||
@@ -241,4 +245,5 @@ /// Take the part of the tree up to the given position. | ||
} | ||
return new Tree(children, positions, at, this.type); | ||
return new Tree(children, positions, at, this.tags, this.type); | ||
}; | ||
/// @internal | ||
Tree.prototype.iterate = function (from, to, enter, leave) { | ||
@@ -251,3 +256,3 @@ var iter = new Iteration(enter, leave); | ||
Tree.prototype.iterInner = function (from, to, offset, iter) { | ||
if (isTagged(this.type) && !iter.doEnter(this.type, offset, offset + this.length)) | ||
if (isTagged(this.type) && !iter.doEnter(this.tag, offset, offset + this.length)) | ||
return; | ||
@@ -275,3 +280,3 @@ if (from <= to) { | ||
if (iter.leave && isTagged(this.type)) | ||
iter.leave(this.type, offset, offset + this.length); | ||
iter.leave(this.tag, offset, offset + this.length); | ||
return; | ||
@@ -284,5 +289,7 @@ }; | ||
}; | ||
/// @internal | ||
Tree.prototype.childBefore = function (pos) { | ||
return this.findChild(pos, -1, 0, this); | ||
}; | ||
/// @internal | ||
Tree.prototype.childAfter = function (pos) { | ||
@@ -333,3 +340,3 @@ return this.findChild(pos, 1, 0, this); | ||
throw new Error("Can't append overlapping trees"); | ||
return new Tree(this.children.concat(other.children), this.positions.concat(other.positions), other.length, this.type); | ||
return new Tree(this.children.concat(other.children), this.positions.concat(other.positions), other.length, this.tags, this.type); | ||
}; | ||
@@ -341,13 +348,13 @@ /// Balance the direct children of this tree. Should only be used on | ||
return this.children.length <= BalanceBranchFactor ? this : | ||
balanceRange(this.type, this.children, this.positions, 0, this.children.length, 0, maxBufferLength); | ||
balanceRange(this.type, this.tags, this.children, this.positions, 0, this.children.length, 0, maxBufferLength); | ||
}; | ||
/// Build a tree from a postfix-ordered buffer of node information, | ||
/// or a cursor over such a buffer. | ||
Tree.build = function (buffer, grammarID, maxBufferLength, reused) { | ||
Tree.build = function (buffer, tags, maxBufferLength, reused) { | ||
if (maxBufferLength === void 0) { maxBufferLength = exports.DefaultBufferLength; } | ||
if (reused === void 0) { reused = []; } | ||
return buildTree(Array.isArray(buffer) ? new FlatBufferCursor(buffer, buffer.length) : buffer, grammarID, maxBufferLength, reused); | ||
return buildTree(Array.isArray(buffer) ? new FlatBufferCursor(buffer, buffer.length) : buffer, tags, maxBufferLength, reused); | ||
}; | ||
/// The empty tree | ||
Tree.empty = new Tree([], [], 0); | ||
Tree.empty = new Tree([], [], 0, []); | ||
return Tree; | ||
@@ -364,18 +371,18 @@ }(Subtree)); | ||
// FIXME store a type in here to efficiently represent nodes whose children all fit in a buffer (esp repeat nodes)? | ||
function TreeBuffer(buffer, length, grammarID) { | ||
function TreeBuffer(buffer, length, tags) { | ||
this.buffer = buffer; | ||
this.length = length; | ||
this.grammarID = grammarID; | ||
this.tags = tags; | ||
} | ||
/// @internal | ||
TreeBuffer.prototype.toString = function (tags) { | ||
TreeBuffer.prototype.toString = function () { | ||
var parts = []; | ||
for (var index = 0; index < this.buffer.length;) | ||
index = this.childToString(index, parts, tags); | ||
index = this.childToString(index, parts); | ||
return parts.join(","); | ||
}; | ||
/// @internal | ||
TreeBuffer.prototype.childToString = function (index, parts, tags) { | ||
TreeBuffer.prototype.childToString = function (index, parts) { | ||
var type = this.buffer[index], endIndex = this.buffer[index + 3]; | ||
var result = String(tags ? tags.get(type | this.grammarID) : type); | ||
var result = this.tags[type >> 1].tag; | ||
index += 4; | ||
@@ -385,3 +392,3 @@ if (endIndex > index) { | ||
while (index < endIndex) | ||
index = this.childToString(index, children, tags); | ||
index = this.childToString(index, children); | ||
result += "(" + children.join(",") + ")"; | ||
@@ -404,3 +411,3 @@ } | ||
} | ||
return new TreeBuffer(newBuffer, Math.min(at, this.length), this.grammarID); | ||
return new TreeBuffer(newBuffer, Math.min(at, this.length), this.tags); | ||
}; | ||
@@ -419,10 +426,10 @@ /// @internal | ||
TreeBuffer.prototype.iterChild = function (from, to, offset, index, iter) { | ||
var type = this.buffer[index++] | this.grammarID, start = this.buffer[index++] + offset, end = this.buffer[index++] + offset, endIndex = this.buffer[index++]; | ||
var tag = this.tags[this.buffer[index++] >> 1], start = this.buffer[index++] + offset, end = this.buffer[index++] + offset, endIndex = this.buffer[index++]; | ||
if (start > to) | ||
return this.buffer.length; | ||
if (end >= from && iter.doEnter(type, start, end)) { | ||
if (end >= from && iter.doEnter(tag, start, end)) { | ||
while (index < endIndex && !iter.done) | ||
index = this.iterChild(from, to, offset, index, iter); | ||
if (iter.leave) | ||
iter.leave(type, start, end); | ||
iter.leave(tag, start, end); | ||
} | ||
@@ -469,6 +476,6 @@ return endIndex; | ||
var base = nextStart; | ||
var type_1 = this.buffer[base] | this.grammarID, start_1 = this.buffer[base + 1] + offset, end_1 = this.buffer[base + 2] + offset; | ||
var type_1 = this.buffer[base], start_1 = this.buffer[base + 1] + offset, end_1 = this.buffer[base + 2] + offset; | ||
takeNext(); | ||
if (start_1 <= from && end_1 >= to) { | ||
if (!iter.doEnter(type_1, start_1, end_1)) { | ||
if (!iter.doEnter(this.tags[type_1 >> 1], start_1, end_1)) { | ||
// Skip the entire node | ||
@@ -482,7 +489,7 @@ index = base; | ||
} | ||
var endIndex_1 = this.buffer[--index], end = this.buffer[--index] + offset, start = this.buffer[--index] + offset, type = this.buffer[--index] | this.grammarID; | ||
var endIndex_1 = this.buffer[--index], end = this.buffer[--index] + offset, start = this.buffer[--index] + offset, type = this.buffer[--index]; | ||
if (start > from || end < to) | ||
continue; | ||
if ((endIndex_1 != index + 4 || iter.doEnter(type, start, end)) && iter.leave) | ||
iter.leave(type, start, end); | ||
if ((endIndex_1 != index + 4 || iter.doEnter(this.tags[type >> 1], start, end)) && iter.leave) | ||
iter.leave(this.tags[type >> 1], start, end); | ||
} | ||
@@ -521,4 +528,4 @@ }; | ||
} | ||
Object.defineProperty(NodeSubtree.prototype, "type", { | ||
get: function () { return this.node.type; }, | ||
Object.defineProperty(NodeSubtree.prototype, "tag", { | ||
get: function () { return this.node.tag; }, | ||
enumerable: true, | ||
@@ -543,3 +550,3 @@ configurable: true | ||
}; | ||
NodeSubtree.prototype.toString = function (tags) { return this.node.toString(tags); }; | ||
NodeSubtree.prototype.toString = function () { return this.node.toString(); }; | ||
NodeSubtree.prototype.iterate = function (from, to, enter, leave) { | ||
@@ -562,4 +569,4 @@ var iter = new Iteration(enter, leave); | ||
} | ||
Object.defineProperty(BufferSubtree.prototype, "type", { | ||
get: function () { return this.buffer.buffer[this.index] | this.buffer.grammarID; }, | ||
Object.defineProperty(BufferSubtree.prototype, "tag", { | ||
get: function () { return this.buffer.tags[this.buffer.buffer[this.index] >> 1]; }, | ||
enumerable: true, | ||
@@ -605,5 +612,5 @@ configurable: true | ||
}; | ||
BufferSubtree.prototype.toString = function (tags) { | ||
BufferSubtree.prototype.toString = function () { | ||
var result = []; | ||
this.buffer.childToString(this.index, result, tags); | ||
this.buffer.childToString(this.index, result); | ||
return result.join(""); | ||
@@ -648,3 +655,3 @@ }; | ||
var BalanceBranchFactor = 8; | ||
function buildTree(cursor, grammarID, maxBufferLength, reused) { | ||
function buildTree(cursor, tags, maxBufferLength, reused) { | ||
function takeNode(parentStart, minPos, children, positions) { | ||
@@ -664,6 +671,6 @@ var type = cursor.type, start = cursor.start, end = cursor.end, size = cursor.size, buffer; | ||
index = copyToBuffer(buffer.start, data, index); | ||
node = new TreeBuffer(data, end - buffer.start, grammarID); | ||
node = new TreeBuffer(data, end - buffer.start, tags); | ||
// Wrap if this is a repeat node | ||
if (!isTagged(type)) | ||
node = new Tree([node], [0], end - buffer.start, type | grammarID); | ||
node = new Tree([node], [0], end - buffer.start, tags, type); | ||
startPos = buffer.start - parentStart; | ||
@@ -679,3 +686,3 @@ } | ||
localPositions.reverse(); | ||
node = new Tree(localChildren, localPositions, end - start, type | grammarID); | ||
node = new Tree(localChildren, localPositions, end - start, tags, type); | ||
} | ||
@@ -686,3 +693,3 @@ children.push(node); | ||
if (!isTagged(type) && (cursor.pos == 0 || cursor.type != type)) | ||
maybeBalanceSiblings(children, positions, type | grammarID); | ||
maybeBalanceSiblings(children, positions, type); | ||
} | ||
@@ -699,3 +706,3 @@ function maybeBalanceSiblings(children, positions, type) { | ||
var start = positions[to - 1]; | ||
var wrapped = balanceRange(type, children.slice(from, to).reverse(), positions.slice(from, to).reverse(), 0, to - from, start, maxBufferLength); | ||
var wrapped = balanceRange(type, tags, children.slice(from, to).reverse(), positions.slice(from, to).reverse(), 0, to - from, start, maxBufferLength); | ||
children.length = positions.length = from + 1; | ||
@@ -753,5 +760,6 @@ children[from] = wrapped; | ||
takeNode(0, 0, children, positions); | ||
return new Tree(children.reverse(), positions.reverse(), children.length ? positions[0] + children[0].length : 0); | ||
var length = children.length ? positions[0] + children[0].length : 0; | ||
return new Tree(children.reverse(), positions.reverse(), length, tags); | ||
} | ||
function balanceRange(type, children, positions, from, to, start, maxBufferLength) { | ||
function balanceRange(type, tags, children, positions, from, to, start, maxBufferLength) { | ||
var length = (positions[to - 1] + children[to - 1].length) - start; | ||
@@ -794,3 +802,3 @@ if (from == to - 1 && start == 0) { | ||
// Wrap with our type to make reuse possible | ||
only = new Tree([only], [0], only.length, type); | ||
only = new Tree([only], [0], only.length, tags, type); | ||
} | ||
@@ -800,3 +808,3 @@ localChildren.push(only); | ||
else { | ||
localChildren.push(balanceRange(type, children, positions, groupFrom, i, groupStart, maxBufferLength)); | ||
localChildren.push(balanceRange(type, tags, children, positions, groupFrom, i, groupStart, maxBufferLength)); | ||
} | ||
@@ -806,42 +814,103 @@ localPositions.push(groupStart - start); | ||
} | ||
return new Tree(localChildren, localPositions, length, type); | ||
return new Tree(localChildren, localPositions, length, tags, type); | ||
} | ||
/// A tag map is a data structure that holds metadata per tagged node | ||
/// type, possibly across grammars. Each parser comes with a tag map | ||
/// that holds the names for its tagged nodes, but you can also define | ||
/// such maps yourself. | ||
var TagMap = /** @class */ (function () { | ||
/// @internal | ||
function TagMap(grammars) { | ||
this.grammars = grammars; | ||
function badTag(tag) { | ||
throw new SyntaxError("Invalid tag " + JSON.stringify(tag)); | ||
} | ||
/// Tags represent information about nodes. They are an ordered | ||
/// collection of parts (more specific ones first) written in the form | ||
/// `boolean.literal.expression` (where `boolean` further specifies | ||
/// `literal`, which in turn further specifies `expression`). | ||
/// | ||
/// A part may also have a value, written after an `=` sign, as in | ||
/// `tag.selector.lang=css`. Part names and values may be double | ||
/// quoted (using JSON string notation) when they contain non-word | ||
/// characters. | ||
/// | ||
/// This wrapper object pre-parses the tag for easy querying. | ||
var Tag = /** @class */ (function () { | ||
/// Create a tag object from a string. | ||
function Tag( | ||
/// The string that the tag is based on. | ||
tag) { | ||
this.tag = tag; | ||
var parts = []; | ||
if (tag.length) | ||
for (var pos = 0;;) { | ||
var start = pos; | ||
while (pos < tag.length) { | ||
var next = tag.charCodeAt(pos); | ||
if (next == 61 || next == 46) | ||
break; // "=" or "." | ||
pos++; | ||
} | ||
if (pos == start) | ||
badTag(tag); | ||
var name = tag.slice(start, pos), value = ""; | ||
if (tag.charCodeAt(pos) == 61) { | ||
var valStart = ++pos; | ||
if (tag.charCodeAt(pos) == 34 /* '"' */) { | ||
for (pos++;;) { | ||
if (pos >= tag.length) | ||
badTag(tag); | ||
var next = tag.charCodeAt(pos++); | ||
if (next == 92) | ||
pos++; | ||
else if (next == 34) | ||
break; | ||
} | ||
value = JSON.parse(tag.slice(valStart, pos)); | ||
} | ||
else { | ||
while (pos < tag.length && tag.charCodeAt(pos) != 46) | ||
pos++; | ||
value = tag.slice(valStart, pos); | ||
} | ||
} | ||
parts.push(name, value); | ||
if (pos == tag.length) | ||
break; | ||
if (tag.charCodeAt(pos) != 46) | ||
badTag(tag); | ||
pos++; | ||
} | ||
this.parts = parts; | ||
} | ||
/// Get the value associated with the given type, if any. | ||
TagMap.prototype.get = function (type) { | ||
if (!isTagged(type)) | ||
return null; | ||
var table = this.grammars[grammarID(type) >> 16]; | ||
return table && table[termID(type) >> 1]; | ||
Tag.prototype.find = function (name, value) { | ||
for (var i = 0; i < this.parts.length; i += 2) { | ||
if (this.parts[i] == name && (value == null || this.parts[i + 1] == value)) | ||
return i; | ||
} | ||
return -1; | ||
}; | ||
/// Create a tag map for a single grammar from a flat array that | ||
/// holds, for each term ID, the associated value. | ||
TagMap.single = function (grammarID, values) { | ||
var _a; | ||
return new TagMap((_a = {}, _a[grammarID >> 16] = values, _a)); | ||
/// Check whether this tag has a part by the given name. If `value` | ||
/// is given, this will only return true when that part also has | ||
/// that specific value. | ||
Tag.prototype.has = function (name, value) { | ||
return this.find(name, value) > -1; | ||
}; | ||
/// Combine tag maps for multiple grammars into a single one that | ||
/// covers all of those grammars. | ||
TagMap.combine = function (maps) { | ||
var grammars = {}; | ||
for (var _i = 0, maps_1 = maps; _i < maps_1.length; _i++) { | ||
var map = maps_1[_i]; | ||
for (var id in map.grammars) | ||
grammars[id] = map.grammars[id]; | ||
/// See whether this tag contains all the parts present in the | ||
/// argument tag, and, if the part has a value in the query tag, the | ||
/// same value in this tag. Returns a specificity score—0 means | ||
/// there was no match, a higher score means the query matched more | ||
/// specific parts of the tag. | ||
Tag.prototype.matches = function (tag) { | ||
var score = 0; | ||
if (tag.parts.length == 0) | ||
return 1; | ||
for (var i = 0; i < tag.parts.length; i += 2) { | ||
var val = tag.parts[i + 1]; | ||
var found = this.find(tag.parts[i], val || undefined); | ||
if (found < 0) | ||
return 0; | ||
score += Math.pow(2, ((tag.parts.length - i) >> 1)) + (val ? 1 : 0); | ||
} | ||
return new TagMap(grammars); | ||
return score; | ||
}; | ||
/// The empty tag map. | ||
TagMap.empty = new TagMap([]); | ||
return TagMap; | ||
/// The empty tag, returned for nodes that don't have a meaningful | ||
/// tag. | ||
Tag.empty = new Tag(""); | ||
return Tag; | ||
}()); | ||
exports.TagMap = TagMap; | ||
exports.Tag = Tag; | ||
//# sourceMappingURL=tree.js.map |
{ | ||
"name": "lezer-tree", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Syntax tree data structure for the lezer parser", | ||
@@ -5,0 +5,0 @@ "main": "dist/tree.js", |
@@ -13,14 +13,6 @@ ### Trees | ||
### Node types | ||
### Node tags | ||
@TagMap | ||
@Tag | ||
@isTagged | ||
@grammarID | ||
@termID | ||
@allocateGrammarID | ||
### Buffers | ||
@@ -27,0 +19,0 @@ |
252
src/tree.ts
/// The default maximum length of a `TreeBuffer` node. | ||
export const DefaultBufferLength = 1024 | ||
/// Checks whether the given node type is a tagged node. | ||
export function isTagged(type: number) { return (type & 1) > 0 } | ||
// Checks whether the given node type is a tagged node. | ||
function isTagged(type: number) { return (type & 1) > 0 } | ||
const GRAMMAR_ID_MASK = (2**14 - 1) << 16, TERM_ID_MASK = 2**16 - 1 | ||
/// Get the ID of the grammar that a node type is part of. | ||
export function grammarID(type: number) { return type & GRAMMAR_ID_MASK } | ||
/// Get the term ID for the given node type. | ||
export function termID(type: number) { return type & TERM_ID_MASK } | ||
let nextGrammarID = 0 | ||
/// Allocate a new unique grammar ID. | ||
export function allocateGrammarID() { return (nextGrammarID++) << 16 } | ||
/// The `unchanged` method expects changed ranges in this format. | ||
@@ -30,3 +20,3 @@ export interface ChangedRange { | ||
/// Signature of the `enter` function passed to `Subtree.iterate`. It is given | ||
/// a node's type, start position, and end position for every node, | ||
/// a node's tag, start position, and end position for every node, | ||
/// and can return... | ||
@@ -41,6 +31,6 @@ /// | ||
/// value from the `iterate` method. | ||
export type EnterFunc<T> = (type: number, start: number, end: number) => T | false | undefined | ||
export type EnterFunc<T> = (tag: Tag, start: number, end: number) => T | false | undefined | ||
/// Signature of the `leave` function passed to `Subtree.iterate`. | ||
export type LeaveFunc = (type: number, start: number, end: number) => void | ||
export type LeaveFunc = (tag: Tag, start: number, end: number) => void | ||
@@ -55,4 +45,4 @@ class Iteration<T> { | ||
doEnter(type: number, start: number, end: number) { | ||
let value = this.enter(type, start, end) | ||
doEnter(tag: Tag, start: number, end: number) { | ||
let value = this.enter(tag, start, end) | ||
if (value === undefined) return true | ||
@@ -70,4 +60,4 @@ if (value !== false) this.result = value | ||
/// The node's type, or 0 if this is the root | ||
abstract type: number | ||
/// The node's tag. Will be `Tag.empty` for the root | ||
abstract tag: Tag | ||
/// The start source offset of this subtree | ||
@@ -93,3 +83,3 @@ abstract start: number | ||
/// @internal | ||
abstract toString(tags?: TagMap<any>): string | ||
abstract toString(): string | ||
@@ -148,5 +138,6 @@ /// Iterate over all nodes in this subtree. Will iterate through the | ||
export class Tree extends Subtree { | ||
/// @internal | ||
parent!: null | ||
/// Construct a tree. | ||
/// @internal | ||
constructor( | ||
@@ -162,4 +153,6 @@ /// The tree's child nodes. Children small enough to fit in a | ||
readonly length: number, | ||
/// This tree's node type. The root node has type 0. | ||
readonly type = 0 | ||
/// Mapping from node types to tags for this grammar @internal | ||
readonly tags: readonly Tag[], | ||
/// This tree's node type. The root node has type 0. @internal | ||
readonly type: number = 0 | ||
) { | ||
@@ -169,12 +162,23 @@ super() | ||
/// @internal | ||
get start() { return 0 } | ||
toString(tags?: TagMap<any>): string { | ||
let name = !isTagged(this.type) ? null : tags ? tags.get(this.type) : this.type | ||
let children = this.children.map(c => c.toString(tags)).join() | ||
/// @internal | ||
get end() { return this.length } | ||
/// @internal | ||
get tag() { return isTagged(this.type) ? this.tags[this.type >> 1] : Tag.empty } | ||
/// Check whether this tree's tag belongs to a given set of tags. | ||
/// Can be used to determine that a node belongs to the grammar | ||
/// defined by a specific parser. | ||
isPartOf(tags: readonly Tag[]) { return this.tags == tags } | ||
/// @internal | ||
toString(): string { | ||
let name = !isTagged(this.type) ? null : this.tag.tag | ||
let children = this.children.map(c => c.toString()).join() | ||
return !name ? children : name + (children.length ? "(" + children + ")" : "") | ||
} | ||
get end() { return this.length } | ||
private partial(start: number, end: number, offset: number, children: (Tree | TreeBuffer)[], positions: number[]) { | ||
@@ -225,3 +229,3 @@ for (let i = 0; i < this.children.length; i++) { | ||
} | ||
return new Tree(children, positions, this.length + off) | ||
return new Tree(children, positions, this.length + off, this.tags) | ||
} | ||
@@ -240,8 +244,9 @@ | ||
} | ||
return new Tree(children, positions, at, this.type) | ||
return new Tree(children, positions, at, this.tags, this.type) | ||
} | ||
/// The empty tree | ||
static empty = new Tree([], [], 0) | ||
static empty = new Tree([], [], 0, []) | ||
/// @internal | ||
iterate<T = any>(from: number, to: number, enter: EnterFunc<T>, leave?: LeaveFunc) { | ||
@@ -255,5 +260,5 @@ let iter = new Iteration(enter, leave) | ||
iterInner<T>(from: number, to: number, offset: number, iter: Iteration<T>) { | ||
if (isTagged(this.type) && !iter.doEnter(this.type, offset, offset + this.length)) | ||
if (isTagged(this.type) && !iter.doEnter(this.tag, offset, offset + this.length)) | ||
return | ||
if (from <= to) { | ||
@@ -274,3 +279,3 @@ for (let i = 0; i < this.children.length && !iter.done; i++) { | ||
} | ||
if (iter.leave && isTagged(this.type)) iter.leave(this.type, offset, offset + this.length) | ||
if (iter.leave && isTagged(this.type)) iter.leave(this.tag, offset, offset + this.length) | ||
return | ||
@@ -284,2 +289,3 @@ } | ||
/// @internal | ||
childBefore(pos: number): Subtree | null { | ||
@@ -289,2 +295,3 @@ return this.findChild(pos, -1, 0, this) | ||
/// @internal | ||
childAfter(pos: number): Subtree | null { | ||
@@ -330,3 +337,3 @@ return this.findChild(pos, 1, 0, this) | ||
if (other.children.length && other.positions[0] < this.length) throw new Error("Can't append overlapping trees") | ||
return new Tree(this.children.concat(other.children), this.positions.concat(other.positions), other.length, this.type) | ||
return new Tree(this.children.concat(other.children), this.positions.concat(other.positions), other.length, this.tags, this.type) | ||
} | ||
@@ -338,3 +345,3 @@ | ||
return this.children.length <= BalanceBranchFactor ? this : | ||
balanceRange(this.type, this.children, this.positions, 0, this.children.length, 0, maxBufferLength) | ||
balanceRange(this.type, this.tags, this.children, this.positions, 0, this.children.length, 0, maxBufferLength) | ||
} | ||
@@ -345,7 +352,7 @@ | ||
static build(buffer: BufferCursor | readonly number[], | ||
grammarID: number, | ||
tags: readonly Tag[], | ||
maxBufferLength: number = DefaultBufferLength, | ||
reused: Tree[] = []) { | ||
return buildTree(Array.isArray(buffer) ? new FlatBufferCursor(buffer, buffer.length) : buffer as BufferCursor, | ||
grammarID, maxBufferLength, reused) | ||
tags, maxBufferLength, reused) | ||
} | ||
@@ -363,9 +370,9 @@ } | ||
// FIXME store a type in here to efficiently represent nodes whose children all fit in a buffer (esp repeat nodes)? | ||
constructor(readonly buffer: Uint16Array, readonly length: number, readonly grammarID: number) {} | ||
constructor(readonly buffer: Uint16Array, readonly length: number, readonly tags: readonly Tag[]) {} | ||
/// @internal | ||
toString(tags?: TagMap<any>) { | ||
toString() { | ||
let parts: string[] = [] | ||
for (let index = 0; index < this.buffer.length;) | ||
index = this.childToString(index, parts, tags) | ||
index = this.childToString(index, parts) | ||
return parts.join(",") | ||
@@ -375,9 +382,9 @@ } | ||
/// @internal | ||
childToString(index: number, parts: string[], tags?: TagMap<any>): number { | ||
childToString(index: number, parts: string[]): number { | ||
let type = this.buffer[index], endIndex = this.buffer[index + 3] | ||
let result = String(tags ? tags.get(type | this.grammarID)! : type) | ||
let result = this.tags[type >> 1].tag | ||
index += 4 | ||
if (endIndex > index) { | ||
let children: string[] = [] | ||
while (index < endIndex) index = this.childToString(index, children, tags) | ||
while (index < endIndex) index = this.childToString(index, children) | ||
result += "(" + children.join(",") + ")" | ||
@@ -400,3 +407,3 @@ } | ||
} | ||
return new TreeBuffer(newBuffer, Math.min(at, this.length), this.grammarID) | ||
return new TreeBuffer(newBuffer, Math.min(at, this.length), this.tags) | ||
} | ||
@@ -416,8 +423,8 @@ | ||
iterChild<T>(from: number, to: number, offset: number, index: number, iter: Iteration<T>) { | ||
let type = this.buffer[index++] | this.grammarID, start = this.buffer[index++] + offset, | ||
let tag = this.tags[this.buffer[index++] >> 1], start = this.buffer[index++] + offset, | ||
end = this.buffer[index++] + offset, endIndex = this.buffer[index++] | ||
if (start > to) return this.buffer.length | ||
if (end >= from && iter.doEnter(type, start, end)) { | ||
if (end >= from && iter.doEnter(tag, start, end)) { | ||
while (index < endIndex && !iter.done) index = this.iterChild(from, to, offset, index, iter) | ||
if (iter.leave) iter.leave(type, start, end) | ||
if (iter.leave) iter.leave(tag, start, end) | ||
} | ||
@@ -461,6 +468,6 @@ return endIndex | ||
let base = nextStart | ||
let type = this.buffer[base] | this.grammarID, start = this.buffer[base + 1] + offset, end = this.buffer[base + 2] + offset | ||
let type = this.buffer[base], start = this.buffer[base + 1] + offset, end = this.buffer[base + 2] + offset | ||
takeNext() | ||
if (start <= from && end >= to) { | ||
if (!iter.doEnter(type, start, end)) { | ||
if (!iter.doEnter(this.tags[type >> 1], start, end)) { | ||
// Skip the entire node | ||
@@ -474,6 +481,6 @@ index = base | ||
let endIndex = this.buffer[--index], end = this.buffer[--index] + offset, | ||
start = this.buffer[--index] + offset, type = this.buffer[--index] | this.grammarID | ||
start = this.buffer[--index] + offset, type = this.buffer[--index] | ||
if (start > from || end < to) continue | ||
if ((endIndex != index + 4 || iter.doEnter(type, start, end)) && iter.leave) | ||
iter.leave(type, start, end) | ||
if ((endIndex != index + 4 || iter.doEnter(this.tags[type >> 1], start, end)) && iter.leave) | ||
iter.leave(this.tags[type >> 1], start, end) | ||
} | ||
@@ -507,3 +514,3 @@ } | ||
get type() { return this.node.type } | ||
get tag() { return this.node.tag } | ||
@@ -526,3 +533,3 @@ get end() { return this.start + this.node.length } | ||
toString(tags?: TagMap<any>) { return this.node.toString(tags) } | ||
toString() { return this.node.toString() } | ||
@@ -544,3 +551,3 @@ iterate<T = any>(from: number, to: number, enter: EnterFunc<T>, leave?: LeaveFunc) { | ||
get type() { return this.buffer.buffer[this.index] | this.buffer.grammarID } | ||
get tag() { return this.buffer.tags[this.buffer.buffer[this.index] >> 1] } | ||
get start() { return this.buffer.buffer[this.index + 1] + this.bufferStart } | ||
@@ -576,5 +583,5 @@ get end() { return this.buffer.buffer[this.index + 2] + this.bufferStart } | ||
toString(tags?: TagMap<any>) { | ||
toString() { | ||
let result: string[] = [] | ||
this.buffer.childToString(this.index, result, tags) | ||
this.buffer.childToString(this.index, result) | ||
return result.join("") | ||
@@ -613,3 +620,3 @@ } | ||
function buildTree(cursor: BufferCursor, grammarID: number, maxBufferLength: number, reused: Tree[]): Tree { | ||
function buildTree(cursor: BufferCursor, tags: readonly Tag[], maxBufferLength: number, reused: Tree[]): Tree { | ||
function takeNode(parentStart: number, minPos: number, children: (Tree | TreeBuffer)[], positions: number[]) { | ||
@@ -628,5 +635,5 @@ let {type, start, end, size} = cursor, buffer!: {size: number, start: number, skip: number} | null | ||
index = copyToBuffer(buffer.start, data, index) | ||
node = new TreeBuffer(data, end - buffer.start, grammarID) | ||
node = new TreeBuffer(data, end - buffer.start, tags) | ||
// Wrap if this is a repeat node | ||
if (!isTagged(type)) node = new Tree([node], [0], end - buffer.start, type | grammarID) | ||
if (!isTagged(type)) node = new Tree([node], [0], end - buffer.start, tags, type) | ||
startPos = buffer.start - parentStart | ||
@@ -640,3 +647,3 @@ } else { // Make it a node | ||
localChildren.reverse(); localPositions.reverse() | ||
node = new Tree(localChildren, localPositions, end - start, type | grammarID) | ||
node = new Tree(localChildren, localPositions, end - start, tags, type) | ||
} | ||
@@ -648,3 +655,3 @@ | ||
if (!isTagged(type) && (cursor.pos == 0 || cursor.type != type)) | ||
maybeBalanceSiblings(children, positions, type | grammarID) | ||
maybeBalanceSiblings(children, positions, type) | ||
} | ||
@@ -660,3 +667,3 @@ | ||
let start = positions[to - 1] | ||
let wrapped = balanceRange(type, children.slice(from, to).reverse(), positions.slice(from, to).reverse(), | ||
let wrapped = balanceRange(type, tags, children.slice(from, to).reverse(), positions.slice(from, to).reverse(), | ||
0, to - from, start, maxBufferLength) | ||
@@ -714,6 +721,8 @@ children.length = positions.length = from + 1 | ||
while (cursor.pos > 0) takeNode(0, 0, children, positions) | ||
return new Tree(children.reverse(), positions.reverse(), children.length ? positions[0] + children[0].length : 0) | ||
let length = children.length ? positions[0] + children[0].length : 0 | ||
return new Tree(children.reverse(), positions.reverse(), length, tags) | ||
} | ||
function balanceRange(type: number, | ||
tags: readonly Tag[], | ||
children: readonly (Tree | TreeBuffer)[], positions: readonly number[], | ||
@@ -755,7 +764,7 @@ from: number, to: number, | ||
// Wrap with our type to make reuse possible | ||
only = new Tree([only], [0], only.length, type) | ||
only = new Tree([only], [0], only.length, tags, type) | ||
} | ||
localChildren.push(only) | ||
} else { | ||
localChildren.push(balanceRange(type, children, positions, groupFrom, i, groupStart, maxBufferLength)) | ||
localChildren.push(balanceRange(type, tags, children, positions, groupFrom, i, groupStart, maxBufferLength)) | ||
} | ||
@@ -765,38 +774,95 @@ localPositions.push(groupStart - start) | ||
} | ||
return new Tree(localChildren, localPositions, length, type) | ||
return new Tree(localChildren, localPositions, length, tags, type) | ||
} | ||
/// A tag map is a data structure that holds metadata per tagged node | ||
/// type, possibly across grammars. Each parser comes with a tag map | ||
/// that holds the names for its tagged nodes, but you can also define | ||
/// such maps yourself. | ||
export class TagMap<T> { | ||
/// @internal | ||
constructor(readonly grammars: {readonly [id: number]: readonly (T | null)[]}) {} | ||
function badTag(tag: string): never { | ||
throw new SyntaxError("Invalid tag " + JSON.stringify(tag)) | ||
} | ||
/// Get the value associated with the given type, if any. | ||
get(type: number): T | null { | ||
if (!isTagged(type)) return null | ||
let table = this.grammars[grammarID(type) >> 16] | ||
return table && table[termID(type) >> 1] | ||
/// Tags represent information about nodes. They are an ordered | ||
/// collection of parts (more specific ones first) written in the form | ||
/// `boolean.literal.expression` (where `boolean` further specifies | ||
/// `literal`, which in turn further specifies `expression`). | ||
/// | ||
/// A part may also have a value, written after an `=` sign, as in | ||
/// `tag.selector.lang=css`. Part names and values may be double | ||
/// quoted (using JSON string notation) when they contain non-word | ||
/// characters. | ||
/// | ||
/// This wrapper object pre-parses the tag for easy querying. | ||
export class Tag { | ||
private parts: readonly string[] | ||
/// Create a tag object from a string. | ||
constructor( | ||
/// The string that the tag is based on. | ||
readonly tag: string | ||
) { | ||
let parts = [] | ||
if (tag.length) for (let pos = 0;;) { | ||
let start = pos | ||
while (pos < tag.length) { | ||
let next = tag.charCodeAt(pos) | ||
if (next == 61 || next == 46) break // "=" or "." | ||
pos++ | ||
} | ||
if (pos == start) badTag(tag) | ||
let name = tag.slice(start, pos), value = "" | ||
if (tag.charCodeAt(pos) == 61) { | ||
let valStart = ++pos | ||
if (tag.charCodeAt(pos) == 34 /* '"' */) { | ||
for (pos++;;) { | ||
if (pos >= tag.length) badTag(tag) | ||
let next = tag.charCodeAt(pos++) | ||
if (next == 92) pos++ | ||
else if (next == 34) break | ||
} | ||
value = JSON.parse(tag.slice(valStart, pos)) | ||
} else { | ||
while (pos < tag.length && tag.charCodeAt(pos) != 46) pos++ | ||
value = tag.slice(valStart, pos) | ||
} | ||
} | ||
parts.push(name, value) | ||
if (pos == tag.length) break | ||
if (tag.charCodeAt(pos) != 46) badTag(tag) | ||
pos++ | ||
} | ||
this.parts = parts | ||
} | ||
/// Create a tag map for a single grammar from a flat array that | ||
/// holds, for each term ID, the associated value. | ||
static single<T>(grammarID: number, values: readonly (T | null)[]) { | ||
return new TagMap({[grammarID >> 16]: values}) | ||
private find(name: string, value?: string) { | ||
for (let i = 0; i < this.parts.length; i += 2) { | ||
if (this.parts[i] == name && (value == null || this.parts[i + 1] == value)) return i | ||
} | ||
return -1 | ||
} | ||
/// Combine tag maps for multiple grammars into a single one that | ||
/// covers all of those grammars. | ||
static combine<T>(maps: TagMap<T>[]) { | ||
let grammars: {[id: number]: readonly (T | null)[]} = {} | ||
for (let map of maps) { | ||
for (let id in map.grammars) grammars[id] = map.grammars[id] | ||
/// Check whether this tag has a part by the given name. If `value` | ||
/// is given, this will only return true when that part also has | ||
/// that specific value. | ||
has(name: string, value?: string) { | ||
return this.find(name, value) > -1 | ||
} | ||
/// See whether this tag contains all the parts present in the | ||
/// argument tag, and, if the part has a value in the query tag, the | ||
/// same value in this tag. Returns a specificity score—0 means | ||
/// there was no match, a higher score means the query matched more | ||
/// specific parts of the tag. | ||
matches(tag: Tag) { | ||
let score = 0 | ||
if (tag.parts.length == 0) return 1 | ||
for (let i = 0; i < tag.parts.length; i += 2) { | ||
let val = tag.parts[i + 1] | ||
let found = this.find(tag.parts[i], val || undefined) | ||
if (found < 0) return 0 | ||
score += 2 ** ((tag.parts.length - i) >> 1) + (val ? 1 : 0) | ||
} | ||
return new TagMap(grammars) | ||
return score | ||
} | ||
/// The empty tag map. | ||
static empty = new TagMap<any>([]) | ||
/// The empty tag, returned for nodes that don't have a meaningful | ||
/// tag. | ||
static empty = new Tag("") | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
105998
1730