0

问题:有没有一种聪明的方法可以将纯文本列表解析为 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>

总之

是怎么做到的?我真的很想了解处理不可预测的递归列表的好方法,因为它让我觉得任何人都可以纠缠不清。

4

8 回答 8

2

我见过的最好的解释来自 Mark Jason Dominus 的 Higher-Order Perl。全文可在http://hop.perl.plover.com/book/在线获取。

尽管这些示例都在 Perl 中,但每个区域背后的逻辑分解都很棒。

第 8 章(!PDF 链接)专门介绍解析。尽管本书中的课程有些相关。

于 2009-06-17T21:37:35.227 回答
2

带有一些pythonic概念的逐行解决方案:

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和文本本身。将其实现为具有 3 个参数、一个输入和 2 个输出字符串的 C++ 函数很简单。

核心是一个虚构的函数,它会去掉和kill_common_beginning的重复部分 。之后,我需要关闭保留在前一个标记中的所有内容并打开保留在当前标记中的所有内容。我可以通过替换、将字符映射到字符串或循环来做到这一点。prevcur

这三行在 C++ 中将非常简单:

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

但是请注意,有一种特殊情况:当缩进没有改变时,这些行不会输出任何内容。如果我们在列表之外,那很好,但在列表中就不行了:所以在这种情况下,我们应该</li><li>手动输出。

于 2009-06-17T19:54:00.317 回答
2

基本迭代技术:

  1. 一个正则表达式或其他一些简单的解析器,可以识别列表的格式,捕获每个列表项(包括那些具有额外缩进级别的项)。
  2. 跟踪当前缩进级别的计数器。
  3. 迭代每个捕获的逻辑,写出<li>s 并插入适当的开始/结束标签 ( <ol></ol>, <ul></ul>) 并在当前缩进级别大于或小于前一个缩进级别时递增/递减缩进计数器。

编辑:这是一个简单的表达式,可能会对您进行一些调整:每个匹配项都是一个顶级列表,有两组命名捕获,标记(字符计数是缩进级别,最后一个字符表示所需的列表类型) 和列表项文本。

(?:(?:^|\n)[\t ]*(?<marker>[*#]+)[\t ]*(?<text>[^\n\r]+)\r*(?=\n|$))+
于 2009-06-17T18:58:24.980 回答
1

这是我自己的解决方案,它似乎是 Shog9 的建议(他的正则表达式的变体,Ruby 不支持命名匹配)和 Ilya 的迭代方法的混合体。我的工作语言是 Ruby。

一些值得注意的事情:我使用了一个基于堆栈的系统,“String#scan(pattern)”实际上只是一个“匹配所有”方法,它返回一个匹配数组。

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。结果确实比我预期的要好。它甚至不觉得笨拙。

于 2009-06-18T13:54:42.980 回答
1

看看纺织

它有多种语言版本。

于 2009-06-17T18:44:33.287 回答
1

这就是你如何使用正则表达式循环来做到这一点(^代表换行符,代表结束行$):

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

这使得它比简单的正则表达式简单得多。它的工作方式:你首先扩展每一行,就好像它是孤立的一样,然后吃掉额外的列表标记。

于 2009-06-17T18:46:01.503 回答
0

试试明胶。语法定义可能是 5 行或更少。

于 2010-01-19T22:59:43.777 回答
0

这个 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 );
}

希望能帮助到你

于 2009-06-18T05:43:11.597 回答