rweb 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/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
|