hamlet 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/hamlet.gemspec CHANGED
@@ -4,7 +4,7 @@ require 'date'
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = 'hamlet'
7
- s.version = 0.1
7
+ s.version = 0.2
8
8
  s.date = Date.today.to_s
9
9
  s.authors = ['Greg Weber']
10
10
  s.email = ['greg@gregweber.info']
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = %w(lib)
21
21
 
22
- s.add_dependency('slim', ['~> 1.0'])
22
+ s.add_dependency('slim', ['~> 1.0.0'])
23
23
 
24
24
  s.add_development_dependency('rake', ['>= 0.8.7'])
25
25
  s.add_development_dependency('sass', ['>= 3.1.0'])
@@ -0,0 +1,448 @@
1
+ module ForkedSlim
2
+ # Parses Slim code and transforms it to a Temple expression
3
+ # @api private
4
+ class Parser
5
+ include Temple::Mixins::Options
6
+
7
+ set_default_options :tabsize => 4,
8
+ :encoding => 'utf-8',
9
+ :default_tag => 'div'
10
+
11
+ class SyntaxError < StandardError
12
+ attr_reader :error, :file, :line, :lineno, :column
13
+
14
+ def initialize(error, file, line, lineno, column)
15
+ @error = error
16
+ @file = file || '(__TEMPLATE__)'
17
+ @line = line.to_s
18
+ @lineno = lineno
19
+ @column = column
20
+ end
21
+
22
+ def to_s
23
+ line = @line.strip
24
+ column = @column + line.size - @line.size
25
+ %{#{error}
26
+ #{file}, Line #{lineno}
27
+ #{line}
28
+ #{' ' * column}^
29
+ }
30
+ end
31
+ end
32
+
33
+ def initialize(options = {})
34
+ super
35
+ @tab = ' ' * @options[:tabsize]
36
+ end
37
+
38
+ # Compile string to Temple expression
39
+ #
40
+ # @param [String] str Slim code
41
+ # @return [Array] Temple expression representing the code]]
42
+ def call(str)
43
+ # Set string encoding if option is set
44
+ if options[:encoding] && str.respond_to?(:encoding)
45
+ old = str.encoding
46
+ str = str.dup if str.frozen?
47
+ str.force_encoding(options[:encoding])
48
+ # Fall back to old encoding if new encoding is invalid
49
+ str.force_encoding(old_enc) unless str.valid_encoding?
50
+ end
51
+
52
+ result = [:multi]
53
+ reset(str.split($/), [result])
54
+
55
+ parse_line while next_line
56
+
57
+ reset
58
+ result
59
+ end
60
+
61
+ private
62
+
63
+ DELIMITERS = {
64
+ '(' => ')',
65
+ '[' => ']',
66
+ '{' => '}',
67
+ }.freeze
68
+
69
+ ATTR_SHORTCUT = {
70
+ '#' => 'id',
71
+ '.' => 'class',
72
+ }.freeze
73
+
74
+ DELIMITER_REGEX = /\A[\(\[\{]/
75
+ ATTR_NAME_REGEX = '\A\s*(\w[:\w-]*)'
76
+
77
+ if RUBY_VERSION > '1.9'
78
+ CLASS_ID_REGEX = /\A(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
79
+ else
80
+ CLASS_ID_REGEX = /\A(#|\.)(\w[\w:-]*)/
81
+ end
82
+
83
+ def reset(lines = nil, stacks = nil)
84
+ # Since you can indent however you like in Slim, we need to keep a list
85
+ # of how deeply indented you are. For instance, in a template like this:
86
+ #
87
+ # doctype # 0 spaces
88
+ # html # 0 spaces
89
+ # head # 1 space
90
+ # title # 4 spaces
91
+ #
92
+ # indents will then contain [0, 1, 4] (when it's processing the last line.)
93
+ #
94
+ # We uses this information to figure out how many steps we must "jump"
95
+ # out when we see an de-indented line.
96
+ @indents = [0]
97
+
98
+ # Whenever we want to output something, we'll *always* output it to the
99
+ # last stack in this array. So when there's a line that expects
100
+ # indentation, we simply push a new stack onto this array. When it
101
+ # processes the next line, the content will then be outputted into that
102
+ # stack.
103
+ @stacks = stacks
104
+
105
+ @lineno = 0
106
+ @lines = lines
107
+ @line = @orig_line = nil
108
+ end
109
+
110
+ def next_line
111
+ if @lines.empty?
112
+ @orig_line = @line = nil
113
+ else
114
+ @orig_line = @lines.shift
115
+ @lineno += 1
116
+ @line = @orig_line.dup
117
+ end
118
+ end
119
+
120
+ def get_indent(line)
121
+ # Figure out the indentation. Kinda ugly/slow way to support tabs,
122
+ # but remember that this is only done at parsing time.
123
+ line[/\A[ \t]*/].gsub("\t", @tab).size
124
+ end
125
+
126
+ def parse_line
127
+ if @line =~ /\A\s*\Z/
128
+ @stacks.last << [:newline]
129
+ return
130
+ end
131
+
132
+ indent = get_indent(@line)
133
+
134
+ # Remove the indentation
135
+ @line.lstrip!
136
+
137
+ # If there's more stacks than indents, it means that the previous
138
+ # line is expecting this line to be indented.
139
+ expecting_indentation = @stacks.size > @indents.size
140
+
141
+ if indent > @indents.last
142
+ # This line was actually indented, so we'll have to check if it was
143
+ # supposed to be indented or not.
144
+ syntax_error!('Unexpected indentation') unless expecting_indentation
145
+
146
+ @indents << indent
147
+ else
148
+ # This line was *not* indented more than the line before,
149
+ # so we'll just forget about the stack that the previous line pushed.
150
+ @stacks.pop if expecting_indentation
151
+
152
+ # This line was deindented.
153
+ # Now we're have to go through the all the indents and figure out
154
+ # how many levels we've deindented.
155
+ while indent < @indents.last
156
+ @indents.pop
157
+ @stacks.pop
158
+ end
159
+
160
+ # This line's indentation happens lie "between" two other line's
161
+ # indentation:
162
+ #
163
+ # hello
164
+ # world
165
+ # this # <- This should not be possible!
166
+ syntax_error!('Malformed indentation') if indent != @indents.last
167
+ end
168
+
169
+ parse_line_indicators
170
+ end
171
+
172
+ def parse_line_indicators
173
+ case @line
174
+ when /\A\//
175
+ # Found a comment block.
176
+ if @line =~ %r{\A/!( ?)(.*)\Z}
177
+ # HTML comment
178
+ block = [:multi]
179
+ @stacks.last << [:html, :comment, block]
180
+ @stacks << block
181
+ @stacks.last << [:slim, :interpolate, $2] unless $2.empty?
182
+ parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2)
183
+ elsif @line =~ %r{\A/\[\s*(.*?)\s*\]\s*\Z}
184
+ # HTML conditional comment
185
+ block = [:multi]
186
+ @stacks.last << [:slim, :condcomment, $1, block]
187
+ @stacks << block
188
+ else
189
+ # Slim comment
190
+ parse_comment_block
191
+ end
192
+ when /\A-/
193
+ # Found a code block.
194
+ # We expect the line to be broken or the next line to be indented.
195
+ block = [:multi]
196
+ @line.slice!(0)
197
+ @stacks.last << [:slim, :control, parse_broken_line, block]
198
+ @stacks << block
199
+ when /\A=/
200
+ # Found an output block.
201
+ # We expect the line to be broken or the next line to be indented.
202
+ @line =~ /\A=(=?)('?)/
203
+ @line = $'
204
+ block = [:multi]
205
+ @stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
206
+ @stacks.last << [:static, ' '] unless $2.empty?
207
+ @stacks << block
208
+ when /\A<(\w+):\s*\Z/
209
+ # Embedded template detected. It is treated as block.
210
+ block = [:multi]
211
+ @stacks.last << [:newline] << [:slim, :embedded, $1, block]
212
+ @stacks << block
213
+ parse_text_block
214
+ return # Don't append newline, this has already been done before
215
+ when /\Adoctype\s+/i
216
+ # Found doctype declaration
217
+ @stacks.last << [:html, :doctype, $'.strip]
218
+ when /\A<([#\.]|\w[:\w-]*)/
219
+ # Found a HTML tag.
220
+ parse_tag($1)
221
+ when /\A(> *)?(.*)?\Z/
222
+ # Found a text block.
223
+ trailing_ws = !$1
224
+ @stacks.last << [:slim, :interpolate, $2] unless $2.empty?
225
+ parse_text_block($2.empty? ? nil : @indents.last + $1.to_s.size)
226
+ @stacks.last << [:static, ' '] if trailing_ws
227
+ else
228
+ syntax_error! 'Unknown line indicator'
229
+ end
230
+ @stacks.last << [:newline]
231
+ end
232
+
233
+ def parse_comment_block
234
+ while !@lines.empty? && (@lines.first =~ /\A\s*\Z/ || get_indent(@lines.first) > @indents.last)
235
+ next_line
236
+ @stacks.last << [:newline]
237
+ end
238
+ end
239
+
240
+ def parse_text_block(text_indent = nil)
241
+ empty_lines = 0
242
+ until @lines.empty?
243
+ if @lines.first =~ /\A\s*\Z/
244
+ next_line
245
+ @stacks.last << [:newline]
246
+ empty_lines += 1 if text_indent
247
+ else
248
+ indent = get_indent(@lines.first)
249
+ break if indent <= @indents.last
250
+
251
+ if empty_lines > 0
252
+ @stacks.last << [:slim, :interpolate, "\n" * empty_lines]
253
+ empty_lines = 0
254
+ end
255
+
256
+ next_line
257
+
258
+ # The text block lines must be at least indented
259
+ # as deep as the first line.
260
+ if text_indent && indent < text_indent
261
+ @line.lstrip!
262
+ syntax_error!('Unexpected text indentation')
263
+ end
264
+
265
+ @line.slice!(0, text_indent || indent)
266
+ @stacks.last << [:slim, :interpolate, (text_indent ? "\n" : '') + @line] << [:newline]
267
+
268
+ # The indentation of first line of the text block
269
+ # determines the text base indentation.
270
+ text_indent ||= indent
271
+ end
272
+ end
273
+ end
274
+
275
+ def parse_broken_line
276
+ broken_line = @line.strip
277
+ while broken_line[-1] == ?\\
278
+ next_line || syntax_error!('Unexpected end of file')
279
+ broken_line << "\n" << @line.strip
280
+ end
281
+ broken_line
282
+ end
283
+
284
+ def parse_tag(tag)
285
+ @line.slice!(0,1) # get rid of leading '<'
286
+ if tag == '#' || tag == '.'
287
+ tag = options[:default_tag]
288
+ else
289
+ @line.slice!(0, tag.size)
290
+ end
291
+
292
+ tag = [:html, :tag, tag, parse_attributes]
293
+ @stacks.last << tag
294
+
295
+ case @line
296
+ when /\A\s*>?=(=?)('?)/
297
+ # Handle output code
298
+ block = [:multi]
299
+ @line = $'
300
+ content = [:slim, :output, $1 != '=', parse_broken_line, block]
301
+ tag << content
302
+ @stacks.last << [:static, ' '] unless $2.empty?
303
+ @stacks << block
304
+ when /\A\s*\//
305
+ # Closed tag. Do nothing
306
+ when /\A\s*>?\s*\Z/
307
+ # Empty content
308
+ content = [:multi]
309
+ tag << content
310
+ @stacks << content
311
+ when /\A( ?)>?(.*)\Z/
312
+ # Text content
313
+ content = [:multi, [:slim, :interpolate, $2]]
314
+ tag << content
315
+ @stacks << content
316
+ parse_text_block(@orig_line.size - @line.size + $1.size)
317
+ end
318
+ end
319
+
320
+ def parse_attributes
321
+ attributes = [:html, :attrs]
322
+
323
+ # Find any literal class/id attributes
324
+ while @line =~ CLASS_ID_REGEX
325
+ # The class/id attribute is :static instead of :slim :text,
326
+ # because we don't want text interpolation in .class or #id shortcut
327
+ attributes << [:html, :attr, ATTR_SHORTCUT[$1], [:static, $2]]
328
+ @line = $'
329
+ end
330
+
331
+ # Check to see if there is a delimiter right after the tag name
332
+ delimiter = nil
333
+ if @line =~ DELIMITER_REGEX
334
+ delimiter = DELIMITERS[$&]
335
+ @line.slice!(0)
336
+ end
337
+
338
+ orig_line = @orig_line
339
+ lineno = @lineno
340
+ while true
341
+ # Parse attributes
342
+ attr_regex = delimiter ? /#{ATTR_NAME_REGEX}(=|\s|(?=#{Regexp.escape delimiter}))/ : /#{ATTR_NAME_REGEX}=/
343
+ while @line =~ attr_regex
344
+ @line = $'
345
+ name = $1
346
+ if delimiter && $2 != '='
347
+ attributes << [:slim, :attr, name, false, 'true']
348
+ elsif @line =~ /\A["']/
349
+ # Value is quoted (static)
350
+ @line = $'
351
+ attributes << [:html, :attr, name, [:slim, :interpolate, parse_quoted_attribute($&)]]
352
+ else
353
+ # Value is ruby code
354
+ escape = @line[0] != ?=
355
+ @line.slice!(0) unless escape
356
+ attributes << [:slim, :attr, name, escape, parse_ruby_attribute(delimiter)]
357
+ end
358
+ end
359
+
360
+ # No ending delimiter, attribute end
361
+ break unless delimiter
362
+
363
+ # Find ending delimiter
364
+ if @line =~ /\A\s*#{Regexp.escape delimiter}/
365
+ @line = $'
366
+ break
367
+ end
368
+
369
+ # Found something where an attribute should be
370
+ @line.lstrip!
371
+ syntax_error!('Expected attribute') unless @line.empty?
372
+
373
+ # Attributes span multiple lines
374
+ @stacks.last << [:newline]
375
+ next_line || syntax_error!("Expected closing delimiter #{delimiter}",
376
+ :orig_line => orig_line,
377
+ :lineno => lineno,
378
+ :column => orig_line.size)
379
+ end
380
+
381
+ attributes
382
+ end
383
+
384
+ def parse_ruby_attribute(outer_delimiter)
385
+ value, count, delimiter, close_delimiter = '', 0, nil, nil
386
+
387
+ # Attribute ends with space or attribute delimiter
388
+ end_regex = /\A[\s#{Regexp.escape outer_delimiter.to_s}]/
389
+
390
+ until @line.empty? || (count == 0 && @line =~ end_regex)
391
+ if count > 0
392
+ if @line[0] == delimiter[0]
393
+ count += 1
394
+ elsif @line[0] == close_delimiter[0]
395
+ count -= 1
396
+ end
397
+ elsif @line =~ DELIMITER_REGEX
398
+ count = 1
399
+ delimiter, close_delimiter = $&, DELIMITERS[$&]
400
+ end
401
+ value << @line.slice!(0)
402
+ end
403
+
404
+ syntax_error!("Expected closing attribute delimiter #{close_delimiter}") if count != 0
405
+ syntax_error!('Invalid empty attribute') if value.empty?
406
+
407
+ # Remove attribute wrapper which doesn't belong to the ruby code
408
+ # e.g id=[hash[:a] + hash[:b]]
409
+ value = value[1..-2] if value =~ DELIMITER_REGEX &&
410
+ DELIMITERS[$&] == value[-1, 1]
411
+
412
+ value
413
+ end
414
+
415
+ def parse_quoted_attribute(quote)
416
+ value, count = '', 0
417
+
418
+ until @line.empty? || (count == 0 && @line[0] == quote[0])
419
+ if count > 0
420
+ if @line[0] == ?{
421
+ count += 1
422
+ elsif @line[0] == ?}
423
+ count -= 1
424
+ end
425
+ elsif @line =~ /\A#\{/
426
+ value << @line.slice!(0)
427
+ count = 1
428
+ end
429
+ value << @line.slice!(0)
430
+ end
431
+
432
+ syntax_error!("Expected closing brace }") if count != 0
433
+ @line.slice!(0)
434
+ value
435
+ end
436
+
437
+ # Helper for raising exceptions
438
+ def syntax_error!(message, args = {})
439
+ args[:orig_line] ||= @orig_line
440
+ args[:line] ||= @line
441
+ args[:lineno] ||= @lineno
442
+ args[:column] ||= args[:orig_line] && args[:line] ?
443
+ args[:orig_line].size - args[:line].size : 0
444
+ raise SyntaxError.new(message, options[:file],
445
+ args[:orig_line], args[:lineno], args[:column])
446
+ end
447
+ end
448
+ end
data/lib/hamlet/parser.rb CHANGED
@@ -1,8 +1,8 @@
1
- require 'slim/parser'
1
+ require 'hamlet/forked_slim_parser'
2
2
 
3
3
  # @api private
4
4
  module Hamlet
5
- class Parser < Slim::Parser
5
+ class Parser < ForkedSlim::Parser
6
6
  private
7
7
 
8
8
  def parse_line_indicators
data/lib/hamlet.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'rubygems'
2
-
3
1
  require 'slim'
4
2
 
5
3
  require 'hamlet/parser'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hamlet
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,18 +13,18 @@ date: 2011-10-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: slim
16
- requirement: &4263040 !ruby/object:Gem::Requirement
16
+ requirement: &3486700 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '1.0'
21
+ version: 1.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *4263040
24
+ version_requirements: *3486700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &4261800 !ruby/object:Gem::Requirement
27
+ requirement: &3485880 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.8.7
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *4261800
35
+ version_requirements: *3485880
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sass
38
- requirement: &4260580 !ruby/object:Gem::Requirement
38
+ requirement: &3484300 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 3.1.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *4260580
46
+ version_requirements: *3484300
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: minitest
49
- requirement: &4259280 !ruby/object:Gem::Requirement
49
+ requirement: &3482000 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *4259280
57
+ version_requirements: *3482000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: kramdown
60
- requirement: &4258240 !ruby/object:Gem::Requirement
60
+ requirement: &3481000 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *4258240
68
+ version_requirements: *3481000
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard
71
- requirement: &4257160 !ruby/object:Gem::Requirement
71
+ requirement: &3442840 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *4257160
79
+ version_requirements: *3442840
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: creole
82
- requirement: &4253900 !ruby/object:Gem::Requirement
82
+ requirement: &3441240 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *4253900
90
+ version_requirements: *3441240
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: builder
93
- requirement: &4253060 !ruby/object:Gem::Requirement
93
+ requirement: &3440020 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *4253060
101
+ version_requirements: *3440020
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: pry
104
- requirement: &4252560 !ruby/object:Gem::Requirement
104
+ requirement: &3438600 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: '0'
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *4252560
112
+ version_requirements: *3438600
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: rcov
115
- requirement: &4251980 !ruby/object:Gem::Requirement
115
+ requirement: &3435800 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ! '>='
@@ -120,7 +120,7 @@ dependencies:
120
120
  version: '0'
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *4251980
123
+ version_requirements: *3435800
124
124
  description: Hamlet is a template language whose goal is reduce HTML syntax to the
125
125
  essential parts.
126
126
  email:
@@ -142,6 +142,7 @@ files:
142
142
  - hamlet.gemspec
143
143
  - lib/hamlet.rb
144
144
  - lib/hamlet/engine.rb
145
+ - lib/hamlet/forked_slim_parser.rb
145
146
  - lib/hamlet/parser.rb
146
147
  - lib/hamlet/template.rb
147
148
  - test/rails/Rakefile