Skip to content

Anatomy of a behaviors object

Hugh A. Cayless edited this page Jun 12, 2020 · 27 revisions

CETEIcean behaviors

(See the Behaviors Cookbook page for more examples)

Behaviors are custom styles, event handlers, and widgets to be added to your TEI elements. CETEIcean has some default behaviors built in, but you can add to or override those by supplying your own behaviors object via the addBehaviors method or one-by-one with the addBehavior method. A behavior is a function that takes the element to be modified as a parameter and (usually) returns a node that will visually replace the content of that element. If content is returned, the element's original content is retained, but hidden.

A behaviors object looks like (e.g.) this:

{
  // No need to re-declare these in a custom behaviors object
  "namespaces": {
    "tei": "http://www.tei-c.org/ns/1.0",
    "teieg": "http://www.tei-c.org/ns/Examples",
    "rng": "http://relaxng.org/ns/structure/1.0"  
  },
  "tei": {
    // wraps the content of <eg> in <pre>...</pre> 
    "eg": ["<pre>","</pre>"],
    // inserts a link inside <ptr> using the @target; the link in the
    // @href is piped through the rw (rewrite) function before insertion
    "ptr": ["<a href=\"$rw@target\">$@target</a>"],
    // wraps the content of the <ref> in an HTML link
    "ref": ["<a href=\"$rw@target\">","</a>"],
    // maps the <graphic> element to a function that returns an <img> with width 
    // height values taken from the graphic.
    "graphic": function(elt) {
      let content = new Image();
      content.src = this.rw(elt.getAttribute("url")); // "this" is the CETEI object.
      if (elt.hasAttribute("width")) {
        content.setAttribute("width",elt.getAttribute("width"));
      }
      if (elt.hasAttribute("height")) {
        content.setAttribute("height",elt.getAttribute("height"));
      }
      return content;
    }, ...,
    "add": ["`","´"],
    // Supplies two behavior functions. One will run on <supplied reason="lost"> and the
    // other on <supplied reason="ommitted">. 
    "supplied": [ 
      ["[reason=lost]", ["[","]"]],
      ["[reason=omitted]", ["&lt;","&gt;"]]
     ]
  }
}

It contains a "namespaces" object, which defines the namespaces used in the input document, mapping them to prefixes, which will be used as the prefixes in the resulting HTML document. Any element in the given namespace will be converted by CETEIcean into an HTML Custom Element with a name taken from the prefix, a dash, and the lowercased original element name (so <eg xmlns="http://www.tei-c.org/ns/1.0"> above becomes <tei-eg>).

For reference, here is an example of how to set up your own behaviors object and add it to a page (from the addBehaviorTest example):

    <script src="../js/CETEI.js"></script>
    <script>
      var c = new CETEI();
      c.addBehaviors({"tei":{
        // Overrides the default ptr behavior, displaying a short link
        "ptr": function(elt) {
          var link = document.createElement("a");
          link.innerHTML = elt.getAttribute("target").replace(/https?:\/\/([^\/]+)\/.*/, "$1");
          link.href = elt.getAttribute("target");
          return link;
        },
        // Adds a new handler for <term>, wrapping it in an HTML <b>
        // Note that you could just do `term: ["<b>","</b>"]` instead.
        "term": function(elt) {
            var b = document.createElement("b");
            b.innerHTML = elt.innerHTML;
            return b;
        },
        // Inserts the first array element before tei:add, and the second, after.
        "add": ["`","´"]
      }});
      c.getHTML5('testTEI.xml', function(data) {
        document.getElementById("TEI").innerHTML = "";
        document.getElementById("TEI").appendChild(data);
      });
    </script>

Because CETEIcean was created for use with TEI, the TEI namespaces are defaults, and your custom behaviors object need not (re)define them. If you're dealing with a source that lacks namespaces, then you can map a prefix to the empty string, e.g.:

{
  "namespaces": {
    "teip4": "" // TEI P4 didn't have a namespace
  }
}

Note: Re-declaring an already-declared prefix will not have any effect. Nor will declaring a new namespace with an existing prefix. If you need to re-use a built-in prefix, like "tei", you have to first unset it, thus:

let c = new CETEI();
/* We want to deal with TEI P4, but re-use some existing
   behaviors (note that they're pretty different and this
   might not be a good idea in practice). */
c.unsetNamespace("http://www.tei-c.org/ns/1.0");
c.addBehaviors({"namespaces":
  {"tei":""},
  "tei": {
    // Your new behaviors here
  }
}

To define mappings of element names to behavior functions, use a property named for the prefix. This property will contain functions named for the element to be modified, arrays of strings containing content to be inserted before and/or after the element's content, or arrays in which the first element is a CSS selector and second a function or array. If an array of strings is supplied, CETEIcean generates a function from it that will return an element to be appended to the original element. If an array of arrays is supplied, the first matching selector will dictate which function is applied. If no matches are found and the last array has the first element "_", then that will serve as a default. Inside behavior functions, this will be the CETEI object, so you will always be able to access its built-in functions and properties.

Behavior functions take the element to be modified as a parameter and return an element to be appended to the parameter element. They may also return nothing and simply modify the passed element (e.g. by adding an attribute) or they may modify another part of the DOM. For example, see the default behavior for <title> inside <titleStmt>, which takes the text content of the title and appends it as an HTML <title> to the HTML document header (changing the window or tab header):

"title": [
  ["tei-titlestmt>tei-title", function(elt) {
    let title = document.createElement("title");
    title.innerHTML = elt.innerText;
    document.querySelector("head").appendChild(title);
  }]
]

Note: This behavior only matches <tei-title> inside a <tei-titlestmt>. All other instances of it will be ignored. The function returns nothing, so the content of the title isn't changed. Although behavior keys use the source document element names, inside the behavior body, you must use the transformed element names (prefixed, all lower-case), because the function is acting upon the document after it has been converted to HTML Custom Elements.

If behavior function returns an element, and the source element already has content, that content will be wrapped in a <span> with the attributes data-original and hidden set on it, so the old content will be invisible, while the new content returned by the function will be inserted inside the input element.

If a one or two item array is supplied instead of a function, the array's contents will be inserted inside the element. If the elements contain no markup, they will be wrapped in an HTML <span>. The first array member will be inserted as the first child of the element, and the second (if any) as the last. The element's original content (if any) will be kept in a hidden <span> These "decorators" provide an alternative to using :before and :after selectors with content in CSS. Because it has been added via CSS, the latter content is not technically part of the page content, and so cannot be selected and therefore can't be copied and pasted. Text inserted by CETEIcean does not have this drawback (which may be an advantage for certain applications). For example,

"add": ["`","´"]

will result in the output:

<tei-add data-origname="add" data-processed>
  <span hidden data-original>added</span>
  <span>`added´</span>
</tei-add>

For a more complex example, consider the ptr element. When a TEI <ptr> element is encountered, an HTML <a href=""> element will be inserted inside it, with the @target of the ptr as both link text and @href. The @href text will be piped through CETEIcean's rw() function, which rewrites relative URLs (because the context of the TEI source is probably different from that of the display). So the default behavior:

"ptr": ["<a href=\"$rw@target\">$@target</a>"]

will turn

<ptr target="addBehaviorTest.html"/>

into:

<tei-ptr target="addBehaviorTest.html" data-teiname="ptr" data-empty><a href="https://teic.github.io/CETEIcean/addBehaviorTest.html">addBehaviorTest.html</a></tei-ptr>

A few more notes:

Elements processed by CETEIcean will acquire a few custom data attributes,

  1. data-origname, which contains the name of the original element, preserving case.
  2. data-empty, if the original element was empty (it may now have content, like <ptr> above).
  3. data-processed, which is an internal flag CETEIcean uses to avoid processing elements repeatedly. CETEIcean provides a method, copyAndReset(node), which attempts to make a "fresh" copy of the node passed in to it, which could (e.g.) be inserted elsewhere in the document. It would be processed at that point. Processed elements can be cloned as normal. copyAndReset(node) is intended for the case where the copied node should behave differently from its original, e.g., because it has been inserted into a different context.
  4. If the element has a default namespace declaration, it will acquire a data-xmlns attribute preserving that. Namespace declarations with prefixes should be passed through as-is.
Clone this wiki locally