asciidoctor 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- data/LICENSE +1 -1
- data/README.md +136 -55
- data/asciidoctor.gemspec +10 -4
- data/lib/asciidoctor.rb +33 -7
- data/lib/asciidoctor/block.rb +161 -24
- data/lib/asciidoctor/debug.rb +12 -1
- data/lib/asciidoctor/document.rb +31 -630
- data/lib/asciidoctor/lexer.rb +654 -0
- data/lib/asciidoctor/list_item.rb +30 -0
- data/lib/asciidoctor/reader.rb +236 -0
- data/lib/asciidoctor/render_templates.rb +3 -3
- data/lib/asciidoctor/renderer.rb +22 -2
- data/lib/asciidoctor/version.rb +1 -1
- data/test/attributes_test.rb +88 -0
- data/test/document_test.rb +2 -8
- data/test/lexer_test.rb +12 -0
- data/test/list_elements_test.rb +1 -1
- data/test/reader_test.rb +56 -0
- data/test/test_helper.rb +7 -2
- data/test/text_test.rb +26 -20
- metadata +113 -95
@@ -9,6 +9,9 @@ class Asciidoctor::ListItem
|
|
9
9
|
# Public: Get/Set the String list item anchor name.
|
10
10
|
attr_accessor :anchor
|
11
11
|
|
12
|
+
# Public: Get/Set the Integer list level (for nesting list elements).
|
13
|
+
attr_accessor :level
|
14
|
+
|
12
15
|
# Public: Initialize an Asciidoctor::ListItem object.
|
13
16
|
#
|
14
17
|
# content - the String content (default '')
|
@@ -16,4 +19,31 @@ class Asciidoctor::ListItem
|
|
16
19
|
@content = content
|
17
20
|
@blocks = []
|
18
21
|
end
|
22
|
+
|
23
|
+
def render
|
24
|
+
output = "<li><p>#{content} (HTMLIFY) "
|
25
|
+
output += blocks.map{|block| block.render}.join
|
26
|
+
output += "</p></li>"
|
27
|
+
end
|
28
|
+
|
29
|
+
def splain(parent_level = 0)
|
30
|
+
parent_level += 1
|
31
|
+
Asciidoctor.puts_indented(parent_level, "List Item anchor: #{anchor}") unless self.anchor.nil?
|
32
|
+
Asciidoctor.puts_indented(parent_level, "Content: #{content}") unless self.content.nil?
|
33
|
+
|
34
|
+
Asciidoctor.puts_indented(parent_level, "Blocks: #{@blocks.count}")
|
35
|
+
|
36
|
+
if @blocks.any?
|
37
|
+
Asciidoctor.puts_indented(parent_level, "Blocks content (#{@blocks.count}):")
|
38
|
+
@blocks.each_with_index do |block, i|
|
39
|
+
Asciidoctor.puts_indented(parent_level, "v" * (60 - parent_level*2))
|
40
|
+
Asciidoctor.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
|
41
|
+
Asciidoctor.puts_indented(parent_level, "Name is #{block.name rescue 'n/a'}")
|
42
|
+
Asciidoctor.puts_indented(parent_level, "=" * 40)
|
43
|
+
block.splain(parent_level) if block.respond_to? :splain
|
44
|
+
Asciidoctor.puts_indented(parent_level, "^" * (60 - parent_level*2))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
nil
|
48
|
+
end
|
19
49
|
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# Public: Methods for retrieving lines from Asciidoc documents
|
2
|
+
class Asciidoctor::Reader
|
3
|
+
|
4
|
+
include Asciidoctor
|
5
|
+
|
6
|
+
# Public: Get the String document source.
|
7
|
+
attr_reader :source
|
8
|
+
|
9
|
+
# Public: Get the Hash of defines
|
10
|
+
attr_reader :defines
|
11
|
+
|
12
|
+
attr_reader :references
|
13
|
+
|
14
|
+
# Public: Convert a string to a legal attribute name.
|
15
|
+
#
|
16
|
+
# name - The String holding the Asciidoc attribute name.
|
17
|
+
#
|
18
|
+
# Returns a String with the legal name.
|
19
|
+
#
|
20
|
+
# Examples
|
21
|
+
#
|
22
|
+
# sanitize_attribute_name('Foo Bar')
|
23
|
+
# => 'foobar'
|
24
|
+
#
|
25
|
+
# sanitize_attribute_name('foo')
|
26
|
+
# => 'foo'
|
27
|
+
#
|
28
|
+
# sanitize_attribute_name('Foo 3 #-Billy')
|
29
|
+
# => 'foo3-billy'
|
30
|
+
def sanitize_attribute_name(name)
|
31
|
+
name.gsub(/[^\w\-_]/, '').downcase
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Initialize the Reader object.
|
35
|
+
#
|
36
|
+
# data - The Array of Strings holding the Asciidoc source document.
|
37
|
+
# block - A block that can be used to retrieve external Asciidoc
|
38
|
+
# data to include in this document.
|
39
|
+
#
|
40
|
+
# Examples
|
41
|
+
#
|
42
|
+
# data = File.readlines(filename)
|
43
|
+
# reader = Asciidoctor::Reader.new(data)
|
44
|
+
def initialize(data = [], &block)
|
45
|
+
raw_source = []
|
46
|
+
@defines = {}
|
47
|
+
@references = {}
|
48
|
+
|
49
|
+
include_regexp = /^include::([^\[]+)\[\]\s*\n?\z/
|
50
|
+
|
51
|
+
data.each do |line|
|
52
|
+
if inc = line.match(include_regexp)
|
53
|
+
if block_given?
|
54
|
+
raw_source << yield(inc[1])
|
55
|
+
else
|
56
|
+
raw_source.concat(File.readlines(inc[1]))
|
57
|
+
end
|
58
|
+
else
|
59
|
+
raw_source << line
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
ifdef_regexp = /^(ifdef|ifndef)::([^\[]+)\[\]/
|
64
|
+
endif_regexp = /^endif::/
|
65
|
+
defattr_regexp = /^:([^:!]+):\s*(.*)\s*$/
|
66
|
+
delete_attr_regexp = /^:([^:]+)!:\s*$/
|
67
|
+
conditional_regexp = /^\s*\{([^\?]+)\?\s*([^\}]+)\s*\}/
|
68
|
+
|
69
|
+
skip_to = nil
|
70
|
+
continuing_value = nil
|
71
|
+
continuing_key = nil
|
72
|
+
@lines = []
|
73
|
+
raw_source.each do |line|
|
74
|
+
if skip_to
|
75
|
+
skip_to = nil if line.match(skip_to)
|
76
|
+
elsif continuing_value
|
77
|
+
close_continue = false
|
78
|
+
# Lines that start with whitespace and end with a '+' are
|
79
|
+
# a continuation, so gobble them up into `value`
|
80
|
+
if match = line.match(/\s+(.+)\s+\+\s*$/)
|
81
|
+
continuing_value += match[1]
|
82
|
+
elsif match = line.match(/\s+(.+)/)
|
83
|
+
# If this continued line doesn't end with a +, then this
|
84
|
+
# is the end of the continuation, no matter what the next
|
85
|
+
# line does.
|
86
|
+
continuing_value += match[1]
|
87
|
+
close_continue = true
|
88
|
+
else
|
89
|
+
# If this line doesn't start with whitespace, then it's
|
90
|
+
# not a valid continuation line, so push it back for processing
|
91
|
+
close_continue = true
|
92
|
+
raw_source.unshift(line)
|
93
|
+
end
|
94
|
+
if close_continue
|
95
|
+
@defines[continuing_key] = continuing_value
|
96
|
+
continuing_key = nil
|
97
|
+
continuing_value = nil
|
98
|
+
end
|
99
|
+
elsif match = line.match(ifdef_regexp)
|
100
|
+
attr = match[2]
|
101
|
+
skip = case match[1]
|
102
|
+
when 'ifdef'; !@defines.has_key?(attr)
|
103
|
+
when 'ifndef'; @defines.has_key?(attr)
|
104
|
+
end
|
105
|
+
skip_to = /^endif::#{attr}\[\]\s*\n/ if skip
|
106
|
+
elsif match = line.match(defattr_regexp)
|
107
|
+
key = sanitize_attribute_name(match[1])
|
108
|
+
value = match[2]
|
109
|
+
if match = value.match(Asciidoctor::REGEXP[:attr_continue])
|
110
|
+
# attribute value continuation line; grab lines until we run out
|
111
|
+
# of continuation lines
|
112
|
+
continuing_key = key
|
113
|
+
continuing_value = match[1] # strip off the spaces and +
|
114
|
+
Asciidoctor.debug "continuing key: #{continuing_key} with partial value: '#{continuing_value}'"
|
115
|
+
else
|
116
|
+
@defines[key] = value
|
117
|
+
Asciidoctor.debug "Defines[#{key}] is '#{value}'"
|
118
|
+
end
|
119
|
+
elsif match = line.match(delete_attr_regexp)
|
120
|
+
key = sanitize_attribute_name(match[1])
|
121
|
+
@defines.delete(key)
|
122
|
+
elsif !line.match(endif_regexp)
|
123
|
+
while match = line.match(conditional_regexp)
|
124
|
+
value = @defines.has_key?(match[1]) ? match[2] : ''
|
125
|
+
line.sub!(conditional_regexp, value)
|
126
|
+
end
|
127
|
+
@lines << line unless line.match(REGEXP[:comment])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Process bibliography references, so they're available when text
|
132
|
+
# before the reference is being rendered.
|
133
|
+
@lines.each do |line|
|
134
|
+
if biblio = line.match(REGEXP[:biblio])
|
135
|
+
@references[biblio[1]] = "[#{biblio[1]}]"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
Asciidoctor.debug "About to leave Reader#init, and references is #{@references.inspect}"
|
140
|
+
@source = @lines.join
|
141
|
+
Asciidoctor.debug "Leaving Reader#init, and I have #{@lines.count} lines"
|
142
|
+
Asciidoctor.debug "Also, has_lines? is #{self.has_lines?}"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Public: Check whether there are any lines left to read.
|
146
|
+
#
|
147
|
+
# Returns true if @lines.any? is true, or false otherwise.
|
148
|
+
def has_lines?
|
149
|
+
@lines.any?
|
150
|
+
end
|
151
|
+
|
152
|
+
# Private: Strip off leading blank lines in the Array of lines.
|
153
|
+
#
|
154
|
+
# Returns nil.
|
155
|
+
#
|
156
|
+
# Examples
|
157
|
+
#
|
158
|
+
# @lines
|
159
|
+
# => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"]
|
160
|
+
#
|
161
|
+
# skip_blank
|
162
|
+
# => nil
|
163
|
+
#
|
164
|
+
# @lines
|
165
|
+
# => ["Foo\n", "Bar\n"]
|
166
|
+
def skip_blank
|
167
|
+
while @lines.any? && @lines.first.strip.empty?
|
168
|
+
@lines.shift
|
169
|
+
end
|
170
|
+
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
# Public: Get the next line of source data. Consumes the line returned.
|
175
|
+
#
|
176
|
+
# Returns the String of the next line of the source data if data is present.
|
177
|
+
# Returns nil if there is no more data.
|
178
|
+
def get_line
|
179
|
+
@lines.shift
|
180
|
+
end
|
181
|
+
|
182
|
+
# Public: Get the next line of source data. Does not consume the line returned.
|
183
|
+
#
|
184
|
+
# Returns a String dup of the next line of the source data if data is present.
|
185
|
+
# Returns nil if there is no more data.
|
186
|
+
def peek_line
|
187
|
+
@lines.first.dup if @lines.first
|
188
|
+
end
|
189
|
+
|
190
|
+
# Public: Push String `line` onto queue of source data lines, unless `line` is nil.
|
191
|
+
#
|
192
|
+
# Returns nil
|
193
|
+
def unshift(line)
|
194
|
+
@lines.unshift(line) if line
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
# Public: Return all the lines from `@lines` until we (1) run out them,
|
199
|
+
# (2) find a blank line with :break_on_blank_lines => true, or (3) find
|
200
|
+
# a line for which the given block evals to true.
|
201
|
+
#
|
202
|
+
# options - an optional Hash of processing options:
|
203
|
+
# * :break_on_blank_lines may be used to specify to break on
|
204
|
+
# blank lines
|
205
|
+
# * :preserve_last_line may be used to specify that the String
|
206
|
+
# causing the method to stop processing lines should be
|
207
|
+
# pushed back onto the `lines` Array.
|
208
|
+
#
|
209
|
+
# Returns the Array of lines forming the next segment.
|
210
|
+
#
|
211
|
+
# Examples
|
212
|
+
#
|
213
|
+
# reader = Reader.new ["First paragraph\n", "Second paragraph\n",
|
214
|
+
# "Open block\n", "\n", "Can have blank lines\n",
|
215
|
+
# "--\n", "\n", "In a different segment\n"]
|
216
|
+
#
|
217
|
+
# reader.grab_lines_until
|
218
|
+
# => ["First paragraph\n", "Second paragraph\n", "Open block\n"]
|
219
|
+
def grab_lines_until(options = {}, &block)
|
220
|
+
buffer = []
|
221
|
+
|
222
|
+
while (this_line = self.get_line)
|
223
|
+
Asciidoctor.debug "Processing line: '#{this_line}'"
|
224
|
+
finis ||= true if options[:break_on_blank_lines] && this_line.strip.empty?
|
225
|
+
finis ||= true if block && value = yield(this_line)
|
226
|
+
if finis
|
227
|
+
self.unshift(this_line) if options[:preserve_last_line]
|
228
|
+
break
|
229
|
+
end
|
230
|
+
|
231
|
+
buffer << this_line
|
232
|
+
end
|
233
|
+
buffer
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
@@ -164,9 +164,9 @@ class SectionUlistTemplate < BaseTemplate
|
|
164
164
|
@template ||= ERB.new <<-EOF
|
165
165
|
<div class='ulist'>
|
166
166
|
<ul>
|
167
|
-
|
168
|
-
|
169
|
-
|
167
|
+
<% content.each do |li| %>
|
168
|
+
<%= li %>
|
169
|
+
<% end %>
|
170
170
|
</ul>
|
171
171
|
</div>
|
172
172
|
EOF
|
data/lib/asciidoctor/renderer.rb
CHANGED
@@ -14,6 +14,26 @@ class Asciidoctor::Renderer
|
|
14
14
|
@views[view] = tc.new
|
15
15
|
end
|
16
16
|
|
17
|
+
# If user passed in a template dir, let them override our base templates
|
18
|
+
if template_dir = options.delete(:template_dir)
|
19
|
+
Asciidoctor.debug "Views going in are like so:"
|
20
|
+
@views.each_pair do |k, v|
|
21
|
+
Asciidoctor.debug "#{k}: #{v}"
|
22
|
+
end
|
23
|
+
Asciidoctor.debug "="*60
|
24
|
+
# Grab the files in the top level of the directory (we're not traversing)
|
25
|
+
files = Dir.glob(File.join(template_dir, '*')).select{|f| File.stat(f).file?}
|
26
|
+
files.inject(@views) do |view_hash, view|
|
27
|
+
name = File.basename(view).split('.').first
|
28
|
+
view_hash.merge!(name => Tilt.new(view, nil, :trim => '<>'))
|
29
|
+
end
|
30
|
+
Asciidoctor.debug "Views are now like so:"
|
31
|
+
@views.each_pair do |k, v|
|
32
|
+
Asciidoctor.debug "#{k}: #{v}"
|
33
|
+
end
|
34
|
+
Asciidoctor.debug "="*60
|
35
|
+
end
|
36
|
+
|
17
37
|
@render_stack = []
|
18
38
|
end
|
19
39
|
|
@@ -37,8 +57,8 @@ class Asciidoctor::Renderer
|
|
37
57
|
STDERR.puts "Rendering:"
|
38
58
|
@render_stack.each do |stack_view, stack_obj|
|
39
59
|
obj_info = case stack_obj
|
40
|
-
when Section; "SECTION #{stack_obj.name}"
|
41
|
-
when Block;
|
60
|
+
when Asciidoctor::Section; "SECTION #{stack_obj.name}"
|
61
|
+
when Asciidoctor::Block;
|
42
62
|
if stack_obj.context == :dlist
|
43
63
|
dt_list = stack_obj.buffer.map{|dt,dd| dt.content.strip}.join(', ')
|
44
64
|
"BLOCK :dlist (#{dt_list})"
|
data/lib/asciidoctor/version.rb
CHANGED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
context "Attributes" do
|
4
|
+
test "creates an attribute" do
|
5
|
+
doc = document_from_string(":frog: Tanglefoot")
|
6
|
+
assert_equal doc.defines['frog'], 'Tanglefoot'
|
7
|
+
end
|
8
|
+
|
9
|
+
test "deletes an attribute" do
|
10
|
+
doc = document_from_string(":frog: Tanglefoot\n:frog!:")
|
11
|
+
assert_equal nil, doc.defines['frog']
|
12
|
+
end
|
13
|
+
|
14
|
+
test "doesn't choke when deleting a non-existing attribute" do
|
15
|
+
doc = document_from_string(":frog!:")
|
16
|
+
assert_equal nil, doc.defines['frog']
|
17
|
+
end
|
18
|
+
|
19
|
+
test "render properly with simple names" do
|
20
|
+
html = render_string(":frog: Tanglefoot\nYo, {frog}!")
|
21
|
+
result = Nokogiri::HTML(html)
|
22
|
+
assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip
|
23
|
+
end
|
24
|
+
|
25
|
+
test "convert multi-word names and render" do
|
26
|
+
html = render_string("Main Header\n===========\n:My frog: Tanglefoot\nYo, {myfrog}!")
|
27
|
+
result = Nokogiri::HTML(html)
|
28
|
+
assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip
|
29
|
+
end
|
30
|
+
|
31
|
+
test "ignores lines with bad attributes" do
|
32
|
+
html = render_string("This is\nblah blah {foobarbaz}\nall there is.")
|
33
|
+
result = Nokogiri::HTML(html)
|
34
|
+
assert_no_match /blah blah/m, result.css("p").first.content.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
# See above - AsciiDoc says we're supposed to delete lines with bad
|
38
|
+
# attribute refs in them. AsciiDoc is strange.
|
39
|
+
#
|
40
|
+
# test "Unknowns" do
|
41
|
+
# html = render_string("Look, a {gobbledygook}")
|
42
|
+
# result = Nokogiri::HTML(html)
|
43
|
+
# assert_equal("Look, a {gobbledygook}", result.css("p").first.content.strip)
|
44
|
+
# end
|
45
|
+
|
46
|
+
test "substitutes inside unordered list items" do
|
47
|
+
html = render_string(":foo: bar\n* snort at the {foo}\n* yawn")
|
48
|
+
result = Nokogiri::HTML(html)
|
49
|
+
assert_match /snort at the bar/, result.css("li").first.content.strip
|
50
|
+
end
|
51
|
+
|
52
|
+
test "renders attribute until it's deleted" do
|
53
|
+
pending "Not working yet (will require adding element-specific attributes or early attr substitution during parsing)"
|
54
|
+
# html = render_string(":foo: bar\nCrossing the {foo}\n\n:foo!:\nBelly up to the {foo}")
|
55
|
+
# result = Nokogiri::HTML(html)
|
56
|
+
# assert_match /Crossing the bar/, result.css("p").first.content.strip
|
57
|
+
# assert_no_match /Belly up to the bar/, result.css("p").last.content.strip
|
58
|
+
end
|
59
|
+
|
60
|
+
test "doesn't disturb attribute-looking things escaped with backslash" do
|
61
|
+
html = render_string(":foo: bar\nThis is a \\{foo} day.")
|
62
|
+
result = Nokogiri::HTML(html)
|
63
|
+
assert_equal 'This is a {foo} day.', result.css('p').first.content.strip
|
64
|
+
end
|
65
|
+
|
66
|
+
test "doesn't disturb attribute-looking things escaped with literals" do
|
67
|
+
html = render_string(":foo: bar\nThis is a +++{foo}+++ day.")
|
68
|
+
result = Nokogiri::HTML(html)
|
69
|
+
assert_equal 'This is a {foo} day.', result.css('p').first.content.strip
|
70
|
+
end
|
71
|
+
|
72
|
+
test "doesn't substitute attributes inside code blocks" do
|
73
|
+
pending "whut?"
|
74
|
+
end
|
75
|
+
|
76
|
+
test "doesn't substitute attributes inside literal blocks" do
|
77
|
+
pending "whut?"
|
78
|
+
end
|
79
|
+
|
80
|
+
test "Intrinsics" do
|
81
|
+
Asciidoctor::INTRINSICS.each_pair do |key, value|
|
82
|
+
html = render_string("Look, a {#{key}} is here")
|
83
|
+
result = Nokogiri::HTML(html)
|
84
|
+
assert_equal("Look, a #{value} is here", result.css("p").first.content.strip)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/test/document_test.rb
CHANGED
@@ -3,7 +3,7 @@ require 'test_helper'
|
|
3
3
|
class DocumentTest < Test::Unit::TestCase
|
4
4
|
# setup for test
|
5
5
|
def setup
|
6
|
-
@doc =
|
6
|
+
@doc = example_document(:asciidoc_index)
|
7
7
|
end
|
8
8
|
|
9
9
|
def test_title
|
@@ -11,13 +11,7 @@ class DocumentTest < Test::Unit::TestCase
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_with_no_title
|
14
|
-
d = Asciidoctor::Document.new("Snorf")
|
14
|
+
d = Asciidoctor::Document.new(["Snorf"])
|
15
15
|
assert_nil d.title
|
16
16
|
end
|
17
|
-
|
18
|
-
def test_is_section_heading
|
19
|
-
assert @doc.send(:is_section_heading?, "AsciiDoc Home Page", "==================")
|
20
|
-
assert @doc.send(:is_section_heading?, "=== AsciiDoc Home Page")
|
21
|
-
end
|
22
|
-
|
23
17
|
end
|