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.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdeck
4
+ class SlideExtractor
5
+ def initialize(presentation_data)
6
+ @presentation = presentation_data
7
+ end
8
+
9
+ def extract_slides
10
+ return [] unless @presentation.slides
11
+
12
+ @presentation.slides.map do |slide_data|
13
+ extract_slide(slide_data)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def extract_slide(slide_data)
20
+ slide = Slide.new
21
+
22
+ if slide_data.slide_properties&.layout_object_id
23
+ layout = find_layout(slide_data.slide_properties.layout_object_id)
24
+ slide.layout = layout&.layout_properties&.display_name || ''
25
+ end
26
+
27
+ slide_data.page_elements&.each do |element|
28
+ process_element(slide, element)
29
+ end
30
+
31
+ if slide_data.slide_properties&.notes_page
32
+ slide.speaker_note = extract_speaker_notes(slide_data.slide_properties.notes_page)
33
+ end
34
+
35
+ slide
36
+ end
37
+
38
+ def process_element(slide, element)
39
+ return unless element.shape
40
+
41
+ shape = element.shape
42
+
43
+ if shape.placeholder
44
+ process_placeholder(slide, shape)
45
+ elsif shape.shape_type == 'TEXT_BOX'
46
+ process_text_box(slide, shape)
47
+ end
48
+
49
+ process_image(slide, element.image) if element.image
50
+
51
+ return unless element.table
52
+
53
+ process_table(slide, element.table)
54
+ end
55
+
56
+ def process_placeholder(slide, shape)
57
+ return unless shape.text
58
+
59
+ placeholder_type = shape.placeholder.type
60
+ text = extract_text(shape.text)
61
+
62
+ case placeholder_type
63
+ when 'TITLE', 'CENTERED_TITLE'
64
+ slide.titles << text unless text.empty?
65
+ slide.title_bodies << extract_body(shape.text)
66
+ when 'SUBTITLE'
67
+ slide.subtitles << text unless text.empty?
68
+ slide.subtitle_bodies << extract_body(shape.text)
69
+ when 'BODY'
70
+ slide.bodies << extract_body(shape.text) unless text.empty?
71
+ end
72
+ end
73
+
74
+ def process_text_box(slide, shape)
75
+ return unless shape.text
76
+
77
+ body = extract_body(shape.text)
78
+ slide.bodies << body unless body.empty?
79
+ end
80
+
81
+ def process_image(slide, image)
82
+ return unless image.content_url
83
+
84
+ begin
85
+ downloaded_image = Image.new(image.content_url, from_markdown: false)
86
+ slide.images << downloaded_image
87
+ rescue StandardError => e
88
+ warn "Failed to process image: #{e.message}"
89
+ end
90
+ end
91
+
92
+ def process_table(slide, table_data)
93
+ return unless table_data.table_rows
94
+
95
+ rows = table_data.table_rows.map do |row_data|
96
+ cells = row_data.table_cells.map do |cell_data|
97
+ fragments = extract_fragments(cell_data.text) if cell_data.text
98
+ TableCell.new(
99
+ fragments: fragments || [],
100
+ alignment: TableCell::ALIGN_START,
101
+ is_header: false
102
+ )
103
+ end
104
+ TableRow.new(cells: cells)
105
+ end
106
+
107
+ table = Table.new(rows: rows)
108
+ slide.tables << table
109
+ end
110
+
111
+ def extract_text(text_content)
112
+ return '' unless text_content&.text_elements
113
+
114
+ text_content.text_elements.map do |element|
115
+ element.text_run&.content || ''
116
+ end.join
117
+ end
118
+
119
+ def extract_body(text_content)
120
+ paragraphs = []
121
+ return Body.new(paragraphs: paragraphs) unless text_content&.text_elements
122
+
123
+ current_paragraph_fragments = []
124
+ current_bullet = Paragraph::BULLET_NONE
125
+ current_nesting = 0
126
+
127
+ text_content.text_elements.each do |element|
128
+ if element.paragraph_marker
129
+ unless current_paragraph_fragments.empty?
130
+ paragraphs << Paragraph.new(
131
+ fragments: current_paragraph_fragments,
132
+ bullet: current_bullet,
133
+ nesting: current_nesting
134
+ )
135
+ current_paragraph_fragments = []
136
+ end
137
+
138
+ marker = element.paragraph_marker
139
+ if marker.bullet
140
+ current_bullet = marker.bullet.list_id ? Paragraph::BULLET_DASH : Paragraph::BULLET_NONE
141
+ current_nesting = marker.bullet.nesting_level || 0
142
+ else
143
+ current_bullet = Paragraph::BULLET_NONE
144
+ current_nesting = 0
145
+ end
146
+ elsif element.text_run
147
+ fragment = text_run_to_fragment(element.text_run)
148
+ current_paragraph_fragments << fragment
149
+ end
150
+ end
151
+
152
+ unless current_paragraph_fragments.empty?
153
+ paragraphs << Paragraph.new(
154
+ fragments: current_paragraph_fragments,
155
+ bullet: current_bullet,
156
+ nesting: current_nesting
157
+ )
158
+ end
159
+
160
+ Body.new(paragraphs: paragraphs)
161
+ end
162
+
163
+ def extract_fragments(text_content)
164
+ fragments = []
165
+ return fragments unless text_content&.text_elements
166
+
167
+ text_content.text_elements.each do |element|
168
+ next unless element.text_run
169
+
170
+ fragment = text_run_to_fragment(element.text_run)
171
+ fragments << fragment
172
+ end
173
+
174
+ fragments
175
+ end
176
+
177
+ def text_run_to_fragment(text_run)
178
+ style = text_run.style || {}
179
+
180
+ Fragment.new(
181
+ value: text_run.content || '',
182
+ bold: style.bold || false,
183
+ italic: style.italic || false,
184
+ link: style.link&.url || '',
185
+ code: is_code_style?(style),
186
+ style_name: ''
187
+ )
188
+ end
189
+
190
+ def is_code_style?(style)
191
+ return false unless style.font_family
192
+
193
+ monospace_fonts = ['Courier New', 'Courier', 'Monaco', 'Consolas', 'Menlo']
194
+ monospace_fonts.any? { |font| style.font_family.include?(font) }
195
+ end
196
+
197
+ def extract_speaker_notes(notes_page)
198
+ return '' unless notes_page.page_elements
199
+
200
+ notes_page.page_elements.filter_map do |element|
201
+ next unless element.shape&.text
202
+
203
+ extract_text(element.shape.text)
204
+ end.join("\n")
205
+ end
206
+
207
+ def find_layout(layout_id)
208
+ @presentation.layouts&.find { |l| l.object_id_prop == layout_id }
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdeck
4
+ class Table
5
+ attr_accessor :rows
6
+
7
+ def initialize(rows: [])
8
+ @rows = rows
9
+ end
10
+
11
+ def ==(other)
12
+ return false unless other.is_a?(Table)
13
+
14
+ rows == other.rows
15
+ end
16
+
17
+ def empty?
18
+ rows.empty?
19
+ end
20
+ end
21
+
22
+ class TableRow
23
+ attr_accessor :cells
24
+
25
+ def initialize(cells: [])
26
+ @cells = cells
27
+ end
28
+
29
+ def ==(other)
30
+ return false unless other.is_a?(TableRow)
31
+
32
+ cells == other.cells
33
+ end
34
+ end
35
+
36
+ class TableCell
37
+ ALIGN_START = 'START'
38
+ ALIGN_CENTER = 'CENTER'
39
+ ALIGN_END = 'END'
40
+
41
+ attr_accessor :fragments, :alignment, :is_header
42
+
43
+ def initialize(fragments: [], alignment: ALIGN_START, is_header: false)
44
+ @fragments = fragments
45
+ @alignment = alignment
46
+ @is_header = is_header
47
+ end
48
+
49
+ def ==(other)
50
+ return false unless other.is_a?(TableCell)
51
+
52
+ fragments == other.fragments &&
53
+ alignment == other.alignment &&
54
+ is_header == other.is_header
55
+ end
56
+
57
+ def text
58
+ fragments.map(&:value).join
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdeck
4
+ VERSION = '0.1.0'
5
+ end
data/lib/rdeck.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rdeck/version'
4
+ require_relative 'rdeck/config'
5
+ require_relative 'rdeck/slide'
6
+ require_relative 'rdeck/body'
7
+ require_relative 'rdeck/paragraph'
8
+ require_relative 'rdeck/fragment'
9
+ require_relative 'rdeck/image'
10
+ require_relative 'rdeck/table'
11
+ require_relative 'rdeck/block_quote'
12
+ require_relative 'rdeck/client'
13
+ require_relative 'rdeck/presentation'
14
+ require_relative 'rdeck/hungarian'
15
+ require_relative 'rdeck/action_generator'
16
+ require_relative 'rdeck/request_builder'
17
+ require_relative 'rdeck/image_uploader'
18
+ require_relative 'rdeck/slide_extractor'
19
+ require_relative 'rdeck/retry_handler'
20
+ require_relative 'rdeck/apply'
21
+ require_relative 'rdeck/markdown/parser'
22
+ require_relative 'rdeck/markdown/frontmatter'
23
+ require_relative 'rdeck/markdown/cel_evaluator'
24
+ require_relative 'rdeck/cli'
25
+
26
+ module Rdeck
27
+ class Error < StandardError; end
28
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rdeck
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: commonmarker
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: google-apis-drive_v3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.76'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.76'
40
+ - !ruby/object:Gem::Dependency
41
+ name: google-apis-slides_v1
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.31'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.31'
54
+ - !ruby/object:Gem::Dependency
55
+ name: googleauth
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.16'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.16'
68
+ - !ruby/object:Gem::Dependency
69
+ name: listen
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.9'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.9'
82
+ - !ruby/object:Gem::Dependency
83
+ name: ostruct
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.6'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.6'
96
+ - !ruby/object:Gem::Dependency
97
+ name: pstore
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.2'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.2'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ruby-vips
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.3'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.3'
124
+ description: A CLI tool that applies Markdown-written presentations to Google Slides,
125
+ enabling continuous deck creation with content and design separation.
126
+ email:
127
+ - t.yudai92@gmail.com
128
+ executables:
129
+ - rdeck
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - CHANGELOG.md
134
+ - LICENSE.txt
135
+ - README.md
136
+ - exe/rdeck
137
+ - lib/rdeck.rb
138
+ - lib/rdeck/action_generator.rb
139
+ - lib/rdeck/apply.rb
140
+ - lib/rdeck/block_quote.rb
141
+ - lib/rdeck/body.rb
142
+ - lib/rdeck/cli.rb
143
+ - lib/rdeck/client.rb
144
+ - lib/rdeck/config.rb
145
+ - lib/rdeck/fragment.rb
146
+ - lib/rdeck/hungarian.rb
147
+ - lib/rdeck/image.rb
148
+ - lib/rdeck/image_uploader.rb
149
+ - lib/rdeck/markdown/cel_evaluator.rb
150
+ - lib/rdeck/markdown/frontmatter.rb
151
+ - lib/rdeck/markdown/parser.rb
152
+ - lib/rdeck/paragraph.rb
153
+ - lib/rdeck/presentation.rb
154
+ - lib/rdeck/request_builder.rb
155
+ - lib/rdeck/retry_handler.rb
156
+ - lib/rdeck/slide.rb
157
+ - lib/rdeck/slide_extractor.rb
158
+ - lib/rdeck/table.rb
159
+ - lib/rdeck/version.rb
160
+ homepage: https://github.com/ydah/rdeck
161
+ licenses:
162
+ - MIT
163
+ metadata:
164
+ homepage_uri: https://github.com/ydah/rdeck
165
+ source_code_uri: https://github.com/ydah/rdeck
166
+ changelog_uri: https://github.com/ydah/rdeck/blob/main/CHANGELOG.md
167
+ rubygems_mfa_required: 'true'
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 3.2.0
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubygems_version: 4.0.3
183
+ specification_version: 4
184
+ summary: Convert Markdown presentations to Google Slides
185
+ test_files: []