`
51 | ..... ..... ..... Triple Backticks (```)
52 | ..... ..... ..... ..... Special Processing Of Code Within Triple Backticks
53 | ..... ..... ..... ..... ..... GraphViz
54 | ..... ..... ..... Indented Text
55 | ..... ..... ..... ``
56 | ..... ..... Funnels
57 | ..... ..... Task List Slides
58 | ..... ..... Slides With More Than One Content Block
59 | ..... ..... Adding Slide Notes
60 | ..... Slides Without Titles
61 | ..... ..... Using A Horizontal Rule
62 | ..... ..... Using A Level 3 Heading With ` `
63 | ..... Hyperlinks And VBA Macros
64 | ..... ..... Coding A URL Reference
65 | ..... ..... Coding A Heading Reference On A Target Slide
66 | ..... ..... Coding A Hyperlink To Another Slide
67 | ..... ..... Invoking A VBA Macro
68 | ..... ..... ..... Sample Macro To Remove The First Slide
69 | ..... ..... ..... Sample Macro To Remove The First Slide And Save As A .pptx File
70 | ..... HTML Comments
71 | ..... Special Text Formatting
72 | CSV Start
73 | CSV Stop
74 | ..... ..... Using HTML `` Elements To Specify Text Effects
75 | ..... ..... ..... Using HTML `` Elements with `class`
76 | ..... ..... ..... Using HTML `` Elements with `style`
77 | ..... ..... HTML Entity References
78 | Column Alignment - spec 'l c l c l c l c '
79 | CSV Start
80 | CSV Stop
81 | ..... ..... Numeric Character References
82 | ..... ..... Escaped Characters
83 | ..... ..... CriticMarkup
84 | ..... Creating A Glossary Of Terms
85 | ..... Creating Footnotes
86 | ..... ..... Creating A Footnote
87 | ..... ..... Referring To A Footnote
88 | ..... Controlling The Presentation With Metadata
89 | ..... ..... Specifying Metadata
90 | ..... ..... ..... Processing Summary
91 | ..... ..... ..... Specifying Colours
92 | ..... ..... ..... ..... Theme Colours
93 | ..... ..... ..... ..... RGB Colours
94 | ..... ..... Metadata Keys
95 | ..... ..... ..... Title And Subtitle Font Sizes And Alignment
96 | ..... ..... ..... ..... Page Title Size - `pageTitleSize`
97 | ..... ..... ..... ..... Page Subtitle Size - `pageSubtitleSize`
98 | ..... ..... ..... ..... Section Title Size - `sectionTitleSize`
99 | ..... ..... ..... ..... Section Subtitle Size - `sectionSubtitleSize`
100 | ..... ..... ..... ..... Presentation Title Size - `presTitleSize`
101 | ..... ..... ..... ..... Presentation Subtitle Size - `presSubtitleSize`
102 | ..... ..... ..... ..... Page Title Alignment `pagetitlealign`
103 | ..... ..... ..... Monospace Font - `monoFont`
104 | ..... ..... ..... Margin size - `marginBase` and `tableMargin`
105 | ..... ..... ..... Controlling Adjusting Title Positions And Sizes - `AdjustTitles`
106 | ..... ..... ..... Associating A Class Name with A Background Colour With `style.bgcolor`
107 | ..... ..... ..... Associating A Class Name with A Foreground Colour With `style.fgcolor`
108 | ..... ..... ..... Associating A Class Name With Text Emphasis With `style.emphasis`
109 | ..... ..... ..... Associating A Class Name With Font Size with `style.fontsize`
110 | ..... ..... ..... Template Presentation - `template`
111 | ..... ..... ..... Hiding Slides - `hidden`
112 | ..... ..... ..... Specifying An Abstract Slide With `abstractTitle`
113 | ..... ..... ..... Specifying Text Size With `baseTextSize` And `baseTextDecrement`
114 | ..... ..... ..... Specifying Bold And Italic Text Colour With `BoldColour` And `ItalicColour`
115 | ..... ..... ..... Specifying Bold And Italic Text Effects With `BoldBold` And `ItalicItalic`
116 | ..... ..... ..... Controlling Task Slide Production With `taskSlides` and `tasksPerSlide`
117 | ..... ..... ..... Controlling Glossary Slide Production With `glossaryTitle`, `glossaryTerm`, `glossaryMeaning`,`glossaryMeaningWidth`, and `glossaryTermsPerPage`
118 | ..... ..... ..... Specifying How Many Spaces Represent An Indentation Level With `IndentSpaces`
119 | ..... ..... ..... Specifying Where Temporary Files Are Stored With `tempDir`
120 | ..... ..... ..... Deleting The First (Processing Summary) Slide - with `DeleteFirstSlide`
121 | ..... ..... ..... Specifying Slide Background Images With `backgroundImage`
122 | ..... ..... ..... Table Metadata
123 | ..... ..... ..... ..... Shrinking Tables With `compactTables`
124 | ..... ..... ..... ..... Adjusting Table Heading Font Size With `tableHeadingSize`
125 | ..... ..... ..... ..... Adding Lines Round Tables And Cells With `addTableLines`
126 | ..... ..... ..... ..... Adding Lines After Table Rows And Columns With `addTableRowLines` And `addTableColumnLines`
127 | ..... ..... ..... ..... Specifying What The Added Table Lines Look Like With `addTableLineColour`, `addTableLineCount` and `addTableLineWidth`
128 | ..... ..... ..... ..... Controlling Whether Empty Table Cells Cause Column Spanning - `SpanCells`
129 | ..... ..... ..... ..... Controlling Whether Tables Have Drop Shadows - `tableShadow`
130 | ..... ..... ..... Card Metadata
131 | ..... ..... ..... ..... Card Background Colour - `CardColour`
132 | ..... ..... ..... ..... Card Border Colour - `CardBorderColour`
133 | ..... ..... ..... ..... Card Border Width - `CardBorderWidth`
134 | ..... ..... ..... ..... Card Title Size - `CardTitleSize`
135 | ..... ..... ..... ..... Card Title Colour - `cardTitleColour`
136 | ..... ..... ..... ..... Card Title Background Colours - `CardTitleBackground`
137 | ..... ..... ..... ..... Card Divider Colour - `cardDividerColour`
138 | ..... ..... ..... ..... Card Shadow - `CardShadow`
139 | ..... ..... ..... ..... Card Size - `CardPercent`
140 | ..... ..... ..... ..... Card Layout Direction - `CardLayout`
141 | ..... ..... ..... ..... Card Title Alignment - `CardTitleAlign`
142 | ..... ..... ..... ..... Card Title Position - `CardTitlePosition`
143 | ..... ..... ..... ..... Card Shape - `CardShape`
144 | ..... ..... ..... ..... Card Horizontal Gap - `CardHorizontalGap`
145 | ..... ..... ..... ..... Card Vertical Gap - `CardVerticalGap`
146 | ..... ..... ..... ..... Card Graphic Position - `CardGraphicPosition`
147 | ..... ..... ..... ..... Card Graphic Size - `CardGraphicSize`
148 | ..... ..... ..... ..... Card Graphic Padding - `CardGraphicPadding`
149 | ..... ..... ..... Code Metadata
150 | ..... ..... ..... ..... Code Column Count - `CodeColumns`
151 | ..... ..... ..... ..... Fixed Pitch Height To Width Ratio - `FPRatio`
152 | ..... ..... ..... ..... Foreground Colour - `CodeForeground`
153 | ..... ..... ..... ..... Background Colour - `CodeBackground`
154 | ..... ..... ..... Funnel Metadata
155 | ..... ..... ..... ..... Funnel Fill Colours - `funnelColours`
156 | ..... ..... ..... ..... Funnel Border Colour - `funnelBorderColour`
157 | ..... ..... ..... ..... Funnel Title Colour - `funnelTitleColour`
158 | ..... ..... ..... ..... Funnel Text Colour - `funnelTextColour`
159 | ..... ..... ..... ..... Funnel Labels Space - `funnelLabelsPercent`
160 | ..... ..... ..... ..... Funnel Labels Position - `funnelLabelsPosition`
161 | ..... ..... ..... ..... Funnel Orientation - `funnelWidest`
162 | ..... ..... ..... Footer And Slide Number Metadata
163 | ..... ..... ..... ..... Slide Numbers - `numbers`
164 | ..... ..... ..... ..... Specifying Slide Number Font Size With `numbersFontSize`
165 | ..... ..... ..... ..... Specifying How Much Space To Reserve For Slide Numbers With `NumbersHeight`
166 | ..... ..... ..... ..... Specifying Footer Text
167 | ..... ..... ..... ..... ..... Footer Flexibility
168 | ..... ..... ..... ..... Specifying Footer Font Size With `footerFontSize`
169 | ..... ..... ..... Slide Heading Levels - `TopHeadingLevel`
170 | Column Alignment - spec 'r l'
171 | CSV Start
172 | CSV Stop
173 | Column Alignment - spec 'r l'
174 | CSV Start
175 | CSV Stop
176 | ..... ..... ..... Slides With Multiple Content Blocks
177 | ..... ..... ..... ..... Horizontal Or Vertical Split - `ContentSplitDirection`
178 | ..... ..... ..... ..... Split Proportions - `ContentSplit`
179 | ..... ..... ..... Graphics Metadata
180 | ..... ..... ..... ..... Exporting Converted SVG And PNG Files - `exportGraphics`
181 | ..... ..... ..... Table Of Contents And Section Slide Metadata
182 | ..... ..... ..... ..... "Chevron Style" Table Of Contents
183 | ..... ..... ..... ..... "Circle Style" Table Of Contents
184 | ..... ..... ..... ..... "Plain Style" Table Of Contents
185 | ..... ..... ..... ..... Table Of Contents Style - `tocStyle`
186 | ..... ..... ..... ..... Table Of Contents Title - `tocTitle`
187 | ..... ..... ..... ..... Table Of Contents Live Links - `tocLinks`
188 | ..... ..... ..... ..... Table Of Contents Item Height - `TOCItemHeight`
189 | ..... ..... ..... ..... Table Of Contents Item Colour - `TOCItemColour`
190 | ..... ..... ..... ..... Table Of Contents Row Gap - `TOCRowGap`
191 | ..... ..... ..... ..... Table Of Contents Font Size - `TOCFontSize`
192 | ..... ..... ..... ..... Section Navigation Buttons - `SectionArrows`
193 | ..... ..... ..... ..... Section Navigation Button Colour - `SectionArrowsColour`
194 | ..... ..... ..... ..... Make Expandable Sections - `SectionsExpand`
195 | ..... ..... ..... Slide Transitions - `Transition`
196 | ..... ..... ..... Python Exit Routines
197 | ..... ..... ..... ..... After Loading - `onPresentationInitialisation`
198 | ..... ..... ..... ..... Before Saving - `onPresentationBeforeSave`
199 | ..... ..... ..... ..... After Saving - `onPresentationAfterSave`
200 | ..... ..... Dynamic Metadata
201 | ..... ..... ..... `hidden`
202 | ..... ..... ..... Tables
203 | ..... ..... ..... ..... `CompactTables`
204 | ..... ..... ..... ..... `TableHeadingSize`
205 | ..... ..... ..... ..... `addTableLines`
206 | ..... ..... ..... ..... `addTableColumnLines` And `addTableRowLines`
207 | ..... ..... ..... ..... Added Table Line Attributes
208 | ..... ..... ..... ..... `SpanCells`
209 | ..... ..... ..... Cards
210 | ..... ..... ..... ..... `CardPercent`
211 | ..... ..... ..... ..... `CardLayout`
212 | ..... ..... ..... ..... `CardColour`
213 | ..... ..... ..... ..... `CardTitleAlign`
214 | ..... ..... ..... ..... `CardTitlePosition`
215 | ..... ..... ..... ..... `CardTitleBackground`
216 | ..... ..... ..... ..... `CardShape`
217 | ..... ..... ..... ..... `CardHorizontalGap`
218 | ..... ..... ..... ..... `CardVerticalGap`
219 | ..... ..... ..... Code
220 | ..... ..... ..... ..... `CodeColumns`
221 | ..... ..... ..... ..... `FPRatio`
222 | ..... ..... ..... ..... `CodeForeground`
223 | ..... ..... ..... ..... `CodeBackground`
224 | ..... ..... ..... Funnel
225 | ..... ..... ..... ..... `FunnelColours`
226 | ..... ..... ..... ..... `FunnelBorderColour`
227 | ..... ..... ..... ..... `FunnelTitleColour`
228 | ..... ..... ..... ..... `FunnelTextColour`
229 | ..... ..... ..... ..... `FunnelLabelsPercent`
230 | ..... ..... ..... ..... `FunnelLabelsPosition`
231 | ..... ..... ..... ..... `FunnelWidest`
232 | ..... ..... ..... `PageTitleSize`
233 | ..... ..... ..... `PageSubtitleSize`
234 | ..... ..... ..... `BaseTextSize`
235 | ..... ..... ..... `BaseTextDecrement`
236 | ..... ..... ..... `ContentSplitDirection`
237 | ..... ..... ..... `ContentSplit`
238 | ..... ..... ..... `IndentSpaces`
239 | ..... ..... ..... `MarginBase`
240 | ..... ..... ..... `NumbersHeight`
241 | ..... ..... ..... `TableMargin`
242 | ..... ..... ..... `Transition`
243 | ..... ..... ..... `BackgroundImage`
244 | ..... Modifying The Slide Template
245 | ..... ..... Basics
246 | ..... ..... Slide Template Sequence
247 | Column Alignment - spec 'l l l l'
248 | CSV Start
249 | CSV Stop
250 | ..... ..... Template Slide Types
251 | ..... ..... ..... Title Slide - `TitleSlideLayout`
252 | ..... ..... ..... Section Slide - `SectionSlideLayout`
253 | ..... ..... ..... Title Only Slide - `TitleOnlyLayout`
254 | ..... ..... ..... Blank Slide - `BlankLayout`
255 | ..... ..... ..... Content Slide - `ContentSlideLayout`
256 | ..... Deviations From Standard Markdown
257 | ..... Running Inline Python
258 | ..... ..... An Important Caution
259 | ..... ..... How To Invoke Python In md2pptx
260 | ..... ..... ..... Coding Inline Python
261 | ..... ..... ..... Importing Python From A File
262 | ..... ..... Variables You Can Rely On
263 | ..... ..... Python Helper Routines
264 | ..... ..... ..... General Helper Routines
265 | ..... ..... ..... ..... RunPython.readCSV
266 | ..... ..... ..... ..... ..... Input
267 | ..... ..... ..... ..... ..... Output
268 | ..... ..... ..... ..... RunPython.filterRows
269 | ..... ..... ..... ..... ..... Input
270 | ..... ..... ..... ..... ..... Output
271 | ..... ..... ..... ..... RunPython.transposeArray
272 | ..... ..... ..... ..... ..... Input
273 | ..... ..... ..... ..... ..... Output
274 | ..... ..... ..... ..... RunPython.ensureTextbox
275 | ..... ..... ..... ..... ..... Input
276 | ..... ..... ..... ..... ..... Output
277 | ..... ..... ..... ..... RunPython.runFromFile
278 | ..... ..... ..... ..... ..... Input
279 | ..... ..... ..... ..... ..... Output
280 | ..... ..... ..... Chart-Related Helper Routines
281 | ..... ..... ..... ..... RunPython.makeChartData
282 | ..... ..... ..... ..... ..... Input
283 | ..... ..... ..... ..... ..... Output
284 | ..... ..... ..... ..... RunPython.makeChart
285 | ..... ..... ..... ..... ..... Input
286 | ..... ..... ..... ..... ..... Output
287 | ..... ..... ..... Table-Related Helper Routines
288 | ..... ..... ..... ..... RunPython.makeTable
289 | ..... ..... ..... ..... ..... Input
290 | ..... ..... ..... ..... ..... Output
291 | ..... ..... ..... ..... RunPython.applyCellFillRGB
292 | ..... ..... ..... ..... ..... Input
293 | ..... ..... ..... ..... ..... Output
294 | ..... ..... ..... ..... RunPython.applyCellListFillRGB
295 | ..... ..... ..... ..... ..... Input
296 | ..... ..... ..... ..... ..... Output
297 | ..... ..... ..... ..... RunPython.alignTableCellText
298 | ..... ..... ..... ..... ..... Input
299 | ..... ..... ..... ..... ..... Output
300 | ..... ..... ..... Drawing-Related Helper Routines
301 | ..... ..... ..... ..... RunPython.makeDrawnShape
302 | ..... ..... ..... ..... ..... Input
303 | ..... ..... ..... ..... ..... Output
304 | ..... ..... ..... Checklist-Related Helper routines
305 | ..... ..... ..... ..... RunPython.makeTruthy
306 | ..... ..... ..... ..... ..... Input
307 | ..... ..... ..... ..... ..... Output
308 | ..... ..... ..... ..... RunPython.checklistFromCSV
309 | ..... ..... ..... ..... ..... Input
310 | ..... ..... ..... ..... ..... Output
311 | ..... ..... ..... ..... RunPython.makeChecklist
312 | ..... ..... ..... ..... ..... Input
313 | ..... ..... ..... ..... ..... Output
314 | ..... ..... ..... ..... RunPython.doChecklistChecks
315 | ..... ..... ..... ..... ..... Input
316 | ..... ..... ..... ..... ..... Output
317 | ..... ..... ..... Text Paragraph Helper Routines
318 | ..... ..... ..... ..... RunPython.removeBullet
319 | ..... ..... ..... ..... ..... Input
320 | ..... ..... ..... ..... ..... Output
321 | ..... ..... ..... ..... RunPython.removeBullets
322 | ..... ..... ..... ..... ..... Input
323 | ..... ..... ..... ..... ..... Output
324 | ..... ..... ..... ..... RunPython.removeSelectedBullets
325 | ..... ..... ..... ..... ..... Input
326 | ..... ..... ..... ..... ..... Output
327 | ..... ..... ..... Annotations-Related Helper routines
328 | ..... ..... ..... ..... RunPython.doAnnotations
329 | ..... ..... ..... ..... ..... Input
330 | ..... ..... ..... ..... ..... Output
331 | ..... ..... ..... ..... RunPython.annotationsFromCSV
332 | ..... ..... ..... ..... ..... Input
333 | ..... ..... ..... ..... ..... Output
334 | ..... ..... Inline Python Examples
335 | ..... ..... ..... Graphing Example
336 | ..... ..... ..... Table Manipulation Example
337 | ..... ..... ..... Slide With A Shape Example
338 | ..... ..... ..... Slide With A Checklist Examples
339 | ..... ..... ..... Slide With Some Bullets Removed Example
340 | ..... ..... ..... Annotations Example
341 | ..... Building This User Guide
342 | -------------------------------------------------------
343 | - Processing completed.
344 | -------------------------------------------------------
345 |
346 |
--------------------------------------------------------------------------------
/docs/vertical-split-1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/vertical-split-1-1.png
--------------------------------------------------------------------------------
/funnel.py:
--------------------------------------------------------------------------------
1 | """
2 | funnel
3 | """
4 |
5 | version = "0.1"
6 |
7 |
8 | import csv
9 | import io
10 | from rectangle import Rectangle
11 | from pptx.util import Inches, Pt
12 | from pptx.enum.text import PP_ALIGN
13 | from pptx.dml.color import RGBColor, MSO_THEME_COLOR
14 | from colour import setColour
15 | from symbols import resolveSymbols
16 |
17 |
18 | def massageFunnelText(text):
19 | fragment = ""
20 | for c in text:
21 | if ord(c) == 236:
22 | fragment = fragment + "<"
23 |
24 | elif ord(c) == 237:
25 | fragment = fragment + ">"
26 |
27 | else:
28 | fragment = fragment + c
29 |
30 | return fragment
31 |
32 |
33 | class Funnel:
34 | def __init__(
35 | self,
36 | ):
37 | pass
38 |
39 | def makeFunnel(
40 | self,
41 | slide,
42 | renderingRectangle,
43 | funnelParts,
44 | partColours,
45 | codeType,
46 | funnelBorderColour,
47 | funnelTitleColour,
48 | funnelTextColour,
49 | funnelLabelsPercent,
50 | funnelLabelPosition,
51 | funnelWidest,
52 | ):
53 | if funnelWidest in ["left", "right", "pipe", "hpipe"]:
54 | direction = "horizontal"
55 | else:
56 | direction = "vertical"
57 |
58 | # Turn label percentage into decimal number to multiply by
59 | funnelLabelsProportion = funnelLabelsPercent / 100
60 |
61 | # Proportion of the stage that the tip - narrowest / shortest
62 | # part is relative to widest / tallest
63 | tipProportion = 1 / 3
64 |
65 | # Define labels rectangle then funnel body rectangle
66 | if direction == "horizontal":
67 | if funnelLabelPosition == "before":
68 | # Labels above stages
69 | funnelLabelsRectangle = Rectangle(
70 | renderingRectangle.top,
71 | renderingRectangle.left,
72 | int(renderingRectangle.height * funnelLabelsProportion),
73 | renderingRectangle.width,
74 | )
75 |
76 | funnelBodyRectangle = Rectangle(
77 | renderingRectangle.top
78 | + int(renderingRectangle.height * funnelLabelsProportion),
79 | renderingRectangle.left,
80 | int(renderingRectangle.height * (1 - funnelLabelsProportion)),
81 | renderingRectangle.width,
82 | )
83 | else:
84 | # Labels below stages
85 | funnelLabelsRectangle = Rectangle(
86 | renderingRectangle.top
87 | + renderingRectangle.height * (1 - funnelLabelsProportion),
88 | renderingRectangle.left,
89 | int(renderingRectangle.height * funnelLabelsProportion),
90 | renderingRectangle.width,
91 | )
92 |
93 | funnelBodyRectangle = Rectangle(
94 | renderingRectangle.top,
95 | renderingRectangle.left,
96 | int(renderingRectangle.height * (1 - funnelLabelsProportion)),
97 | renderingRectangle.width,
98 | )
99 | else:
100 | if funnelLabelPosition == "before":
101 | # Labels left of stages
102 | funnelLabelsRectangle = Rectangle(
103 | renderingRectangle.top,
104 | renderingRectangle.left,
105 | renderingRectangle.height,
106 | int(renderingRectangle.width * funnelLabelsProportion),
107 | )
108 |
109 | funnelBodyRectangle = Rectangle(
110 | renderingRectangle.top,
111 | renderingRectangle.left
112 | + int(renderingRectangle.width * funnelLabelsProportion),
113 | renderingRectangle.height,
114 | int(renderingRectangle.width * (1 - funnelLabelsProportion)),
115 | )
116 | else:
117 | # Labels right of stages
118 | funnelLabelsRectangle = Rectangle(
119 | renderingRectangle.top,
120 | renderingRectangle.left
121 | + renderingRectangle.width * (1 - funnelLabelsProportion),
122 | renderingRectangle.height,
123 | int(renderingRectangle.width * funnelLabelsProportion),
124 | )
125 |
126 | funnelBodyRectangle = Rectangle(
127 | renderingRectangle.top,
128 | renderingRectangle.left,
129 | renderingRectangle.height,
130 | int(renderingRectangle.width * (1 - funnelLabelsProportion)),
131 | )
132 |
133 | if direction == "horizontal":
134 | # How high the narrowest part of the funnel / pipe is
135 | tipHeight = funnelBodyRectangle.height * tipProportion
136 | else:
137 | # How wide the narrowest part of the funnel / pipe is
138 | tipWidth = funnelBodyRectangle.width * tipProportion
139 |
140 | partColourCount = len(partColours)
141 |
142 | # Get the underlying rows. Only first 2 cells of each row used
143 | funnelPartRows = [
144 | r
145 | for r in csv.reader(
146 | io.StringIO(str.join("\n", funnelParts)),
147 | escapechar="\\",
148 | skipinitialspace=True,
149 | )
150 | ]
151 |
152 | funnelPartCount = len(funnelPartRows)
153 |
154 | # Build lists of labels and Body
155 | funnelLabels = []
156 | funnelBody = []
157 |
158 | for row in funnelPartRows:
159 | cell1 = row[0].strip()
160 | if len(row) == 0:
161 | funnelLabels.append("")
162 | funnelBody.append("")
163 | else:
164 | funnelLabels.append(cell1)
165 | if len(row) == 1:
166 | funnelBody.append("")
167 | else:
168 | cell2 = row[1].strip()
169 | funnelBody.append(cell2)
170 |
171 | if direction == "horizontal":
172 | partWidth = renderingRectangle.width / funnelPartCount
173 | else:
174 | partHeight = renderingRectangle.height / funnelPartCount
175 |
176 | # Create the labels
177 | for l, label in enumerate(funnelLabels):
178 | if direction == "horizontal":
179 | tb = slide.shapes.add_textbox(
180 | funnelLabelsRectangle.left + l * partWidth,
181 | funnelLabelsRectangle.top,
182 | partWidth,
183 | funnelLabelsRectangle.height,
184 | )
185 | else:
186 | tb = slide.shapes.add_textbox(
187 | funnelLabelsRectangle.left,
188 | funnelLabelsRectangle.top + l * partHeight,
189 | funnelLabelsRectangle.width,
190 | partHeight,
191 | )
192 |
193 | tb.text = massageFunnelText(resolveSymbols(label.replace("
", "\n")))
194 | for p in tb.text_frame.paragraphs:
195 | p.alignment = PP_ALIGN.CENTER
196 | if funnelTitleColour != ("None", ""):
197 | setColour(p.font.color, funnelTitleColour)
198 |
199 | # Create the parts of the funnel
200 |
201 | for b, body in enumerate(funnelBody):
202 | if direction == "horizontal":
203 | # Horizontal stages
204 |
205 | # Left extremity of stage - both top and bottom
206 | partLeft = funnelBodyRectangle.left + b * partWidth
207 |
208 | # Right extremity of stage - both top and bottom
209 | partRight = partLeft + partWidth
210 |
211 | if (
212 | ((b == funnelPartCount - 1) & (funnelWidest == "left"))
213 | | ((b == 0) & (funnelWidest == "right"))
214 | | (funnelWidest in ["pipe", "hpipe"])
215 | ):
216 | # Rectangular / pipe stage
217 |
218 | # Calculate space to be above and below rectangular stage
219 | leftSpaceAboveBelow = (funnelBodyRectangle.height - tipHeight) / 2
220 | rightSpaceAboveBelow = leftSpaceAboveBelow
221 |
222 | elif funnelWidest == "left":
223 | # Calculate space to be above and below left end of stage
224 | leftSpaceAboveBelow = (
225 | (funnelBodyRectangle.height - tipHeight)
226 | / 2
227 | * b
228 | / (funnelPartCount - 1)
229 | )
230 |
231 | # Calculate space to be above and below right end of stage
232 | rightSpaceAboveBelow = (
233 | (funnelBodyRectangle.height - tipHeight)
234 | / 2
235 | * (b + 1)
236 | / (funnelPartCount - 1)
237 | )
238 |
239 | else:
240 | # Calculate space to be above and below left end of stage
241 | leftSpaceAboveBelow = (
242 | (funnelBodyRectangle.height - tipHeight)
243 | / 2
244 | * (funnelPartCount - b)
245 | / (funnelPartCount - 1)
246 | )
247 |
248 | # Calculate space to be above and below right end of stage
249 | rightSpaceAboveBelow = (
250 | (funnelBodyRectangle.height - tipHeight)
251 | / 2
252 | * (funnelPartCount - b - 1)
253 | / (funnelPartCount - 1)
254 | )
255 |
256 | # Calculate y coordinate of top left corner
257 | partTopLeft = funnelBodyRectangle.top + leftSpaceAboveBelow
258 |
259 | # Calculate y coordinate of top right corner
260 | partTopRight = funnelBodyRectangle.top + rightSpaceAboveBelow
261 |
262 | # Calculate y coordinate of bottom left corner
263 | partBottomLeft = (
264 | funnelBodyRectangle.top
265 | + funnelBodyRectangle.height
266 | - leftSpaceAboveBelow
267 | )
268 |
269 | # Calculate y coordinate of bottom right corner
270 | partBottomRight = (
271 | funnelBodyRectangle.top
272 | + funnelBodyRectangle.height
273 | - rightSpaceAboveBelow
274 | )
275 |
276 | stagePoints = [
277 | (partLeft, partTopLeft),
278 | (partLeft, partBottomLeft),
279 | (partRight, partBottomRight),
280 | (partRight, partTopRight),
281 | ]
282 | else:
283 | # Vertical stages
284 |
285 | # Top extremity of stage - both left and right
286 | partTop = funnelBodyRectangle.top + b * partHeight
287 |
288 | # Bottom extremity of stage - both left and right
289 | partBottom = partTop + partHeight
290 |
291 | if (
292 | ((b == funnelPartCount - 1) & (funnelWidest == "top"))
293 | | ((b == 0) & (funnelWidest == "bottom"))
294 | | (funnelWidest == "vpipe")
295 | ):
296 | # Rectangular / pipe stage
297 |
298 | # Calculate space to be left either side of rectangular stage
299 | topSpaceLeftRight = (funnelBodyRectangle.width - tipWidth) / 2
300 | bottomSpaceLeftRight = topSpaceLeftRight
301 |
302 | elif funnelWidest == "top":
303 | # Calculate space to be left at left and right top end of stage
304 | topSpaceLeftRight = (
305 | (funnelBodyRectangle.width - tipWidth)
306 | / 2
307 | * b
308 | / (funnelPartCount - 1)
309 | )
310 |
311 | # Calculate space to be left at left and right bottom end of stage
312 | bottomSpaceLeftRight = (
313 | (funnelBodyRectangle.width - tipWidth)
314 | / 2
315 | * (b + 1)
316 | / (funnelPartCount - 1)
317 | )
318 |
319 | else:
320 | # Calculate space to be left at left and right top end of stage
321 | topSpaceLeftRight = (
322 | (funnelBodyRectangle.width - tipWidth)
323 | / 2
324 | * (funnelPartCount - b)
325 | / (funnelPartCount - 1)
326 | )
327 |
328 | # Calculate space to be left at left and right bottom end of stage
329 | bottomSpaceLeftRight = (
330 | (funnelBodyRectangle.width - tipWidth)
331 | / 2
332 | * (funnelPartCount - b - 1)
333 | / (funnelPartCount - 1)
334 | )
335 |
336 | # Calculate x coordinate of top left corner
337 | partTopLeft = funnelBodyRectangle.left + topSpaceLeftRight
338 |
339 | # Calculate x coordinate of bottom left corner
340 | partBottomLeft = funnelBodyRectangle.left + bottomSpaceLeftRight
341 |
342 | # Calculate x coordinate of top right corner
343 | partTopRight = (
344 | funnelBodyRectangle.left
345 | + funnelBodyRectangle.width
346 | - topSpaceLeftRight
347 | )
348 |
349 | # Calculate x coordinate of bottom right corner
350 | partBottomRight = (
351 | funnelBodyRectangle.left
352 | + funnelBodyRectangle.width
353 | - bottomSpaceLeftRight
354 | )
355 |
356 | stagePoints = [
357 | (partTopLeft, partTop),
358 | (partTopRight, partTop),
359 | (partBottomRight, partBottom),
360 | (partBottomLeft, partBottom),
361 | ]
362 |
363 | # Start shape builder with first point
364 | ffBuilder = slide.shapes.build_freeform(*stagePoints[0])
365 |
366 | ffBuilder.add_line_segments(stagePoints[1:],
367 | close=True,
368 | )
369 |
370 | s = ffBuilder.convert_to_shape()
371 | s.text = massageFunnelText(resolveSymbols(body.replace("
", "\n")))
372 |
373 | for p in s.text_frame.paragraphs:
374 | p.alignment = PP_ALIGN.CENTER
375 | if funnelTextColour != ("None", ""):
376 | setColour(p.font.color, funnelTextColour)
377 |
378 | s.fill.solid()
379 |
380 | partColourType, partColourValue = partColours[b % partColourCount]
381 | if partColourType == "Theme":
382 | s.fill.fore_color.theme_color = partColourValue
383 | else:
384 | s.fill.fore_color.rgb = RGBColor.from_string(partColourValue[1:])
385 |
386 | if funnelBorderColour != ("None", ""):
387 | setColour(s.line.color, funnelBorderColour)
388 |
--------------------------------------------------------------------------------
/globals.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | globals.py
4 |
5 | """
6 |
7 | import re
8 |
9 | from processingOptions import ProcessingOptions
10 |
11 | global processingOptions
12 | processingOptions = ProcessingOptions()
13 |
14 | global spanClassRegex
15 | spanClassRegex = re.compile("" strings with newline single character
81 | text2 = text2.replace("
", "\n")
82 |
83 | # Replace any escaped asterisk strings with entity reference
84 | text2 = text2.replace("\\*", "∗")
85 |
86 | # Replace any asterisks with spaces either side with entity reference
87 | text2 = text2.replace(" * ", " ∗ ")
88 | if text2[-2:] == " *":
89 | text2 = text2[:-2] + " ∗"
90 |
91 | # Replace any footnote reference starts with char "\uFDD0"
92 | text2 = text2.replace("[^", u"\uFDD0")
93 |
94 | # Replace any span style starts with char "\uFDD1"
95 | text2 = re.sub(globals.spanStyleRegex, u"\uFDD1", text2)
96 |
97 | # Replace any span class starts with char "\uFDD2"
98 | text2 = re.sub(globals.spanClassRegex, u"\uFDD2", text2)
99 |
100 | # Replace any span ends with char "\uFDD3"
101 | text2 = text2.replace("", u"\uFDD3")
102 |
103 | # Replace any abbreviation starts with char "\uFDD4"
104 | text2 = text2.replace("", u"\uFDD5")
108 |
109 | # Replace any \[ with char "\uFDD6"
110 | text2 = text2.replace(r"\[", u"\uFDD6")
111 |
112 | # Replace any \] with char "\uFDD7"
113 | text2 = text2.replace(r"\]", u"\uFDD7")
114 |
115 | # "\uFDD8" is link separator special character. See below
116 |
117 | # Replace any {~~ with char "\uFDD8"
118 | text2 = text2.replace("{~~", u"\uFDD8")
119 |
120 | # Replace any ~~} with char "\uFDD8"
121 | text2 = text2.replace("~~}", u"\uFDD8")
122 |
123 | # Replace any {== with char "\uFDD9"
124 | # Danish character)
125 | text2 = text2.replace("{==", u"\uFDD9")
126 |
127 | # Replace any ==} with char "\uFDD9"
128 | text2 = text2.replace("==}", u"\uFDD9")
129 |
130 | # Replace any {>> with char "\uFDDA"
131 | text2 = text2.replace("{>>", u"\uFDDA")
132 |
133 | # Replace any <<} with char "\uFDDA"
134 | text2 = text2.replace("<<}", u"\uFDDA")
135 |
136 | # Replace any {-- with char "\uFDDB"
137 | text2 = text2.replace("{--", u"\uFDDB")
138 |
139 | # Replace any --} with char "\uFDDB
140 | text2 = text2.replace("--}", u"\uFDDB")
141 |
142 | # Replace any {++ with char "\uFDDC"
143 | text2 = text2.replace("{++", u"\uFDDC")
144 |
145 | # Replace any ++} with char "\uFDDC"
146 | text2 = text2.replace("++}", u"\uFDDC")
147 |
148 | # Replace any with char "\uFDDD"
149 | text2 = text2.replace("", u"\uFDDD")
150 |
151 | # Replace any with char "\uFDDD"
152 | text2 = text2.replace("", u"\uFDDD")
153 |
154 | # Replace any with char "\uFDDE"
155 | text2 = text2.replace("", u"\uFDDE")
156 |
157 | # Replace any with char "\uFDDE"
158 | text2 = text2.replace("", u"\uFDDE")
159 |
160 | # Replace any with char "\uFDDF"
161 | text2 = text2.replace("", u"\uFDDF")
162 |
163 | # Replace any with char "\uFDDF"
164 | text2 = text2.replace("", u"\uFDDF")
165 |
166 | # Replace any with char "\uFDE0"
167 | text2 = text2.replace("", u"\uFDE0")
168 |
169 | # Replace any with char "\uFDE0"
170 | text2 = text2.replace("", u"\uFDE0")
171 |
172 | # Note FDE1 - FDE3 used in resolveSymbols
173 |
174 | # Handle escaped underscore
175 | text2 = text2.replace("\\_", "_")
176 |
177 | # Unescape any numeric character references
178 | text3 = resolveSymbols(text2)
179 |
180 | for c in text3:
181 | if c == "*":
182 | # Changing state
183 | if state == "N":
184 | # First * potentially starts italic
185 | textArray.append([state, fragment])
186 | fragment = ""
187 | state = "I"
188 |
189 | elif state == "I":
190 | # Either go to bold or end italic
191 | if lastChar == "*":
192 | # Go to bold
193 | state = "B1"
194 |
195 | else:
196 | # End italic
197 | textArray.append([state, fragment])
198 | fragment = ""
199 | state = "N"
200 |
201 | elif state == "B1":
202 | # Starting to close bold bracket
203 | state = "B2"
204 |
205 | elif lastChar == "*":
206 | # closing either bold or italic bracket
207 | textArray.append([state, fragment])
208 | fragment = ""
209 | state = "N"
210 |
211 | elif c == "`":
212 | if state == "N":
213 | # Going to code
214 | textArray.append([state, fragment])
215 | fragment = ""
216 | state = "C"
217 |
218 | else:
219 | # exiting code
220 | textArray.append([state, fragment])
221 | fragment = ""
222 | state = "N"
223 |
224 | elif c == u"\uFDD8":
225 | # Entering or leaving CriticMarkup replacement
226 | if state == "N":
227 | # Going to CriticMarkup replacement
228 | textArray.append([state, fragment])
229 | fragment = ""
230 | state = "CMRep"
231 |
232 | else:
233 | # exiting CriticMarkup replacement
234 | textArray.append([state, fragment])
235 | fragment = ""
236 | state = "N"
237 |
238 | elif c == u"\uFDD9":
239 | # Entering or leaving CriticMarkup highlight
240 | if state == "N":
241 | # Going to CriticMarkup highlight
242 | textArray.append([state, fragment])
243 | fragment = ""
244 | state = "CMHig"
245 |
246 | else:
247 | # exiting CriticMarkup highlight
248 | textArray.append([state, fragment])
249 | fragment = ""
250 | state = "N"
251 |
252 | elif c == u"\uFDDA":
253 | # Entering or leaving CriticMarkup comment
254 | if state == "N":
255 | # Going to CriticMarkup comment
256 | textArray.append([state, fragment])
257 | fragment = ""
258 | state = "CMCom"
259 |
260 | else:
261 | # exiting CriticMarkup comment
262 | textArray.append([state, fragment])
263 | fragment = ""
264 | state = "N"
265 |
266 | elif c == u"\uFDDB":
267 | # Entering or leaving CriticMarkup deletion
268 | if state == "N":
269 | # Going to CriticMarkup deletion
270 | textArray.append([state, fragment])
271 | fragment = ""
272 | state = "CMDel"
273 |
274 | else:
275 | # exiting CriticMarkup deletion
276 | textArray.append([state, fragment])
277 | fragment = ""
278 | state = "N"
279 |
280 | elif c == u"\uFDDC":
281 | # Entering or leaving CriticMarkup addition
282 | if state == "N":
283 | # Going to CriticMarkup addition
284 | textArray.append([state, fragment])
285 | fragment = ""
286 | state = "CMAdd"
287 |
288 | else:
289 | # exiting CriticMarkup addition
290 | textArray.append([state, fragment])
291 | fragment = ""
292 | state = "N"
293 |
294 | elif c == u"\uFDDD":
295 | # Entering or leaving underline
296 | if state == "N":
297 | # Going to underline
298 | textArray.append([state, fragment])
299 | fragment = ""
300 | state = "Ins"
301 |
302 | else:
303 | # exiting underline
304 | textArray.append([state, fragment])
305 | fragment = ""
306 | state = "N"
307 |
308 | elif c == u"\uFDDE":
309 | # Entering or leaving strikethrough
310 | if state == "N":
311 | # Going to strikethrough
312 | textArray.append([state, fragment])
313 | fragment = ""
314 | state = "Del"
315 |
316 | else:
317 | # exiting strikethrough
318 | textArray.append([state, fragment])
319 | fragment = ""
320 | state = "N"
321 | elif c == u"\uFDDF":
322 | # Entering or leaving subscript
323 | if state == "N":
324 | # Going to subscript
325 | textArray.append([state, fragment])
326 | fragment = ""
327 | state = "Sub"
328 |
329 | else:
330 | # exiting subscript
331 | textArray.append([state, fragment])
332 | fragment = ""
333 | state = "N"
334 |
335 | elif c == u"\uFDE0":
336 | # Entering or leaving superscript
337 | if state == "N":
338 | # Going to superscript
339 | textArray.append([state, fragment])
340 | fragment = ""
341 | state = "Sup"
342 |
343 | else:
344 | # exiting superscript
345 | textArray.append([state, fragment])
346 | fragment = ""
347 | state = "N"
348 |
349 | elif c == "[":
350 | if state == "N":
351 | # Could be entering a Link
352 | if fragment != "":
353 | textArray.append([state, fragment])
354 |
355 | # The bracket is kept in in case there is no matching ]
356 | fragment = "["
357 | state = "LinkText1"
358 |
359 | elif state == "LinkText2":
360 | # Could be entering an indirect reference
361 | indLinkText = fragment[:-1]
362 |
363 | # The bracket is kept in in case there is no matching ]
364 | fragment = "["
365 | state = "LinkRef1"
366 |
367 | elif c == "]":
368 | # Could be ending picking up the link text
369 | if state == "LinkText1":
370 | # Picked up end of link text
371 | state = "LinkText2"
372 |
373 | # Remove [ and add a separator to allow for link URL
374 | fragment = fragment[1:] + u"\uFDD8"
375 |
376 | elif state == "fnref":
377 | # This terminates a footnote reference
378 | textArray.append([state, fragment])
379 | state = "N"
380 | fragment = ""
381 |
382 | elif state == "LinkRef1":
383 | # Picked up link reference
384 | reference = fragment[1:]
385 |
386 | # Attempt to look up reference
387 | foundReference = False
388 | for indref, indURL in globals.indirectAnchors:
389 | if indref == reference:
390 | foundReference = True
391 | break
392 |
393 | if foundReference:
394 | # Append fragment with resolved reference
395 | textArray.append(["Link", indLinkText + u"\uFDD8" + indURL])
396 | else:
397 | print(f"Reference {reference} not resolved.\n")
398 | textArray.append(["N", indLinkText])
399 | fragment = ""
400 | state = "N"
401 |
402 | else:
403 | # This was an ordinary square bracket
404 | fragment += "]"
405 |
406 | elif c == "(":
407 | # Could be starting to pick up the link URL
408 | if state == "LinkText2":
409 | # Picked up start of link URL
410 | state = "LinkURL1"
411 | else:
412 | fragment = fragment + c
413 |
414 | elif c == ")":
415 | # Could be ending picking up the link URL
416 | if state == "LinkURL1":
417 | # Picked up end of link URL
418 | textArray.append(["Link", fragment])
419 | fragment = ""
420 | state = "N"
421 | else:
422 | fragment = fragment + c
423 |
424 | elif c == u"\uFDE3":
425 | fragment = fragment + "`"
426 |
427 | elif c == u"\uFDE1":
428 | fragment = fragment + "<"
429 |
430 | elif c == u"\uFDD6":
431 | fragment = fragment + "["
432 |
433 | elif c == u"\uFDD7":
434 | fragment = fragment + "]"
435 |
436 | elif c == u"\uFDD5":
437 | dictEntry = fragment.split(">")
438 | dictAbbrev = dictEntry[1]
439 | dictFull = dictEntry[0].strip().strip("'").strip('"')
440 | abbrevDictionary[dictAbbrev] = dictFull
441 | textArray.append(["Gloss", dictAbbrev, dictAbbrev, dictFull])
442 | fragment = ""
443 |
444 | elif c == u"\uFDD4":
445 | if fragment != "":
446 | textArray.append([state, fragment])
447 | fragment = ""
448 | dictEntry = ""
449 |
450 | elif c == u"\uFDD3":
451 | # End of span
452 | if spanState == "Class":
453 | # Span with class
454 | splitting = fragment.split(">")
455 | spanText = splitting[1]
456 | className = splitting[0].strip().strip("'").strip('"').lower()
457 | styleText = ""
458 | if (
459 | (className in globals.bgcolors)
460 | | (className in globals.fgcolors)
461 | | (className in globals.emphases)
462 | | (className in globals.fontsizes)
463 | ):
464 | textArray.append(["SpanClass", [className, spanText]])
465 |
466 | fragment = ""
467 |
468 | else:
469 | print(
470 | f"{className} is not defined. Ignoring reference to it in element."
471 | )
472 |
473 | fragment = spanText
474 | else:
475 | # Span with style
476 | splitting = fragment.split(">")
477 | spanText = splitting[1]
478 | styleText = splitting[0].strip().strip("'").strip('"')
479 | textArray.append(["SpanStyle", [styleText, spanText]])
480 | className = ""
481 | fragment = ""
482 |
483 | spanState = "None"
484 |
485 | elif c == u"\uFDD2":
486 | # In span element where we hit the class name
487 | if fragment != "":
488 | textArray.append([state, fragment])
489 |
490 | fragment = ""
491 | spanState = "Class"
492 |
493 | elif c == u"\uFDD1":
494 | # In span element where we hit the style text
495 | if fragment != "":
496 | textArray.append([state, fragment])
497 |
498 | fragment = ""
499 | spanState = "Style"
500 | elif c == u"\uFDD0":
501 | if fragment != "":
502 | textArray.append([state, fragment])
503 |
504 | fragment = ""
505 | state = "fnref"
506 | else:
507 | fragment = fragment + c
508 |
509 | lastChar = c
510 |
511 | if fragment != "":
512 | textArray.append([state, fragment])
513 | return textArray
514 |
515 |
516 | # Calls the tokeniser and then handles the fragments it gets back
517 | def addFormattedText(p, text):
518 | boldBold = globals.processingOptions.getCurrentOption("boldBold")
519 | boldColour = globals.processingOptions.getCurrentOption("boldColour")
520 | italicItalic = globals.processingOptions.getCurrentOption("italicItalic")
521 | italicColour = globals.processingOptions.getCurrentOption("italicColour")
522 | monoFont = globals.processingOptions.getCurrentOption("monoFont")
523 |
524 | # Get back parsed text fragments, along with control information on each
525 | # fragment
526 | parsedText = parseText(text)
527 |
528 | # Replace u"\uFDE2" with > in each Fragment
529 | for f in range(len(parsedText)):
530 | if parsedText[f][0] in ["SpanClass", "SpanStyle"]:
531 | parsedText[f][1][1] = parsedText[f][1][1].replace(u"\uFDE2", ">")
532 | else:
533 | parsedText[f][-1] = parsedText[f][-1].replace(u"\uFDE2", ">")
534 |
535 | # Prime flattened Text
536 | flattenedText = ""
537 | for fragment in parsedText:
538 | if fragment[0] == "Gloss":
539 | fragType, fragDetail, fragTerm, fragTitle = fragment
540 | else:
541 | fragType, fragDetail = fragment
542 |
543 | # Break into subfragments around a newline
544 | if fragType == "SpanClass":
545 | className, fragText = fragDetail
546 | styleText = ""
547 | subfragments = fragText.split("\n")
548 | elif fragType == "SpanStyle":
549 | styleText, fragText = fragDetail
550 | className = ""
551 | subfragments = fragText.split("\n")
552 | else:
553 | subfragments = fragDetail.split("\n")
554 |
555 | # Process each subfragment
556 | sfnum = 0
557 | for subfragment in subfragments:
558 | if sfnum > 0:
559 | # Subfragments after the first need to be preceded by a line break
560 | p.add_line_break()
561 |
562 | sfnum += 1
563 | # Ensure "\*" is rendered as a literal asterisk
564 | subfragment = subfragment.replace("∗", "*")
565 |
566 | # Ensure "\#" is rendered as a literal octothorpe
567 | subfragment = subfragment.replace("#", "#")
568 |
569 | run = p.add_run()
570 |
571 | if fragType not in ["Link", "fnref", "Gloss"]:
572 | run.text = subfragment
573 | elif fragType == "Gloss":
574 | run.text = fragTerm
575 |
576 | if fragType == "I":
577 | font = run.font
578 |
579 | if italicItalic == True:
580 | font.italic = True
581 |
582 | if italicColour != ("None", ""):
583 | setColour(font.color, italicColour)
584 |
585 | elif fragType == "Gloss":
586 | # Add this run to abbrevRunsDictionary - for Glossary fix ups later
587 | if fragTerm not in abbrevRunsDictionary:
588 | abbrevRunsDictionary[fragTerm] = []
589 | abbrevRunsDictionary[fragTerm].append(run)
590 | elif fragType == "fnref":
591 | font = run.font
592 | font.size = Pt(16)
593 | set_superscript(font)
594 | fnref = fragment[1]
595 | if fnref in footnoteReferences:
596 | footnoteNumber = footnoteReferences.index(fnref)
597 | run.text = str(footnoteNumber + 1)
598 | footnoteRunsDictionary[footnoteNumber] = run
599 | else:
600 | run.text = "[?]"
601 | print("Error: Footnote reference '" + fnref + "' unresolved.")
602 | linkText = "!"
603 | fragment = ""
604 |
605 | elif fragType == "SpanClass":
606 | handleSpanClass(run, className)
607 |
608 | elif fragType == "SpanStyle":
609 | handleSpanStyle(run, styleText)
610 |
611 | elif fragType == "B2":
612 | font = run.font
613 |
614 | if boldBold == True:
615 | font.bold = True
616 |
617 | if boldColour != ("None", ""):
618 | setColour(font.color, boldColour)
619 |
620 | elif fragType == "C":
621 | font = run.font
622 | font.name = monoFont
623 | elif fragType == "CMRep":
624 | font = run.font
625 | font.color.rgb = RGBColor(255, 140, 0)
626 | run.text = "{~~" + subfragment + "~~}"
627 | elif fragType == "CMHig":
628 | font = run.font
629 | font.color.rgb = RGBColor(195, 0, 195)
630 | run.text = "{==" + subfragment + "==}"
631 | elif fragType == "CMCom":
632 | font = run.font
633 | font.color.rgb = RGBColor(0, 0, 195)
634 | run.text = "{>>" + subfragment + "<<}"
635 | elif fragType == "CMDel":
636 | font = run.font
637 | font.color.rgb = RGBColor(195, 0, 0)
638 | run.text = "{--" + subfragment + "--}"
639 | elif fragType == "CMAdd":
640 | font = run.font
641 | font.color.rgb = RGBColor(0, 195, 0)
642 | run.text = "{++" + subfragment + "++}"
643 | elif fragType == "Ins":
644 | font = run.font
645 | font.underline = True
646 | elif fragType == "Del":
647 | font = run.font
648 | setStrikethrough(font)
649 | elif fragType == "Sub":
650 | font = run.font
651 | set_subscript(font)
652 | elif fragType == "Sup":
653 | font = run.font
654 | set_superscript(font)
655 | elif fragType == "Link":
656 | linkArray = subfragment.split(u"\uFDD8")
657 | linkText = linkArray[0]
658 | linkURL = linkArray[1]
659 | run.text = linkText
660 | if linkURL.startswith("#"):
661 | # Is an internal Url
662 | linkHref = linkURL[1:].strip()
663 | globals.href_runs[linkHref] = run
664 | else:
665 | # Not an internal link so create it
666 | hlink = run.hyperlink
667 | hlink.address = linkURL
668 |
669 | # URL might be a macro reference
670 | if linkURL[:11] == "ppaction://":
671 | # URL is indeed a macro reference, so treat it as such
672 | hlink._hlinkClick.action = linkURL
673 |
674 | # Add the flattened text from this subfragment
675 | if fragType == "Link":
676 | flattenedText = flattenedText + linkText
677 | else:
678 | flattenedText = flattenedText + subfragment
679 |
680 | return flattenedText
681 |
682 | def handleSpanClass(run, className):
683 | if className in globals.bgcolors:
684 | run = setHighlight(run, globals.bgcolors[className])
685 |
686 | if className in globals.fgcolors:
687 | font = run.font
688 | font.color.rgb = RGBColor.from_string(globals.fgcolors[className])
689 |
690 | if className in globals.emphases:
691 | font = run.font
692 | if " bold " in " " + globals.emphases[className] + " ":
693 | font.bold = True
694 | else:
695 | font.bold = False
696 | if " italic " in " " + globals.emphases[className] + " ":
697 | font.italic = True
698 | else:
699 | font.italic = False
700 | if " underline " in " " + globals.emphases[className] + " ":
701 | font.underline = True
702 | else:
703 | font.underline = False
704 |
705 | if className in globals.fontsizes:
706 | font = run.font
707 | font.size = Pt(float(globals.fontsizes[className]))
708 |
709 |
710 | def handleSpanStyle(run, styleText):
711 | styleElements = styleText.split(";")
712 |
713 | # Handle the non-empty ones - as the empty one is after the final semicolon
714 | for styleElement in list(filter(lambda e: e != "", styleElements)):
715 | styleElementSplit = styleElement.split(":")
716 | styleElementName = styleElementSplit[0].strip()
717 | styleElementValue = styleElementSplit[1].strip()
718 |
719 | if styleElementName == "color":
720 | check, RGBstring = parseRGB(styleElementValue)
721 | if check:
722 | run.font.color.rgb = RGBColor.from_string(RGBstring)
723 | else:
724 | print(f"Invalid {styleElementName} RGB value {styleElementValue}")
725 |
726 | elif styleElementName == "background-color":
727 | check, RGBstring = parseRGB(styleElementValue)
728 | if check:
729 | setHighlight(run, RGBstring)
730 | else:
731 | print(f"Invalid {styleElementName} RGB value {styleElementValue}")
732 |
733 | elif styleElementName == "text-decoration":
734 | if styleElementValue == "underline":
735 | run.font.underline = True
736 |
737 | elif styleElementName == "font-weight":
738 | if styleElementValue == "bold":
739 | run.font.bold = True
740 |
741 | elif styleElementName == "font-style":
742 | if styleElementValue == "italic":
743 | run.font.italic = True
744 |
745 | elif styleElementName == "font-size":
746 | run.font.size = Pt(float(styleElementValue[:-2]))
747 |
748 |
--------------------------------------------------------------------------------
/processingOptions.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | processingOptions
4 |
5 | """
6 |
7 | # Note: Options stored with lower case keys
8 | class ProcessingOptions:
9 | def __init__(self):
10 | self.defaultOptions = {}
11 | self.presentationOptions = {}
12 | self.currentOptions = {}
13 | self.dynamicallyChangedOptions = {}
14 | self.hideMetadataStyle = False
15 |
16 | def getDefaultOption(self, optionName):
17 | return self.defaultOptions[optionName.lower()]
18 |
19 | def setDefaultOption(self, optionName, value):
20 | self.defaultOptions[optionName.lower()] = value
21 |
22 | def getPresentationOption(self, optionName):
23 | return self.presentationOptions[optionName.lower()]
24 |
25 | def setPresentationOption(self, optionName, value):
26 | self.presentationOptions[optionName.lower()] = value
27 |
28 | def getCurrentOption(self, optionName):
29 | return self.currentOptions[optionName.lower()][-1]
30 |
31 | # Note: Can't pop to an empty stack. Will always have default available
32 | def popCurrentOption(self, optionName):
33 | key = optionName.lower()
34 | if len(self.currentOptions[key]) > 1:
35 | # Have a non-default value to use
36 | self.currentOptions[key].pop()
37 |
38 | def setCurrentOption(self, optionName, value):
39 | key = optionName.lower()
40 |
41 | if key in self.currentOptions:
42 | # Add new value to existing stack
43 | self.currentOptions[key].append(value)
44 | else:
45 | # Start a new stack for this option
46 | self.currentOptions[key] = [value]
47 |
48 | def setOptionValues(self, optionName, value):
49 | key = optionName.lower()
50 | if key not in self.defaultOptions:
51 | self.setDefaultOption(optionName, value)
52 |
53 | self.setPresentationOption(optionName, value)
54 |
55 | self.setCurrentOption(optionName, value)
56 |
57 | def setOptionValuesArray(self, optionArray):
58 | for keyValuePair in optionArray:
59 | self.setOptionValues(keyValuePair[0], keyValuePair[1])
60 |
61 | def dynamicallySetOption(self, optionName, optionValue, conversion):
62 | lowerName = optionName.lower()
63 | if optionValue == "default":
64 | self.setCurrentOption(lowerName, self.getDefaultOption(lowerName))
65 |
66 | elif optionValue == "pres":
67 | self.setCurrentOption(lowerName, self.getPresentationOption(lowerName))
68 |
69 | elif optionValue in ["pop", "prev"]:
70 | self.popCurrentOption(lowerName)
71 |
72 | elif conversion == "":
73 | self.setCurrentOption(lowerName, optionValue)
74 |
75 | elif conversion == "float":
76 | self.setCurrentOption(lowerName, float(optionValue))
77 |
78 | elif conversion == "sortednumericlist":
79 | self.setCurrentOption(lowerName, sortedNumericList(optionValue))
80 |
81 | elif conversion == "int":
82 | self.setCurrentOption(lowerName, int(optionValue))
83 |
84 | self.dynamicallyChangedOptions[lowerName] = True
85 |
--------------------------------------------------------------------------------
/rectangle.py:
--------------------------------------------------------------------------------
1 | """
2 | rectangle
3 | """
4 |
5 | # Expected units are those of md2pptx
6 | class Rectangle:
7 | def __init__(self, top, left, height, width):
8 | self.top = top
9 | self.left = left
10 | self.height = height
11 | self.width = width
12 |
13 |
--------------------------------------------------------------------------------
/runPython.py:
--------------------------------------------------------------------------------
1 | """
2 | runPython
3 | """
4 |
5 | version = "0.9"
6 |
7 | import csv
8 | from pptx.chart.data import CategoryChartData
9 | from pptx.oxml.xmlchemy import OxmlElement, serialize_for_reading
10 | from pptx.oxml import parse_xml
11 | from pptx.dml.color import RGBColor
12 | from pptx.enum.chart import XL_CHART_TYPE
13 | from pptx.enum.chart import XL_LEGEND_POSITION
14 | from pptx.enum.text import PP_ALIGN
15 | from pptx.dml.color import RGBColor
16 | from colour import setColour, parseColour
17 | from pptx.enum.shapes import MSO_SHAPE_TYPE
18 | from pptx.enum.shapes import PP_PLACEHOLDER
19 | from paragraph import *
20 | from pptx.util import Inches, Pt
21 | from pptx.enum.shapes import MSO_CONNECTOR, MSO_SHAPE
22 |
23 | import globals
24 |
25 | class RunPython:
26 | def __init__(
27 | self,
28 | ):
29 | pass
30 |
31 | # Execute the lines of code passed in
32 | def run(self, prs, slide, renderingRectangle, codeLines, codeType):
33 | concatenatedCodeLines = "\n".join(codeLines)
34 | exec(concatenatedCodeLines)
35 |
36 | def runFromFile(self, filename, prs, slide, renderingRectangle):
37 | exec(open(filename).read())
38 |
39 |
40 | # Helper function for run-python
41 | def readCSV(filename):
42 | my_csv = []
43 | with open(filename, 'r') as csvfile:
44 | chart_reader = csv.reader(csvfile, quoting = csv.QUOTE_NONNUMERIC)
45 | for row in chart_reader:
46 | my_csv.append(row)
47 |
48 | return my_csv
49 |
50 | def filterRows(my_array, filterFunction):
51 | my_array2 = []
52 | for rowNumber, row in enumerate(my_array):
53 | if filterFunction(rowNumber, row):
54 | my_array2.append(row)
55 |
56 | return my_array2
57 |
58 |
59 | def transposeArray(chart_array):
60 | return list(map(list, zip(*chart_array)))
61 |
62 | def makeChartData(chart_array, seriesIsColumn = True, columns = None):
63 |
64 | chart_data = CategoryChartData()
65 |
66 | if columns is not None:
67 | chart_array2 = []
68 | for rowNumber, row in enumerate(chart_array):
69 | chart_row = []
70 | for column in columns:
71 | chart_row.append(row[column])
72 |
73 | chart_array2.append(chart_row)
74 | chart_array = chart_array2
75 | print(pptx.enum.chart)
76 |
77 |
78 | if seriesIsColumn:
79 | # Transpose input data
80 | chart_array = RunPython.transposeArray(chart_array)
81 |
82 | # x values
83 | chart_data.categories = chart_array[0][1:]
84 |
85 | # Series
86 | for rowNumber, row in enumerate(chart_array[1:]):
87 | chart_data.add_series(row[0],row[1:])
88 |
89 | return chart_data
90 |
91 | # Helper function to make a chart. The result can be further manipulated
92 | def makeChart(slide,
93 | chart_type,
94 | renderingRectangle,
95 | chart_data,
96 | title = None,
97 | legendPosition = None):
98 | c = slide.shapes.add_chart(
99 | chart_type,
100 | renderingRectangle.left,
101 | renderingRectangle.top,
102 | renderingRectangle.width,
103 | renderingRectangle.height,
104 | chart_data
105 | )
106 |
107 | chart = c.chart
108 |
109 | if title is not None:
110 | chart.has_title = True
111 | chart.chart_title.text_frame.text = title
112 |
113 | if legendPosition is not None:
114 | chart.has_legend = True
115 | chart.legend.position = legendPosition
116 | chart.legend.include_in_layout = False
117 |
118 |
119 | return c
120 |
121 | # Helper routine to make a table. The result can be further manipulated
122 | def makeTable(slide,
123 | renderingRectangle,
124 | table_array):
125 |
126 | height = len(table_array)
127 | width = len(table_array[0])
128 |
129 | t = slide.shapes.add_table(height,width,
130 | renderingRectangle.left,
131 | renderingRectangle.top,
132 | renderingRectangle.width,
133 | renderingRectangle.height)
134 |
135 | table = t.table
136 |
137 | for i in range(height):
138 | for j in range(width):
139 | c = table.cell(i, j)
140 | c.text = str(table_array[i][j])
141 |
142 | return t
143 |
144 | def applyCellFillRGB(table, row, column, red, green, blue):
145 | cff = table.table.cell(row, column).fill
146 | cff.solid()
147 | cff.fore_color.rgb = RGBColor(red, green, blue)
148 |
149 | def applyCellListFillRGB(table, cellList, red, green, blue):
150 | for row, column in cellList:
151 | RunPython.applyCellFillRGB(table, row, column, red, green, blue)
152 |
153 | def alignTableCellText(tableFrame, rowNumber, columnNumber, alignment, paragraphNumber = None):
154 | # Get the cell's text_frame
155 | tableCellFrame = tableFrame.table.cell(rowNumber, columnNumber).text_frame
156 |
157 | if paragraphNumber == None:
158 | # Iterate over the cell's paaragraph's, aligning right
159 | for p in tableCellFrame.paragraphs:
160 | p.alignment = alignment
161 | else:
162 | tableCellFrame.paragraphs[paragraphNumber].alignment = alignment
163 |
164 | def makeDrawnShape(slide, vertices, fill = False, text = None, textColor = None, fillColor = None, closed = True):
165 | ffBuilder = slide.shapes.build_freeform(*vertices[0], True)
166 |
167 | ffBuilder.add_line_segments(vertices[1:], close = closed)
168 |
169 | s = ffBuilder.convert_to_shape()
170 |
171 | if text is not None:
172 | s.text = text
173 | p = s.text_frame.paragraphs[0]
174 | p.alignment = PP_ALIGN.CENTER
175 | if textColor is None:
176 | setColour(p.font.color, parseColour('#000000'))
177 | else:
178 | setColour(p.font.color, parseColour(textColor))
179 |
180 | if fill:
181 | s.fill.solid()
182 | if fillColor is not None:
183 | setColour(s.fill.fore_color, parseColour(fillColor))
184 |
185 | return s
186 |
187 | def doChecklistChecks(placeholder, checklist, colourChecks = False):
188 | tf = placeholder.text_frame
189 | paras = tf.paragraphs
190 |
191 | for paraNumber, para in enumerate(paras):
192 | # Save original font size
193 | originalFontSize = para.font.size
194 |
195 | # Save original indentation level
196 | level = para.level
197 |
198 | # Remove the original pPr element
199 | para._element.remove(para._element.getchildren()[0])
200 |
201 | xml = ''
202 |
203 | # Note the level insertion
204 | xml += f''
205 |
206 |
207 | if checklist[paraNumber] == None:
208 | # Checkbox unchecked
209 |
210 | # Set the bullet to an empty square - and don't colour it
211 | xml += ' '
212 | xml += ' '
213 | elif checklist[paraNumber] == True:
214 | # Checkbox ticked
215 |
216 | # Maybe colour the mark
217 | if colourChecks:
218 | xml += ''
219 | xml += ' '
220 | xml += ' '
221 |
222 | # Set the bullet to a square with a tick
223 | xml += ' '
224 | xml += ' '
225 | else:
226 | # Checkbox crossed
227 |
228 | # Maybe colour the mark
229 | if colourChecks:
230 | xml += ''
231 | xml += ' '
232 | xml += ' '
233 |
234 | # Set the bullet to a square with a cross
235 | xml += ' '
236 | xml += ' '
237 |
238 |
239 | xml += ' '
240 |
241 | # Parse this XML
242 | parsed_xml = parse_xml(xml)
243 |
244 | # Insert the parsed XML fragment as a child of the pPr element
245 | para._element.insert(0, parsed_xml)
246 |
247 | # Restore original font size
248 | para.font.size = originalFontSize
249 |
250 | return placeholder
251 |
252 | def makeChecklist(placeholder, checklist, checkTextIndex = 0, checkMarkIndex = 1, levelIndex = 2, colourChecks = False):
253 | checkMarks = []
254 |
255 | for paraNumber, checklistItem in enumerate(checklist):
256 | checkMarks.append(checklistItem[checkMarkIndex])
257 |
258 | if paraNumber == 0:
259 | para = placeholder.text_frame.paragraphs[0]
260 | else:
261 | para = placeholder.text_frame.add_paragraph()
262 |
263 | addFormattedText(para, checklistItem[checkTextIndex])
264 |
265 | # The actual level setting in the surviving XML is done by doChecklistChecks
266 | # The below sets the level as a hint for doChecklistChecks to work with
267 | if len(checklistItem) > levelIndex:
268 | try:
269 | level = int(checklistItem[levelIndex])
270 |
271 | para.level = level - 1
272 | except ValueError:
273 | para.level = 0
274 | else: para.level = 0
275 |
276 | RunPython.doChecklistChecks(placeholder, checkMarks, colourChecks)
277 |
278 | return placeholder
279 |
280 | def makeTruthy(table_array, columnNumber = 1, trueString = "Yes", falseString = "No", unsetString = ""):
281 | for row in table_array:
282 | if row[columnNumber] == unsetString:
283 | row[columnNumber] = None
284 | elif row[columnNumber] == trueString:
285 | row[columnNumber] = True
286 | else:
287 | row[columnNumber] = False
288 |
289 | return table_array
290 |
291 | def ensureTextbox(slide, renderingRectangle, shapeIndex = None):
292 | if shapeIndex is not None:
293 | if len(slide.shapes) < shapeIndex + 1:
294 | # Need to create a text box as shape index too high
295 | newShape = slide.shapes.add_textbox(
296 | renderingRectangle.left,
297 | renderingRectangle.top,
298 | renderingRectangle.width,
299 | renderingRectangle.height
300 | )
301 |
302 | # Return new shape
303 | return newShape
304 | else:
305 | # shape Index is valid
306 | if (theShape.shape_type == MSO_SHAPE_TYPE.TEXT_BOX) | (theShape.placeholder_format.type == PP_PLACEHOLDER.OBJECT):
307 | # shape is a text box so return it
308 | return slide.shapes[shapeIndex]
309 | else:
310 | # Shape isn't a text box so create one
311 | newShape = slide.shapes.add_textbox(renderingRectangle.left, renderingRectangle.top, renderingRectangle.width, renderingRectangle.height)
312 |
313 | # Return new shape
314 | return newShape
315 |
316 | # shapeIndex wasn't set so potentially search for the last text box
317 | if len(slide.shapes) < 2:
318 | # Need to create a text box as only shape is presumed to be a title
319 | newShape = slide.shapes.add_textbox(renderingRectangle.left, renderingRectangle.top, renderingRectangle.width, renderingRectangle.height)
320 |
321 | # Return new shape
322 | return newShape
323 |
324 | # Search for last text box
325 | for shapeIndex, theShape in reversed(list(enumerate(slide.shapes))):
326 | if (theShape.shape_type == MSO_SHAPE_TYPE.TEXT_BOX) | (theShape.placeholder_format.type == PP_PLACEHOLDER.OBJECT):
327 | return theShape
328 |
329 |
330 | # Need to create a text box none found - other than perhaps at Index 0 (title)
331 | newShape = slide.shapes.add_textbox(renderingRectangle.left, renderingRectangle.top, renderingRectangle.width, renderingRectangle.height)
332 |
333 | # Return new shape's index as presumed to be the text box we want
334 | return newShape
335 |
336 | def checklistFromCSV(slide, renderingRectangle, filename, shapeIndex = None, colourChecks = False):
337 | # Read in CSV and turn second column into "truthy" values
338 | myChecklist = RunPython.makeTruthy(RunPython.readCSV(filename), 1)
339 |
340 | # Ensure we have a placeholder - whether first or second or specified
341 | textShape = RunPython.ensureTextbox(slide, renderingRectangle, shapeIndex)
342 |
343 | # Make the checklist in this placeholder from the imported file
344 | RunPython.makeChecklist(textShape, myChecklist, 0, 1, 2, colourChecks)
345 |
346 | return textShape
347 |
348 | def removeBullet(theShape, paragraphNumber):
349 | removeBullet(theShape.text_frame.paragraphs[paragraphNumber])
350 |
351 | return theShape
352 |
353 | def removeBullets(theShape):
354 | for p in theShape.text_frame.paragraphs:
355 | removeBullet(p)
356 |
357 | return theShape
358 |
359 | def removeSelectedBullets(theShape, removalArray):
360 | removeSelectedBullets(theShape.text_frame, removalArray)
361 |
362 | return theShape
363 |
364 | def getParagraphs(slide, wantedParagraphs = []):
365 | return getParagraphs(slide, wantedParagraphs)
366 |
367 | def doAnnotations(slide, annotationList, lineWidth = None, shapeWidth = None):
368 | for annotation in annotationList:
369 | x = Inches(float(annotation[0]))
370 | y = Inches(float(annotation[1]))
371 | w = Inches(float(annotation[2]))
372 | h = Inches(float(annotation[3]))
373 |
374 | text = annotation[4]
375 |
376 | if text in [
377 | "-",
378 | "<-",
379 | "->",
380 | "<->",
381 | "=",
382 | "<=",
383 | "=>",
384 | "<=>",
385 | ]:
386 | # Draw an line from x, y to x+w, y+h
387 | c = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x, y, x + w, y + h)
388 |
389 | if text != "-":
390 | # Will need an a:ln element
391 |
392 | # Find the spPr element to hang this off
393 | for element in c._element.getchildren():
394 | if element.tag == "{http://schemas.openxmlformats.org/presentationml/2006/main}spPr":
395 | spPr = element
396 | break
397 | if "=" in text:
398 | cmpd = "dbl"
399 | else:
400 | cmpd = "sng"
401 |
402 | xml = ''
403 |
404 | if "<" in text:
405 | xml += ' '
406 |
407 | if ">" in text:
408 | xml += ' '
409 |
410 | xml += ' '
411 |
412 | # Parse this XML
413 | parsed_xml = parse_xml(xml)
414 |
415 | # Insert the parsed XML fragment as a child of the pPr element
416 | spPr.append(parsed_xml)
417 | if lineWidth is not None:
418 | c.line.width = Pt(float(lineWidth))
419 |
420 | if len(annotation) > 5:
421 | toColour = c.line.color
422 | setColour(toColour, parseColour(annotation[5]))
423 |
424 | elif text[0] == "!":
425 | filename = text[1:]
426 | slide.shapes.add_picture(filename, x, y, w, h)
427 |
428 | elif text in [
429 | "[]",
430 | "()",
431 | "[-]",
432 | "(-)",
433 | "[=]",
434 | "(=)",
435 | "o",
436 | "O",
437 | ]:
438 | if text in [
439 | "[]",
440 | "[-]",
441 | "[=]",
442 | ]:
443 | b = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
444 |
445 | elif text in [
446 | "o",
447 | "O",
448 | ]:
449 | b = slide.shapes.add_shape(MSO_SHAPE.OVAL, x, y, w, h)
450 |
451 | else:
452 | b = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h)
453 |
454 | if len(annotation) > 5:
455 | b.text = annotation[5]
456 | f = b.text_frame
457 | p = f.paragraphs[0].alignment = PP_ALIGN.CENTER
458 |
459 | if len(annotation) > 6:
460 | # Foreground colour
461 | foreColour = annotation[6]
462 | if foreColour != "":
463 | toColour = b.text_frame.paragraphs[0].runs[0].font.color
464 | setColour(toColour, parseColour(foreColour))
465 |
466 | if len(annotation) > 7:
467 | # Background colour
468 | backColour = annotation[7]
469 | if backColour != "":
470 | b.fill.solid()
471 | toColour = b.fill.fore_color
472 | setColour(toColour, parseColour(annotation[7]))
473 |
474 | if shapeWidth is not None:
475 | b.line.width = Pt(float(shapeWidth))
476 |
477 | if ("=" in text) | (text == "O"):
478 | be = b._element
479 | ln= b.get_or_add_ln()
480 | ln.set("cmpd", "dbl")
481 | else:
482 | t = slide.shapes.add_textbox(x, y, w, h)
483 | t.text = text
484 | if len(annotation) > 5:
485 | toColour = t.text_frame.paragraphs[0].runs[0].font.color
486 | setColour(toColour, parseColour(annotation[5]))
487 |
488 | def annotationsFromCSV(slide, filename, lineWidth = None, shapeWidth = None):
489 | annotations = RunPython.readCSV(filename)
490 |
491 | RunPython.doAnnotations(slide, annotations, lineWidth, shapeWidth)
--------------------------------------------------------------------------------
/run_pyto.py:
--------------------------------------------------------------------------------
1 | # Pyto.app
2 | # note: first implementation to run on ios
3 | # interpreter Python 3.8+ in iOS
4 | import sys
5 | import runpy
6 |
7 | sys.argv.append('test/fullPresentation.md')
8 | sys.argv.append('test.pptx')
9 |
10 | runpy.run_path('./md2pptx', init_globals={'sys': sys})
11 |
--------------------------------------------------------------------------------
/symbols.py:
--------------------------------------------------------------------------------
1 | """
2 | symbols
3 | """
4 | import re
5 | import html
6 |
7 |
8 | # Resolve symbols and unescape any numeric character references
9 | def resolveSymbols(text):
10 | # h = html.parser.HTMLParser()
11 |
12 | textSplit = re.split("(&\#x?[0-9a-f]{2,6};)", text, flags=re.IGNORECASE)
13 | text2 = ""
14 | for t in textSplit:
15 | if t == "":
16 | text2 = text2 + t
17 | elif (t[0:2] == "") & (t[-1] == ";"):
18 | text2 = text2 + html.unescape(t)
19 | else:
20 | text2 = text2 + t
21 |
22 | # Replace certain entity references with actual characters
23 | replacementRules = [
24 | ("=", "="),
25 | ("<", u"\uFDE1"),
26 | (">", u"\uFDE2"),
27 | ("≤", "≤"),
28 | ("≥", "≥"),
29 | ("≈", "≈"),
30 | ("Δ", "Δ"),
31 | ("δ", "δ"),
32 | ("∼", "∼"),
33 | (" ", chr(160)),
34 | (";", ";"),
35 | (":", ":"),
36 | (",", ","),
37 | ("&", "&"),
38 | ("←", "←"),
39 | ("→", "→"),
40 | ("↑", "↑"),
41 | ("↓", "↓"),
42 | ("↔", "↔"),
43 | ("↕", "↕"),
44 | ("↖", "↖"),
45 | ("↗", "↗"),
46 | ("↙", "↙"),
47 | ("↘", "↘"),
48 | ("[", r"\["),
49 | ("]", r"\]"),
50 | ("∞", "∞"),
51 | ("ä", "ä"),
52 | ("Ä", "Ä"),
53 | ("ü", "ü"),
54 | ("Ü", "Ü"),
55 | ("ö", "ö"),
56 | ("Ö", "Ö"),
57 | ("ß", "ß"),
58 | ("€", "€"),
59 | ("✓", "✓"),
60 | ("…", "…"),
61 | ("×", "×"),
62 | ("%", "%"),
63 | ("÷", "÷"),
64 | ("∀", "∀"),
65 | ("∃", "∃"),
66 | ("λ", "λ"),
67 | ("μ", "μ"),
68 | ("ν", "ν"),
69 | ("π", "π"),
70 | ("ρ", "ρ"),
71 | ("‐", "-"),
72 | ("\`", u"\uFDE3"),
73 | ("`", u"\uFDE3"),
74 | (""", "\""),
75 | ("“", u"\u201C"),
76 | ("”", u"\u201D"),
77 | ("'", "'"),
78 | ("‘", u"\u2018"),
79 | ("’", u"\u2019"),
80 | ("Ø",u"\u00D8"),
81 | ("ø",u"\u00F8"),
82 | ]
83 |
84 | for term, replacement in replacementRules:
85 | text2 = text2.replace(term, replacement)
86 |
87 | return text2
88 |
--------------------------------------------------------------------------------
/test/Battery W2M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/Battery W2M.png
--------------------------------------------------------------------------------
/test/Battery W3M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/Battery W3M.png
--------------------------------------------------------------------------------
/test/EnclaveCadence.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/SVGtest.md:
--------------------------------------------------------------------------------
1 | pageTitleSize: 30
2 | sectionTitleSize: 48
3 | sectionSubtitleSize: 32
4 | numbers: no
5 | abstractTitle: Abstract
6 | template: Martin Template.pptx
7 |
8 |
9 | # Even More Fun With DDF
10 | Martin Packer 🦕 , IBM
11 |
12 | ### SVG Test
13 |
14 | 
15 |
16 | ### Web SVG Sample
17 |
18 | 
19 |
20 | ### Web PNG Example
21 |
22 | 
23 |
24 |
25 | ### PNG file
26 |
27 | 
28 |
29 |
--------------------------------------------------------------------------------
/test/audiotest.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/audiotest.m4a
--------------------------------------------------------------------------------
/test/cardTest.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 | cardlayout: horizontal
3 | baseTextSize: 20
4 | CardColour: BACKGROUND 2
5 | CardTitlePosition: inside
6 | cardshadow: yes
7 | cardshape: rounded
8 |
9 | # Card Test
10 | Subtitle of title slide
11 |
12 | ### Horizontal Cards
13 |
14 | * Here is a bullet above the cards
15 | * And here's another
16 |
17 | #### Card One
18 |
19 | * Some content for Card One
20 | * And some more content
21 |
22 | #### Card Two
23 |
24 | * Some content for Card Two
25 | * And some more content
26 |
27 | #### Card Three
28 |
29 | * Some content for Card Three
30 | * And some more content
31 |
32 | #### Card Four
33 |
34 | * Some content for Card Four
35 | * And some more content
36 |
37 | ### Vertical Cards
38 |
39 |
40 |
41 |
42 |
43 |
44 | * Here is a bullet above the cards
45 | * And here's another
46 | * And a little more content
47 |
48 | #### Card One
49 |
50 | * Some content for Card One
51 | * And some more content
52 |
53 | #### Card Two
54 |
55 | * Some content for Card Two
56 | * And some more content
57 |
58 | #### Card Three
59 |
60 | * Some content for Card Three
61 | * And some more content
62 |
63 | #### Card Four
64 |
65 | * Some content for Card Four
66 | * And some more content
67 |
68 | ### Horizontal Cards - No Bullets Above
69 |
70 |
71 |
72 |
73 | #### Card One
74 |
75 | * Some content for Card One
76 | * And some more content
77 |
78 | #### Card Two
79 |
80 | * Some content for Card Two
81 | * And some more content
82 |
83 | #### Card Three
84 |
85 | * Some content for Card Three
86 | * And some more content
87 |
88 | #### Card Four
89 |
90 | * Some content for Card Four
91 | * And some more content
92 |
93 | ### Vertical Cards - No Bullets Above
94 |
95 |
96 |
97 |
98 |
99 |
100 | #### Card One
101 |
102 | * Some content for Card One
103 | * And some more content
104 | * And a little more content
105 |
106 | #### Card Two
107 |
108 | * Some content for Card Two
109 | * And some more content
110 | * And a little more content
111 |
112 | #### Card Three
113 |
114 | * Some content for Card Three
115 | * And some more content
116 | * And a little more content
117 |
118 | ### Just Bullets
119 |
120 |
121 | * One
122 | * Two
123 | * Three
--------------------------------------------------------------------------------
/test/cardTest.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/cardTest.pptx
--------------------------------------------------------------------------------
/test/chartdata.csv:
--------------------------------------------------------------------------------
1 | " ","East","West","Midwest"
2 | "Series 1",19.2,21.4,16.7
3 | "Series 2",17.8,13.1,22.9
4 | "Series 3",19.2,21.4,16.7
5 | "Series 4",17.8,13.1,22.9
--------------------------------------------------------------------------------
/test/codetest.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 | contentsplit: 1 2
3 | contentsplitdirn: h
4 | style.fontsize.christopher: 45px
5 | style.fgcolor.christopher: FF0000
6 | hidden: yes
7 |
8 |
13 |
14 | # Code Test
15 |
16 | ### Here Is A Slide With A Graph
17 |
18 | ``` run-python
19 |
20 | # Read chart data from CSV file
21 | chart_csv = RunPython.readCSV("chartdata.csv")
22 |
23 | # Make chart data from the array. Second parameter defaults to True for "Series Is Column"
24 | chart_data = RunPython.makeChartData(chart_csv, True)
25 |
26 | chart1 = RunPython.makeChart(slide,
27 | XL_CHART_TYPE.COLUMN_CLUSTERED,
28 | renderingRectangle,
29 | chart_data,
30 | "My Important Chart",
31 | XL_LEGEND_POSITION.BOTTOM)
32 | ```
33 |
34 | ## Here Is A Table
35 |
36 | ``` run-python
37 |
38 | # Read chart data from CSV file
39 | chart_csv = RunPython.readCSV("chartdata.csv")
40 |
41 | # Make the table with the data
42 | table1 = RunPython.makeTable(slide,renderingRectangle, chart_csv)
43 |
44 | # Set a cell background to yellow
45 | RunPython.applyCellFillRGB(table1, 2, 3, 255, 255, 0)
46 |
47 | # Set list of cells to green
48 | greenList = [(0, 0), (2,1), (3,2)]
49 | RunPython.applyCellListFillRGB(table1, greenList, 0, 255, 0)
50 | ```
--------------------------------------------------------------------------------
/test/codetest.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/codetest.pptx
--------------------------------------------------------------------------------
/test/fullPresentation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Full Presentation
19 |
20 | Martin Packer, IBM
21 |
22 | Some Additional WLM-Related Information
23 |
24 | IEAOPTxx INITIMP=
25 |
26 |
27 | - Sets Initiator Code WLM Importance
28 | - Values are 0, 1, 2, 3, E
29 |
30 |
31 | - 0 means Dispatching Priority 254 (FE)
32 | - 1 ,2, or 3 — Defines that the initiator dispatching priority has to be lower than the dispatching priority for CPU critical work with the same or a higher importance level
33 |
34 |
35 | - If no service class with the CPU critical attribute and a corresponding or higher importance level is defined in the WLM policy, the dispatching priority is calculated in the same way as for parameter INITIMP=E
36 |
37 | - E - will be calculated in the same way as the enqueue promotion dispatching priority. The dispatching priority is calculated dynamically to ensure access to the processor. It should not impact high importance work; however, there is no guarantee that CPU critical work will always have a higher dispatching priority.
38 |
39 | - SMF30ICU helps size this CPU requirement
40 |
41 |
42 | Percentile Goal Transaction Ending Buckets
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Bucket
55 | Minimum % Of Goal
56 | Maximum % of Goal
57 | PI Value
58 |
59 |
60 |
61 |
62 |
63 | 1
64 | 0
65 | 50
66 | 0.5
67 |
68 |
69 | 2
70 | 50
71 | 60
72 | 0.6
73 |
74 |
75 | 3
76 | 60
77 | 70
78 | 0.7
79 |
80 |
81 | 4
82 | 70
83 | 80
84 | 0.8
85 |
86 |
87 | 5
88 | 80
89 | 90
90 | 0.9
91 |
92 |
93 | 6
94 | 90
95 | 100
96 | 1.0
97 |
98 |
99 | 7
100 | 100
101 | 110
102 | 1.1
103 |
104 |
105 | 8
106 | 110
107 | 120
108 | 1.2
109 |
110 |
111 | 9
112 | 120
113 | 130
114 | 1.3
115 |
116 |
117 | 10
118 | 130
119 | 140
120 | 1.4
121 |
122 |
123 | 11
124 | 140
125 | 150
126 | 1.5
127 |
128 |
129 | 12
130 | 150
131 | 200
132 | 2.0
133 |
134 |
135 | 13
136 | 200
137 | 400
138 | 4.0
139 |
140 |
141 | 14
142 | 400
143 | ∞
144 | 4.0
145 |
146 |
147 |
148 |
149 | Service Class Periods
150 |
151 |
152 | - “Transactions” accumulate service
153 |
154 |
155 | - Transactions can be eg DDF transactions, but also batch jobs
156 |
157 | - Service is typically CPU
158 | - Transactions start in Period 1
159 | - When a transaction’s service exceeds the duration for Period 1 it switches to Period 2
160 |
161 |
162 | - Duration is not Elapsed Time
163 | - Likewise from Period 2 to Period 3
164 |
165 | - Each Service Class period can have its own goal
166 |
167 |
168 | - Usually later periods' goals have progressively lower importances
169 |
170 |
171 | - And progressively more relaxed response time targets
172 |
173 |
174 | - RMF reports comprehensively on each Service Class period
175 |
176 |
177 | - In Workload Activity Report (SMF 72)
178 |
179 | - Note: CICS and IMS transactions cannot have multi-period goals
180 |
181 |
182 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/test/fullPresentation.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 | pageTitleSize: 22
3 | sectionTitleSize: 30
4 | baseTextSize: 22
5 | compactTables: 20
6 | numbers: no
7 | style.fgcolor.blue: 0000FF
8 | style.fgcolor.red: FF0000
9 | style.fgcolor.green: 00FF00
10 | style.fgcolor.purple: FF00FF
11 |
12 | # Full Presentation
13 | Martin Packer, IBM
14 |
15 |
16 | ## Some Additional WLM-Related Information
17 |
18 | ### IEAOPT*xx* INITIMP=
19 |
20 | * Sets Initiator Code WLM Importance
21 | * Values are *0*, *1*, *2*, *3*, *E*
22 | * *0* means Dispatching Priority 254 (FE)
23 | * *1* ,*2*, or *3* — Defines that the initiator dispatching priority has to be lower than the dispatching priority for CPU critical work with the same or a higher importance level
24 | * If no service class with the CPU critical attribute and a corresponding or higher importance level is defined in the WLM policy, the dispatching priority is calculated in the same way as for parameter INITIMP=E
25 | * *E* - will be calculated in the same way as the enqueue promotion dispatching priority. The dispatching priority is calculated dynamically to ensure access to the processor. It should not impact high importance work; however, there is no guarantee that CPU critical work will always have a higher dispatching priority.
26 | * SMF30ICU helps size this CPU requirement
27 |
28 | ### Percentile Goal Transaction Ending Buckets
29 |
30 | |Bucket|Minimum % Of Goal|Maximum % of Goal|PI Value|
31 | |-:|--:|--:|-:|
32 | |1|0|**50**|0.5|
33 | |2|50|60|0.6|
34 | |3|60|70|0.7|
35 | |4|70|80|0.8|
36 | |5|80|90|0.9|
37 | |6|90|**100**|1.0|
38 | |7|100|110|1.1|
39 | |8|110|120|1.2|
40 | |9|120|130|1.3|
41 | |10|130|140|1.4|
42 | |11|140|150|1.5|
43 | |12|150|200|2.0|
44 | |13|200|**400**|4.0|
45 | |14|**400**|**∞**|4.0|
46 |
47 | ### Service Class Periods
48 |
49 | * "Transactions" accumulate service
50 | * Transactions can be eg DDF transactions, but also batch jobs
51 | * Service is typically CPU
52 | * Transactions start in Period 1
53 | * When a transaction's service exceeds the duration for Period 1 it switches to Period 2
54 | * Duration is **not** Elapsed Time
55 | * Likewise from Period 2 to Period 3
56 | * Each Service Class period can have its own goal
57 | * Usually later periods' goals have progressively lower importances
58 | * And progressively more relaxed response time targets
59 | * RMF reports comprehensively on each Service Class period
60 | * In Workload Activity Report (SMF 72)
61 | * **Note:** CICS and IMS transactions cannot have multi-period goals
62 |
--------------------------------------------------------------------------------
/test/fullPresentation.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/fullPresentation.pptx
--------------------------------------------------------------------------------
/test/linktest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Slide One IBM
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 | -
20 |
Hyperlink was problematic [ This is after open ↩
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/linktest.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 |
3 | ### Slide One [IBM](http://w3.ibm.com)
4 |
5 | * Here is **bold text** with a hyperlink[^hl] in it - [IBM](http://w3.ibm.com)
6 |
7 | [^hl]: Hyperlink was problematic [ This is after open
--------------------------------------------------------------------------------
/test/linktest.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/linktest.pptx
--------------------------------------------------------------------------------
/test/smartCells.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 |
3 | ## Here Is A Table - With Smart Colouring
4 |
5 | ``` run-python
6 |
7 | chart_csv = RunPython.readCSV("chartdata2.csv")
8 |
9 | # Make the table with the data
10 | table1 = RunPython.makeTable(slide,renderingRectangle, chart_csv)
11 |
12 | # Cycle through the cells, formatting them
13 | redList = []
14 | greenList = []
15 | orangeList = []
16 |
17 | for rowNumber, row in enumerate(chart_csv):
18 | for columnNumber, cell in enumerate(row):
19 | try:
20 | isFloat = True
21 | floatValue = float(cell)
22 | if columnNumber == 2:
23 | if floatValue >= 99:
24 | redList.append((rowNumber, columnNumber))
25 | elif floatValue >= 95:
26 | orangeList.append((rowNumber, columnNumber))
27 | else:
28 | greenList.append((rowNumber, columnNumber))
29 | except ValueError:
30 | isFloat = False
31 |
32 | # Align last two columns right
33 | if columnNumber > 0:
34 | RunPython.alignTableCellText(table1, rowNumber, columnNumber, PP_ALIGN.RIGHT)
35 |
36 | # Set appropriate list of cells to red
37 | RunPython.applyCellListFillRGB(table1, redList, 255, 0, 0)
38 |
39 | # Set appropriate list of cells to orange
40 | RunPython.applyCellListFillRGB(table1, orangeList, 255, 255, 0)
41 |
42 | # Set appropriate list of cells to green
43 | RunPython.applyCellListFillRGB(table1, greenList, 0, 255, 0)
44 |
45 |
46 | ```
47 |
--------------------------------------------------------------------------------
/test/smartCells.mdp:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 |
3 | ## Here Is A Table - With Smart Colouring
4 |
5 | ``` run-python
6 |
7 | =include smartCells.py
8 |
9 | ```
--------------------------------------------------------------------------------
/test/smartCells.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/smartCells.pptx
--------------------------------------------------------------------------------
/test/smartCells.py:
--------------------------------------------------------------------------------
1 | chart_csv = RunPython.readCSV("chartdata2.csv")
2 |
3 | # Make the table with the data
4 | table1 = RunPython.makeTable(slide,renderingRectangle, chart_csv)
5 |
6 | # Cycle through the cells, formatting them
7 | redList = []
8 | greenList = []
9 | orangeList = []
10 |
11 | for rowNumber, row in enumerate(chart_csv):
12 | for columnNumber, cell in enumerate(row):
13 | try:
14 | isFloat = True
15 | floatValue = float(cell)
16 | if columnNumber == 2:
17 | if floatValue >= 99:
18 | redList.append((rowNumber, columnNumber))
19 | elif floatValue >= 95:
20 | orangeList.append((rowNumber, columnNumber))
21 | else:
22 | greenList.append((rowNumber, columnNumber))
23 | except ValueError:
24 | isFloat = False
25 |
26 | # Align last two columns right
27 | if columnNumber > 0:
28 | RunPython.alignTableCellText(table1, rowNumber, columnNumber, PP_ALIGN.RIGHT)
29 |
30 | # Set appropriate list of cells to red
31 | RunPython.applyCellListFillRGB(table1, redList, 255, 0, 0)
32 |
33 | # Set appropriate list of cells to orange
34 | RunPython.applyCellListFillRGB(table1, orangeList, 255, 255, 0)
35 |
36 | # Set appropriate list of cells to green
37 | RunPython.applyCellListFillRGB(table1, greenList, 0, 255, 0)
38 |
39 |
--------------------------------------------------------------------------------
/test/threeGraphics.md:
--------------------------------------------------------------------------------
1 | pageTitleSize: 30
2 | sectionTitleSize: 48
3 | sectionSubtitleSize: 32
4 | numbers: no
5 | abstractTitle: Abstract
6 |
7 |
8 | # Even More Fun With DDF
9 | Martin Packer 🦕 , IBM
10 |
11 | ### Abstract
12 |
13 |
14 | |||
15 | |||
16 |
17 |
--------------------------------------------------------------------------------
/test/threeGraphics.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/threeGraphics.pptx
--------------------------------------------------------------------------------
/test/twoGraphics.md:
--------------------------------------------------------------------------------
1 | master: Martin Master.pptx
2 | pageTitleSize: 30
3 | sectionTitleSize: 48
4 | sectionSubtitleSize: 32
5 | numbers: no
6 | abstractTitle: Abstract
7 |
8 |
9 | # Even More Fun With DDF
10 | Martin Packer 🦕 , IBM
11 |
12 | ### Abstract
13 |
14 | * The idea of "alien" DB2 work coming into your system through DDF strikes fear into even the most seasoned Performance Specialist...
15 |
16 | * How will I classify it?
17 |
18 | * What will stop it from taking over my machine?
19 |
20 | * This presentation describes how to use performance data to address both of those questions, based on the author's recent experiences with numerous customers.
21 |
22 | * It also enables you to understand what applications and machines are issuing the DDF requests, improving your knowledge of the application landscape.
23 |
24 | * This presentation has substantially evolved in 2017.
25 |
26 | ### Class 1 CPU From Two Batteries Of 16 Websphere Application Servers
Not balanced within each battery or between batteries
27 |
28 | |||
29 |
30 |
--------------------------------------------------------------------------------
/test/twoGraphics.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/twoGraphics.pptx
--------------------------------------------------------------------------------
/test/videoSlide.md:
--------------------------------------------------------------------------------
1 | template: Martin Template.pptx
2 |
3 | ### Test Heading
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/videoSlide.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/videoSlide.pptx
--------------------------------------------------------------------------------
/test/waterdrop.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/test/waterdrop.mp4
--------------------------------------------------------------------------------