circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)"
and augment it with some Javascript that should be run when the rule is complete:
circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)" {% CIRCLE(data) %}
The Javascript to be run is enclosed in “{%" and “%}" and must be a function that takes a single argument. In this case, I'm calling a Javascript function called CIRCLE and passing in “data" as an argument. What's this “data" thing? It's the parse tree result for this rule. In this case, it's an array with 8 items. The first item is the string “CIRCLE". The second item is the string “(". The third item is the string that matched the “decimal" rule. And so on. So the CIRCLE Javascript function could use that data array to decide how to draw the circle and draw it right as the description of the circle is parsed. If I extended that to all parts of our language, I could draw the complete compass as the CDL description was being parsed in Nearley.
That's pretty cool, but there are a couple of reasons I don't want to draw that way. First of all, the kind of Javascript allowed in the post-processing step of Nearley rules is pretty limited. For example, I probably want to draw that circle in a particular page element but there isn't any easy way to pass in that information. Second, I would be limited to drawing routines that are neatly encompassed in a single Nearley rule. Doing something like saving a color and reusing it later would be difficult.
A more flexible approach is to use the post-processing to translate into an “intermediate" language -- some kind of data structure that's easier to use than text -- and then have an interpreter that translates from the intermediate language into a drawing. For the intermediate language I'm going to use a Javascript object for each CDL command and I'll fill the object in with an operation type and the parsed arguments.
circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)"
{% data => ({op: “CIRCLE", lineWidth: data[2], lineColor: data[4], fillColor: data[6]}) %}
So now I have a Javascript arrow function that takes the parse result from this rule and uses it to fill in a Javascript object. Nearley will now return this object instead of the parse result. Once I've done this for all the commands in the Compass Description Language the parser will return a list of parsed objects instead of the parse tree.
The rule for the space command is straightforward:
spaceElement -> “SPACE" “(" decimal “)" {% data => ({op:SPACE, n: data[2]}) %}
There's one more change that I need to make in the top-level rule, mostly for convenience:
compassBG -> BGelement:+ {% data => data[0].map(a => a[0]) %}
Without this fix, every object is returned as an array of a single element. (In general, there might be multiple return values for every rule, but this grammar always has only one.) This strips off all those unnecessary arrays.
With these changes in place, when I run the parser the result is in the intermediate language -- an array of objects:
An interpreter is a computer program that takes a computer program in a (usually) different language and interprets it, i.e., makes the program do what it is supposed to do. Of course writing an interpreter for a language like Javascript is a difficult undertaking, but CDL is a very simple language and I'll be able to write a very simple interpreter.
A program in CDL is just a list of objects, so at a high level the interpreter will just be a loop that runs through the list of objects and interprets each object based upon the operation:
function interpretCDL(cdl, debug=false) { for(let i=0;i<cdl.length;i++) { const op = cdl[i].op; if (op == 'CIRCLE') { // Draw a circle if (debug) console.log('Draw a circle.'); } else if (op == 'SPACE') { // Skip some space if (debug) console.log('Skip some space.'); } else { console.log('executeCDL encountered an unknown opcode: '+op); }; }; };
This doesn't do much yet, but it's enough code to test out. I'll modify the code on the Test button so that it passes the parsed CDL to the interpreter.
function test() { console.log('About to test.'); const result = interpretCDL(parseCDL('SPACE(10) CIRCLE(1.5, "black", "none")', true), true); };
And here's the result:
So we've taken a simple program in CDL, parsed it and interpreted it!<div id="map" style="margin-left:10px;"> <svg id="svg" width="500" height="500"></svg> </div>
The basic structure of test.html is stolen from Dragons Abound. There's a column along the left side of the web page that is reserved for controls like the Test button, and the remainder of the page is the “map" area and is filled with a 500x500 SVG.
To pass the SVG element into the Javascript requires some more work in the HTML file for the web page. That part looks like this:
I've used the HTML <script> tag to add Javascript to the web page. After importing the compass code, I use getElementById() to get a pointer to the Test button. getElementById() is a function that walks through HTML to find an HTML element with the specified name. In this case, the “document" variable refers to the current web page, so document.getElementById() searches the current web page to find a named element. Once I've found the Test button I can attach the test function from my compass code to the button so that it runs when the button is clicked.<script type="module"> import Compass from './compass.js'; var testButton = document.getElementById("testButton"); var svg = d3.select("#svg"); testButton.onclick = function () { Compass.test(svg); }; </script>
svg.append('circle') .attr('cx', 25) .attr('cy', 25) .attr('r', 25) .style('fill', 'purple');
Suggestions to Explore
- The printed output of the CDL parser isn't very useful because the returned values just print as “[object Object]". Write a function to “pretty print" the CDL objects in some format that shows the operation name, e.g., “{CIRCLE op}" and use this to improve the debug output.
- Read about D3 and see if you can add lines to the purple circle from the center out to 12 o'clock, 3 o'clock, 6 o'clock and 9 o'clock.
- The if-then-else at the heart of the interpreter exists to associate the operation name (e.g., SPACE, CIRCLE) with the code to implement that operation. Another way to go from a string to an associated value in Javascript is to build an object that you use as a code dictionary:
let code = {'CIRCLE': function () { console.log('Draw a circle.'); }, 'SPACE': function () { console.log('Skip some space'); } };
Modify interpretCDL() to use a code dictionary as above. What are the advantages / disadvantages of this approach compared to the if-then-else approach?
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.