lmt 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ require "lmt/version"
2
+
3
+ module Lmt
4
+ end
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: utf-8
3
+
4
+ require 'optparse'
5
+ require 'methadone'
6
+ require 'lmt/version'
7
+
8
+ module Lmt
9
+
10
+ class Tangle
11
+ include Methadone::Main
12
+ include Methadone::CLILogging
13
+
14
+ @dev = false
15
+
16
+ main do
17
+ check_arguments()
18
+ begin
19
+ self_test()
20
+ tangler = Tangle::Tangler.new(options[:file])
21
+ tangler.tangle()
22
+ tangler.write(options[:output])
23
+ rescue Exception => e
24
+ puts "Error: #{e.message} #{extract_causes(e)}At:"
25
+ e.backtrace.each do |trace|
26
+ puts " #{trace}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.extract_causes(error)
32
+ if (error.cause)
33
+ " Caused by: #{error.cause.message}\n#{extract_causes(error.cause)}"
34
+ else
35
+ ""
36
+ end
37
+ end
38
+
39
+ def self.self_test()
40
+
41
+ block_replacement = true
42
+ replaced_block = false
43
+ block_appendment = false
44
+
45
+ # this is the replacement
46
+ replaced_block = true
47
+ # Yay appended code gets injected
48
+ block_appendment = true
49
+ insertion_works_with_spaces = false
50
+ insertion_works_with_spaces = true
51
+ escaped_string = '⦅macro_description⦆'
52
+ # These require the code in the macro to work.
53
+ report_self_test_failure("block replacement doesn't work") unless block_replacement and replaced_block
54
+ report_self_test_failure("appending to macros doesn't work") unless block_appendment
55
+ report_self_test_failure("insertion must support spaces") unless insertion_works_with_spaces
56
+ report_self_test_failure("double parentheses may be escaped") unless escaped_string[0] != '\\'
57
+ two_macros = "foo foo"
58
+ report_self_test_failure("Should be able to place two macros on the same line") unless two_macros == "foo foo"
59
+ string_with_backslash = "this string ends in \\."
60
+ report_self_test_failure("ruby escape doesn't escape backslash") unless string_with_backslash =~ /\\.?/
61
+ included_string = "I came from lmt_include.lmd"
62
+ report_self_test_failure("included replacements should replace blocks") unless included_string == "I came from lmt_include.lmd"
63
+
64
+ end
65
+
66
+ def self.report_self_test_failure(message)
67
+ if @dev
68
+ p message
69
+ else
70
+ throw message
71
+ end
72
+ end
73
+
74
+ class Filter
75
+ def initialize(&block)
76
+ @code = block;
77
+ end
78
+
79
+ def filter(lines)
80
+ @code.call(lines)
81
+ end
82
+ end
83
+ class LineFilter < Filter
84
+ def filter(lines)
85
+ lines.map do |line|
86
+ @code.call(line)
87
+ end
88
+ end
89
+ end
90
+
91
+ class Tangler
92
+ class << self
93
+ attr_reader :filters
94
+ end
95
+
96
+ @filters = {
97
+ 'ruby_escape' => LineFilter.new do |line|
98
+ line.dump[1..-2]
99
+ end
100
+ }
101
+
102
+ def initialize(input)
103
+ @input = input
104
+ @block = ""
105
+ @blocks = {}
106
+ @tangled = false
107
+ end
108
+
109
+ def tangle()
110
+ contents = include_includes(read_file(@input))
111
+ @block, @blocks = parse_blocks(contents)
112
+ if @block
113
+ @block = expand_macros(@block)
114
+ @block = unescape_double_parens(@block)
115
+ end
116
+ @tangled = true
117
+ end
118
+
119
+ def read_file(file)
120
+ File.open(file, 'r') do |f|
121
+ f.readlines
122
+ end
123
+ end
124
+
125
+ def include_includes(lines, current_file = @input, depth = 0)
126
+ raise "too many includes" if depth > 1000
127
+ include_exp = /^!\s+include\s+\[.*\]\((.*)\)\s*$/
128
+ lines.map do |line|
129
+ match = include_exp.match(line)
130
+ if match
131
+ file = File.dirname(current_file) + '/' + match[1]
132
+ include_includes(read_file(file), file, depth + 1)
133
+ else
134
+ [line]
135
+ end
136
+ end.flatten(1)
137
+ end
138
+
139
+ def parse_blocks(lines)
140
+ code_block_exp = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
141
+ in_block = false
142
+ blocks = lines.find_all do |line|
143
+ in_block = !in_block if line =~ code_block_exp
144
+ in_block
145
+ end.slice_before do |line|
146
+ code_block_exp =~ line
147
+ end.map do |(header, *rest)|
148
+ white_space, language, replacement_mark, name = code_block_exp.match(header)[1..-1]
149
+ [name, replacement_mark, rest]
150
+ end.group_by do |(name, _, _)|
151
+ name
152
+ end.transform_values do |bodies|
153
+ last_replacement_index = get_last_replacement_index(bodies)
154
+ bodies[last_replacement_index..-1].map { |(_, _, body)| body}
155
+ .flatten(1)
156
+ end.transform_values do |body_lines|
157
+ body_lines[-1] = body_lines[-1].chomp if body_lines[-1]
158
+ body_lines
159
+ end
160
+ throw "Missing code fence" if in_block
161
+ main = blocks[""]
162
+ blocks.delete("")
163
+ [main, blocks]
164
+ end
165
+
166
+ def expand_macros(lines, depth = 0)
167
+ throw "too deep macro expansion {depth}" if depth > 1000
168
+ lines.map do |line|
169
+ begin
170
+ expand_macro_on_line(line, depth)
171
+ rescue Exception => e
172
+ raise Exception, "Failed to process line: #{line}", e.backtrace
173
+ end
174
+ end.flatten(1)
175
+ end
176
+
177
+ def apply_filters(strings, filters)
178
+ filters.map do |filter_name|
179
+ Tangler.filters[filter_name]
180
+ end.inject(strings) do |strings, filter|
181
+ filter.filter(strings)
182
+ end
183
+ end
184
+ def unescape_double_parens(block)
185
+ block.map do |l|
186
+ l = l.gsub("\\⦅", "⦅")
187
+ l = l.gsub("\\⦆", "⦆")
188
+ l
189
+ end
190
+ end
191
+
192
+ def write(output)
193
+ tangle() unless @tangled
194
+ if @block
195
+ fout = File.open(output, 'w')
196
+ @block.each {|line| fout << line}
197
+ end
198
+ end
199
+
200
+
201
+ private
202
+ def get_last_replacement_index(bodies)
203
+ last_replacement = bodies.each_with_index
204
+ .select do |((_, replacement_mark, _), _)|
205
+ replacement_mark == '='
206
+ end[-1]
207
+ if last_replacement
208
+ last_replacement[1]
209
+ else
210
+ 0
211
+ end
212
+ end
213
+
214
+ def expand_macro_on_line(line, depth)
215
+ white_space_exp = /^(\s*)(.*\n?)/
216
+ macro_substitution_exp = /(?<!\\)⦅ *([-\w | ]*) *⦆/
217
+ filter_extraction_exp = / *\| *([-\w]+) */
218
+ white_space, text = white_space_exp.match(line)[1..2]
219
+ section = text.split(macro_substitution_exp)
220
+ .each_slice(2)
221
+ .map do |(text_before_macro, macro_match)|
222
+ if (macro_match)
223
+ macro_name, *filters = macro_match.strip.split(filter_extraction_exp)
224
+ [text_before_macro, macro_name, filters.each_slice(2).map(&:first)]
225
+ else
226
+ [text_before_macro]
227
+ end
228
+ end.inject([white_space]) do
229
+ |(*new_lines, last_line), (text_before_macro, macro_name, filters)|
230
+ if macro_name.nil?
231
+ last_line = "" unless last_line
232
+ new_lines << last_line + text_before_macro
233
+ else
234
+ throw "Macro '#{macro_name}' unknown" unless @blocks[macro_name]
235
+ macro_lines = apply_filters(
236
+ expand_macros(@blocks[macro_name], depth + 1), filters)
237
+ unless macro_lines.empty?
238
+ new_line = last_line + text_before_macro + macro_lines[0]
239
+ macro_continued = macro_lines[1..-1].map do |macro_line|
240
+ white_space + macro_line
241
+ end
242
+ (new_lines << new_line) + macro_continued
243
+ else
244
+ new_lines
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def self.required(*options)
252
+ @required_options = options
253
+ end
254
+
255
+ def self.check_arguments
256
+ missing = @required_options.select{ |p| options[p].nil?}
257
+ unless missing.empty?
258
+ message = "Missing Required Argument(s): #{missing.join(', ')}"
259
+
260
+ abort("#{message}\n\n#{opts.help()}")
261
+ end
262
+ end
263
+
264
+ description "A literate Markdown tangle tool written in Ruby."
265
+ on("--file FILE", "-f", "Required: input file")
266
+ on("--output FILE", "-o", "Required: output file")
267
+ on("--dev", "disables self test failure for development")
268
+ required(:file, :output)
269
+
270
+ version Lmt::VERSION
271
+
272
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
273
+
274
+ go! if __FILE__ == $0
275
+ end
276
+
277
+ end
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: utf-8
3
+
4
+ require 'optparse'
5
+ require 'methadone'
6
+
7
+ require 'pry'
8
+
9
+ module Lmt
10
+
11
+ class Lmw
12
+ include Methadone::Main
13
+ include Methadone::CLILogging
14
+
15
+ @dev = true
16
+
17
+ main do
18
+ check_arguments()
19
+ begin
20
+ self_test()
21
+ weave = Lmw::Weave.from_file(options[:file])
22
+ weave.weave()
23
+ weave.write(options[:output])
24
+ rescue Exception => e
25
+ puts "Error: #{e.message} #{extract_causes(e)}At:"
26
+ e.backtrace.each do |trace|
27
+ puts " #{trace}"
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.extract_causes(error)
33
+ if (error.cause)
34
+ " Caused by: #{error.cause.message}\n#{extract_causes(error.cause)}"
35
+ else
36
+ ""
37
+ end
38
+ end
39
+
40
+ def self.self_test()
41
+ end
42
+
43
+ def self.report_self_test_failure(message)
44
+ if @dev
45
+ p message
46
+ else
47
+ throw message
48
+ end
49
+ end
50
+
51
+ class Weave
52
+ class << self
53
+ def from_file(file)
54
+ File.open(file, 'r') do |f|
55
+ Weave.new(f.readlines, file)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ def initialize(lines, file_name = "")
62
+ @file_name = file_name
63
+ @lines = lines
64
+ @weaved = false
65
+ end
66
+ def weave()
67
+ @blocks = find_blocks(@lines)
68
+ @weaved_lines = substitute_directives_and_headers(
69
+ @lines.map do |line|
70
+ replace_markdown_links(line)
71
+ end)
72
+ @weaved = true
73
+ end
74
+ def write(output)
75
+ fout = File.open(output, 'w')
76
+ weave() unless @weaved
77
+ @weaved_lines.each {|line| fout << line}
78
+ end
79
+
80
+
81
+ private
82
+ def include_includes(lines, current_file = @file_name, current_path = '', depth = 0)
83
+ raise "too many includes" if depth > 1000
84
+ include_exp = /^!\s+include\s+\[.*\]\((.*)\)\s*$/
85
+ lines.map do |line|
86
+ match = include_exp.match(line)
87
+ if match
88
+ file = File.dirname(current_file) + '/' + match[1]
89
+ path = File.dirname(current_path) + '/' + match[1]
90
+ new_lines = File.open(file, 'r') {|f| f.readlines}
91
+ include_includes(new_lines, file, path, depth + 1)
92
+ else
93
+ [[line, current_path]]
94
+ end
95
+ end.flatten(1)
96
+ end
97
+
98
+ def find_blocks(lines)
99
+ lines_with_includes = include_includes(lines)
100
+ code_block_exp = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
101
+ headers_and_footers = lines_with_includes.filter do |(line, source_file)|
102
+ code_block_exp =~ line
103
+ end
104
+ throw "Missing code fence" if headers_and_footers.length % 2 != 0
105
+ headers_and_footers.each_slice(2).map(&:first)
106
+ .map do |(header, source_file)|
107
+ white_space, language, replacement_mark, name = code_block_exp.match(header)[1..-1]
108
+ [name, source_file, language, replacement_mark]
109
+ end.group_by do |name, _, _, _|
110
+ name
111
+ end.transform_values do |blocks|
112
+ block_name, _, block_language, _ = blocks[0]
113
+ count, _ = blocks.inject(0) do |count, (name, source_file, language, replacement_mark)|
114
+ throw "block #{block_name} has multiple languages" unless language == block_language
115
+ count + 1
116
+ end
117
+ block_locations = blocks.each_with_index.map do |(name, source_file, language, replacement_mark), index|
118
+ [name, index, source_file]
119
+ end
120
+ {:count => count, :block_locations => block_locations}
121
+ end
122
+ end
123
+ def substitute_directives_and_headers(lines)
124
+ include_expression = /^!\s+include\s+\[.*\]\((.*)\)\s*$/
125
+ code_block_expression = /^([s]*)``` ?([\w]*) ?(=?)([-\w]*)?/
126
+ in_block = false
127
+ block_name = ""
128
+ lines.map do |line|
129
+ case line
130
+ when include_expression
131
+ include_file = $1
132
+ ["**See include:** [#{include_file}](include_file)\n"]
133
+ when code_block_expression
134
+ in_block = !in_block
135
+ if in_block
136
+ white_space, language, replacement_mark, name =
137
+ code_block_expression.match(line)[1..-1]
138
+ human_name = name.gsub(/[-_]/, ' ').split(' ').map(&:capitalize).join(' ')
139
+ replacing = if replacement_mark == "="
140
+ " Replacing"
141
+ else
142
+ ""
143
+ end
144
+ header = if name != ""
145
+ "#######{replacing} Code Block: #{human_name}\n\n"
146
+ else
147
+ "#######{replacing} Output Block\n\n"
148
+ end
149
+ [header,
150
+ "#{white_space}``` #{language}\n"]
151
+ else
152
+ [line]
153
+ end
154
+ else
155
+ [line]
156
+ end
157
+ end.flatten(1)
158
+ end
159
+ def replace_markdown_links(line)
160
+ line
161
+ end
162
+ end
163
+
164
+ def self.required(*options)
165
+ @required_options = options
166
+ end
167
+
168
+ def self.check_arguments
169
+ missing = @required_options.select{ |p| options[p].nil?}
170
+ unless missing.empty?
171
+ message = "Missing Required Argument(s): #{missing.join(', ')}"
172
+
173
+ abort("#{message}\n\n#{opts.help()}")
174
+ end
175
+ end
176
+
177
+ description "A literate Markdown weave tool written in Ruby."
178
+ on("--file FILE", "-f", "Required: input file")
179
+ on("--output FILE", "-o", "Required: output file")
180
+ on("--dev", "disables self test failure for development")
181
+ required(:file, :output)
182
+
183
+ version Lmt::VERSION
184
+
185
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
186
+
187
+ go! if __FILE__ == $0
188
+ end
189
+
190
+ end