forked from k1LoW/deck
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconvert.go
More file actions
306 lines (270 loc) · 8.44 KB
/
Copy pathconvert.go
File metadata and controls
306 lines (270 loc) · 8.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// convert.go contains the processing to convert Google Slides data structures into internal deck data structures.
package deck
import (
"regexp"
"strings"
"google.golang.org/api/slides/v1"
)
func convertToSlide(p *slides.Page, layoutObjectIdMap map[string]*slides.Page) *Slide {
slide := &Slide{
Layout: "",
Freeze: false,
}
if p.SlideProperties != nil {
page, ok := layoutObjectIdMap[p.SlideProperties.LayoutObjectId]
if ok {
slide.Layout = page.LayoutProperties.DisplayName
}
}
var titles []string
var subtitles []string
var bodies []*Body
var images []*Image
var blockQuotes []*BlockQuote
var tables []*Table
// Extract titles, subtitles, and bodies from page elements
for _, element := range p.PageElements {
switch {
case element.Shape != nil && element.Shape.Text != nil && element.Shape.Placeholder != nil:
switch element.Shape.Placeholder.Type {
case "CENTERED_TITLE", "TITLE":
text := extractText(element.Shape.Text)
if text != "" {
titles = append(titles, text)
}
case "SUBTITLE":
text := extractText(element.Shape.Text)
if text != "" {
subtitles = append(subtitles, text)
}
case "BODY":
paragraphs := convertToParagraphs(element.Shape.Text)
if len(paragraphs) > 0 {
bodies = append(bodies, &Body{
Paragraphs: paragraphs,
})
}
}
case element.Image != nil:
var (
image *Image
err error
)
if element.Description == descriptionImageFromMarkdown {
image, err = NewImageFromMarkdown(element.Image.ContentUrl)
if err != nil {
continue // Skip if image cannot be created
}
} else {
image, err = NewImage(element.Image.ContentUrl)
if err != nil {
continue // Skip if image cannot be created
}
}
if element.Image.ImageProperties != nil && element.Image.ImageProperties.Link != nil {
image.link = element.Image.ImageProperties.Link.Url
}
images = append(images, image)
case element.Shape != nil && element.Shape.ShapeType == "TEXT_BOX" && element.Shape.Text != nil:
if element.Description != descriptionTextboxFromMarkdown {
continue
}
bq := &BlockQuote{
Paragraphs: convertToParagraphs(element.Shape.Text),
}
blockQuotes = append(blockQuotes, bq)
case element.Table != nil:
// Convert Google Slides table to deck Table
table := convertSlidesToTable(element.Table)
if table != nil {
tables = append(tables, table)
}
}
}
slide.Titles = titles
slide.Subtitles = subtitles
slide.Bodies = bodies
slide.Images = images
slide.BlockQuotes = blockQuotes
slide.Tables = tables
// Extract speaker notes
if p.SlideProperties != nil && p.SlideProperties.NotesPage != nil {
for _, element := range p.SlideProperties.NotesPage.PageElements {
if element.Shape != nil && element.Shape.Text != nil && element.Shape.Placeholder != nil {
if element.Shape.Placeholder.Type == "BODY" {
slide.SpeakerNote = extractText(element.Shape.Text)
break
}
}
}
}
return slide
}
// extractText extracts plain text from Shape.Text.
func extractText(text *slides.TextContent) string {
if text == nil || len(text.TextElements) == 0 {
return ""
}
var result strings.Builder
for _, element := range text.TextElements {
if element.TextRun != nil {
result.WriteString(element.TextRun.Content)
}
}
str := strings.ReplaceAll(result.String(), "\v", "\n")
return strings.TrimSpace(str)
}
// Regexp to match numberd (ordered) bullet points. (e.g., "1.", "2.", "A.", "B.", "a.", "b.", "i.", "ii.", "iii.").
var numberedBulletReg = regexp.MustCompile(`^(?:[0-9]+|[a-zA-Z]|i+)\.$`)
// convertToParagraphs converts TextContent to a slice of Paragraphs.
func convertToParagraphs(text *slides.TextContent) []*Paragraph {
if text == nil || len(text.TextElements) == 0 {
return nil
}
var paragraphs []*Paragraph
var currentParagraph *Paragraph
var currentBullet Bullet
for _, element := range text.TextElements {
switch {
case element.ParagraphMarker != nil:
// Start of a new paragraph
if currentParagraph != nil && len(currentParagraph.Fragments) > 0 {
paragraphs = append(paragraphs, currentParagraph)
}
currentParagraph = &Paragraph{
Fragments: []*Fragment{},
Nesting: 0,
}
// Process bullet points
if element.ParagraphMarker.Bullet != nil {
// Determine the type of bullet points based on glyph content
if element.ParagraphMarker.Bullet.Glyph != "" {
glyph := element.ParagraphMarker.Bullet.Glyph
if numberedBulletReg.MatchString(glyph) {
currentBullet = BulletNumbered
} else {
currentBullet = BulletDash
}
} else {
// If no glyph, assume it's a dash bullet
currentBullet = BulletDash
}
currentParagraph.Bullet = currentBullet
// Set nesting level
currentParagraph.Nesting = int(element.ParagraphMarker.Bullet.NestingLevel)
} else {
currentBullet = BulletNone
currentParagraph.Bullet = currentBullet
}
case element.TextRun != nil:
if currentParagraph == nil {
continue
}
if frag := convertTextRunToFragment(element.TextRun); frag != nil {
currentParagraph.Fragments = append(currentParagraph.Fragments, frag)
}
case element.AutoText != nil:
// Only one of ParagraphMarker, TextRun, or AutoText in the element's properties will be non-nil.
// Currently, nothing happens with AutoText, but we will prepare a branch just in case.
}
}
// Add the last paragraph
if currentParagraph != nil && len(currentParagraph.Fragments) > 0 {
paragraphs = append(paragraphs, currentParagraph)
}
return paragraphs
}
func convertTextRunToFragment(textRun *slides.TextRun) *Fragment {
// Get styles from TextRun
var bold, italic, code bool
var link string
if textRun.Style != nil {
bold = textRun.Style.Bold
italic = textRun.Style.Italic
if textRun.Style.Link != nil && textRun.Style.Link.Url != "" {
link = textRun.Style.Link.Url
}
// Detect code style (based on font family and background color)
if textRun.Style.FontFamily == defaultCodeFontFamily ||
(textRun.Style.BackgroundColor != nil &&
textRun.Style.BackgroundColor.OpaqueColor != nil &&
textRun.Style.BackgroundColor.OpaqueColor.RgbColor != nil) {
code = true
}
}
content := textRun.Content
// When checking the API response, a newline is always added to the end of the value of the
// TextRun element before the modified paragraph, but since it is not necessary for the
// information structure, we will delete it.
content = strings.TrimSuffix(content, "\n")
// When checking the API response, inline line breaks seem to be converted as vertical tabs,
// so we will normalize them to line breaks.
content = strings.ReplaceAll(content, "\v", "\n")
if content == "" {
return nil
}
return &Fragment{
Value: content,
Bold: bold,
Italic: italic,
Code: code,
Link: link,
}
}
// convertSlidesToTable converts a Google Slides table to deck Table structure.
func convertSlidesToTable(slidesTable *slides.Table) *Table {
if slidesTable == nil || len(slidesTable.TableRows) == 0 {
return nil
}
table := &Table{
Rows: make([]*TableRow, len(slidesTable.TableRows)),
}
for i, slidesRow := range slidesTable.TableRows {
if slidesRow == nil {
continue
}
row := &TableRow{
Cells: make([]*TableCell, len(slidesRow.TableCells)),
}
for j, slidesCell := range slidesRow.TableCells {
if slidesCell == nil {
continue
}
row.Cells[j] = &TableCell{
Fragments: extractFragmentsFromTableCell(slidesCell),
Alignment: extractAlignmentFromTableCell(slidesCell),
IsHeader: i == 0,
}
}
table.Rows[i] = row
}
return table
}
// extractFragmentsFromTableCell extracts text fragments from a table cell.
func extractFragmentsFromTableCell(cell *slides.TableCell) []*Fragment {
if cell.Text == nil || len(cell.Text.TextElements) == 0 {
return nil
}
var fragments []*Fragment
for _, element := range cell.Text.TextElements {
if element.TextRun != nil {
if frag := convertTextRunToFragment(element.TextRun); frag != nil {
fragments = append(fragments, frag)
}
}
}
return fragments
}
// extractAlignmentFromTableCell extracts text alignment from a table cell.
func extractAlignmentFromTableCell(cell *slides.TableCell) string {
if cell.Text == nil || len(cell.Text.TextElements) == 0 {
return ""
}
// Look for paragraph style in the first text element
for _, element := range cell.Text.TextElements {
if element.ParagraphMarker != nil && element.ParagraphMarker.Style != nil {
return element.ParagraphMarker.Style.Alignment
}
}
return ""
}