├── Drafts Actions └── Title Slide.draftsAction ├── LICENSE ├── Martin Template.pptx ├── README.md ├── card.py ├── colour.py ├── docs ├── CardSlide.png ├── Simple1.png ├── Simple2.png ├── Simple3.png ├── Simple4.png ├── WLM-Tiering.png ├── cardslide-lines.png ├── checklist.png ├── chevronSection.png ├── chevronTOC.png ├── circleSection.png ├── circleSectionNavigationButtons.png ├── circleTOC.png ├── colouredChecklist.png ├── funnel-slide.png ├── graphviz-rendered.png ├── horizontal-split-2-1.png ├── indentedChecklist.png ├── makedocs ├── plainTOC.png ├── python-graph-slide.png ├── python-table.example.png ├── selectedRemovedBullets.png ├── shapeSlide.png ├── two-up.md ├── user-guide.html ├── user-guide.log ├── user-guide.md ├── user-guide.mdp └── vertical-split-1-1.png ├── funnel.py ├── globals.py ├── md2pptx ├── md2pptx.py ├── paragraph.py ├── processingOptions.py ├── rectangle.py ├── runPython.py ├── run_pyto.py ├── symbols.py └── test ├── Battery W2M.png ├── Battery W3M.png ├── EnclaveCadence.svg ├── SVGtest.md ├── audiotest.m4a ├── cardTest.md ├── cardTest.pptx ├── chartdata.csv ├── codetest.md ├── codetest.pptx ├── fullPresentation.html ├── fullPresentation.md ├── fullPresentation.pptx ├── linktest.html ├── linktest.md ├── linktest.pptx ├── smartCells.md ├── smartCells.mdp ├── smartCells.pptx ├── smartCells.py ├── threeGraphics.md ├── threeGraphics.pptx ├── twoGraphics.md ├── twoGraphics.pptx ├── videoSlide.md ├── videoSlide.pptx └── waterdrop.mp4 /Drafts Actions/Title Slide.draftsAction: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/Drafts Actions/Title Slide.draftsAction -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Martin Packer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Martin Template.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/Martin Template.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # md2pptx 2 | Markdown to Powerpoint Converter 3 | 4 | **Note:** md2pptx only supports Python 3. So the installation instructions are for that. 5 | 6 | **Usage:** 7 | 8 | `python3 md2pptx output.pptx < input.markdown` 9 | 10 | or 11 | 12 | `md2pptx output.pptx < input.markdown` 13 | 14 | 15 | ### Supported Python Releases 16 | 17 | As was previously mentioned, Python 3 is required. 18 | 19 | Specifically python-pptx 20 | 21 | * requires 3.8 or later - to use the "Walrus Operator" 22 | * has been tested with 3.12 and alphas of 3.13 - with some changes made to handle issues 23 | 24 | #### Future Python Release Support 25 | 26 | Please note, and plan accordingly: 27 | 28 | * From 1 April, 2025 expect releases to only support 3.10 or later 29 | * From 1 April, 2026 expect releases to only support 3.11 or later 30 | 31 | md2pptx moves forward on Python every so often to: 32 | 33 | * Take advantage of new language capabilities 34 | * Remain on a supported Python release with adequate fix likelihood. 35 | 36 | ### Installation 37 | 38 | Installation is straightforward: 39 | 40 | 1. Install python-pptx 41 | 2. Clone md2pptx into a new directory 42 | 43 | The md2pptx repo includes all the essentials, such as funnel.py. You don't install these with eg pip. There are some optional packages, outlined in the User Guide. 44 | 45 | You can install python-pptx with 46 | 47 | `pip3 install python-pptx` 48 | 49 | (On a Raspberry Pi you might want to use `pip3` (or `python3 -m pip`) to install for Python 3.) 50 | 51 | You will probably need to issue the following command from the directory where you install it: 52 | 53 | `chmod +x md2pptx` 54 | 55 | ### Starting To Use md2pptx 56 | 57 | I would also suggest you start with a presentation that references Martin Template.pptx in the metadata (before the first blank line). \ 58 | Here is a very simple deck that does exactly that. 59 | 60 | ``` 61 | template: Martin Template.pptx 62 | 63 | # This Is A Presentation Title Page 64 | 65 | ## This Is A Presentation Section Page 66 | 67 | ### This Is A Bulleted List Page 68 | 69 | * One 70 | * One A 71 | * One B 72 | * Two 73 | 74 | Here are some slide notes. Note you leave an empty line between the content - in this case a bulleted list - and the notes. 75 | 76 | You can do multiple paragraphs and even use symbols. 77 | ``` 78 | 79 | ### Documentation 80 | 81 | As md2pptx has lots of function the documentation is a good place to discover it. 82 | 83 | See `docs/user-guide.html` or `docs/user-guide.md`. 84 | 85 | ### Issues & Suggestions 86 | 87 | This repo's Issues are regularly monitored. Use them for bug reports, suggestions, and questions. 88 | -------------------------------------------------------------------------------- /card.py: -------------------------------------------------------------------------------- 1 | """ 2 | card 3 | """ 4 | 5 | myVersion = "0.1" 6 | 7 | __version__ = myVersion 8 | 9 | class Card: 10 | def __init__( 11 | self, 12 | ): 13 | self.title = "" 14 | self.titleShape = None 15 | 16 | self.bullets = "" 17 | 18 | self.graphic = "" 19 | self.graphicShape = None 20 | self.graphicDimensions = None 21 | self.graphicTitle = None 22 | 23 | self.printableFilename = None 24 | 25 | # Both audio and video 26 | self.mediaInfo = None 27 | self.mediaDimensions = None 28 | self.mediaShape = None 29 | 30 | self.mediaURL = None 31 | 32 | self.backgroundShape = None 33 | self.backgroundTop = None 34 | 35 | self.bodyShape = None 36 | self.bodyTop = None 37 | 38 | self.top = None 39 | self.left = None 40 | 41 | -------------------------------------------------------------------------------- /colour.py: -------------------------------------------------------------------------------- 1 | """ 2 | colour 3 | """ 4 | 5 | from pptx.dml.color import RGBColor, MSO_THEME_COLOR 6 | import re 7 | 8 | RGBRegex = re.compile("#([0-9a-fA-F]{6})") 9 | 10 | def setColour(x, colour): 11 | colourType, colourValue = colour 12 | if colourType == "Theme": 13 | x.theme_color = colourValue 14 | else: 15 | x.rgb = RGBColor.from_string(colourValue[1:]) 16 | 17 | 18 | def parseThemeColour(value): 19 | value2 = value.upper() 20 | if value2 == "NONE": 21 | return MSO_THEME_COLOR.NOT_THEME_COLOR 22 | elif value2 == "ACCENT 1": 23 | return MSO_THEME_COLOR.ACCENT_1 24 | elif value2 == "ACCENT 2": 25 | return MSO_THEME_COLOR.ACCENT_2 26 | elif value2 == "ACCENT 3": 27 | return MSO_THEME_COLOR.ACCENT_3 28 | elif value2 == "ACCENT 4": 29 | return MSO_THEME_COLOR.ACCENT_4 30 | elif value2 == "ACCENT 5": 31 | return MSO_THEME_COLOR.ACCENT_5 32 | elif value2 == "ACCENT 6": 33 | return MSO_THEME_COLOR.ACCENT_6 34 | elif value2 == "BACKGROUND 1": 35 | return MSO_THEME_COLOR.BACKGROUND_1 36 | elif value2 == "BACKGROUND 2": 37 | return MSO_THEME_COLOR.BACKGROUND_2 38 | elif value2 == "DARK 1": 39 | return MSO_THEME_COLOR.DARK_1 40 | elif value2 == "DARK 2": 41 | return MSO_THEME_COLOR.DARK_2 42 | elif value2 == "FOLLOWED HYPERLINK": 43 | return MSO_THEME_COLOR.FOLLOWED_HYPERLINK 44 | elif value2 == "HYPERLINK": 45 | return MSO_THEME_COLOR.HYPERLINK 46 | elif value2 == "LIGHT 1": 47 | return MSO_THEME_COLOR.LIGHT_1 48 | elif value2 == "LIGHT 2": 49 | return MSO_THEME_COLOR.LIGHT_2 50 | elif value2 == "TEXT 1": 51 | return MSO_THEME_COLOR.TEXT_1 52 | elif value2 == "TEXT 2": 53 | return MSO_THEME_COLOR.TEXT_2 54 | elif value2 == "MIXED": 55 | return MSO_THEME_COLOR.MIXED 56 | 57 | def parseColour(value): 58 | if value[0] == "#": 59 | return ("RGB", value) 60 | else: 61 | return ("Theme", parseThemeColour(value)) 62 | 63 | def parseRGB(str): 64 | if RGBmatch := RGBRegex.match(str): 65 | # Matches 66 | return (True, RGBmatch.group(1)) 67 | else: 68 | return (False, "") 69 | 70 | -------------------------------------------------------------------------------- /docs/CardSlide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/CardSlide.png -------------------------------------------------------------------------------- /docs/Simple1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/Simple1.png -------------------------------------------------------------------------------- /docs/Simple2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/Simple2.png -------------------------------------------------------------------------------- /docs/Simple3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/Simple3.png -------------------------------------------------------------------------------- /docs/Simple4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/Simple4.png -------------------------------------------------------------------------------- /docs/WLM-Tiering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/WLM-Tiering.png -------------------------------------------------------------------------------- /docs/cardslide-lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/cardslide-lines.png -------------------------------------------------------------------------------- /docs/checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/checklist.png -------------------------------------------------------------------------------- /docs/chevronSection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/chevronSection.png -------------------------------------------------------------------------------- /docs/chevronTOC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/chevronTOC.png -------------------------------------------------------------------------------- /docs/circleSection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/circleSection.png -------------------------------------------------------------------------------- /docs/circleSectionNavigationButtons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/circleSectionNavigationButtons.png -------------------------------------------------------------------------------- /docs/circleTOC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/circleTOC.png -------------------------------------------------------------------------------- /docs/colouredChecklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/colouredChecklist.png -------------------------------------------------------------------------------- /docs/funnel-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/funnel-slide.png -------------------------------------------------------------------------------- /docs/graphviz-rendered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/graphviz-rendered.png -------------------------------------------------------------------------------- /docs/horizontal-split-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/horizontal-split-2-1.png -------------------------------------------------------------------------------- /docs/indentedChecklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/indentedChecklist.png -------------------------------------------------------------------------------- /docs/makedocs: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # makedocs 4 | # -------- 5 | # 6 | # Builds the md2pptx user guide, creating the Markdown .md and log .log 7 | # file in the process. It doesn't convert the Markdown to HTML. I use 8 | # Sublime Text for that. There are no quirks in the HTML generated so you could 9 | # probably use anything - such as Marked. 10 | # 11 | # To do the build you will need mdpre - https://github.com/MartinPacker/mdpre 12 | # 13 | # If you are on Windows you should still be able to run the mdpre command, but the 14 | # syntax for specifying input and output files is probably a little different. 15 | 16 | mdpre -v < user-guide.mdp > user-guide.md 2> user-guide.log 17 | -------------------------------------------------------------------------------- /docs/plainTOC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/plainTOC.png -------------------------------------------------------------------------------- /docs/python-graph-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/python-graph-slide.png -------------------------------------------------------------------------------- /docs/python-table.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/python-table.example.png -------------------------------------------------------------------------------- /docs/selectedRemovedBullets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/selectedRemovedBullets.png -------------------------------------------------------------------------------- /docs/shapeSlide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinPacker/md2pptx/d0d1265c4f671320b48a29964de9f7a5687eb45e/docs/shapeSlide.png -------------------------------------------------------------------------------- /docs/two-up.md: -------------------------------------------------------------------------------- 1 | template: Martin Template.pptx 2 | 3 | ### Workload Manager Policies Should Follow The Model Policy 4 | 5 | 6 | 7 | ![](WLM-Tiering.png) 8 | 9 | * The tiering doesn't 100% imply Importance levels 10 | * But pretty close 11 | * Some middleware might not be present 12 | * For example MQ 13 | * DDF work is difficult to fit into this scheme 14 | * But remarkably flexibly classifiedclassifiable -------------------------------------------------------------------------------- /docs/user-guide.log: -------------------------------------------------------------------------------- 1 | 2 | mdpre Markdown Preprocessor v0.6.9 (17 September, 2024) 3 | ======================================================= 4 | - opened for writing 5 | Def mdpre_date = 17 September, 2024 6 | Def mdpre_level = 0.6.9 7 | Def userid = martinpacker 8 | Def time = 11:18 9 | Def date = 14 April, 2025 10 | Def TOC = Table Of Contents 11 | Def md = Markdown 12 | Def pp = Powerpoint 13 | Markdown To Powerpoint User Guide 14 | Table Of Contents - spec '2 * Table Of Contents' 15 | 2 6 Table Of Contents 16 | ..... Why md2pptx? 17 | ..... ..... A Real World Use Case 18 | ..... ..... Using md2pptx With mdpre 19 | ..... How Do You Use md2pptx? 20 | ..... ..... Installation 21 | ..... ..... Updating 22 | ..... ..... Use 23 | ..... python-pptx license 24 | ..... Change Log 25 | Column Alignment - spec 'l r l' 26 | CSV Start 27 | CSV Stop 28 | ..... Creating Slides 29 | ..... ..... Presentation Title Slides 30 | ..... ..... Presentation Section Slides 31 | ..... ..... Bullet Slides 32 | ..... ..... ..... Numbered List Items 33 | ..... ..... Graphics, Video And Audio slides 34 | ..... ..... ..... Graphics Slides 35 | ..... ..... ..... ..... Clickable Pictures 36 | ..... ..... ..... ..... Graphics File References 37 | ..... ..... ..... Video And Audio Slides 38 | ..... ..... ..... ..... Video Slides 39 | ..... ..... ..... ..... Audio Slides 40 | ..... ..... Table Slides 41 | ..... ..... ..... Special Case: Two Graphics Side By Side 42 | ..... ..... ..... Special Case: Two By Two Grid Of Graphics 43 | ..... ..... ..... Special Case: Three Graphics On A Slide 44 | ..... ..... ..... Special Case: One Graphic Above Another 45 | ..... ..... ..... Multi-Column Table Cells 46 | ..... ..... Card Slides 47 | ..... ..... ..... Card Titles 48 | ..... ..... ..... Card Graphics, Video, & Audio 49 | ..... ..... Code Slides 50 | ..... ..... ..... `` 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 | 010020030040050060070080090017th 0317th 0617th 0917th 1217th 1417th 1717th 2017th 2318th 0218th 0518th 0818th 1118th 1318th 1618th 1918th 2219th 0119th 0419th 0719th 1019th 12:3019th 1519th 1819th 2120th 0020th 0320th 0620th 0920th 1220th 1420th 1720th 2020th 2321th 0221th 0521th 0821th 1121th 1321th 1621th 1921th 2222th 0122th 0422th 0722th 1022th 12:3022th 1522th 1822th 21DDF Transaction Rates By SubsystemAll DaysPDB3 on SYSCPDB4 on SYSDPDB5 on SYSAPDB6 on SYSB -------------------------------------------------------------------------------- /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 | ![](EnclaveCadence.svg) 15 | 16 | ### Web SVG Sample 17 | 18 | ![](https://cdn.shopify.com/s/files/1/0496/1029/files/Freesample.svg?5153) 19 | 20 | ### Web PNG Example 21 | 22 | ![](https://ichef.bbci.co.uk/live-experience/cps/624/cpsprodpb/vivo/live/images/2021/1/31/4405412f-3c56-486a-9fda-77b1633e2dbe.jpg) 23 | 24 | 25 | ### PNG file 26 | 27 | ![](Battery W3M.png) 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 | 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 |
BucketMinimum % Of GoalMaximum % of GoalPI Value
10500.5
250600.6
360700.7
470800.8
580900.9
6901001.0
71001101.1
81101201.2
91201301.3
101301401.4
111401501.5
121502002.0
132004004.0
144004.0
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 |
    12 |
  • Here is bold text with a hyperlink1 in it - IBM
  • 13 |
14 | 15 |
16 |
17 |
    18 | 19 |
  1. 20 |

    Hyperlink was problematic [ This is after open  ↩

    21 |
  2. 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 | |![](Battery W2M.png)|| 15 | |![](Battery W2M.png)|![](Battery W3M.png)| 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 | |![](Battery W2M.png)|![](Battery W3M.png)| 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 --------------------------------------------------------------------------------