lmt 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- The second regular 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).
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
- /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
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
- The third expression 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.
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 header for code blocks
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
- white_space, language, replacement_mark, name =
330
- code_block_expression.match(line)[1..-1]
331
- human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
332
- replacing = if replacement_mark == "="
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
- "#######{replacing} Output Block\n\n"
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 = include_includes(read_file(@input))
111
- @block, @blocks = parse_blocks(contents)
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 = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
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
- Tangler.filters[filter_name]
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 = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
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 = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
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
- white_space, language, replacement_mark, name =
137
- code_block_expression.match(line)[1..-1]
138
- human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
139
- replacing = if replacement_mark == "="
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
- "#######{replacing} Output Block\n\n"
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
@@ -1,3 +1,3 @@
1
1
  module Lmt
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
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 extend the tangler with Ruby code from a block.
29
- 2. The ability to write out other files.
30
- 3. Source mapping
31
- 4. Further source verification. For instance, all instances of the same block should be in the same language. Also, detect and prevent double inclusion.
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
- Ouput 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.
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 = include_includes(read_file(@input))
290
- @block, @blocks = parse_blocks(contents)
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
- Tangler.filters[filter_name]
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.