Full highlighting functionality with symfony, Geshi and TinyMCE
Symfony | Technical | Development | Doctrine | September 7, 2010
Most of the time when displaying programming code in a web page is better to see it highlighted. It is better for the visitors who try to read the code, to see it highlighted like in an IDE. Well, in this post, a full highlighting functionality will be described step by step with symfony, GeSHI and TinyMCE, based on a previous post.
Schema
First we will focus on the schema for the database.
GeSHI will parse code like:
[php] echo 'symfony, GeSHI and TinyMCE'; [/php]
And will produce code like:
<p><span class="kw1">echo</span><span class="st_h"> 'symfony, GeSHI and TinyMCE'</span>;</p>
This parsing is quite intensive, then we can not do it everytime a post needs to be shown. Then we must have a cache table for our posts, where to save the parsed code. This way we will have two tables: post and post_index
Post:
tableName: post
actAs:
Sluggable: { fields: [ title ] }
Timestampable:
columns:
id: { type: integer , length: 20 , primary: true , autoincrement: true }
title: { type: string , length: 200 , notnull: true }
content: { type: string , length: 20000 , notnull: true }
excerpt: { type: string , length: 500 , notnull: true }
indexes:
u_title: { fields: [ title ] , type: unique }
i_content: { fields: { content: { length: 400 } } }
i_excerpt: { fields: { excerpt: { length: 100 } } }
relations:
PostIndex: { class: PostIndex , local: id , foreign: post_id , type: one , alias: PostIndex }
PostIndex:
tableName: post_index
columns:
id: { type: integer , length: 20 , primary: true , autoincrement: true }
post_id: { type: integer , length: 20 , notnull: true }
title: { type: string , length: 200 , notnull: true }
content: { type: string , length: 20000 , notnull: true }
indexes:
u_title: { fields: [ title ] , type: unique }
i_content: { fields: { content: { length: 400 } } }
relations:
Post: { class: Post , local: post_id , foreign: id , type: one , alias: Post , onDelete: CASCADE , onUpdate: CASCADE }
Form
To properly insert and update data to our new brand post table we need to customize a PostForm class:
class PostForm extends BasePostForm { public function configure() { $this->setWidgets(array ( 'id' => new sfWidgetFormInputHidden(), 'title' => new sfWidgetFormInputText(array(), array('size' => 60)), 'content' => new sfWidgetFormTextareaTinyMCE(array ( 'width' => 550, 'height' => 350, 'config' => 'theme_advanced_disable: "anchor,cleanup,help"', )), } protected function updateContentColumn($content) { $index_content = preg_replace_callback('#< pre>\[(\w+)\](.*?)\[/\\1\]< /pre>#s', array($this, 'processCode'), $content); $this->object->getPostIndex()->setContent($index_content); return $content; } protected function processCode($matches) { $code = str_replace('& nbsp;', ' ', $matches[2]); $code = html_entity_decode($code); $geshi = new GeSHi($code, $matches[1]); $geshi->set_header_type(GESHI_HEADER_PRE); $geshi->enable_classes(); return $geshi->parse_code(); } }
In this class we configure the post form to show the title and content widgets. In the content widget it is been used the great sfWidgetFormTextareaTinyMCE class from the sfFormExtraPlugin.
In the updateContentColumn method it is applied the processCode method for each coincidence of
< pre>[(text)] ..... [/(text)]< /pre>
, where text is the type of desired code to be highlighted, for example it could be: php, java, yaml, css, javascript, etc. For further explanation about that part see the previous post about highlighting code with symfony and Geshi.
Also note that the parsed code is assigned to the content of the PostIndex object:
$this->object->getPostIndex()->setContent($index_content);
TinyMCE
Now it is the time to talk about one big issue with this editor when trying to paste code.
The heart of the problem lies in the fact that each time some code is pasted in TinyMCE, automatically it is added html formatting, that is tags with a lot of &nbps; and all weird characters like -> are escape.
The problem with this is GeSHI needs clean code, like the one produced when copying to a simple textarea, but not the TinyMCE formatted one.
The solution is the PreElementFix Plugin for TinyMCE. The instructions for the installation are in the previous link. The great thing about this plugin is that it does not allow the addition of tags when pasting some code. The bad things is that there is still escape sequences. Well this is not a bad thing at all, it has to be this way for the code to be HTML compliant.
To configure the widget you can use the updated render method in the sfWidgetFormTextareaTinyMCE class:
public function render($name, $value = null, $attributes = array(), $errors = array()) { $textarea = parent::render($name, $value, $attributes, $errors); $js = sprintf(<<< script type="text/javascript"> tinyMCE.init({ mode: "exact", elements: "%s", theme: "%s", plugins : "paste,preelementfix", %s %s theme_advanced_toolbar_location: "top", theme_advanced_toolbar_align: "left", theme_advanced_statusbar_location: "bottom", theme_advanced_resizing: true, paste_auto_cleanup_on_paste : true %s }); EOF return $textarea.$js; }
And now, the solution to the escape sequences is to return them to their previous state. We can do this with this two lines of code in the form class:
$code = str_replace('& nbsp;', ' ', $matches[2]); $code = html_entity_decode($code);
You can think, why to use the first line when at first sight html_entity_decode will do the job. However this function has some problems with &nbps; check that in the html_entity_decode php info.
After following all of these steps you will have an TinyMCE like:

Finally, it is shown that with the use of a combination of tools like symfony, GeSHI and TinyMCE we can get clean and highlighted code like the one you saw in this post.
PS: An exercise for the reader is to discover why there are tiny on purpose errors on the code above, for example with &nbps; being & nbsp; .