lmt 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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