prawn-format 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Manifest +37 -0
- data/Rakefile +31 -0
- data/examples/basic-formatting.rb +37 -0
- data/examples/christmas-carol.txt +717 -0
- data/examples/document.rb +61 -0
- data/examples/flowing.rb +24 -0
- data/examples/style-classes.rb +12 -0
- data/examples/syntax-highlighting.rb +31 -0
- data/examples/tags.rb +24 -0
- data/lib/prawn/format.rb +211 -0
- data/lib/prawn/format/effects/link.rb +30 -0
- data/lib/prawn/format/effects/underline.rb +32 -0
- data/lib/prawn/format/instructions/base.rb +62 -0
- data/lib/prawn/format/instructions/tag_close.rb +52 -0
- data/lib/prawn/format/instructions/tag_open.rb +95 -0
- data/lib/prawn/format/instructions/text.rb +89 -0
- data/lib/prawn/format/layout_builder.rb +113 -0
- data/lib/prawn/format/lexer.rb +222 -0
- data/lib/prawn/format/line.rb +99 -0
- data/lib/prawn/format/parser.rb +181 -0
- data/lib/prawn/format/state.rb +189 -0
- data/lib/prawn/format/text_object.rb +107 -0
- data/lib/prawn/format/version.rb +11 -0
- data/manual/html.rb +187 -0
- data/manual/include/basics.rb +6 -0
- data/manual/include/breaks.rb +13 -0
- data/manual/include/custom-tags.rb +10 -0
- data/manual/include/custom-tags2.rb +2 -0
- data/manual/include/indent.rb +4 -0
- data/manual/include/options.rb +15 -0
- data/manual/include/style-classes.rb +5 -0
- data/manual/manual.txt +101 -0
- data/manual/pdf.rb +204 -0
- data/prawn-format.gemspec +45 -0
- data/spec/layout_builder_spec.rb +27 -0
- data/spec/lexer_spec.rb +91 -0
- data/spec/parser_spec.rb +103 -0
- data/spec/spec_helper.rb +24 -0
- metadata +157 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/format/instructions/base'
|
4
|
+
|
5
|
+
module Prawn
|
6
|
+
module Format
|
7
|
+
module Instructions
|
8
|
+
|
9
|
+
class TagClose < Base
|
10
|
+
def self.close(state, tag, draw_state)
|
11
|
+
closer = new(state, tag)
|
12
|
+
closer.draw(state.document, draw_state)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :tag
|
16
|
+
|
17
|
+
def initialize(state, tag)
|
18
|
+
super(state)
|
19
|
+
@tag = tag
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](property)
|
23
|
+
@tag[:style][property]
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw(document, draw_state, options={})
|
27
|
+
(@tag[:effects] || []).each do |effect|
|
28
|
+
effect.finish(document, draw_state)
|
29
|
+
draw_state[:pending_effects].delete(effect)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def break?
|
34
|
+
force_break?
|
35
|
+
end
|
36
|
+
|
37
|
+
def style
|
38
|
+
@tag[:style]
|
39
|
+
end
|
40
|
+
|
41
|
+
def force_break?
|
42
|
+
@tag[:style][:display] == :break
|
43
|
+
end
|
44
|
+
|
45
|
+
def end_verbatim?
|
46
|
+
@tag[:style][:white_space] == :pre
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/format/instructions/base'
|
4
|
+
require 'prawn/format/effects/link'
|
5
|
+
require 'prawn/format/effects/underline'
|
6
|
+
|
7
|
+
module Prawn
|
8
|
+
module Format
|
9
|
+
module Instructions
|
10
|
+
|
11
|
+
class TagOpen < Base
|
12
|
+
attr_reader :tag
|
13
|
+
|
14
|
+
def initialize(state, tag)
|
15
|
+
super(state)
|
16
|
+
@tag = tag
|
17
|
+
end
|
18
|
+
|
19
|
+
def draw(document, draw_state, options={})
|
20
|
+
draw_width(document, draw_state)
|
21
|
+
draw_destination(document, draw_state)
|
22
|
+
draw_link(document, draw_state)
|
23
|
+
draw_underline(document, draw_state)
|
24
|
+
end
|
25
|
+
|
26
|
+
def start_verbatim?
|
27
|
+
@tag[:style][:white_space] == :pre
|
28
|
+
end
|
29
|
+
|
30
|
+
def style
|
31
|
+
@tag[:style]
|
32
|
+
end
|
33
|
+
|
34
|
+
def width
|
35
|
+
@state.width
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def draw_width(document, draw_state)
|
41
|
+
if width > 0
|
42
|
+
draw_state[:dx] += width
|
43
|
+
draw_state[:text].move_to(draw_state[:dx], draw_state[:dy])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw_destination(document, draw_state)
|
48
|
+
return unless tag[:style][:anchor]
|
49
|
+
|
50
|
+
x = draw_state[:real_x]
|
51
|
+
y = draw_state[:real_y] + draw_state[:dy] + ascent
|
52
|
+
|
53
|
+
label, destination = case tag[:style][:anchor]
|
54
|
+
when /^zoom=([\d\.]+):(.*)$/
|
55
|
+
[$2, document.dest_xyz(x, y, $1.to_f)]
|
56
|
+
when /^fit:(.*)$/
|
57
|
+
[$1, document.dest_fit]
|
58
|
+
when /^fith:(.*)$/
|
59
|
+
[$1, document.dest_fit_horizontally(y)]
|
60
|
+
when /^fitv:(.*)$/
|
61
|
+
[$1, document.dest_fit_vertically(x)]
|
62
|
+
when /^fitb:(.*)$/
|
63
|
+
[$1, document.dest_fit_bounds]
|
64
|
+
when /^fitbh:(.*)$/
|
65
|
+
[$1, document.dest_fit_bounds_horizontally(y)]
|
66
|
+
when /^fitbv:(.*)$/
|
67
|
+
[$1, document.dest_fit_bounds_vertically(x)]
|
68
|
+
else
|
69
|
+
[tag[:style][:anchor], document.dest_fit_bounds]
|
70
|
+
end
|
71
|
+
|
72
|
+
document.add_dest(label, destination)
|
73
|
+
end
|
74
|
+
|
75
|
+
def draw_link(document, draw_state)
|
76
|
+
return unless tag[:style][:target]
|
77
|
+
add_effect(Effects::Link.new(tag[:style][:target], draw_state[:dx]), draw_state)
|
78
|
+
end
|
79
|
+
|
80
|
+
def draw_underline(document, draw_state)
|
81
|
+
return unless tag[:style][:text_decoration] == :underline
|
82
|
+
add_effect(Effects::Underline.new(draw_state[:dx], @state), draw_state)
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_effect(effect, draw_state)
|
86
|
+
tag[:effects] ||= []
|
87
|
+
tag[:effects].push(effect)
|
88
|
+
|
89
|
+
draw_state[:pending_effects].push(effect)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/format/instructions/base'
|
4
|
+
|
5
|
+
module Prawn
|
6
|
+
module Format
|
7
|
+
module Instructions
|
8
|
+
|
9
|
+
class Text < Base
|
10
|
+
attr_reader :text
|
11
|
+
|
12
|
+
def initialize(state, text, options={})
|
13
|
+
super(state)
|
14
|
+
@text = text
|
15
|
+
@break = options.key?(:break) ? options[:break] : text.index(/[-\xE2\x80\x94\s]/)
|
16
|
+
@discardable = options.key?(:discardable) ? options[:discardable] : text.index(/\s/)
|
17
|
+
state.font.normalize_encoding(@text) if options.fetch(:normalize, true)
|
18
|
+
end
|
19
|
+
|
20
|
+
def dup
|
21
|
+
self.class.new(state, @text.dup, :normalize => false,
|
22
|
+
:break => @break, :discardable => @discardable)
|
23
|
+
end
|
24
|
+
|
25
|
+
def accumulate(list)
|
26
|
+
if list.last.is_a?(Text) && list.last.state == state
|
27
|
+
list.last.text << @text
|
28
|
+
else
|
29
|
+
list.push(dup)
|
30
|
+
end
|
31
|
+
|
32
|
+
return list
|
33
|
+
end
|
34
|
+
|
35
|
+
def spaces
|
36
|
+
@spaces ||= @text.scan(/ /).length
|
37
|
+
end
|
38
|
+
|
39
|
+
def height(ignore_discardable=false)
|
40
|
+
if ignore_discardable && discardable?
|
41
|
+
0
|
42
|
+
else
|
43
|
+
@height
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def break?
|
48
|
+
@break
|
49
|
+
end
|
50
|
+
|
51
|
+
def discardable?
|
52
|
+
@discardable
|
53
|
+
end
|
54
|
+
|
55
|
+
def compatible?(with)
|
56
|
+
with.is_a?(self.class) && with.state == state
|
57
|
+
end
|
58
|
+
|
59
|
+
def width(type=:all)
|
60
|
+
@width ||= @state.font.width_of(@text, :size => @state.font_size, :kerning => @state.kerning?)
|
61
|
+
|
62
|
+
case type
|
63
|
+
when :discardable then discardable? ? @width : 0
|
64
|
+
when :nondiscardable then discardable? ? 0 : @width
|
65
|
+
else @width
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
@text
|
71
|
+
end
|
72
|
+
|
73
|
+
def draw(document, draw_state, options={})
|
74
|
+
@state.apply!(draw_state[:text], draw_state[:cookies])
|
75
|
+
|
76
|
+
encoded_text = @state.font.encode_text(@text, :kerning => @state.kerning?)
|
77
|
+
encoded_text.each do |subset, chunk|
|
78
|
+
@state.apply_font!(draw_state[:text], draw_state[:cookies], subset)
|
79
|
+
draw_state[:text].show(chunk)
|
80
|
+
end
|
81
|
+
draw_state[:dx] += width
|
82
|
+
|
83
|
+
draw_state[:dx] += draw_state[:padding] * spaces if draw_state[:padding]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/format/line'
|
4
|
+
require 'prawn/format/parser'
|
5
|
+
|
6
|
+
module Prawn
|
7
|
+
module Format
|
8
|
+
class LayoutBuilder
|
9
|
+
attr_reader :document, :options
|
10
|
+
|
11
|
+
def initialize(document, text, options={})
|
12
|
+
@document = document
|
13
|
+
@options = options
|
14
|
+
@tags = document.tags.merge(options[:tags] || {})
|
15
|
+
@styles = document.styles.merge(options[:styles] || {})
|
16
|
+
style = document.default_style.merge(options[:default_style] || {})
|
17
|
+
|
18
|
+
translate_prawn_options(style, options)
|
19
|
+
|
20
|
+
@parser = Parser.new(@document, text,
|
21
|
+
:tags => @tags, :styles => @styles, :style => style)
|
22
|
+
|
23
|
+
@state = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def done?
|
27
|
+
@parser.eos?
|
28
|
+
end
|
29
|
+
|
30
|
+
def word_wrap(width, options={}, &block)
|
31
|
+
if options[:height] && block
|
32
|
+
raise ArgumentError, "cannot specify both height and a block"
|
33
|
+
elsif options[:height]
|
34
|
+
block = Proc.new { |l, h| h > options[:height] }
|
35
|
+
elsif block.nil?
|
36
|
+
block = Proc.new { |l, h| false }
|
37
|
+
end
|
38
|
+
|
39
|
+
lines = []
|
40
|
+
total_height = 0
|
41
|
+
|
42
|
+
while (line = self.next(width))
|
43
|
+
if block[line, total_height + line.height]
|
44
|
+
unget(line)
|
45
|
+
break
|
46
|
+
end
|
47
|
+
|
48
|
+
total_height += line.height
|
49
|
+
lines.push(line)
|
50
|
+
end
|
51
|
+
|
52
|
+
return lines
|
53
|
+
end
|
54
|
+
|
55
|
+
def fill(x, y, width, fill_options={}, &block)
|
56
|
+
lines = word_wrap(width, fill_options, &block)
|
57
|
+
draw_options = options.merge(fill_options).merge(:state => @state)
|
58
|
+
@state = document.draw_lines(x, y, width, lines, draw_options)
|
59
|
+
@state.delete(:cookies)
|
60
|
+
return @state[:dy] + y
|
61
|
+
end
|
62
|
+
|
63
|
+
def next(line_width=nil)
|
64
|
+
line = []
|
65
|
+
width = 0
|
66
|
+
break_at = nil
|
67
|
+
|
68
|
+
while (instruction = @parser.next)
|
69
|
+
next if !@parser.verbatim? && line.empty? && instruction.discardable? # ignore discardables at line start
|
70
|
+
line.push(instruction)
|
71
|
+
|
72
|
+
if instruction.break?
|
73
|
+
width += instruction.width(:nondiscardable)
|
74
|
+
break_at = line.length if line_width && width <= line_width
|
75
|
+
width += instruction.width(:discardable)
|
76
|
+
else
|
77
|
+
width += instruction.width
|
78
|
+
end
|
79
|
+
|
80
|
+
if instruction.force_break? || line_width && width >= line_width
|
81
|
+
break_at ||= line.length
|
82
|
+
|
83
|
+
@parser.push(line.pop) while line.length > break_at
|
84
|
+
hard_break = instruction.force_break? || @parser.eos?
|
85
|
+
|
86
|
+
return Line.new(line, hard_break)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
Line.new(line, true) if line.any?
|
91
|
+
end
|
92
|
+
|
93
|
+
def unget(line)
|
94
|
+
line.source.reverse_each { |instruction| @parser.push(instruction) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def translate_prawn_options(style, options)
|
98
|
+
style[:kerning] = options[:kerning] if options.key?(:kerning)
|
99
|
+
style[:font_size] = options[:size] if options.key?(:size)
|
100
|
+
|
101
|
+
case options[:style]
|
102
|
+
when :bold then
|
103
|
+
style[:font_weight] = :bold
|
104
|
+
when :italic then
|
105
|
+
style[:font_style] = :italic
|
106
|
+
when :bold_italic then
|
107
|
+
style[:font_weight] = :bold
|
108
|
+
style[:font_style] = :italic
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module Prawn
|
6
|
+
module Format
|
7
|
+
|
8
|
+
# The Lexer class is used by the formatting subsystem to scan a string
|
9
|
+
# and extract tokens from it. The tokens it looks for are either text,
|
10
|
+
# XML entities, or XML tags.
|
11
|
+
#
|
12
|
+
# Note that the lexer only scans for a subset of XML--it is not a true
|
13
|
+
# XML scanner, and understands just enough to provide a basic markup
|
14
|
+
# language for use in formatting documents.
|
15
|
+
#
|
16
|
+
# The subset includes only XML entities and tags--instructions, comments,
|
17
|
+
# and the like are not supported.
|
18
|
+
class Lexer
|
19
|
+
# When the scanner encounters a state or entity it is not able to
|
20
|
+
# handle, this exception will be raised.
|
21
|
+
class InvalidFormat < RuntimeError; end
|
22
|
+
|
23
|
+
# Controls whether whitespace is lexed verbatim or not. If not,
|
24
|
+
# adjacent whitespace is compressed into a single space character
|
25
|
+
# (this includes newlines).
|
26
|
+
attr_accessor :verbatim
|
27
|
+
|
28
|
+
# Create a new lexer that will scan the given text. The text must be
|
29
|
+
# UTF-8 encoded, and must consist of well-formed XML in the subset
|
30
|
+
# understand by the lexer.
|
31
|
+
def initialize(text)
|
32
|
+
@scanner = StringScanner.new(text)
|
33
|
+
@state = :start
|
34
|
+
@verbatim = false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the next token from the scanner. If the end of the string
|
38
|
+
# has been reached, this will return nil. Otherwise, the token itself
|
39
|
+
# is returned as a hash. The hash will always include a :type key,
|
40
|
+
# identifying the type of the token. It will be one of :text, :open,
|
41
|
+
# or :close.
|
42
|
+
#
|
43
|
+
# For :text tokens, the hash will also contain a :text key, which will
|
44
|
+
# point to an array of strings. Each element of the array contains
|
45
|
+
# either word, whitespace, or some other character at which the line
|
46
|
+
# may be broken.
|
47
|
+
#
|
48
|
+
# For :open tokens, the hash will contain a :tag key which identifies
|
49
|
+
# the name of the tag (as a symbol), and an :options key, which
|
50
|
+
# is another hash that contains the options that were given with the
|
51
|
+
# tag.
|
52
|
+
#
|
53
|
+
# For :close tokens, the hash will contain only a :tag key.
|
54
|
+
def next
|
55
|
+
if @state == :start && @scanner.eos?
|
56
|
+
return nil
|
57
|
+
else
|
58
|
+
scan_next_token
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Iterates over each token in the string, until the end of the string
|
63
|
+
# is reached. Each token is yielded. See #next for a discussion of the
|
64
|
+
# available token types.
|
65
|
+
def each
|
66
|
+
while (token = next_token)
|
67
|
+
yield token
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def scan_next_token
|
74
|
+
case @state
|
75
|
+
when :start then scan_start_state
|
76
|
+
when :self_close then scan_self_close_state
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if RUBY_VERSION >= "1.9.0"
|
81
|
+
def scan_other_text
|
82
|
+
@scanner.scan(/[^-\xE2\x80\x94\s<&]+/)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
def scan_other_text
|
86
|
+
return nil if @scanner.eos?
|
87
|
+
|
88
|
+
result = @scanner.scan_until(/[-\s<&]|\xE2\x80\x94/)
|
89
|
+
if result
|
90
|
+
@scanner.pos -= @scanner.matched.length
|
91
|
+
return nil if result == "<" || result == "&"
|
92
|
+
return result[0,result.length - @scanner.matched.length]
|
93
|
+
else
|
94
|
+
result = @scanner.rest
|
95
|
+
@scanner.terminate
|
96
|
+
return result
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def scan_text_chunk
|
102
|
+
@scanner.scan(/-/) || # hyphen
|
103
|
+
@scanner.scan(/\xe2\x80\x94/) || # mdash
|
104
|
+
scan_other_text
|
105
|
+
end
|
106
|
+
|
107
|
+
def scan_verbatim_text_chunk
|
108
|
+
@scanner.scan(/\r\n|\r|\n/) || # newline
|
109
|
+
@scanner.scan(/\t/) || # tab
|
110
|
+
@scanner.scan(/ +/) || # spaces
|
111
|
+
scan_text_chunk
|
112
|
+
end
|
113
|
+
|
114
|
+
def scan_nonverbatim_text_chunk
|
115
|
+
(@scanner.scan(/\s+/) && " ") || # whitespace
|
116
|
+
scan_text_chunk
|
117
|
+
end
|
118
|
+
|
119
|
+
def scan_next_text_chunk
|
120
|
+
if @verbatim
|
121
|
+
scan_verbatim_text_chunk
|
122
|
+
else
|
123
|
+
scan_nonverbatim_text_chunk
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def scan_start_state
|
128
|
+
if @scanner.scan(/</)
|
129
|
+
if @scanner.scan(%r(/))
|
130
|
+
scan_end_tag
|
131
|
+
else
|
132
|
+
scan_open_tag
|
133
|
+
end
|
134
|
+
elsif @scanner.scan(/&/)
|
135
|
+
scan_entity
|
136
|
+
else
|
137
|
+
pieces = []
|
138
|
+
loop do
|
139
|
+
chunk = scan_next_text_chunk or break
|
140
|
+
pieces << chunk
|
141
|
+
end
|
142
|
+
{ :type => :text, :text => pieces }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
ENTITY_MAP = {
|
147
|
+
"lt" => "<",
|
148
|
+
"gt" => ">",
|
149
|
+
"amp" => "&",
|
150
|
+
"mdash" => "\xE2\x80\x94",
|
151
|
+
"ndash" => "\xE2\x80\x93",
|
152
|
+
"nbsp" => "\xC2\xA0",
|
153
|
+
"bull" => "\342\200\242",
|
154
|
+
"quot" => '"',
|
155
|
+
}
|
156
|
+
|
157
|
+
def scan_entity
|
158
|
+
entity = @scanner.scan(/(?:#x?)?\w+/) or error("bad format for entity")
|
159
|
+
@scanner.scan(/;/) or error("missing semicolon to terminate entity")
|
160
|
+
|
161
|
+
text = case entity
|
162
|
+
when /#(\d+)/ then [$1.to_i].pack("U*")
|
163
|
+
when /#x([0-9a-f]+)/ then [$1.to_i(16)].pack("U*")
|
164
|
+
else
|
165
|
+
result = ENTITY_MAP[entity] or error("unrecognized entity #{entity.inspect}")
|
166
|
+
result.dup
|
167
|
+
end
|
168
|
+
|
169
|
+
{ :type => :text, :text => [text] }
|
170
|
+
end
|
171
|
+
|
172
|
+
def scan_open_tag
|
173
|
+
tag = @scanner.scan(/\w+/) or error("'<' without valid tag")
|
174
|
+
tag = tag.downcase.to_sym
|
175
|
+
|
176
|
+
options = {}
|
177
|
+
@scanner.skip(/\s*/)
|
178
|
+
while !@scanner.eos? && @scanner.peek(1) =~ /\w/
|
179
|
+
name = @scanner.scan(/\w+/)
|
180
|
+
@scanner.scan(/\s*=\s*/) or error("expected assigment after option #{name}")
|
181
|
+
if (delim = @scanner.scan(/['"]/))
|
182
|
+
value = @scanner.scan(/[^#{delim}]*/)
|
183
|
+
@scanner.scan(/#{delim}/) or error("expected option value to end with #{delim}")
|
184
|
+
else
|
185
|
+
value = @scanner.scan(/[^\s>]*/)
|
186
|
+
end
|
187
|
+
options[name.downcase.to_sym] = value
|
188
|
+
@scanner.skip(/\s*/)
|
189
|
+
end
|
190
|
+
|
191
|
+
if @scanner.scan(%r(/))
|
192
|
+
@self_close = true
|
193
|
+
@tag = tag
|
194
|
+
@state = :self_close
|
195
|
+
else
|
196
|
+
@self_close = false
|
197
|
+
@state = :start
|
198
|
+
end
|
199
|
+
|
200
|
+
@scanner.scan(/>/) or error("unclosed tag #{tag.inspect}")
|
201
|
+
|
202
|
+
{ :type => :open, :tag => tag, :options => options }
|
203
|
+
end
|
204
|
+
|
205
|
+
def scan_end_tag
|
206
|
+
tag = @scanner.scan(/\w+/).to_sym
|
207
|
+
@scanner.skip(/\s*/)
|
208
|
+
@scanner.scan(/>/) or error("unclosed ending tag #{tag.inspect}")
|
209
|
+
{ :type => :close, :tag => tag }
|
210
|
+
end
|
211
|
+
|
212
|
+
def scan_self_close_state
|
213
|
+
@state = :start
|
214
|
+
{ :type => :close, :tag => @tag }
|
215
|
+
end
|
216
|
+
|
217
|
+
def error(message)
|
218
|
+
raise InvalidFormat, "#{message} at #{@scanner.pos} -> #{@scanner.rest.inspect[0,50]}..."
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|