2

Some users enter body copy and comments with the caps lock on. All uppercase is considered shouting and I would like to convert it to lowercase or "natural" with uppercase characters at the beginning of a sentence.

Any easy solutions?

  • 1
    my site is live now, it's generating tons of "challenges", plus I have tons of ideas and my members come up with some too ;-) – uwe Apr 12 '12 at 16:06
2

Converting the post to all lower case would be pretty simple; you can implement your own filter like so:

function MYMODULE_filter_info() {
  $filter['allcaps_filter'] = array(
    'title' => t('Disallow ALL CAPS posts'),
    'description' => t('Rudimentary filter which converts all text to lowercase'),
    'process callback' => 'MYMODULE_allcaps_filter_process'
  );

  return $filter;
}

function MYMODULE_allcaps_filter_process($text) {
  // Attempt to make the first letter of the first word uppercase. This is very
  // likely to fail as the inputted string will probably be HTML.
  return ucfirst(strtolower($text));
}

Once you clear the caches you'll be able to add that filter to any text format you want.

It's worth noting that the description rudimentary in the comments is very, very apt...for example this will also convert any HTML attributes/tags to lowercase which might break embedded image paths etc. It might provide a good place to start though.

Constructing more naturally cased alternatives is going to be very tricky, mainly because you haven't just got text in there...you've got HTML as well. I can't think of a decent way to do this at the moment to be honest, short of importing the string into a DOMDocument and looping through every element, converting the text in chunks as you go.

I'd provide an example for that but I think you've got one ;)

  • 2
    I think CSS's text-transform:capitalize; would do what this filter does without [a|e]ffecting any tags. – Jimajamma Apr 12 '12 at 15:17
  • 1
    @Jimajamma Yeah, visually text-trasnform would work even better than the above filter...but text would still come up in all caps in, for example, SERPs. This might be fine though, you should probably put that as an answer – Clive Apr 12 '12 at 15:20
  • thanks guys. Looks like I could create a lot of headache downstream if it's not implemented right. I think what I'm going to do is add a Capslock detector on my input forms and then display a message to the user why they shouldn't use all uppercase: code.stephenmorley.org/javascript/detecting-the-caps-lock-key Thanks again!!! – uwe Apr 14 '12 at 3:15
2

As alternative to the Clive solution, I would implement different hooks, which are hook_comment_view_alter() for comments, and hook_node_view_alter() for nodes; you could also implement hook_entity_view_alter() that is called also for nodes, and comments.

There are some differences between the two approaches that needs to be considered.

  • Input filters don't get any information about the source of the text they are called to process; they can be called for a node body, a node content, a field content, or a view.

    The following screenshot shows part of the settings for a "long text" field; when "Filtered text" is selected, the user can select the input format that is used for the field being written.

    screenshot

    The following screenshot shows the settings the Views module has for a view header; selecting "Global: Text area" as header, these are the settings Views would present for the view's header.

    screenshot

    In any of those cases, the input filter is not able to understand when it is called, and for what. If your purpose is just changing the content of nodes, and comments, an input filter is not what you should implement. If your purpose is to change every input given from users (independently from the roles they have), then you can implement an input filter, but consider that input filters can be used in contexts completely different from the ones you could know of. What done from the Views module can be done from any other module that accepts input from the users, and allows them to decide the type of the input format (between the ones they can use).

    Differently, hook_comment_view_alter() is given some information about the comment being viewed, as those hook implementations are called with the following code. (See comment_view().)

      // Populate $comment->content with a render() array.
      comment_build_content($comment, $node, $view_mode, $langcode);
    
      $build = $comment->content;
      // We don't need duplicate rendering info in comment->content.
      unset($comment->content);
    
      $build += array(
        '#theme' => 'comment__node_' . $node->type, 
        '#comment' => $comment, 
        '#node' => $node, 
        '#view_mode' => $view_mode, 
        '#language' => $langcode,
      );
    
      // …
    
      // Allow modules to modify the structured comment.
      $type = 'comment';
      drupal_alter(array('comment_view', 'entity_view'), $build, $type);
    

    The same happens to hook_node_view_alter() which is invoked by node_view().

      // Populate $node->content with a render() array.
      node_build_content($node, $view_mode, $langcode);
    
      $build = $node->content;
      // We don't need duplicate rendering info in node->content.
      unset($node->content);
    
      $build += array(
        '#theme' => 'node', 
        '#node' => $node, 
        '#view_mode' => $view_mode, 
        '#language' => $langcode,
      );
    
      // Add contextual links for this node, except when the node is already being
      // displayed on its own page. Modules may alter this behavior (for example,
      // to restrict contextual links to certain view modes) by implementing
      // hook_node_view_alter().
      if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) {
        $build['#contextual_links']['node'] = array('node', array($node->nid));
      }
    
      // Allow modules to modify the structured node.
      $type = 'node';
      drupal_alter(array('node_view', 'entity_view'), $build, $type);
    

    In both the cases, also hook_entity_view_alter() is invoked, and it gets a parameter saying which type of entity is being altered. hook_entity_view_alter() is probably preferable when the same code is used for more than one entity (such as nodes, and comments).

  • The output of check_markup(), which is the function used to get the output of an input format, is cached. With the actual code, the cache is not used for comments, as the function (whose declaration is check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE)) is called without its fourth parameter, such as in comment_submit().

    if (isset($comment_body['format'])) {
      $comment_text = check_markup($comment_body['value'], $comment_body['format']);
    }
    else {
      $comment_text = check_plain($comment_body['value']);
    }
    

    This is not what happens with fields, where the code used is the following one. (See _text_sanitize().)

    // If the value uses a cacheable text format, text_field_load() precomputes
    // the sanitized string.
    if (isset($item["safe_$column"])) {
      return $item["safe_$column"];
    }
    return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode) : check_plain($item[$column]);
    

    The code that in check_markup() checks for values stored in the cache is the following one.

    // Check for a cached version of this piece of text.
    $cache = $cache && !empty($format->cache);
    $cache_id = '';
    if ($cache) {
      $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);
      if ($cached = cache_get($cache_id, 'cache_filter')) {
        return $cached->data;
      }
    }
    

    When $cache is FALSE, $cache && !empty($format->cache) is FALSE too, and the cache is not used. This is what happens with comments, for example. For fields, it is the module implementing the field that decides when to use a cached value, or not. (In the case of the node body, it is used a cached value.)

 

As side note, when the input of a user can contain any UTF-8 code, it is preferable to use drupal_strlower(), and drupal_ucfirst(), which are coded to always work with UTF-8 strings. See the code of drupal_strlower() as example. (drupal_ucfirst() calls drupal_strlower().)

  global $multibyte;
  if ($multibyte == UNICODE_MULTIBYTE) {
    return mb_strtoupper($text);
  }
  else {
    // Use C-locale for ASCII-only uppercase
    $text = strtoupper($text);
    // Case flip Latin-1 accented letters
    $text = preg_replace_callback('/\xC3[\xA0-\xB6\xB8-\xBE]/', '_unicode_caseflip', $text);
    return $text;
  • thanks for the very detailed answer. Looks a bit too complex for my simple case but I did learn a bunch reading it! – uwe Apr 14 '12 at 3:06
1

QUICK AND DIRTY, or should I say quick and dirty....you could plug this logic into @Clive's filter....

    <?php

$text='This is a TEST of the <b>EMERGENCY BROADCAST</b> system.<br>This is only a test. Had this been an actual <span class="EMERGENCY">emergency</span>, you would have been vaporized about <i>3 milliseconds</i> ago.  this concludes this test.';

$input=str_split($text);
$output='';

$intag=FALSE;
$afterperiod=TRUE;
$aftercap=FALSE;

$numupper=0;
$total=strlen($text);

foreach($input as $t) {

  if ($intag) {
    switch ($t) {
      case '>':
        $intag=FALSE;
      default:
        $output.=$t;
    }
  } else {
    if ($afterperiod) {
      switch($t) {
        case '<':
          $intag=TRUE;
        case ' ':
        case '.':
          $output.=$t;
          break;

        default:
          $output.=$t;
          $afterperiod=FALSE;

          if (ctype_upper($t)) {
            $numupper++;
          }
      }
    } else {
      switch ($t) {
        case '.':
          $afterperiod=TRUE;
          $output.=$t;
          break;

        case '<':
          $intag=true;
          $output.=$t;
          break;

        default:
          $output.=strtolower($t);

          if (ctype_upper($t)) {
            $numupper++;
          }
      }
    }
  }
}

$shouting=$numupper/$total;

if ($shouting > .5) {
  return $output;
} else {
  return $text;
}

but this turns all caps to lower in the text except for those at the beginning of a sentence (assuming no newlines, tabs, etc) and ignoring any tags. Additional logic could be added to look for only repeating caps etc. ADDITION: But then again, it does this only if more than half the characters are uppercase....

-1
text-transform:uppercase;

use this in css

  • this would actually do the exact opposite of what he wants :) – Jimajamma Apr 12 '12 at 18:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.