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.
@@ -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