"""
* 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(
""
) and not line.startswith(
""
):
write_lines += [line + "\n"]
else:
write_lines += [
"
+ ")\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