Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
"""Markdown to plain text conversion and notes slide XML generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from language_tags import normalize_language_tag
|
||||
|
||||
from ..drawingml.utils import (
|
||||
detect_text_lang,
|
||||
text_has_rtl_characters,
|
||||
text_uses_rtl,
|
||||
)
|
||||
|
||||
|
||||
def markdown_to_plain_text(md_content: str) -> str:
|
||||
"""Convert Markdown notes to plain text for PPTX notes.
|
||||
|
||||
Args:
|
||||
md_content: Markdown formatted notes content.
|
||||
|
||||
Returns:
|
||||
Plain text content.
|
||||
"""
|
||||
def strip_inline_bold(text: str) -> str:
|
||||
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
||||
text = re.sub(r'__(.+?)__', r'\1', text)
|
||||
return text
|
||||
|
||||
lines: list[str] = []
|
||||
for line in md_content.split('\n'):
|
||||
if line.startswith('#'):
|
||||
text = re.sub(r'^#+\s*', '', line).strip()
|
||||
text = strip_inline_bold(text)
|
||||
if text:
|
||||
lines.append(text)
|
||||
lines.append('')
|
||||
elif line.strip().startswith('- '):
|
||||
item_text = line.strip()[2:]
|
||||
item_text = strip_inline_bold(item_text)
|
||||
lines.append('• ' + item_text)
|
||||
elif line.strip():
|
||||
text = strip_inline_bold(line.strip())
|
||||
lines.append(text)
|
||||
else:
|
||||
lines.append('')
|
||||
|
||||
# Merge consecutive empty lines
|
||||
result: list[str] = []
|
||||
is_prev_empty = False
|
||||
for line in lines:
|
||||
if line == '':
|
||||
if not is_prev_empty:
|
||||
result.append(line)
|
||||
is_prev_empty = True
|
||||
else:
|
||||
result.append(line)
|
||||
is_prev_empty = False
|
||||
|
||||
return '\n'.join(result).strip()
|
||||
|
||||
|
||||
def create_notes_slide_xml(
|
||||
slide_num: int,
|
||||
notes_text: str,
|
||||
primary_language: str | None = None,
|
||||
) -> str:
|
||||
"""Create notes slide XML.
|
||||
|
||||
Args:
|
||||
slide_num: Slide number.
|
||||
notes_text: Notes text in plain text format.
|
||||
primary_language: Canonical BCP-47 deck language, when available.
|
||||
|
||||
Returns:
|
||||
Notes slide XML string.
|
||||
"""
|
||||
primary_language = (
|
||||
normalize_language_tag(primary_language)
|
||||
if primary_language is not None
|
||||
else None
|
||||
)
|
||||
default_language = primary_language or 'en-US'
|
||||
notes_text = (notes_text
|
||||
.replace('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
|
||||
paragraphs: list[str] = []
|
||||
for para in notes_text.split('\n'):
|
||||
if para.strip():
|
||||
lang = detect_text_lang(para, primary_language)
|
||||
paragraph_rtl = ' rtl="1"' if text_uses_rtl(
|
||||
para,
|
||||
primary_language,
|
||||
) else ''
|
||||
run_rtl = (
|
||||
'<a:rtl val="1"/>'
|
||||
if text_has_rtl_characters(para)
|
||||
else ''
|
||||
)
|
||||
paragraphs.append(f'''<a:p>
|
||||
<a:pPr{paragraph_rtl}/>
|
||||
<a:r>
|
||||
<a:rPr lang="{lang}" dirty="0">{run_rtl}</a:rPr>
|
||||
<a:t>{para}</a:t>
|
||||
</a:r>
|
||||
</a:p>''')
|
||||
else:
|
||||
paragraph_rtl = (
|
||||
' rtl="1"'
|
||||
if primary_language and text_uses_rtl('', primary_language)
|
||||
else ''
|
||||
)
|
||||
paragraphs.append(
|
||||
f'<a:p><a:pPr{paragraph_rtl}/>'
|
||||
f'<a:endParaRPr lang="{default_language}" dirty="0"/></a:p>'
|
||||
)
|
||||
|
||||
paragraphs_xml = (
|
||||
'\n '.join(paragraphs)
|
||||
if paragraphs
|
||||
else (
|
||||
f'<a:p><a:endParaRPr lang="{default_language}" dirty="0"/></a:p>'
|
||||
)
|
||||
)
|
||||
|
||||
return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
||||
<p:cSld>
|
||||
<p:spTree>
|
||||
<p:nvGrpSpPr>
|
||||
<p:cNvPr id="1" name=""/>
|
||||
<p:cNvGrpSpPr/>
|
||||
<p:nvPr/>
|
||||
</p:nvGrpSpPr>
|
||||
<p:grpSpPr>
|
||||
<a:xfrm>
|
||||
<a:off x="0" y="0"/>
|
||||
<a:ext cx="0" cy="0"/>
|
||||
<a:chOff x="0" y="0"/>
|
||||
<a:chExt cx="0" cy="0"/>
|
||||
</a:xfrm>
|
||||
</p:grpSpPr>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="2" name="Slide Image Placeholder 1"/>
|
||||
<p:cNvSpPr>
|
||||
<a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/>
|
||||
</p:cNvSpPr>
|
||||
<p:nvPr>
|
||||
<p:ph type="sldImg" idx="2"/>
|
||||
</p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr/>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="3" name="Notes Placeholder 2"/>
|
||||
<p:cNvSpPr>
|
||||
<a:spLocks noGrp="1"/>
|
||||
</p:cNvSpPr>
|
||||
<p:nvPr>
|
||||
<p:ph type="body" sz="quarter" idx="3"/>
|
||||
</p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr/>
|
||||
<p:txBody>
|
||||
<a:bodyPr/>
|
||||
<a:lstStyle/>
|
||||
{paragraphs_xml}
|
||||
</p:txBody>
|
||||
</p:sp>
|
||||
</p:spTree>
|
||||
</p:cSld>
|
||||
<p:clrMapOvr>
|
||||
<a:masterClrMapping/>
|
||||
</p:clrMapOvr>
|
||||
</p:notes>'''
|
||||
|
||||
|
||||
def create_notes_slide_rels_xml(slide_num: int) -> str:
|
||||
"""Create notes slide relationship file XML.
|
||||
|
||||
Args:
|
||||
slide_num: Slide number.
|
||||
|
||||
Returns:
|
||||
Relationship file XML string.
|
||||
"""
|
||||
return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>
|
||||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide{slide_num}.xml"/>
|
||||
</Relationships>'''
|
||||
|
||||
|
||||
def create_notes_master_xml(primary_language: str | None = None) -> str:
|
||||
"""Create a minimal PowerPoint-compatible notes master XML."""
|
||||
language = (
|
||||
normalize_language_tag(primary_language)
|
||||
if primary_language is not None
|
||||
else 'en-US'
|
||||
)
|
||||
paragraph_rtl = ' rtl="1"' if text_uses_rtl('', language) else ''
|
||||
return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
||||
<p:cSld>
|
||||
<p:spTree>
|
||||
<p:nvGrpSpPr>
|
||||
<p:cNvPr id="1" name=""/>
|
||||
<p:cNvGrpSpPr/>
|
||||
<p:nvPr/>
|
||||
</p:nvGrpSpPr>
|
||||
<p:grpSpPr>
|
||||
<a:xfrm>
|
||||
<a:off x="0" y="0"/>
|
||||
<a:ext cx="0" cy="0"/>
|
||||
<a:chOff x="0" y="0"/>
|
||||
<a:chExt cx="0" cy="0"/>
|
||||
</a:xfrm>
|
||||
</p:grpSpPr>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="2" name="Header Placeholder 1"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="hdr" sz="quarter"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="0" y="0"/>
|
||||
<a:ext cx="2971800" cy="457200"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="3" name="Date Placeholder 2"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="dt" sz="half" idx="1"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="3884613" y="0"/>
|
||||
<a:ext cx="2971800" cy="457200"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="4" name="Slide Image Placeholder 3"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="1143000" y="685800"/>
|
||||
<a:ext cx="4572000" cy="3429000"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="5" name="Notes Placeholder 4"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="685800" y="4343400"/>
|
||||
<a:ext cx="5486400" cy="4114800"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="6" name="Footer Placeholder 5"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="ftr" sz="quarter" idx="4"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="0" y="8685213"/>
|
||||
<a:ext cx="2971800" cy="457200"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
|
||||
</p:sp>
|
||||
<p:sp>
|
||||
<p:nvSpPr>
|
||||
<p:cNvPr id="7" name="Slide Number Placeholder 6"/>
|
||||
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||
<p:nvPr><p:ph type="sldNum" sz="quarter" idx="5"/></p:nvPr>
|
||||
</p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm>
|
||||
<a:off x="3884613" y="8685213"/>
|
||||
<a:ext cx="2971800" cy="457200"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
||||
</p:spPr>
|
||||
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
|
||||
</p:sp>
|
||||
</p:spTree>
|
||||
</p:cSld>
|
||||
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"
|
||||
accent1="accent1" accent2="accent2" accent3="accent3"
|
||||
accent4="accent4" accent5="accent5" accent6="accent6"
|
||||
hlink="hlink" folHlink="folHlink"/>
|
||||
<p:hf/>
|
||||
<p:notesStyle>
|
||||
<a:lvl1pPr marL="0" algn="l"{paragraph_rtl}>
|
||||
<a:defRPr sz="1200" lang="{language}"/>
|
||||
</a:lvl1pPr>
|
||||
</p:notesStyle>
|
||||
</p:notesMaster>'''
|
||||
|
||||
|
||||
def create_notes_master_rels_xml() -> str:
|
||||
"""Create notes master relationships XML."""
|
||||
return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme2.xml"/>
|
||||
</Relationships>'''
|
||||
Reference in New Issue
Block a user