A slide's content (body) placeholder carries three paragraphs of text, each rendered as a bullet via the layout's list style.
Library verdicts: python-pptx: — pptxjs: fail
| Feature id | pptx/bullet-list |
|---|---|
| Format | pptx |
| Category | slide-content |
| Spec | ecma-376-5-part-1 § 21.1.2 a:p |
| ID / part | Predicate | XPath | python-pptx | pptxjs |
|---|---|---|---|---|
body-placeholder-has-three-paragraphsppt/slides/slide1.xml | existThe content (body) placeholder's text body must carry at least three <a:p> paragraphs. | //p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]/p:txBody[count(a:p)>=3] | — | — |
body-placeholder-item-oneppt/slides/slide1.xml | existThe content placeholder must contain a paragraph with text 'Item one'. | //p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item one'] | — | — |
body-placeholder-item-twoppt/slides/slide1.xml | existThe content placeholder must contain a paragraph with text 'Item two'. | //p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item two'] | — | — |
body-placeholder-item-threeppt/slides/slide1.xml | existThe content placeholder must contain a paragraph with text 'Item three'. | //p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item three'] | — | — |
| ID | Predicate | Selector | python-pptx | pptxjs |
|---|---|---|---|---|
bullet-list-three-items | css_selector/ equal-countThree bullet-item DOM nodes must be rendered for the content placeholder. `.oox-page .oox-paragraph` is the cross-format shared-class selector (W9-F) emitted by docxjs/pptxjs/xlsxjs; the remaining selectors are kept as back-compat aliases. | .oox-page .oox-paragraph, ul li, .pptx-list li, [data-placeholder-type='body'] li, [data-placeholder] p | — | fail |
bullet-list-item-one | css_selector/ match-text = Item oneFirst rendered bullet item must contain 'Item one'. Fallback text-match in case the selector-count assertion fails on a renderer using non-list markup. | .oox-page .oox-paragraph, ul li, .pptx-list li, [data-placeholder-type='body'] li, [data-placeholder] p | — | fail |
bullet-list-item-two | css_selector/ match-text = Item twoSecond rendered bullet item must contain 'Item two'. | .oox-page .oox-paragraph:nth-of-type(2), ul li:nth-of-type(2), .pptx-list li:nth-of-type(2), [data-placeholder-type='body'] li:nth-of-type(2), [data-placeholder] p:nth-of-type(2) | — | fail |
bullet-list-item-three | css_selector/ match-text = Item threeThird rendered bullet item must contain 'Item three'. | .oox-page .oox-paragraph:nth-of-type(3), ul li:nth-of-type(3), .pptx-list li:nth-of-type(3), [data-placeholder-type='body'] li:nth-of-type(3), [data-placeholder] p:nth-of-type(3) | — | fail |
slide-count | css_selector/ equal-countExactly one slide container must be rendered. Catches renderers that emit zero slides (hard failure) or duplicate the slide. | section.pptx-slide, .pptx-slide, section[data-slide], .slide, [data-slide] | — | — |
scripts/gen_bullet_list.py
#!/usr/bin/env python3
"""Generate ``fixtures/pptx/bullet-list.pptx``.
A one-slide presentation whose content (body) placeholder carries three
paragraphs — each rendered as a bullet by the layout's list style.
Designed to satisfy the assertions in ``features/pptx/bullet-list.json``.
"""
from __future__ import annotations
from pathlib import Path
from pptx import Presentation
import datetime as _dt
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "pptx" / "bullet-list.pptx"
def main() -> int:
prs = Presentation()
# slide_layouts[1] is the default title + content layout; its body
# placeholder (idx=1) inherits a bulleted list style from the master.
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Bullets"
content_tf = slide.placeholders[1].text_frame
content_tf.text = "Item one"
content_tf.add_paragraph().text = "Item two"
content_tf.add_paragraph().text = "Item three"
_OUT.parent.mkdir(parents=True, exist_ok=True)
prs.save(_OUT, zip_date_time=_REPRODUCIBLE_DT)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main()){
"$schema": "../manifest.schema.json",
"id": "pptx/bullet-list",
"title": "Bulleted list in content placeholder",
"format": "pptx",
"category": "slide-content",
"summary": "A slide's content (body) placeholder carries three paragraphs of text, each rendered as a bullet via the layout's list style.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "21.1.2",
"element": "a:p",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/DrawingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/dml-text.xsd",
"notes": "python-pptx emits the content placeholder as <p:ph idx=\"1\"/> (no type attribute). Bullet formatting is inherited from the slide layout's list-style rather than carried explicitly on each <a:p>. Any renderer that honours layout-level list styles will render each paragraph as a bullet."
},
"fixtures": {
"machine": "pptx/bullet-list",
"office": "pptx/bullet-list"
},
"generator": {
"python": "scripts/gen_bullet_list.py"
},
"assertions": [
{
"id": "body-placeholder-has-three-paragraphs",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]/p:txBody[count(a:p)>=3]",
"must": "exist",
"description": "The content (body) placeholder's text body must carry at least three <a:p> paragraphs."
},
{
"id": "body-placeholder-item-one",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item one']",
"must": "exist",
"description": "The content placeholder must contain a paragraph with text 'Item one'."
},
{
"id": "body-placeholder-item-two",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item two']",
"must": "exist",
"description": "The content placeholder must contain a paragraph with text 'Item two'."
},
{
"id": "body-placeholder-item-three",
"part": "ppt/slides/slide1.xml",
"namespaces": {
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
},
"xpath": "//p:sp[p:nvSpPr/p:nvPr/p:ph[@idx='1']]//a:p/a:r/a:t[normalize-space()='Item three']",
"must": "exist",
"description": "The content placeholder must contain a paragraph with text 'Item three'."
}
],
"render_assertions": [
{
"id": "bullet-list-three-items",
"kind": "css_selector",
"selector": ".oox-page .oox-paragraph, ul li, .pptx-list li, [data-placeholder-type='body'] li, [data-placeholder] p",
"must": "equal-count",
"count": 3,
"description": "Three bullet-item DOM nodes must be rendered for the content placeholder. `.oox-page .oox-paragraph` is the cross-format shared-class selector (W9-F) emitted by docxjs/pptxjs/xlsxjs; the remaining selectors are kept as back-compat aliases."
},
{
"id": "bullet-list-item-one",
"kind": "css_selector",
"selector": ".oox-page .oox-paragraph, ul li, .pptx-list li, [data-placeholder-type='body'] li, [data-placeholder] p",
"must": "match-text",
"value": "Item one",
"description": "First rendered bullet item must contain 'Item one'. Fallback text-match in case the selector-count assertion fails on a renderer using non-list markup."
},
{
"id": "bullet-list-item-two",
"kind": "css_selector",
"selector": ".oox-page .oox-paragraph:nth-of-type(2), ul li:nth-of-type(2), .pptx-list li:nth-of-type(2), [data-placeholder-type='body'] li:nth-of-type(2), [data-placeholder] p:nth-of-type(2)",
"must": "match-text",
"value": "Item two",
"description": "Second rendered bullet item must contain 'Item two'."
},
{
"id": "bullet-list-item-three",
"kind": "css_selector",
"selector": ".oox-page .oox-paragraph:nth-of-type(3), ul li:nth-of-type(3), .pptx-list li:nth-of-type(3), [data-placeholder-type='body'] li:nth-of-type(3), [data-placeholder] p:nth-of-type(3)",
"must": "match-text",
"value": "Item three",
"description": "Third rendered bullet item must contain 'Item three'."
},
{
"id": "slide-count",
"kind": "css_selector",
"selector": "section.pptx-slide, .pptx-slide, section[data-slide], .slide, [data-slide]",
"must": "equal-count",
"count": 1,
"description": "Exactly one slide container must be rendered. Catches renderers that emit zero slides (hard failure) or duplicate the slide."
}
]
}
