You are here

syntaxhighlighter.module in Syntax Highlighter 8

Syntax highlight code using the SyntaxHighlighter Javascript library.

See http://alexgorbatchev.com/SyntaxHighlighter for details.

@author: Matthew Young <www.hddigitalworks.com/contact>

File

syntaxhighlighter.module
View source
<?php

/**
 * @file
 * Syntax highlight code using the SyntaxHighlighter Javascript library.
 *
 * See http://alexgorbatchev.com/SyntaxHighlighter for details.
 *
 * @author: Matthew Young <www.hddigitalworks.com/contact>
 */
use Drupal\Core\Config\Config;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\filter\Entity\FilterFormat;

/**
 * Inject SyntaxHighlighter on every page except the listed pages.
 */
const SYNTAXHIGHLIGHTER_INJECT_EXCEPT_LISTED = 0;

/**
 * Inject SyntaxHighlighter on only the listed pages.
 */
const SYNTAXHIGHLIGHTER_INJECT_IF_LISTED = 1;

/**
 * Inject SyntaxHighlighter if the associated PHP code returns TRUE.
 */
const SYNTAXHIGHLIGHTER_INJECT_PHP = 2;
const SYNTAXHIGHLIGHTER_PHP_PERMISSION = 'use PHP for syntaxhighlighter js/css code inject control';

/**
 * Define a placeholder for the syntaxhighlighter tag.
 *
 * Use a completely nonsense word for replacement when filtering so there is
 * absolutely no chance this will conflict with something in the content text.
 */
const SYNTAXHIGHLIGHTER_TAG_STRING = '-_sYnTaXhIgHlIgHtEr_-';

/**
 * Implements hook_help().
 */
function syntaxhighlighter_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.syntaxhighlighter':
      return _syntaxhighlighter_filter_tips(0, 0, TRUE);
  }
}

/**
 * Implements hook_library_info_build().
 *
 * NOTE: the assets could have been defined in syntaxhighlighter.library.yml
 * as well using an absolute path, but in that case the library location would
 * have been to be hardcoded, e.g.:
 *
 *   core:
 *     js:
 *       /libraries/syntaxhighlighter/scripts/shCore.js: { minified: true }
 */
function syntaxhighlighter_library_info_build() {

  // Make the path absolute.
  //
  // This is needed because:
  // 1. libraries_get_path('syntaxhighlighter') returns the path _relative_ to
  //    the site root path,
  // 2. however hook_library_info_build assume that relative paths for the
  //    assets are relative to the path of the module they are defined in (the
  //    current module),
  // 3. while the external library assets we are defining are NOT from the
  //    current module.
  //
  // So using an absolute path makes sure to look for the assets starting from
  // the site root path.
  $lib_location = '/' . libraries_get_path('syntaxhighlighter');
  $styles_path = $lib_location . '/styles/';
  $scripts_path = $lib_location . '/scripts/';
  $libraries = [];
  $config = \Drupal::config('syntaxhighlighter.settings');
  $theme = $config
    ->get('theme');
  $libraries['core'] = [
    'css' => [
      'theme' => [
        $styles_path . 'shCore.css' => [],
        $styles_path . $theme => [],
      ],
    ],
    'js' => [
      $scripts_path . 'shCore.js' => [
        'minified' => TRUE,
      ],
    ],
  ];
  $libraries['legacy'] = [
    'js' => [
      $scripts_path . 'shLegacy.js' => [
        'minified' => TRUE,
      ],
    ],
  ];
  $libraries['brushes'] = [
    'js' => [],
  ];
  $enabled_languages = $config
    ->get('enabled_languages');
  foreach ($enabled_languages as $lang) {
    if (!empty($lang)) {
      $libraries['brushes']['js'] += [
        $scripts_path . $lang => [],
      ];
    }
  }

  // This also needs to be an absolute path, for the reasons stated above.
  $libraries['autoloader'] = [
    'js' => [
      $scripts_path . 'shAutoloader.js' => [],
      // PublicStream::basePath() does the same thing as the old D6
      // file_directory_path() which is to give the default public 'files'
      // directory path relative the the web root.
      '/' . PublicStream::basePath() . '/js/syntaxhighlighter.autoloader.js' => [],
    ],
  ];
  return $libraries;
}

/**
 * Implements hook_page_attachments().
 */
function syntaxhighlighter_page_attachments(array &$attachments) {
  if (!_syntaxhighlighter_page_match()) {
    return;
  }
  $config = \Drupal::config('syntaxhighlighter.settings');
  $attachments['#attached']['library'][] = 'syntaxhighlighter/core';
  if ($config
    ->get('legacy_mode')) {
    $attachments['#attached']['library'][] = 'syntaxhighlighter/legacy';
    $settings['legacyMode'] = TRUE;
  }
  if ($config
    ->get('use_autoloader')) {
    $attachments['#attached']['library'][] = 'syntaxhighlighter/autoloader';
    $settings['useAutoloader'] = TRUE;
  }
  else {
    $attachments['#attached']['library'][] = 'syntaxhighlighter/brushes';
  }
  $tag_name = $config
    ->get('tagname');
  if ($tag_name !== 'pre') {
    $settings['tagName'] = $tag_name;
  }
  $lib_location = libraries_get_path('syntaxhighlighter');
  $scripts_path = $lib_location . '/scripts/';
  if (file_exists($scripts_path . 'clipboard.swf')) {
    $settings['clipboard'] = base_path() . $scripts_path . 'clipboard.swf';
  }
  $default_expressions = $config
    ->get('default_expressions');
  if ($default_expressions) {
    $settings['default_expressions'] = $default_expressions;
  }
  if (isset($settings)) {
    $attachments['#attached']['drupalSettings']['syntaxhighlighter'] = $settings;
  }
  $attachments['#attached']['library'][] = 'syntaxhighlighter/syntaxhighlighter';
}

/**
 * Page matching helper.
 */
function _syntaxhighlighter_page_match() {
  $config = \Drupal::config('syntaxhighlighter.settings');
  $inject = $config
    ->get('inject');
  $pages = $config
    ->get('pages');
  if ($inject != SYNTAXHIGHLIGHTER_INJECT_PHP) {
    $current_path = \Drupal::service('path.current')
      ->getPath();
    $path_alias = Drupal::service('path.alias_manager')
      ->getAliasByPath($current_path);

    // Compare with the internal and path alias (if any).
    $page_match = \Drupal::service('path.matcher')
      ->matchPath($path_alias, $pages);
    if ($path_alias != $current_path) {
      $page_match = $page_match || \Drupal::service('path.matcher')
        ->matchPath($current_path, $pages);
    }
    return !($inject xor $page_match);
  }
  else {

    // If the PHP module is not enabled, we just return FALSE
    // which just ends up disabling the syntaxhighlighter.
    return function_exists('php_eval') && php_eval($pages);
  }
}

/**
 * Implements hook_modules_disabled().
 */
function syntaxhighlighter_modules_disabled($modules) {
  $config = \Drupal::config('syntaxhighlighter.settings');
  if (in_array('php', $modules) && $config
    ->get('inject') == SYNTAXHIGHLIGHTER_INJECT_PHP) {
    drupal_set_message(t('The "%syntaxhighlighter" module is currently configured to use PHP for js/css code inject control, disabling the "%module_name" module will effectively turn off syntax highlighting on all pages.', [
      '%syntaxhighlighter' => t('Syntax Highlighter'),
      '%module_name' => t('PHP filter'),
    ]), 'warning');
  }
}

/**
 * Show usage tips for the syntaxhighlighter filter.
 */
function _syntaxhighlighter_filter_tips($filter, $format, $long = FALSE) {
  $config = \Drupal::config('syntaxhighlighter.settings');
  $tag_name = $config
    ->get('tagname');
  $tip = t('Syntax highlight code surrounded by the <code>@ex0</code> tags, where @lang is one of the following language brushes: %brushes.', [
    '@ex0' => "<{$tag_name} class=\"brush: LANG\">...</{$tag_name}>",
    '@lang' => "LANG",
    '%brushes' => implode(', ', _syntaxhighlighter_get_enabled_language_brushes()),
  ]);
  if ($long) {
    $tip .= '<br/>' . t('See <a href=":url0">the SyntaxHighlighter javascript library site</a> for additional help.', [
      ':url0' => 'http://alexgorbatchev.com/SyntaxHighlighter/',
    ]);
  }
  return $tip;
}

/**
 * Get the enabled programming languages from brushes names.
 *
 * @return array
 *   An array of all enabled languages.
 */
function _syntaxhighlighter_get_enabled_language_brushes() {
  $config = \Drupal::config('syntaxhighlighter.settings');
  $brushes =& drupal_static(__FUNCTION__);
  if (!isset($brushes)) {
    $brushes = [];
    foreach ($config
      ->get('enabled_languages') as $val) {
      if ($val) {
        $brushes[] = strtolower(substr(substr($val, 7), 0, -3));
      }
    }
  }
  return $brushes;
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 *
 * Install custom node validate function.
 */
function syntaxhighlighter_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = '_syntaxhighlighter_node_validate';
}

/**
 * Validate the node input text to be sure that there are no bad tags.
 */
function _syntaxhighlighter_node_validate($form, FormStateInterface $form_state) {
  $body = $form_state
    ->getValue('body');
  if (isset($body)) {
    $format = isset($body[0]['format']) ? $body[0]['format'] : filter_fallback_format();
    if (_syntaxhighlighter_format_has_syntaxhighlighter_filter($format)) {
      if (!empty($body[0]['summary'])) {
        _syntaxhighlighter_validate_input($form_state, "body][0][summary", $body[0]['summary']);
      }
      _syntaxhighlighter_validate_input($form_state, "body][0][value", $body[0]['value']);
    }
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 *
 * Install custom comment validate function.
 */
function syntaxhighlighter_form_comment_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = '_syntaxhighlighter_comment_validate';
}

/**
 * Validate the comment input text to be sure that there are no bad tags.
 */
function _syntaxhighlighter_comment_validate($form, FormStateInterface $form_state) {
  $comment_body = $form_state
    ->getValue('comment_body');
  if (isset($comment_body)) {
    $format = isset($comment_body[0]['format']) ? $comment_body[0]['format'] : filter_fallback_format();
    if (_syntaxhighlighter_format_has_syntaxhighlighter_filter($format)) {
      _syntaxhighlighter_validate_input($form_state, "comment_body][0][value", $comment_body[0]['value']);
    }
  }
}

/**
 * Validate the input form text.
 *
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form being validated.
 * @param string $field
 *   The input field being checked, used in the error message if any error is
 *   found.
 * @param string $text
 *   The input text to validate.
 */
function _syntaxhighlighter_validate_input(FormStateInterface $form_state, $field, $text) {
  $config = \Drupal::config('syntaxhighlighter.settings');
  $tag_name = $config
    ->get('tagname');

  // Check that the number of open and close tags is the same.
  preg_match_all("#<{$tag_name}\\s*[^>]*>#", $text, $matches_open);
  preg_match_all("#</\\s*{$tag_name}\\s*>#", $text, $matches_close);
  if (count($matches_open[0]) != count($matches_close[0])) {
    $error = t('Unbalanced @tag tags.', [
      '@tag' => Markup::create("<{$tag_name}>"),
    ]);
    $form_state
      ->setErrorByName($field, (string) $error);
  }
  else {

    // If they are, make sure that the tags are also in the proper order.
    preg_match_all("#<{$tag_name}\\s*[^>]*>.*</\\s*{$tag_name}\\s*>#sU", $text, $matches_pair);
    if (count($matches_pair[0]) != count($matches_open[0]) || count($matches_pair[0]) != count($matches_close[0])) {
      $error = t('@tag tags in the wrong order (e.g. unbalanced or nested).', [
        '@tag' => Markup::create("<{$tag_name}>"),
      ]);
      $form_state
        ->setErrorByName($field, (string) $error);
    }
  }
}

/**
 * Create the autoload setup script file.
 *
 * Must call this whenever the lib location  and/or the enable brushes change.
 * Make sure never call this if the js lib is not found.
 */
function _syntaxhighlighter_setup_autoloader_script(Config $config) {
  $dir = 'public://js';
  $path = $dir . '/syntaxhighlighter.autoloader.js';
  if ($config
    ->get('use_autoloader')) {
    $script_path = base_path() . libraries_get_path('syntaxhighlighter') . '/scripts/';
    $script_data = <<<__HERE__
/*
 * This file is generated by the Syntax Highlighter module
 */
__HERE__;
    $script_data .= "\nfunction syntaxhighlighterAutoloaderSetup() {\n  SyntaxHighlighter.autoloader(\n";
    $need_ending = FALSE;
    $brushes = $config
      ->get('enabled_languages');
    foreach ($brushes as $b) {
      if ($b) {
        if ($need_ending) {
          $script_data .= ",\n";
        }
        $alias = strtolower(substr(substr($b, 7), 0, -3));
        $script_data .= "    '{$alias} {$script_path}{$b}'";
        $need_ending = TRUE;
      }
    }
    $script_data .= "\n);\n}\n";
    file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
    file_unmanaged_save_data($script_data, $path, FILE_EXISTS_REPLACE);
  }
  elseif (file_exists($path)) {
    file_unmanaged_delete($path);
    @\Drupal::service('file_system')
      ->rmdir($dir);
  }
}

/**
 * Check if the format uses the syntaxhighlighter filter.
 */
function _syntaxhighlighter_format_has_syntaxhighlighter_filter($format_id) {
  return FilterFormat::load($format_id)
    ->filters()
    ->has('filter_syntaxhighlighter');
}

/**
 * Implements hook_libraries_info().
 *
 * NOTE: this hook is deprecated and will be dropped in the stable Drupal 8
 * release of the libraries module.
 */
function syntaxhighlighter_libraries_info() {
  $libraries['syntaxhighlighter'] = [
    // Only used in administrative UI of Libraries API.
    'name' => 'SyntaxHighlighter',
    'vendor url' => 'http://alexgorbatchev.com/SyntaxHighlighter/',
    'download url' => 'https://github.com/syntaxhighlighter/SyntaxHighlighter-Site/tree/master/content/SyntaxHighlighter/download',
    'path' => 'scripts',
    'version arguments' => [
      'file' => 'scripts/shCore.js',
      'pattern' => '@([0-9]+.[0-9]+.[0-9]+)@',
      'lines' => 15,
      'cols' => 30,
    ],
  ];
  return $libraries;
}

Functions

Namesort descending Description
syntaxhighlighter_form_comment_form_alter Implements hook_form_BASE_FORM_ID_alter().
syntaxhighlighter_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter().
syntaxhighlighter_help Implements hook_help().
syntaxhighlighter_libraries_info Implements hook_libraries_info().
syntaxhighlighter_library_info_build Implements hook_library_info_build().
syntaxhighlighter_modules_disabled Implements hook_modules_disabled().
syntaxhighlighter_page_attachments Implements hook_page_attachments().
_syntaxhighlighter_comment_validate Validate the comment input text to be sure that there are no bad tags.
_syntaxhighlighter_filter_tips Show usage tips for the syntaxhighlighter filter.
_syntaxhighlighter_format_has_syntaxhighlighter_filter Check if the format uses the syntaxhighlighter filter.
_syntaxhighlighter_get_enabled_language_brushes Get the enabled programming languages from brushes names.
_syntaxhighlighter_node_validate Validate the node input text to be sure that there are no bad tags.
_syntaxhighlighter_page_match Page matching helper.
_syntaxhighlighter_setup_autoloader_script Create the autoload setup script file.
_syntaxhighlighter_validate_input Validate the input form text.

Constants

Namesort descending Description
SYNTAXHIGHLIGHTER_INJECT_EXCEPT_LISTED Inject SyntaxHighlighter on every page except the listed pages.
SYNTAXHIGHLIGHTER_INJECT_IF_LISTED Inject SyntaxHighlighter on only the listed pages.
SYNTAXHIGHLIGHTER_INJECT_PHP Inject SyntaxHighlighter if the associated PHP code returns TRUE.
SYNTAXHIGHLIGHTER_PHP_PERMISSION
SYNTAXHIGHLIGHTER_TAG_STRING Define a placeholder for the syntaxhighlighter tag.