Ruby: Как реализовать перенос слов, который игнорирует теги <span>при вычислении длины строки? - PullRequest
1 голос
/ 07 мая 2009

Мне нужно написать небольшую рубиновую функцию, которая выполняет перенос слов. У меня есть следующая функция:

def word_wrap(text, line_width)
  if line_width.nil? || line_width < 2
    line_width = 40
  end
  text.split("\n").collect do |line|
   line.length > line_width ? line.gsub(/.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
  end * "\n"
end

Это в основном функция word_wrap, включенная в Rails.

Я хотел бы написать аналогичную функцию, которая анализирует строку с элементами span внутри, за исключением того, что теги не должны учитываться для переноса строки.

Пример:

s = "Lorem <span>ipsum dolor</span> si<span>t</span> amet, conse<span>ctetur adipiscing elit</span> Praesent"

На данный момент word_wrap (s, 20) дает что-то вроде этого:

Lorem <span>ipsum
dolor</span>
si<span>t</span>
amet,
conse<span>ctetur
adipiscing
elit</span> Praesent

Это должно быть:

Lorem <span>ipsum dolor</span> si
<span>t</span> amet, conse<span>ctetur 
adipiscing elit</span> 
Praesent

Как видите, новая функция word_wrap создает строки длиной не более 20 символов, не считая теги <span> и </span>.

Как бы вы это сделали? Все предложения приветствуются!

Заранее спасибо за помощь.

Ответы [ 2 ]

2 голосов
/ 07 мая 2009

Вот решение для регулярных выражений

irb> SPAN_RE = /(?i:<\/?span[^>]*>)/
#=> /(?i:<\/?span[^>]*>)/
irb> ALL_SPANS_RE = /(?:#{SPAN_RE}*(?!#{SPAN_RE}))/
#=> /(?:(?i-mx:<\/?span[^>]*>)*(?!(?i-mx:<\/?span[^>]*>)))/
irb> def word_wrap(str,width)
         full_re = /((?:#{ALL_SPANS_RE}.){0,#{width-1}}#{ALL_SPANS_RE}\S(?:#{SPAN_RE}+|\b))\s*/
         str.gsub(/\s*\n/, ' ').gsub(full_re, "\\1\n")
     end
#=> nil
irb> text =<<TEXT
     Lorem <span>ipsum
     dolor</span>
     si<span>t</span>
     amet,
     conse<span>ctetur
     adipiscing
     elit</span> Praesent
     TEXT
#=> "Lorem <span>ipsum\ndolor</span>\nsi<span>t</span>\namet,\nconse<span>ctetur\nadipiscing\nelit</span> Praesent\n"
irb> puts word_wrap(text,20)
Lorem <span>ipsum dolor</span> si<span>
t</span> amet, conse<span>ctetur
adipiscing elit</span>
Praesent
#=> nil
irb> word_wrap(text,20)
#=> "Lorem <span>ipsum dolor</span> si<span>\nt</span> amet, conse<span>ctetur\nadipiscing elit</span>\nPraesent\n"

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

Я разобью, как работает регулярное выражение:

SPAN_RE соответствует одному тегу span (либо <span> или </span>, либо или ...)

(?i:    - Start of a non-capturing parenthesis (useful for grouping patterns)
          The i flag means the inner pattern is case-insensitive
  <     - a literal '<' character
  \/?   - 0 or 1 a forward slashes
  span  - the letters "span"
  [^>]* - 0 or more other characters that are not a '>' character
  >     - a literal '>' character
)       - end of the non-capturing parenthesis

ALL_SPAN_RE соответствует всем интервалам в данной позиции, гарантируя, что следующий символ соответствует , а не начало тега span.

(?:             - Start of a non-capturing parenthesis (useful for grouping patterns)
  #{SPAN_RE}*   - 0 or more spans
  (?!           - Start of a negative lookahead
    #{SPAN_RE}  - exactly 1 span
                  Since this is inside a negative lookahead, it means that the next 
                  character in the string is not allowed to start a span
  )             - end of the negative lookahead
)               - end of the non-capturing parenthesis

Это означает, что мы можем сопоставить один символ после ALL_SPAN_RE и быть уверенным, что мы не захват части пролета.

full_re тогда просто жадно сопоставляет столько символов, сколько может, до желаемой ширины (игнорируя пролеты), убедившись, что он заканчивается на непустой символ, который является либо концом слова, либо сопровождается интервалом.

(                     - start of a capturing parenthesis
  (?:                 - start of a non-capturing parenthesis
    #{ALL_SPANS_RE}   - any and all spans
    .                 - one character (which can't be the start of a span)
  )                   - end of non-capturing parenthesis
  {0,#{width-1}}      - match preceding pattern up to width-1 times
                        so this matches width-1 characters (ignoring spans)
  #{ALL_SPANS_RE}     - any and all spans
  \S                  - a non-whitespace character
                        we don't want to insert a "\n" after whitespace
  (?:                 - a non-capturing parenthesis
    #{SPAN_RE}+       - 1 or more spans
    |                 - OR
    \b                - the end of a word
                        these alternatives makes sure we aren't breaking in the middle of a word
   )                  - end of non-capturing parentheis
 )                    - end of capturing parenthesis
 \s*                  - any whitespace
                        since we're wrapping, we can just toss this when we insert the newline
0 голосов
/ 07 мая 2009

Это весело! (хотя и имеет немного домашнего запаха)

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

s = "Lorem <span>ipsum dolor</span> si<span>t</span> amet, conse<span>ctetur adipiscing elit</span> Praesent"

def word_wrap(s_arg, line_width = 40)
  producer = s_arg.dup
  consumer = ""
  counter  = 0
  while !producer.empty?
    if producer =~ %r[\A</?span>]
      consumer << producer.slice!(0, $&.length)
      next
    end
    consumer << producer.slice!(0, 1)
    counter += 1
    next if counter <= line_width
    consumer.sub!(/ (\S*?)\z/, "\n\\1")
    counter = $1.length
  end
  consumer
end

puts word_wrap(s, 20)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...