🏰 ghast.js v0.6.7 'FLAY'
ghast.js is an abstract syntax tree designed for use with Peggy/PEG.js.
Usage
Installation
npm install ghast.js
The ast
function
ghast.js provides the AST
class and ast
helper function:
const { AST, ast } = require('ghast.js')
You probably won't need to interact with the AST
class itself. The helper is
a wrapper around new AST()
. It takes an ID and zero-or-more syntax elements.
A syntax element may be a string, another AST
node or an array of these.
Example:
ast('Function',
ast('Ident', 'foo'),
"(", ast('String', '"', 'bar', '"'), ")"
)
This will return a small tree representing a call to function foo
with one
string as its argument, "bar"
.
The classify
function
The ast.classify
function takes a number of tags and returns a new helper
function that automatically applies these tags to created nodes. For example:
const foo = ast.classify('foo')
const n1 = foo('Int', '55') // n1 will have the tag 'foo'
const bar = foo.classify('bar')
const n2 = bar('Str', 'foo') // n2 will be tagged 'foo bar'
The locate
function
ast.locate
takes a location function and returns a new helper
function that automatically adds location data to any created nodes. In a
grammar you would use it like this:
{
const ast = options.ast.locate(location)
}
Any created nodes will capture location data from the rule where they're
created, available as node.location
. Note that all nodes created in an action
will share the same location information; to get information on a portion of the
match, create another rule for just that portion.
Using ghast.js in a Grammar File
To use ghast in a grammar file, create a parser and place the ast
helper
function in the parser's options. For example:
const peggy = require('peggy')
const { ast } = require('ghast.js')
const parser = peggy.generate(GRAMMAR)
const tree = parser.parse(INPUT, {ast})
The ast
function will be available in your grammar:
{
const ast = options.ast.locate(location)
const node = ast.classify('Node')
const val = ast.classify('Value')
}
Example = ex:Atom* { return ast('Example', ex) }
Atom = A / B / N / S / [ \n]
Sub = "(" Atom* ")"
A = x:("a" Sub / "a") { return node('A', x) }
B = x:("b" Sub / "b") { return node('B', x) }
N = n:$[0-9]+ { return val('Number', n) }
S = "'" C "'"
C = c:$[^']+ { return val('String', c) }
The parser will now return a ghast AST
which can be used to manipulate the
parsed syntax. This will remove all B elements directly below an A element:
const tree = parser.parse(INPUT, {ast})
tree.select("A", {id: "B", depth: 0}, b=> b.remove())
API
Complete API documentation is available. Below is an overview of common methods:
The each
method is used to query the tree:
node.each() // return all descendants of `node`
node.each({self: true}) // return `node` and all of its descendants
node.each('Section') // return all descendants with id `Section`
node.each({id: 'Section'}) // same as above
node.each({id: 'X', tag: 'y'}) // return all descendants with both id `X` and tag `y`
node.each({tag: 'val key'}) // return all descendants tagged `val` and `key`
node.each({id: 'A', first: true}) // return the first descendant with id `A`
node.each({leaf: true}) // return all descendant leaf nodes
node.each({stem: true}) // return all non-leaf descendant nodes
node.each({depth: 0}) // return all direct children of `node`
node.each({depth: 1}) // return all direct children and grandchildren
node.each({up: true}) // return all ancestors of `node`
node.ancestor() // same as above
node.each({up: true, tag: 'x'}) // return all ancestors of `node` tagged `x`
node.ancestor({tag: 'x'}) // same as above
node.climb(3) // return nth ancestor of `node`
The select
method creates complex selections from multiple traverses,
similar to CSS selectors. The following is similar to A .foo > B
:
node.select('A', {tag: 'foo'}, {id: 'B', depth: 0})
The when
method is used to visit nodes. Each visitor is an array of
queries followed by a callback which will be called for each node
matching any of its associated queries:
node.when(
[{id: 'A', tag: 'foo'}, n=> n.foo()],
['T', 'V', n=> n.bar()],
[{tag: 'bar'}, {leaf: true}, n=> n.baz()],
)
The following methods exist to modify the tree:
// replace a child node with another:
node.replace(node.first(), ast('Test', 'test'))
// self-replace a node with another:
node.replace(ast('Test', 'test'))
// transform nodes in-place:
node.mutate({id: 'Foo', attrs: {x: 1}})
node.mutate({tags: 'x y z', syntax: ['foo']})
// remove a child node:
node.remove(node.first())
// self-remove a node:
node.remove()
Nodes can be tagged:
node.tag('foo') // tag a node
node.tag('foo bar baz') // apply multiple tags at once
node.hasTags // true if the node has any tags
node.hasTag('foo') // true if the node has the tag `foo`
node.hasTag('bar baz') // true if the node has all of the given tags
Nodes have attributes:
node.attr('a', 100) // set a single attribute
node.attr({foo: 1, bar: 2}) // set one or more attributes
node.attrs.foo // accessing attributes
node.attrs['foo'] // accessing attributes
The read
method deep-reads attributes; it merges the attributes of this node
with all of its descendants and returns the value of the given property:
const node = ast('Function',
ast('Ident', 'foo').attr('foo', 1),
"(", ast('String', '"', 'bar', '"').attr('bar', 2), ")"
)
node.read('foo') // returns 1
node.read('bar') // returns 2
Location data for a node can be set with loc
:
node.loc({start: 1, end: 2})
node.location // read set location data
Examples
Two examples are provided:
License
Available under the terms of the MIT license.
Copyright 2023 0E9B061F