""" * Version: 0.1.0.0.0 * Purposes: * Generates PowerPoint from Markdown files. * Workflow: 1. Read MD file to gather the statistics of the view. 2. Advice a suggested presentation layout in the master template based on the statistics. 3. Dynamically places the code area based on the statistics. 4. Smartly places images based on the statistics. (One and two images are recommended, at most tree in special situations) 5. Smartly places table and resizes table cells based on the statistics. """ import platform, re, os from PIL import Image from pptx import Presentation from pptx.enum.text import PP_PARAGRAPH_ALIGNMENT from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.dml import MSO_THEME_COLOR from pptx.util import Pt import pingcap.pe.flash.util as util import pingcap.pe.flash.util_advisor as advisor import pingcap.pe.flash.ai_editor as editor import pingcap.pe.flash.markdown_processor as mp def _add_version_number(version_number: str, slide): textbox = slide.shapes.add_textbox(Pt(640), Pt(-8), Pt(60), Pt(30)) text_frame = textbox.text_frame p = text_frame.add_paragraph() r = p.add_run() font = r.font font.name = "DM Sans" font.size = Pt(14) font.bold = True font.color.rgb = RGBColor.from_string("FFFFFF") r.text = "v" + version_number def _add_code_line_v2(ch: int, paragraph, code_line: str, code_font_size=8, hints=None): # font_name = "Fira Code Regular" font_name = "Fira Code" highlight_font_name = "Fira Code" if ch > 0: highlight_begin_pattern = "~!@" highlight_end_pattern = "@!~" mb = re.findall(highlight_begin_pattern, code_line) me = re.findall(highlight_end_pattern, code_line) if mb != None and me != None and len(mb) == len(me) == 1: start_line = code_line.split(highlight_begin_pattern)[0] remain = code_line.split(highlight_begin_pattern)[1] keyword = remain.split(highlight_end_pattern)[0] end_line = remain.split(highlight_end_pattern)[1] line = [start_line, keyword, end_line] pos = 0 for section in line: run = paragraph.add_run() run.text = section font = run.font font.size = Pt(code_font_size) font.italic = None if pos == 1: font.name = highlight_font_name font.color.theme_color = MSO_THEME_COLOR.ACCENT_6 font.bold = True else: font.name = font_name font.color.rgb = RGBColor.from_string("F0F0F0") font.bold = True pos += 1 else: run = paragraph.add_run() run.text = code_line font = run.font font.name = font_name font.size = Pt(code_font_size) font.italic = None font.color.rgb = RGBColor.from_string("F0F0F0") font.bold = True def _post_process_line(input_line: str) -> str: line = input_line.replace('
', "").replace("
", "") return line def _add_bullet_line( paragraph, text_line, hints, font_size=None, mode="standard", bold=False ): """ Handles bulletin line and inline codings (` pair) within it. <= Remove them. Handles bulletin line and inline bold (** pair) within it. <= Bold them. """ print(util.speak_result("bullet_line_in: " + text_line)) s3_replace_counter = 0 for _ in re.findall("\*\*\*", text_line): # *** *** s3_replace_counter += 1 if s3_replace_counter % 2 == 0: text_line = text_line.replace("***", "*^^^", 1) else: text_line = text_line.replace("***", "^^^*", 1) text_line = text_line.replace("**", "^^^").replace("`", "") text_runs = text_line.split("^^^") pos = 0 first_run_remove = False punct_pattern = "^[,;:.,、;:。]" # run (r) is the phrase separated by highligh markers. for r in text_runs: r = advisor.apply_text_format_rules(r, hints) run1 = paragraph.add_run() run2 = paragraph.add_run() run3 = paragraph.add_run() if (r == None or len(r) == 0) and pos == 0: first_run_remove = True if pos % 2 != 0: font = run2.font if r.strip(" ").startswith("*") and r.strip(" ").endswith( "*" ): # *** *** follow up r = r.strip(" ").strip("*") font.italic = True elif r.strip(" ").startswith("_") and r.strip(" ").endswith( "_" ): # **_ _** follow up r = r.strip(" ").strip("_") font.fill.solid() font.color.rgb = RGBColor.from_string("8A1214") font.underline = True font.italic = False else: font.italic = False if hints in ["zh", "en2zh"]: font.name = "Noto Sans SC" font.bold = True font_size = 11 elif hints in ["en", "zh2en"]: font.name = "DM Sans" font.bold = True elif hints in ["ja", "en2ja"]: font.name = "Noto Sans JP" font.bold = True font_size = 10.5 # font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 if font_size != None: run1.font.size = Pt(font_size) run2.font.size = Pt(font_size) run3.font.size = Pt(font_size) if pos == 1: if first_run_remove: if len(re.findall(punct_pattern, text_runs[pos + 1])) > 0: run2.text = r.strip() else: run2.text = r.strip() run3.text = " " else: if len(re.findall(punct_pattern, text_runs[pos + 1])) > 0: run1.text = " " run2.text = r.strip() else: run1.text = " " run2.text = r.strip() run3.text = " " else: if len(re.findall(punct_pattern, text_runs[pos + 1])) > 0: run1.text = " " run2.text = r.strip() else: run1.text = " " run2.text = r.strip() run3.text = " " if bold: font.bold = True else: if hints != None and hints == "en2zh" and len(r) > 0: ppl = _post_process_line(r).strip() elif hints != None and hints == "zh2en" and len(r) > 0: ppl = _post_process_line(r).strip() else: ppl = _post_process_line(r).strip() run2.text = ppl font = run2.font if hints in ["zh", "en2zh"]: font.name = "Noto Sans SC" font_size = 11 elif hints in ["en", "zh2en"]: font.name = "DM Sans" elif hints in ["ja", "en2ja"]: font.name = "Noto Sans JP" font_size = 10.5 if font_size != None: run2.font.size = Pt(font_size) font.bold = False font.italic = None if bold: font.bold = True pos += 1 print(util.speak_result("bullet_line_out: " + str(text_runs))) def _add_md_file_info(slide, md_file_path): slide_desc_shape = slide.shapes.add_shape( MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Pt(10), Pt(370), Pt(7 * len(md_file_path)), Pt(12), ) pic_desc = slide_desc_shape.text_frame pic_desc.clear() pic_desc_run = pic_desc.paragraphs[0].add_run() pic_desc_run.text = md_file_path font = pic_desc_run.font font.name = "Fira Code" font.size = Pt(7) font.italic = True font.color.rgb = RGBColor.from_string("FFFF00") font.bold = True def _add_picture( slide, md_native_picture_path, stats, pos_hint, component_left_margin, slide_base_top_margin, bullet_normalized_unit_height, inspect=False, md_file_path=None, use_most_current_version=True, ): """ _add_picture() can handle pos_hint: [`smart`|`left`|`center`|`right`|`background`] """ picture_path = md_native_picture_path.split("(")[1].replace(")", "") try: if not picture_path.startswith("./") and not picture_path.startswith("/"): picture_path = "./student-guide/diagram/" + picture_path if use_most_current_version: picture_basename = picture_path.replace("./student-guide/diagram/", "") picture_partition = picture_basename.split("_") picture_version = picture_partition[2] picture_current_version_number = int(picture_version.lstrip("0")) picture_max_version_number = picture_current_version_number found_version_number = None for f in os.listdir("./student-guide/diagram/"): if f.startswith( picture_partition[0] + "_" + picture_partition[1] ) and f.endswith(picture_partition[3]): found_version_number = int(f.split("_")[2].lstrip("0")) if found_version_number > picture_max_version_number: picture_max_version_number = found_version_number picture_max_version = str(picture_max_version_number).zfill(3) picture_path = ( "./student-guide/diagram/" + picture_partition[0] + "_" + picture_partition[1] + "_" + picture_max_version + "_" + picture_partition[3] ) if picture_max_version != picture_version: print("ppt_creator: Newer picture used - " + picture_path) fr = open(md_file_path, "r") lines = fr.read() fr.close() fb = open(md_file_path + ".bak", "x") fb.write(lines) fb.close() f = open(md_file_path, "w") write_lines = [] for line in lines.split("\n"): if not line.startswith( "![`" + pos_hint + "`](" + picture_basename + ")" ) and not line.startswith( "![`" + pos_hint + "`](" + picture_path + ")" ): write_lines += [line + "\n"] else: write_lines += [ "![`" + pos_hint + "`](" + picture_path.replace("./student-guide/diagram/", "") + ")\n" ] f.write("".join(write_lines)) f.close() os.remove(md_file_path + ".bak") img = Image.open(picture_path) if pos_hint == "background": slide.shapes[0].insert_picture(picture_path) return pic_pos_attrs = advisor.advise_picture_attributes( stats, pos_hint, component_left_margin, slide_base_top_margin, bullet_normalized_unit_height, img, ) if pic_pos_attrs[2] + pic_pos_attrs[3] == 0: print("Error: Cannot decide neither width nor height of the picture.") exit(-1) width = pic_pos_attrs[2] if width > 0: slide.shapes.add_picture( picture_path, Pt(pic_pos_attrs[0]), Pt(pic_pos_attrs[1]), width=Pt(pic_pos_attrs[2]), ) else: slide.shapes.add_picture( picture_path, Pt(pic_pos_attrs[0]), Pt(pic_pos_attrs[1]), height=Pt(pic_pos_attrs[3]), ) if inspect: pic_desc_shape = slide.shapes.add_shape( MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Pt(pic_pos_attrs[0]), Pt(pic_pos_attrs[1] - 9), Pt(4 * len(picture_path)), Pt(30), ) pic_desc = pic_desc_shape.text_frame pic_desc.clear() pic_desc_run = pic_desc.paragraphs[0].add_run() pic_desc_run.text = picture_path.replace( "./student-guide/diagram/", "" ).replace(".png", "") font = pic_desc_run.font font.name = "Fira Code" font.size = Pt(10) font.italic = True font.color.rgb = RGBColor.from_string("FFFF00") font.bold = True except FileNotFoundError as ex: print("ppt_creator: \t", ex) def _add_link(slide, view_line): bulletin_shape = slide.shapes[1] tf = bulletin_shape.text_frame p = tf.add_paragraph() r = p.add_run() r.font.bold = True r.font.italic = True r.text = view_line[1].strip("`") hlink = r.hyperlink hlink.address = re.findall("\]\((.*)\)", view_line[2])[0] p.level = 1 def _add_slide_title(slide, view_line, en_1gram_terms_list): title_shape = slide.shapes.title # Remove Flash prefix if any actual_title = advisor.apply_text_format_rules( re.sub( "`VERSION::[^ ]*` ", "", re.sub("`PARAMETER::[^ ]*` ", "", view_line[1]) ), None, ) title_shape.text = advisor.pretty_format_title(actual_title, en_1gram_terms_list) def _pretty_format_notes(note_line: str) -> str: # Remove additional white spaces compress_space = re.sub(" {1,}", " ", note_line).strip() return compress_space def create_chapter_ppt_batch(template_path: str, hints: str, team) -> list: """ Call create_chapter_ppt in loop. :param template_path: The empty PowerPoint to use as the template, which contains the required layouts. See decide_slide_layout(). :param hints: As a supplemental infomation passed in to tell this routine what hint to use. Currently, the language code is the only hint in action. 1. zh 2. en 3. zh2en 4. en2zh :param team: Team name (or tag), such as pe, doc, tse, etc. """ if platform.system() == "Windows": view_folder_base = ".\\student-guide\\view\\" + hints else: view_folder_base = "./student-guide/view/" + hints chapter_folders = sorted(os.listdir(view_folder_base)) batch_output_list = [] for chapter_folder in chapter_folders: chapter_folder_pattern_matches = re.findall( "^chapter-[0-9][0-9]", chapter_folder ) if ( chapter_folder_pattern_matches != None and len(chapter_folder_pattern_matches) > 0 and not chapter_folder.endswith("-lms") ): chapter_markdown_folder = view_folder_base + "/" + chapter_folder chapter_content = mp.parse_markdown_folder(chapter_markdown_folder) dest_file_path = "./stage/" + chapter_folder + "-" + hints + ".pptx" create_chapter_ppt( chapter_content, dest_file_path, template_path, None, hints, team, None ) batch_output_list += [dest_file_path] return batch_output_list def create_chapter_ppt( chapter: list, output_ppt_path: str, template_path: str, chno: str, hints: str, team="pe", prs=None, inspect_mode="false", ) -> Presentation: """ Compose PowerPoint from Markdown files, and save it to disk path. :param chapter: A list of tuple generated by parse_markdown_folder(). :param output_ppt_path: The PowerPoint output path with filename. :param template_path: The empty PowerPoint to use as the template, which contains the required layouts. See decide_slide_layout(). :param chno: Assign a chapter number for the targeting PowerPoint. Can be None. :param hints: As a supplemental infomation passed in to tell this routine what hint to use. Currently, the language code is the only hint in action. 1. zh 2. en 3. zh2en 4. en2zh 5. en2ja 6. ja2en :param team: Team name (or tag), such as pe, doc, tse, etc. Default is `pe`. :returns: The constructed PowerPoint presentation. """ inspect = False if inspect_mode.lower() == "true": inspect = True component_left_margin = 64 # Pt slide_base_top_margin = 73 # Pt bullet_normalized_unit_height = 22.2 # Pt codearea_normalized_unit_width = 5 # Pt codearea_box_margin = 5 # Pt codearea_minimal_height = 38 # Pt codearea1_font_size = 8 # Default font size for code area 1 codearea2_font_size = 8 # Default font size for code area 2 cell_normalized_unit_width_for_nonheader_max_column = 0.1 # Inches en_1gram_terms_list = util.load_1gram_terms_en(team) need_save = True if hints == None: print("ALERT: ppt_creator.create_chapter_ppt: hints: NO") if prs == None: prs = Presentation(template_path) else: need_save = False close_presented = False summary_presented = False objectives_cache = None for view in chapter: image_counter = 0 codearea_counter = 0 if view[2].strip("\n").endswith("99close.md"): close_presented = True if view[2].strip("\n").endswith("98summary.md"): summary_presented = True if ( view[2].strip("\n").endswith("02.md") or view[2].strip("\n").endswith("03.md") ) and hints in ["en", "zh2en", "ja2en"]: if ( view[0][0][1].lower().startswith("objectives") or view[0][0][1].lower().startswith("**objectives") or view[0][0][1].lower().startswith("learning objectives") ): objectives_cache = view elif ( view[2].strip("\n").endswith("02.md") or view[2].strip("\n").endswith("03.md") ) and hints in ["zh", "en2zh"]: if ( view[0][0][1].startswith("目标") or view[0][0][1].lower().startswith("**目标") or view[0][0][1].startswith("学习目标") ): objectives_cache = view elif ( view[2].strip("\n").endswith("02.md") or view[2].strip("\n").endswith("03.md") ) and hints in ["ja", "en2ja"]: if view[0][0][1].startswith("目標") or view[0][0][1].lower().startswith( "**目標" ): objectives_cache = view # Choose the slide layout. stats = advisor.analyze_view(view) print("slide_stats:", stats["info"]) suggested_layout_idx = advisor.advise_slide_layout_idx(stats) layout = prs.slide_layouts[suggested_layout_idx] slide = prs.slides.add_slide(layout) print("slide_layout_idx:", suggested_layout_idx) # Bullet cleaner. if ( stats["info"]["BH"] == 0 and stats["info"]["CH"] + stats["info"]["TR"] + stats["image_count"] > 0 and stats["url_link_count"] == 0 ): bulletin_shape = slide.shapes[1] bulletin_shape.text_frame.clear() slide.shapes.element.remove(bulletin_shape.element) # Codearea preparation. codearea_paragraph = None codearea_text_frame1 = None codearea_text_frame2 = None if stats["info"]["CH"] > 0: # At least 1 codearea top_amplifier = 1.05 left = component_left_margin - 28 if stats["info"]["CC"] == 2: left -= 40 bh = stats["info"]["BH"] if stats["parameter_view"]: bh += 3 top = ( slide_base_top_margin + bh * bullet_normalized_unit_height ) * top_amplifier # Title margin + BH width = ( 2 * codearea_box_margin + stats["info"]["CW"] * codearea_normalized_unit_width ) if ( stats["image_count"] == 0 and stats["table_count"] == 0 and stats["info"]["CC"] == 1 ): width = 680 - left height = stats["info"]["CH"] * 13 # Area margin + CH if (height + top) > 376 or stats["info"]["CW"] > 114: print( "Risk: codearea & bullet overflow - ", stats["info"]["MD"], "BH+CH", str(bh) + "+" + str(stats["info"]["CH"]), ) if ( stats["info"]["CH"] + bh ) > 25: # Please add more higher condition above. # TODO if bh < 10: top -= 8 if bh > 7 or stats["info"]["CH2"] > 23: codearea1_font_size = 6 height = height * 0.6 if stats["info"]["CC"] == 2: width = width * 0.6 else: codearea1_font_size = 7 height = height * 0.7 if stats["info"]["CC"] == 2: width = width * 0.7 elif ( stats["info"]["CH"] + bh > 16 ): # Shrink code area font size condition 1. if bh > 8 or stats["info"]["CH"] > 19: codearea1_font_size = 6 height = height * 0.7 if stats["info"]["CC"] == 2: width = width * 0.7 else: codearea1_font_size = 7 height = height * 0.8 if stats["info"]["CC"] == 2: width = width * 0.8 try_height1 = 376 - top if try_height1 < height: height = try_height1 if height < codearea_minimal_height: height = codearea_minimal_height codearea_background = slide.shapes.add_shape( MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Pt(left), Pt(top), Pt(width), Pt(height), ) codearea_background.fill.solid() # codearea_background.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_2 codearea_background.fill.fore_color.rgb = RGBColor.from_string("262626") codearea_background.adjustments[0] = 0.05 # Codearea Shape codearea_textbox1 = slide.shapes.add_textbox( Pt(left + 5), Pt(top + 5), Pt(width - 10), Pt(height - 10) ) codearea_text_frame1 = codearea_textbox1.text_frame if stats["info"]["CC"] > 1: # At least two codeareas presented. left2 = left + width + 10 width2 = ( 2 * codearea_box_margin + stats["info"]["CW2"] * codearea_normalized_unit_width ) height2 = stats["info"]["CH2"] * 13 # Area margin + CH if (height2 + top) > 376 or stats["info"]["CW2"] > 114: print( "Risk: codearea2 & bullet overflow - ", stats["info"]["MD"], "BH+CH2", str(stats["info"]["BH"]) + "+" + str(stats["info"]["CH2"]), ) if ( stats["info"]["CH2"] + stats["info"]["BH"] > 25 ): # Please add more higher condition above. # TODO if stats["info"]["BH"] > 7 or stats["info"]["CH2"] > 23: codearea2_font_size = 6 height2 = height2 * 0.6 width2 = width2 * 0.6 else: codearea2_font_size = 7 height2 = height2 * 0.7 width2 = width2 * 0.7 elif ( stats["info"]["CH2"] + stats["info"]["BH"] > 16 ): # Shrink code area font size condition 1. if stats["info"]["BH"] > 8 or stats["info"]["CH2"] > 19: codearea2_font_size = 6 height2 = height2 * 0.7 width2 = width2 * 0.7 else: codearea2_font_size = 7 height = height * 0.8 width = width * 0.8 try_height2 = 376 - top if try_height2 < height2: height2 = try_height2 if height2 < codearea_minimal_height: height2 = codearea_minimal_height codearea_background = slide.shapes.add_shape( MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Pt(left2), Pt(top), Pt(width2), Pt(height2), ) codearea_background.fill.solid() # codearea_background.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_2 codearea_background.fill.fore_color.rgb = RGBColor.from_string("262626") codearea_background.adjustments[0] = 0.05 # Codearea Shape codearea_textbox = slide.shapes.add_textbox( Pt(left2 + 5), Pt(top + 5), Pt(width2 - 10), Pt(height2 - 10) ) codearea_text_frame2 = codearea_textbox.text_frame # Handling slide for view_line in view[0]: if view_line[0] == "chapter_title:main": # Chapter Title Main title_line = view_line[1] if chno != None: title_line = "Chapter " + chno + ": " + title_line slide.shapes.title.text = advisor.pretty_format_title( title_line, en_1gram_terms_list ) elif view_line[0] == "chapter_subtitle:main": # Chapter Sub Title Main subtitle_line = view_line[1] bulletin_shape = slide.shapes[1] tf = bulletin_shape.text_frame p = tf.add_paragraph() r = p.add_run() r.text = advisor.apply_text_format_rules(subtitle_line, None) elif view_line[0].startswith("picture") or view_line[0].startswith( "background" ): # Picture. _add_picture( slide, view_line[2], stats, view_line[0].split(":")[1], component_left_margin, slide_base_top_margin, bullet_normalized_unit_height, inspect, view[2], ) elif view_line[0] == "title:slide": # Slide title. _add_slide_title(slide, view_line, en_1gram_terms_list) elif view_line[0] == "link:url": # URL link. _add_link(slide, view_line) elif view_line[0].startswith("bulletin"): # Bullet line. bulletin_shape = slide.shapes[1] tf = bulletin_shape.text_frame p = tf.add_paragraph() if view_line[1] == None: continue else: _add_bullet_line( p, advisor.pretty_format_bulletin( view_line[1], en_1gram_terms_list, md2md=False, mode="silent", ), hints, None, mode="silent", ) bullet_level = int(view_line[0].split(":")[1]) if bullet_level > 0: p.level = bullet_level elif view_line[0].startswith("code_area"): # Code area. if view_line[0] == "code_area:end": if codearea_counter == 1: codearea1_font_size = 8 elif codearea_counter == 2: codearea2_font_size = 8 codearea_paragraph = None elif view_line[0] == "code_area:begin": codearea_counter += 1 if ( view_line[1] != None and len(view_line[1]) > 0 and view_line[1] != "\n" ): if codearea_counter == 1: codearea1_font_size = int(view_line[1].strip()) elif codearea_counter == 2: codearea2_font_size = int(view_line[1].strip()) elif view_line[0] == "code_area:body": if codearea_counter == 1: codearea_paragraph = codearea_text_frame1.paragraphs[0] elif codearea_counter == 2: codearea_paragraph = codearea_text_frame2.paragraphs[0] if codearea_counter == 1: _add_code_line_v2( stats["info"]["CH"], codearea_paragraph, view_line[1], codearea1_font_size, hints, ) elif codearea_counter == 2: _add_code_line_v2( stats["info"]["CH2"], codearea_paragraph, view_line[1], codearea2_font_size, hints, ) # Handling version_view for the slide if stats["info"]["VN"] != 0: _add_version_number(stats["info"]["VN"], slide) # Handling table on slide if len(view) > 3 and view[3] != None and len(view[3]) > 0: # Table presents. # x, y, cx, cy = Inches(1), Inches(2), Inches(7), Inches(1) table_suggested_pos = advisor.advise_table_attributes( stats, component_left_margin, slide_base_top_margin, bullet_normalized_unit_height, 1, ) table_col_count = len(view[3][0][1]) table_row_count = len(view[3]) table_shape = slide.shapes.add_table( table_row_count, table_col_count, Pt(table_suggested_pos[0]), Pt(table_suggested_pos[1]), width=Inches(0.0), height=Inches(0.0), ) table = table_shape.table # Use Dark theme table_shape._element.graphic.graphicData.tbl[0][ -1 ].text = "{125E5076-3810-47DD-B79F-674D7AD40C01}" row_idx = 0 # Column settings column_settings = [] for row in view[3]: if row[0] == "table:column_settings": for cell_value in row[1]: if cell_value.strip().startswith( ":" ) and cell_value.strip().endswith(":"): column_settings += [PP_PARAGRAPH_ALIGNMENT.CENTER] elif cell_value.strip().startswith(":"): column_settings += [PP_PARAGRAPH_ALIGNMENT.LEFT] elif cell_value.strip().endswith(":"): column_settings += [PP_PARAGRAPH_ALIGNMENT.RIGHT] for row in view[3]: col_idx = 0 if row[0] in ["table:header", "table:row"]: for cell_value in row[1]: cell = table.cell(row_idx, col_idx) cell.text_frame.clear() c_paragraph = cell.text_frame.paragraphs[0] c_paragraph.alignment = column_settings[col_idx] if row[0] == "table:header": _add_bullet_line( c_paragraph, advisor.pretty_format_title( cell_value.strip(), en_1gram_terms_list ), hints, 9, mode="silent", bold=True, ) elif row[0] == "table:row": _add_bullet_line( c_paragraph, cell_value.strip(), hints, 8, mode="silent", bold=False, ) col_idx += 1 row_idx += 1 # Remove the trailing empty table._tbl.remove(table.rows[row_idx]._tr) for c_idx in range(0, len(stats["info"]["TCML"])): factor = 1.0 if stats["info"]["TCML"][c_idx][1]: factor = 1.0 else: if (stats["info"]["TCML"][c_idx][0] * 0.4) > stats["info"]["TCAL"][ c_idx ]: factor = 0.4 elif (stats["info"]["TCML"][c_idx][0] * 0.5) > stats["info"][ "TCAL" ][c_idx]: factor = 0.5 elif (stats["info"]["TCML"][c_idx][0] * 0.6) > stats["info"][ "TCAL" ][c_idx]: factor = 0.7 elif (stats["info"]["TCML"][c_idx][0] * 0.7) > stats["info"][ "TCAL" ][c_idx]: factor = 0.8 else: factor = 1.0 table.columns[c_idx].width = Inches( (stats["info"]["TCML"][c_idx][0] * factor) * cell_normalized_unit_width_for_nonheader_max_column ) # Handling notes if view[1] != None and len(view[1]) > 0: notes_slide = slide.notes_slide text_frame = notes_slide.notes_text_frame paragraph = text_frame.paragraphs[0] run = paragraph.add_run() notes = "".join(view[1]) processed_text = notes.replace('
', "").replace( "
", "" ) run.text = _pretty_format_notes(processed_text.replace("`", "")) font = run.font font.name = "Courier New" if hints.endswith("ja"): font.name = "Noto Sans JP" font.size = Pt(9) # Adding MD file info for inspection if inspect: _add_md_file_info(slide, view[2]) # Append the "Summary" if any if not summary_presented: print(util.speak_result("bullet_line_in: Generating summary for presentation.")) if hints in ["en", "zh2en", "ja2en"]: summary_title = "Summary" elif hints in ["ja", "en2ja", "zh2ja"]: summary_title = "まとめ" elif hints in ["zh", "en2zh"]: summary_title = "总结" if objectives_cache: summary = prs.slides.add_slide(prs.slide_layouts[13]) summary.shapes.title.text = advisor.pretty_format_title( summary_title, en_1gram_terms_list ) ja_summary_bullet = [] for view_line in objectives_cache[0]: if view_line[0].startswith("bulletin"): bulletin_shape = summary.shapes[1] tf = bulletin_shape.text_frame p = tf.add_paragraph() b = view_line[1] ja_summary_bullet += [b] if hints in ["en2ja", "zh2ja"]: b = editor.convert_ja_to_past_tense(b).replace("。", "") elif hints == "ja": b = b.replace("。", "") b = b.replace("します", "しました") b = b.replace("めます", "めました") _add_bullet_line( p, advisor.pretty_format_bulletin( b, en_1gram_terms_list, md2md=False ), hints, None, mode="silent", ) bullet_level = int(view_line[0].split(":")[1]) if bullet_level > 0: p.level = bullet_level # Append the "Thanks" if any if not close_presented: ending = prs.slides.add_slide(prs.slide_layouts[5]) if hints in ["en", "zh2en", "ja2en"]: ending.shapes.title.text = advisor.pretty_format_title( "Thank You", en_1gram_terms_list ) elif hints in ["ja", "en2ja"]: ending.shapes.title.text = advisor.pretty_format_title( "ありがとうございました", en_1gram_terms_list ) elif hints in ["zh", "en2zh"]: ending.shapes.title.text = advisor.pretty_format_title( "谢谢!", en_1gram_terms_list ) else: ending.shapes.title.text = advisor.pretty_format_title( "Thank You", en_1gram_terms_list ) # Save the PowerPoint? if need_save: prs.save(output_ppt_path) return prs def create_course_ppt( template_path: str, hints: str, team: str, inspect_mode="false" ) -> str: """ Pack PowerPoint from Markdown files from all chapter folders, and save it to disk path. :param template_path: The empty PowerPoint to use as the template, which contains the required layouts. See decide_slide_layout(). :param hints: As a supplemental infomation passed in to tell this routine what hint to use. Currently, the language code is the only hint in action. 1. zh 2. en 3. zh2en 4. en2zh 5. en2ja :param team: Team name (or tag), such as pe, doc, tse, etc. :returns: The constructed PowerPoint file path. """ if hints.endswith("en"): if platform.system() == "Windows": view_folder_base = ".\\student-guide\\view\\" + hints course_title_md = ".\\metadata\\raw\\course-title-en.md" else: view_folder_base = "./student-guide/view/" + hints course_title_md = "./metadata/raw/course-title-en.md" elif hints.endswith("zh"): if platform.system() == "Windows": view_folder_base = ".\\student-guide\\view\\" + hints course_title_md = ".\\metadata\\raw\\course-title-zh.md" else: view_folder_base = "./student-guide/view/" + hints course_title_md = "./metadata/raw/course-title-zh.md" elif hints.endswith("ja"): if platform.system() == "Windows": view_folder_base = ".\\student-guide\\view\\" + hints course_title_md = ".\\metadata\\raw\\course-title-ja.md" else: view_folder_base = "./student-guide/view/" + hints course_title_md = "./metadata/raw/course-title-ja.md" title_lines = mp.parse_markdown(course_title_md)[0] course_title = "Default Course Title" for line in title_lines: if line[0] == "title:slide": course_title = line[1] break chapter_folders = sorted(os.listdir(view_folder_base)) prs = Presentation(template_path) course_title_slide = prs.slides.add_slide(prs.slide_layouts[2]) en_1gram_terms_list = util.load_1gram_terms_en(team) course_title_slide.shapes.title.text = advisor.pretty_format_title( course_title, en_1gram_terms_list ) for chapter_folder in chapter_folders: chapter_folder_pattern_matches = re.findall( "^chapter-[0-9][0-9]", chapter_folder ) if ( chapter_folder_pattern_matches != None and len(chapter_folder_pattern_matches) > 0 and not chapter_folder.endswith("-lms") ): chapter_markdown_folder = view_folder_base + "/" + chapter_folder chapter_idx = chapter_folder.replace("chapter-0", "").replace( "chapter-", "" ) print("Chapter:", chapter_idx) chapter_content = mp.parse_markdown_folder(chapter_markdown_folder) prs = create_chapter_ppt( chapter_content, None, None, str(chapter_idx), hints, team, prs, inspect_mode, ) if platform.system() == "Windows": if inspect_mode == "false": dest_file = ( ".\\stage\\" + os.path.basename(os.getcwd()) + "-" + hints + ".pptx" ) else: dest_file = ( ".\\stage\\" + os.path.basename(os.getcwd()) + "-" + hints + "-inspected" + ".pptx" ) else: if inspect_mode == "false": dest_file = ( "./stage/" + os.path.basename(os.getcwd()) + "-" + hints + ".pptx" ) else: dest_file = ( "./stage/" + os.path.basename(os.getcwd()) + "-" + hints + "-inspected" + ".pptx" ) prs.save(dest_file) return dest_file