返回 ppt-master
notes.py
1 """Markdown to plain text conversion and notes slide XML generation."""
2
3 from __future__ import annotations
4
5 import re
6
7 from language_tags import normalize_language_tag
8
9 from ..drawingml.utils import (
10 detect_text_lang,
11 text_has_rtl_characters,
12 text_uses_rtl,
13 )
14
15
16 def markdown_to_plain_text(md_content: str) -> str:
17 """Convert Markdown notes to plain text for PPTX notes.
18
19 Args:
20 md_content: Markdown formatted notes content.
21
22 Returns:
23 Plain text content.
24 """
25 def strip_inline_bold(text: str) -> str:
26 text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
27 text = re.sub(r'__(.+?)__', r'\1', text)
28 return text
29
30 lines: list[str] = []
31 for line in md_content.split('\n'):
32 if line.startswith('#'):
33 text = re.sub(r'^#+\s*', '', line).strip()
34 text = strip_inline_bold(text)
35 if text:
36 lines.append(text)
37 lines.append('')
38 elif line.strip().startswith('- '):
39 item_text = line.strip()[2:]
40 item_text = strip_inline_bold(item_text)
41 lines.append('• ' + item_text)
42 elif line.strip():
43 text = strip_inline_bold(line.strip())
44 lines.append(text)
45 else:
46 lines.append('')
47
48 # Merge consecutive empty lines
49 result: list[str] = []
50 is_prev_empty = False
51 for line in lines:
52 if line == '':
53 if not is_prev_empty:
54 result.append(line)
55 is_prev_empty = True
56 else:
57 result.append(line)
58 is_prev_empty = False
59
60 return '\n'.join(result).strip()
61
62
63 def create_notes_slide_xml(
64 slide_num: int,
65 notes_text: str,
66 primary_language: str | None = None,
67 ) -> str:
68 """Create notes slide XML.
69
70 Args:
71 slide_num: Slide number.
72 notes_text: Notes text in plain text format.
73 primary_language: Canonical BCP-47 deck language, when available.
74
75 Returns:
76 Notes slide XML string.
77 """
78 primary_language = (
79 normalize_language_tag(primary_language)
80 if primary_language is not None
81 else None
82 )
83 default_language = primary_language or 'en-US'
84 notes_text = (notes_text
85 .replace('&', '&')
86 .replace('<', '&lt;')
87 .replace('>', '&gt;'))
88
89 paragraphs: list[str] = []
90 for para in notes_text.split('\n'):
91 if para.strip():
92 lang = detect_text_lang(para, primary_language)
93 paragraph_rtl = ' rtl="1"' if text_uses_rtl(
94 para,
95 primary_language,
96 ) else ''
97 run_rtl = (
98 '<a:rtl val="1"/>'
99 if text_has_rtl_characters(para)
100 else ''
101 )
102 paragraphs.append(f'''<a:p>
103 <a:pPr{paragraph_rtl}/>
104 <a:r>
105 <a:rPr lang="{lang}" dirty="0">{run_rtl}</a:rPr>
106 <a:t>{para}</a:t>
107 </a:r>
108 </a:p>''')
109 else:
110 paragraph_rtl = (
111 ' rtl="1"'
112 if primary_language and text_uses_rtl('', primary_language)
113 else ''
114 )
115 paragraphs.append(
116 f'<a:p><a:pPr{paragraph_rtl}/>'
117 f'<a:endParaRPr lang="{default_language}" dirty="0"/></a:p>'
118 )
119
120 paragraphs_xml = (
121 '\n '.join(paragraphs)
122 if paragraphs
123 else (
124 f'<a:p><a:endParaRPr lang="{default_language}" dirty="0"/></a:p>'
125 )
126 )
127
128 return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
129 <p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
130 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
131 xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
132 <p:cSld>
133 <p:spTree>
134 <p:nvGrpSpPr>
135 <p:cNvPr id="1" name=""/>
136 <p:cNvGrpSpPr/>
137 <p:nvPr/>
138 </p:nvGrpSpPr>
139 <p:grpSpPr>
140 <a:xfrm>
141 <a:off x="0" y="0"/>
142 <a:ext cx="0" cy="0"/>
143 <a:chOff x="0" y="0"/>
144 <a:chExt cx="0" cy="0"/>
145 </a:xfrm>
146 </p:grpSpPr>
147 <p:sp>
148 <p:nvSpPr>
149 <p:cNvPr id="2" name="Slide Image Placeholder 1"/>
150 <p:cNvSpPr>
151 <a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/>
152 </p:cNvSpPr>
153 <p:nvPr>
154 <p:ph type="sldImg" idx="2"/>
155 </p:nvPr>
156 </p:nvSpPr>
157 <p:spPr/>
158 </p:sp>
159 <p:sp>
160 <p:nvSpPr>
161 <p:cNvPr id="3" name="Notes Placeholder 2"/>
162 <p:cNvSpPr>
163 <a:spLocks noGrp="1"/>
164 </p:cNvSpPr>
165 <p:nvPr>
166 <p:ph type="body" sz="quarter" idx="3"/>
167 </p:nvPr>
168 </p:nvSpPr>
169 <p:spPr/>
170 <p:txBody>
171 <a:bodyPr/>
172 <a:lstStyle/>
173 {paragraphs_xml}
174 </p:txBody>
175 </p:sp>
176 </p:spTree>
177 </p:cSld>
178 <p:clrMapOvr>
179 <a:masterClrMapping/>
180 </p:clrMapOvr>
181 </p:notes>'''
182
183
184 def create_notes_slide_rels_xml(slide_num: int) -> str:
185 """Create notes slide relationship file XML.
186
187 Args:
188 slide_num: Slide number.
189
190 Returns:
191 Relationship file XML string.
192 """
193 return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
194 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
195 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>
196 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide{slide_num}.xml"/>
197 </Relationships>'''
198
199
200 def create_notes_master_xml(primary_language: str | None = None) -> str:
201 """Create a minimal PowerPoint-compatible notes master XML."""
202 language = (
203 normalize_language_tag(primary_language)
204 if primary_language is not None
205 else 'en-US'
206 )
207 paragraph_rtl = ' rtl="1"' if text_uses_rtl('', language) else ''
208 return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
209 <p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
210 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
211 xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
212 <p:cSld>
213 <p:spTree>
214 <p:nvGrpSpPr>
215 <p:cNvPr id="1" name=""/>
216 <p:cNvGrpSpPr/>
217 <p:nvPr/>
218 </p:nvGrpSpPr>
219 <p:grpSpPr>
220 <a:xfrm>
221 <a:off x="0" y="0"/>
222 <a:ext cx="0" cy="0"/>
223 <a:chOff x="0" y="0"/>
224 <a:chExt cx="0" cy="0"/>
225 </a:xfrm>
226 </p:grpSpPr>
227 <p:sp>
228 <p:nvSpPr>
229 <p:cNvPr id="2" name="Header Placeholder 1"/>
230 <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
231 <p:nvPr><p:ph type="hdr" sz="quarter"/></p:nvPr>
232 </p:nvSpPr>
233 <p:spPr>
234 <a:xfrm>
235 <a:off x="0" y="0"/>
236 <a:ext cx="2971800" cy="457200"/>
237 </a:xfrm>
238 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
239 </p:spPr>
240 <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
241 </p:sp>
242 <p:sp>
243 <p:nvSpPr>
244 <p:cNvPr id="3" name="Date Placeholder 2"/>
245 <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
246 <p:nvPr><p:ph type="dt" sz="half" idx="1"/></p:nvPr>
247 </p:nvSpPr>
248 <p:spPr>
249 <a:xfrm>
250 <a:off x="3884613" y="0"/>
251 <a:ext cx="2971800" cy="457200"/>
252 </a:xfrm>
253 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
254 </p:spPr>
255 <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
256 </p:sp>
257 <p:sp>
258 <p:nvSpPr>
259 <p:cNvPr id="4" name="Slide Image Placeholder 3"/>
260 <p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr>
261 <p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr>
262 </p:nvSpPr>
263 <p:spPr>
264 <a:xfrm>
265 <a:off x="1143000" y="685800"/>
266 <a:ext cx="4572000" cy="3429000"/>
267 </a:xfrm>
268 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
269 </p:spPr>
270 </p:sp>
271 <p:sp>
272 <p:nvSpPr>
273 <p:cNvPr id="5" name="Notes Placeholder 4"/>
274 <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
275 <p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr>
276 </p:nvSpPr>
277 <p:spPr>
278 <a:xfrm>
279 <a:off x="685800" y="4343400"/>
280 <a:ext cx="5486400" cy="4114800"/>
281 </a:xfrm>
282 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
283 </p:spPr>
284 <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
285 </p:sp>
286 <p:sp>
287 <p:nvSpPr>
288 <p:cNvPr id="6" name="Footer Placeholder 5"/>
289 <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
290 <p:nvPr><p:ph type="ftr" sz="quarter" idx="4"/></p:nvPr>
291 </p:nvSpPr>
292 <p:spPr>
293 <a:xfrm>
294 <a:off x="0" y="8685213"/>
295 <a:ext cx="2971800" cy="457200"/>
296 </a:xfrm>
297 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
298 </p:spPr>
299 <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
300 </p:sp>
301 <p:sp>
302 <p:nvSpPr>
303 <p:cNvPr id="7" name="Slide Number Placeholder 6"/>
304 <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
305 <p:nvPr><p:ph type="sldNum" sz="quarter" idx="5"/></p:nvPr>
306 </p:nvSpPr>
307 <p:spPr>
308 <a:xfrm>
309 <a:off x="3884613" y="8685213"/>
310 <a:ext cx="2971800" cy="457200"/>
311 </a:xfrm>
312 <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
313 </p:spPr>
314 <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr{paragraph_rtl}/><a:endParaRPr lang="{language}"/></a:p></p:txBody>
315 </p:sp>
316 </p:spTree>
317 </p:cSld>
318 <p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"
319 accent1="accent1" accent2="accent2" accent3="accent3"
320 accent4="accent4" accent5="accent5" accent6="accent6"
321 hlink="hlink" folHlink="folHlink"/>
322 <p:hf/>
323 <p:notesStyle>
324 <a:lvl1pPr marL="0" algn="l"{paragraph_rtl}>
325 <a:defRPr sz="1200" lang="{language}"/>
326 </a:lvl1pPr>
327 </p:notesStyle>
328 </p:notesMaster>'''
329
330
331 def create_notes_master_rels_xml() -> str:
332 """Create notes master relationships XML."""
333 return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
334 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
335 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme2.xml"/>
336 </Relationships>'''
337
337 lines PYTHON