New Open Source Project: LINQ to XML for JavaScript

Return to the
Open XML and JavaScript
Developer Center

Today, I’m very pleased to announce the release on CodePlex (at ltxmljs.codeplex.com) of the first release of an XML processing library for JavaScript that is inspired by and largely similar to the LINQ to XML library for .NET.  One of the most fantastic benefits of LINQ to XML is the expressive way that you can write code to create and transform XML.  The powerful idioms that you can use in LINQ to XML make tough XML processing problems much easier.  In general, this was my motivation to develop this project.

More specifically, over the years, I have written thousands of lines of LINQ to XML code to create and modify Open XML documents.  I have been working on a project that requires that I implement some interesting Open XML functionality in JavaScript.  In order to implement this project in the most efficient manner, I wrote this small JavaScript library that enabled me to, with a minimum of editing, convert a large amount of existing C# code to JavaScript.  I have already translated some pretty complex code and it works well.  In the future, I am going to translate some of the most popular modules from PowerTools for Open XML into JavaScript, and then we’ll see some powerful ways to build web applications that create, transform, query, and view Open XML documents.  This is going to be fun!

This enables web applications of any variety to more easily add Open XML functionality.  It doesn’t matter if your web application is written in PHP, Ruby, Java, C, ASP.NET, legacy ASP, Cold Fusion, or 8080A assembler.  If you can serve up JavaScript to the browser, you can add Open XML functionality into your application:

The list goes on and on.  As I develop each one of these scenarios, I’ll be hosting this JavaScript code right here on OpenXMLDeveloper.org.  You will be able to run these examples without leaving the blog post that introduces each example.

All of these scenarios require sophisticated processing of XML, and I’m sorry, DOM Document is just not going to cut it.  Therefore, I needed a JavaScript implementation of a library that enables the super-expressive coding patterns of LINQ to XML.

LINQ to XML for JavaScript is dependent on the excellent LINQ for JavaScript project. Both projects are licensed under the MIT license.

There is an Open XML / JavaScript Resource Center, where you can find all of the content that we have put together on using Open XML from JavaScript.

But enough of this – let’s dive into some code.

JavaScript is not C#, and there are fundamental semantic differences between them, of course, so there are a number of places where the code changes a bit.  In the rest of this blog post, I’m going to assume knowledge of LINQ to XML.  Further, in this blog post, I am not going to go into all of the technical reasons why the differences are there.  I’ll reserve that for future blog posts.

Different Idiom for Instantiating Namespace and Name Objects

In the following C# snippet, the ns variable is initialized by assigning a string to an XNamespace object, and the XName objects are initialized by ‘adding’ a namespace to a string.  The assignment of a string to an XNamespace object works because there is an implicit type conversion.  The ‘addition’ of an XName object and a string works
because there is an operator overload that produces an XName object.

XNamespace ns = “http://www.ericwhite.com/one”;

 

XName root = ns + “root”;

XName anAttribute = “anAttribute”;

XName child = ns + “child”;

 

XElement x = new XElement(root,

    new XAttribute(anAttribute,
123),

    new XAttribute(“xmlns”,
ns.NamespaceName),

    new XElement(child,
456));

 

Console.WriteLine(x);

 

In JavaScript, there are no implicit conversions that you can write, so instead you must use the new operator to create XNamespace and XName objects.

var ns = new XNamespace(“http://www.ericwhite.com/one”);

 

var root = new XName(ns + “root”);

var anAttribute = new XName(“anAttribute”);

var child = new XName(ns + “child”);

 

var x = new
XElement(root,

    new
XAttribute(anAttribute, 123),

    new XAttribute(“xmlns”, ns.namespaceName),

    new XElement(child, 456));

 

document.writeln(convertToEntities(x.toString(true)));

 

As you will notice in the above example, the idiom of adding a namespace to a string works in JavaScript as well.  However, the mechanism by which it works is different.  Instead of overloading the ‘+’ operator, it works because calling toString on a namespace object results in its curly bracket form, such as ‘{http://www.ericwhite.com}’.  Adding the local name as a string results in something like: ‘{http://www.ericwhite.com}root’.  The ltxml.js module then uses that string to create (or retrieve) an atomized XName
object.  More on this in the next post.

Renaming to Conform to JavaScript Conventions

The next thing that you will notice is that the names of the various properties and methods are in lowerCamelCase, which conforms to JavaScript conventions.  Per JavaScript conventions, a method should start with an uppercase letter on when it is intended to be called using the constructor pattern, in other words, using the new keyword.

So, for example, the public properties and methods of XElement objects look like this:

new XElement(xelement)          copy constructor

new XElement(xname)

new XElement(xname, varargs)

XElement.ancestorsAndSelf()

XElement.ancestorsAndSelf(xname)

XElement.attribute(xname)

XElement.attributes()

XElement.attributes(xname)

XElement.descendantNodesAndSelf()

XElement.descendantsAndSelf()

XElement.descendantsAndSelf(xname)

XElement.getDefaultNamespace()

XElement.getNamespaceOfPrefix()

XElement.getPrefixOfNamespace()

XElement.load(XMLDocument)

XElement.parse()

XElement.removeAll()

XElement.removeAttributes()

XElement.replaceAll(content)

XElement.replaceAttributes(content)

XElement.setAttributeValue(xname, value)

XElement.setElementValue(xname, value)

XElement.toString()

XElement.toString(indent)

 

***** props implemented as fields *****

XElement.name

XElement.nodeType

XElement.parent

 

***** props *****

XElement.getFirstAttribute()

XElement.firstAttribute

 

XElement.getHasAttributes()

XElement.hasAttributes

 

XElement.getHasElements()

XElement.hasElements

 

XElement.getIsEmpty()

XElement.isEmpty

 

XElement.getLastAttribute()

XElement.lastAttribute

 

XElement.getValue

XElement.setValue()

XElement.value

 

Of course, these are only the properties and methods implemented by XElement objects.  Those objects inherit from XContainer, which implements another set of properties and methods.  XContainer inherits from XNode, which again implements some properties and methods.

The class hierarchy of ltxml.js is almost identical to that of LINQ to XML.  I say ‘almost’ because there are a few differences.  For instance in ltxml.js, there is an XEntity class that enables you to serialize entities in an easy way.

Use of Getter and Setter Properties

LINQ to XML implements a number of properties, such as value, isEmpty, and hasAttributes on the XElement class.  If you are using versions of JavaScript prior to ES5, you can use functions that have the word ‘get’ or ‘set’ prepended to them.  For instance, for the value property, you can use setValue(value) and getValue.  For the isEmpty property, you can use getIsEmpty().  Of course, if you are using ES5, you can just use the properties themselves.

Some Properties are implemented as Fields

Certain properties can be implemented as simple properties and it does not make sense to implement getters and setters, or get/set methods.  As an example, in the XElement class, the name, nodeType, and parent properties are simple fields.

***** props implemented as fields *****

* XElement.name

* XElement.nodeType

* XElement.parent

Annotations

In LINQ to XML, annotations are type based, which is to say that you add, retrieve, and remove annotations by identifying them by their type.  Of course, with JavaScript, types are not meaningful in this context, so instead annotations are identified by a unique string, which is the first argument to the addAnnotation method:

o.addAnnotation(‘mySimulatedType’, ‘hello
world’
);

o.addAnnotation(‘text’, ‘goodbye’);

o.addAnnotation(‘number’,
123);

 

You then retrieve them by the same string:

o.annotations(‘text’)

The Ltxml Global Variable

ltxml.js uses the module pattern, which means that it
exposes one and only one global, named Ltxml.  This means that, by
default, you would need to code as follows:

var ns1 = Ltxml.XNamespace.getNone();

var e = Ltxml.XNamespace(“http://www.ericwhite.com”);

 

var x = newLtxml.XElement(e + “root”,

    new Ltxml.XAttribute(Ltxml.XNamespace.getXmlns() + “e”, e.namespaceName),

    new
Ltxml.XElement(e +
“boo”, “yar”),

    newLtxml.XElement(e + “yar”, “boo”));

 

However, prepending Ltxml to the beginning of every reference is inconvenient, and causes you to write code that does not look like LINQ to XML code when using C#.  Instead, at the beginning of your module where you are going to use ltxml.js, you can include the following code:

var XAttribute = Ltxml.XAttribute;

var XCData = Ltxml.XCData;

var XComment = Ltxml.XComment;

var XContainer = Ltxml.XContainer;

var XDeclaration = Ltxml.XDeclaration;

var XDocument = Ltxml.XDocument;

var XElement = Ltxml.XElement;

var XName = Ltxml.XName;

var XNamespace = Ltxml.XNamespace;

var XNode = Ltxml.XNode;

var XObject = Ltxml.XObject;

var XProcessingInstruction =
Ltxml.XProcessingInstruction;

var XText = Ltxml.XText;

var XEntity = Ltxml.XEntity;

var XEnumerable = Ltxml.XEnumerable;

 

Enumerable.prototype.elements =
Ltxml.XEnumerable.prototype.elements;

Enumerable.prototype.ancestors =
Ltxml.XEnumerable.prototype.ancestors;

Enumerable.prototype.ancestorsAndSelf =
Ltxml.XEnumerable.prototype.ancestorsAndSelf;

Enumerable.prototype.attributes =
Ltxml.XEnumerable.prototype.attributes;

Enumerable.prototype.descendantNodes =
Ltxml.XEnumerable.prototype.descendantNodes;

Enumerable.prototype.descendantNodesAndSelf =
Ltxml.XEnumerable.prototype.descendantNodesAndSelf;

Enumerable.prototype.descendants =
Ltxml.XEnumerable.prototype.descendants;

Enumerable.prototype.descendantsAndSelf =
Ltxml.XEnumerable.prototype.descendantsAndSelf;

Enumerable.prototype.elements =
Ltxml.XEnumerable.prototype.elements;

Enumerable.prototype.nodes =
Ltxml.XEnumerable.prototype.nodes;

Enumerable.prototype.remove =
Ltxml.XEnumerable.prototype.remove;

 

This enables you to write code in a very similar way to using LINQ to XML with C#.

Download, Experiment, and Enjoy

ltxml.js is dependent on linqjs, which is available at http://linqjs.codeplex.com.  It is based on version 3.0.3 – Beta 4.  The recommended download if ltxml.js contains the version of linq.js that it has been tested against.  After linq.js is released in its non-beta form, I will update ltxml.js, validating against the latest release.

There are a large number of tests and examples of the use of ltxml.js in the file ltxmlTest.html, which is in the download.  In the near future, I will be updating this file, as well as creating a number of examples that teach how to use ltxml.js, but most LINQ to XML developers will find ltxml.js very familiar.  While I am calling this a beta release, I am using it in a real-world project. I have written about 7,000
lines of JavaScript code that uses this API, and it is working well.

So download it at ltxmljs.codeplex.com,
and let me know how it works for you.

Here is the entire API of ltxml.js:

***** Ltxml *****

* Ltxml.clearCache()

 

****************************************************

*===== XName =====

* new XName(namespace, name)      // namespace is
an XNamespace object, name is string

* new XName(name)                 // name is
string, is in no namespace

* new XName(name)                 // name =
‘{namespaceURI}name’

* XName.get(expandedName)

* XName.get(namespace, localName)

* XName.toString()

 

***** props implemented as fields *****

* XName.localName

* XName.namespace

* XName.namespaceName

 

****************************************************

*===== XNamespace =====

* new XNamespace(uri)

* XNamespace.get(uri)

* XNamespace.getName(localName)

* XNamespace.toString()

 

***** props implemented as fields *****

* XNamespace.namespaceName

 

***** static props *****

* XNamespace.getNone()               // returns
namespace for ‘no namespace’

* XNamespace.none

 

* XNamespace.getXml()                //
http://www.w3.org/XML/1998/namespace

* XNamespace.xml

 

* XNamespace.getXmlns()              //
http://www.w3.org/2000/xmlns/

* XNamespace.xmlns

 

****************************************************

*===== XObject (abstract) =====

* XObject.addAnnotation(type, object)  // type is
string

* XObject.annotation(type)

* XObject.annotations(type)

* XObject.removeAnnotations

* XObject.removeAnnotations(type)

 

***** props implemented as fields *****

* XObject.nodeType

* XObject.parent

 

***** props *****

* XObject.getDocument()

* XObject.document

 

****************************************************

*===== XNode: XObject (abstract) =====

* XNode.addAfterSelf(varargs)

* XNode.addBeforeSelf(varargs)

* XNode.ancestors()

* XNode.ancestors(xname)

* XNode.deepEquals

* XNode.elementsAfterSelf()

* XNode.elementsAfterSelf(xname)

* XNode.elementsBeforeSelf()

* XNode.elementsBeforeSelf(xname)

* XNode.nodesAfterSelf()

* XNode.nodesBeforeSelf()

* XNode.remove()

* XNode.replaceWith(content)

 

***** props implemented as field *****

* XNode.nodeType

* XNode.parent

 

***** props *****

* XNode.getNextNode()

* XNode.nextNode

 

* XNode.getPreviousNode()

* XNode.previousNode

 

****************************************************

*===== XAttribute: XObject =====

* new XAttribute(name, value)

* new XAttribute(XAttribute)

* XAttribute.remove()

* XAttribute.setValue(value)

* XAttribute.toString()

 

***** props implemented as fields *****

* XAttribute.isNamespaceDeclaration

* XAttribute.name

* XAttribute.nodeType

* XAttribute.parent

* XAttribute.value

 

***** props *****

* XAttribute.getNextAttribute()

* XAttribute.nextAttribute

 

* XAttribute.getPreviousAttribute()

* XAttribute.previousAttribute

 

****************************************************

*===== XComment: XNode =====

* new XComment(value)

* new XComment(xcomment)

* XComment.toString()

* XComment.toString(indent)

 

***** props implemented as fields *****

* XComment.nodeType

* XComment.parent

* XComment.value

 

****************************************************

*===== XContainer: XNode =====

* XContainer.add(content)

* XContainer.addFirst(content)

* XContainer.descendantNodes

* XContainer.descendants()

* XContainer.descendants(xname)

* XContainer.element(xname)

* XContainer.elements()

* XContainer.elements(xname)

* XContainer.nodes()

* XContainer.removeNodes()

* XContainer.replaceNodes(content)

 

***** props implemented as fields *****

* XContainer.nodeType

* XContainer.parent

 

***** props *****

* XContainer.getFirstNode()

* XContainer.firstNode

 

* XContainer.getLastNode()

* XContainer.lastNode

 

****************************************************

*===== XDeclaration =====

* new XDeclaration(version, encoding, standalone)

* new XDeclaration(xdeclaration)

* XDeclaration.toString(indent)

 

***** props implemented as fields *****

* XDeclaration.encoding

* XDeclaration.standalone

* XDeclaration.version

 

****************************************************

*===== XDocument: XContainer =====

* new XDocument()

* new XDocument(content)

* new XDocument(xdocument)

* new XDocument(xdeclaration, content)

* XDocument.descendants()

* XDocument.descendants(xname)

* XDocument.parse(xml)

* XDocument.load(XMLDocument)

* XDocument.toString()

* XDocument.toString(indent)

 

***** props implemented as fields *****

* XDocument.nodeType

* XDocument.parent

* XDocument.declaration

 

***** props *****

* XDocument.getRoot()

* XDocument.root

 

****************************************************

*===== XElement: XContainer =====

* new XElement(xelement)          copy
constructor

* new XElement(xname)

* new XElement(xname, varargs)

* XElement.ancestorsAndSelf()

* XElement.ancestorsAndSelf(xname)

* XElement.attribute(xname)

* XElement.attributes()

* XElement.attributes(xname)

* XElement.descendantNodesAndSelf()

* XElement.descendantsAndSelf()

* XElement.descendantsAndSelf(xname)

* XElement.getDefaultNamespace()

* XElement.getNamespaceOfPrefix()

* XElement.getPrefixOfNamespace()

* XElement.load(XMLDocument)

* XElement.parse()

* XElement.removeAll()

* XElement.removeAttributes()

* XElement.replaceAll(content)

* XElement.replaceAttributes(content)

* XElement.setAttributeValue(xname, value)

* XElement.setElementValue(xname, value)

* XElement.toString()

* XElement.toString(indent)

 

***** props implemented as fields *****

* XElement.name

* XElement.nodeType

* XElement.parent

 

***** props *****

* XElement.getFirstAttribute()

* XElement.firstAttribute

 

* XElement.getHasAttributes()

* XElement.hasAttributes

 

* XElement.getHasElements()

* XElement.hasElements

 

* XElement.getIsEmpty()

* XElement.isEmpty

 

* XElement.getLastAttribute()

* XElement.lastAttribute

 

* XElement.getValue

* XElement.setValue()

* XElement.value

 

****************************************************

*===== XProcessingInstruction: XNode =====

* new
XProcessingInstruction(xprocessingInstruction)

* new XProcessingInstruction(target, data)

* XProcessingInstruction.toString()

* XProcessingInstruction.toString(indent)

 

***** props implemented as fields *****

* XProcessingInstruction.data

* XProcessingInstruction.nodeType

* XProcessingInstruction.parent

* XProcessingInstruction.target

 

****************************************************

*===== XText: XNode =====

* new XText(value)

* new XText(XText)

* XText.toString()

 

***** props implemented as fields *****

* XText.nodeType

* XText.parent

* XText.value

 

****************************************************

*===== XEntity: XNode =====

* new XEntity(value)

* new XEntity(XEntity)

* XEntity.toString()

 

***** props implemented as fields *****

* XEntity.nodeType

* XEntity.parent

* XEntity.value

 

****************************************************

*===== XCData: XText =====

* new XCData(value)

* new XCData(XCData)

* XCData.toString()

 

***** props implemented as fields *****

* XCData.nodeType

* XCData.parent

* XCData.value

 

****************************************************

*===== Extension methods =====

* ancestors()

* ancestors(xname)

* ancestorsAndSelf()

* ancestorsAndSelf(xname)

* attributes()

* attributes(xname)

* descendantNodes()

* descendantNodesAndSelf()

* descendants()

* descendants(xname)

* descendantsAndSelf()

* descendantsAndSelf(xname)

* elements()

* elements(xname)

* nodes()

* remove(xname)