You are here

codefilter.module in Code Filter 7

Same filename and directory in other branches
  1. 8 codefilter.module
  2. 5 codefilter.module
  3. 6 codefilter.module

Text filter for highlighting PHP source code.

File

codefilter.module
View source
<?php

/**
 * @file
 * Text filter for highlighting PHP source code.
 */

/**
 * Processes chunks of escaped PHP code into HTML.
 */
function codefilter_process_php($text) {

  // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
  // Undo possible linebreak filter conversion.
  $text = preg_replace('@</?(br|p)\\s*/?>@', '', str_replace('\\"', '"', $text));

  // Undo the escaping in the prepare step.
  $text = decode_entities($text);

  // Trim leading and trailing linebreaks.
  $text = trim($text, "\r\n");

  // Highlight as PHP.
  $text = '<div class="codeblock"><pre>' . highlight_string("<?php\n{$text}\n?>", 1) . '</pre></div>';

  // Remove newlines to avoid clashing with the linebreak filter.
  $text = str_replace("\n", '', $text);
  return codefilter_fix_spaces($text);
}

/**
 * Callback to replace content of the <?php ?> elements.
 */
function _codefilter_process_php_callback($matches) {
  return codefilter_process_php($matches[1]);
}

/**
 * Helper function for codefilter_process_code().
 */
function codefilter_process_php_inline($matches) {

  // Undo nl2br.
  $text = str_replace('<br />', '', $matches[0]);

  // Decode entities (the highlighter re-entifies) and highlight text.
  $text = highlight_string(decode_entities($text), 1);

  // Remove PHPs own added code tags.
  $text = str_replace(array(
    '<code>',
    '</code>',
    "\n",
  ), array(
    '',
    '',
    '',
  ), $text);
  return $text;
}

/**
 * Processes chunks of escaped code into HTML.
 */
function codefilter_process_code($text, $attributes = NULL) {

  // Undo linebreak escaping.
  $text = str_replace('&#10;', "\n", $text);

  // Inline or block level piece?
  $multiline = strpos($text, "\n") !== FALSE;

  // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
  $text = preg_replace("/^\n/", '', preg_replace('@</?(br|p)\\s*/?>@', '', str_replace('\\"', '"', $text)));

  // Trim leading and trailing linebreaks.
  $text = trim($text, "\n");

  // Escape newlines.
  $text = nl2br($text);

  // PHP code in regular code.
  $text = preg_replace_callback('/&lt;\\?php.+?\\?&gt;/s', 'codefilter_process_php_inline', $text);
  $text = '<code' . $attributes . '>' . codefilter_fix_spaces(str_replace(' ', '&nbsp;', $text)) . '</code>';
  $text = $multiline ? '<div class="codeblock">' . $text . '</div>' : $text;

  // Remove newlines to avoid clashing with the linebreak filter.
  return str_replace("\n", '', $text);
}

/**
 * Callback to replace content of the <code> elements.
 *
 * @param array $matches
 *   An array of matches passed by preg_replace_callback().
 *
 * @return string
 *   A formatted string.
 */
function _codefilter_process_code_callback($matches) {
  return codefilter_process_code($matches[2], $matches[1]);
}

/**
 * Replace html space elements with literal space characters.
 *
 * @param string $text
 *   A string to fix spaces for.
 *
 * @return string
 *   A formatted string.
 */
function codefilter_fix_spaces($text) {
  $text = preg_replace('@&nbsp;(?!&nbsp;)@', ' ', $text);

  // A single space before text is ignored by browsers. If a single space
  // follows a break tag, replace it with a non-breaking space.
  $text = preg_replace('@<br /> ([^ ])@', '<br />&nbsp;$1', $text);
  return $text;
}

/**
 * Escape code blocks during input filter 'prepare'.
 *
 * @param string $text
 *   The string to escape.
 * @param string $type
 *   The type of code block, either 'code' or 'php'.
 * @param string|null $attributes
 *   (optional) The HTML attributes of the code block.
 *
 * @return string
 *   The escaped string.
 */
function codefilter_escape($text, $type = 'code', $attributes = NULL) {

  // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
  $text = check_plain(str_replace('\\"', '"', $text));

  // Protect newlines from line break converter.
  $text = str_replace(array(
    "\r",
    "\n",
  ), array(
    '',
    '&#10;',
  ), $text);

  // Prepare the class HTML attribute for the codefilter tag.
  if (isset($attributes)) {
    $attributes = ' ' . $attributes;
  }

  // Add codefilter escape tags.
  $text = "[codefilter_{$type}{$attributes}]{$text}[/codefilter_{$type}]";
  return $text;
}

/**
 * Callback to escape content of <code> elements.
 */
function _codefilter_escape_code_tag_callback($matches) {

  // Extract a potentially existing class HTML attribute.
  $attributes = NULL;
  if (!empty($matches[1]) && preg_match('@class="[^"]+"@', $matches[1], $class_matches)) {
    $attributes = $class_matches[0];
  }
  return codefilter_escape($matches[2], 'code', $attributes);
}

/**
 * Callback to escape content of <?php ?>, [?php ?], <% %>, and [% %] elements.
 */
function _codefilter_escape_php_tag_callback($matches) {
  return codefilter_escape($matches[2], 'php');
}

/**
 * Implements hook_filter_info().
 */
function codefilter_filter_info() {
  $filters['codefilter'] = array(
    'title' => t('Code filter'),
    'description' => t('Allows users to post code verbatim using &lt;code&gt; and &lt;?php ?&gt; tags.'),
    'prepare callback' => '_codefilter_prepare',
    'process callback' => '_codefilter_process',
    'settings callback' => '_codefilter_settings',
    'default settings' => array(
      'nowrap_expand' => FALSE,
      'codefilter_prism' => FALSE,
    ),
    'tips callback' => '_codefilter_tips',
  );
  return $filters;
}

/**
 * Implements hook_filter_FILTER_prepare().
 *
 * Escape code tags to prevent other filters from acting on them.
 * <code> </code>, <?php ?>, [?php ?], <% %>, and [% %] tags are escaped.
 */
function _codefilter_prepare($text, $format) {
  $callback_prefix = $format->settings['codefilter_prism'] ? '_codefilter_prism' : '_codefilter';
  $text = preg_replace_callback('@<code([^>]*)>(.+?)</code>@s', "{$callback_prefix}_escape_code_tag_callback", $text);
  $text = preg_replace_callback('@[\\[<](\\?php)(.+?)(\\?)[\\]>]@s', "{$callback_prefix}_escape_php_tag_callback", $text);
  return $text;
}

/**
 * Implements hook_filter_FILTER_process().
 */
function _codefilter_process($text, $format) {
  if ($format->settings['codefilter_prism'] && function_exists('_codefilter_prism_process_code_callback')) {
    $text = preg_replace_callback('@\\[codefilter_prism_code( [^\\]]+)?\\](.+?)\\[/codefilter_prism_code\\]@s', '_codefilter_prism_process_code_callback', $text);
    $text = preg_replace_callback('@\\[codefilter_prism_php\\](.+?)\\[/codefilter_prism_php\\]@s', '_codefilter_prism_process_php_callback', $text);
  }
  else {
    $text = preg_replace_callback('@\\[codefilter_code( [^\\]]+)?\\](.+?)\\[/codefilter_code\\]@s', '_codefilter_process_code_callback', $text);
    $text = preg_replace_callback('@\\[codefilter_php\\](.+?)\\[/codefilter_php\\]@s', '_codefilter_process_php_callback', $text);
  }

  // A hack, so we can conditionally nowrap based on filter settings.
  // @todo Refactor how replacements are done so we can do this more cleanly.
  if ($format->settings['nowrap_expand']) {
    $text = str_replace('class="codeblock"', 'class="codeblock nowrap-expand"', $text);
  }
  return $text;
}

/**
 * Implements hook_filter_FILTER_tips().
 */
function _codefilter_tips($format, $long = FALSE) {
  if ($long) {
    return t('To post pieces of code, surround them with &lt;code&gt;...&lt;/code&gt; tags. For PHP code, you can use &lt;?php ... ?&gt;, which will also colour it based on syntax.');
  }
  else {
    return t('You may post code using &lt;code&gt;...&lt;/code&gt; (generic) or &lt;?php ... ?&gt; (highlighted PHP) tags.');
  }
}

/**
 * Settings callback for the codefilter filter.
 *
 * @see hook_filter_info()
 * @see hook_filter_FILTER_settings()
 */
function _codefilter_settings($form, &$form_state, $filter, $format, $defaults) {
  $filter->settings += $defaults;
  $elements = array();
  $elements['nowrap_expand'] = array(
    '#type' => 'checkbox',
    '#title' => t('Expand code boxes on hover.'),
    '#description' => t('By default, code boxes inherit text wrapping from the active theme. With this setting, code boxes will not wrap, but will expand to full width on hover (with Javascript).'),
    '#default_value' => $filter->settings['nowrap_expand'],
  );
  if (module_exists('codefilter_prism')) {
    $elements['codefilter_prism'] = array(
      '#type' => 'checkbox',
      '#title' => t('Prism Library Support'),
      '#description' => t('Enable code styling using the <a href="http://prismjs.com/">Prism library</a> (requires Javascript). To enable highlighting, download the prism.js and prism.js and add them to your site\'s libraries folder.'),
      '#default_value' => $filter->settings['codefilter_prism'],
    );
  }
  return $elements;
}

Functions

Namesort descending Description
codefilter_escape Escape code blocks during input filter 'prepare'.
codefilter_filter_info Implements hook_filter_info().
codefilter_fix_spaces Replace html space elements with literal space characters.
codefilter_process_code Processes chunks of escaped code into HTML.
codefilter_process_php Processes chunks of escaped PHP code into HTML.
codefilter_process_php_inline Helper function for codefilter_process_code().
_codefilter_escape_code_tag_callback Callback to escape content of <code> elements.
_codefilter_escape_php_tag_callback Callback to escape content of <?php ?>, [?php ?], <% %>, and [% %] elements.
_codefilter_prepare Implements hook_filter_FILTER_prepare().
_codefilter_process Implements hook_filter_FILTER_process().
_codefilter_process_code_callback Callback to replace content of the <code> elements.
_codefilter_process_php_callback Callback to replace content of the <?php ?> elements.
_codefilter_settings Settings callback for the codefilter filter.
_codefilter_tips Implements hook_filter_FILTER_tips().