Ruby: How to implement word wrapping that ignores <span> tags when calculating line length?

I need to write a small ruby ​​function that does word wrap. I have the following function:

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

      

It's basically the word_wrap feature included in Rails.

I would like to write a similar function that parses a string with span elements inside, except that tags do not have to be counted to wrap the string.

Example:

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

      

Currently word_wrap (s, 20) gives something like this:

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

      

It should be:

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

      

As you can see, the new feature creates a line word_wrap (maximum) 20 characters, excluding the tag <span>

and </span>

.

How do you do it? All suggestions are welcome!

Thanks in advance for your help.

+1


a source to share


2 answers


Here's the regex

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"

      

Basically we grab as many characters as we can up to the width of the word, ignoring the spacing (and making sure we don’t capture parts of the spans), and making sure we end up with a nonspatial character followed by either a break or a word.

I'll break down how the regex works:

SPAN_RE

matches a single span tag (either <span>

, or </span>

or or ...)

(?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

matches all spans at a given position - ensuring that the next character that matches is not the start of a span tag.

(?:             - 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

      

This means that we can match one character after ALL_SPAN_RE

and be sure that we are not capturing part of the span.

full_re

then just greedily matches as many characters as it can to the desired width (excluding spans), making sure it ends with a nonspatial character that is either word end or spacing.

(                     - 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

      

+2


a source


It's fun! (although has a little housework)

I deleted my previous attempts, you can check the answer history if interested. Here's my final answer, slightly longer vertically but more on the ground and not as hacky as the previous one.



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)

      

0


a source







All Articles