marked-conductor 1.0.0

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,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