This document expects the reader is professionally experienced in the authoring of Web content.
Knowledge of the DOM, HTML and the difference between the two are pre-requisite.
This document avoids prescribing the larger User eXperience of a Hybrid Editor, and merely (hah! merely) attempts to define the broad categories of editing modalities, and capabilities a Hybrid Editor provides.
Further this document attempts to define a performant and maintainable surface area upon which Hybrid Editors may be
built.
Where possible this document is descriptive rather than specific.
Dear reader. Please Ask Questions Under this Section.
Typically tools combining HTML and DOM editing into one authoring tool have been met with negative reaction.
This is not without reason. A hybrid HTML/DOM Editor has many challenges and failure to meet these challenges leads to a sub-satisfactory work-flow.
Some of these challenges are detailed and explored:
Authors:
Q: How do I Ask a Question?
An HTML Editor is a typical Text editor. Any program capable of opening a text file, allowing manipulation of it’s text content, and saving the changes to the file may be used as an HTML editor.
Many applications go further:
The direct or conceptual rendering of a Document Object Model to a 2d plane. A Typical Canvas is a web browser. Any subsection of the DOM may be referred to as a Canvas.
A DOM Editor is a special program capable of rendering a Document Object Model Object graph to a Canvas. A DOM Editor facilitates updating the edges and nodes of that graph while re-painting the Canvas. Most Web Browsers are DOM Editors. The specification for the DOM includes an API for editing DOM graphs with methods such as:
document.createElement(nodeName)
Node#appendChild(childNode)
document.createAttribute(attributeName)
DOM Editors may go further, facilitating creation and manipulation of a DOM graph without directly using the API methods. High level modalities such as “Drag and Drop” may be used. Visual lists of Node types may be shown to content authors. And text may be edited directly with a keyboard. These Editors are generally known by the term: WYSIWYG, or “What You See is What You Get”.
Code projection is the direct mapping of source code from an HTML Editor to a DOM Editor.
A sub-component of Code Projection is Source Mapping, or the reverse mapping of DOM Elements to character positions in the HTML Editor.
There may not be an 1-to-1 mapping of the tags written in the HTML source to the Nodes in the Canvas.
In the course of projection some elements may be transformed, expanded, or exploded to present alternate, extended, or generated content.
This transformation capability will be explored further in the Plugins section.
It is ultimately the combination of Code Projection and Source Mapping that define the Hybrid Editor as a unique system.
The Quality of the Code Projection is of ultimate importance to the function, development, and maintainability of a Hybrid Editor.
Allowing plugins to manipulate the projection between HTML and DOM.
A: Ask a question by creating a new sub-item in the Q & A item
. Hopefully somebody will answer it quickly. If you see an unanswered question and you know the answer, answer it.
Knowing which character ranges in the HTML source correspond to which DOM elements in the Graph is useful for the following capabilities:
And more importantly:
This problem has been solved by Mozilla in their “Slowparse” module: https://github.com/mozilla/slowparse
Contrary to the name, “Slowparse” is fast. Fast enough to parse a 700+ line text file on 2013 hardware in > 10ms
. On other hardware this time may be in the 10-60ms
range. This makes Slowparse “Fast Enough” for typical workflows where source files can be kept under several hundred lines long. On the slower end of this scale Slowparse maybe ‘debounced’ to only run after user input has halted for a fraction of a second.
Slow parse emits a DOM Tree in which Nodes and Attributes are given a parseInfo
expando detailing the beginning and ending positions in the source text.
Further performance might be attained by implementing a re-entrant parser. Rather than parsing the entire document each time the source code changes, the parseInfo
could be used to determine what part of the DOM corresponds to the current character position and making that change directly or resuming parsing in the middle of the document.
That would necessitate traversing the DOM and manually updating the parseInfo
to include the expanded or contracted character range. The DOM is an optimal structure for traversing the nested intervals.
Roughly the algorithm looks like this:
(Glossing over special cases that exist depending on plugin functions.)
# Where delta is an Integer.
# + for character insertion
# - for character deletion
nudgeIntervals(domNode, delta) ->
domNode.parseInfo.endChar += delta
while domNode = domNode.parentNode
domNode.parseInfo.endChar += delta
HTML to DOM mapping is not currently solved by Slowparse.
This is a necessary problem to solve for the following capability:
Changing the selection selection of the HTML Editor highlights the corresponding Element in the Canvas.
Minimally updating the Canvas as the HTML source is edited. See “Minimal Changes”
There are two flavors of this problem.
For the Point case the solution is the object in the DOM with the shortest difference between the start and end of it’s parseInfo
that overlaps the character position of the selection.
For the Range case the solution is all objects in the DOM whose parseInfo
ranges partially or fully intersect the character positions in the selection.
Note that a DOM -> HTML source mapping is a special case of Interval Tree. No intervals in the mapping between. All nested intervals are wholly contained by their parents.
AS edits are made to the HTML source a hidden Canvas will be updated with it’s contents.
A recursive algorithm will compare this hidden Canvas with the Canvas used for DOM Editing.
Only elements that have been deemed “changed” will be updated. If only an attribute has been changed/added/removed the element will not be reconstructed.
To save speed the hidden Canvas may not be a fully ‘inflated’ DOM but a similar tree of objects. This way only objects in the DOM that must be replaced will actually be re-constructed.
Libraries for finding diffs between DOM trees can serve as the basis for this algorithm, though changes will be required to allow for projection plugins, parseInfo
updates, and the noted speed increases.
https://github.com/pomax/DOM-diff/tree/gh-pages
https://github.com/facebook/react
Updates from the DOM to the HTML should be performed through an API and not directly to the Canvas.
This is because plugins may exist between the HTML source and the projected DOM.
It is simpler if there is a single execution path for projection in the direction of HTML —> DOM.
Fortunately this API can be made to mirror the existing W3C api for DOM editing. It may also be extended to a jQuery style api for convenience. This way the implementation of the DOM editor need not worry about how it’s changes will affect the source.
A possible editor might call this:
newChild = document.createElement('span')
API(event.target).append newChild
or
API(event.target).append """
<section class="span4">
"""
or
API(event.target).classList.remove 'jumbotron'
etc.
The API
will detect and interpret the parseInfo
and insert/delete specific characters in the HTML source accordingly.
Changes through the Projector API will be treated like end-user edits and kick off the normal HTML —> DOM update procedures.
In the course of editing it may be desireable to affix “Chrome”, or visual indications of the end-users current or potential actions .
Because mappings between the HTML and DOM are tracked through parseInfo
exapandos on DOM elements, and all edits to the DOM are sent through a Projector API, it is safe to insert elements to the Canvas DOM tree via the existing DOM editing methods. These elements will be ignored.
The special case is when Editor “Chrome” has been inserted into an element which has been removed or so substantially changed that the diff algorithm considers the parent to have changed. Then the contents of the element will be destroyed.
Given the following data object:
{
"currentPage": {
"title":"Welcome to Example.Org"
}
}
It may be desirable to transform the following source HTML
<title>{{currentPage.title}}</title>
into the following DOM (represented as HTML)
<title>Welcome to Example.Org</title>
A plugin might define the following behavior.
Given source HTML:
<ol>
<li repeat="x in 1..3">
List Item #{{x}}
</li>
</ol>
The following DOM (represented here as HTML) will be generated:
<ol>
<li>List Item #1<li>
<li>List Item #2<li>
<li>List Item #3<li>
</ol>
The same plugin might define the following behavior:
Given this source HTML
<dl repeat="key, value of things">
<dt>{{key}}</dt>
<dd>{{value}}</dd>
</dl>
And a JavasScript Object similar to this:
things = {
"Ice Cream": "Delicious",
"Bears": "Terrifying"
}
The following DOM (again as HTML) is produced:
<dl>
<dt>Ice Cream</dt>
<dd>Delicious</dd>
<dt>Bears</dt>
<dd>Terrifying</dd>
</dl>
It is common in Web Development to have a system of Layouts and Partials, the exact nomenclature differs, but many tools provide these concepts.
Given a File: layouts/index.html
<header>
<img src="logo.png" alt="Example.Org" />
<template ref="header-content" />
</header>
<nav>
<template ref="sidebar-content" />
</nav>
<section class="container">
<template ref="template-content" />
</section>
And a file: index.html
<template rel="layouts/index.html>
<template id="header-content">
<h1>Hello!</h1
</template>
<template id="sidebar-content">
<a href="/about.html">Learn About Us</a>
</template>
<template id="template-content">
<h1>You've reached the Index :D</h1>
<p>That's all Folks</p>
</template>
</template>
While the index.html
file is opened, a plugin might project the following content in the Canvas:
<header>
<img src="logo.png" alt="Example.Org" />
<h1>Hello!</h1>
</header>
<nav>
<a href="/about.html">Learn About Us</a>
</nav>
<section class="container">
<h1>You've reached the Index :D</h1>
<p>That's all Folks</p>
</section>
Here, intersecting sections of the DOM are projected from two files.
Pending