md2slides 0.0.3 → 0.0.5
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 +4 -4
- data/README.md +29 -26
- data/exe/md2slides +50 -27
- data/lib/md2slides/audio.rb +10 -5
- data/lib/md2slides/md.rb +23 -11
- data/lib/md2slides/presentation.rb +37 -24
- data/lib/md2slides/text_to_speech.rb +6 -1
- data/lib/md2slides/version.rb +1 -1
- data/lib/md2slides/video.rb +9 -5
- data/lib/md2slides.rb +0 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8940d8d5f290e134eae76153e0e1761a9133be69d0abfdb3ba63ef2ee0076906
|
4
|
+
data.tar.gz: d0d09ed3fc8cd268c8126445c0edcd66aa7cfb7f16832b6819e9bdca57f79ecf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f50b81972a9c0bcb5308f1cb37a3b748a154b5435b19b6438518f58d270ef437e486f2fc2fd598f574eca89ade8bc604176dbd43efe4e0e7f899999ec17cbbe6
|
7
|
+
data.tar.gz: 9c326d70264073f5f0d5aa1c5c9ce052732d4dae7c6cecaa9a80b19f6cd6888d88a451f7d5ee340058c0aeefb264b8886e7c6fe14ce8d280daf94263f56f629b
|
data/README.md
CHANGED
@@ -3,8 +3,27 @@
|
|
3
3
|
A video file of a presentation will be also generated, and it may contains narration if supplied.
|
4
4
|
Narration can be given as notes in slides, and the audio will be generated using Google text-to-speech API and ffmpeg.
|
5
5
|
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
install by gem:
|
9
|
+
|
10
|
+
% gem install md2slides
|
11
|
+
|
12
|
+
Or add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'md2slides'
|
16
|
+
```
|
17
|
+
|
18
|
+
and then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
6
22
|
## Quick start
|
7
|
-
1. Generate your Google API credentials
|
23
|
+
1. Generate your Google API credentials on API service page of your Google cloud console:
|
24
|
+
https://console.cloud.google.com/apis/
|
25
|
+
|
26
|
+
2. copy API credentials:
|
8
27
|
```
|
9
28
|
% mkdir ~/.config/gcloud
|
10
29
|
% cat >> ~/.config/gcloud/credentials.json
|
@@ -22,14 +41,10 @@ Narration can be given as notes in slides, and the audio will be generated using
|
|
22
41
|
}
|
23
42
|
^D (this means CTRL + D)
|
24
43
|
```
|
25
|
-
2. install ffmpeg if necessary (if producing video file).
|
26
44
|
|
27
|
-
3. install necessary
|
28
|
-
```
|
29
|
-
% bundle install --path vendor/bundle
|
30
|
-
```
|
45
|
+
3. install ffmpeg if necessary (if producing video file).
|
31
46
|
|
32
|
-
4. create an empty Google slide.
|
47
|
+
4. create an empty Google slide, share it with **client_email** user, and copy the URL.
|
33
48
|
|
34
49
|
5. write a markdown file.
|
35
50
|
```
|
@@ -53,33 +68,21 @@ narration text...
|
|
53
68
|
^D (this means CTRL + D)
|
54
69
|
```
|
55
70
|
|
56
|
-
6.
|
71
|
+
6. run the script.
|
57
72
|
```
|
58
|
-
|
73
|
+
% md2slides deploy sample.md
|
59
74
|
```
|
60
75
|
|
61
|
-
|
62
|
-
|
63
|
-
## Installation
|
64
|
-
|
65
|
-
Add this line to your application's Gemfile:
|
66
|
-
|
67
|
-
```ruby
|
68
|
-
gem 'md2slides'
|
69
|
-
```
|
70
|
-
|
71
|
-
And then execute:
|
72
|
-
|
73
|
-
$ bundle
|
74
|
-
|
75
|
-
Or install it yourself as:
|
76
|
-
|
77
|
-
$ gem install md2slides
|
76
|
+
the Google presentation is updated, and the video is stored in the current directory.
|
78
77
|
|
79
78
|
## Usage
|
80
79
|
|
81
80
|
TODO: Write usage instructions here
|
82
81
|
|
82
|
+
## TODO
|
83
|
+
|
84
|
+
See [TODO.md](https://github.com/ohmori7/md2slides/blob/main/TODO.md "TODO.md")
|
85
|
+
|
83
86
|
## Development
|
84
87
|
|
85
88
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/md2slides
CHANGED
@@ -3,24 +3,37 @@
|
|
3
3
|
|
4
4
|
# Copyright (c) 2025 Motoyuki OHMORI All rights reserved.
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
require 'optparse'
|
8
7
|
require 'md2slides'
|
9
8
|
|
9
|
+
options = {}
|
10
|
+
$opts = OptionParser.new do |opts|
|
11
|
+
progname = File.basename(__FILE__)
|
12
|
+
opts.banner = <<~EOF
|
13
|
+
Usage:
|
14
|
+
#{progname} list <MD> [<URL or ID>]
|
15
|
+
#{progname} update <MD> [<URL or ID>]
|
16
|
+
#{progname} fetch <MD> [<URL or ID>]
|
17
|
+
#{progname} audio <MD> [<URL or ID>]
|
18
|
+
#{progname} video <MD> [<URL or ID>]
|
19
|
+
#{progname} deploy <MD> [<URL or ID>]
|
20
|
+
#{progname} -h
|
21
|
+
EOF
|
22
|
+
opts.on('-h', '--help', 'show usage') { options[:h] = true }
|
23
|
+
opts.parse!
|
24
|
+
end
|
25
|
+
|
10
26
|
def usage(errmsg = nil)
|
11
27
|
puts "ERROR: #{errmsg}" if errmsg
|
12
28
|
puts <<~EOF
|
13
|
-
|
14
|
-
#{$progname} [-f] <file> [<URL or id>]
|
15
|
-
#{$progname} -h
|
16
|
-
|
29
|
+
#{$opts.banner}
|
17
30
|
Description:
|
18
|
-
generate a Google
|
31
|
+
generate a Google presentation file.
|
19
32
|
|
20
33
|
Argument:
|
21
34
|
-h: output this message.
|
22
|
-
<
|
23
|
-
<URL or id>: a URL or a ID of a presentation
|
35
|
+
<MD>: a file written in markdown
|
36
|
+
<URL or id>: a URL or a ID of a Google presentation
|
24
37
|
|
25
38
|
BUGS:
|
26
39
|
only .md is allowed for an input file for now.
|
@@ -29,35 +42,45 @@ BUGS:
|
|
29
42
|
exit 1
|
30
43
|
end
|
31
44
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
when nil
|
37
|
-
usage
|
38
|
-
end
|
39
|
-
|
40
|
-
path = v
|
45
|
+
usage if options[:h]
|
46
|
+
usage if ARGV.size < 2
|
47
|
+
cmd = ARGV.shift
|
48
|
+
path = ARGV.shift
|
41
49
|
filename = File.basename(path)
|
42
50
|
if filename =~ /^(.*)\.(md)$/i
|
43
51
|
name, ext = $1, $2
|
44
52
|
else
|
45
|
-
|
53
|
+
usage("cannot find the file name extention: #{filename}")
|
46
54
|
end
|
47
55
|
|
48
56
|
md = MD.new(path)
|
49
57
|
url = md.attributes[:url]
|
50
58
|
if url.nil?
|
51
59
|
url = ARGV.shift
|
52
|
-
usage("No URL and ID specified in the presentation and the argument") if
|
60
|
+
usage("No URL and ID specified in the presentation and the argument") if url.nil?
|
61
|
+
usage("extra arguments: #{ARGV.join(", ")}") if ! ARGV.nil
|
53
62
|
else
|
54
63
|
usage("URL or ID duplicatedly specified!!") if ARGV.size != 0
|
55
64
|
end
|
56
65
|
|
57
|
-
presentation = Presentation.new(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
presentation.
|
63
|
-
presentation.
|
66
|
+
presentation = Presentation.new(md)
|
67
|
+
case cmd
|
68
|
+
when 'list'
|
69
|
+
presentation.list
|
70
|
+
when 'update'
|
71
|
+
presentation.update
|
72
|
+
#presentation.stick_out_check
|
73
|
+
when 'fetch'
|
74
|
+
presentation.download
|
75
|
+
when 'audio'
|
76
|
+
presentation.generate_audio
|
77
|
+
when 'video'
|
78
|
+
presentation.generate_video
|
79
|
+
when 'deploy'
|
80
|
+
presentation.update
|
81
|
+
presentation.download
|
82
|
+
presentation.generate_audio
|
83
|
+
presentation.generate_video
|
84
|
+
else
|
85
|
+
usage("unknown command: #{cmd}")
|
86
|
+
end
|
data/lib/md2slides/audio.rb
CHANGED
@@ -2,9 +2,8 @@ class Presentation
|
|
2
2
|
# XXX: Google Text to Speech produces at this rate...
|
3
3
|
AUDIO_RATE = 24000
|
4
4
|
|
5
|
-
def generate_audio0(i,
|
6
|
-
print "slide \##{i
|
7
|
-
notes = get_slide_note(slide)
|
5
|
+
def generate_audio0(i, notes, dir)
|
6
|
+
print "slide \##{i}: generating audio... "
|
8
7
|
path = __data_slide_path(i, '.m4a', dir)
|
9
8
|
if notes
|
10
9
|
opath = __data_slide_path(i, '.mp3', dir)
|
@@ -45,8 +44,14 @@ class Presentation
|
|
45
44
|
|
46
45
|
def generate_audio(dir = nil)
|
47
46
|
dir = __data_path(dir)
|
48
|
-
@
|
49
|
-
|
47
|
+
if @md
|
48
|
+
@md.each_with_index do |page, i|
|
49
|
+
generate_audio0(i + 1, page.comments, dir)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
@presentation.slides.each_with_index do |slide, i|
|
53
|
+
generate_audio0(i + 1, get_slide_note(slide), dir)
|
54
|
+
end
|
50
55
|
end
|
51
56
|
end
|
52
57
|
end
|
data/lib/md2slides/md.rb
CHANGED
@@ -3,13 +3,13 @@ class MD
|
|
3
3
|
include Enumerable
|
4
4
|
|
5
5
|
class Element
|
6
|
-
attr_reader :type, :value
|
7
|
-
def initialize(type, value)
|
8
|
-
@type, @value = type, value
|
6
|
+
attr_reader :type, :value, :attributes
|
7
|
+
def initialize(type, value, attributes)
|
8
|
+
@type, @value, @attributes = type, value, attributes
|
9
9
|
end
|
10
10
|
|
11
11
|
def to_s
|
12
|
-
"#{@type.to_s}: #{@value}"
|
12
|
+
"#{@type.to_s}: #{@value} (#{@attributes.map { |k, v| "#{k}: #{v}"}.join(",")})"
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -18,9 +18,15 @@ class MD
|
|
18
18
|
@comments = []
|
19
19
|
end
|
20
20
|
|
21
|
-
def add(type, value)
|
21
|
+
def add(type, value, attributes = nil)
|
22
22
|
return if value.nil? || value.empty?
|
23
|
-
|
23
|
+
case type.to_s
|
24
|
+
when /^h([0-9]+)$/
|
25
|
+
n = $1.to_i - 1
|
26
|
+
attributes ||= {}
|
27
|
+
attributes[:indent] = n
|
28
|
+
end
|
29
|
+
@elements.push(Element.new(type, value, attributes))
|
24
30
|
end
|
25
31
|
|
26
32
|
def add_comment(c)
|
@@ -63,6 +69,8 @@ class MD
|
|
63
69
|
end
|
64
70
|
end
|
65
71
|
|
72
|
+
include Enumerable
|
73
|
+
|
66
74
|
attr_reader :attributes
|
67
75
|
|
68
76
|
def initialize(path)
|
@@ -79,8 +87,8 @@ class MD
|
|
79
87
|
@pages.each(&block)
|
80
88
|
end
|
81
89
|
|
82
|
-
def
|
83
|
-
@pages.
|
90
|
+
def size
|
91
|
+
@pages.size
|
84
92
|
end
|
85
93
|
|
86
94
|
def parse_header(text)
|
@@ -99,8 +107,8 @@ class MD
|
|
99
107
|
def parse_page(text)
|
100
108
|
@pages << page = Page.new
|
101
109
|
is_in_comment = false
|
102
|
-
text.each_line do |
|
103
|
-
l.strip
|
110
|
+
text.each_line do |l0|
|
111
|
+
l = l0.strip
|
104
112
|
next if l.empty?
|
105
113
|
if is_in_comment
|
106
114
|
if l =~ /(.*) ?-->(.*)$/
|
@@ -118,7 +126,11 @@ class MD
|
|
118
126
|
h = "h#{sharps.size}"
|
119
127
|
page.add(h.to_sym, title)
|
120
128
|
when /^[-*] *(.*)$/
|
121
|
-
|
129
|
+
s = $1
|
130
|
+
if l0 =~ /^( +).*$/
|
131
|
+
indent = { indent: $1.size }
|
132
|
+
end
|
133
|
+
page.add(:li, s, indent)
|
122
134
|
when /^<!-- *(.*)$/
|
123
135
|
l = $1
|
124
136
|
if l =~ /(.*) ?-->(.*)$/
|
@@ -16,10 +16,12 @@ class Presentation
|
|
16
16
|
attr_reader :id
|
17
17
|
|
18
18
|
def self.filename_sanitize(s)
|
19
|
-
s
|
19
|
+
s&.gsub(/[\/\\:\*\?"<>\|]/, '')
|
20
20
|
end
|
21
21
|
|
22
|
-
def initialize(
|
22
|
+
def initialize(md = nil)
|
23
|
+
@md = md
|
24
|
+
url = md&.attributes[:url]
|
23
25
|
if url =~ %r{https://docs.google.com/presentation/d/([^\/ ]+).*$}
|
24
26
|
@id = $1
|
25
27
|
elsif url
|
@@ -29,7 +31,6 @@ class Presentation
|
|
29
31
|
@authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
|
30
32
|
json_key_io: File.open(ENV['GOOGLE_APPLICATION_CREDENTIALS']),
|
31
33
|
scope: GOOGLE_OAUTH_SCOPE)
|
32
|
-
@authorizer.fetch_access_token!
|
33
34
|
|
34
35
|
@slides_service = Google::Apis::SlidesV1::SlidesService.new
|
35
36
|
@slides_service.client_options.application_name = APPLICATION_NAME
|
@@ -39,13 +40,9 @@ class Presentation
|
|
39
40
|
@drive_service.client_options.application_name = APPLICATION_NAME
|
40
41
|
@drive_service.authorization = @authorizer
|
41
42
|
|
42
|
-
if @id
|
43
|
-
@presentation = @slides_service.get_presentation(@id)
|
44
|
-
end
|
45
|
-
|
46
43
|
@requests = []
|
47
44
|
|
48
|
-
# XXX: this
|
45
|
+
# XXX: this seript always runs by an API user...
|
49
46
|
#people_service = Google::Apis::PeopleV1::PeopleServiceService.new
|
50
47
|
#people_service.authorization = @authorizer
|
51
48
|
#profile = people_service.get_person("people/me", person_fields: "names,emailAddresses")
|
@@ -54,13 +51,29 @@ class Presentation
|
|
54
51
|
#puts "#{name}: #{email}"
|
55
52
|
end
|
56
53
|
|
54
|
+
def __get_presentation
|
55
|
+
begin
|
56
|
+
@authorizer.fetch_access_token!
|
57
|
+
@presentation = @slides_service.get_presentation(@id)
|
58
|
+
rescue => e
|
59
|
+
require 'webrick'
|
60
|
+
raise(e, "#{e.message} (#{e.status_code} " +
|
61
|
+
"#{WEBrick::HTTPStatus.reason_phrase(e.status_code)})\n" +
|
62
|
+
"#{e.full_message}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
57
66
|
def exists?
|
58
|
-
|
67
|
+
return false if ! @id
|
68
|
+
if @presentation.nil?
|
69
|
+
__get_presentation
|
70
|
+
end
|
71
|
+
!! @presentation
|
59
72
|
end
|
60
73
|
|
61
74
|
def existence_check
|
62
75
|
if ! exists?
|
63
|
-
raise("presentation (ID: #{@id})does not exists!!!")
|
76
|
+
raise("presentation (ID: #{@id}) does not exists!!!")
|
64
77
|
end
|
65
78
|
end
|
66
79
|
|
@@ -141,8 +154,7 @@ class Presentation
|
|
141
154
|
@slides_service.batch_update_presentation(@id, batch_update_request)
|
142
155
|
@requests = []
|
143
156
|
rescue => e
|
144
|
-
|
145
|
-
raise e
|
157
|
+
raise(e, "#{e.body}\n#{e.full_message}")
|
146
158
|
end
|
147
159
|
@presentation = @slides_service.get_presentation(@id)
|
148
160
|
end
|
@@ -299,7 +311,7 @@ class Presentation
|
|
299
311
|
puts "title: #{@presentation.title}"
|
300
312
|
puts "pages: #{@presentation.slides.size}"
|
301
313
|
@presentation.slides.each_with_index do |slide, i|
|
302
|
-
puts "-
|
314
|
+
puts "- slide \##{i + 1} contains #{slide.page_elements.count} elements."
|
303
315
|
end
|
304
316
|
end
|
305
317
|
|
@@ -311,14 +323,14 @@ class Presentation
|
|
311
323
|
end
|
312
324
|
end
|
313
325
|
|
314
|
-
def update
|
326
|
+
def update
|
315
327
|
#
|
316
328
|
# XXX: currently, clear all slides...
|
317
329
|
#
|
318
330
|
if exists?
|
319
331
|
clear
|
320
332
|
end
|
321
|
-
md.each do |page|
|
333
|
+
@md.each do |page|
|
322
334
|
if page.title_subtitle_only?
|
323
335
|
layout = 'TITLE'
|
324
336
|
elsif page.title_only?
|
@@ -334,12 +346,8 @@ class Presentation
|
|
334
346
|
set_slide_subtitle(slide, page.subtitle)
|
335
347
|
else
|
336
348
|
texts = page.map do |e|
|
337
|
-
|
338
|
-
|
339
|
-
n = $1.to_i - 1
|
340
|
-
else
|
341
|
-
n = 1
|
342
|
-
end
|
349
|
+
# calculate the indentation.
|
350
|
+
n = (e.attributes&.[](:indent).to_i / 2).to_i
|
343
351
|
"\t" * n + e.value
|
344
352
|
end.join("\n")
|
345
353
|
if texts.size > 0
|
@@ -357,14 +365,19 @@ class Presentation
|
|
357
365
|
|
358
366
|
def __data_path(basedir)
|
359
367
|
basedir = '.' if basedir.nil?
|
360
|
-
|
368
|
+
if @md
|
369
|
+
title = @md.first.title
|
370
|
+
subtitle = @md.first.subtitle
|
371
|
+
elsif @presentation
|
372
|
+
title, subtitle = get_title
|
373
|
+
end
|
361
374
|
title = self.class.filename_sanitize(title)
|
362
375
|
subtitle = self.class.filename_sanitize(subtitle)
|
363
376
|
File.join(basedir, "#{title}-#{subtitle}-#{@id}")
|
364
377
|
end
|
365
378
|
|
366
379
|
def __data_slide_path(i, ext, basedir = nil)
|
367
|
-
path = "slide-#{i
|
380
|
+
path = "slide-#{i}#{ext}"
|
368
381
|
if basedir
|
369
382
|
path = File.join(basedir, path)
|
370
383
|
end
|
@@ -419,7 +432,7 @@ class Presentation
|
|
419
432
|
File.open(lockfile, File::RDWR|File::CREAT, 0644) do |f|
|
420
433
|
f.flock(File::LOCK_EX | File::LOCK_NB)
|
421
434
|
@presentation.slides.each_with_index do |slide, i|
|
422
|
-
download_slide(i, slide, dir)
|
435
|
+
download_slide(i + 1, slide, dir)
|
423
436
|
end
|
424
437
|
end
|
425
438
|
end
|
@@ -16,9 +16,14 @@ class Presentation
|
|
16
16
|
end
|
17
17
|
filename += '.mp3'
|
18
18
|
|
19
|
+
text.strip!
|
20
|
+
if text !~ /^<speak>/
|
21
|
+
text = "<speak>#{text}</speak>"
|
22
|
+
end
|
23
|
+
|
19
24
|
response = Google::Cloud::TextToSpeech.new.synthesize_speech(
|
20
25
|
# XXX: I don't know how to create an object of Google::Cloud::TextToSpeech::SynthesisInput...
|
21
|
-
{
|
26
|
+
{ ssml: text },
|
22
27
|
#
|
23
28
|
# Standard is the cheapest.
|
24
29
|
# ja-JP-Standard-A, B female
|
data/lib/md2slides/version.rb
CHANGED
data/lib/md2slides/video.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'open3'
|
2
2
|
|
3
3
|
class Presentation
|
4
|
+
VIDEO_FRAME_RATE = 1
|
5
|
+
VIDEO_ONLY_DURATION = 4
|
6
|
+
|
4
7
|
def generate_slide_video(i, dir)
|
5
8
|
img = __data_slide_path(i, '.png', dir)
|
6
9
|
audio = __data_slide_path(i, '.m4a', dir)
|
@@ -11,11 +14,11 @@ class Presentation
|
|
11
14
|
timeopt = '-shortest'
|
12
15
|
else
|
13
16
|
audioin = "-f lavfi -i aevalsrc=0"
|
14
|
-
timeopt = "-vframes
|
17
|
+
timeopt = "-vframes #{VIDEO_FRAME_RATE * VIDEO_ONLY_DURATION}"
|
15
18
|
end
|
16
19
|
cmd = <<~CMD
|
17
20
|
ffmpeg -hide_banner -y \
|
18
|
-
-framerate
|
21
|
+
-framerate #{VIDEO_FRAME_RATE} -loop 1 -i "#{img}" \
|
19
22
|
#{audioin} -map 0:v:0 -map 1:a:0 \
|
20
23
|
-c:v libx264 -tune stillimage \
|
21
24
|
-c:a aac -ar #{AUDIO_RATE} -ac 1 \
|
@@ -29,8 +32,9 @@ class Presentation
|
|
29
32
|
|
30
33
|
def generate_video(dir = nil)
|
31
34
|
dir = __data_path(dir)
|
32
|
-
@presentation
|
33
|
-
|
35
|
+
pages = @md&.size || @presentation&.slides&.size.to_i
|
36
|
+
for i in 1..pages
|
37
|
+
print "slide \##{i}: generating video..."
|
34
38
|
generate_slide_video(i, dir)
|
35
39
|
puts "done"
|
36
40
|
end
|
@@ -38,7 +42,7 @@ class Presentation
|
|
38
42
|
print "concatenate video files..."
|
39
43
|
videolist = 'video-list.txt'
|
40
44
|
File.open(File.join(dir, videolist), 'w') do |f|
|
41
|
-
|
45
|
+
for i in 1..pages
|
42
46
|
f.puts("file #{__data_slide_path(i, '.mp4')}")
|
43
47
|
end
|
44
48
|
end
|
data/lib/md2slides.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: md2slides
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Motoyuki OHMORI
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: googleauth
|