lmt 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.markdownlint.json +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/README.md +73 -0
- data/Rakefile +75 -0
- data/bin/console +25 -0
- data/bin/lmt +6 -0
- data/bin/lmw +5 -0
- data/bin/setup +8 -0
- data/doc/lmt/error_reporting.md +15 -0
- data/doc/lmt/lmt.rb.md +742 -0
- data/doc/lmt/lmt_expressions.md +33 -0
- data/doc/lmt/lmt_include.md +9 -0
- data/doc/lmt/lmw.rb.md +396 -0
- data/doc/lmt/option_verification.md +20 -0
- data/lib/lmt.rb +4 -0
- data/lib/lmt/lmt.rb +277 -0
- data/lib/lmt/lmw.rb +190 -0
- data/lib/lmt/version.rb +3 -0
- data/lmt.gemspec +30 -0
- data/src/lmt/error_reporting.lmd +13 -0
- data/src/lmt/lmt.rb.lmd +652 -0
- data/src/lmt/lmt_expressions.lmd +27 -0
- data/src/lmt/lmt_include.lmd +7 -0
- data/src/lmt/lmw.rb.lmd +358 -0
- data/src/lmt/option_verification.lmd +18 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.markdownlint.json
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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>>.
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/bin/console
ADDED
@@ -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
data/bin/lmw
ADDED
data/bin/setup
ADDED
@@ -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
|
+
```
|
data/doc/lmt/lmt.rb.md
ADDED
@@ -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
|
+
∎
|