Hack #35. Enter Textile Markup in Web Forms

Add a button to textareas to convert textile input to XHTML.

Textile is a minimalist markup language invented by Dean Allen for his weblog publishing system, Textpattern. Dean originally wrote a Textile-to-XHTML library in PHP. I quickly ported it to Python, and Jeff Minard took my Python version and ported it to JavaScript. Roberto De Almeida hacked together a Greasemonkey script to allow you to enter Textile markup in web forms by calling a CGI script on his server to do the conversion. Then, Phil Wilson improved on Roberto's work by integrating Jeff's JavaScript library, thus making the entire hack self-contained and free of external dependencies.

People ask why I love open source; this hack is why. This script was written by one person, then improved by a second person by integrating code written by a third person, who based his code on the work of a fourth person, who in turn based his code on the work of a fifth person. The end result of this collaboration is that you can write Textile markup in web forms and then convert it to XHTML with a single click. Everything is done locally, and then the form is submitted to the originating site as usual. There are no calls to third-party servers, and the originating site never has to know or care that you originally entered your comments in Textile format. That's just beautiful.

Tip

See a sample of Textile markup at http://textism.com/tools/textile/?sample=2.

The Code

This user script runs on all pages. The most complex part is the textile function, which converts Textile markup to XHTML. The rest of the script is straightforward: find all the <textarea> elements and create a button next to each textarea that calls the textile function.

Save the following user script as textile.user.js:

		// ==UserScript==
		// @name		  Instant Textile
		// @namespace	  http://philwilson.org/
		// @description	  Allow Textile input in web forms
		// @include		  http*://*
		// ==/UserScript==

		// based on code by Phil Wilson, Robert De Almeida, and Jeff Minard
		// and included here with their gracious permission
		function textile(s) {
			var r = s;
			// quick tags first
			qtags = [['\\*', 'strong'],
				 ['\\?\\?', 'cite'],
				 ['\\+', 'ins'],
				 ['~', 'sub'],
				 ['\\^', 'sup'],
				 ['@', 'code']];
			for (var i=0;i<qtags.length;i++) {
				ttag = qtags[i][0]; htag = qtags[i][1];
				re = new RegExp(ttag+'\\b(.+?)\\b'+ttag,'g');
				r = r.replace(re,'<'+htag+'>'+'$1'+'</'+htag+'>');
			}

			// underscores count as part of a word, so do them separately
			re = new RegExp('\\b_(.+?)_\\b','g');
			r = r.replace(re,'<em>$1</em>');

			//jeff: so do dashes
			re = new RegExp('[\s\n]-(.+?)-[\s\n]','g');
			r = r.replace(re,'<del>$1</del>');

			// links
			re = new RegExp('"\\b(.+?)\\(\\b(.+?)\\b\\)":([^\\s]+)','g');
			r = r.replace(re,'<a href="$3" title="$2">$1</a>');
			re = new RegExp('"\\b(.+?)\\b":([^\\s]+)','g');
			r = r.replace(re,'<a href="$2">$1</a>');
			
			// images
			re = new RegExp('!\\b(.+?)\\(\\b(.+?)\\b\\)!','g');
			r = r.replace(re,'<img src="$1" alt="$2">');
			re = new RegExp('!\\b(.+?)\\b!','g');
			r = r.replace(re,'<img src="$1">');
			
			// block level formatting
		
			// Jeff's hack to show single line breaks as they should.
			// insert breaks - but you get some….stupid ones
			re = new RegExp('(.*)\n([^#\*\n].*)','g');
			r = r.replace(re,'$1<br />$2');
			// remove the stupid breaks.
			re = new RegExp('\n<br />','g');
			r = r.replace(re,'\n');
			
			lines = r.split('\n');
			nr = '';
			for (var i=0;i<lines.length;i++) {
				line = lines[i].replace(/\s*$/,'');
				changed = 0;
				if (line.search(/^\s*bq\.\s+/) != -1) {
				   line = line.replace(/^\s*bq\.\s+/,'\t<blockquote>') +
				   '</blockquote>';
				changed = 1;
			}
			
			// jeff adds h#.
			if (line.search(/^\s*h[1-6]\.\s+/) != -1) {
				re = new RegExp('h([1-6])\.(.+)','g');
				line = line.replace(re,'<h$1>$2</h$1>');
				changed = 1;
			}

			if (line.search(/^\s*\*\s+/) != -1) {
				line = line.replace(/^\s*\*\s+/,'\t<liu>') + '</liu>';
				changed = 1;
			} // * for bullet list; make up an liu tag to be fixed later
			if (line.search(/^\s*#\s+/) != -1) {
				line = line.replace(/^\s*#\s+/,'\t<lio>') + '</lio>';
				changed = 1;
			}

			// # for numeric list; make up an lio tag to be fixed later
			if (!changed && (line.replace(/\s/g,'').length > 0)) {
				line = '<p>'+line+'</p>';
			}
			lines[i] = line + '\n';
		}
		
		// Second pass to do lists
		inlist = 0;
			listtype = '';
		for (var i=0;i<lines.length;i++) {
			line = lines[i];
			if (inlist && listtype == 'ul' && !line.match(/^\t<liu/)) {
				line = '</ul>\n' + line;
				inlist = 0;
			}
			if (inlist && listtype == 'ol' && !line.match(/^\t<lio/)) {
				line = '</ol>\n' + line;
				inlist = 0;
			}
			if (!inlist && line.match(/^\t<liu/)) {
				line = '<ul>' + line;
				inlist = 1;
				listtype = 'ul';
			}
			if (!inlist && line.match(/^\t<lio/)) {
				line = '<ol>' + line;
				inlist = 1;
				listtype = 'ol';
			}
			lines[i] = line;
		}
		r = lines.join('\n');

		// finally, replace <li(o|u)> AND </li(o|u)> created earlier
		r = r.replace(/li[o|u]>/g,'li>');

		return r;
	}

	var arTextareas = document.getElementsByTagName("textarea");
	for (var i = 0; i < arTextareas.length; i++) {		
		var elmTextarea = arTextareas[i];
		var sID = elmTextarea.id;
		var elmButton = document.createElement("input");
		elmButton.type = "button";
		elmButton.value = "Textile it!";
		elmButton.addEventListener('click', function() {
			elmTextarea.value = textile(elmTextarea.value);
		}, true);
		elmTextarea.parentNode.insertBefore(elmButton,
			elmTextarea.nextSibling);
	}

Running the Hack

After installing the user script (Tools Install This User Script), go to http://simon.incutio.com/archive/2005/07/17/django. At the bottom of the page is a form for submitting comments. Enter some Textile markup, as shown in Figure 4-8.

Textile markup

Figure 4-8. Textile markup

Now, click the "Textile it!" button, and your Textile comment will be converted to valid XHTML, as shown in Figure 4-9.

Now you can submit your comment by clicking Preview Comment.

Hacking the Hack

This hack is cool, but it still requires an extra click to convert your comments from Textile to XHTML. What's that? An extra step, you say? Bah. Let's trap the form submission itself and automatically convert all the <textarea> elements.

Textile converted to XHTML

Figure 4-9. Textile converted to XHTML

This is trickier than it sounds. There are two ways to submit a web form: the user can click on an <input type="submit"> button, or the page can programmatically call the form.submit() method. When the user clicks a Submit button, Firefox fires an onsubmit event, which we can trap and insert our Textile conversion function before the browser submits the form data to the server. But if a script calls the form's submit method, Firefox never fires the onsubmit event. To trap form submission, in both cases, we need to actually override the submit method in the HTMLFormElement class.

Save the following user script as autotextile.user.js:

		   // @name			Auto-Textile
		   // @namespace	http://philwilson.org/
		   // @description	Allow Textile input in web forms
		   // @include		http://www.example.com/
		   // ==/UserScript==

		   // Dear reader: I have omitted the textile() function here
		   // to save trees. Go hug a nearby tree, and then copy the
		   // textile() function from the textile.user.js script.
		   
		   function textile_and_submit(event) {
			   var form = event ? event.target : this;
			   
			   var arTextareas = form.getElementsByTagName('textarea');
			   for (var i = arTextareas.length - 1; i >= 0; i--) {
				   var elmTextarea = arTextareas[i];
				   elmTextarea.value = textile(elmTextarea.value);
			   }
			
			   form._submit();
			}
			
			// trap onsubmit event, for when user clicks an <input type="submit">
			window.addEventListener('submit', textile_and_submit, true);
			// override submit method, for when page script calls form.submit()
			HTMLFormElement.prototype._submit = HTMLFormElement.prototype.submit;
			HTMLFormElement.prototype.submit = textile_and_submit;

With these changes, any web form is automatically and transparently Textile-enabled. No extra buttons, no extra clicks. Of course, this breaks a large number of sites that weren't expecting XHTML markup, so running this script on every site would cause lots of virtual pain and suffering. The default @include parameter lists only an example site. You should add specific sites that expect XHTML comments.

Get Greasemonkey Hacks now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.