Tuesday, January 1, 2013

Automatic Preview

Introduction

This is a fun little post. I created a client-side preview box which automatically updates as the user types. It supports Alex Gorbatchev's Syntax Highlighter and MathJax.

Small Demo

Here's a small demo of how the code works. You can enter in basically raw HTML. I have a small MathJax example pre-loaded.














Enter Text Here

Preview Area

\[ a^2 + b^2 = c^2 \]

What's going on

The code listens to the text area for update event triggers (in this case, onkeydown, onchange, and onkeypress). The update listener sets a timer which will trigger the actual update of the preview area. Everytime the listener recieves an event it resets the timer. This prevents too many updates from happening as the user is typing.

The actual update code basically copies the text from the box and pastes it into the preview element. This means that this code as-is will run and process the raw HTML. Since in my example above the entered text can't be uploaded to a server or another user's client, there shouldn't be any significant security exploit.

However, if you're using HTML input on your site it's important to process the HTML to prevent cross-site scripting (xss). This is by far the most common security exploit as of recent, and should be properly handled. An example implementation is the use of HTML in comments on this page. Blogger uses a very restrictive whitelist to limit what HTML is allowed, effectively preventing a comment from being an exploit for cross site scripting.

The Code

This is the main Updater class. You provide it with the datasource, which is some kind of text input source where the user can type, and a buffer, which is the output preview area. There is also a delay parameter (in milliseconds) which determines how long the script should wait before performing the update.

function path()
{
  var args = arguments,
      result = [];
       
  for(var i = 0; i < args.length; i++)
      result.push(args[i].replace('@', 'http://alexgorbatchev.com/pub/sh/current/scripts/'));
       
  return result;
};

function Updater (datasource, buffer1, delay)
{
 this.timeout_id = null;
 this.datasource = datasource;
 this.buffer1 = buffer1;
 this.delay = delay;
 this.old_text = datasource.value;

 this.langs = path(
        'applescript            @shBrushAppleScript.js',
        'actionscript3 as3      @shBrushAS3.js',
        'bash shell             @shBrushBash.js',
        'coldfusion cf          @shBrushColdFusion.js',
        'cpp c                  @shBrushCpp.js',
        'c# c-sharp csharp      @shBrushCSharp.js',
        'css                    @shBrushCss.js',
        'delphi pascal          @shBrushDelphi.js',
        'diff patch pas         @shBrushDiff.js',
        'erl erlang             @shBrushErlang.js',
        'groovy                 @shBrushGroovy.js',
        'java                   @shBrushJava.js',
        'jfx javafx             @shBrushJavaFX.js',
        'js jscript javascript  @shBrushJScript.js',
        'perl pl                @shBrushPerl.js',
        'php                    @shBrushPhp.js',
        'text plain             @shBrushPlain.js',
        'py python              @shBrushPython.js',
        'ruby rails ror rb      @shBrushRuby.js',
        'sass scss              @shBrushSass.js',
        'scala                  @shBrushScala.js',
        'sql mysql                    @shBrushSql.js',
        'vb vbnet               @shBrushVb.js',
        'xml xhtml xslt html    @shBrushXml.js'
      );
 langs.push('matlabkey http://www.undermyhat.org/blog/wp-content/uploads/2009/09/shBrushMatlabSimple.js');
 
 this._do_highlight();
 
 var _this = this;
 var temp = function() {_this.update();};
 datasource.onchange = temp;
 datasource.onkeydown = temp;
 datasource.onkeypress = temp;
}

Updater.prototype.update = function()
{
 if(this.timeout_id !== null)
 {
  window.clearTimeout(this.timeout_id);
 }
 // get around quirks with setTimeout and this
 var _this = this; 
 this.timeout_id = window.setTimeout(function() {_this._update_callback();}, this.delay);
};

Updater.prototype._update_callback = function()
{
 var text = this.datasource.value;
 if(text === this.old_text)
 {
  // don't need to update if text didn't change
  return;
 }
 this.old_text = text;
 this.timeout_id = null;
 // basic parsing to remove script and link tags
 this.buffer1.innerHTML = text;
 // reset numbering
 window.MathJax.Hub.Queue(function () {
        if (MathJax.InputJax.TeX.resetEquationNumbers) {
            MathJax.InputJax.TeX.resetEquationNumbers();
          }
        },["Typeset", window.MathJax.Hub], ["_do_highlight", this]);
};

Updater.prototype._do_highlight = function ()
{
 SyntaxHighlighter.autoloader.apply(null, this.langs);
 
 SyntaxHighlighter.all();
};

Using the Updater:

<script type="text/javascript">
var updater;

// This uses JQuery, but you don't need to, just set to the window.onready event
$(document).ready(function()
{
 updater = new Updater(document.getElementById("text"), document.getElementById("prev1"), 500);
 // delete all items under brushes
 // terrible hack, but works
 var node = document.getElementById("brushes");
 node.parentNode.removeChild(node);
});
</script>

<!-- autoload all langs -->
<div id="brushes">
<pre class="brush: bash"></pre>
<pre class="brush: cpp"></pre>
<pre class="brush: csharp"></pre>
<pre class="brush: css"></pre>
<pre class="brush: diff"></pre>
<pre class="brush: java"></pre>
<pre class="brush: js"></pre>
<pre class="brush: php"></pre>
<pre class="brush: text"></pre>
<pre class="brush: py"></pre>
<pre class="brush: sql"></pre>
<pre class="brush: xml"></pre>
<pre class="brush: matlabkey"></pre>
</div>
<h2 class="h2">Enter Text Here</h2>
<textarea id="text" class="edit_area">\[
a^2 + b^2 = c^2
\]</textarea>
<h2 class="h2">Preview Area</h2>
<div id="prev1" style="border: solid 1px;">\[
a^2 + b^2 = c^2
\]</div>

For the most part using this is fairly simply. However, I did encounter some issues with Alex Gorbatchev's Syntax Highlighter. For some reason it wouldn't load any new brushes after you perform the highlighting. The only way I could get around this was to pre-load all the brushes I intend to support.

Conclusion

That's it for now, like I said above this code basically allows raw HTML input. Like I said above, if you're planning on allowing raw html input you should have adequate safe-guards in place to prevent cross site scripting attacks. It's also not too much of a stretch to use AJAX and have the server update the preview window. There are also probably better ways to filter the input than how I did it.

No comments :

Post a Comment