diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 0f9ecc15..d708712f 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -26,6 +26,7 @@ ST_TextTypeface, ST_TextWrappingType, XsdBoolean, + XsdInt, ) from pptx.oxml.xmlchemy import ( BaseOxmlElement, @@ -325,6 +326,7 @@ class CT_TextCharacterProperties(BaseOxmlElement): u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType] "u", MSO_TEXT_UNDERLINE_TYPE ) + baseline: int | None = OptionalAttribute("baseline", XsdInt) # pyright: ignore[reportAssignmentType] def _new_gradFill(self): return CT_GradientFillProperties.new_gradFill() diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index e139410c..e2d19c1d 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -396,6 +396,69 @@ def size(self, emu: Length | None): sz = Emu(emu).centipoints self._rPr.sz = sz + @property + def superscript(self) -> int | None: + """ + Return the superscript baseline offset if set, otherwise None. + + A positive integer represents the text's upward shift relative to the baseline. + The default superscript offset is `30000` (when set via `True`). + """ + if self._rPr.baseline and self._rPr.baseline > 0 : + return self._rPr.baseline + return None + + @superscript.setter + def superscript(self, value: bool | int | None): + """ + Set or remove superscript formatting. + + - `True` sets the baseline to `30000` (default superscript offset). + - An `int` value explicitly sets the baseline to that amount. + - `False` or `None` removes superscript formatting. + """ + if value is True: + self._rPr.baseline = 30000 + elif value is False: + self._rPr.baseline = None + elif isinstance(value, int) and value >= 0: + self._rPr.baseline = value + else: + self._rPr.baseline = None + + @property + def subscript(self) -> int | None: + """ + Return the subscript baseline offset if set, otherwise None. + + A negative integer represents the text's downward shift relative to the baseline. + The default subscript offset is `-15000` (when set via `True`). + """ + if self._rPr.baseline and self._rPr.baseline < 0: + return self._rPr.baseline + + return None + + @subscript.setter + def subscript(self, value: bool | int | None): + """ + Set or remove subscript formatting. + + - `True` sets the baseline to `-15000` (default subscript offset). + - An `int` value explicitly sets the baseline to that amount. + - `False` or `None` removes subscript formatting. + """ + if value is True: + self._rPr.baseline = -15000 + elif value is False: + self._rPr.baseline = None + elif isinstance(value, int) and value <= 0: + self._rPr.baseline = value + else: + self._rPr.baseline = None + + + @property def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None: """Indicaties the underline setting for this font. diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 3a1a7a0b..5e35b88c 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -498,6 +498,25 @@ def it_can_change_its_language_id_setting(self, language_id_set_fixture): font.language_id = new_value assert font._element.xml == expected_xml + def it_knows_its_superscript_setting(self, superscript_get_fixture): + font, expected_value = superscript_get_fixture + assert font.superscript == expected_value + + def it_can_change_its_superscript_setting(self, superscript_set_fixture): + font, new_value, expected_xml = superscript_set_fixture + font.superscript = new_value + assert font._element.xml == expected_xml + + def it_knows_its_subscript_setting(self, subscript_get_fixture): + font, expected_value = subscript_get_fixture + assert font.subscript == expected_value + + def it_can_change_its_subscript_setting(self, subscript_set_fixture): + font, new_value, expected_xml = subscript_set_fixture + font.subscript = new_value + assert font._element.xml == expected_xml + + def it_knows_its_underline_setting(self, underline_get_fixture): font, expected_value = underline_get_fixture assert font.underline is expected_value, "got %s" % font.underline @@ -636,6 +655,59 @@ def size_set_fixture(self, request): expected_xml = xml(expected_rPr_cxml) return font, new_value, expected_xml + @pytest.fixture( + params=[ + ("a:rPr", None), + ("a:rPr{baseline=30000}", 30000), + ("a:rPr{baseline=15000}", 15000) + ] + ) + def superscript_get_fixture(self, request): + rPr_cxml, expected_value = request.param + font = Font(element(rPr_cxml)) + return font, expected_value + + @pytest.fixture( + params=[ + ("a:rPr", True, "a:rPr{baseline=30000}"), + ("a:rPr", 20000, "a:rPr{baseline=20000}"), + ("a:rPr{baseline=30000}", False, "a:rPr"), + ("a:rPr{baseline=15000}", None, "a:rPr") + ] + ) + def superscript_set_fixture(self, request): + rPr_cxml, new_value, expected_rPr_cxml = request.param + font = Font(element(rPr_cxml)) + expected_xml = xml(expected_rPr_cxml) + return font, new_value, expected_xml + + @pytest.fixture( + params=[ + ("a:rPr", None), + ("a:rPr{baseline=-10000}", -10000), + ("a:rPr{baseline=-15000}", -15000) + ] + ) + def subscript_get_fixture(self, request): + rPr_cxml, expected_value = request.param + font = Font(element(rPr_cxml)) + return font, expected_value + + @pytest.fixture( + params=[ + ("a:rPr", True, "a:rPr{baseline=-15000}"), + ("a:rPr", -10000, "a:rPr{baseline=-10000}"), + ("a:rPr", 10000, "a:rPr"), + ("a:rPr{baseline=-10000}", False, "a:rPr"), + ("a:rPr{baseline=-15000}", None, "a:rPr") + ] + ) + def subscript_set_fixture(self, request): + rPr_cxml, new_value, expected_rPr_cxml = request.param + font = Font(element(rPr_cxml)) + expected_xml = xml(expected_rPr_cxml) + return font, new_value, expected_xml + @pytest.fixture( params=[ ("a:rPr", None),