haml_parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5d8e1da7981dd07cc815bdbc297e870a4ac6112b
4
+ data.tar.gz: 2194786147cc3fd76d711695ab52e94fa4aafdf6
5
+ SHA512:
6
+ metadata.gz: 3e43dcd816b101a1809c61cc64a8bd49e1505cbc8b78df5daa565c377717227b003853fea553a89ed017c1513a5a0fecebe3a7341bb2fa7dbe42c279e0c1f4fe
7
+ data.tar.gz: a9a087e3f0969c6b221cca84234c32cc9d3f7cb7ad4b958144996396996d0551f27783567fca63b56d4078507c55952bb6c2cfe6f32e247dc5b008741ddb3b37
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+ - ruby-head
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in haml_parser.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Kohei Suzuki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,73 @@
1
+ # HamlParser
2
+ [![Gem Version](https://badge.fury.io/rb/haml_parser.svg)](http://badge.fury.io/rb/haml_parser)
3
+ [![Build Status](https://travis-ci.org/eagletmt/haml_parser.svg?branch=master)](https://travis-ci.org/eagletmt/haml_parser)
4
+ [![Coverage Status](https://coveralls.io/repos/eagletmt/haml_parser/badge.svg?branch=master&service=github)](https://coveralls.io/github/eagletmt/haml_parser?branch=master)
5
+ [![Code Climate](https://codeclimate.com/github/eagletmt/haml_parser/badges/gpa.svg)](https://codeclimate.com/github/eagletmt/haml_parser)
6
+
7
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/haml_parser`. To experiment with that code, run `bin/console` for an interactive prompt.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'haml_parser'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install haml_parser
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ parser = HamlParser::Parser.new(filename: 'input.haml')
29
+ ast = parser.call(File.read('input.haml'))
30
+ ```
31
+
32
+ Simple CLI interface is also available.
33
+
34
+ ```
35
+ % cat input.haml
36
+ %p hello world
37
+ % haml_parser input.haml
38
+ #<struct HamlParser::Ast::Root
39
+ children=
40
+ [#<struct HamlParser::Ast::Element
41
+ children=[],
42
+ tag_name="p",
43
+ static_class="",
44
+ static_id="",
45
+ attributes="",
46
+ oneline_child=
47
+ #<struct HamlParser::Ast::Text
48
+ text="hello world",
49
+ escape_html=true,
50
+ filename="in.haml",
51
+ lineno=1>,
52
+ self_closing=false,
53
+ nuke_inner_whitespace=false,
54
+ nuke_outer_whitespace=false,
55
+ filename="in.haml",
56
+ lineno=1>]>
57
+ ```
58
+
59
+ ## Development
60
+
61
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake false` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
62
+
63
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
64
+
65
+ ## Contributing
66
+
67
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eagletmt/haml_parser.
68
+
69
+
70
+ ## License
71
+
72
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
73
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => :spec
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "haml_parser"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'haml_parser/cli'
3
+
4
+ HamlParser::CLI.start(ARGV)
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'haml_parser/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "haml_parser"
8
+ spec.version = HamlParser::VERSION
9
+ spec.authors = ["Kohei Suzuki"]
10
+ spec.email = ["eagletmt@gmail.com"]
11
+
12
+ spec.summary = %q{Parser of Haml template language}
13
+ spec.description = %q{Parser of Haml template language}
14
+ spec.homepage = "https://github.com/eagletmt/haml_parser"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+ spec.required_ruby_version = ">= 2.0.0"
22
+
23
+ spec.add_development_dependency "bundler"
24
+ spec.add_development_dependency "coveralls"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", ">= 3.3.0"
28
+ spec.add_development_dependency "simplecov"
29
+ end
@@ -0,0 +1,5 @@
1
+ require "haml_parser/version"
2
+
3
+ module HamlParser
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,157 @@
1
+ module HamlParser
2
+ module Ast
3
+ module HasChildren
4
+ def initialize(*)
5
+ super
6
+ self.children ||= []
7
+ end
8
+
9
+ def <<(ast)
10
+ self.children << ast
11
+ end
12
+
13
+ def to_h
14
+ super.merge(children: children.map(&:to_h))
15
+ end
16
+ end
17
+
18
+ class Root < Struct.new(:children)
19
+ include HasChildren
20
+
21
+ def to_h
22
+ super.merge(type: 'root')
23
+ end
24
+ end
25
+
26
+ class Doctype < Struct.new(:doctype, :filename, :lineno)
27
+ def to_h
28
+ super.merge(type: 'doctype')
29
+ end
30
+ end
31
+
32
+ class Element < Struct.new(
33
+ :children,
34
+ :tag_name,
35
+ :static_class,
36
+ :static_id,
37
+ :attributes,
38
+ :oneline_child,
39
+ :self_closing,
40
+ :nuke_inner_whitespace,
41
+ :nuke_outer_whitespace,
42
+ :filename,
43
+ :lineno,
44
+ )
45
+ include HasChildren
46
+
47
+ def initialize(*)
48
+ super
49
+ self.static_class ||= ''
50
+ self.static_id ||= ''
51
+ self.attributes ||= ''
52
+ self.self_closing ||= false
53
+ self.nuke_inner_whitespace ||= false
54
+ self.nuke_outer_whitespace ||= false
55
+ end
56
+
57
+ def to_h
58
+ super.merge(
59
+ type: 'element',
60
+ oneline_child: oneline_child && oneline_child.to_h,
61
+ )
62
+ end
63
+ end
64
+
65
+ class Script < Struct.new(
66
+ :children,
67
+ :script,
68
+ :escape_html,
69
+ :preserve,
70
+ :filename,
71
+ :lineno,
72
+ )
73
+ include HasChildren
74
+
75
+ def initialize(*)
76
+ super
77
+ if self.escape_html.nil?
78
+ self.escape_html = true
79
+ end
80
+ if self.preserve.nil?
81
+ self.preserve = false
82
+ end
83
+ end
84
+
85
+ def to_h
86
+ super.merge(type: 'script')
87
+ end
88
+ end
89
+
90
+ class SilentScript < Struct.new(:children, :script, :mid_block_keyword, :filename, :lineno)
91
+ include HasChildren
92
+
93
+ def initialize(*)
94
+ super
95
+ if self.mid_block_keyword.nil?
96
+ self.mid_block_keyword = false
97
+ end
98
+ end
99
+
100
+ def to_h
101
+ super.merge(type: 'silent_script')
102
+ end
103
+ end
104
+
105
+ class HtmlComment < Struct.new(:children, :comment, :conditional, :filename, :lineno)
106
+ include HasChildren
107
+
108
+ def initialize(*)
109
+ super
110
+ self.comment ||= ''
111
+ self.conditional ||= ''
112
+ end
113
+
114
+ def to_h
115
+ super.merge(type: 'html_comment')
116
+ end
117
+ end
118
+
119
+ class HamlComment < Struct.new(:children, :filename, :lineno)
120
+ include HasChildren
121
+
122
+ def to_h
123
+ super.merge(type: 'haml_comment')
124
+ end
125
+ end
126
+
127
+ class Text < Struct.new(:text, :escape_html, :filename, :lineno)
128
+ def initialize(*)
129
+ super
130
+ if self.escape_html.nil?
131
+ self.escape_html = true
132
+ end
133
+ end
134
+
135
+ def to_h
136
+ super.merge(type: 'text')
137
+ end
138
+ end
139
+
140
+ class Filter < Struct.new(:name, :texts, :filename, :lineno)
141
+ def initialize(*)
142
+ super
143
+ self.texts ||= []
144
+ end
145
+
146
+ def to_h
147
+ super.merge(type: 'filter')
148
+ end
149
+ end
150
+
151
+ class Empty < Struct.new(:filename, :lineno)
152
+ def to_h
153
+ super.merge(type: 'empty')
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,44 @@
1
+ require 'optparse'
2
+
3
+ module HamlParser
4
+ class CLI
5
+ def self.start(argv)
6
+ new.start(argv)
7
+ end
8
+
9
+ def start(argv)
10
+ formatter = 'pretty'
11
+ OptionParser.new.tap do |parser|
12
+ parser.version = VERSION
13
+ parser.on('-f FORMAT', '--format FORMAT', 'Select formatter') { |v| formatter = v }
14
+ end.parse!(argv)
15
+
16
+ require 'haml_parser/parser'
17
+ argv.each do |file|
18
+ format(parse_file(file), formatter)
19
+ end
20
+ end
21
+
22
+ def parse_file(file)
23
+ HamlParser::Parser.new(filename: file).call(File.read(file))
24
+ end
25
+
26
+ private
27
+
28
+ def format(ast, formatter)
29
+ case formatter
30
+ when 'pretty'
31
+ require 'pp'
32
+ pp ast
33
+ when 'pry'
34
+ require 'pry'
35
+ Pry::ColorPrinter.pp ast
36
+ when 'json'
37
+ require 'json'
38
+ puts JSON.generate(ast.to_h)
39
+ else
40
+ abort "Unknown formatter: #{formatter}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,235 @@
1
+ require 'strscan'
2
+ require 'haml_parser/ast'
3
+ require 'haml_parser/utils'
4
+ require 'haml_parser/ruby_multiline'
5
+ require 'haml_parser/script_parser'
6
+ require 'haml_parser/error'
7
+
8
+ module HamlParser
9
+ class ElementParser
10
+ def initialize(line_parser)
11
+ @line_parser = line_parser
12
+ end
13
+
14
+ ELEMENT_REGEXP = /\A%([-:\w]+)([-:\w.#]*)(.+)?\z/o
15
+
16
+ def parse(text)
17
+ m = text.match(ELEMENT_REGEXP)
18
+ unless m
19
+ syntax_error!('Invalid element declaration')
20
+ end
21
+
22
+ element = Ast::Element.new
23
+ element.filename = @line_parser.filename
24
+ element.lineno = @line_parser.lineno
25
+ element.tag_name = m[1]
26
+ element.static_class, element.static_id = parse_class_and_id(m[2])
27
+ rest = m[3] || ''
28
+
29
+ element.attributes, rest = parse_attributes(rest)
30
+ element.nuke_inner_whitespace, element.nuke_outer_whitespace, rest = parse_nuke_whitespace(rest)
31
+ element.self_closing, rest = parse_self_closing(rest)
32
+ element.oneline_child = ScriptParser.new(@line_parser).parse(rest)
33
+
34
+ element
35
+ end
36
+
37
+ private
38
+
39
+ def parse_class_and_id(class_and_id)
40
+ classes = []
41
+ id = ''
42
+ scanner = StringScanner.new(class_and_id)
43
+ until scanner.eos?
44
+ unless scanner.scan(/([#.])([-:_a-zA-Z0-9]+)/)
45
+ syntax_error!('Illegal element: classes and ids must have values.')
46
+ end
47
+ case scanner[1]
48
+ when '.'
49
+ classes << scanner[2]
50
+ when '#'
51
+ id = scanner[2]
52
+ end
53
+ end
54
+
55
+ [classes.join(' '), id]
56
+ end
57
+
58
+ OLD_ATTRIBUTE_BEGIN = '{'
59
+ NEW_ATTRIBUTE_BEGIN = '('
60
+
61
+ def parse_attributes(rest)
62
+ old_attributes = ''
63
+ new_attributes = ''
64
+
65
+ loop do
66
+ case rest[0]
67
+ when OLD_ATTRIBUTE_BEGIN
68
+ unless old_attributes.empty?
69
+ break
70
+ end
71
+ old_attributes, rest = parse_old_attributes(rest)
72
+ when NEW_ATTRIBUTE_BEGIN
73
+ unless new_attributes.empty?
74
+ break
75
+ end
76
+ new_attributes, rest = parse_new_attributes(rest)
77
+ else
78
+ break
79
+ end
80
+ end
81
+
82
+ attributes = old_attributes
83
+ unless new_attributes.empty?
84
+ if attributes.empty?
85
+ attributes = new_attributes
86
+ else
87
+ attributes << ", " << new_attributes
88
+ end
89
+ end
90
+ [attributes, rest]
91
+ end
92
+
93
+ def parse_old_attributes(text)
94
+ text = text.dup
95
+ s = StringScanner.new(text)
96
+ s.pos = 1
97
+ depth = 1
98
+ loop do
99
+ depth = Utils.balance(s, '{', '}', depth)
100
+ if depth == 0
101
+ attr = s.pre_match + s.matched
102
+ return [attr[1, attr.size-2], s.rest]
103
+ else
104
+ if /,\s*\z/ === text && @line_parser.has_next?
105
+ text << "\n" << @line_parser.next_line
106
+ else
107
+ syntax_error!('Unmatched brace')
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def parse_new_attributes(text)
114
+ text = text.dup
115
+ s = StringScanner.new(text)
116
+ s.pos = 1
117
+ depth = 1
118
+ loop do
119
+ pre_pos = s.pos
120
+ depth = Utils.balance(s, '(', ')', depth)
121
+ if depth == 0
122
+ t = s.string.byteslice(pre_pos ... s.pos-1)
123
+ return [parse_new_attribute_list(t), s.rest]
124
+ else
125
+ if @line_parser.has_next?
126
+ text << "\n" << @line_parser.next_line
127
+ else
128
+ syntax_error!('Unmatched paren')
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def parse_new_attribute_list(text)
135
+ s = StringScanner.new(text)
136
+ attributes = []
137
+ until s.eos?
138
+ name = scan_key(s)
139
+ s.skip(/\s*/)
140
+
141
+ if scan_operator(s)
142
+ s.skip(/\s*/)
143
+ value = scan_value(s)
144
+ else
145
+ value = 'true'
146
+ end
147
+ spaces = s.scan(/\s*/)
148
+ line_count = spaces.count("\n")
149
+
150
+ attributes << "#{name.inspect} => #{value},#{"\n" * line_count}"
151
+ end
152
+ attributes.join
153
+ end
154
+
155
+ def scan_key(scanner)
156
+ scanner.scan(/[-:\w]+/).tap do |name|
157
+ unless name
158
+ syntax_error!('Invalid attribute list (missing attribute name)')
159
+ end
160
+ end
161
+ end
162
+
163
+ def scan_operator(scanner)
164
+ scanner.skip(/=/)
165
+ end
166
+
167
+ def scan_value(scanner)
168
+ if quote = scanner.scan(/["']/)
169
+ scan_quoted_value(scanner, quote)
170
+ else
171
+ scan_variable_value(scanner)
172
+ end
173
+ end
174
+
175
+ def scan_quoted_value(scanner, quote)
176
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
177
+ pos = scanner.pos
178
+ loop do
179
+ unless scanner.scan(re)
180
+ syntax_error!('Invalid attribute list (mismatched quotation)')
181
+ end
182
+ if scanner[2] == quote
183
+ break
184
+ end
185
+ depth = Utils.balance(scanner, '{', '}')
186
+ if depth != 0
187
+ syntax_error!('Invalid attribute list (mismatched interpolation)')
188
+ end
189
+ end
190
+ str = scanner.string.byteslice(pos-1 .. scanner.pos-1)
191
+
192
+ # Even if the quote is single, string interpolation is performed in Haml.
193
+ str[0] = '"'
194
+ str[-1] = '"'
195
+ str
196
+ end
197
+
198
+ def scan_variable_value(scanner)
199
+ scanner.scan(/(@@?|\$)?\w+/).tap do |var|
200
+ unless var
201
+ syntax_error!('Invalid attribute list (invalid variable name)')
202
+ end
203
+ end
204
+ end
205
+
206
+ def parse_nuke_whitespace(rest)
207
+ m = rest.match(/\A(><|<>|[><])(.*)\z/)
208
+ if m
209
+ nuke_whitespace = m[1]
210
+ [
211
+ nuke_whitespace.include?('<'),
212
+ nuke_whitespace.include?('>'),
213
+ m[2],
214
+ ]
215
+ else
216
+ [false, false, rest]
217
+ end
218
+ end
219
+
220
+ def parse_self_closing(rest)
221
+ if rest[0] == '/'
222
+ if rest.size > 1
223
+ syntax_error!("Self-closing tags can't have content")
224
+ end
225
+ [true, '']
226
+ else
227
+ [false, rest]
228
+ end
229
+ end
230
+
231
+ def syntax_error!(message)
232
+ raise Error.new(message, @line_parser.lineno)
233
+ end
234
+ end
235
+ end