Есть ли умный способ для анализа списков простого текста в HTML? - PullRequest
0 голосов
/ 17 июня 2009

Вопрос: Есть ли умный способ для разбора списков простого текста в HTML?

Или мы должны прибегнуть к эзотерическим рекурсивным методам или просто грубой силе?

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

Так в чем же умный путь?

Предположения

Необходимо создать сценарий, поэтому это мои предположения.

  1. Списки могут быть вложены на 3 уровня глубиной (как минимум) из неупорядоченных или упорядоченных списков. Тип и глубина списка определяются его префиксом:

    1. После префикса есть обязательный пробел.
    2. Глубина списка зависит от того, сколько в префиксе символов без пробелов; ***** будет вложено в пять списков глубиной.
    3. Тип списка определяется типом символов: * или - - неупорядоченный список, # - неупорядоченный список.
  2. Элементы разделены только 1 \n символом. (Давайте представим, что две последовательные новые строки квалифицируются как «группа», абзац, div или какой-либо другой тег HTML, как в Markdown или Textile.)

  3. Типы списков могут свободно смешиваться.

  4. Вывод должен быть действительным HTML 4, предпочтительно с окончанием </li> s

  5. Разбор может выполняться с или без Regex по желанию.

Образец разметки

* List
*# List
** List
**# List
** List

# List
#* List
## List
##* List
## List

Желаемый выход

Немного разбит на части для удобочитаемости, но это должно быть допустимым вариантом этого (помните, что я просто делаю это красиво!):

<ul>
  <li>List</li>
  <li>
    <ol><li>list</li></ol>
    <ul><li>List</li></ul>
  </li>
  <li>List</li>
  <li>
    <ol><li>List</li></ol>
  </li>
  <li>List</li>
</ul>


<ol>
  <li>List</li>
  <li>
    <ul><li>list</li></ul>
    <ol><li>List</li></ol>
  </li>
  <li>List</li>
  <li>
    <ul><li>List</li></ul>
  </li>
  <li>List</li>
</ol>

В итоге

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

Ответы [ 8 ]

2 голосов
/ 18 июня 2009

Лучшее объяснение, которое я видел, от Perl высшего порядка Марка Джейсона Домина. Полный текст доступен онлайн на http://hop.perl.plover.com/book/.

Хотя примеры приведены на Perl, логика каждой области фантастическая.

Глава 8 (!! PDF link) специально о разборе. Хотя уроки в книге несколько связаны.

2 голосов
/ 17 июня 2009

Линейное решение с некоторыми понятиями питона:

cur = ''
for line in lines():
    prev = cur
    cur, text = split_line_into_marker_and_remainder(line)
    if cur && (cur == prev) :
         print '</li><li>'
    else :
         nprev, ncur = kill_common_beginning(prev, cur)
         for c in nprev: print '</li>' + ((c == '#') ? '</ol>' : '</ul>') 
         for c in ncur:  print           ((c == '#') ? '<ol>'  : '<ul>' )  + '<li>'
    print text 

Вот как это работает: для обработки строки я сравниваю маркер для предыдущей строки с маркером для этой строки.

Я использую вымышленную функцию split_line_into_marker_and_remainder, которая возвращает два результата, маркер cur и сам текст. Тривиально реализовать его как функцию C ++ с 3 аргументами, входными и 2 выходными строками.

В основе лежит вымышленная функция kill_common_beginning, которая уберет повторяющиеся части prev и cur. После этого мне нужно закрыть все, что осталось в предыдущем маркере, и открыть все, что осталось в текущем маркере. Я могу сделать это с заменой, отображением символов в строку или с помощью цикла.

Три строки будут довольно просты в C ++:

char * saved = prev;
for (; *prev && (*prev == *cur);  prev++, cur++ ); // "kill_common_beginning"
while (*prev) *(prev++) == '#' ? ...
while (*cur)  *(cur++) == '#' ? ...
cur = saved;

Обратите внимание, однако, что есть особый случай: когда отступ не изменился, эти строки ничего не выводят. Это хорошо, если мы за пределами списка, но это не очень хорошо в списке: так что в этом случае мы должны вывести </li><li> вручную.

2 голосов
/ 17 июня 2009

Базовая итерационная техника:

  1. Регулярное выражение или какой-то другой простой синтаксический анализатор, который распознает формат списка, захватывая каждый элемент списка (включая элементы с дополнительными уровнями отступов).
  2. Счетчик для отслеживания текущего уровня отступа.
  3. Логика для перебора каждого захвата, записи <li> s и вставки соответствующих начальных / конечных тегов (<ol></ol>, <ul></ul>) и увеличения / уменьшения счетчика отступов всякий раз, когда текущий уровень отступа больше или меньше значения предыдущий.

Редактировать: Вот простое выражение, которое, вероятно, подойдет вам с небольшой настройкой: каждое совпадение - это список верхнего уровня, с двумя наборами именованных захватов, маркеров (количество символов уровень отступа, последний символ обозначает желаемый тип списка) и текст элемента списка.

(?:(?:^|\n)[\t ]*(?<marker>[*#]+)[\t ]*(?<text>[^\n\r]+)\r*(?=\n|$))+
1 голос
/ 18 июня 2009

Вот мое собственное решение, которое похоже на гибрид предложений Shog9 (вариант его регулярного выражения, Ruby не поддерживает именованные совпадения) и итеративный метод Ильи. Мой рабочий язык был Ruby.

Некоторые примечания: я использовал систему, основанную на стеке, и тот факт, что «String # scan (pattern)» на самом деле является методом «match-all», который возвращает массив совпадений.

def list(text)
  # returns [['*','text'],...]
  parts = text.scan(/(?:(?:^|\n)([#*]+)[\t ]*(.+)(?=\n|$))/)

  # returns ul/ol based on the byte passed in
  list_type = lambda { |c| (c == '*' ? 'ul' : 'ol') }

  prev = []
  tags = [list_type.call(parts[0][0][0].chr)]
  result = parts.inject("<#{tags.last}><li>") do |output,newline|
    unless prev.count == 0
      # the following comparison says whether added or removed,
      # this is the "how much"
      diff = (prev[0].length - newline[0].length).abs
      case prev[0].length <=> newline[0].length
        when -1: # new tags to add
          part = ((diff > 1) ? newline[0].slice(-1 - diff,-1) : newline[0][-1].chr)
          part.each_char do |c|
            tags << list_type.call(c)
            output << "<#{tags.last}><li>"
          end
        when 0: # no new tags... but possibly changed
          if newline[0] == prev[0]
            output << '</li><li>'
          else
            STDERR.puts "Bad input string: #{newline.join(' ')}"
          end
        when 1: # tags removed
          diff.times{ output << "</li></#{tags.pop}>" }
          output << '</li><li>'
      end
    end

    prev = newline
    output + newline[1]
  end

  tags.reverse.each { |t| result << "</li></#{t}>" }
  result
end

К счастью, этот код работает и генерирует корректный HTML. И это оказалось лучше, чем я ожидал. Это даже не кажется неуклюжим.

1 голос
/ 17 июня 2009

Вот как вы можете сделать это с помощью регулярных выражений и цикла (^ обозначает новую строку, $ - конечную):

do { 
    ^#anything$ -> <ol><li>$^anything</li></ol>$
    ^*anything$ -> <ul><li>$^anything</li></ul>$
} while any of those above applies

do {
    </ol><ol> -> 
    </ul><ul> -> 
    </li><li> -> 
} while any of those above applies

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

1 голос
/ 17 июня 2009

Посмотрите на Текстиль .

Он доступен на нескольких языках.

0 голосов
/ 20 января 2010

Попробуйте Желатин . Определение синтаксиса, вероятно, будет 5 строк или менее.

0 голосов
/ 18 июня 2009

Эта Perl-программа - первая попытка сделать это.

#! /usr/bin/env perl
use strict;
use warnings;
use 5.010;

my $data = [];
while( my $line = <> ){
  last if $line =~ /^[.]{3,3}$/;
  my($nest,$rest) = $line =~ /^([\#*]*)\s+(.*)$/x;
  my @nest = split '', $nest;

  if( @nest ){
    recourse($data,$rest,@nest);
  }else{
    push @$data, $line;
  }
}

de_recourse($data);

sub de_recourse{
  my($ref) = @_;
  my %de_map = (
    '*' => 'ul',
    '#' => 'ol'
  );

  if( ref $ref ){
    my($type,@elem) = @$ref;
    if( ref $type ){
      for my $elem (@$ref){
        de_recourse($elem);
      }
    }else{
      $type = $de_map{$type};

      say "<$type>";
      for my $elem (@elem){
        say "<li>";
        de_recourse($elem);
        say "</li>"
      }
      say "</$type>";
    }
  }else{
    print $ref;
  }
}

sub recourse{
  my($last_ref,$str,@nest) = @_;
  die unless @_ >= 2;
  die unless ref $last_ref;
  my $nest = shift @nest;

  if( @_ == 2 ){
    push @$last_ref, $str;
    return;
  }

  my $previous = $last_ref->[-1];
  if( ref $previous ){
    if( $previous->[0] eq $nest ){
      recourse( $previous,$str,@nest );
      return;
    }
  }

  my $new_ref = [ $nest ];
  push @$last_ref, $new_ref;
  recourse( $new_ref, $str, @nest );
}

Надеюсь, это поможет

...