lmt 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # Lmt Regular Expressions
2
+
3
+ Our Lmt language depends is a regular language and depends on a few regular expressions, we are listing them here because both the tangler and weave care about them.
4
+
5
+ ## The Include Expression
6
+
7
+ The first regular expression handles the detection of include directives. It recognizes lines like `! include [some description](some-file)` and extracts `some-file`.
8
+
9
+ ``` ruby include_expression
10
+ /^!\s+include\s+\[.*\]\((.*)\)\s*$/
11
+ ```
12
+
13
+ ## The Code Block Expression
14
+
15
+ The second regular expression is intended to note when whe enter or leave a code block. It detects markdown code fences and processes the special directives. It has four groups. The first identifies white space at the beginning of the line. The second detects the language. The third determines if this is a replacement. The fourth is the name of the block (if applicable).
16
+
17
+ ``` ruby code_block_expression
18
+ /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
19
+ ```
20
+
21
+ ## The Macro Substitution Expression
22
+
23
+ The third expression identifies macro expansions surrounded with `⦅` and `⦆`. The first bit deals with making sure that the opening `⦅` isn't escaped. Then there is one group which contains the name of the macro combined and any filters which are being used.
24
+
25
+ ``` ruby macro_substitution_expression
26
+ /(?<!\\)⦅ *([-\w | ]*) *⦆/
27
+ ```
@@ -0,0 +1,7 @@
1
+ # A file to be included in Lmt-Ruby
2
+
3
+ This is just a file to be included in lmt-Ruby. The contents of this file will be included in the output of lmt but not lmm. Any blocks defined here will be available for use in Lmt.
4
+
5
+ ``` ruby =included_block
6
+ included_string = "I came from lmt_include.lmd"
7
+ ```
@@ -0,0 +1,358 @@
1
+ # Lmw-Ruby
2
+
3
+ ``` text description
4
+ A literate Markdown weave tool written in Ruby.
5
+ ```
6
+
7
+ Lmw is a literate Markdown weave program for [literate programing](https://en.wikipedia.org/wiki/Literate_programming). This is a fairly simple program designed to turn a literate Markdown file into a more normal Markdown file without the special semantics. It is interprets the Markdown as described in [lmt-ruby](lmt.lmd). The primary changes to the output is that the header for code blocks is extracted and rendered in standard Markdown. File names are also changed from .lmd to .md. This change is also applied to links.
8
+
9
+ ## Features
10
+
11
+ In order to effectively weave a lmt file we must:
12
+
13
+ 1) Replace the lmt headers with something that a standard markdown parser will make sense of.
14
+ 2) Replace include directives with a more informative text.
15
+ 3) Update all links to .lmd files with a similar link to a .md file.
16
+
17
+ A few nice to have features:
18
+
19
+ 1) Links between reopenings of a given block in the lmw output.
20
+ 2) Add links from macro substitutions to the body of the macro.
21
+ 3) syntax verification: check for balanced code fences, make sure that all reopenings of a block are in the same language, etc.
22
+
23
+ Ideally, any links between and to blocks would also go to included files.
24
+
25
+ Currently, it puts headers on blocks, and replaces include directives with a more human version. We still need to handle the parts and linking. We also need to handle the link updating.
26
+
27
+ ## Interface
28
+
29
+ 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
30
+
31
+ ``` ruby options
32
+ on("--file FILE", "-f", "Required: input file")
33
+ on("--output FILE", "-o", "Required: output file")
34
+ on("--dev", "disables self test failure for development")
35
+ ```
36
+
37
+ Of which, both are required
38
+
39
+ ``` ruby options
40
+ required(:file, :output)
41
+ ```
42
+
43
+ ## Implementation and Example
44
+
45
+ Now for an example in implementation. Using Ruby we can write a template as below:
46
+
47
+ ```ruby
48
+ #!/usr/bin/env ruby
49
+ # Encoding: utf-8
50
+
51
+ ⦅includes⦆
52
+
53
+ module Lmt
54
+
55
+ class Lmw
56
+ include Methadone::Main
57
+ include Methadone::CLILogging
58
+
59
+ @dev = true
60
+
61
+ main do
62
+ check_arguments()
63
+ begin
64
+ ⦅main_body⦆
65
+ rescue Exception => e
66
+ puts "Error: #{e.message} #{extract_causes(e)}At:"
67
+ e.backtrace.each do |trace|
68
+ puts " #{trace}"
69
+ end
70
+ end
71
+ end
72
+
73
+ def self.extract_causes(error)
74
+ if (error.cause)
75
+ " Caused by: #{error.cause.message}\n#{extract_causes(error.cause)}"
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ ⦅self_test⦆
82
+
83
+ ⦅report_self_test_failure⦆
84
+
85
+ ⦅weave_class⦆
86
+
87
+ ⦅option_verification⦆
88
+
89
+ description "⦅description⦆"
90
+ ⦅options⦆
91
+
92
+ version Lmt::VERSION
93
+
94
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
95
+
96
+ go! if __FILE__ == $0
97
+ end
98
+
99
+ end
100
+ ```
101
+
102
+ 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).
103
+
104
+ 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.
105
+
106
+ ``` ruby main_body
107
+ self_test()
108
+ weave = Lmw::Weave.from_file(options[:file])
109
+ weave.weave()
110
+ weave.write(options[:output])
111
+ ```
112
+
113
+ Finally, we have the dependencies. Optparse and methadone are used for cli argument handling and other niceties.
114
+
115
+ ``` ruby includes
116
+ require 'optparse'
117
+ require 'methadone'
118
+
119
+ require 'pry'
120
+ ```
121
+
122
+ There, now we are done with the boilerplate. On to:
123
+
124
+ ## The Actual Weaver
125
+
126
+ The weaver is defined within a class that contains the weaving implementation
127
+
128
+ ``` ruby weave_class
129
+ class Weave
130
+ class << self
131
+ ⦅from_file⦆
132
+ end
133
+
134
+ ⦅initializer⦆
135
+ ⦅weave⦆
136
+ ⦅write⦆
137
+
138
+ private
139
+ ⦅weave_class_privates⦆
140
+ end
141
+ ```
142
+
143
+ There may be some private methods, we need a block for them. They will be inserted where needed.
144
+
145
+ ``` ruby weave_class_privates
146
+ ⦅include_includes⦆
147
+ ```
148
+
149
+ ### Initializer
150
+
151
+ The initializer takes the input file and sets up our state.
152
+
153
+ ``` ruby initializer
154
+ def initialize(lines, file_name = "")
155
+ @file_name = file_name
156
+ @lines = lines
157
+ @weaved = false
158
+ end
159
+ ```
160
+
161
+ #### Factory
162
+
163
+ For testing, we want to be able to create an instance with a hard coded set of lines. Furthermore, because this processing is stateful, we want to make the input immutable. Reading from a file needs to be handled. A factory can do it.
164
+
165
+ ##### Reading the File
166
+
167
+ This is fairly self explanatory, though note, we are storing the file in memory as an array of lines.
168
+
169
+ ``` ruby from_file
170
+ def from_file(file)
171
+ File.open(file, 'r') do |f|
172
+ Weave.new(f.readlines, file)
173
+ end
174
+ end
175
+
176
+ ```
177
+
178
+ ### Weave
179
+
180
+ To weave a file, first we have to identify and construct metadata on all the blocks. Then we use that metadata to transform any lines containing a block declaration into an appropriate header for that block. Finally, we replace any links to an .lmd file with the equivalent .md link.
181
+
182
+ ``` ruby weave
183
+ def weave()
184
+ @blocks = find_blocks(@lines)
185
+ @weaved_lines = substitute_directives_and_headers(
186
+ @lines.map do |line|
187
+ replace_markdown_links(line)
188
+ end)
189
+ @weaved = true
190
+ end
191
+ ```
192
+
193
+ #### Finding the Blocks
194
+
195
+ In order to find the blocks we will need the regular expressions defined in:
196
+
197
+ ! include [lmt_expressions](lmt_expressions.lmd)
198
+
199
+ First, we get the lines from includes. then we filter the lines for only the headers and footers and check for unmatched headers and footers.
200
+
201
+ ``` ruby weave_class_privates
202
+ def find_blocks(lines)
203
+ lines_with_includes = include_includes(lines)
204
+ code_block_exp = ⦅code_block_expression⦆
205
+ headers_and_footers = lines_with_includes.filter do |(line, source_file)|
206
+ code_block_exp =~ line
207
+ end
208
+ throw "Missing code fence" if headers_and_footers.length % 2 != 0
209
+ ```
210
+
211
+ Now, we throw out all the footers and use the code_block_exp to parse them, group them by name, and generate the metadata. In this case the metadata includes 1) a count of the number of blocks with a particular name in this file. 2) the source file of each block.
212
+
213
+ We are also validating that a block only has one language.
214
+
215
+ ``` ruby weave_class_privates
216
+ headers_and_footers.each_slice(2).map(&:first)
217
+ .map do |(header, source_file)|
218
+ white_space, language, replacement_mark, name = code_block_exp.match(header)[1..-1]
219
+ [name, source_file, language, replacement_mark]
220
+ end.group_by do |name, _, _, _|
221
+ name
222
+ end.transform_values do |blocks|
223
+ block_name, _, block_language, _ = blocks[0]
224
+ count, _ = blocks.inject(0) do |count, (name, source_file, language, replacement_mark)|
225
+ throw "block #{block_name} has multiple languages" unless language == block_language
226
+ count + 1
227
+ end
228
+ block_locations = blocks.each_with_index.map do |(name, source_file, language, replacement_mark), index|
229
+ [name, index, source_file]
230
+ end
231
+ {:count => count, :block_locations => block_locations}
232
+ end
233
+ end
234
+ ```
235
+
236
+ ### Including the Includes
237
+
238
+ This depends on the expression in [lmt_expressions][lmt_expressions.md#The-Include-Expression]
239
+
240
+ 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. For each line, we also need to add the file that it came from.
241
+
242
+ ``` ruby include_includes
243
+ def include_includes(lines, current_file = @file_name, current_path = '', depth = 0)
244
+ raise "too many includes" if depth > 1000
245
+ include_exp = ⦅include_expression⦆
246
+ lines.map do |line|
247
+ match = include_exp.match(line)
248
+ if match
249
+ file = File.dirname(current_file) + '/' + match[1]
250
+ path = File.dirname(current_path) + '/' + match[1]
251
+ new_lines = File.open(file, 'r') {|f| f.readlines}
252
+ include_includes(new_lines, file, path, depth + 1)
253
+ else
254
+ [[line, current_path]]
255
+ end
256
+ end.flatten(1)
257
+ end
258
+
259
+ ```
260
+
261
+ ### Substituting the Directives and Headers
262
+
263
+ Now we need to substitute both the directives and headers with appropriate markdown replacements. To do so we need the [include expression](lmt_expressions.lmd#The-Include-Expression) and the [code block expression](lmt_expressions.lmd#The-Code-Block-Expression).
264
+
265
+ We will match the lines against the expressions and, when a match occurs, we will substitute the appropriate template. There is a little complexity when dealing with entering and exiting code fences. Specifically, we will need to toggle between entering and exiting code fence behavior.
266
+
267
+ ``` ruby weave_class_privates
268
+ def substitute_directives_and_headers(lines)
269
+ include_expression = ⦅include_expression⦆
270
+ code_block_expression = ⦅code_block_expression⦆
271
+ in_block = false
272
+ block_name = ""
273
+ lines.map do |line|
274
+ case line
275
+ when include_expression
276
+ include_file = $1
277
+ ["**See include:** [#{include_file}](include_file)\n"]
278
+ when code_block_expression
279
+ in_block = !in_block
280
+ if in_block
281
+ ⦅make_code_block_header⦆
282
+ else
283
+ [line]
284
+ end
285
+ else
286
+ [line]
287
+ end
288
+ end.flatten(1)
289
+ end
290
+ ```
291
+
292
+ #### The header for code blocks
293
+
294
+ Code blocks need to be headed appropriately as markdown parsing eats the code block name. Because of this we put it in a `h6` header. When the block is repeated, we add a `(part n)` to the end. We also should be adding links for the next and last version of this header.
295
+
296
+ ``` ruby make_code_block_header
297
+ white_space, language, replacement_mark, name =
298
+ code_block_expression.match(line)[1..-1]
299
+ human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
300
+ replacing = if replacement_mark == "="
301
+ " Replacing"
302
+ else
303
+ ""
304
+ end
305
+ header = if name != ""
306
+ "#######{replacing} Code Block: #{human_name}\n\n"
307
+ else
308
+ "#######{replacing} Output Block\n\n"
309
+ end
310
+ [header,
311
+ "#{white_space}``` #{language}\n"]
312
+ ```
313
+
314
+ ### Replacing the Markdown Links
315
+
316
+ ``` ruby weave_class_privates
317
+ def replace_markdown_links(line)
318
+ line
319
+ end
320
+ ```
321
+
322
+ ### Write The Output
323
+
324
+ Finally, write the output.
325
+
326
+ ``` ruby write
327
+ def write(output)
328
+ fout = File.open(output, 'w')
329
+ weave() unless @weaved
330
+ @weaved_lines.each {|line| fout << line}
331
+ end
332
+
333
+ ```
334
+
335
+ ## Option Verification
336
+
337
+ Option verification is described here:
338
+
339
+ ! include [Option verification](option_verification.lmd)
340
+
341
+ ## Testing
342
+
343
+ Of course, we will also need a testing procedure. In this case, we will be passing a set of strings in to the weave and seeing if the output is sane.
344
+
345
+ First, we need a method to report test failures:
346
+
347
+ ! include [Error reporting](error_reporting.lmd)
348
+
349
+ ``` ruby self_test
350
+ def self.self_test()
351
+ end
352
+ ```
353
+
354
+ ## Fin ┐( ˘_˘)┌
355
+
356
+ And with that, we have weaved some Markdown.
357
+
358
+
@@ -0,0 +1,18 @@
1
+ # Option Verification
2
+
3
+ Sadly neither Methadone nor Optparser offer mandatory option verification, so we have to add it ourselves. (In the future, we will probably want to move this to a support library) Doing so requires two methods, required and check_arguments
4
+
5
+ ``` ruby option_verification
6
+ def self.required(*options)
7
+ @required_options = options
8
+ end
9
+
10
+ def self.check_arguments
11
+ missing = @required_options.select{ |p| options[p].nil?}
12
+ unless missing.empty?
13
+ message = "Missing Required Argument(s): #{missing.join(', ')}"
14
+
15
+ abort("#{message}\n\n#{opts.help()}")
16
+ end
17
+ end
18
+ ```
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lmt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Marty Gentillon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rdoc
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: methadone
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.9.5
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.9.5
83
+ - !ruby/object:Gem::Dependency
84
+ name: test-unit
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: A literate tangler written in Ruby for use with MarkDown.
98
+ email:
99
+ - marty.gentillon+lmt-ruby@gmail.com
100
+ executables:
101
+ - lmt
102
+ - lmw
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".markdownlint.json"
108
+ - Gemfile
109
+ - Gemfile.lock
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/lmt
114
+ - bin/lmw
115
+ - bin/setup
116
+ - doc/lmt/error_reporting.md
117
+ - doc/lmt/lmt.rb.md
118
+ - doc/lmt/lmt_expressions.md
119
+ - doc/lmt/lmt_include.md
120
+ - doc/lmt/lmw.rb.md
121
+ - doc/lmt/option_verification.md
122
+ - lib/lmt.rb
123
+ - lib/lmt/lmt.rb
124
+ - lib/lmt/lmw.rb
125
+ - lib/lmt/version.rb
126
+ - lmt.gemspec
127
+ - src/lmt/error_reporting.lmd
128
+ - src/lmt/lmt.rb.lmd
129
+ - src/lmt/lmt_expressions.lmd
130
+ - src/lmt/lmt_include.lmd
131
+ - src/lmt/lmw.rb.lmd
132
+ - src/lmt/option_verification.lmd
133
+ homepage: https://github.com/MartyGentillon/lmt-ruby
134
+ licenses:
135
+ - MIT
136
+ metadata: {}
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.0.1
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: A literate tangler written in Ruby for use with MarkDown.
156
+ test_files: []