rweb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +316 -0
- data/Changes.rdoc +5 -0
- data/README.rdoc +132 -0
- data/Rakefile +249 -0
- data/TODO.rdoc +4 -0
- data/bin/rtangle +115 -0
- data/bin/rweave +115 -0
- data/docs/rtangle.txt +90 -0
- data/docs/rweave.txt +90 -0
- data/lib/rweb.rb +233 -0
- data/tests/tc_rweb.rb +57 -0
- metadata +62 -0
data/docs/rtangle.txt
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
RTANGLE
|
2
|
+
=======
|
3
|
+
This is a simple utility that acts as a wrapper around RWEB's tangle and,
|
4
|
+
instead of executing the resulting code, either prints it to stdout or saves it
|
5
|
+
to a given file. Note that RTangle is itself written in RWEB's executable format
|
6
|
+
with the boilerplate before the __END__ statement and the actual program
|
7
|
+
document afterwards.
|
8
|
+
|
9
|
+
The mainline code is quite simple:
|
10
|
+
|
11
|
+
RWEB Mainline
|
12
|
+
-------------
|
13
|
+
> {<<global requires>>}
|
14
|
+
>
|
15
|
+
> {<<command line processing>>}
|
16
|
+
>
|
17
|
+
> {<<tangling>>}
|
18
|
+
>
|
19
|
+
> {<<main>>}
|
20
|
+
|
21
|
+
The only require we need to introduce is to require RWEB itself. This is not, of
|
22
|
+
course, necessary if we execute the RWEB document directly. Since, however, it
|
23
|
+
is highly likely that we may at some point wish to tangle the RTangle utility
|
24
|
+
itself into a Ruby source file, we should, pro forma, require the library
|
25
|
+
anyway.
|
26
|
+
|
27
|
+
global requires
|
28
|
+
---------------
|
29
|
+
> require 'rweb'
|
30
|
+
|
31
|
+
The tangling stage itself is a very simple function which takes any kind of IO
|
32
|
+
object for input, tangles it, then places the resulting string into the output,
|
33
|
+
itself an IO object of any kind. It also doesn't care in the slightest exactly
|
34
|
+
what kind of IO object is being used, thus suited to application as a filter in
|
35
|
+
a potentially longer chain of tools.
|
36
|
+
|
37
|
+
tangling
|
38
|
+
--------
|
39
|
+
> def rtangle(io_in, io_out)
|
40
|
+
> io_out << RWEB.tangle(io_in)
|
41
|
+
> end
|
42
|
+
|
43
|
+
As can be seen, the true heavy lifting is done inside of the RWEB module. This
|
44
|
+
utility is a hyper-simple shell around it by comparison.
|
45
|
+
|
46
|
+
The mainline function is pretty simple as well. It just takes the arguments from
|
47
|
+
the command line and passes them to the command line processor blindly,
|
48
|
+
accepting in return a pair of IO objects -- one for input, the other for output.
|
49
|
+
It then calls tangle with these objects, wrapping in a begin/ensure clause to
|
50
|
+
make sure the IO objects are closed before exiting. The only additional step it
|
51
|
+
takes is a call to the function find_block_begin which shadows the IO object
|
52
|
+
used for input and silently sweeps away the stuff before __END__ in a document
|
53
|
+
if it exists in the first 20 lines.
|
54
|
+
|
55
|
+
main
|
56
|
+
----
|
57
|
+
> io_in, io_out = process_argv ARGV
|
58
|
+
> begin
|
59
|
+
> rtangle(io_in, io_out)
|
60
|
+
> ensure
|
61
|
+
> io_in.close
|
62
|
+
> io_out.close
|
63
|
+
> end
|
64
|
+
|
65
|
+
All that's left is the command line processor.
|
66
|
+
|
67
|
+
command line processing
|
68
|
+
-----------------------
|
69
|
+
> def process_argv args
|
70
|
+
> io_in = STDIN
|
71
|
+
> io_out = STDOUT
|
72
|
+
> if args.length > 2
|
73
|
+
> {<<display usage>>}
|
74
|
+
> end
|
75
|
+
> arg = args.shift
|
76
|
+
> io_in = File.open(arg, "r") if arg
|
77
|
+
> arg = args.shift
|
78
|
+
> io_out = File.open(arg, "w") if arg
|
79
|
+
> return io_in, io_out
|
80
|
+
> end
|
81
|
+
|
82
|
+
The usage message is straightforward and broken out only to reduce code clutter.
|
83
|
+
|
84
|
+
display usage
|
85
|
+
-------------
|
86
|
+
> puts "Incorrect command line."
|
87
|
+
> puts "Usage:"
|
88
|
+
> puts " rtangle [input_file [output_file]]"
|
89
|
+
> puts
|
90
|
+
> puts "Files default to stdin and stdout respectively."
|
data/docs/rweave.txt
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
RWEAVE
|
2
|
+
======
|
3
|
+
This is a simple utility that acts as a wrapper around RWEB's weave and, instead
|
4
|
+
of executing the resulting code, either prints it to stdout or saves it to a
|
5
|
+
given file. Note that RWeave is itself written in RWEB's executable format with
|
6
|
+
the boilerplate before the __END__ statement and the actual program document
|
7
|
+
afterwards.
|
8
|
+
|
9
|
+
The mainline code is quite simple:
|
10
|
+
|
11
|
+
RWEB Mainline
|
12
|
+
-------------
|
13
|
+
> {<<global requires>>}
|
14
|
+
>
|
15
|
+
> {<<command line processing>>}
|
16
|
+
>
|
17
|
+
> {<<weaving>>}
|
18
|
+
>
|
19
|
+
> {<<main>>}
|
20
|
+
|
21
|
+
The only require we need to introduce is to require RWEB itself. This is not, of
|
22
|
+
course, necessary if we execute the RWEB document directly. Since, however, it
|
23
|
+
is highly likely that we may at some point wish to weave the RWeave utility
|
24
|
+
itself into a Ruby source file, we should, pro forma, require the library
|
25
|
+
anyway.
|
26
|
+
|
27
|
+
global requires
|
28
|
+
---------------
|
29
|
+
> require 'rweb'
|
30
|
+
|
31
|
+
The weaving stage itself is a very simple function which takes any kind of IO
|
32
|
+
object for input, weaves it, then places the resulting string into the output,
|
33
|
+
itself an IO object of any kind. It also doesn't care in the slightest exactly
|
34
|
+
what kind of IO object is being used, thus suited to application as a filter in
|
35
|
+
a potentially longer chain of tools.
|
36
|
+
|
37
|
+
weaving
|
38
|
+
-------
|
39
|
+
> def rweave(io_in, io_out)
|
40
|
+
> io_out << RWEB.weave(io_in)
|
41
|
+
> end
|
42
|
+
|
43
|
+
As can be seen, the true heavy lifting is done inside of the RWEB module. This
|
44
|
+
utility is a hyper-simple shell around it by comparison.
|
45
|
+
|
46
|
+
The mainline function is pretty simple as well. It just takes the arguments from
|
47
|
+
the command line and passes them to the command line processor blindly,
|
48
|
+
accepting in return a pair of IO objects -- one for input, the other for output.
|
49
|
+
It then calls weave with these objects, wrapping in a begin/ensure clause to
|
50
|
+
make sure the IO objects are closed before exiting. The only additional step it
|
51
|
+
takes is a call to the function find_block_begin which shadows the IO object
|
52
|
+
used for input and silently sweeps away the stuff before __END__ in a document
|
53
|
+
if it exists in the first 20 lines.
|
54
|
+
|
55
|
+
main
|
56
|
+
----
|
57
|
+
> io_in, io_out = process_argv ARGV
|
58
|
+
> begin
|
59
|
+
> rweave(io_in, io_out)
|
60
|
+
> ensure
|
61
|
+
> io_in.close
|
62
|
+
> io_out.close
|
63
|
+
> end
|
64
|
+
|
65
|
+
All that's left is the command line processor.
|
66
|
+
|
67
|
+
command line processing
|
68
|
+
-----------------------
|
69
|
+
> def process_argv args
|
70
|
+
> io_in = STDIN
|
71
|
+
> io_out = STDOUT
|
72
|
+
> if args.length > 2
|
73
|
+
> {<<display usage>>}
|
74
|
+
> end
|
75
|
+
> arg = args.shift
|
76
|
+
> io_in = File.open(arg, "r") if arg
|
77
|
+
> arg = args.shift
|
78
|
+
> io_out = File.open(arg, "w") if arg
|
79
|
+
> return io_in, io_out
|
80
|
+
> end
|
81
|
+
|
82
|
+
The usage message is straightforward and broken out only to reduce code clutter.
|
83
|
+
|
84
|
+
display usage
|
85
|
+
-------------
|
86
|
+
> puts "Incorrect command line."
|
87
|
+
> puts "Usage:"
|
88
|
+
> puts " rweave [input_file [output_file]]"
|
89
|
+
> puts
|
90
|
+
> puts "Files default to stdin and stdout respectively."
|
data/lib/rweb.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
# RWEB.RB -- A library for literate programming in Ruby
|
2
|
+
# Copyright (C) 2007 Michael T. Richter <ttmrichter@gmail.com>
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; Version 2, June 1991 (and no other).
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program (in the file COPYING); if not, write to the Free
|
15
|
+
# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307,
|
16
|
+
# USA
|
17
|
+
|
18
|
+
# Ironically, this library cannot be written in the RWEB style because it
|
19
|
+
# *implements* the RWEB style of programming for Ruby.
|
20
|
+
|
21
|
+
=begin rdoc
|
22
|
+
Our default directives are contained in a Hash object. We would like to be able
|
23
|
+
to copy that set of defaults without having the original changed, so we make up
|
24
|
+
for a slight design deficiency in the Ruby standard library by providing a
|
25
|
+
(somewhat limited) deep copy method.
|
26
|
+
=end
|
27
|
+
class Object
|
28
|
+
|
29
|
+
# Deep copy an object by first marshalling, then unmarshalling it.
|
30
|
+
def deep_copy
|
31
|
+
Marshal.load(Marshal.dump(self))
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
# The default values for directives. The default style for weaving is plain
|
37
|
+
# text and the default title is "Untitled".
|
38
|
+
DEFAULT_DIRECTIVES = {
|
39
|
+
:style => "Plain",
|
40
|
+
:title => "Untitled"
|
41
|
+
}
|
42
|
+
|
43
|
+
# Take a provided IO stream and disassemble it into a set of directives, an
|
44
|
+
# intermediate document form and a collection of (unexpanded) chunks. Any
|
45
|
+
# boilerplate from a self-tangling script is passed over.
|
46
|
+
def disassemble(lines)
|
47
|
+
directives, lines = find_directives(strip_boilerplate(lines), DEFAULT_DIRECTIVES.deep_copy)
|
48
|
+
docs, chunks = get_docs_and_chunks(lines)
|
49
|
+
return directives, docs, chunks
|
50
|
+
end
|
51
|
+
|
52
|
+
# Strip the boilerplate from self-tangling scripts before processing the rest of
|
53
|
+
# the document.
|
54
|
+
def strip_boilerplate(lines)
|
55
|
+
# if we find the __END__ directive in the lines, we return all the lines after
|
56
|
+
# that point
|
57
|
+
h = 0
|
58
|
+
if lines.find { |l| h += 1 ; l == "__END__\n" }
|
59
|
+
return lines[h..-1]
|
60
|
+
# otherwise this is not a self-tangling script so all the lines count
|
61
|
+
else
|
62
|
+
return lines
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Take the IO stream and search for any directives. The technique is to read
|
67
|
+
# the IO object one line at a time, passing over any blank lines and searching
|
68
|
+
# for any directives. The first non-blank, non-directive line is assumed the
|
69
|
+
# beginning of the documentation block and reinserted into the stream.
|
70
|
+
def find_directives(lines, directives)
|
71
|
+
while line = lines.shift
|
72
|
+
# skip blank lines
|
73
|
+
if %r{^[:blank:]*$}.match line
|
74
|
+
next
|
75
|
+
# process directive lines
|
76
|
+
elsif %r{^\{([[:print:]]+)[[:blank:]]+=>[[:blank:]]+([[:print:]]+)\}\n$}.match line
|
77
|
+
set_directive $1, $2, directives if directives
|
78
|
+
next
|
79
|
+
# restore the first line of documentation, return the directives
|
80
|
+
else
|
81
|
+
return directives, lines.unshift(line)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Take a given directive key/value pair and set if if valid. Valid keys are, at
|
87
|
+
# this point, "style" and "title". "style" may have the values "Plain" or\
|
88
|
+
# "XHTML" (not currently implemented).
|
89
|
+
def set_directive(key, value, directives)
|
90
|
+
case key
|
91
|
+
when "style"
|
92
|
+
if ["Plain", "XHTML"].include?(value)
|
93
|
+
directives[:style] = value
|
94
|
+
else
|
95
|
+
raise ArgumentError, "unknown style #{value}"
|
96
|
+
end
|
97
|
+
when "title"
|
98
|
+
directives[:title] = value
|
99
|
+
else
|
100
|
+
raise ArgumentError, "unknown directive '#{m[1]}''"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Break the main body of an RWEB document into the documentation intermediate
|
105
|
+
# form and a collection of unexpanded chunks.
|
106
|
+
def get_docs_and_chunks(lines)
|
107
|
+
docs = [] ; chunks = {}
|
108
|
+
# directives are already gone, so we start in documentation
|
109
|
+
while line = lines.shift
|
110
|
+
# if a chunk starts, push the name of the chunk into the docs and then
|
111
|
+
# extract the chunk
|
112
|
+
if %r{^\<\<([[:print:]]*)\{[[:blank:]]*$}.match line
|
113
|
+
name = $1.strip
|
114
|
+
docs.push "{<<#{name}>>}\n"
|
115
|
+
get_chunk lines, name, chunks, docs
|
116
|
+
# otherwise just push the line into the accumulated documentation
|
117
|
+
else
|
118
|
+
docs.push line
|
119
|
+
end
|
120
|
+
end
|
121
|
+
return docs, chunks
|
122
|
+
end
|
123
|
+
|
124
|
+
# Extract a detected chunk of code.
|
125
|
+
def get_chunk(lines, name, chunks, docs)
|
126
|
+
chunks[name] = [] unless chunks.has_key?(name)
|
127
|
+
while line = lines.shift
|
128
|
+
# if we find the closing tag of a chunk, drop it and return
|
129
|
+
if %r{^\}\>\>[[:blank:]]*$}.match line
|
130
|
+
return
|
131
|
+
# otherwise add the current line to the named chunk and add the code,
|
132
|
+
# marked, to the documentation array for weaving
|
133
|
+
else
|
134
|
+
chunks[name].push line
|
135
|
+
docs.push "}}} #{line}" # flag the code in the document stream
|
136
|
+
end
|
137
|
+
end
|
138
|
+
raise RuntimeError, "chunk '#{name}' not closed by end of input"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Take a raw chunk and expand all internal references.
|
142
|
+
def expand_chunk(chunk, chunks, prefix = "", chain = [])
|
143
|
+
out = []
|
144
|
+
while line = chunk.shift
|
145
|
+
# if we spot a chunk reference, validate it and expand its contents into the
|
146
|
+
# current chunk
|
147
|
+
if %r{^([[:print:][:blank:]]*)\{\<\<([[:print:]]+)\>\>\}([[:print:][:blank:]]*)$}.match line
|
148
|
+
name = $2.strip
|
149
|
+
# Illegal conditions are:
|
150
|
+
# 1. A chunk referenced which has not been defined.
|
151
|
+
raise RuntimeError, "chunk '#{name}' referenced without definition" unless chunks.has_key?(name)
|
152
|
+
# 2. A chunk reference with anything but whitespace in front or behind.
|
153
|
+
raise RuntimeError, "invalid prefix '#{$1.strip}' when referencing chunk '#{name}'" unless $1.strip == ""
|
154
|
+
raise RuntimeError, "invalid suffix '#{$3.strip}' when referencing chunk '#{name}'" unless $3.strip == ""
|
155
|
+
# 3. A circular expansion of a chunk.
|
156
|
+
raise RuntimeError, "circular expansion of chunk '#{name}'\nexpansion chain is:\n=> #{chain.join("\n=> ")}\n" if chain.include?(name)
|
157
|
+
chain.push name
|
158
|
+
out.push "#{prefix + $1}# #{name}\n"
|
159
|
+
out.push "#{prefix + $1}# #{"-" * name.length}\n"
|
160
|
+
out += expand_chunk(chunks[name], chunks, prefix + $1, chain)
|
161
|
+
chain.pop
|
162
|
+
else
|
163
|
+
out.push prefix + line
|
164
|
+
end
|
165
|
+
end
|
166
|
+
out
|
167
|
+
end
|
168
|
+
|
169
|
+
# Generate plaintext documentation from the documentation intermediate form.
|
170
|
+
# The following actions are taken:
|
171
|
+
# 1. The title is placed at the top and double-underlined.
|
172
|
+
# 2. Documentation lines are passed through unaltered.
|
173
|
+
# 3. Chunk names are emitted without the tags and with single-underlining.
|
174
|
+
# 4. Chunk contents are emitted with "> " prepended.
|
175
|
+
def generate_plain_document(docs, directives)
|
176
|
+
raise RuntimeError, "style directive not set to Plain" unless directives[:style] == "Plain"
|
177
|
+
out = []
|
178
|
+
title = directives[:title]
|
179
|
+
|
180
|
+
out << ["#{title}\n","#{"="*title.length}\n"]
|
181
|
+
docs.each do |line|
|
182
|
+
# if we find a chunk reference, extract the name and underline it, the
|
183
|
+
# mainline chunk being given the title "RWEB Mainline"
|
184
|
+
if %r{^\{\<\<([[:print:]]*)\>\>\}\n$}.match line
|
185
|
+
line = $1
|
186
|
+
line = "RWEB Mainline" if line == ""
|
187
|
+
out << ["#{line}\n","#{"-"*line.length}\n"]
|
188
|
+
# if we find a line of code, emit it with added "chicken feet" to the front.
|
189
|
+
elsif %r{^\}\}\} (.*)\n$}.match line
|
190
|
+
out << ["> #{$1}\n"]
|
191
|
+
# otherwise just emit the line
|
192
|
+
else
|
193
|
+
out << line
|
194
|
+
end
|
195
|
+
end
|
196
|
+
out.to_s
|
197
|
+
end
|
198
|
+
|
199
|
+
# Generate XHTML documentation from the documentation intermediate form.
|
200
|
+
# NOT YET IMPLEMENTED!
|
201
|
+
def generate_xhtml_document(docs, directives)
|
202
|
+
raise RuntimeError, "style directive not set to XHTML" unless directives[:style] == "XHTML"
|
203
|
+
raise NotImplementedError, "document style not implemented"
|
204
|
+
end
|
205
|
+
|
206
|
+
=begin rdoc
|
207
|
+
The RWEB module is the public interface to the RWEB library and contains only
|
208
|
+
three publicly-accessible symbols. First, the version of the library is
|
209
|
+
provided (for build purposes). Second, the RWEB.tangle function is exposed
|
210
|
+
which takes an IO object and returns a string containing the tangled code ready
|
211
|
+
for execution. Third, the RWEB.weave function is exposed which takes an IO
|
212
|
+
object and returns a string containing the documentation in the appropriate
|
213
|
+
format.
|
214
|
+
=end
|
215
|
+
module RWEB
|
216
|
+
|
217
|
+
VERSION = "0.1.0"
|
218
|
+
|
219
|
+
# Tangle an RWEB document and return the code as a string.
|
220
|
+
def RWEB.tangle(io)
|
221
|
+
directives, docs, chunks = disassemble(io.readlines)
|
222
|
+
raise RuntimeError, "no mainline chunk found in file" unless chunks.has_key?("")
|
223
|
+
"# #{directives[:title]}\n# #{"=" * directives[:title].length}\n\n" + expand_chunk(chunks[""], chunks).to_s
|
224
|
+
end
|
225
|
+
|
226
|
+
# Weave the RWEB document into another format according to directives and
|
227
|
+
# return as a string.
|
228
|
+
def RWEB.weave(io)
|
229
|
+
directives, docs, chunks = disassemble(io.readlines)
|
230
|
+
eval "generate_#{directives[:style].downcase}_document(docs, directives)"
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
data/tests/tc_rweb.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rweb'
|
2
|
+
require 'stringio'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
class TestRWEB < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
|
9
|
+
@pure_plaintext = <<-END
|
10
|
+
This is an RWEB document that contains nothing but documentation. There are no
|
11
|
+
directives nor are there any code chunks. What comes out when weaving should be
|
12
|
+
identical to what goes in except for adding a title.
|
13
|
+
END
|
14
|
+
|
15
|
+
@plaintext_with_directives1 =<<-END
|
16
|
+
{style => Plain}
|
17
|
+
{title => My Title}
|
18
|
+
This is an RWEB document that contains documentation and valid directives. There
|
19
|
+
are no code chunks. What comes out when weaving should be identical to what goes
|
20
|
+
in except for adding a title and removing directives.
|
21
|
+
END
|
22
|
+
|
23
|
+
@plaintext_with_directives2 =<<-END
|
24
|
+
{style => XHTML}
|
25
|
+
{title => My Title}
|
26
|
+
This is an RWEB document that contains documentation and valid directives, but
|
27
|
+
with the style being an unimplemented one. There are no code chunks. What comes
|
28
|
+
out when weaving should be identical to what goes in except for adding a title
|
29
|
+
and removing directives.
|
30
|
+
END
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_pure_plaintext
|
35
|
+
out = RWEB.weave(StringIO.new(@pure_plaintext))
|
36
|
+
out = out.split("\n")
|
37
|
+
assert_equal("Untitled", out.shift)
|
38
|
+
assert_equal("========", out.shift)
|
39
|
+
assert_equal(@pure_plaintext.split("\n"), out)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_plaintext_with_directives
|
43
|
+
out = []
|
44
|
+
assert_nothing_raised {
|
45
|
+
out = RWEB.weave(StringIO.new(@plaintext_with_directives1))
|
46
|
+
out = out.split("\n")
|
47
|
+
}
|
48
|
+
assert_equal("My Title", out.shift)
|
49
|
+
assert_equal("========", out.shift)
|
50
|
+
assert_equal(@plaintext_with_directives1.split("\n")[2..-1], out)
|
51
|
+
|
52
|
+
assert_raise(NotImplementedError) {
|
53
|
+
RWEB.weave(StringIO.new(@plaintext_with_directives2))
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|