UBB with highlighting

So I wrote this UBB class

See the code below, and the changelog.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<?php
/*
 * Class for printing code in a HTML formatted block, complete with line numbers.
 * It is also suited to be included in other UBB-like functionality.
 */
class CodeBlock
{
    protected $options;
    protected $output;

    public function __construct($text$options=array()) {
        // Set options; first overwrites second (the defaults).
        $this->options $options self::getDefaultOptions();

        // Do we still need to strip tags?
        if (!$this->options['tags_stripped'] && ini_get('magic_quotes_gpc')) {
            $returnText stripslashes($text);
        } else {
            $returnText $text;
        }

        // Convert any tabs to spaces.
        $returnText str_replace("\t"str_repeat(' '$this->options['tabspaces']), $returnText);

        // Convert all linebreaks to "\n".
        $returnText str_replace("\r\n""\n"$returnText);

        // Remove all whitespace characters from the end of each line, this will also trim empty lines.
        $lines      explode("\n"$returnText);
        $returnText implode("\n"array_map('rtrim'$lines));

        // Do we want to apply PHP code highlighting?
        if ($this->options['highlight']) {
            $returnText = @highlight_string($returnTexttrue); // suppress possible errors

            // Clean up some of the HTML generated by highlight_string():
            // Replace all <br /> tags with "\n" and fix the start and end of the returned HTML.
            $returnText str_replace(
                array('<br />''<code><span style="color: #000000">'."\n"'</span>'."\n".'</code>'),
                array("\n",     '<span style="color: #000000">',            '</span>'),
                $returnText
            );
        } else {
            // Just neutralize everything.
            $returnText htmlspecialchars($returnTextENT_QUOTES'UTF-8');
        }

        // Create the code block, starting with linenumbers.
        $nrs implode("\n"range(1count($lines))); // seems faster than a for-loop, alternatively: use <br /> instead of \n for implosion
        ob_start();
            ?><div class="code-wrapper">
                <div class="index"><pre><?php echo $nrs ?></pre></div>
                <div class="code"><pre><?php echo $returnText ?></pre></div>
            </div><?php
        $this->output ob_get_clean();
    } // __construct

    public static function getDefaultOptions() {
        return array(
            'tabspaces'     => 4,       // number of spaces that tab-characters are replaced with
            'highlight'     => true,    // whether to use the PHP function highlight_string() for highlighting code
            // The next default should not be changed here unless you use the CodeBlock class directly,
            // or when magic_quotes_gpc is on but you already stripped these extra slashes before you
            // passed the input to this class.
            'tags_stripped' => false,   // keeps track of whether we already stripped tags for when magic_quotes_gpc is on
        );
    } // getDefaultOptions

    public function getOutput() {
        return $this->output;
    } // getOutput
// class

/*
 * Class for formatting output, codeblocks can be picked up as well.
 */
class Ubb
{
    protected $options;
    protected $output;
    protected $codeBlocks;  // stack (array) of CodeBlock objects
    protected $tag;         // shorthand for $this->options['code_block_tag'], used for delimiting blocks of code

    public function __construct($text$options=array()) {
        $this->codeBlocks = array();

        $this->options $options + array(
            'allow_html'            => false,   // allow HTML outside code blocks? be VERY careful with this
            'use_code_block'        => true,    // use CodeBlock class for formatting blocks of code?
            'code_block_tag'        => 'code',  // name of the UBB-tag for delimiting blocks of code, it will also enable us to print this code using this class :)
            'code_block_options'    => array(), // styling options for code blocks, see CodeBlock::getDefaultOptions()
        );
        $this->options['code_block_options'] = $this->options['code_block_options'] + CodeBlock::getDefaultOptions();

        $this->tag $this->options['code_block_tag'];

        // Strip slashes if magic_quotes_gpc is on.
        if (ini_get('magic_quotes_gpc')) {
            $returnText stripslashes($text);
        } else {
            $returnText $text;
        }
        // We stripped tags; do not strip them again in CodeBlock calls.
        $this->options['code_block_options']['tags_stripped'] = true;

        if ($this->options['use_code_block']) {
            // Backup code blocks, it needs separate treatment from the rest of the UBB-code).
            // preg_replace() will become deprecated in PHP 5.5.0, we use preg_replace_callback instead.
            $returnText preg_replace_callback(
                '#\['.$this->tag.'](.*)\[/'.$this->tag.']#sU',
                array($this'backupCodeBlock'),
                $returnText
            );
        }

        // Perform the rest of the UBB formatting, starting with (dis)allowing HTML.
        if ($this->options['allow_html'] == false) {
            // No HTML allowed, neutralize all HTML and convert newline characters to linebreak-tags.
            $returnText nl2br(htmlspecialchars($returnTextENT_QUOTES'UTF-8'));

            if ($this->options['use_code_block']) {
                // Some extra styling: remove the <br /> after the closing tag of a code block.
                $returnText str_replace('[/'.$this->tag.']<br />''[/'.$this->tag.']'$returnText);
            }
        } // allow_html false

        // Put the rest, if any, of your own UBB-styling here.

        if ($this->options['use_code_block']) {
            // Restore code blocks.
            $returnText preg_replace_callback(
                '#\['.$this->tag.'](\d+)\[/'.$this->tag.']#sU',
                array($this'restoreCodeBlock'),
                $returnText
            );
        }

        // Store the processed input in $this->output.
        $this->output $returnText;
    } // __construct

    // Callback function for storing blocks of code.
    protected function backupCodeBlock($matches) {
        $currentBlock count($this->codeBlocks);

        $this->codeBlocks[$currentBlock] = new CodeBlock(
            // Note: strip backslash from escaped double quotes due to /e-switch is no longer necessary
            //       because we do not use preg_match anymore.
            $matches[1],
            $this->options['code_block_options']
        );

        // Return a HTML-safe placeholder to indicate where the codeblock should be placed back after
        // processing the rest of the UBB-code.
        return '['.$this->tag.']'.$currentBlock.'[/'.$this->tag.']';
    } // backupCodeBlock

    // Callback function for restoring code blocks after the rest of the UBB-code is applied.
    protected function restoreCodeBlock($matches) {
        return $this->codeBlocks[$matches[1]]->getOutput();
    } // restoreCodeBlock

    public function getOutput() {
        return $this->output;
    }
} // class
?>

And the CSS:

1
2
3
4
5
6
7
@CHARSET "UTF-8";

div.code-wrapper            { display: block; margin: 0; padding: 0; border: 1px solid #cccccc; background-color: #eeeeee; position: relative; overflow-x: auto; overflow-y: hidden; }
div.code-wrapper pre        { font-family: monospace; font-size: 13px; margin: 0; padding: 2px 5px; border: 0; line-height: 15px; }
div.code-wrapper div        { margin: 0; padding: 0; border: 0; }
div.code-wrapper div.index  { display: block; width: 40px; text-align: right; background-color: #cccccc; color: #333333; }
div.code-wrapper div.code   { display: block; position: absolute; top: 0px; left: 40px; color: #000000; }

Simply include the PHP file and CSS file, and call your text-to-be-formatted-by-UBB like this:

1
2
3
4
5
6
7
<?php
// create an Ubb object
$ubb = new Ubb('your_text_with_codeblocks_here');

// print it!
echo $ubb->getOutput();
?>

Or, if you only want to print one codeblock, for when your entire input consists of code:

1
2
3
4
5
6
7
<?php
// create a CodeBlock object
$code = new CodeBlock('your_code_here');

// print it!
echo $code->getOutput();
?>

This article makes use of this code as well! It allows HTML outside codeblocks and was set up as follows:

1
2
3
4
5
6
<?php
// ...
$ubb = new Ubb('article_text_from_database', array('allow_html' => true));
// ...
echo $ubb->getOutput();
?>

If you do not use the UTF-8 character encoding, you might want to consider to start using that. Otherwise change the third parameter in the htmlspecialchars() calls to the (supported) character encoding you are using.

Changelog

2014-10-07

2014-09-16

2014-05-21