XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Simple Text Wrapping

Simple Text Wrapping

September 11, 2002

Introduction

SVG 1.0 includes support for manipulating and representing text. There's an entire chapter devoted to text in the specification. Text in SVG is real text; to write Hello World! in an SVG document, you have to write something like <text>Hello World!</text>. This comes in handy with regard to accessibility as it means that SVG text is searchable and indexable. Looking through the chapter we can see a number of appealing text features: precise text positioning, support for bidirectional text, text on a path, and so on. However, you'll find that text wrapping is missing. Let's see what can be done with the current set of SVG 1.0 features to extend it to do some simple text wrapping.

The Task at Hand

Before delving into the problem, we should specify the features we want to support. The main thing is to be able to break a string into a multiline paragraph, given a column width. Next, we might take a crack at text alignment: left, right, center, and full justification. Line-breaking will only be done on spaces, no funny stuff with hyphens or dictionaries. That's it. For refinements, we'll consider CSS for font properties, line intervals, and text rendering quality. But we also want to provide a nice architecture for our component; we're going to give it a nice XML front-end. So here's what we came up with:


<text:wrap xmlns:text="http://xmlns.graougraou.com/svg/text/"
 					     x="10.5" y="47.5" width="440"
 					     style="font-family: arial;
                  font-size: 11px;
                  text-rendering: optimizeLegibility;
                  text-align: justify;
                  line-interval: 1.5em"><!-- your text here --></text:wrap>
			

We introduce a new element wrap within a new namespace text that is singled out among other namespaces by its URI http://xmlns.graougraou.com/svg/text/. This new element <text:wrap> has four attributes. You can position your element using the x and y attribute much in the same way you would position a regular SVG <text> element. We also define the attribute width to specify the width of the box we want our text to be wrapped into. Having a width attribute shows our strong bias for western languages -- no vertical stuff here. We should really have come up with a length attribute that would have worked with vertical text as well since it would not have been that much more work. So with those three attributes we specify an abstract rectangle with a free height that will be the bounds for our wrapping. The style attribute introduces CSS properties for some simple text styling.

Before reading further, have a look at the demonstration file for the final component. The idea is to supply the user with two types of control so that they can interact with the component. They are provided with buttons to change the text alignment to left, right, center or justify, and increase or decrease the font-size. Here's what the demo looks like on a Mac:

Screenshot of the Simple SVG Text Wrapper

A Quick Look at the TextWrap class

In order to implement this new <text:wrap> element, we're going to design a TextWrap JavaScript class that will interact with our JavaScript document. Before our class can do anything interesting, we need a way to analyze our document for these new elements. We're going to have the TextFlow._init() method just for that:


TextWrap._init = function () {
	var elements = document.documentElement.getElementsByTagNameNS(this.ns, 'wrap');
	for (var i=0; i<elements.length; i++) {
		this._instances.push( new TextWrap(i, elements.item(i)) );
	}
}
			

Previously in the code, we have declared two class members TextFlow.ns and TextFlow._instances to match to our elements namespaces and store our new TextFlow instances into an array:


TextWrap.ns = 'http://xmlns.graougraou.com/svg/text/';

TextWrap._instances = new Array();
			

Wwe need to bootstrap the script with the SVG so that our initialization method runs when the document is loaded, and so that our new elements are within the correct namespace. We also make sure that if the document is viewed with the latest incarnation of the Adobe SVG Viewer, then it will use its own scripting engine rather than the browser's. Our root element now looks like this:


<svg 	xmlns="http://www.w3.org/2000/svg"
			xmlns:text="http://xmlns.graougraou.com/svg/text/"
			xmlns:a3="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
			a3:scriptImplementation="Adobe" onload="TextWrap._init()">
			

What happens when we create a new TextWrap instance and store it into our _instances array? Essentially, creating a new instance of a TextWrap object will call the TextWrap constructor which looks like this:


function TextWrap (id, node) {
	this._id = id;
	this._node = node;
	this._string = null;
	this._x = null;
	this._y = null;
	this._width = null;
	this._font = null;
	this._size = null;
	this._align = null;
	this._quality = null;
	this._interval = null;
	this._svg = null;
	this._lines = null;
	this._initialized = false;
	this._construct();
}
			

First, we keep track of the two parameters passed to our constructor. Looking back at _init() we see that it passes an id and a pointer to the <text:wrap> element. Now, using _id we will be able to refer back to our instance within the _instances array, and using _node we will be able to get and parse the data we need from our original <text:wrap> element. Then, we initialize a whole lot of instance members in order to keep track of the string, the x and y position, the box width, the font and its size, alignment type, rendering quality, line interval, a pointer to the generated SVG fragment, and finally a pointer to the array of lines that the text is broken into. Finally, our constructor says our current instance is not yet fully initialized and calls the _construct() method that will go forward in the creation of our text-wrapped paragraph. Actually, all of our fields were set to null because we will initialize all of them with a call to _construct():


TextWrap.prototype._construct = function () {
	this._build();
	this._svg.setAttribute('style', this._node.getAttribute('style'));
	var style = this._svg.style;
	this._node.normalize();
	
	this.setString( this._node.firstChild.data );
	this.setX( parseInt(this._node.getAttribute('x')) );
	this.setY( parseInt(this._node.getAttribute('y')) );
	this.setWidth( parseInt(this._node.getAttribute('width')) );
	this.setTextAlign( style.getPropertyValue('text-align') );
	this.setFontFamily( style.getPropertyValue('font-family') );
	this.setFontSize( style.getPropertyValue('font-size') );
	this.setTextRendering( style.getPropertyValue('text-rendering') );
	this.setLineInterval( style.getPropertyValue('line-interval') );

	this._splitString();
	this._layout();
	this._initialized = true;
}
			

Our _construct() method is pretty simple. We start by asking our component to build itself with a call to _build() which will actually generate an SVG place holder <text> element. Then it's time for setting our object's fields. We start off with a neat little trick we've used before (in a previous article about XForms widgets generation) in order to get an actual CSS DOM CSSStyleDeclaration object from a simple text string. Most of the data that we base field initialization on is stored as CSS properties of our <text:wrap> element. To get the actual string of the element we normalize() the node to make it one single text node. Then we eventually initialize our fields...except we don't. We call a whole bunch of setter methods instead. Our fields are actually invisible to the user, much as if they were private -- that's why we have those funny "_" in the field names. So the only public (or recommended) access to our API is through setter methods. These are the same methods that we use in our demo when interacting with the toolbar at the top. Clicking on the "+" icon will only make a call to setFontSize. So once we're done with all this we round off by calling two more methods. Now that we know what the string of our text is, we might want to get around splitting it into different lines with _splitString(), and once the string is split we need to call for a new _layout() so our view is refreshed. So we set our _initialized field to true just in case we want to check everything is setup correctly.

Pages: 1, 2

Next Pagearrow