1<?php
2/// @cond ALL
3
4/**
5  * Collection of templates and templating utilities
6  */
7class LuminousHTMLTemplates {
8
9    // NOTE Don't worry about whitespace in the templates - it gets stripped from the innerHTML,
10    // so the <pre>s aren't affected. Make it readable :)
11
12    /// Normal container
13    const container_template = '
14      <div
15        class="luminous"
16        data-language="{language}"
17        style="{height_css}"
18      >
19        {subelement}
20      </div>';
21
22    /// Inline code container
23    const inline_template = '
24        <div
25          class="luminous inline"
26          data-language="{language}"
27        >
28          {subelement}
29        </div>';
30
31    /// line number-less
32    const numberless_template = '
33      <pre
34        class="code"
35      >
36        {code}
37      </pre>';
38
39    /// line numbered
40    // NOTE: there's a good reason we use tables here and that's because
41    // nothing else works reliably.
42    const numbered_template = '
43      <table>
44        <tbody>
45          <tr>
46            <td>
47                <pre class="line-numbers">
48                  {line_numbers}
49                </pre>
50            </td>
51
52            <td class="code-container">
53              <pre class="code numbered"
54                data-startline="{start_line}"
55                data-highlightlines="{highlight_lines}"
56              >
57                {code}
58              </pre>
59            </td>
60          </tr>
61        </tbody>
62      </table>';
63
64
65
66
67    private static function _strip_template_whitespace_cb($matches) {
68        return ($matches[0][0] === '<')? $matches[0] : '';
69    }
70    private static function _strip_template_whitespace($string) {
71        return preg_replace_callback('/\s+|<[^>]++>/',
72          array('self', '_strip_template_whitespace_cb'),
73          $string);
74    }
75
76    /**
77      * Formats a string with a given set of values
78      * The format syntax uses {xyz} as a placeholder, which will be
79      * substituted from the 'xyz' key from $variables
80      *
81      * @param $template The template string
82      * @param $variables An associative (keyed) array of values to be substituted
83      * @param $strip_whitespace_from_template If @c TRUE, the template's whitespace is removed.
84      *   This allows templates to be written to be easeier to read, without having to worry about
85      *   the pre element inherting any unintended whitespace
86      */
87    public static function format($template, $variables, $strip_whitespace_from_template = true) {
88
89        if ($strip_whitespace_from_template) {
90            $template = self::_strip_template_whitespace($template);
91        }
92
93        foreach($variables as $search => $replace) {
94            $template = str_replace("{" . $search . "}", $replace, $template);
95        }
96        return $template;
97    }
98}
99
100class LuminousFormatterHTML extends LuminousFormatter {
101
102  // overridden by inline formatter
103  protected $inline = false;
104  public $height = 0;
105  /**
106   * strict HTML standards: the target attribute won't be used in links
107   * \since  0.5.7
108   */
109  public $strict_standards = false;
110
111  private function height_css() {
112    $height = trim('' . $this->height);
113    $css = '';
114    if (!empty($height) && (int)$height > 0) {
115      // look for units, use px is there are none
116      if (!preg_match('/\D$/', $height)) $height .= 'px';
117      $css = "max-height: {$height};";
118    }
119    else
120      $css = '';
121    return $css;
122   }
123
124  private static function template_cb($matches) {
125
126  }
127
128  // strips out unnecessary whitespace from a template
129  private static function template($t, $vars=array()) {
130    $t = preg_replace_callback('/\s+|<[^>]++>/',
131      array('self', 'template_cb'),
132      $t);
133    array_unshift($vars, $t);
134    $code = call_user_func_array('sprintf', $vars);
135    return $code;
136  }
137
138  private function lines_numberless($src) {
139    $lines = array();
140    $lines_original = explode("\n", $src);
141    foreach($lines_original as $line) {
142      $l = $line;
143      $num = $this->wrap_line($l, $this->wrap_length);
144      // strip the newline if we're going to join it. Seems the easiest way to
145      // fix http://code.google.com/p/luminous/issues/detail?id=10
146      $l = substr($l, 0, -1);
147      $lines[] = $l;
148    }
149    $lines = implode("\n", $lines);
150    return $lines;
151  }
152
153  private function format_numberless($src) {
154    return LuminousHTMLTemplates::format(
155      LuminousHTMLTemplates::numberless_template,
156      array(
157        'height_css' => $this->height_css(),
158        'code' => $this->lines_numberless($src)
159      )
160    );
161  }
162
163
164  public function format($src) {
165
166    $line_numbers = false;
167
168    if ($this->link)  $src = $this->linkify($src);
169
170    $code_block = null;
171    if ($this->line_numbers) {
172        $code_block = $this->format_numbered($src);
173    }
174    else {
175        $code_block = $this->format_numberless($src);
176    }
177
178    // convert </ABC> to </span>
179    $code_block = preg_replace('/(?<=<\/)[A-Z_0-9]+(?=>)/S', 'span',
180      $code_block);
181    // convert <ABC> to <span class=ABC>
182    $cb = create_function('$matches',
183                          '$m1 = strtolower($matches[1]);
184                          return "<span class=\'" . $m1 . "\'>";
185                          ');
186    $code_block = preg_replace_callback('/<([A-Z_0-9]+)>/', $cb, $code_block);
187
188    $format_data = array(
189      'language' => ($this->language === null)? '' : htmlentities($this->language),
190      'subelement' => $code_block,
191      'height_css' => $this->height_css()
192    );
193    return LuminousHTMLTemplates::format(
194      $this->inline? LuminousHTMLTemplates::inline_template :
195        LuminousHTMLTemplates::container_template,
196      $format_data
197    );
198  }
199
200  /**
201   * Detects and links URLs - callback
202   */
203  protected function linkify_cb($matches) {
204    $uri = (isset($matches[1]) && strlen(trim($matches[1])))? $matches[0]
205      : "http://" . $matches[0];
206
207    // we dont want to link if it would cause malformed HTML
208    $open_tags = array();
209    $close_tags = array();
210    preg_match_all("/<(?!\/)([^\s>]*).*?>/", $matches[0], $open_tags,
211      PREG_SET_ORDER);
212    preg_match_all("/<\/([^\s>]*).*?>/", $matches[0], $close_tags,
213      PREG_SET_ORDER);
214
215    if (count($open_tags) != count($close_tags))
216      return $matches[0];
217    if (isset($open_tags[0])
218      && trim($open_tags[0][1]) !== trim($close_tags[0][1])
219    )
220      return $matches[0];
221
222    $uri = strip_tags($uri);
223
224    $target = ($this->strict_standards)? '' : ' target="_blank"';
225    return "<a href='{$uri}' class='link'{$target}>{$matches[0]}</a>";
226  }
227
228  /**
229   * Detects and links URLs
230   */
231  protected function linkify($src) {
232    if (stripos($src, "http") === false && stripos($src, "www") === false)
233        return $src;
234
235    $chars = "0-9a-zA-Z\$\-_\.+!\*,%";
236    $src_ = $src;
237    // everyone stand back, I know regular expressions
238    $src = preg_replace_callback(
239      "@(?<![\w])
240      (?:(https?://(?:www[0-9]*\.)?) | (?:www\d*\.)   )
241
242      # domain and tld
243      (?:[$chars]+)+\.[$chars]{2,}
244      # we don't include tags at the EOL because these are likely to be
245      # line-enclosing tags.
246      (?:[/$chars/?=\#;]+|&amp;|<[^>]+>(?!$))*
247      @xm",
248      array($this, 'linkify_cb'), $src);
249    // this can hit a backtracking limit, in which case it nulls our string
250    // FIXME: see if we can make the above regex more resiliant wrt
251    // backtracking
252    if (preg_last_error() !== PREG_NO_ERROR) {
253      $src = $src_;
254    }
255    return $src;
256  }
257
258
259  private function format_numbered($src) {
260
261    $lines = '<span>' .
262      str_replace("\n", "\n</span><span>", $src, $num_replacements) .
263      "\n</span>";
264    $num_lines = $num_replacements + 1;
265
266    $line_numbers = '<span>' . implode('</span><span>',
267      range($this->start_line, $this->start_line + $num_lines - 1, 1)
268    ) . '</span>';
269
270
271    $format_data = array(
272      'line_number_digits' => strlen( (string)($this->start_line) + $num_lines ), // max number of digits in the line - this is used by the CSS
273      'start_line' => $this->start_line,
274      'height_css' => $this->height_css(),
275      'highlight_lines' => implode(',', $this->highlight_lines),
276      'code' => $lines,
277      'line_numbers' => $line_numbers
278    );
279
280    return LuminousHTMLTemplates::format(
281      LuminousHTMLTemplates::numbered_template,
282      $format_data
283    );
284
285  }
286}
287
288
289class LuminousFormatterHTMLInline extends LuminousFormatterHTML {
290
291  public function format($src) {
292    $this->line_numbers = false;
293    $this->height = 0;
294    $this->inline = true;
295    return parent::format($src);
296  }
297
298}
299
300
301class LuminousFormatterHTMLFullPage extends LuminousFormatterHTML {
302  protected $theme_css = null;
303  protected $css = null;
304  public function set_theme($css) {
305    $this->theme_css = $css;
306  }
307  protected function get_layout() {
308    // this path info shouldn't really be here
309    $path = luminous::root() . '/style/luminous.css';
310    $this->css = file_get_contents($path);
311  }
312  public function format($src) {
313    $this->height = 0;
314    $this->get_layout();
315    $fmted = parent::format($src);
316    return <<<EOF
317<!DOCTYPE html>
318<html>
319  <head>
320    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
321    <title></title>
322    <style type='text/css'>
323    body {
324      margin: 0;
325    }
326    /* luminous.css */
327    {$this->css}
328    /* End luminous.css */
329    /* Theme CSS */
330    {$this->theme_css}
331    /* End theme CSS */
332    </style>
333  </head>
334  <body>
335    <!-- Begin luminous code //-->
336    $fmted
337    <!-- End Luminous code //-->
338  </body>
339</html>
340
341EOF;
342  }
343}
344/// @endcond
345