rweb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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."
@@ -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."
@@ -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
@@ -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