Welcome to PML, Path Matching Language — a generic tree/node object traversal and path matching framework.
PML allows you to bind tree-traversal handlers using a visitor-esque pattern to the corresponding node placement in a graph, rather than just their types. This is comparable to the manner with which CSS (Cascading Style Sheet) selectors matched on a Document Object Model (DOM) tree.
For example, assume an inheritance hierarchy of Node
type implementations; ANode
, BNode
, and CNode
. These types correspond to their associated NodeHandler<>
implementations; ANodeHandler
, BNodeHandler
, and CNodeHandler
.
classDiagram
direction TB
class Node {
<<Interface>>
+List~Node~ childNodes
}
class NodeHandler~TNode : Node~ {
<<Interface>>
+handleNode(TNode node, List~Node~ ancestorNodes)
}
Node <|-- ANode
Node <|-- BNode
Node <|-- CNode
NodeHandler <|-- ANodeHandler
NodeHandler <|-- BNodeHandler
NodeHandler <|-- CNodeHandler
This pattern should be familiar, and is seen widely in software projects. However, without additional logic polluting the handleNode
implementations, a visitor would generally dispatch node handling based on type rather than graph position.
When using PML, one can add path matching metadata (annotations) to the handlers which allow finer-grain evaluation.
For example, we will use the following annotations and some additional NodeHandler<>
implementations.
classDiagram
direction TB
class NodeHandler~TNode : Node~ {
<<Interface>>
+handleNode(TNode node, List~TNode~ ancestorNodes)
}
NodeHandler <|-- ANodeHandler
NodeHandler <|-- BNodeHandler
NodeHandler <|-- InsideACNodeHandler
NodeHandler <|-- InsideBCNodeHandler
class ANodeHandler {
@PmlQuery("~ ANode")
}
class BNodeHandler {
@PmlQuery("~ BNode")
}
class InsideACNodeHandler {
@PmlQuery("~ ANode > CNode")
}
class InsideBCNodeHandler {
@PmlQuery("~ BNode > CNode")
}
ANodeHandler
will match anyANode
with the query~ ANode
.BNodeHandler
will match anyBNode
with the query~ BNode
.InsideACNodeHandler
will ONLY match aCNode
if it's a direct child of anANode
, with the query~ ANode > CNode
.InsideBCNodeHandler
will ONLY match aCNode
if it's a direct child of anBNode
, with the query~ BNode > CNode
.
Note the use of the
~
and>
operators, for global and direct-child matching respectively.
In practice, one of these annotated handlers would look something like the following.
@PmlQuery("~ ANode > CNode")
class InsideACNodeHandler : NodeHandler<CNode>
{
override fun handleNode(node: CNode, ancestorNodes: List<Node>)
{
prindentln(ancestorNodes.size, "InsideACNodeHandler (~ A > C) : ${node::class.simpleName}")
}
}
With this in mind, the following object graph can be traversed and use the corresponding handling.
flowchart TD
ANode1
ANode2
BNode1
CNode1
CNode2
CNode3
CNode4
ANode1 --> BNode1
BNode1 --> ANode2
ANode1 --> CNode1
BNode1 --> CNode2
ANode2 --> CNode3
CNode2 --> CNode4
ANode1
is handled byANodeHandler
.BNode1
is handled byBNodeHandler
.CNode1
is handled byInsideACNodeHandler
, as theCNode
is a direct child ofANode
.ANode2
is handled byANodeHandler
.CNode2
is handled byInsideBCNodeHandler
, as theCNode
is a direct child ofBNode
.CNode3
is handled byInsideACNodeHandler
, as theCNode
is a direct child ofANode
.CNode4
is not handled, as there are no handlers to match arbitraryCNode
objects.
PML selectors use the following grammar.
( ( '>' | '~' ) ( <name> | '*' ) ( '{' <expression> '}' )? )+
- A traversal operator — either
~
for deep matching, or>
for direct child (flat) matching. - An element identifier — either a name for node name matching, or
*
for any (wildcard) matching. - An optional boolean logic expression to perform fine grain matching.
TODO
PML selector specificity is, again, similar to the implementation familiar to those in CSS. When competing selectors target the same node, the selector with the highest specificity will be used.
The selector specificity is implemented as a 3-value type, in order to allow for simple precedence comparison.
- The first value is derived from traversal operators.
- The second value is derived from element matching types.
- The third value is derived from whether expressions are present, and the expression(s) complexity.
For example the following selectors both target BNode
with differing specificity.
~ BNode
This selector has a specificity of (a=1, b=10, c=0)
- A deep matching operator increases
a
by1
. - A named element matching selector increases
b
by10
. - There are no expressions so
c
remains0
.
~ ANode > BNode
This selector has a specificity of (a=11, b=20, c=1)
- A deep matching operator increases
a
by1
, and a child matching operator increasesa
by10
, to a total of11
- Two named element matching selectors increase
b
by10
each, to a total of20
. - There are no expressions so
c
remains0
.
TODO
TODO
The MIT License; please see the license file for more information.