lmt 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +8 -2
- data/Rakefile +4 -3
- data/doc/lmt/lmt.rb.md +439 -19
- data/doc/lmt/lmt_expressions.md +41 -3
- data/doc/lmt/lmw.rb.md +21 -14
- data/lib/lmt/lmt.rb +151 -14
- data/lib/lmt/lmw.rb +22 -15
- data/lib/lmt/version.rb +1 -1
- data/src/lmt/lmt.rb.lmd +377 -19
- data/src/lmt/lmt_expressions.lmd +31 -3
- data/src/lmt/lmw.rb.lmd +21 -14
- metadata +2 -2
data/doc/lmt/lmt_expressions.md
CHANGED
@@ -12,19 +12,57 @@ The first regular expression handles the detection of include directives. It re
|
|
12
12
|
/^!\s+include\s+\[.*\]\((.*)\)\s*$/
|
13
13
|
```
|
14
14
|
|
15
|
+
## The Block Expressions
|
16
|
+
|
17
|
+
These regular expression recognize if, elseif, else, and end directives. The first group contains the conditional for blocks that have a conditional..
|
18
|
+
|
19
|
+
###### Code Block: If Expression
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
/^!\s+if\s+(.*)$/
|
23
|
+
```
|
24
|
+
|
25
|
+
###### Code Block: Else Expression
|
26
|
+
|
27
|
+
``` ruby
|
28
|
+
/^!\s+else/
|
29
|
+
```
|
30
|
+
|
31
|
+
###### Code Block: Elsif Expression
|
32
|
+
|
33
|
+
``` ruby
|
34
|
+
/^!\s+elsif\s+(.*)$/
|
35
|
+
```
|
36
|
+
|
37
|
+
###### Code Block: End Expression
|
38
|
+
|
39
|
+
``` ruby
|
40
|
+
/^!\s+end$/
|
41
|
+
```
|
42
|
+
|
15
43
|
## The Code Block Expression
|
16
44
|
|
17
|
-
|
45
|
+
This expression is intended to note when whe enter or leave a code block. It detects markdown code fences and processes the special directives. It has four groups. The first identifies white space at the beginning of the line. The second detects the language. The third determines if this is a replacement. The fourth is the name of the block (if applicable).
|
18
46
|
|
19
47
|
###### Code Block: Code Block Expression
|
20
48
|
|
21
49
|
``` ruby
|
22
|
-
/^(
|
50
|
+
/^(\s*)``` ?([\w]*) ?(=?)([-\w]*)?/
|
51
|
+
```
|
52
|
+
|
53
|
+
## The Extension Expression
|
54
|
+
|
55
|
+
This expression identifies blocks of code which are to be executed. The first group identifies white space at the beginning of the line.
|
56
|
+
|
57
|
+
###### Code Block: Extension Expression
|
58
|
+
|
59
|
+
``` ruby
|
60
|
+
/^(\s*)``` ruby !/
|
23
61
|
```
|
24
62
|
|
25
63
|
## The Macro Substitution Expression
|
26
64
|
|
27
|
-
|
65
|
+
This identifies macro expansions surrounded with `⦅` and `⦆`. The first bit deals with making sure that the opening `⦅` isn't escaped. Then there is one group which contains the name of the macro combined and any filters which are being used.
|
28
66
|
|
29
67
|
###### Code Block: Macro Substitution Expression
|
30
68
|
|
data/doc/lmt/lmw.rb.md
CHANGED
@@ -298,6 +298,7 @@ We will match the lines against the expressions and, when a match occurs, we wil
|
|
298
298
|
def substitute_directives_and_headers(lines)
|
299
299
|
include_expression = ⦅include_expression⦆
|
300
300
|
code_block_expression = ⦅code_block_expression⦆
|
301
|
+
extension_block_expression = ⦅extension_expression⦆
|
301
302
|
in_block = false
|
302
303
|
block_name = ""
|
303
304
|
lines.map do |line|
|
@@ -319,28 +320,34 @@ def substitute_directives_and_headers(lines)
|
|
319
320
|
end
|
320
321
|
```
|
321
322
|
|
322
|
-
#### The
|
323
|
+
#### The Header for Code Blocks
|
323
324
|
|
324
325
|
Code blocks need to be headed appropriately as markdown parsing eats the code block name. Because of this we put it in a `h6` header. When the block is repeated, we add a `(part n)` to the end. We also should be adding links for the next and last version of this header.
|
325
326
|
|
326
327
|
###### Code Block: Make Code Block Header
|
327
328
|
|
328
329
|
``` ruby
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
" Replacing"
|
334
|
-
else
|
335
|
-
""
|
336
|
-
end
|
337
|
-
header = if name != ""
|
338
|
-
"#######{replacing} Code Block: #{human_name}\n\n"
|
330
|
+
if line =~ extension_block_expression
|
331
|
+
white_space = extension_block_expression.match(line)[1]
|
332
|
+
header = "###### Execute Extension Block\n\n"
|
333
|
+
[header, "#{white_space}``` ruby\n"]
|
339
334
|
else
|
340
|
-
|
335
|
+
white_space, language, replacement_mark, name =
|
336
|
+
code_block_expression.match(line)[1..-1]
|
337
|
+
human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
|
338
|
+
replacing = if replacement_mark == "="
|
339
|
+
" Replacing"
|
340
|
+
else
|
341
|
+
""
|
342
|
+
end
|
343
|
+
header = if name != ""
|
344
|
+
"#######{replacing} Code Block: #{human_name}\n\n"
|
345
|
+
else
|
346
|
+
"#######{replacing} Output Block\n\n"
|
347
|
+
end
|
348
|
+
[header,
|
349
|
+
"#{white_space}``` #{language}\n"]
|
341
350
|
end
|
342
|
-
[header,
|
343
|
-
"#{white_space}``` #{language}\n"]
|
344
351
|
```
|
345
352
|
|
346
353
|
### Replacing the Markdown Links
|
data/lib/lmt/lmt.rb
CHANGED
@@ -16,6 +16,7 @@ class Tangle
|
|
16
16
|
main do
|
17
17
|
check_arguments()
|
18
18
|
begin
|
19
|
+
@dev = options[:dev]
|
19
20
|
self_test()
|
20
21
|
tangler = Tangle::Tangler.new(options[:file])
|
21
22
|
tangler.tangle()
|
@@ -58,9 +59,39 @@ class Tangle
|
|
58
59
|
report_self_test_failure("Should be able to place two macros on the same line") unless two_macros == "foo foo"
|
59
60
|
string_with_backslash = "this string ends in \\."
|
60
61
|
report_self_test_failure("ruby escape doesn't escape backslash") unless string_with_backslash =~ /\\.?/
|
62
|
+
some_text = "some text"
|
63
|
+
report_self_test_failure("Double quote doesn't double quote") unless some_text == "some text"
|
64
|
+
some_indented_text = " some text"
|
65
|
+
report_self_test_failure("Indent lines should add two spaces to lines") unless some_indented_text == " some text"
|
66
|
+
items = ["item 1",
|
67
|
+
"item 2",]
|
68
|
+
report_self_test_failure("Add comma isn't adding commas") unless items == ["item 1", "item 2"]
|
61
69
|
included_string = "I came from lmt_include.lmd"
|
62
70
|
report_self_test_failure("included replacements should replace blocks") unless included_string == "I came from lmt_include.lmd"
|
71
|
+
|
72
|
+
from_extension = true
|
63
73
|
|
74
|
+
report_self_test_failure("extension hook should be able to add blocks") unless from_extension
|
75
|
+
conditional_output_else = true
|
76
|
+
conditional_output_elsif = true
|
77
|
+
report_self_test_failure("conditional output elseif should not be output when elseif is false") unless conditional_output_elsif
|
78
|
+
report_self_test_failure("conditional output else should not be output when if true") unless conditional_output_else
|
79
|
+
conditional_output_else = true
|
80
|
+
conditional_output_elsif = true
|
81
|
+
report_self_test_failure("conditional output elsif should not be output even if true when is also true") unless conditional_output_elsif
|
82
|
+
report_self_test_failure("conditional output else should not be output when if is true (if and elseif)") unless conditional_output_else
|
83
|
+
conditional_output_if = true
|
84
|
+
conditional_output_else = true
|
85
|
+
conditional_output_elsif = true
|
86
|
+
report_self_test_failure("conditional output if should not be output when false") unless conditional_output_if
|
87
|
+
report_self_test_failure("conditional output elseif should be output when elseif is true") unless conditional_output_elsif
|
88
|
+
report_self_test_failure("conditional output else should not be output when elseif is true") unless conditional_output_else
|
89
|
+
conditional_output_if = true
|
90
|
+
conditional_output_elsif = true
|
91
|
+
conditional_output_else = true
|
92
|
+
report_self_test_failure("conditional output if should not be output when false") unless conditional_output_if
|
93
|
+
report_self_test_failure("conditional output elseif should not be output when elseif is false") unless conditional_output_elsif
|
94
|
+
report_self_test_failure("conditional output else should be output when neither if nor elseif is true") unless conditional_output_else
|
64
95
|
end
|
65
96
|
|
66
97
|
def self.report_self_test_failure(message)
|
@@ -89,17 +120,29 @@ class Tangle
|
|
89
120
|
end
|
90
121
|
|
91
122
|
class Tangler
|
92
|
-
class << self
|
93
|
-
attr_reader :filters
|
94
|
-
end
|
95
|
-
|
96
|
-
@filters = {
|
97
|
-
'ruby_escape' => LineFilter.new do |line|
|
98
|
-
line.dump[1..-2]
|
99
|
-
end
|
100
|
-
}
|
101
|
-
|
102
123
|
def initialize(input)
|
124
|
+
@extension_context = Context.new()
|
125
|
+
@extension_context.filters = {
|
126
|
+
'ruby_escape' => LineFilter.new do |line|
|
127
|
+
line.dump[1..-2]
|
128
|
+
end,
|
129
|
+
'double_quote' => LineFilter.new do |line|
|
130
|
+
before_white = /^\s*/.match(line)[0]
|
131
|
+
after_white = /\s*$/.match(line)[0]
|
132
|
+
"#{before_white}\"#{line.strip}\"#{after_white}"
|
133
|
+
end,
|
134
|
+
'add_comma' => LineFilter.new do |line|
|
135
|
+
before_white = /^\s*/.match(line)[0]
|
136
|
+
after_white = /\s*$/.match(line)[0]
|
137
|
+
"#{before_white}#{line.strip},#{after_white}"
|
138
|
+
end,
|
139
|
+
'indent_continuation' => Filter.new do |lines|
|
140
|
+
[lines[0], *lines[1..-1].map {|l| " #{l}"}]
|
141
|
+
end,
|
142
|
+
'indent_lines' => LineFilter.new do |line|
|
143
|
+
" #{line}"
|
144
|
+
end
|
145
|
+
}
|
103
146
|
@input = input
|
104
147
|
@block = ""
|
105
148
|
@blocks = {}
|
@@ -107,8 +150,9 @@ class Tangle
|
|
107
150
|
end
|
108
151
|
|
109
152
|
def tangle()
|
110
|
-
contents =
|
111
|
-
|
153
|
+
contents = handle_extensions_and_conditionals(
|
154
|
+
include_includes(read_file(@input)))
|
155
|
+
@block, @blocks = @extension_context.parse_hook(*parse_blocks(contents))
|
112
156
|
if @block
|
113
157
|
@block = expand_macros(@block)
|
114
158
|
@block = unescape_double_parens(@block)
|
@@ -136,8 +180,41 @@ class Tangle
|
|
136
180
|
end.flatten(1)
|
137
181
|
end
|
138
182
|
|
183
|
+
def handle_extensions_and_conditionals(lines)
|
184
|
+
extension_expression = /^(\s*)``` ruby !/
|
185
|
+
condition_processor = ConditionalProcessor.new(@extension_context)
|
186
|
+
extension_exit_expression = /```/
|
187
|
+
in_extension_block = false
|
188
|
+
current_extension_block = []
|
189
|
+
|
190
|
+
other_lines = lines.lazy
|
191
|
+
.find_all do |line|
|
192
|
+
condition_processor.should_output(line)
|
193
|
+
end.find_all do |line|
|
194
|
+
unless in_extension_block
|
195
|
+
in_extension_block = line =~ extension_expression
|
196
|
+
if in_extension_block
|
197
|
+
current_extension_block = []
|
198
|
+
end
|
199
|
+
!in_extension_block
|
200
|
+
else
|
201
|
+
in_extension_block = !(line =~ extension_exit_expression)
|
202
|
+
if in_extension_block
|
203
|
+
current_extension_block << line
|
204
|
+
else
|
205
|
+
@extension_context.get_binding.eval(current_extension_block.join)
|
206
|
+
end
|
207
|
+
false
|
208
|
+
end
|
209
|
+
end.force
|
210
|
+
|
211
|
+
condition_processor.check_block_balance()
|
212
|
+
|
213
|
+
other_lines
|
214
|
+
end
|
215
|
+
|
139
216
|
def parse_blocks(lines)
|
140
|
-
code_block_exp = /^(
|
217
|
+
code_block_exp = /^(\s*)``` ?([\w]*) ?(=?)([-\w]*)?/
|
141
218
|
in_block = false
|
142
219
|
blocks = lines.find_all do |line|
|
143
220
|
in_block = !in_block if line =~ code_block_exp
|
@@ -176,7 +253,7 @@ class Tangle
|
|
176
253
|
|
177
254
|
def apply_filters(strings, filters)
|
178
255
|
filters.map do |filter_name|
|
179
|
-
|
256
|
+
filters_map[filter_name]
|
180
257
|
end.inject(strings) do |strings, filter|
|
181
258
|
filter.filter(strings)
|
182
259
|
end
|
@@ -199,6 +276,9 @@ class Tangle
|
|
199
276
|
|
200
277
|
|
201
278
|
private
|
279
|
+
def filters_map
|
280
|
+
@extension_context.filters
|
281
|
+
end
|
202
282
|
def get_last_replacement_index(bodies)
|
203
283
|
last_replacement = bodies.each_with_index
|
204
284
|
.select do |((_, replacement_mark, _), _)|
|
@@ -246,7 +326,64 @@ class Tangle
|
|
246
326
|
end
|
247
327
|
end
|
248
328
|
end
|
329
|
+
|
330
|
+
class ConditionalProcessor
|
331
|
+
|
332
|
+
def initialize(extension_context)
|
333
|
+
@if_expression = /^!\s+if\s+(.*)$/
|
334
|
+
@elsif_expression = /^!\s+elsif\s+(.*)$/
|
335
|
+
@else_expression = /^!\s+else/
|
336
|
+
@end_expression = /^!\s+end$/
|
337
|
+
@output_enabled = true
|
338
|
+
@stack = []
|
339
|
+
@extension_context = extension_context
|
340
|
+
end
|
341
|
+
|
342
|
+
def should_output(line)
|
343
|
+
case line
|
344
|
+
when @if_expression
|
345
|
+
condition = $1
|
346
|
+
prior_state = @output_enabled
|
347
|
+
@output_enabled = !!@extension_context.get_binding.eval(condition)
|
348
|
+
@stack.push([:if, prior_state, !@output_enabled])
|
349
|
+
when @elsif_expression
|
350
|
+
throw "elsif statement missing if" if @stack.empty?
|
351
|
+
condition = $1
|
352
|
+
type, prior_state, execute_else = @stack.pop()
|
353
|
+
@output_enabled = execute_else && !!@extension_context.get_binding.eval(condition)
|
354
|
+
@stack.push([type, prior_state, execute_else && !@output_enabled])
|
355
|
+
when @else_expression
|
356
|
+
throw "else statement missing if" if @stack.empty?
|
357
|
+
type, prior_state, execute_else = @stack.pop()
|
358
|
+
@output_enabled = execute_else
|
359
|
+
@stack.push([type, prior_state, execute_else])
|
360
|
+
when @end_expression
|
361
|
+
throw "end statement missing begin" if @stack.empty?
|
362
|
+
type, prior_state, execute_else = @stack.pop()
|
363
|
+
@output_enabled = prior_state
|
364
|
+
end
|
365
|
+
@output_enabled
|
366
|
+
end
|
367
|
+
|
368
|
+
def check_block_balance
|
369
|
+
throw "unbalanced blocks" unless @stack.empty?
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
class Context
|
376
|
+
attr_accessor :filters
|
377
|
+
|
378
|
+
def get_binding
|
379
|
+
binding
|
380
|
+
end
|
381
|
+
|
382
|
+
def parse_hook(main_block, blocks)
|
383
|
+
[main_block, blocks]
|
384
|
+
end
|
249
385
|
end
|
386
|
+
|
250
387
|
|
251
388
|
def self.required(*options)
|
252
389
|
@required_options = options
|
data/lib/lmt/lmw.rb
CHANGED
@@ -97,7 +97,7 @@ class Lmw
|
|
97
97
|
|
98
98
|
def find_blocks(lines)
|
99
99
|
lines_with_includes = include_includes(lines)
|
100
|
-
code_block_exp = /^(
|
100
|
+
code_block_exp = /^(\s*)``` ?([\w]*) ?(=?)([-\w]*)?/
|
101
101
|
headers_and_footers = lines_with_includes.filter do |(line, source_file)|
|
102
102
|
code_block_exp =~ line
|
103
103
|
end
|
@@ -122,7 +122,8 @@ class Lmw
|
|
122
122
|
end
|
123
123
|
def substitute_directives_and_headers(lines)
|
124
124
|
include_expression = /^!\s+include\s+\[.*\]\((.*)\)\s*$/
|
125
|
-
code_block_expression = /^(
|
125
|
+
code_block_expression = /^(\s*)``` ?([\w]*) ?(=?)([-\w]*)?/
|
126
|
+
extension_block_expression = /^(\s*)``` ruby !/
|
126
127
|
in_block = false
|
127
128
|
block_name = ""
|
128
129
|
lines.map do |line|
|
@@ -133,21 +134,27 @@ class Lmw
|
|
133
134
|
when code_block_expression
|
134
135
|
in_block = !in_block
|
135
136
|
if in_block
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
" Replacing"
|
141
|
-
else
|
142
|
-
""
|
143
|
-
end
|
144
|
-
header = if name != ""
|
145
|
-
"#######{replacing} Code Block: #{human_name}\n\n"
|
137
|
+
if line =~ extension_block_expression
|
138
|
+
white_space = extension_block_expression.match(line)[1]
|
139
|
+
header = "###### Execute Extension Block\n\n"
|
140
|
+
[header, "#{white_space}``` ruby\n"]
|
146
141
|
else
|
147
|
-
|
142
|
+
white_space, language, replacement_mark, name =
|
143
|
+
code_block_expression.match(line)[1..-1]
|
144
|
+
human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
|
145
|
+
replacing = if replacement_mark == "="
|
146
|
+
" Replacing"
|
147
|
+
else
|
148
|
+
""
|
149
|
+
end
|
150
|
+
header = if name != ""
|
151
|
+
"#######{replacing} Code Block: #{human_name}\n\n"
|
152
|
+
else
|
153
|
+
"#######{replacing} Output Block\n\n"
|
154
|
+
end
|
155
|
+
[header,
|
156
|
+
"#{white_space}``` #{language}\n"]
|
148
157
|
end
|
149
|
-
[header,
|
150
|
-
"#{white_space}``` #{language}\n"]
|
151
158
|
else
|
152
159
|
[line]
|
153
160
|
end
|
data/lib/lmt/version.rb
CHANGED
data/src/lmt/lmt.rb.lmd
CHANGED
@@ -22,15 +22,15 @@ In order to be useful for literate programming we need a few features:
|
|
22
22
|
4. The ability to to identify code blocks which will be expanded when referenced
|
23
23
|
5. The ability to append to or replace code blocks
|
24
24
|
6. The ability to include another file.
|
25
|
+
7. The ability to extend the tangler with Ruby code from a block.
|
26
|
+
8. Simple conditional logic to enable output only under certain circumstances
|
25
27
|
|
26
28
|
There are also a few potentially useful features that are not implemented but might be in the future:
|
27
29
|
|
28
|
-
1. The ability to
|
29
|
-
2.
|
30
|
-
3.
|
31
|
-
4.
|
32
|
-
|
33
|
-
Also, the only filter currently existing just escapes strings for ruby code. There are many more that could be useful.
|
30
|
+
1. The ability to write out other files.
|
31
|
+
2. Source mapping
|
32
|
+
3. Further source verification. For instance, all instances of the same block should be in the same language. Also, detect and prevent double inclusion.
|
33
|
+
4. include path semantics.
|
34
34
|
|
35
35
|
### Blocks
|
36
36
|
|
@@ -38,7 +38,7 @@ Markdown already supports code blocks expressed with code fences starting with t
|
|
38
38
|
|
39
39
|
There are two types of blocks: the default block and macro blocks.
|
40
40
|
|
41
|
-
|
41
|
+
Output begins with the default block. It is simply a markdown code block which has no macro name. with no further information. It looks like this.
|
42
42
|
|
43
43
|
``` ruby
|
44
44
|
#Output starts here
|
@@ -114,7 +114,11 @@ There are a few built in filters:
|
|
114
114
|
|
115
115
|
``` ruby filter_list
|
116
116
|
{
|
117
|
-
'ruby_escape' => ⦅ruby_escape
|
117
|
+
'ruby_escape' => ⦅ruby_escape⦆,
|
118
|
+
'double_quote' => ⦅double_quote⦆,
|
119
|
+
'add_comma' => ⦅add_comma⦆,
|
120
|
+
'indent_continuation' => ⦅indent_continuation⦆,
|
121
|
+
'indent_lines' => ⦅indent_lines⦆
|
118
122
|
}
|
119
123
|
```
|
120
124
|
|
@@ -130,6 +134,55 @@ included_string = "I am in lmt.lmd"
|
|
130
134
|
|
131
135
|
! include [an include](lmt_include.lmd)
|
132
136
|
|
137
|
+
### Extension
|
138
|
+
|
139
|
+
In order to extend the tangler, it must be possible to mark a block for evaluation. To do so we will extend the mechanism to indicate replacement. Let's use `!`. A block simply named `!` will just be executed within a contained scope. Within this scope, it will be possible to access the map of filters through the `@filters` variable. All of these blocks are executed and removed from the stream before any further processing is done.
|
140
|
+
|
141
|
+
However, after blocks have been parsed, the map of blocks will be passed to the `parse_hook` method which an extension may define. It will be passed a two arguments. The first is an array with the lines of the main block, the second is a map of block name to line arrays. It is expected to return the same data structure in a two value array. An example parse hook which adds a block to the list of know blocks follows:
|
142
|
+
|
143
|
+
``` ruby !
|
144
|
+
def parse_hook(main_block, blocks)
|
145
|
+
blocks["from_extension"] = ["from_extension = true\n"]
|
146
|
+
[main_block, blocks]
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
### Conditional Output
|
151
|
+
|
152
|
+
Under certain circumstances it is useful to have certain output only happen under certain circumstances. For instance, a file prepared for Windows might have slightly different content than the same file prepared for Linux. In order to enable this, a variable may be set within an extension block and then output may be enabled / disabled using directives based on them.
|
153
|
+
|
154
|
+
``` ruby !
|
155
|
+
@a_variable = true
|
156
|
+
@another_variable = false
|
157
|
+
```
|
158
|
+
|
159
|
+
We can then disable and enable output using the if, else, elsif, and end directives. The if directive takes a line of ruby code, executes it.
|
160
|
+
|
161
|
+
! if @a_variable
|
162
|
+
|
163
|
+
Since a_variable is true, the next block will be processed.
|
164
|
+
|
165
|
+
``` ruby conditional_output
|
166
|
+
conditional_output_else = true
|
167
|
+
conditional_output_elsif = true
|
168
|
+
```
|
169
|
+
|
170
|
+
! elsif @another_variable
|
171
|
+
|
172
|
+
``` ruby conditional_output
|
173
|
+
conditional_output_elsif = false
|
174
|
+
```
|
175
|
+
|
176
|
+
! else
|
177
|
+
|
178
|
+
And the following block will have no effect.
|
179
|
+
|
180
|
+
``` ruby conditional_output
|
181
|
+
conditional_output_else = false
|
182
|
+
```
|
183
|
+
|
184
|
+
! end
|
185
|
+
|
133
186
|
### Self Test
|
134
187
|
|
135
188
|
Of course, we will also need a testing procedure. Since this is written as a literate program, our test procedure is: can we tangle ourself. If the output of the tangler run on this file can tangle this file, then we know that the tangler works.
|
@@ -202,6 +255,8 @@ class Tangle
|
|
202
255
|
|
203
256
|
⦅tangle_class⦆
|
204
257
|
|
258
|
+
⦅context_class⦆
|
259
|
+
|
205
260
|
⦅option_verification⦆
|
206
261
|
|
207
262
|
description "⦅description⦆"
|
@@ -223,6 +278,7 @@ This is a basic template using the [Ruby methadone](https://github.com/davetron5
|
|
223
278
|
The main body will first test itself then, invoke the library component, which isn't in lib as traditional because it is in this file and I don't want to move it around.
|
224
279
|
|
225
280
|
``` ruby main_body
|
281
|
+
@dev = options[:dev]
|
226
282
|
self_test()
|
227
283
|
tangler = Tangle::Tangler.new(options[:file])
|
228
284
|
tangler.tangle()
|
@@ -245,16 +301,11 @@ The tangler is defined within a class that contains the tangling implementation.
|
|
245
301
|
|
246
302
|
``` ruby tangle_class
|
247
303
|
class Tangler
|
248
|
-
class << self
|
249
|
-
attr_reader :filters
|
250
|
-
end
|
251
|
-
|
252
|
-
@filters = ⦅filter_list⦆
|
253
|
-
|
254
304
|
⦅initializer⦆
|
255
305
|
⦅tangle⦆
|
256
306
|
⦅read_file⦆
|
257
307
|
⦅include_includes⦆
|
308
|
+
⦅handle_extensions_and_conditionals⦆
|
258
309
|
⦅parse_blocks⦆
|
259
310
|
⦅expand_macros⦆
|
260
311
|
⦅apply_filters⦆
|
@@ -262,7 +313,12 @@ class Tangler
|
|
262
313
|
⦅write⦆
|
263
314
|
|
264
315
|
private
|
316
|
+
def filters_map
|
317
|
+
@extension_context.filters
|
318
|
+
end
|
265
319
|
⦅tangle_class_privates⦆
|
320
|
+
|
321
|
+
⦅conditional_processor⦆
|
266
322
|
end
|
267
323
|
```
|
268
324
|
|
@@ -272,6 +328,8 @@ The initializer takes in the input file and sets up our state. We are keeping t
|
|
272
328
|
|
273
329
|
``` ruby initializer
|
274
330
|
def initialize(input)
|
331
|
+
@extension_context = Context.new()
|
332
|
+
@extension_context.filters = ⦅filter_list⦆
|
275
333
|
@input = input
|
276
334
|
@block = ""
|
277
335
|
@blocks = {}
|
@@ -282,12 +340,13 @@ end
|
|
282
340
|
|
283
341
|
### Tangle
|
284
342
|
|
285
|
-
Now we have the basic tangle process wherein a file is read, includes are substituted, the blocks extracted, macros expanded recursively, and escaped double parentheses unescaped. If there is no default block, then there is no further work to be done.
|
343
|
+
Now we have the basic tangle process wherein a file is read, includes are substituted, extensions and conditionals are processed, the blocks extracted, the extension hook called, macros expanded recursively, and escaped double parentheses unescaped. If there is no default block, then there is no further work to be done.
|
286
344
|
|
287
345
|
``` ruby tangle
|
288
346
|
def tangle()
|
289
|
-
contents =
|
290
|
-
|
347
|
+
contents = handle_extensions_and_conditionals(
|
348
|
+
include_includes(read_file(@input)))
|
349
|
+
@block, @blocks = @extension_context.parse_hook(*parse_blocks(contents))
|
291
350
|
if @block
|
292
351
|
@block = expand_macros(@block)
|
293
352
|
@block = unescape_double_parens(@block)
|
@@ -335,6 +394,116 @@ end
|
|
335
394
|
|
336
395
|
```
|
337
396
|
|
397
|
+
### Evaling the Extensions and Processing the Conditionals
|
398
|
+
|
399
|
+
The extensions are executed within the following context. This context is also
|
400
|
+
used to evaluate conditionals.
|
401
|
+
|
402
|
+
``` ruby context_class
|
403
|
+
class Context
|
404
|
+
attr_accessor :filters
|
405
|
+
|
406
|
+
def get_binding
|
407
|
+
binding
|
408
|
+
end
|
409
|
+
|
410
|
+
def parse_hook(main_block, blocks)
|
411
|
+
[main_block, blocks]
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
```
|
416
|
+
|
417
|
+
Because conditional processing must occur concurrently with bock evaling, we have to build up each block and eval it the moment it is complete. To do so, we find all the lines that are not in an extension block. When we enter a new extension block, we clear the current extension block, and when we leave an extension block, we eval it.
|
418
|
+
|
419
|
+
``` ruby handle_extensions_and_conditionals
|
420
|
+
def handle_extensions_and_conditionals(lines)
|
421
|
+
extension_expression = ⦅extension_expression⦆
|
422
|
+
condition_processor = ConditionalProcessor.new(@extension_context)
|
423
|
+
extension_exit_expression = /```/
|
424
|
+
in_extension_block = false
|
425
|
+
current_extension_block = []
|
426
|
+
|
427
|
+
other_lines = lines.lazy
|
428
|
+
.find_all do |line|
|
429
|
+
condition_processor.should_output(line)
|
430
|
+
end.find_all do |line|
|
431
|
+
unless in_extension_block
|
432
|
+
in_extension_block = line =~ extension_expression
|
433
|
+
if in_extension_block
|
434
|
+
current_extension_block = []
|
435
|
+
end
|
436
|
+
!in_extension_block
|
437
|
+
else
|
438
|
+
in_extension_block = !(line =~ extension_exit_expression)
|
439
|
+
if in_extension_block
|
440
|
+
current_extension_block << line
|
441
|
+
else
|
442
|
+
@extension_context.get_binding.eval(current_extension_block.join)
|
443
|
+
end
|
444
|
+
false
|
445
|
+
end
|
446
|
+
end.force
|
447
|
+
|
448
|
+
condition_processor.check_block_balance()
|
449
|
+
|
450
|
+
other_lines
|
451
|
+
end
|
452
|
+
|
453
|
+
```
|
454
|
+
|
455
|
+
### Processing The Conditionals
|
456
|
+
|
457
|
+
To process the conditionals, we need a stack. Given that we are handling if, elsif, and else, we will need to track: 1) the type of statement (in case we want to add loops later), 2) the state before we encounter the if and, 3) if the else should be executed. For if statements, we can store these in an array like `[type, prior_state, execute_else]`
|
458
|
+
|
459
|
+
Since this process happens concurrently with evaling the included blocks, it's process is represented by a class. Should_output is the inside of the filter statement which is used to filter the lines.
|
460
|
+
|
461
|
+
``` ruby conditional_processor
|
462
|
+
class ConditionalProcessor
|
463
|
+
|
464
|
+
def initialize(extension_context)
|
465
|
+
@if_expression = ⦅if_expression⦆
|
466
|
+
@elsif_expression = ⦅elsif_expression⦆
|
467
|
+
@else_expression = ⦅else_expression⦆
|
468
|
+
@end_expression = ⦅end_expression⦆
|
469
|
+
@output_enabled = true
|
470
|
+
@stack = []
|
471
|
+
@extension_context = extension_context
|
472
|
+
end
|
473
|
+
|
474
|
+
def should_output(line)
|
475
|
+
case line
|
476
|
+
when @if_expression
|
477
|
+
condition = $1
|
478
|
+
prior_state = @output_enabled
|
479
|
+
@output_enabled = !!@extension_context.get_binding.eval(condition)
|
480
|
+
@stack.push([:if, prior_state, !@output_enabled])
|
481
|
+
when @elsif_expression
|
482
|
+
throw "elsif statement missing if" if @stack.empty?
|
483
|
+
condition = $1
|
484
|
+
type, prior_state, execute_else = @stack.pop()
|
485
|
+
@output_enabled = execute_else && !!@extension_context.get_binding.eval(condition)
|
486
|
+
@stack.push([type, prior_state, execute_else && !@output_enabled])
|
487
|
+
when @else_expression
|
488
|
+
throw "else statement missing if" if @stack.empty?
|
489
|
+
type, prior_state, execute_else = @stack.pop()
|
490
|
+
@output_enabled = execute_else
|
491
|
+
@stack.push([type, prior_state, execute_else])
|
492
|
+
when @end_expression
|
493
|
+
throw "end statement missing begin" if @stack.empty?
|
494
|
+
type, prior_state, execute_else = @stack.pop()
|
495
|
+
@output_enabled = prior_state
|
496
|
+
end
|
497
|
+
@output_enabled
|
498
|
+
end
|
499
|
+
|
500
|
+
def check_block_balance
|
501
|
+
throw "unbalanced blocks" unless @stack.empty?
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
```
|
506
|
+
|
338
507
|
### Parsing The Blocks
|
339
508
|
|
340
509
|
Now we get to the meat of the algorithm. This uses the regular expression in [lmt_expressions](lmt_expressions.lmd#The-Code-Block-Expression)
|
@@ -534,14 +703,13 @@ Filters are applied by the following method:
|
|
534
703
|
``` ruby apply_filters
|
535
704
|
def apply_filters(strings, filters)
|
536
705
|
filters.map do |filter_name|
|
537
|
-
|
706
|
+
filters_map[filter_name]
|
538
707
|
end.inject(strings) do |strings, filter|
|
539
708
|
filter.filter(strings)
|
540
709
|
end
|
541
710
|
end
|
542
711
|
```
|
543
712
|
|
544
|
-
|
545
713
|
### Ruby Escape
|
546
714
|
|
547
715
|
Ruby escape escapes strings appropriately for Ruby.
|
@@ -552,6 +720,50 @@ LineFilter.new do |line|
|
|
552
720
|
end
|
553
721
|
```
|
554
722
|
|
723
|
+
### Double Quote
|
724
|
+
|
725
|
+
Double quote surrounds strings in double quotes
|
726
|
+
|
727
|
+
``` ruby double_quote
|
728
|
+
LineFilter.new do |line|
|
729
|
+
before_white = /^\s*/.match(line)[0]
|
730
|
+
after_white = /\s*$/.match(line)[0]
|
731
|
+
"#{before_white}\"#{line.strip}\"#{after_white}"
|
732
|
+
end
|
733
|
+
```
|
734
|
+
|
735
|
+
### Add Commas
|
736
|
+
|
737
|
+
Add commas adds comma to the end of each line.
|
738
|
+
|
739
|
+
``` ruby add_comma
|
740
|
+
LineFilter.new do |line|
|
741
|
+
before_white = /^\s*/.match(line)[0]
|
742
|
+
after_white = /\s*$/.match(line)[0]
|
743
|
+
"#{before_white}#{line.strip},#{after_white}"
|
744
|
+
end
|
745
|
+
```
|
746
|
+
|
747
|
+
### Indent continuation
|
748
|
+
|
749
|
+
Adds two spaces to the front of each line after the first
|
750
|
+
|
751
|
+
``` ruby indent_continuation
|
752
|
+
Filter.new do |lines|
|
753
|
+
[lines[0], *lines[1..-1].map {|l| " #{l}"}]
|
754
|
+
end
|
755
|
+
```
|
756
|
+
|
757
|
+
### Indent Lines
|
758
|
+
|
759
|
+
Adds two spaces to the front of each line
|
760
|
+
|
761
|
+
``` ruby indent_lines
|
762
|
+
LineFilter.new do |line|
|
763
|
+
" #{line}"
|
764
|
+
end
|
765
|
+
```
|
766
|
+
|
555
767
|
## Option Verification
|
556
768
|
|
557
769
|
Option verification is described here:
|
@@ -574,6 +786,8 @@ Then we need the tests we are doing. The intentionally empty block is included
|
|
574
786
|
⦅test_filters⦆
|
575
787
|
⦅test_inclusion⦆
|
576
788
|
⦅intentionally_empty_block⦆
|
789
|
+
⦅test_extensions⦆
|
790
|
+
⦅test_conditional_output⦆
|
577
791
|
```
|
578
792
|
|
579
793
|
### Testing: Macros
|
@@ -612,9 +826,24 @@ foo
|
|
612
826
|
|
613
827
|
At the [top of the file](Filters) we described the usage of filters. Let's make sure that works. The extra `.?` in the regular expression is a workaround for an editor bug in Visual Studio Code, where, apparently, `/\\/` escapes the `/` rather than the `\`.... annoying.
|
614
828
|
|
829
|
+
``` text some_text
|
830
|
+
some text
|
831
|
+
```
|
832
|
+
|
833
|
+
``` text a_list
|
834
|
+
item 1
|
835
|
+
item 2
|
836
|
+
```
|
837
|
+
|
615
838
|
``` ruby test_filters
|
616
839
|
⦅filter_use_description⦆
|
617
840
|
report_self_test_failure("ruby escape doesn't escape backslash") unless string_with_backslash =~ /\\.?/
|
841
|
+
some_text = ⦅some_text | double_quote⦆
|
842
|
+
report_self_test_failure("Double quote doesn't double quote") unless some_text == "⦅some_text⦆"
|
843
|
+
some_indented_text = "⦅some_text | indent_lines⦆"
|
844
|
+
report_self_test_failure("Indent lines should add two spaces to lines") unless some_indented_text == " ⦅some_text⦆"
|
845
|
+
items = [⦅a_list | double_quote | add_comma | indent_continuation⦆]
|
846
|
+
report_self_test_failure("Add comma isn't adding commas") unless items == ["item 1", "item 2"]
|
618
847
|
```
|
619
848
|
|
620
849
|
### Testing: Inclusion
|
@@ -624,6 +853,135 @@ report_self_test_failure("ruby escape doesn't escape backslash") unless string_w
|
|
624
853
|
report_self_test_failure("included replacements should replace blocks") unless included_string == "I came from lmt_include.lmd"
|
625
854
|
```
|
626
855
|
|
856
|
+
### Testing: Extensions
|
857
|
+
|
858
|
+
``` ruby test_extensions
|
859
|
+
⦅from_extension⦆
|
860
|
+
report_self_test_failure("extension hook should be able to add blocks") unless from_extension
|
861
|
+
```
|
862
|
+
|
863
|
+
### Testing: Conditional Output
|
864
|
+
|
865
|
+
In the description, the if statement was to be executed.
|
866
|
+
|
867
|
+
``` ruby test_conditional_output
|
868
|
+
⦅conditional_output⦆
|
869
|
+
report_self_test_failure("conditional output elseif should not be output when elseif is false") unless conditional_output_elsif
|
870
|
+
report_self_test_failure("conditional output else should not be output when if true") unless conditional_output_else
|
871
|
+
```
|
872
|
+
|
873
|
+
#### If and elsif
|
874
|
+
|
875
|
+
Neither the elsif or elsif statement are output when if is true.
|
876
|
+
|
877
|
+
``` ruby test_conditional_output
|
878
|
+
⦅conditional_output_if_and_elsif⦆
|
879
|
+
report_self_test_failure("conditional output elsif should not be output even if true when is also true") unless conditional_output_elsif
|
880
|
+
report_self_test_failure("conditional output else should not be output when if is true (if and elseif)") unless conditional_output_else
|
881
|
+
```
|
882
|
+
|
883
|
+
``` ruby !
|
884
|
+
@a_variable = true
|
885
|
+
@another_variable = true
|
886
|
+
```
|
887
|
+
|
888
|
+
! if @a_variable
|
889
|
+
|
890
|
+
``` ruby conditional_output_if_and_elsif
|
891
|
+
conditional_output_else = true
|
892
|
+
conditional_output_elsif = true
|
893
|
+
```
|
894
|
+
|
895
|
+
! elsif @another_variable
|
896
|
+
|
897
|
+
``` ruby conditional_output_if_and_elsif
|
898
|
+
conditional_output_elsif = false
|
899
|
+
```
|
900
|
+
|
901
|
+
! else
|
902
|
+
|
903
|
+
``` ruby conditional_output_if_and_elsif
|
904
|
+
conditional_output_else = false
|
905
|
+
```
|
906
|
+
|
907
|
+
! end
|
908
|
+
|
909
|
+
#### Elsif
|
910
|
+
|
911
|
+
When the if is false but the elsif true, only the elsif is output.
|
912
|
+
|
913
|
+
``` ruby test_conditional_output
|
914
|
+
conditional_output_if = true
|
915
|
+
⦅conditional_output_elsif⦆
|
916
|
+
report_self_test_failure("conditional output if should not be output when false") unless conditional_output_if
|
917
|
+
report_self_test_failure("conditional output elseif should be output when elseif is true") unless conditional_output_elsif
|
918
|
+
report_self_test_failure("conditional output else should not be output when elseif is true") unless conditional_output_else
|
919
|
+
```
|
920
|
+
|
921
|
+
``` ruby !
|
922
|
+
@a_variable = false
|
923
|
+
@another_variable = true
|
924
|
+
```
|
925
|
+
|
926
|
+
! if @a_variable
|
927
|
+
|
928
|
+
``` ruby conditional_output_elsif
|
929
|
+
conditional_output_if = false
|
930
|
+
```
|
931
|
+
|
932
|
+
! elsif @another_variable
|
933
|
+
|
934
|
+
``` ruby conditional_output_elsif
|
935
|
+
conditional_output_else = true
|
936
|
+
conditional_output_elsif = true
|
937
|
+
```
|
938
|
+
|
939
|
+
! else
|
940
|
+
|
941
|
+
``` ruby conditional_output_elsif
|
942
|
+
conditional_output_else = false
|
943
|
+
```
|
944
|
+
|
945
|
+
! end
|
946
|
+
|
947
|
+
#### Else
|
948
|
+
|
949
|
+
The else is output when none of the if or elsif statements are true.
|
950
|
+
|
951
|
+
``` ruby test_conditional_output
|
952
|
+
conditional_output_if = true
|
953
|
+
conditional_output_elsif = true
|
954
|
+
⦅conditional_output_else⦆
|
955
|
+
report_self_test_failure("conditional output if should not be output when false") unless conditional_output_if
|
956
|
+
report_self_test_failure("conditional output elseif should not be output when elseif is false") unless conditional_output_elsif
|
957
|
+
report_self_test_failure("conditional output else should be output when neither if nor elseif is true") unless conditional_output_else
|
958
|
+
```
|
959
|
+
|
960
|
+
``` ruby !
|
961
|
+
@a_variable = false
|
962
|
+
@another_variable = false
|
963
|
+
```
|
964
|
+
|
965
|
+
! if @a_variable
|
966
|
+
|
967
|
+
``` ruby conditional_output_else
|
968
|
+
conditional_output_if = false
|
969
|
+
```
|
970
|
+
|
971
|
+
! elsif @another_variable
|
972
|
+
|
973
|
+
``` ruby conditional_output_else
|
974
|
+
conditional_output_elsif = false
|
975
|
+
```
|
976
|
+
|
977
|
+
! else
|
978
|
+
|
979
|
+
``` ruby conditional_output_else
|
980
|
+
conditional_output_else = true
|
981
|
+
```
|
982
|
+
|
983
|
+
! end
|
984
|
+
|
627
985
|
### Regressions
|
628
986
|
|
629
987
|
Some regressions / edge cases that we need to watch for. These should not break our tangle operation.
|