Commit 9d4658f9 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Handle excessive close tags in HTML auto-correction.

The #open and #close methods of HtmlStack now take StringScanner
as input and work at a higher level.
parent 34187eb4
Loading
Loading
Loading
Loading
+5 −3
Original line number Diff line number Diff line
@@ -110,13 +110,15 @@ module JsDuck
          # There might still be "{" that doesn't begin {@link} or {@img} - ignore it
          out += s.scan(/[{]/)
        elsif substitute = @inline_example.replace(s)
          tags.push_tag("pre")
          tags.push_tag("code")
          out += substitute
        elsif s.check(/<\w/)
          # Open HTML tag
          out += s.scan(/</) + tags.open(s.scan(/\w+/)) + s.scan_until(/>|\Z/)
          out += tags.open(s)
        elsif s.check(/<\/\w+>/)
          # Close HTML tag
          out += s.scan(/<\//) + tags.close(s.scan(/\w+/)) + s.scan(/>/)
          out += tags.close(s)
        elsif s.check(/</)
          # Ignore plain '<' char.
          out += s.scan(/</)
@@ -128,7 +130,7 @@ module JsDuck
        end
      end

      out + tags.close_unfinished
      out
    end

    # Creates a link based on the link template.
+46 −32
Original line number Diff line number Diff line
@@ -4,18 +4,6 @@ module JsDuck

  # Tracks opening and closing of HTML tags, with the purpose of
  # closing down the unfinished tags.
  #
  # Synopsis:
  #
  #     tags = HtmlStack.new
  #     # open and close a bunch of tags
  #     tags.open("a")
  #     tags.open("b")
  #     tags.close("b")
  #
  #     # ask which tags still need to be closed
  #     tags.close_unfinished --> "</a>"
  #
  class HtmlStack

    # Initializes the stack with two optional parameters:
@@ -28,18 +16,23 @@ module JsDuck
      @open_tags = []
    end

    # Registers opening of a tag.  Returns the tag.
    def open(tag)
      @open_tags.unshift(tag) unless void?(tag)
      tag
    # Scans an opening tag in HTML using the passed in StringScanner.
    def open(s)
      s.scan(/</) + push_tag(s.scan(/\w+/)) + s.scan_until(/>|\Z/)
    end

    # Registers closing of a tag.  Returns the tag.
    def close(tag)
      if @open_tags.include?(tag)
        # delete the most recent unclosed tag in our tags stack
        @open_tags.delete_at(@open_tags.index(tag))
    # Scans a closing tag in HTML using the passed in StringScanner.
    def close(s)
      s.scan(/<\//)
      tag = s.scan(/\w+/)
      s.scan(/>/)

      pop_tags(tag).map {|t| "</#{t}>" }.join
    end

    # Registers opening of a tag.  Returns the tag.
    def push_tag(tag)
      @open_tags.push(tag) unless void?(tag)
      tag
    end

@@ -48,22 +41,43 @@ module JsDuck
      @open_tags.include?(tag)
    end

    # Returns HTML for closing the still open tags.
    # Also prints warnings for all the unclosed tags.
    def close_unfinished
      return "" if @open_tags.length == 0
    private

      warn_unfinished
    # Registers closing of a tag.  Returns all the tags that need to
    # be closed at that point.
    def pop_tags(tag)
      if !@open_tags.include?(tag)
        if @ignore_html[tag]
          return [tag]
        else
          warn_unopened(tag)
          return []
        end
      end

      @open_tags.map {|tag| "</#{tag}>" }.join
      popped = []
      begin
        popped << t = @open_tags.pop
        if t != tag
          warn_unclosed(t)
        end
      end until t == tag

    private
      popped
    end

    def warn_unopened(*tags)
      warn("Unopened HTML tag", tags)
    end

    def warn_unclosed(*tags)
      warn("Unclosed HTML tag", tags)
    end

    def warn_unfinished
    def warn(msg, tags)
      ctx = @doc_context
      tag_list = @open_tags.map {|tag| "<#{tag}>" }.join(", ")
      Logger.warn(:html, "Unclosed HTML tag: #{tag_list}", ctx[:filename], ctx[:linenr])
      tag_list = tags.map {|tag| "<#{tag}>" }.join(", ")
      Logger.warn(:html, "#{msg}: #{tag_list}", ctx[:filename], ctx[:linenr])
    end

    def void?(tag)
+13 −0
Original line number Diff line number Diff line
@@ -431,6 +431,19 @@ describe JsDuck::DocFormatter do
      @formatter.format("<img>").should_not =~ /<\/img>/
    end

    it "closes unclosed <b> when closing of <a> is encountered." do
      @formatter.format("<a><b>blah</a>").should =~ Regexp.new("</b></a>")
    end

    it "throws away excessive close tags" do
      @formatter.format("blah</div>").should_not =~ Regexp.new("</div>")
    end

    it "allows for p-s nested inside divs" do
      @formatter.format("<div><small><p>Blah</p><p>Fah</p></small></div>").should =~
             Regexp.new("<div><small><p>Blah</p><p>Fah</p></small></div>")
    end

    shared_examples_for "code blocks" do
      it "contains text before" do
        @html.should =~ /Some code/