Extending Daxe


Introduction

XML editors which do not have knowledge of specific XML languages cannot provide a very good user interface. For instance, it would be hard to edit HTML documents if the p (paragraph) and a (anchor or link) elements from HTML were displayed in the same way. One is a block element, separating parts of the text, while the other is an inline element, which can be mixed with text. While some (usually data-oriented) XML languages can easily be edited with a tree, a tree user interface does not work so well with other XML languages.

On the other hand, creating a new editor from scratch for each XML language is not the most efficient solution, as at least half of the code will be the same from one editor to another. For instance, all editors have to ensure document validity, and they have to provide ways to edit content with a cursor aware of the XML structure. Common parts of XML editors should be reused.

Daxe is an answer to these questions: it provides all the building blocks for a new XML editor for a specific language, many predefined displays for elements which can be associated to the language XML elements, and ways to extend it further and include it into a larger application.

Daxe configuration files provide an easy way to customize the editor for a given XML language using built-in solutions. The following pages describe how to go beyond what can be done with configuration files, especially to create new display types.


Creating a new extension

An extension of Daxe has to be implemented in Dart, so the Dart SDK needs to be installed. Daxe's API documentation can be generated from the the source with the dartdoc command launched in the root of the source tree (the daxe directory).

An extension is a Dart web application. Here is a basic example, adding a new display type named mydn with the class MyDN. This code would be in a file named my_daxe.dart:

      library my_daxe;
      
      import 'package:daxe/daxe.dart';
      part 'my_dn.dart';
      
      void main() {
        NodeFactory.addCoreDisplayTypes();
        
        setDisplayType('mydn',
              (x.Element ref) => new MyDN.fromRef(ref),
              (x.Node node, DaxeNode parent) => new MyDN.fromNode(node, parent)
          );
      
        Strings.load().then((bool b) {
          initDaxe();
        }).catchError((e) {
          h.document.body.appendText('Error when loading the strings.');
        });
      }
    

Creating new display types

A display type is defined by a class deriving from the DaxeNode class in the daxe package.

The implementation of the display type class needs 2 constructors, .fromRef() to create a new instance from the language definition of the element, and .fromNode(), to create a new instance from a DOM node. It also needs at least one method, html(), which returns the DOM node for the node to display, with the node id. As an example, let's look at the implementation for the string display, which is simply an inline display with a start tag and an end tag:

      part of nodes;
      
      class DNString extends DaxeNode {
        Tag _b1, _b2;
        
        DNString.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
          _b1 = new Tag(this, Tag.START);
          _b2 = new Tag(this, Tag.END);
        }
        
        DNString.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
          _b1 = new Tag(this, Tag.START);
          _b2 = new Tag(this, Tag.END);
        }
        
        @override
        h.Element html() {
          var span = new h.SpanElement();
          span.id = "$id";
          span.classes.add('dn');
          if (!valid)
            span.classes.add('invalid');
          span.append(_b1.html());
          var contents = new h.SpanElement();
          DaxeNode dn = firstChild;
          while (dn != null) {
            contents.append(dn.html());
            dn = dn.nextSibling;
          }
          setStyle(contents);
          span.append(contents);
          span.append(_b2.html());
          return(span);
        }
        
        @override
        h.Element getHTMLContentsNode() {
          return(getHTMLNode().nodes[1]);
        }
      }
    

The constructors derive from the fromRef() and fromNode() constructors in DaxeNode, doing all the basic initialization for free. The start and end tags are created in the constructors, using the Tag class. The html() method creates a span for the DOM node, sets the id based on DaxeNode.id, adds an invalid CSS class if necessary, and appends the tags' HTML nodes and the contents in another span. Style is applied to the contents with DaxeNode.setStyle(), and the span is returned.

Another method is overridden, getHTMLContentsNode(), to return the DOM node containing the children, which can be different depending on the implementation of html(). In this case, we can simply return the second child of the node's DOM node, which we can get with DaxeNode.getHTMLNode().

While all display type classes have to derive from DaxeNode, all the methods can be overridden, so these classes have complete control over appearance and resulting DOM, for the node itself and all the descendants.


More customization

Configuration files can be used to customize the menus used to insert nodes, but it is also possible to define menus with custom functions, and only a Daxe extension can implement these. It is also easy to customize the toolbar, the left panel, and the function used to save documents.

Function menus in configuration files are defined with a FUNCTION_MENU element, and these have a function_name attribute with the function name. This function can be added to Daxe in an extension with the addCustomFunction() function in the daxe package.

More customization can be done by passing named parameters to the initDaxe() function:

On top of that, more code can be executed after initialization, by using the fact that initDaxe() returns a Future. Here is an example combining different customizations:

(Daxe's DOM package is included with import 'package:daxe/src/xmldom/xmldom.dart' as x; in this example)

        // use a custom insert panel, MyOwnInsertPanel, implemented elsewhere
        InsertPanel insertP = new MyOwnInsertPanel();
        LeftPanel left = new LeftPanel(insert:insertP);
        ActionFunction saveFunction= () {
          // display an alert after a save
          doc.save().then((_) {
            h.window.alert(Strings.get('save.success'));
          }, onError: (DaxeException ex) {
            h.window.alert(Strings.get('save.error') + ': ' + ex.message);
          });
        };
        ActionFunction customizeToolbar = () {
          // add a button to insert a mydn node
          Toolbar toolbar = page.toolbar;
          List<x.Element> refs = doc.cfg.elementsWithType('mydn');
          if (refs != null && refs.length > 0) {
            ToolbarBox myBox = new ToolbarBox();
            toolbar.addInsertButton(doc.cfg, myBox, refs, 'my_dn.png');
            toolbar.add(myBox);
          }
        };
        Strings.load().then((bool b) {
          initDaxe(left:left, saveFunction:saveFunction,
              customizeToolbar:customizeToolbar).then((v) {
            // more customizations can be added here
          }).catchError((e) {
            String msg = 'Initialization error: ' + (e is String ? e : e.toString());
            print(msg);
            h.document.body.appendText(msg);
          });
        }).catchError((e) {
          String msg = 'Initialization error: ' + (e is String ? e : e.toString());
          print(msg);
          h.document.body.appendText(msg);
        });
    

A large example of an extension is LON-CAPA Daxe, which lets LON-CAPA users edit documents using the LON-CAPA language, featuring a mix of HTML and custom elements for online problems. The source code is currently (as of 2017) available here.