lmt 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92a8904e1d7de30d5a09b28df29ffa7b164f4afe7e8bf78806c7c94a8381804a
4
+ data.tar.gz: 9cfeaf0336b6e7b4162162aa258f7082a95078ac3299153d217fccb7b110bfcb
5
+ SHA512:
6
+ metadata.gz: db3d8d46c342cbde06a564b7db00fe674d0eda158edc1f95bf8263e7b641739fe72ff3f1568dab3d7b79e12cc0b6a3793f9aa58c5b2c2844d1b188772c27d26e
7
+ data.tar.gz: a3d8b02b2d7cbdd2aadb112149f65b1b7cb31d91649a16a176166014888e761c67924eb53e2ac4e4455773d29b70c562d0f3c018c72f0777118d6d7dd1f04076
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ results.html
3
+ pkg
4
+ html
5
+ .history/
6
+ *.bak
7
+ *.bak2
@@ -0,0 +1,7 @@
1
+ {
2
+ "default": true,
3
+ "header-increment": false,
4
+ "line-length": false,
5
+ "no-reversed-links": true,
6
+ "no-trailing-punctuation": false
7
+ }
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in lmt.gemspec
6
+ gemspec
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lmt (0.1.2)
5
+ methadone (~> 1.9.5)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ coderay (1.1.2)
11
+ methadone (1.9.5)
12
+ bundler
13
+ method_source (0.9.0)
14
+ power_assert (1.1.1)
15
+ pry (0.11.3)
16
+ coderay (~> 1.1.0)
17
+ method_source (~> 0.9.0)
18
+ rake (10.5.0)
19
+ rdoc (6.0.3)
20
+ test-unit (3.2.7)
21
+ power_assert
22
+
23
+ PLATFORMS
24
+ x64-mingw32
25
+
26
+ DEPENDENCIES
27
+ bundler (~> 1.16)
28
+ lmt!
29
+ pry
30
+ rake (~> 10.0)
31
+ rdoc
32
+ test-unit
33
+
34
+ BUNDLED WITH
35
+ 1.17.2
@@ -0,0 +1,73 @@
1
+ # Lmt
2
+
3
+ Lmt is a literate markdown tangle and weave 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.
4
+
5
+ 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".
6
+
7
+ For a more detailed description and example, see the tangle program in [src/lmt.lmd](./src/lmt.lmd) and the weave program in [src/lmw.lmd](./src/lmw.lmd).
8
+
9
+ The weaved output may be found in [doc/lmt.md](./doc/lmt.md) and [doc/lmw.md](./doc/lmw.md)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'lmt'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install lmt
26
+
27
+ ## Usage
28
+
29
+ The tangle program takes input files and produces tangled output files. It is used as follows:
30
+
31
+ ``` bash
32
+ bin/lmt --file {input file} --output {tangled destination}
33
+ ```
34
+
35
+ The weave program is similar but produces weaved output files. It does not recurse down include statements, and so will need to be run independently for each included file. An example usage:
36
+
37
+ ``` bash
38
+ bin/lmw --file {input file} --output [weaved destination]
39
+ ```
40
+
41
+ ## Development
42
+
43
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
44
+
45
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
46
+
47
+ Remember, this is a bundler app, and to rum it without installing, you must use the `bundle exec` command. As an example, the self-tangling command for development is:
48
+
49
+ ``` bash
50
+ bundle exec ruby bin/lmt --file src/lmt/lmt.rb.lmd --output lib/lmt/lmt.rb
51
+ ```
52
+
53
+ To test the weave you can use the following command which will weave the weaver and write it to the doc directory.
54
+
55
+ ``` bash
56
+ bundle exec ruby bin/lmt --file src/lmt/lmt.rb.lmd --output lib/lmt/lmt.rb; bundle exec ruby bin/lmw --file src/lmt/lmw.rb.lmd --output doc/lmt/lmw.rb.md
57
+ ```
58
+
59
+ ## Prior Art
60
+
61
+ Some related and similar tools that the reader might find interesting:
62
+
63
+ * <<https://github.com/driusan/lmt>>
64
+ * <<https://github.com/rebcabin/tangledown>>
65
+ * <<https://github.com/vlead/literate-tools>>
66
+ * <<https://github.com/zyedidia/Literate>>
67
+ * <<https://github.com/mqsoh/knot>>
68
+ * <<https://fsprojects.github.io/FSharp.Formatting/sidemarkdown.html>>
69
+ * <<https://github.com/richorama/literate>>
70
+
71
+ ## Contributing
72
+
73
+ Bug reports and pull requests are welcome on GitHub at <<https://github.com/MartyGentillon/lmt-ruby>>.
@@ -0,0 +1,75 @@
1
+ def dump_load_path
2
+ puts $LOAD_PATH.join("\n")
3
+ found = nil
4
+ $LOAD_PATH.each do |path|
5
+ if File.exists?(File.join(path,"rspec"))
6
+ puts "Found rspec in #{path}"
7
+ if File.exists?(File.join(path,"rspec","core"))
8
+ puts "Found core"
9
+ if File.exists?(File.join(path,"rspec","core","rake_task"))
10
+ puts "Found rake_task"
11
+ found = path
12
+ else
13
+ puts "!! no rake_task"
14
+ end
15
+ else
16
+ puts "!!! no core"
17
+ end
18
+ end
19
+ end
20
+ if found.nil?
21
+ puts "Didn't find rspec/core/rake_task anywhere"
22
+ else
23
+ puts "Found in #{path}"
24
+ end
25
+ end
26
+ require 'bundler'
27
+ require 'rake/clean'
28
+
29
+ require 'rake/testtask'
30
+
31
+ gem 'rdoc' # we need the installed RDoc gem, not the system one
32
+ require 'rdoc/task'
33
+
34
+ include Rake::DSL
35
+
36
+ Bundler::GemHelper.install_tasks
37
+
38
+
39
+ task :test => :build
40
+ task :release => :test
41
+ task :install => :build
42
+
43
+ task :build => :tangle
44
+ task :build => :weave
45
+
46
+ lmd_files = Rake::FileList['src/**/*.lmd']
47
+ outputs = lmd_files.pathmap('%{^src,lib}X')
48
+ docs = lmd_files.pathmap('%{^src,doc}X.md')
49
+
50
+ task :tangle => outputs
51
+ task :weave => docs
52
+
53
+ lmd_files.zip(outputs, docs).each do |lmd_file, output, doc|
54
+ directory output_dir = output.pathmap('%d')
55
+ directory doc_dir = doc.pathmap('%d')
56
+ file output => [output_dir, lmd_file] do
57
+ sh "ruby bin/lmt --file #{lmd_file} --output #{output}"
58
+ end
59
+ file doc => [doc_dir, lmd_file] do
60
+ sh "ruby bin/lmw --file #{lmd_file} --output #{doc}"
61
+ end
62
+ end
63
+
64
+ Rake::TestTask.new do |t|
65
+ t.pattern = 'test/tc_*.rb'
66
+ end
67
+
68
+ Rake::RDocTask.new do |rd|
69
+ rd.main = "README.rdoc"
70
+
71
+ rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
72
+ end
73
+
74
+ task :default => [:test]
75
+
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: utf-8
3
+
4
+ require "bundler/setup"
5
+ require "lmt/lmt"
6
+ require "lmt/lmw"
7
+ load File.expand_path(File.dirname(__FILE__) + '/lmt')
8
+
9
+ # You can add fixtures and/or initialization code here to make experimenting
10
+ # with your gem easier. You can also use a different console, if you like.
11
+ tangle = nil
12
+ weave = nil
13
+
14
+ def make_tangle()
15
+ tangle = Lmt::Tangle::Tangler.new("src/lmt/lmt.rb.lmd")
16
+ end
17
+
18
+ def make_weave()
19
+ weave = Lmt::Lmw::Weave.from_file("src/lmt/lmw.rb.lmd")
20
+ end
21
+
22
+ # (If you use this, don't forget to add pry to your Gemfile!)
23
+ require "pry"
24
+ Pry.start
25
+
data/bin/lmt ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: utf-8
3
+
4
+ require 'lmt/lmt'
5
+
6
+ Lmt::Tangle.go!
data/bin/lmw ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: utf-8
3
+ require 'lmt/lmw'
4
+
5
+ Lmt::Lmw.go!
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ # Error Reporting
2
+
3
+ A simple method to make sure that errors get reported.
4
+
5
+ ###### Code Block: Report Self Test Failure
6
+
7
+ ``` ruby
8
+ def self.report_self_test_failure(message)
9
+ if @dev
10
+ p message
11
+ else
12
+ throw message
13
+ end
14
+ end
15
+ ```
@@ -0,0 +1,742 @@
1
+ # Lmt-Ruby
2
+
3
+ ###### Code Block: Description
4
+
5
+ ``` text
6
+ A literate Markdown tangle tool written in Ruby.
7
+ ```
8
+
9
+ 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.
10
+
11
+ 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).
12
+
13
+ ## Why?
14
+
15
+ 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.
16
+
17
+ ## Features
18
+
19
+ In order to be useful for literate programming we need a few features:
20
+
21
+ 1. The ability to strip code out of a Markdown file and place it into a tangled output file.
22
+ 2. The ability to embed macros so that the code can be expressed in any order desired.
23
+ 3. The ability to apply filters on the contents of a macro
24
+ 4. The ability to to identify code blocks which will be expanded when referenced
25
+ 5. The ability to append to or replace code blocks
26
+ 6. The ability to include another file.
27
+
28
+ There are also a few potentially useful features that are not implemented but might be in the future:
29
+
30
+ 1. The ability to extend the tangler with Ruby code from a block.
31
+ 2. The ability to write out other files.
32
+ 3. Source mapping
33
+ 4. Further source verification. For instance, all instances of the same block should be in the same language. Also, detect and prevent double inclusion.
34
+
35
+ Also, the only filter currently existing just escapes strings for ruby code. There are many more that could be useful.
36
+
37
+ ### Blocks
38
+
39
+ 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.
40
+
41
+ There are two types of blocks: the default block and macro blocks.
42
+
43
+ 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.
44
+
45
+ ###### Output Block
46
+
47
+ ``` ruby
48
+ #Output starts here
49
+ ```
50
+
51
+ If there is no default block, then no output file will be created.
52
+
53
+ 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:
54
+
55
+ ###### Code Block: Macro Description
56
+
57
+ ``` ruby
58
+ # this shouldn't be in the output, it should have been replaced.
59
+ block_replacement = false
60
+ ```
61
+
62
+ Of course, these do not play well with Markdown rendering, so we will need a weaver to display the name appropriately.
63
+
64
+ To replace a block put `=` before the block name like so:
65
+
66
+ ###### Replacing Code Block: Macro Description
67
+
68
+ ``` ruby
69
+ # this is the replacement
70
+ replaced_block = true
71
+ ```
72
+
73
+ To append to a block, just open it again. The macro expansion only happens after the entire file is read.
74
+
75
+ ###### Code Block: Macro Description
76
+
77
+ ``` ruby
78
+ # Yay appended code gets injected
79
+ block_appendment = true
80
+ ```
81
+
82
+ #### Macros
83
+
84
+ 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.
85
+
86
+ ###### Code Block: Macro Insertion Description
87
+
88
+ ``` ruby
89
+ block_replacement = true
90
+ replaced_block = false
91
+ block_appendment = false
92
+
93
+ ⦅macro_description⦆
94
+ ```
95
+
96
+ Given the definition of `macro_description` above, all the variables will be true at the end of that block.
97
+
98
+ This also works with spaces inside the `⦅⦆`
99
+
100
+ ###### Code Block: Macro Insertion Description
101
+
102
+ ``` ruby
103
+ insertion_works_with_spaces = false
104
+ ⦅ insertion_works_with_spaces ⦆
105
+ ```
106
+
107
+ 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 `⦅`
108
+
109
+ ###### Code Block: Macro Insertion Description
110
+
111
+ ``` ruby
112
+ escaped_string = '\⦅macro_description\⦆'
113
+ ```
114
+
115
+ ### Filters
116
+
117
+ 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
118
+
119
+ ###### Code Block: String With Backslash
120
+
121
+ ``` text
122
+ this string ends in \.
123
+ ```
124
+
125
+ The following will escape the `\`
126
+
127
+ ###### Code Block: Filter Use Description
128
+
129
+ ``` ruby
130
+ string_with_backslash = "⦅string_with_backslash | ruby_escape⦆"
131
+ ```
132
+
133
+ There are a few built in filters:
134
+
135
+ ###### Code Block: Filter List
136
+
137
+ ``` ruby
138
+ {
139
+ 'ruby_escape' => ⦅ruby_escape⦆
140
+ }
141
+ ```
142
+
143
+ ### Includes
144
+
145
+ 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.
146
+
147
+ 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
148
+
149
+ ###### Code Block: Included Block
150
+
151
+ ``` ruby
152
+ included_string = "I am in lmt.lmd"
153
+ ```
154
+
155
+ **See include:** [lmt_include.lmd](include_file)
156
+
157
+ ### Self Test
158
+
159
+ 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.
160
+
161
+ ###### Code Block: Self Test
162
+
163
+ ``` ruby
164
+ def self.self_test()
165
+ ⦅test_description⦆
166
+ end
167
+ ```
168
+
169
+ ## Interface
170
+
171
+ 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
172
+
173
+ ###### Code Block: Options
174
+
175
+ ``` ruby
176
+ on("--file FILE", "-f", "Required: input file")
177
+ on("--output FILE", "-o", "Required: output file")
178
+ on("--dev", "disables self test failure for development")
179
+ ```
180
+
181
+ Of which, both are required
182
+
183
+ ###### Code Block: Options
184
+
185
+ ``` ruby
186
+ required(:file, :output)
187
+ ```
188
+
189
+ ## Implementation and Example
190
+
191
+ 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 #!.)
192
+
193
+ ###### Replacing Output Block
194
+
195
+ ``` ruby
196
+ #!/usr/bin/env ruby
197
+ # Encoding: utf-8
198
+
199
+ ⦅includes⦆
200
+
201
+ module Lmt
202
+
203
+ class Tangle
204
+ include Methadone::Main
205
+ include Methadone::CLILogging
206
+
207
+ @dev = false
208
+
209
+ main do
210
+ check_arguments()
211
+ begin
212
+ ⦅main_body⦆
213
+ rescue Exception => e
214
+ puts "Error: #{e.message} #{extract_causes(e)}At:"
215
+ e.backtrace.each do |trace|
216
+ puts " #{trace}"
217
+ end
218
+ end
219
+ end
220
+
221
+ def self.extract_causes(error)
222
+ if (error.cause)
223
+ " Caused by: #{error.cause.message}\n#{extract_causes(error.cause)}"
224
+ else
225
+ ""
226
+ end
227
+ end
228
+
229
+ ⦅self_test⦆
230
+
231
+ ⦅report_self_test_failure⦆
232
+
233
+ ⦅filter_class⦆
234
+
235
+ ⦅tangle_class⦆
236
+
237
+ ⦅option_verification⦆
238
+
239
+ description "⦅description⦆"
240
+ ⦅options⦆
241
+
242
+ version Lmt::VERSION
243
+
244
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
245
+
246
+ go! if __FILE__ == $0
247
+ end
248
+
249
+ end
250
+
251
+ ```
252
+
253
+ 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).
254
+
255
+ 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.
256
+
257
+ ###### Code Block: Main Body
258
+
259
+ ``` ruby
260
+ self_test()
261
+ tangler = Tangle::Tangler.new(options[:file])
262
+ tangler.tangle()
263
+ tangler.write(options[:output])
264
+ ```
265
+
266
+ Finally, we have the dependencies. Optparse and methadone are used for cli argument handling and other niceties.
267
+
268
+ ###### Code Block: Includes
269
+
270
+ ``` ruby
271
+ require 'optparse'
272
+ require 'methadone'
273
+ require 'lmt/version'
274
+ ```
275
+
276
+ There, now we are done with the boilerplate. On to:
277
+
278
+ ## The Actual Tangler
279
+
280
+ The tangler is defined within a class that contains the tangling implementation. It contains the following blocks
281
+
282
+ ###### Code Block: Tangle Class
283
+
284
+ ``` ruby
285
+ class Tangler
286
+ class << self
287
+ attr_reader :filters
288
+ end
289
+
290
+ @filters = ⦅filter_list⦆
291
+
292
+ ⦅initializer⦆
293
+ ⦅tangle⦆
294
+ ⦅read_file⦆
295
+ ⦅include_includes⦆
296
+ ⦅parse_blocks⦆
297
+ ⦅expand_macros⦆
298
+ ⦅apply_filters⦆
299
+ ⦅unescape_double_parens⦆
300
+ ⦅write⦆
301
+
302
+ private
303
+ ⦅tangle_class_privates⦆
304
+ end
305
+ ```
306
+
307
+ ### Initializer
308
+
309
+ 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.
310
+
311
+ ###### Code Block: Initializer
312
+
313
+ ``` ruby
314
+ def initialize(input)
315
+ @input = input
316
+ @block = ""
317
+ @blocks = {}
318
+ @tangled = false
319
+ end
320
+
321
+ ```
322
+
323
+ ### Tangle
324
+
325
+ 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.
326
+
327
+ ###### Code Block: Tangle
328
+
329
+ ``` ruby
330
+ def tangle()
331
+ contents = include_includes(read_file(@input))
332
+ @block, @blocks = parse_blocks(contents)
333
+ if @block
334
+ @block = expand_macros(@block)
335
+ @block = unescape_double_parens(@block)
336
+ end
337
+ @tangled = true
338
+ end
339
+
340
+ ```
341
+
342
+ ### Reading The File
343
+
344
+ This is fairly self explanatory, though note, we are storing the file in memory as an array of lines.
345
+
346
+ ###### Code Block: Read File
347
+
348
+ ``` ruby
349
+ def read_file(file)
350
+ File.open(file, 'r') do |f|
351
+ f.readlines
352
+ end
353
+ end
354
+
355
+ ```
356
+
357
+ ### Including the Includes
358
+
359
+ 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:
360
+
361
+ **See include:** [lmt_expressions.lmd](include_file)
362
+
363
+ 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.
364
+
365
+ ###### Code Block: Include Includes
366
+
367
+ ``` ruby
368
+ def include_includes(lines, current_file = @input, depth = 0)
369
+ raise "too many includes" if depth > 1000
370
+ include_exp = ⦅include_expression⦆
371
+ lines.map do |line|
372
+ match = include_exp.match(line)
373
+ if match
374
+ file = File.dirname(current_file) + '/' + match[1]
375
+ include_includes(read_file(file), file, depth + 1)
376
+ else
377
+ [line]
378
+ end
379
+ end.flatten(1)
380
+ end
381
+
382
+ ```
383
+
384
+ ### Parsing The Blocks
385
+
386
+ Now we get to the meat of the algorithm. This uses the regular expression in [lmt_expressions](lmt_expressions.lmd#The-Code-Block-Expression)
387
+
388
+ 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.
389
+
390
+ 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.
391
+
392
+ 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.
393
+
394
+ ###### Code Block: Parse Blocks
395
+
396
+ ``` ruby
397
+ def parse_blocks(lines)
398
+ code_block_exp = ⦅code_block_expression⦆
399
+ in_block = false
400
+ blocks = lines.find_all do |line|
401
+ in_block = !in_block if line =~ code_block_exp
402
+ in_block
403
+ end.slice_before do |line|
404
+ code_block_exp =~ line
405
+ end.map do |(header, *rest)|
406
+ white_space, language, replacement_mark, name = code_block_exp.match(header)[1..-1]
407
+ [name, replacement_mark, rest]
408
+ end.group_by do |(name, _, _)|
409
+ name
410
+ end.transform_values do |bodies|
411
+ last_replacement_index = get_last_replacement_index(bodies)
412
+ bodies[last_replacement_index..-1].map { |(_, _, body)| body}
413
+ .flatten(1)
414
+ end.transform_values do |body_lines|
415
+ body_lines[-1] = body_lines[-1].chomp if body_lines[-1]
416
+ body_lines
417
+ end
418
+ throw "Missing code fence" if in_block
419
+ main = blocks[""]
420
+ blocks.delete("")
421
+ [main, blocks]
422
+ end
423
+
424
+ ```
425
+
426
+ 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.
427
+
428
+ ###### Code Block: Tangle Class Privates
429
+
430
+ ``` ruby
431
+ def get_last_replacement_index(bodies)
432
+ last_replacement = bodies.each_with_index
433
+ .select do |((_, replacement_mark, _), _)|
434
+ replacement_mark == '='
435
+ end[-1]
436
+ if last_replacement
437
+ last_replacement[1]
438
+ else
439
+ 0
440
+ end
441
+ end
442
+
443
+ ```
444
+
445
+ ### Handling the macros
446
+
447
+ 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.
448
+
449
+ 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.)
450
+
451
+ ###### Code Block: Expand Macros
452
+
453
+ ``` ruby
454
+ def expand_macros(lines, depth = 0)
455
+ throw "too deep macro expansion {depth}" if depth > 1000
456
+ lines.map do |line|
457
+ begin
458
+ expand_macro_on_line(line, depth)
459
+ rescue Exception => e
460
+ raise Exception, "Failed to process line: #{line}", e.backtrace
461
+ end
462
+ end.flatten(1)
463
+ end
464
+
465
+ ```
466
+
467
+ Expand_macro_on_line turns a line into a list of lines. The collected results will have to be flattened by 1.
468
+
469
+ 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.
470
+
471
+ ###### Code Block: Tangle Class Privates
472
+
473
+ ``` ruby
474
+ def expand_macro_on_line(line, depth)
475
+ white_space_exp = /^(\s*)(.*\n?)/
476
+ macro_substitution_exp = ⦅macro_substitution_expression⦆
477
+ filter_extraction_exp = / *\| *([-\w]+) */
478
+ white_space, text = white_space_exp.match(line)[1..2]
479
+ ```
480
+
481
+ 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.
482
+
483
+ ###### Code Block: Tangle Class Privates
484
+
485
+ ``` ruby
486
+ section = text.split(macro_substitution_exp)
487
+ .each_slice(2)
488
+ .map do |(text_before_macro, macro_match)|
489
+ if (macro_match)
490
+ macro_name, *filters = macro_match.strip.split(filter_extraction_exp)
491
+ [text_before_macro, macro_name, filters.each_slice(2).map(&:first)]
492
+ else
493
+ [text_before_macro]
494
+ end
495
+ ```
496
+
497
+ 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.
498
+
499
+ ###### Code Block: Tangle Class Privates
500
+
501
+ ``` ruby
502
+ end.inject([white_space]) do
503
+ |(*new_lines, last_line), (text_before_macro, macro_name, filters)|
504
+ if macro_name.nil?
505
+ last_line = "" unless last_line
506
+ new_lines << last_line + text_before_macro
507
+ else
508
+ ```
509
+
510
+ 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.
511
+
512
+ ###### Code Block: Tangle Class Privates
513
+
514
+ ``` ruby
515
+ throw "Macro '#{macro_name}' unknown" unless @blocks[macro_name]
516
+ macro_lines = apply_filters(
517
+ expand_macros(@blocks[macro_name], depth + 1), filters)
518
+ unless macro_lines.empty?
519
+ new_line = last_line + text_before_macro + macro_lines[0]
520
+ macro_continued = macro_lines[1..-1].map do |macro_line|
521
+ white_space + macro_line
522
+ end
523
+ (new_lines << new_line) + macro_continued
524
+ else
525
+ new_lines
526
+ end
527
+ end
528
+ end
529
+ end
530
+ ```
531
+
532
+ 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.
533
+
534
+ ### Unescaping Double Parentheses
535
+
536
+ 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.
537
+
538
+ ###### Code Block: Unescape Double Parens
539
+
540
+ ``` ruby
541
+ def unescape_double_parens(block)
542
+ block.map do |l|
543
+ l = l.gsub("\\\⦅", "⦅")
544
+ l = l.gsub("\\\⦆", "⦆")
545
+ l
546
+ end
547
+ end
548
+
549
+ ```
550
+
551
+ ### Write The Output
552
+
553
+ Finally, if there is a default block, write the output.
554
+
555
+ ###### Code Block: Write
556
+
557
+ ``` ruby
558
+ def write(output)
559
+ tangle() unless @tangled
560
+ if @block
561
+ fout = File.open(output, 'w')
562
+ @block.each {|line| fout << line}
563
+ end
564
+ end
565
+
566
+ ```
567
+
568
+ ## The Filters
569
+
570
+ 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.
571
+
572
+ ###### Code Block: Filter Class
573
+
574
+ ``` ruby
575
+ class Filter
576
+ def initialize(&block)
577
+ @code = block;
578
+ end
579
+
580
+ def filter(lines)
581
+ @code.call(lines)
582
+ end
583
+ end
584
+ ```
585
+
586
+ Because it is fairly common to filter lines one at a time, LineFilter will pass in each line instead of the whole block.
587
+
588
+ ###### Code Block: Filter Class
589
+
590
+ ``` ruby
591
+ class LineFilter < Filter
592
+ def filter(lines)
593
+ lines.map do |line|
594
+ @code.call(line)
595
+ end
596
+ end
597
+ end
598
+ ```
599
+
600
+ Filters are applied by the following method:
601
+
602
+ ###### Code Block: Apply Filters
603
+
604
+ ``` ruby
605
+ def apply_filters(strings, filters)
606
+ filters.map do |filter_name|
607
+ Tangler.filters[filter_name]
608
+ end.inject(strings) do |strings, filter|
609
+ filter.filter(strings)
610
+ end
611
+ end
612
+ ```
613
+
614
+
615
+ ### Ruby Escape
616
+
617
+ Ruby escape escapes strings appropriately for Ruby.
618
+
619
+ ###### Code Block: Ruby Escape
620
+
621
+ ``` ruby
622
+ LineFilter.new do |line|
623
+ line.dump[1..-2]
624
+ end
625
+ ```
626
+
627
+ ## Option Verification
628
+
629
+ Option verification is described here:
630
+
631
+ **See include:** [option_verification.lmd](include_file)
632
+
633
+ ## Self Test, Details
634
+
635
+ So, now we need to go into details of our self test and also include regressions which have caused problems.
636
+
637
+ First, we need a method to report test failures:
638
+
639
+ **See include:** [error_reporting.lmd](include_file)
640
+
641
+ 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.
642
+
643
+ ###### Code Block: Test Description
644
+
645
+ ``` ruby
646
+ ⦅intentionally_empty_block⦆
647
+ ⦅test_macro_insertion_description⦆
648
+ ⦅test_filters⦆
649
+ ⦅test_inclusion⦆
650
+ ⦅intentionally_empty_block⦆
651
+ ```
652
+
653
+ ### Testing: Macros
654
+
655
+ 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
656
+
657
+ ###### Code Block: Test Macro Insertion Description
658
+
659
+ ``` ruby
660
+ ⦅macro_insertion_description⦆
661
+ # These require the code in the macro to work.
662
+ report_self_test_failure("block replacement doesn't work") unless block_replacement and replaced_block
663
+ report_self_test_failure("appending to macros doesn't work") unless block_appendment
664
+ report_self_test_failure("insertion must support spaces") unless insertion_works_with_spaces
665
+ report_self_test_failure("double parentheses may be escaped") unless escaped_string[0] != '\\'
666
+ ```
667
+
668
+ Finally, we need to make sure two macros on the same line works.
669
+
670
+ ###### Code Block: Test Macro Insertion Description
671
+
672
+ ``` ruby
673
+ two_macros = "⦅foo⦆ ⦅foo⦆"
674
+ report_self_test_failure("Should be able to place two macros on the same line") unless two_macros == "foo foo"
675
+ ```
676
+
677
+ For that to work we need:
678
+
679
+ ###### Code Block: Insertion Works With Spaces
680
+
681
+ ``` ruby
682
+ insertion_works_with_spaces = true
683
+ ```
684
+
685
+ and
686
+
687
+ ###### Code Block: Foo
688
+
689
+ ``` ruby
690
+ foo
691
+ ```
692
+
693
+ ### Testing: Filters
694
+
695
+ 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.
696
+
697
+ ###### Code Block: Test Filters
698
+
699
+ ``` ruby
700
+ ⦅filter_use_description⦆
701
+ report_self_test_failure("ruby escape doesn't escape backslash") unless string_with_backslash =~ /\\.?/
702
+ ```
703
+
704
+ ### Testing: Inclusion
705
+
706
+ ###### Code Block: Test Inclusion
707
+
708
+ ``` ruby
709
+ ⦅included_block⦆
710
+ report_self_test_failure("included replacements should replace blocks") unless included_string == "I came from lmt_include.lmd"
711
+ ```
712
+
713
+ ### Regressions
714
+
715
+ Some regressions / edge cases that we need to watch for. These should not break our tangle operation.
716
+
717
+ #### Empty Blocks
718
+
719
+ We need to be able to tangle empty blocks such as:
720
+
721
+ ###### Code Block: Intentionally Empty Block
722
+
723
+ ``` ruby
724
+ ```
725
+
726
+ #### Unused blocks referencing nonexistent blocks
727
+
728
+ If a block is unused, then don't break if it uses a nonexistent block.
729
+
730
+ ###### Code Block: Unused Block
731
+
732
+ ``` ruby
733
+ ⦅this_block_does_not_exist⦆
734
+ ```
735
+
736
+ ## Fin ┐( ˘_˘)┌
737
+
738
+ And with that, we have tangled a file.
739
+
740
+ 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.
741
+
742
+