lmt 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module Lmt
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "lmt/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "lmt"
8
+ spec.license = "MIT"
9
+ spec.version = Lmt::VERSION
10
+ spec.authors = ["Marty Gentillon"]
11
+ spec.email = ["marty.gentillon+lmt-ruby@gmail.com"]
12
+
13
+ spec.summary = %q{A literate tangler written in Ruby for use with MarkDown.}
14
+ spec.description = %q{A literate tangler written in Ruby for use with MarkDown.}
15
+ spec.homepage = "https://github.com/MartyGentillon/lmt-ruby"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "bin"
21
+ spec.executables = ["lmt", "lmw"]
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency('rdoc')
27
+ spec.add_development_dependency('pry')
28
+ spec.add_dependency('methadone', '~> 1.9.5')
29
+ spec.add_development_dependency('test-unit')
30
+ end
@@ -0,0 +1,13 @@
1
+ # Error Reporting
2
+
3
+ A simple method to make sure that errors get reported.
4
+
5
+ ``` ruby report_self_test_failure
6
+ def self.report_self_test_failure(message)
7
+ if @dev
8
+ p message
9
+ else
10
+ throw message
11
+ end
12
+ end
13
+ ```
@@ -0,0 +1,652 @@
1
+ # Lmt-Ruby
2
+
3
+ ``` text description
4
+ A literate Markdown tangle tool written in Ruby.
5
+ ```
6
+
7
+ Lmt is a literate Markdown tangle program for [literate programing](https://en.wikipedia.org/wiki/Literate_programming) in a slightly extended [Markdown](http://daringfireball.net/projects/markdown/syntax) syntax that is written in Ruby.
8
+
9
+ In literate programming, a program is contained within a prose essay describing the thinking which goes into the program. The source code is then extracted from the essay using a program called tangle (this application). The essay can also be formatted into a document for human consumption using a program called "weave" and can be found [here](lmm.md).
10
+
11
+ ## Why?
12
+
13
+ While there are other Markdown tanglers available (especially [lmt](https://github.com/driusan/lmt), which this program is designed to be superficially similar to) none quite match the combination of simplicity and extensibility which I need.
14
+
15
+ ## Features
16
+
17
+ In order to be useful for literate programming we need a few features:
18
+
19
+ 1. The ability to strip code out of a Markdown file and place it into a tangled output file.
20
+ 2. The ability to embed macros so that the code can be expressed in any order desired.
21
+ 3. The ability to apply filters on the contents of a macro
22
+ 4. The ability to to identify code blocks which will be expanded when referenced
23
+ 5. The ability to append to or replace code blocks
24
+ 6. The ability to include another file.
25
+
26
+ There are also a few potentially useful features that are not implemented but might be in the future:
27
+
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.
34
+
35
+ ### Blocks
36
+
37
+ Markdown already supports code blocks expressed with code fences starting with three backticks, usually enabling syntax highlighting on the output. This should work excellently for identifying block boundaries.
38
+
39
+ There are two types of blocks: the default block and macro blocks.
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.
42
+
43
+ ``` ruby
44
+ #Output starts here
45
+ ```
46
+
47
+ If there is no default block, then no output file will be created.
48
+
49
+ In order to add the macro feature we need, we will need to add header content at the beginning of such a quote. For code blocks, we can add it after the language name. For example to create a macro named macro_description we could use:
50
+
51
+ ```ruby macro_description
52
+ # this shouldn't be in the output, it should have been replaced.
53
+ block_replacement = false
54
+ ```
55
+
56
+ Of course, these do not play well with Markdown rendering, so we will need a weaver to display the name appropriately.
57
+
58
+ To replace a block put `=` before the block name like so:
59
+
60
+ ```ruby =macro_description
61
+ # this is the replacement
62
+ replaced_block = true
63
+ ```
64
+
65
+ To append to a block, just open it again. The macro expansion only happens after the entire file is read.
66
+
67
+ ```ruby macro_description
68
+ # Yay appended code gets injected
69
+ block_appendment = true
70
+ ```
71
+
72
+ #### Macros
73
+
74
+ We will also need a way to trigger macro insertion. Given that unicode tends not to be in use, why don't we say that anything inside `⦅⦆` refers to a block by name and should be replaced by the contents of that block.
75
+
76
+ ```ruby macro_insertion_description
77
+ block_replacement = true
78
+ replaced_block = false
79
+ block_appendment = false
80
+
81
+ ⦅macro_description⦆
82
+ ```
83
+
84
+ Given the definition of `macro_description` above, all the variables will be true at the end of that block.
85
+
86
+ This also works with spaces inside the `⦅⦆`
87
+
88
+ ``` ruby macro_insertion_description
89
+ insertion_works_with_spaces = false
90
+ ⦅ insertion_works_with_spaces ⦆
91
+ ```
92
+
93
+ Finally, if substitution isn't desired, you may escape the `⦅` and `⦆` with `\` which will prevent macro expansion. As below; the first character of escaped string is `⦅`
94
+
95
+ ``` ruby macro_insertion_description
96
+ escaped_string = '\⦅macro_description\⦆'
97
+ ```
98
+
99
+ ### Filters
100
+
101
+ Filters can be defined as functions which take an array of lines and return the altered array. They are applied after a macro's contents are expanded and before it is inserted. They are triggered with the `|` symbol in expansion. for example: given
102
+
103
+ ``` text string_with_backslash
104
+ this string ends in \.
105
+ ```
106
+
107
+ The following will escape the `\`
108
+
109
+ ``` ruby filter_use_description
110
+ string_with_backslash = "⦅string_with_backslash | ruby_escape⦆"
111
+ ```
112
+
113
+ There are a few built in filters:
114
+
115
+ ``` ruby filter_list
116
+ {
117
+ 'ruby_escape' => ⦅ruby_escape⦆
118
+ }
119
+ ```
120
+
121
+ ### Includes
122
+
123
+ Other files may be using an include directive and a markdown link. Include directive are lines starting with `! include` followed by a space. No further text may follow the markdown link. Paths are relative to the file being included from.
124
+
125
+ During tangle the link line will be replaced with the lines from the included file. This means that they may replace blocks defined in the file that includes them such as this one
126
+
127
+ ``` ruby included_block
128
+ included_string = "I am in lmt.lmd"
129
+ ```
130
+
131
+ ! include [an include](lmt_include.lmd)
132
+
133
+ ### Self Test
134
+
135
+ 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.
136
+
137
+ ``` ruby self_test
138
+ def self.self_test()
139
+ ⦅test_description⦆
140
+ end
141
+ ```
142
+
143
+ ## Interface
144
+
145
+ We need to know where to get the input from and where to send the output to. For that, we will use the following command line options
146
+
147
+ ``` ruby options
148
+ on("--file FILE", "-f", "Required: input file")
149
+ on("--output FILE", "-o", "Required: output file")
150
+ on("--dev", "disables self test failure for development")
151
+ ```
152
+
153
+ Of which, both are required
154
+
155
+ ``` ruby options
156
+ required(:file, :output)
157
+ ```
158
+
159
+ ## Implementation and Example
160
+
161
+ Now for an example in implementation. Using Ruby we can write a template as below: (We are replacing the default block because the version above doesn't have a #!.)
162
+
163
+ ```ruby =
164
+ #!/usr/bin/env ruby
165
+ # Encoding: utf-8
166
+
167
+ ⦅includes⦆
168
+
169
+ module Lmt
170
+
171
+ class Tangle
172
+ include Methadone::Main
173
+ include Methadone::CLILogging
174
+
175
+ @dev = false
176
+
177
+ main do
178
+ check_arguments()
179
+ begin
180
+ ⦅main_body⦆
181
+ rescue Exception => e
182
+ puts "Error: #{e.message} #{extract_causes(e)}At:"
183
+ e.backtrace.each do |trace|
184
+ puts " #{trace}"
185
+ end
186
+ end
187
+ end
188
+
189
+ def self.extract_causes(error)
190
+ if (error.cause)
191
+ " Caused by: #{error.cause.message}\n#{extract_causes(error.cause)}"
192
+ else
193
+ ""
194
+ end
195
+ end
196
+
197
+ ⦅self_test⦆
198
+
199
+ ⦅report_self_test_failure⦆
200
+
201
+ ⦅filter_class⦆
202
+
203
+ ⦅tangle_class⦆
204
+
205
+ ⦅option_verification⦆
206
+
207
+ description "⦅description⦆"
208
+ ⦅options⦆
209
+
210
+ version Lmt::VERSION
211
+
212
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
213
+
214
+ go! if __FILE__ == $0
215
+ end
216
+
217
+ end
218
+
219
+ ```
220
+
221
+ This is a basic template using the [Ruby methadone](https://github.com/davetron5000/methadone) command line application framework and making sure that we report errors (because silent failure sucks).
222
+
223
+ 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
+
225
+ ``` ruby main_body
226
+ self_test()
227
+ tangler = Tangle::Tangler.new(options[:file])
228
+ tangler.tangle()
229
+ tangler.write(options[:output])
230
+ ```
231
+
232
+ Finally, we have the dependencies. Optparse and methadone are used for cli argument handling and other niceties.
233
+
234
+ ``` ruby includes
235
+ require 'optparse'
236
+ require 'methadone'
237
+ require 'lmt/version'
238
+ ```
239
+
240
+ There, now we are done with the boilerplate. On to:
241
+
242
+ ## The Actual Tangler
243
+
244
+ The tangler is defined within a class that contains the tangling implementation. It contains the following blocks
245
+
246
+ ``` ruby tangle_class
247
+ class Tangler
248
+ class << self
249
+ attr_reader :filters
250
+ end
251
+
252
+ @filters = ⦅filter_list⦆
253
+
254
+ ⦅initializer⦆
255
+ ⦅tangle⦆
256
+ ⦅read_file⦆
257
+ ⦅include_includes⦆
258
+ ⦅parse_blocks⦆
259
+ ⦅expand_macros⦆
260
+ ⦅apply_filters⦆
261
+ ⦅unescape_double_parens⦆
262
+ ⦅write⦆
263
+
264
+ private
265
+ ⦅tangle_class_privates⦆
266
+ end
267
+ ```
268
+
269
+ ### Initializer
270
+
271
+ The initializer takes in the input file and sets up our state. We are keeping the unnamed top level block separate from the rest. Then we have a hash of blocks. Finally, we need to make sure we have tangled before we write the output.
272
+
273
+ ``` ruby initializer
274
+ def initialize(input)
275
+ @input = input
276
+ @block = ""
277
+ @blocks = {}
278
+ @tangled = false
279
+ end
280
+
281
+ ```
282
+
283
+ ### Tangle
284
+
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.
286
+
287
+ ``` ruby tangle
288
+ def tangle()
289
+ contents = include_includes(read_file(@input))
290
+ @block, @blocks = parse_blocks(contents)
291
+ if @block
292
+ @block = expand_macros(@block)
293
+ @block = unescape_double_parens(@block)
294
+ end
295
+ @tangled = true
296
+ end
297
+
298
+ ```
299
+
300
+ ### Reading The File
301
+
302
+ This is fairly self explanatory, though note, we are storing the file in memory as an array of lines.
303
+
304
+ ``` ruby read_file
305
+ def read_file(file)
306
+ File.open(file, 'r') do |f|
307
+ f.readlines
308
+ end
309
+ end
310
+
311
+ ```
312
+
313
+ ### Including the Includes
314
+
315
+ As our specification is a regular language (we do not support any kind of nesting), we will be using regular expressions to process it. Those expressions are detailed in:
316
+
317
+ ! include [here](lmt_expressions.lmd)
318
+
319
+ Here we go through each line looking for an include statement. When we find one, we replace it with the lines from that file. Those lines will, of course, need to have includes processed as well.
320
+
321
+ ``` ruby include_includes
322
+ def include_includes(lines, current_file = @input, depth = 0)
323
+ raise "too many includes" if depth > 1000
324
+ include_exp = ⦅include_expression⦆
325
+ lines.map do |line|
326
+ match = include_exp.match(line)
327
+ if match
328
+ file = File.dirname(current_file) + '/' + match[1]
329
+ include_includes(read_file(file), file, depth + 1)
330
+ else
331
+ [line]
332
+ end
333
+ end.flatten(1)
334
+ end
335
+
336
+ ```
337
+
338
+ ### Parsing The Blocks
339
+
340
+ Now we get to the meat of the algorithm. This uses the regular expression in [lmt_expressions](lmt_expressions.lmd#The-Code-Block-Expression)
341
+
342
+ First, we filter out all non block lines, keeping the block headers, slice it into separate blocks at the header, process the header and turn it into a map of lists of lines. We then group by the headers and combine the blocks which follow the last reset for that block name.
343
+
344
+ We also need to remove the last newline from the block as it causes problems when injecting a block onto a line with stuff after the end.
345
+
346
+ Finally, (after making sure we aren't missing a code fence) we extract the unnamed block from the hash and return both it and the rest.
347
+
348
+ ``` ruby parse_blocks
349
+ def parse_blocks(lines)
350
+ code_block_exp = ⦅code_block_expression⦆
351
+ in_block = false
352
+ blocks = lines.find_all do |line|
353
+ in_block = !in_block if line =~ code_block_exp
354
+ in_block
355
+ end.slice_before do |line|
356
+ code_block_exp =~ line
357
+ end.map do |(header, *rest)|
358
+ white_space, language, replacement_mark, name = code_block_exp.match(header)[1..-1]
359
+ [name, replacement_mark, rest]
360
+ end.group_by do |(name, _, _)|
361
+ name
362
+ end.transform_values do |bodies|
363
+ last_replacement_index = get_last_replacement_index(bodies)
364
+ bodies[last_replacement_index..-1].map { |(_, _, body)| body}
365
+ .flatten(1)
366
+ end.transform_values do |body_lines|
367
+ body_lines[-1] = body_lines[-1].chomp if body_lines[-1]
368
+ body_lines
369
+ end
370
+ throw "Missing code fence" if in_block
371
+ main = blocks[""]
372
+ blocks.delete("")
373
+ [main, blocks]
374
+ end
375
+
376
+ ```
377
+
378
+ We have a private helper helper method here. So, after we turn each block chunk into an array of `[name, replacement_mark, body]` we can find the last one by scanning for a replacement mark set to `=`. Otherwise the answer is `0` as there is no replacement index.
379
+
380
+ ``` ruby tangle_class_privates
381
+ def get_last_replacement_index(bodies)
382
+ last_replacement = bodies.each_with_index
383
+ .select do |((_, replacement_mark, _), _)|
384
+ replacement_mark == '='
385
+ end[-1]
386
+ if last_replacement
387
+ last_replacement[1]
388
+ else
389
+ 0
390
+ end
391
+ end
392
+
393
+ ```
394
+
395
+ ### Handling the macros
396
+
397
+ The other half of the meat. Here we use two regular expressions. One to identify and propagate whitespace and the other to actually find the replacements in a line.
398
+
399
+ This is implemented by splitting the line on the replacement section, grouping into pairs, and then reducing. Afterwords, we end up with an extra layer of lists which need to be flattened. (Yes I am using a monad and bind.)
400
+
401
+ ``` ruby expand_macros
402
+ def expand_macros(lines, depth = 0)
403
+ throw "too deep macro expansion {depth}" if depth > 1000
404
+ lines.map do |line|
405
+ begin
406
+ expand_macro_on_line(line, depth)
407
+ rescue Exception => e
408
+ raise Exception, "Failed to process line: #{line}", e.backtrace
409
+ end
410
+ end.flatten(1)
411
+ end
412
+
413
+ ```
414
+
415
+ Expand_macro_on_line turns a line into a list of lines. The collected results will have to be flattened by 1.
416
+
417
+ First we process the white space off the front of the expression. This will be added to each line in the extended macros so that the output file is nicely indented. It also means that indentation sensitive languages like python will be tangled correctly.
418
+
419
+ ``` ruby tangle_class_privates
420
+ def expand_macro_on_line(line, depth)
421
+ white_space_exp = /^(\s*)(.*\n?)/
422
+ macro_substitution_exp = ⦅macro_substitution_expression⦆
423
+ filter_extraction_exp = / *\| *([-\w]+) */
424
+ white_space, text = white_space_exp.match(line)[1..2]
425
+ ```
426
+
427
+ Then we chop it into pieces using the [macro substitution expression](lmt_expressions.lmd#The-Macro-Substitution-Expression) This results in text, macro_name / filter pairs. If there is a macro name, we then split the filter names off with the filter expression which provides filter names followed by stuff between them (nothing) which we discard.
428
+
429
+ ``` ruby tangle_class_privates
430
+ section = text.split(macro_substitution_exp)
431
+ .each_slice(2)
432
+ .map do |(text_before_macro, macro_match)|
433
+ if (macro_match)
434
+ macro_name, *filters = macro_match.strip.split(filter_extraction_exp)
435
+ [text_before_macro, macro_name, filters.each_slice(2).map(&:first)]
436
+ else
437
+ [text_before_macro]
438
+ end
439
+ ```
440
+
441
+ Finally, we are ready to actually process the text and macros. We build the list of ines with just the white space, and appending the results of precessing to the end. Each potential line is built up by appending to the end of the last line. If there is no macro, then we can just append the text.
442
+
443
+ ``` ruby tangle_class_privates
444
+ end.inject([white_space]) do
445
+ |(*new_lines, last_line), (text_before_macro, macro_name, filters)|
446
+ if macro_name.nil?
447
+ last_line = "" unless last_line
448
+ new_lines << last_line + text_before_macro
449
+ else
450
+ ```
451
+
452
+ If there is a macro substitution, first we get the new lines. The we append the first line of the macro text to the last line. Finally, we append the white space to the front of each of the macro's lines and insert them into the middle. of the list of lines we are building.
453
+
454
+ ``` ruby tangle_class_privates
455
+ throw "Macro '#{macro_name}' unknown" unless @blocks[macro_name]
456
+ macro_lines = apply_filters(
457
+ expand_macros(@blocks[macro_name], depth + 1), filters)
458
+ unless macro_lines.empty?
459
+ new_line = last_line + text_before_macro + macro_lines[0]
460
+ macro_continued = macro_lines[1..-1].map do |macro_line|
461
+ white_space + macro_line
462
+ end
463
+ (new_lines << new_line) + macro_continued
464
+ else
465
+ new_lines
466
+ end
467
+ end
468
+ end
469
+ end
470
+ ```
471
+
472
+ Finally, throughout this process, we have to be ware that a macro may have no content. We must deal with `nil` and empty lists where they occur.
473
+
474
+ ### Unescaping Double Parentheses
475
+
476
+ This is fairly self explanatory, gsub is global substitution. We need three `\`s two to match the escape sequence for `\` in ruby and a third to handle the escaped `⦅` and `⦆` when this file itself is tangled.
477
+
478
+ ``` ruby unescape_double_parens
479
+ def unescape_double_parens(block)
480
+ block.map do |l|
481
+ l = l.gsub("\\\⦅", "⦅")
482
+ l = l.gsub("\\\⦆", "⦆")
483
+ l
484
+ end
485
+ end
486
+
487
+ ```
488
+
489
+ ### Write The Output
490
+
491
+ Finally, if there is a default block, write the output.
492
+
493
+ ``` ruby write
494
+ def write(output)
495
+ tangle() unless @tangled
496
+ if @block
497
+ fout = File.open(output, 'w')
498
+ @block.each {|line| fout << line}
499
+ end
500
+ end
501
+
502
+ ```
503
+
504
+ ## The Filters
505
+
506
+ The filters are instances of the Filter class which can be created by passing a block to the initializer of the class. When the filter is executed, this block of code will be called on all of the lines of code being filtered.
507
+
508
+ ``` ruby filter_class
509
+ class Filter
510
+ def initialize(&block)
511
+ @code = block;
512
+ end
513
+
514
+ def filter(lines)
515
+ @code.call(lines)
516
+ end
517
+ end
518
+ ```
519
+
520
+ Because it is fairly common to filter lines one at a time, LineFilter will pass in each line instead of the whole block.
521
+
522
+ ``` ruby filter_class
523
+ class LineFilter < Filter
524
+ def filter(lines)
525
+ lines.map do |line|
526
+ @code.call(line)
527
+ end
528
+ end
529
+ end
530
+ ```
531
+
532
+ Filters are applied by the following method:
533
+
534
+ ``` ruby apply_filters
535
+ def apply_filters(strings, filters)
536
+ filters.map do |filter_name|
537
+ Tangler.filters[filter_name]
538
+ end.inject(strings) do |strings, filter|
539
+ filter.filter(strings)
540
+ end
541
+ end
542
+ ```
543
+
544
+
545
+ ### Ruby Escape
546
+
547
+ Ruby escape escapes strings appropriately for Ruby.
548
+
549
+ ``` ruby ruby_escape
550
+ LineFilter.new do |line|
551
+ line.dump[1..-2]
552
+ end
553
+ ```
554
+
555
+ ## Option Verification
556
+
557
+ Option verification is described here:
558
+
559
+ ! include [Option verification](option_verification.lmd)
560
+
561
+ ## Self Test, Details
562
+
563
+ So, now we need to go into details of our self test and also include regressions which have caused problems.
564
+
565
+ First, we need a method to report test failures:
566
+
567
+ ! include [Error reporting](error_reporting.lmd)
568
+
569
+ Then we need the tests we are doing. The intentionally empty block is included both at the beginning and end to make sure that we handled all the edge cases related to empty blocks appropriately.
570
+
571
+ ``` ruby test_description
572
+ ⦅intentionally_empty_block⦆
573
+ ⦅test_macro_insertion_description⦆
574
+ ⦅test_filters⦆
575
+ ⦅test_inclusion⦆
576
+ ⦅intentionally_empty_block⦆
577
+ ```
578
+
579
+ ### Testing: Macros
580
+
581
+ At [the top of the file](#Macros), we described the macros. Lets make sure that works by ensuring the variables are as they were described above
582
+
583
+ ``` ruby test_macro_insertion_description
584
+ ⦅macro_insertion_description⦆
585
+ # These require the code in the macro to work.
586
+ report_self_test_failure("block replacement doesn't work") unless block_replacement and replaced_block
587
+ report_self_test_failure("appending to macros doesn't work") unless block_appendment
588
+ report_self_test_failure("insertion must support spaces") unless insertion_works_with_spaces
589
+ report_self_test_failure("double parentheses may be escaped") unless escaped_string[0] != '\\'
590
+ ```
591
+
592
+ Finally, we need to make sure two macros on the same line works.
593
+
594
+ ``` ruby test_macro_insertion_description
595
+ two_macros = "⦅foo⦆ ⦅foo⦆"
596
+ report_self_test_failure("Should be able to place two macros on the same line") unless two_macros == "foo foo"
597
+ ```
598
+
599
+ For that to work we need:
600
+
601
+ ``` ruby insertion_works_with_spaces
602
+ insertion_works_with_spaces = true
603
+ ```
604
+
605
+ and
606
+
607
+ ``` ruby foo
608
+ foo
609
+ ```
610
+
611
+ ### Testing: Filters
612
+
613
+ 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
+
615
+ ``` ruby test_filters
616
+ ⦅filter_use_description⦆
617
+ report_self_test_failure("ruby escape doesn't escape backslash") unless string_with_backslash =~ /\\.?/
618
+ ```
619
+
620
+ ### Testing: Inclusion
621
+
622
+ ``` ruby test_inclusion
623
+ ⦅included_block⦆
624
+ report_self_test_failure("included replacements should replace blocks") unless included_string == "I came from lmt_include.lmd"
625
+ ```
626
+
627
+ ### Regressions
628
+
629
+ Some regressions / edge cases that we need to watch for. These should not break our tangle operation.
630
+
631
+ #### Empty Blocks
632
+
633
+ We need to be able to tangle empty blocks such as:
634
+
635
+ ``` ruby intentionally_empty_block
636
+ ```
637
+
638
+ #### Unused blocks referencing nonexistent blocks
639
+
640
+ If a block is unused, then don't break if it uses a nonexistent block.
641
+
642
+ ``` ruby unused_block
643
+ ⦅this_block_does_not_exist⦆
644
+ ```
645
+
646
+ ## Fin ┐( ˘_˘)┌
647
+
648
+ And with that, we have tangled a file.
649
+
650
+ At current, there are a few more features that would be nice to have. First, this does not yet support extension by commands. Second, we cannot write to any file other than the output file. Third, we don't have many filters. These features can wait for now.
651
+
652
+