marked-conductor 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ class Condition
5
+ def initialize(condition)
6
+ @condition = condition
7
+ @env = Conductor::Env.env
8
+ end
9
+
10
+ def true?
11
+ parse_condition
12
+ end
13
+
14
+ def split_booleans(condition)
15
+ split = condition.split(/ ((?:AND )?NOT|AND|OR) /)
16
+
17
+ if split.count == 1
18
+ test_condition(split[0])
19
+ else
20
+ res = nil
21
+ bool = nil
22
+ prev = false
23
+ split.each do |cond|
24
+ if cond =~ /((?:AND )?NOT|AND|OR)/
25
+ bool = cond.bool_to_symbol
26
+ next
27
+ end
28
+
29
+ r = split_booleans(cond)
30
+
31
+ if bool == :and && (!r || !prev)
32
+ res = false
33
+ elsif bool == :or && (r || prev)
34
+ return true
35
+ elsif bool == :not && (r || prev)
36
+ res = false
37
+ else
38
+ res = r
39
+ end
40
+
41
+ prev = res
42
+ end
43
+ res
44
+ end
45
+ end
46
+
47
+ def test_operator(value1, value2, operator)
48
+ case operator
49
+ when :gt
50
+ value1.to_f > value2.to_f
51
+ when :lt
52
+ value1.to_f < value2.to_f
53
+ when :contains
54
+ value1.to_s =~ /#{value2}/i
55
+ when :starts_with
56
+ value1.to_s =~ /^#{value2}/i
57
+ when :ends_with
58
+ value1.to_s =~ /#{value2}$/i
59
+ when :not_equal
60
+ value1 != value2
61
+ when :equal
62
+ value1 == value2
63
+ end
64
+ end
65
+
66
+ def split_condition(condition)
67
+ res = condition.match(/^(?<val1>.*?)(?:(?: +(?<op>(?:is|does)(?: not)?(?: an?|type(?: of)?|equals?(?: to))?|!?==?|[gl]t|(?:greater|less)(?: than)?|<|>|(?:starts|ends) with|(?:ha(?:s|ve) )?(?:prefix|suffix)|has|contains?|includes?) +)(?<val2>.*?))?$/i)
68
+ [res['val1'], res['val2'], operator_to_symbol(res['op'])]
69
+ end
70
+
71
+ def test_type(val1, val2, operator)
72
+ res = case val2
73
+ when /array/i
74
+ val1.is_a?(Array)
75
+ when /(string|text)/i
76
+ val1.is_a?(String)
77
+ when /date/i
78
+ val1.date?
79
+ end
80
+ operator == :type_of ? res : !res
81
+ end
82
+
83
+ def test_string(val1, val2, operator)
84
+ return operator == :not_equal ? val1.nil? : !val1.nil? if val2.nil?
85
+
86
+ return operator == :not_equal if val1.nil?
87
+
88
+ if val1.date?
89
+ if val2.time?
90
+ date1 = val1.to_date
91
+ date2 = val2.to_date
92
+ else
93
+ date1 = operator == :gt ? val1.to_day(:end) : val1.to_day
94
+ date2 = operator == :gt ? val2.to_day(:end) : val1.to_day
95
+ end
96
+
97
+ res = case operator
98
+ when :gt
99
+ date1 > date2
100
+ when :lt
101
+ date1 < date2
102
+ when :equal
103
+ date1 == date2
104
+ when :not_equal
105
+ date1 != date2
106
+ end
107
+ return res unless res.nil?
108
+ end
109
+
110
+ val2 = if val2.strip =~ %r{^/.*?/$}
111
+ val2.gsub(%r{(^/|/$)}, '')
112
+ else
113
+ Regexp.escape(val2)
114
+ end
115
+
116
+ case operator
117
+ when :contains
118
+ val1.to_s =~ /#{val2}/i
119
+ when :not_starts_with
120
+ val1.to_s !~ /^#{val2}/i
121
+ when :not_ends_with
122
+ val1.to_s !~ /#{val2}$/i
123
+ when :starts_with
124
+ val1.to_s =~ /^#{val2}/i
125
+ when :ends_with
126
+ val1.to_s =~ /#{val2}$/i
127
+ when :equal
128
+ val1.to_s =~ /^#{val2}$/i
129
+ when :not_equal
130
+ val1.to_s !~ /^#{val2}$/i
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+ def test_tree(origin, value, operator)
137
+ return true if File.exist?(File.join(origin, value))
138
+
139
+ dir = File.dirname(origin)
140
+
141
+ if Dir.exist?(File.join(dir, value))
142
+ true
143
+ elsif [Dir.home, '/'].include?(dir)
144
+ false
145
+ else
146
+ test_tree(dir, value, operator)
147
+ end
148
+ end
149
+
150
+ def test_truthy(value1, value2, operator)
151
+ return false unless value2.bool?
152
+
153
+ value2.to_bool!
154
+
155
+ res = value1 == value2
156
+
157
+ operator == :not_equal ? !res : res
158
+ end
159
+
160
+ def test_condition(condition)
161
+ type, value, operator = split_condition(condition)
162
+
163
+ if operator.nil?
164
+ return case type
165
+ when /^(true|any|all|else|\*+|catch(all)?)$/i
166
+ true
167
+ else
168
+ false
169
+ end
170
+ end
171
+
172
+ case type
173
+ when /^ext/i
174
+ test_string(@env[:ext], value, operator) ? true : false
175
+ when /^tree/i
176
+ test_tree(@env[:origin], value, operator)
177
+ when /^(path|dir)/i
178
+ test_string(@env[:origin], value, operator) ? true : false
179
+ when /^phase/i
180
+ test_string(@env[:phase], value, :starts_with) ? true : false
181
+ when /^text/i
182
+ test_string(IO.read(@env[:filepath]), value, operator) ? true : false
183
+ when /^(yaml|headers|frontmatter)(?::(.*?))?$/i
184
+ m = Regexp.last_match
185
+ content = IO.read(@env[:filepath])
186
+ return false unless content =~ /^---/
187
+
188
+ yaml = YAML.safe_load(content.split(/(---|\.\.\.)/)[1])
189
+ if m[2]
190
+ value1 = yaml[m[2]]
191
+ value1 = value1.join(',') if value1.is_a?(Array)
192
+ if %i[type_of not_type_of].include?(operator)
193
+ test_type(value1, value, operator)
194
+ elsif value1.is_a?(Boolean)
195
+ test_truthy(value1, value, operator)
196
+ elsif value1.number? && value2.number? && %i[gt lt equal not_equal].include?(operator)
197
+ test_operator(value1, value, operator)
198
+ else
199
+ test_string(value1, value, operator)
200
+ end
201
+ else
202
+ res = value? ? yaml.key?(value) : true
203
+ operator == :not_equal ? !res : res
204
+ end
205
+ else
206
+ false
207
+ end
208
+ end
209
+
210
+ def operator_to_symbol(operator)
211
+ return operator if operator.nil?
212
+
213
+ case operator
214
+ when /(gt|greater( than)?|>|(?:is )?after)/i
215
+ :gt
216
+ when /(lt|less( than)?|<|(?:is )?before)/i
217
+ :lt
218
+ when /(ha(?:s|ve)|contains|includes|match(es)?|\*=)/i
219
+ :contains
220
+ when /not (suffix|ends? with)/i
221
+ :not_ends_with
222
+ when /not (prefix|(starts?|begins?) with)/i
223
+ :not_starts_with
224
+ when /(suffix|ends with|\$=)/i
225
+ :ends_with
226
+ when /(prefix|(starts?|begins?) with|\^=)/i
227
+ :starts_with
228
+ when /is not (an?|type( of)?)/i
229
+ :not_type_of
230
+ when /is (an?|type( of)?)/i
231
+ :type_of
232
+ when /((?:(?:is|does) )?not(?: equals?)?|!==?)/i
233
+ :not_equal
234
+ when /(is|==?|equals?)/i
235
+ :equal
236
+ end
237
+ end
238
+
239
+ def parse_condition
240
+ cond = @condition.to_s.gsub(/\((.*?)\)/) do
241
+ condition = Regexp.last_match(1)
242
+ split_booleans(condition)
243
+ end
244
+
245
+ split_booleans(cond)
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ # Configuration methods
5
+ class Config
6
+ attr_reader :config, :tracks
7
+
8
+ def initialize
9
+ config_file = File.expand_path('~/.config/conductor/tracks.yaml')
10
+
11
+ create_config(config_file) unless File.exist?(config_file)
12
+
13
+ @config ||= YAML.safe_load(IO.read(config_file))
14
+
15
+ @tracks = @config['tracks'].symbolize_keys
16
+ end
17
+ end
18
+
19
+ def create_config(config_file)
20
+ config_dir = File.dirname(config_file)
21
+ scripts_dir = File.dirname(File.join(config_file, 'scripts'))
22
+ FileUtils.mkdir_p(config_dir) unless File.directory?(config_dir)
23
+ FileUtils.mkdir_p(scripts_dir) unless File.directory?(scripts_dir)
24
+ File.open(config_file, 'w') { |f| f.puts sample_config }
25
+ puts "Sample config created at #{config_file}"
26
+
27
+ Process.exit 0
28
+ end
29
+
30
+ def sample_config
31
+ <<~EOCONFIG
32
+ tracks:
33
+ - condition: phase is pre
34
+ tracks:
35
+ - condition: tree contains .obsidian
36
+ tracks:
37
+ - condition: extension is md
38
+ script: obsidian-md-filter
39
+ - condition: extension is md
40
+ command: rdiscount $file
41
+ - condition: yaml includes comments
42
+ script: blog-processor
43
+ - condition: any
44
+ command: echo 'NOCUSTOM'
45
+ EOCONFIG
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ module Env
5
+ def self.env
6
+ @env ||= if ENV['CONDUCTOR_TEST'] == 'true'
7
+ load_test_env
8
+ else
9
+ @env ||= {
10
+ home: ENV['HOME'],
11
+ css_path: ENV['MARKED_CSS_PATH'],
12
+ ext: ENV['MARKED_EXT'],
13
+ includes: ENV['MARKED_INCLUDES'],
14
+ origin: ENV['MARKED_ORIGIN'],
15
+ filepath: ENV['MARKED_PATH'],
16
+ phase: ENV['MARKED_PHASE'],
17
+ outline: ENV['OUTLINE'],
18
+ path: ENV['PATH']
19
+ }
20
+ end
21
+
22
+ @env
23
+ end
24
+
25
+ def self.load_test_env
26
+ @env = {
27
+ home: '/Users/ttscoff',
28
+ css_path: '/Applications/Marked 2.app/Contents/Resources/swiss.css',
29
+ ext: 'md',
30
+ includes: [],
31
+ origin: '/Users/ttscoff/Library/Mobile Documents/9CR7T2DMDG~com~ngocluu~onewriter/Documents/nvALT2.2/',
32
+ filepath: '/Users/ttscoff/Library/Mobile Documents/9CR7T2DMDG~com~ngocluu~onewriter/Documents/nvALT2.2/bt.com App Review- AeroPress timer for iPhone.md',
33
+ phase: 'PREPROCESS',
34
+ outline: 'NONE',
35
+ path: '/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/Users/ttscoff/Library/Mobile Documents/9CR7T2DMDG~com~ngocluu~onewriter/Documents/nvALT2.2'
36
+ }
37
+ end
38
+
39
+ def self.to_s
40
+ out_h = {
41
+ 'HOME' => @env[:home],
42
+ 'MARKED_CSS_PATH' => @env[:css_path],
43
+ 'MARKED_EXT' => @env[:ext],
44
+ 'MARKED_ORIGIN' => @env[:origin],
45
+ 'MARKED_INCLUDES' => @env[:includes],
46
+ 'MARKED_PATH' => @env[:filepath],
47
+ 'MARKED_PHASE' => @env[:phase],
48
+ 'OUTLINE' => @env[:outline],
49
+ 'PATH'=> @env[:path]
50
+ }
51
+ out_h.map { |k, v| %(#{k}="#{v}") }.join(' ')
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Hash
4
+ def symbolize_keys!
5
+ replace symbolize_keys
6
+ end
7
+
8
+ def symbolize_keys
9
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ # Script runner
5
+ class Script
6
+ attr_reader :args, :path
7
+
8
+ def initialize(script)
9
+ parts = Shellwords.split(script)
10
+ self.path = parts[0]
11
+ self.args = parts[1..].join(' ')
12
+ end
13
+
14
+ def path=(path)
15
+ @path = if path =~ %r{^[%/]}
16
+ File.expand_path(path)
17
+ else
18
+ script_dir = File.expand_path('~/.config/conductor/scripts')
19
+ if File.exist?(File.join(script_dir, path))
20
+ File.join(script_dir, path)
21
+ elsif TTY::Which.exist?(path)
22
+ TTY::Which.which(path)
23
+ else
24
+ raise "Path to #{path} not found"
25
+
26
+ end
27
+ end
28
+ end
29
+
30
+ def args=(array)
31
+ @args = if array.is_a?(Array)
32
+ array.join(' ')
33
+ else
34
+ array
35
+ end
36
+ end
37
+
38
+ def run
39
+ stdin = Conductor.stdin
40
+
41
+ raise 'Script path not defined' unless @path
42
+
43
+ use_stdin = true
44
+ if args =~ /\$\{?file\}?/
45
+ use_stdin = false
46
+ args.sub!(/\$\{?file\}?/, Env.env[:filepath])
47
+ else
48
+ raise 'No input' unless stdin
49
+
50
+ end
51
+
52
+ if use_stdin
53
+ `echo #{Shellwords.escape(stdin)} | #{Env} #{path} #{args}`
54
+ else
55
+ `#{Env} #{path} #{args}`
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # String helpers
4
+ class ::String
5
+ def bool_to_symbol
6
+ case self
7
+ when /NOT/
8
+ :not
9
+ when /AND/
10
+ :and
11
+ else
12
+ :or
13
+ end
14
+ end
15
+
16
+ def date?
17
+ match(/^\d{4}-\d{2}-\d{2}/) ? true : false
18
+ end
19
+
20
+ def time?
21
+ match(/ \d{1,2}(:\d\d)? *([ap]m)?/i)
22
+ end
23
+
24
+ def to_date
25
+ Chronic.parse(self)
26
+ end
27
+
28
+ def strip_time
29
+ sub(/ \d{1,2}(:\d\d)? *([ap]m)?/i, '')
30
+ end
31
+
32
+ def to_day(time = :end)
33
+ t = time == :end ? '23:59' : '00:00'
34
+ Chronic.parse("#{self.strip_time} #{t}")
35
+ end
36
+
37
+ def number?
38
+ to_f > 0
39
+ end
40
+
41
+ def bool?
42
+ match(/^(?:y(?:es)?|no?|t(?:rue)?|f(?:alse)?)$/) ? true : false
43
+ end
44
+
45
+ def to_bool!
46
+ replace to_bool
47
+ end
48
+
49
+ ##
50
+ ## Returns a bool representation of the string.
51
+ ##
52
+ ## @return [Boolean] Bool representation of the object.
53
+ ##
54
+ def to_bool
55
+ case self
56
+ when /^[yt]/i
57
+ true
58
+ else
59
+ false
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ VERSION = '1.0.0'
5
+ end
data/lib/conductor.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-which'
4
+ require 'yaml'
5
+ require 'shellwords'
6
+ require 'fcntl'
7
+ require 'time'
8
+ require 'chronic'
9
+ require 'fileutils'
10
+ require_relative 'conductor/env'
11
+ require_relative 'conductor/config'
12
+ require_relative 'conductor/hash'
13
+ require_relative 'conductor/array'
14
+ require_relative 'conductor/string'
15
+ require_relative 'conductor/script'
16
+ require_relative 'conductor/command'
17
+ require_relative 'conductor/condition'
18
+
19
+ module Conductor
20
+ class << self
21
+ def stdin
22
+ @stdin ||= $stdin.read.strip if $stdin.stat.size.positive? || $stdin.fcntl(Fcntl::F_GETFL, 0).zero?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/conductor/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "marked-conductor"
7
+ spec.version = Conductor::VERSION
8
+ spec.authors = ["Brett Terpstra"]
9
+ spec.email = ["me@brettterpstra.com"]
10
+
11
+ spec.summary = "A custom processor manager for Marked 2 (Mac)"
12
+ spec.description = "Conductor allows easy configuration of multiple scripts that are run as custom pre/processors for Marked based on conditional statements."
13
+ spec.homepage = "https://github.com/ttscoff/marked-conductor"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/ttscoff/marked-conductor"
19
+ spec.metadata["changelog_uri"] = "https://github.com/ttscoff/marked-conductor/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "bin"
29
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_development_dependency "pry", "~> 0.14.2"
33
+ spec.add_development_dependency "awesome_print", "~> 1.9.2"
34
+
35
+ # Uncomment to register a new dependency of your gem
36
+ spec.add_dependency "tty-which", "~> 0.5.0"
37
+ spec.add_dependency "chronic", "~> 0.10.2"
38
+ # For more information and examples about making a new gem, checkout our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end