Skip to content

Instantly share code, notes, and snippets.

@snipsnipsnip
Created November 26, 2015 11:36
Show Gist options
  • Save snipsnipsnip/9c14aa2f41375e086a15 to your computer and use it in GitHub Desktop.
Save snipsnipsnip/9c14aa2f41375e086a15 to your computer and use it in GitHub Desktop.
tom.rb: primitive JSP lint
# coding: utf-8
#
# tom: JSPのタグ開閉の対応チェッカ
# MIT license, @snipsnipsnip
#
=begin
JSPの文法は普通のHTMLと違ってかなりゆるい。
・特殊なタグがある(<%%>とか<s:text>とか)。
後者はXMLのネームスペース記法のように見えるが、実際taglibという特殊タグがファイルの頭に書いてあればルートノードにxmlns属性がなくても動くのでたぶん別物。
・タグ属性内にタグを書くことができ、その中ではダブルクォートを使って良い。<hoge fuga="<foo bar="baz"/>">
・タグ属性内に展開式(OGNL)を書くことができ、その中ではダブルクォートを使って良い。 <hoge fuga="%{"hoge"}">
・タグ属性内に式(OGNL)をそのまま書くことができ、その中では大なり小なりを使って良い。 <s:if test="foo > 3">
・HTMLタグ内にタグを書くこともできる。 <hoge <fuga/> >
・通常のコメントと別にJSPのコメントがある。 <%-- foo --%>
・タグの開きと閉じが対応する必要はない。
<s:if>,<s:elseif>,<s:else>といったタグの中に開きタグがある場合、出力されるのはどれか一つなので、その出力後のHTMLがつりあいがとれていればブラウザで表示ができる。
これは対応がめんどいので普通にエラー扱いにする。
=end
require 'strscan'
class Tom
SELF_CLOSING_TAGS = /\A(?:meta|br)\z/i
def initialize(str)
@s = StringScanner.new(str)
@stack = []
@line = 1
end
def check
s = @s
stack = @stack
until eos?
skip(/\s*/)
case state
when :double_quote
skip /[^"<]*/
case
when skip(/"/)
stack.pop
when skip(/<[a-z:]+/i)
stack.push s[0][1..-1], '<'
when skip(/</)
when !eos?
unknown_pattern
end
when :single_quote
skip /[^'<]*/
case
when skip(/'/)
stack.pop
when skip(/<[a-z:]+/i)
stack.push s[0][1..-1], '<'
when skip(/</)
when !eos?
unknown_pattern
end
when :tag
skip(/[\-\s=a-z]*/i)
case
when skip(/<[a-z:]+/i)
stack.push s[0][1..-1], '<'
when skip(/\/>/)
stack.pop(2)
when skip(/>/)
if stack[-2] =~ SELF_CLOSING_TAGS
stack.pop(2)
else
stack.pop
end
when skip(/"/)
stack.push '"'
when skip(/'/)
stack.push "'"
when !eos?
unknown_pattern
end
when :comment
skip(/[^\-]*/)
if skip(/-->/)
stack.pop
elsif skip(/-/)
elsif !eos?
unknown_pattern
end
when :script
skip(/[^<]*/)
if skip(/<\/script>/)
stack.pop
elsif skip(/</)
elsif !eos?
unknown_pattern
end
when :style
skip(/[^<]*/)
if skip(/<\/style>/)
stack.pop
elsif skip(/</)
elsif !eos?
unknown_pattern
end
when :jsp_tag
skip(/[^%]*/)
if skip(/%>/)
stack.pop
elsif skip(/%/)
elsif !eos?
unknown_pattern
end
when :body
skip(/[^<>]*/)
case
when skip(/<!--/)
stack.push '<!--'
when skip(/<%/)
stack.push '<%'
when skip(/<[a-z:]+/i)
stack.push s[0][1..-1], '<'
when skip(/<\/([a-z:]+)\s*>/i)
if s[1] == stack.last
stack.pop
else
raise "#{s[1]} を閉じようとしましたが #{stack.last} が閉じていません (#{@line}行目)\n#{@s.peek(30).inspect}.."
end
when !eos?
unknown_pattern
end
when :root
skip(/[^<>]*/)
if skip(/<%/)
stack.push '<%'
elsif skip(/<[^\s>]+/)
stack.push s[0][1..-1], '<'
elsif !eos?
unknown_pattern
end
else
raise "unexpected state: #{@stack.last} (#{@line}行目)"
end
end
unless stack.empty?
raise "閉じ括弧が足りません: #{stack.inspect}"
end
end
private
def unknown_pattern
raise "unknown pattern: #{@s.peek(30).inspect}.. (#{@line}行目)"
end
def eos?
@s.eos?
end
def skip(pat)
if r = @s.skip(pat)
@line += @s[0].count("\n")
end
r
end
def state
case @stack.last
when /\A'\z/
:single_quote
when /\A"\z/
:double_quote
when /\A<\z/
:tag
when /\A<%\z/
:jsp_tag
when /\A<!--\z/
:comment
when /\Astyle\z/i
:style
when /\Ascript\z/i
:script
when /\A[a-z:]+\z/i
:body
when nil
:root
else
:wtf
end
end
def self.main
ARGV.flat_map {|a| Dir[a] }.each do |a|
puts File.basename(a)
Tom.new(File.read(a, encoding: 'utf-8')).check
end
end
end
Tom.main if __FILE__ == $0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment