rdeck 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +290 -0
- data/exe/rdeck +9 -0
- data/lib/rdeck/action_generator.rb +179 -0
- data/lib/rdeck/apply.rb +171 -0
- data/lib/rdeck/block_quote.rb +27 -0
- data/lib/rdeck/body.rb +25 -0
- data/lib/rdeck/cli.rb +361 -0
- data/lib/rdeck/client.rb +128 -0
- data/lib/rdeck/config.rb +97 -0
- data/lib/rdeck/fragment.rb +34 -0
- data/lib/rdeck/hungarian.rb +215 -0
- data/lib/rdeck/image.rb +100 -0
- data/lib/rdeck/image_uploader.rb +75 -0
- data/lib/rdeck/markdown/cel_evaluator.rb +109 -0
- data/lib/rdeck/markdown/frontmatter.rb +75 -0
- data/lib/rdeck/markdown/parser.rb +449 -0
- data/lib/rdeck/paragraph.rb +33 -0
- data/lib/rdeck/presentation.rb +216 -0
- data/lib/rdeck/request_builder.rb +416 -0
- data/lib/rdeck/retry_handler.rb +58 -0
- data/lib/rdeck/slide.rb +64 -0
- data/lib/rdeck/slide_extractor.rb +211 -0
- data/lib/rdeck/table.rb +61 -0
- data/lib/rdeck/version.rb +5 -0
- data/lib/rdeck.rb +28 -0
- metadata +185 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdeck
|
|
4
|
+
class Hungarian
|
|
5
|
+
def initialize(cost_matrix)
|
|
6
|
+
@original_matrix = cost_matrix
|
|
7
|
+
@n = cost_matrix.size
|
|
8
|
+
@matrix = deep_copy(cost_matrix)
|
|
9
|
+
@marked_rows = Array.new(@n, false)
|
|
10
|
+
@marked_cols = Array.new(@n, false)
|
|
11
|
+
@starred = Array.new(@n) { Array.new(@n, false) }
|
|
12
|
+
@primed = Array.new(@n) { Array.new(@n, false) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def solve
|
|
16
|
+
negate_matrix!
|
|
17
|
+
|
|
18
|
+
subtract_row_minimums!
|
|
19
|
+
|
|
20
|
+
subtract_column_minimums!
|
|
21
|
+
|
|
22
|
+
loop do
|
|
23
|
+
clear_marks!
|
|
24
|
+
star_zeros!
|
|
25
|
+
|
|
26
|
+
break if count_starred_zeros == @n
|
|
27
|
+
|
|
28
|
+
cover_columns!
|
|
29
|
+
|
|
30
|
+
loop do
|
|
31
|
+
row, col = find_uncovered_zero
|
|
32
|
+
if row.nil?
|
|
33
|
+
adjust_matrix!
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@primed[row][col] = true
|
|
38
|
+
|
|
39
|
+
star_col = find_star_in_row(row)
|
|
40
|
+
if star_col.nil?
|
|
41
|
+
augment_path(row, col)
|
|
42
|
+
clear_primes!
|
|
43
|
+
break
|
|
44
|
+
else
|
|
45
|
+
@marked_rows[row] = true
|
|
46
|
+
@marked_cols[star_col] = false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
break if count_starred_zeros == @n
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
extract_solution
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def deep_copy(matrix)
|
|
59
|
+
matrix.map(&:dup)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def negate_matrix!
|
|
63
|
+
@n.times do |i|
|
|
64
|
+
@n.times do |j|
|
|
65
|
+
@matrix[i][j] = -@matrix[i][j]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def subtract_row_minimums!
|
|
71
|
+
@n.times do |i|
|
|
72
|
+
min = @matrix[i].min
|
|
73
|
+
@n.times do |j|
|
|
74
|
+
@matrix[i][j] -= min
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def subtract_column_minimums!
|
|
80
|
+
@n.times do |j|
|
|
81
|
+
min = Array.new(@n) { |i| @matrix[i][j] }.min
|
|
82
|
+
@n.times do |i|
|
|
83
|
+
@matrix[i][j] -= min
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def clear_marks!
|
|
89
|
+
@marked_rows.fill(false)
|
|
90
|
+
@marked_cols.fill(false)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def star_zeros!
|
|
94
|
+
@n.times do |i|
|
|
95
|
+
@n.times do |j|
|
|
96
|
+
next unless @matrix[i][j].zero?
|
|
97
|
+
next if row_has_star?(i) || col_has_star?(j)
|
|
98
|
+
|
|
99
|
+
@starred[i][j] = true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def row_has_star?(row)
|
|
105
|
+
@starred[row].any?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def col_has_star?(col)
|
|
109
|
+
@n.times.any? { |i| @starred[i][col] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def count_starred_zeros
|
|
113
|
+
@starred.flatten.count(true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cover_columns!
|
|
117
|
+
@n.times do |j|
|
|
118
|
+
@marked_cols[j] = true if col_has_star?(j)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def find_uncovered_zero
|
|
123
|
+
@n.times do |i|
|
|
124
|
+
next if @marked_rows[i]
|
|
125
|
+
|
|
126
|
+
@n.times do |j|
|
|
127
|
+
next if @marked_cols[j]
|
|
128
|
+
|
|
129
|
+
return [i, j] if @matrix[i][j].zero?
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def find_star_in_row(row)
|
|
136
|
+
@n.times do |j|
|
|
137
|
+
return j if @starred[row][j]
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def find_prime_in_row(row)
|
|
143
|
+
@n.times do |j|
|
|
144
|
+
return j if @primed[row][j]
|
|
145
|
+
end
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def find_star_in_col(col)
|
|
150
|
+
@n.times do |i|
|
|
151
|
+
return i if @starred[i][col]
|
|
152
|
+
end
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def augment_path(row, col)
|
|
157
|
+
path = [[row, col]]
|
|
158
|
+
|
|
159
|
+
loop do
|
|
160
|
+
star_row = find_star_in_col(path.last[1])
|
|
161
|
+
break if star_row.nil?
|
|
162
|
+
|
|
163
|
+
path << [star_row, path.last[1]]
|
|
164
|
+
|
|
165
|
+
prime_col = find_prime_in_row(star_row)
|
|
166
|
+
path << [star_row, prime_col]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
path.each_with_index do |(r, c), idx|
|
|
170
|
+
@starred[r][c] = idx.even?
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def clear_primes!
|
|
175
|
+
@n.times do |i|
|
|
176
|
+
@n.times do |j|
|
|
177
|
+
@primed[i][j] = false
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def adjust_matrix!
|
|
183
|
+
min = Float::INFINITY
|
|
184
|
+
@n.times do |i|
|
|
185
|
+
next if @marked_rows[i]
|
|
186
|
+
|
|
187
|
+
@n.times do |j|
|
|
188
|
+
next if @marked_cols[j]
|
|
189
|
+
|
|
190
|
+
min = @matrix[i][j] if @matrix[i][j] < min
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
@n.times do |i|
|
|
195
|
+
@n.times do |j|
|
|
196
|
+
if @marked_rows[i] && @marked_cols[j]
|
|
197
|
+
@matrix[i][j] += min
|
|
198
|
+
elsif !@marked_rows[i] && !@marked_cols[j]
|
|
199
|
+
@matrix[i][j] -= min
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def extract_solution
|
|
206
|
+
solution = {}
|
|
207
|
+
@n.times do |i|
|
|
208
|
+
@n.times do |j|
|
|
209
|
+
solution[i] = j if @starred[i][j]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
solution
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/lib/rdeck/image.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
|
|
7
|
+
module Rdeck
|
|
8
|
+
class Image
|
|
9
|
+
MIME_PNG = 'image/png'
|
|
10
|
+
MIME_JPEG = 'image/jpeg'
|
|
11
|
+
MIME_GIF = 'image/gif'
|
|
12
|
+
|
|
13
|
+
attr_accessor :data, :mime_type, :url, :from_markdown, :checksum, :phash, :mod_time, :link
|
|
14
|
+
|
|
15
|
+
def initialize(path_or_url, from_markdown: false, link: '')
|
|
16
|
+
@from_markdown = from_markdown
|
|
17
|
+
@link = link
|
|
18
|
+
load_from(path_or_url)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def equivalent?(other)
|
|
22
|
+
return false unless other.is_a?(Image)
|
|
23
|
+
return false if mime_type != other.mime_type
|
|
24
|
+
return false if link != other.link
|
|
25
|
+
return true if checksum == other.checksum
|
|
26
|
+
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def checksum
|
|
31
|
+
@checksum ||= Zlib.crc32(data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def phash
|
|
35
|
+
@phash ||= calculate_phash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mime_type_to_ext
|
|
39
|
+
case mime_type
|
|
40
|
+
when MIME_PNG then '.png'
|
|
41
|
+
when MIME_JPEG then '.jpg'
|
|
42
|
+
when MIME_GIF then '.gif'
|
|
43
|
+
else '.png'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def load_from(path_or_url)
|
|
50
|
+
if path_or_url.start_with?('http://', 'https://')
|
|
51
|
+
load_from_url(path_or_url)
|
|
52
|
+
else
|
|
53
|
+
load_from_file(path_or_url)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def load_from_url(url_str)
|
|
58
|
+
@url = url_str
|
|
59
|
+
uri = URI.parse(url_str)
|
|
60
|
+
response = Net::HTTP.get_response(uri)
|
|
61
|
+
raise Error, "Failed to fetch image from #{url_str}: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
62
|
+
|
|
63
|
+
@data = response.body
|
|
64
|
+
@mime_type = detect_mime_type(response['content-type'])
|
|
65
|
+
@mod_time = Time.now
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_from_file(path)
|
|
69
|
+
raise Error, "Image file not found: #{path}" unless File.exist?(path)
|
|
70
|
+
|
|
71
|
+
@data = File.binread(path)
|
|
72
|
+
@mime_type = detect_mime_type_from_data(@data)
|
|
73
|
+
@mod_time = File.mtime(path)
|
|
74
|
+
@url = "file://#{File.expand_path(path)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def detect_mime_type(content_type)
|
|
78
|
+
return MIME_PNG if content_type&.include?('png')
|
|
79
|
+
return MIME_JPEG if content_type&.include?('jpeg') || content_type&.include?('jpg')
|
|
80
|
+
return MIME_GIF if content_type&.include?('gif')
|
|
81
|
+
|
|
82
|
+
detect_mime_type_from_data(@data)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def detect_mime_type_from_data(data)
|
|
86
|
+
return MIME_PNG if data[0, 8] == "\x89PNG\r\n\x1A\n".b
|
|
87
|
+
return MIME_JPEG if data[0, 2] == "\xFF\xD8".b
|
|
88
|
+
return MIME_GIF if %w[GIF87a GIF89a].include?(data[0, 6])
|
|
89
|
+
|
|
90
|
+
MIME_PNG
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def calculate_phash
|
|
94
|
+
checksum
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
warn "Failed to calculate pHash: #{e.message}"
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
module Rdeck
|
|
6
|
+
class ImageUploader
|
|
7
|
+
def initialize(drive_service, folder_id: nil)
|
|
8
|
+
@drive_service = drive_service
|
|
9
|
+
@folder_id = folder_id
|
|
10
|
+
@uploaded_files = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def upload(image)
|
|
14
|
+
temp_file = create_temp_file(image)
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
file_metadata = Google::Apis::DriveV3::File.new(
|
|
18
|
+
name: "rdeck_temp_#{Time.now.to_i}_#{rand(10_000)}#{image.mime_type_to_ext}",
|
|
19
|
+
parents: @folder_id ? [@folder_id] : nil
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
file = @drive_service.create_file(
|
|
23
|
+
file_metadata,
|
|
24
|
+
fields: 'id,webContentLink,webViewLink',
|
|
25
|
+
upload_source: temp_file.path,
|
|
26
|
+
content_type: image.mime_type,
|
|
27
|
+
supports_all_drives: true
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
permission = Google::Apis::DriveV3::Permission.new(
|
|
31
|
+
type: 'anyone',
|
|
32
|
+
role: 'reader'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@drive_service.create_permission(
|
|
36
|
+
file.id,
|
|
37
|
+
permission,
|
|
38
|
+
supports_all_drives: true
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@uploaded_files << file.id
|
|
42
|
+
|
|
43
|
+
file.web_content_link || file.web_view_link
|
|
44
|
+
ensure
|
|
45
|
+
temp_file.close
|
|
46
|
+
temp_file.unlink
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def upload_batch(images)
|
|
51
|
+
images.map { |image| upload(image) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cleanup
|
|
55
|
+
@uploaded_files.each do |file_id|
|
|
56
|
+
@drive_service.delete_file(file_id, supports_all_drives: true)
|
|
57
|
+
rescue Google::Apis::Error => e
|
|
58
|
+
warn "Failed to delete temporary file #{file_id}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
@uploaded_files.clear
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def create_temp_file(image)
|
|
66
|
+
ext = image.mime_type_to_ext
|
|
67
|
+
temp_file = Tempfile.new(['rdeck_image', ext])
|
|
68
|
+
temp_file.binmode
|
|
69
|
+
temp_file.write(image.data)
|
|
70
|
+
temp_file.flush
|
|
71
|
+
temp_file.rewind
|
|
72
|
+
temp_file
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdeck
|
|
4
|
+
module Markdown
|
|
5
|
+
class CELEvaluator
|
|
6
|
+
def evaluate(expression, context)
|
|
7
|
+
return false if expression.nil? || expression.empty?
|
|
8
|
+
|
|
9
|
+
expr = expression.dup
|
|
10
|
+
context.each do |key, value|
|
|
11
|
+
expr = replace_variable(expr, key.to_s, value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
expr = transform_methods(expr)
|
|
15
|
+
|
|
16
|
+
safe_eval(expr, context)
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
warn "Failed to evaluate CEL expression '#{expression}': #{e.message}"
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def replace_variable(expr, key, value)
|
|
25
|
+
case value
|
|
26
|
+
when Integer, Float
|
|
27
|
+
expr.gsub(/\b#{Regexp.escape(key)}\b/, value.to_s)
|
|
28
|
+
when String
|
|
29
|
+
expr.gsub(/\b#{Regexp.escape(key)}\b/, "\"#{escape_string(value)}\"")
|
|
30
|
+
when Array
|
|
31
|
+
array_str = "[#{value.map { |v| serialize_value(v) }.join(', ')}]"
|
|
32
|
+
expr.gsub(/\b#{Regexp.escape(key)}\b/, array_str)
|
|
33
|
+
when Hash
|
|
34
|
+
hash_str = "{#{value.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }.join(', ')}}"
|
|
35
|
+
expr.gsub(/\b#{Regexp.escape(key)}\b/, hash_str)
|
|
36
|
+
when true, false
|
|
37
|
+
expr.gsub(/\b#{Regexp.escape(key)}\b/, value.to_s)
|
|
38
|
+
else
|
|
39
|
+
expr
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def serialize_value(value)
|
|
44
|
+
case value
|
|
45
|
+
when Integer, Float, true, false
|
|
46
|
+
value.to_s
|
|
47
|
+
when String
|
|
48
|
+
"\"#{escape_string(value)}\""
|
|
49
|
+
when Array
|
|
50
|
+
"[#{value.map { |v| serialize_value(v) }.join(', ')}]"
|
|
51
|
+
when Hash
|
|
52
|
+
"{#{value.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }.join(', ')}}"
|
|
53
|
+
else
|
|
54
|
+
'nil'
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def escape_string(str)
|
|
59
|
+
str.gsub('"', '\\"').gsub("\n", '\\n')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transform_methods(expr)
|
|
63
|
+
expr
|
|
64
|
+
.gsub('.size()', '.size')
|
|
65
|
+
.gsub(/\.contains\(([^)]+)\)/, '.include?(\1)')
|
|
66
|
+
.gsub(/\.startsWith\(([^)]+)\)/, '.start_with?(\1)')
|
|
67
|
+
.gsub(/\.endsWith\(([^)]+)\)/, '.end_with?(\1)')
|
|
68
|
+
.gsub(/\.matches\(([^)]+)\)/, '.match?(\1)')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def safe_eval(expr, _context)
|
|
72
|
+
return false unless safe_expression?(expr)
|
|
73
|
+
|
|
74
|
+
# rubocop:disable Security/Eval
|
|
75
|
+
eval(expr)
|
|
76
|
+
# rubocop:enable Security/Eval
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def safe_expression?(expr)
|
|
82
|
+
dangerous_patterns = [
|
|
83
|
+
/`/,
|
|
84
|
+
/system/i,
|
|
85
|
+
/exec/i,
|
|
86
|
+
/fork/i,
|
|
87
|
+
/spawn/i,
|
|
88
|
+
/load/i,
|
|
89
|
+
/require/i,
|
|
90
|
+
/File\./,
|
|
91
|
+
/Dir\./,
|
|
92
|
+
/IO\./,
|
|
93
|
+
/Kernel\./,
|
|
94
|
+
/\beval\b/,
|
|
95
|
+
/send/i,
|
|
96
|
+
/instance_eval/i,
|
|
97
|
+
/class_eval/i,
|
|
98
|
+
/module_eval/i,
|
|
99
|
+
/define_method/i,
|
|
100
|
+
/__send__/,
|
|
101
|
+
/const_get/i,
|
|
102
|
+
/const_set/i
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
dangerous_patterns.none? { |pattern| expr.match?(pattern) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Rdeck
|
|
6
|
+
module Markdown
|
|
7
|
+
class Frontmatter
|
|
8
|
+
attr_accessor :presentation_id, :title, :breaks, :defaults, :code_block_to_image_command
|
|
9
|
+
|
|
10
|
+
def initialize(attrs = {})
|
|
11
|
+
@presentation_id = attrs[:presentationID] || attrs['presentationID']
|
|
12
|
+
@title = attrs[:title] || attrs['title']
|
|
13
|
+
@breaks = attrs[:breaks] || attrs['breaks']
|
|
14
|
+
@defaults = parse_defaults(attrs[:defaults] || attrs['defaults'])
|
|
15
|
+
@code_block_to_image_command = attrs[:codeBlockToImageCommand] || attrs['codeBlockToImageCommand']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.extract(content)
|
|
19
|
+
return [nil, content] unless content.start_with?("---\n")
|
|
20
|
+
|
|
21
|
+
lines = content.lines
|
|
22
|
+
return [nil, content] unless lines.first == "---\n"
|
|
23
|
+
|
|
24
|
+
closing_index = lines[1..].index { |line| line.strip == '---' }
|
|
25
|
+
return [nil, content] unless closing_index
|
|
26
|
+
|
|
27
|
+
yaml_content = lines[1..closing_index].join
|
|
28
|
+
body_content = lines[(closing_index + 2)..].join
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
yaml = YAML.safe_load(yaml_content, permitted_classes: [Symbol], symbolize_names: true)
|
|
32
|
+
frontmatter = new(yaml || {})
|
|
33
|
+
[frontmatter, body_content]
|
|
34
|
+
rescue Psych::SyntaxError => e
|
|
35
|
+
warn "Failed to parse frontmatter: #{e.message}"
|
|
36
|
+
[nil, content]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.apply_to_file(file_path, title, presentation_id)
|
|
41
|
+
content = File.exist?(file_path) ? File.read(file_path) : ''
|
|
42
|
+
frontmatter, body = extract(content)
|
|
43
|
+
|
|
44
|
+
frontmatter ||= new
|
|
45
|
+
frontmatter.title = title if title
|
|
46
|
+
frontmatter.presentation_id = presentation_id if presentation_id
|
|
47
|
+
|
|
48
|
+
new_content = "#{to_yaml_frontmatter(frontmatter)}\n#{body}"
|
|
49
|
+
File.write(file_path, new_content)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.to_yaml_frontmatter(frontmatter)
|
|
53
|
+
hash = {}
|
|
54
|
+
hash['presentationID'] = frontmatter.presentation_id if frontmatter.presentation_id
|
|
55
|
+
hash['title'] = frontmatter.title if frontmatter.title
|
|
56
|
+
hash['breaks'] = frontmatter.breaks unless frontmatter.breaks.nil?
|
|
57
|
+
hash['defaults'] = frontmatter.defaults if frontmatter.defaults && !frontmatter.defaults.empty?
|
|
58
|
+
if frontmatter.code_block_to_image_command
|
|
59
|
+
hash['codeBlockToImageCommand'] =
|
|
60
|
+
frontmatter.code_block_to_image_command
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
"---\n#{hash.to_yaml.sub(/^---\n/, '')}---"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def parse_defaults(defaults_array)
|
|
69
|
+
return [] unless defaults_array.is_a?(Array)
|
|
70
|
+
|
|
71
|
+
defaults_array.map { |default| DefaultCondition.new(default) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|