diff --git a/lib/hamlit/concerns/indentable.rb b/lib/hamlit/concerns/indentable.rb index 71fc3d57..16ab2b0d 100644 --- a/lib/hamlit/concerns/indentable.rb +++ b/lib/hamlit/concerns/indentable.rb @@ -8,12 +8,14 @@ module Hamlit include Concerns::Error def reset_indent + @indent_logs = [] @current_indent = 0 end # Return nearest line's indent level since next line. This method ignores # empty line. It returns -1 if next_line does not exist. def next_indent + return 1 if !@indent_space && fetch_indent(next_line).length > 0 count_indent(next_line) end @@ -28,19 +30,16 @@ module Hamlit @current_indent -= 1 end - def count_indent(line, strict: false) + def count_indent(line) return EOF unless line - width = count_width(line) + return 0 if indent_rule == 0 - return (width + 1) / 2 unless strict - compile_error!('Expected to count even-width indent') if width.odd? - - width / 2 + line.match(/\A[ \t]+/).to_s.length / indent_rule end def count_width(line) return EOF unless line - line[/\A +/].to_s.length + line[/\A[ \t]+/].to_s.length end def same_indent?(line) @@ -50,31 +49,30 @@ module Hamlit # Validate current line's indentation def validate_indentation!(ast) - width = next_width - return false if width == @current_indent * 2 + return true unless next_line - if width != Hamlit::EOF && (width > @current_indent * 2 || width.odd?) - ast << [:newline] - ast << syntax_error( - "inconsistent indentation: #{2 * @current_indent} spaces used for indentation, "\ - "but the rest of the document was indented using #{width} spaces" - ) + indent = fetch_indent(next_line) + if indent.include?(' ') && indent.include?("\t") + syntax_error!("Indentation can't use both tabs and spaces.") end - true + @indent_logs << indent + + if !@indent_space && @indent_logs.last != '' + @indent_space = @indent_logs.last + end + validate_indentation_consistency!(indent) + + next_indent != @current_indent end # Validate the template is using consitent indentation, 2 spaces or a tab. - def validate_indentation_consistency!(template) - last_indent = '' + def validate_indentation_consistency!(indent) + return false if indent.empty? + return false if !@indent_space || @indent_space.empty? - indents = template.scan(/^[ \t]+/) - indents.each do |indent| - if last_indent.include?(' ') && indent.include?("\t") || - last_indent.include?("\t") && indent.include?(' ') - syntax_error!(%Q{Inconsistent indentation: #{indent_label(indent)} used for indentation, but the rest of the document was indented using #{indent_label(last_indent)}.}) - end - - last_indent = indent + if indent[0] != @indent_space[0] || indent.length < @indent_space.length + syntax_error!("Inconsistent indentation: #{indent_label(indent)} used for indentation, "\ + "but the rest of the document was indented using #{indent_label(@indent_space)}.") end end @@ -87,19 +85,43 @@ module Hamlit "#{length} #{label}#{'s' if length > 1}" end - # Replace hard tabs into 2 spaces - def replace_hard_tabs(template) - lines = [] - template.each_line do |line| - lines << line.gsub(/^\t+/) do |match| - ' ' * (match.length * 2) - end - end - lines.join + def has_block? + return false unless next_line + return fetch_indent(next_line).length > 0 unless @indent_space + + next_indent > @current_indent end - def has_block? - next_indent > @current_indent + private + + def indent_label(indent) + return %Q{"#{indent}"} if indent.include?(' ') && indent.include?("\t") + + label = indent.include?(' ') ? 'space' : 'tab' + length = indent.match(/[ \t]+/).to_s.length + + "#{length} #{label}#{'s' if length > 1}" + end + + def count_width(line) + return EOF unless line + line[/\A +/].to_s.length + end + + def next_space + next_line[/\A +/].to_s + end + + def next_width + count_width(next_line) + end + + def indent_rule + (@indent_space || '').length + end + + def fetch_indent(str) + str.match(/^[ \t]+/).to_s end end end diff --git a/lib/hamlit/concerns/line_reader.rb b/lib/hamlit/concerns/line_reader.rb index 524d3c49..0adedada 100644 --- a/lib/hamlit/concerns/line_reader.rb +++ b/lib/hamlit/concerns/line_reader.rb @@ -25,7 +25,7 @@ module Hamlit end def skip_lines - while next_indent >= @current_indent + while next_line && next_indent >= @current_indent @current_lineno += 1 end end @@ -47,7 +47,8 @@ module Hamlit end def read_line? - return true if count_indent(next_line, strict: false) >= @current_indent + return false unless next_line + return true if next_line.index(/^#{@indent_logs.last}[ \t]/) line = @lines[@current_lineno + 1] return false unless line diff --git a/lib/hamlit/parser.rb b/lib/hamlit/parser.rb index 4ded5906..8e1c4524 100644 --- a/lib/hamlit/parser.rb +++ b/lib/hamlit/parser.rb @@ -38,7 +38,6 @@ module Hamlit # Reset the parser state. def reset(template) validate_indentation_consistency!(template) - template = replace_hard_tabs(template) template = preprocess_multilines(template) reset_lines(template.split("\n")) @@ -62,6 +61,9 @@ module Hamlit ast << [:static, "\n"] unless skip_newline?(node) end ast + rescue => e + ast << syntax_error(e.message) + ast end # Parse current line and return AST. @@ -69,7 +71,7 @@ module Hamlit return [:multi] if empty_line?(line) scanner = wrap_scanner(line) - scanner.scan(/ +/) + scanner.scan(/[ \t]+/) unless inline ast = parse_outer_line(scanner) diff --git a/spec/hamlit/engine/error_spec.rb b/spec/hamlit/engine/error_spec.rb index 64afc327..18968832 100644 --- a/spec/hamlit/engine/error_spec.rb +++ b/spec/hamlit/engine/error_spec.rb @@ -11,16 +11,18 @@ describe Hamlit::Engine do expect { render_string(<<-HAML.unindent) }. %a %b + %a HAML - to raise_error(Hamlit::SyntaxError, 'inconsistent indentation: 2 spaces used for indentation, but the rest of the document was indented using 4 spaces') + to raise_error(Hamlit::SyntaxError, 'Inconsistent indentation: 1 space used for indentation, but the rest of the document was indented using 4 spaces.') end it 'raises syntax error for illegal indentation' do expect { render_string(<<-HAML.unindent) }. %a - %b + \t\t%a + \t%a HAML - to raise_error(Hamlit::SyntaxError, 'inconsistent indentation: 2 spaces used for indentation, but the rest of the document was indented using 1 spaces') + to raise_error(Hamlit::SyntaxError, 'Inconsistent indentation: 1 tab used for indentation, but the rest of the document was indented using 2 tabs.') end it 'raises syntax error which has correct line number in backtrace' do @@ -59,5 +61,13 @@ describe Hamlit::Engine do HAML to raise_error(Hamlit::SyntaxError, 'Unbalanced brackets.') end + + it 'raises syntax error for an inconsistent indentation' do + expect { render_string(<<-HAML.unindent) }. + %p + \t %span + HAML + to raise_error(Hamlit::SyntaxError, "Indentation can't use both tabs and spaces.") + end end end diff --git a/spec/hamlit/engine/indent_spec.rb b/spec/hamlit/engine/indent_spec.rb index ff9b5c0a..2f94f503 100644 --- a/spec/hamlit/engine/indent_spec.rb +++ b/spec/hamlit/engine/indent_spec.rb @@ -10,5 +10,33 @@ describe Hamlit::Engine do

HTML end + + it 'accepts N-space indentation' do + assert_render(<<-HAML, <<-HTML) + %p + %span + foo + HAML +

+ + foo + +

+ HTML + end + + it 'accepts N-tab indentation' do + assert_render(<<-HAML, <<-HTML, compatible_only: :haml) + %p + \t%span + \t\tfoo + HAML +

+ + foo + +

+ HTML + end end end