Поиск точки в строке, которая не находится внутри BBCodes

У меня есть строка, содержащая текст статьи. Это обсыпано BBCodes (между квадратными скобками). Мне нужно иметь возможность получить первые, скажем, 200 символов статьи, не обрезая их посередине bbcode. Поэтому мне нужен индекс, где безопасно его отрезать. Это даст мне резюме статьи.

  • Сводка должна состоять минимум из 200 символов, но может быть длиннее, чтобы «выйти» из bbcode. (это значение длины фактически будет параметром функции).
  • Это не должно давать мне точку внутри отдельного bbcode (см. канал), например: [lis|t].
  • Это не должно давать мне точку между начальным и конечным bbcode, например: [url="http://www.google.com"]Go To Goo|gle[/url].
  • Это не должно давать мне точку внутри начального или конечного bbcode или между ними, как в приведенном выше примере.

Это должно дать мне «безопасный» индекс, который находится после 200 и не отсекает никаких BBCodes.

Надеюсь, это имеет смысл. Я боролся с этим некоторое время. Мои навыки регулярных выражений только умеренные. Спасибо за любую помощь!


person Sherri    schedule 28.07.2009    source источник
comment
Возможно, мне придется пересмотреть этот вопрос благодаря комментарию krdluzni о том, что если bbcode обернут вокруг всей статьи. Что, я думаю, мне нужно сделать, так это убедиться, что точка отсечки не находится внутри какого-либо начального или конечного кода, а затем закрыть все незакрытые теги. Хотя я не уверен, как определить, находится ли он внутри тега bbcode...   -  person Sherri    schedule 20.08.2009


Ответы (5)


Во-первых, я бы посоветовал подумать о том, что вы будете делать с публикацией, полностью обернутой в BB-коды, как это часто бывает в случае с тегом шрифта. Другими словами, решение проблемы, как указано, легко приведет к «резюме», содержащему всю статью. Может оказаться более полезным определить, какие теги все еще открыты, и добавить необходимые BB-коды, чтобы закрыть их. Конечно, в случае ссылки потребуется дополнительная работа, чтобы убедиться, что вы не сломаете ее.

person krdluzni    schedule 28.07.2009
comment
Оооо, это очень, очень хороший момент, о том, что, если все это завернуто в bbcode. Возможно, мне придется переосмыслить это. - person Sherri; 29.07.2009

Что ж, очевидный простой ответ состоит в том, чтобы представить ваше «резюме» вообще без какой-либо разметки, основанной на bbcode (регулярное выражение ниже взято из здесь)

$summary = substr( preg_replace( '|[[\/\!]*?[^\[\]]*?]|si', '', $article ), 0, 200 );

Однако для выполнения работы, которую вы явно описываете, потребуется больше, чем просто регулярное выражение. С этим справится лексер/парсер, но это довольно сложная тема. Я посмотрю, смогу ли я придумать что-нибудь.

РЕДАКТИРОВАТЬ

Вот симпатичная гетто-версия лексера, но для этого примера она работает. Это преобразует входную строку в токены bbcode.

<?php

class SimpleBBCodeLexer
{
  protected
      $tokens = array()
    , $patterns = array(
        self::TOKEN_OPEN_TAG  => "/\\[[a-z].*?\\]/"
      , self::TOKEN_CLOSE_TAG => "/\\[\\/[a-z].*?\\]/"
    );

  const TOKEN_TEXT      = 'TEXT';
  const TOKEN_OPEN_TAG  = 'OPEN_TAG';
  const TOKEN_CLOSE_TAG = 'CLOSE_TAG';

  public function __construct( $input )
  {
    for ( $i = 0, $l = strlen( $input ); $i < $l; $i++ )
    {
      $this->processChar( $input{$i} );
    }
    $this->processChar();
  }

  protected function processChar( $char=null )
  {
    static $tokenFragment = '';
    $tokenFragment = $this->processTokenFragment( $tokenFragment );
    if ( is_null( $char ) )
    {
      $this->addToken( $tokenFragment );
    } else {
      $tokenFragment .= $char;
    }
  }

  protected function processTokenFragment( $tokenFragment )
  {
    foreach ( $this->patterns as $type => $pattern )
    {
      if ( preg_match( $pattern, $tokenFragment, $matches ) )
      {
        if ( $matches[0] != $tokenFragment )
        {
          $this->addToken( substr( $tokenFragment, 0, -( strlen( $matches[0] ) ) ) );
        }
        $this->addToken( $matches[0], $type );
        return '';
      }
    }
    return $tokenFragment;
  }

  protected function addToken( $token, $type=self::TOKEN_TEXT )
  {
    $this->tokens[] = array( $type => $token );
  }

  public function getTokens()
  {
    return $this->tokens;
  }
}

$l = new SimpleBBCodeLexer( 'some [b]sample[/b] bbcode that [i] should [url="http://www.google.com"]support[/url] what [/i] you need.' );

echo '<pre>';
print_r( $l->getTokens() );
echo '</pre>';

Следующим шагом будет создание синтаксического анализатора, который перебирает эти токены и предпринимает действия при обнаружении каждого типа. Может быть, у меня будет время, чтобы сделать это позже...

person Peter Bailey    schedule 28.07.2009

Это не похоже на работу для (только) регулярного выражения. Логика "простого программирования" - лучший вариант:

  • возьмите символ, отличный от '[', увеличьте счетчик;
  • если вы встретите открывающий тег, продолжайте продвигаться, пока не дойдете до закрывающего тега (не увеличивайте счетчик!);
  • прекратите захват текста, когда ваш счетчик достигнет 200.
person Bart Kiers    schedule 28.07.2009

Вот начало. В данный момент у меня нет доступа к PHP, поэтому вам может потребоваться некоторая настройка, чтобы заставить его работать. Кроме того, это не гарантирует закрытие тегов (т. е. в строке может быть [url] без [/url]). Кроме того, если строка недействительна (т. е. не все квадратные скобки совпадают), она может не вернуть то, что вы хотите.

function getIndex($str, $minLen = 200)
{
  //on short input, return the whole string
  if(strlen($str) <= $minLen)
    return strlen($str);

  //get first minLen characters
  $substr = substr($str, 0, $minLen);

  //does it have a '[' that is not closed?
  if(preg_match('/\[[^\]]*$/', $substr))
  {
    //find the next ']', if there is one
    $pos = strpos($str, ']', $minLen);

    //now, make the substr go all the way to that ']'
    if($pos !== false)
      $substr = substr($str, 0, $pos+1);
  }

  //now, it may be better to return $subStr, but you specifically
  //asked for the index, which is the length of this substring.
  return strlen($substr);
}
person Kip    schedule 28.07.2009

Я написал эту функцию, которая должна делать именно то, что вы хотите. Он подсчитывает n символов (кроме тех, что в тегах), а затем закрывает теги, которые необходимо закрыть. Пример использования включен в код. Код написан на питоне, но его очень легко портировать на другие языки, такие как php.

def limit(input, length):
  """Splits a text after (length) characters, preserving bbcode"""

  stack = []
  counter = 0
  output = ""
  tag = ""
  insideTag = 0           # 0 = Outside tag, 1 = Opening tag, 2 = Closing tag, 3 = Opening tag, parameters section

  for i in input:
    if counter >= length: # If we have reached the max length (add " and i == ' '") to not make it split in a word
      break
    elif i == '[':        # If we have reached a tag
      insideTag = 1
    elif i == '/':        # If we reach a slash...
      if insideTag == 1:  # And we are in an opening tag
        insideTag = 2
    elif i == '=':        # If we have reached the parameters
      if insideTag >= 1:  # If we actually are in a tag
        insideTag = 3
    elif i == ']':        # If we have reached the closing of a tag
      if insideTag == 2:  # If we are in a closing tag
        stack.pop()       # Pop the last tag, we closed it
      elif insideTag >= 1:# If we are in a tag, parameters or not
        stack.append(tag) # Add current tag to the tag-stack
      if insideTag >= 0:  # If are in some type of tag
        insideTag = 0
        tag = ""
    elif insideTag == 0:  # If we are not in a tag
      counter += 1
    elif insideTag <= 2:  # If we are in a tag and not among the parameters
      tag += i
    output += i

  while len(stack) > 0:
    output += '[/'+stack.pop()+']'   # Add the remaining tags

  return output

cutText = limit('[font]This should be easy:[img]yippee.png[/img][i][u][url="http://www.stackoverflow.com"]Check out this site[/url][/u]Should be cut here somewhere [/i][/font]', 60)
print cutText
person Håkon    schedule 15.08.2009