2
0
Fork 0
textout/textoutpc/builtin.py

1186 lines
33 KiB
Python

#!/usr/bin/env python
# *****************************************************************************
# Copyright (C) 2018-2023 Thomas Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
# *****************************************************************************
"""Built-in tags for textoutpc."""
from __future__ import annotations
from collections.abc import Sequence
import re
from string import ascii_lowercase, ascii_uppercase
from typing import ClassVar, Iterator, Literal
from urllib.parse import urlparse
from docutils.nodes import (
Node,
Element,
Text,
citation,
container,
emphasis,
image,
literal,
reference,
strong,
subtitle,
target,
title,
)
from thcolor.colors import Color
from thcolor.errors import ColorExpressionSyntaxError
from .exceptions import InvalidValue, MissingValue, UnexpectedValue
from .nodes import progress, spoiler
from .tags import RawTag, Tag
BIG_FONT_SIZE: float = 2.00
SMALL_FONT_SIZE: float = 0.75
COLOR_TAG_NAMES: set[str] = {
"red",
"green",
"blue",
"yellow",
"maroon",
"purple",
"gray",
"grey",
"brown",
}
FONT_NAMES: dict[str, str] = {
"arial": "Arial",
"comic": "Comic MS",
"tahoma": "Tahoma",
"courier": "Courier",
"haettenschweiler": "Haettenschweiler",
"mono": "monospace",
"monospace": "monospace",
}
class TitleTag(RawTag):
"""Title tag.
Example uses::
[title]Example title[/]
See :ref:`markup-title` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
yield title("", children)
class SubtitleTag(RawTag):
"""Subtitle tag.
Example uses::
[subtitle]Example subtitle[/]
See :ref:`markup-title` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
yield subtitle("", children)
class TextTag(Tag):
"""Main tag for setting text formatting.
Example uses::
[b]Bold text.[/b]
[i]Italic text.[/i]
[u]Underlined text.[/u]
[strike]Striked text.[/strike]
[striked]Text strikes again.[/striked]
[font=arial]Arial text.[/font]
[arial]Arial text again.[/arial]
[blue]This will be in blue[/blue]
[color=blue]This as well[/color]
[color=rgb(255, 255, 255, 0.4)]BLACKNESS[/color]
[color=hsl(0, 100%, 0.5)]This will be red.[/color]
Also supports a hack used on Planète Casio for a while, which
is a CSS injection, e.g.::
[color=brown; size: 16pt]Hello world![/color]
See the following sections for more information:
* :ref:`markup-strong`
* :ref:`markup-italic`
* :ref:`markup-decoration`
* :ref:`markup-font`
* :ref:`markup-color`
"""
__slots__ = (
"strong",
"italic",
"underline",
"overline",
"strike",
"font_name",
"font_size",
"font_size_unit",
"text_color",
"back_color",
)
strong: bool
"""Whether the text should be set as strong or not."""
italic: bool
"""Whether the text should be set as italic or not."""
underline: bool
"""Whether the text should be underlined or not."""
overline: bool
"""Whether the text should be overlined or not."""
strike: bool
"""Whether the text should be striked or not."""
font_name: str | None
"""Name of the font to set to the text."""
font_size: float | None
"""Size of the font to set to the text."""
font_size_unit: Literal["pt", "em"]
"""Unit of the font size."""
text_color: Color | None
"""Color to set to the text."""
back_color: Color | None
"""Color to set to the text background."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
self.strong = self.name == "[b]"
self.italic = self.name == "[i]"
self.underline = self.name == "[u]"
self.overline = self.name == "[o]"
self.strike = self.name in ("[s]", "[strike]", "[striked]")
self.font_name = None
self.font_size = None
self.font_size_unit = "pt"
self.text_color = None
self.back_color = None
# Historically, such tags used to be used for CSS injections.
# We want to support limited CSS injections here, by parsing a lighter
# syntax of CSS.
value = self.value
css_properties: list[str] = []
if value is not None:
css_properties = value.split(";")
value = css_properties.pop(0)
if self.name == "[font]":
if value is None:
raise MissingValue()
if value not in FONT_NAMES:
raise InvalidValue(
'Invalid font name "{font_name}".',
font_name=value,
)
self.font_name = FONT_NAMES[value]
elif self.name[1:-1] in FONT_NAMES:
if value is not None:
raise UnexpectedValue()
self.font_name = FONT_NAMES[self.name[1:-1]]
elif self.name == "[big]":
if value is not None:
raise UnexpectedValue()
self.font_size = BIG_FONT_SIZE
elif self.name == "[small]":
if value is not None:
raise UnexpectedValue()
self.font_size = SMALL_FONT_SIZE
elif self.name == "[size]":
if value is None:
raise MissingValue()
if value == "big":
self.font_size = BIG_FONT_SIZE
elif value == "small":
self.font_size = SMALL_FONT_SIZE
else:
try:
self.font_size = round(int(value) / 100.0, 2)
except ValueError:
raise InvalidValue(
"Invalid font size: {value}",
value=value,
)
if self.font_size <= 0 or self.font_size > 3.0:
raise InvalidValue(
"Invalid font size: {value}",
value=value,
)
elif self.name in ("[c]", "[color]"):
if value is None:
raise MissingValue()
try:
self.text_color = Color.fromtext(value)
except (ColorExpressionSyntaxError, ValueError) as exc:
raise InvalidValue(f"Invalid color: {exc}") from exc
elif self.name == "[f]":
if value is None:
raise MissingValue()
try:
self.back_color = Color.fromtext(value)
except (ColorExpressionSyntaxError, ValueError) as exc:
raise InvalidValue(f"Invalid color: {exc}") from exc
elif self.name[1:-1] in COLOR_TAG_NAMES:
if value is not None:
raise UnexpectedValue()
self.text_color = Color.fromtext(self.name[1:-1])
elif self.name == "[css]":
if value is None:
raise MissingValue()
css_properties.insert(0, value)
elif self.value is not None:
# Other tags do not expect any value.
raise UnexpectedValue()
# CSS properties.
for css_property in css_properties:
name, *value_list = css_property.split(":")
if not value_list:
continue
name = name.strip()
value = ":".join(value_list).strip()
if name in ("size", "font-size"):
unit: Literal["pt", "em"]
if value.endswith("pt"):
value = value[:-2].rstrip()
unit = "pt"
elif value.endswith("em"):
value = value[:-2].rstrip()
unit = "em"
try:
size = float(int(value))
except ValueError:
continue
if size <= 0:
continue
self.font_size = size
self.font_size_unit = unit
elif name == "color":
try:
self.text_color = Color.fromtext(value)
except ValueError:
continue
elif name == "background-color":
try:
self.back_color = Color.fromtext(value)
except ValueError:
continue
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
style_items = []
if self.text_color is not None:
for css_color in self.text_color.css():
style_items.append(("color", css_color))
if self.back_color is not None:
for css_color in self.back_color.css():
style_items.append(("background-color", css_color))
if self.font_name is not None:
style_items.append(("font-family", self.font_name))
if self.font_size is not None:
if int(self.font_size) == self.font_size:
font_size = f"{int(self.font_size):d}"
else:
font_size = f"{self.font_size:.02f}"
style_items.append(
(
"font-size",
f"{font_size}{self.font_size_unit}",
),
)
text_decorations = []
if self.underline:
text_decorations.append("underline")
if self.overline:
text_decorations.append("overline")
if self.strike:
text_decorations.append("line-through")
if text_decorations:
style_items.append(("text-decoration", " ".join(text_decorations)))
if self.strong:
children = [strong("", *children)]
if self.italic:
children = [emphasis("", *children)]
if style_items:
con = container("", *children)
con.attributes["style"] = "; ".join(
f"{key}: {value}" for key, value in style_items
)
children = [con]
yield from children
class AlignTag(Tag):
"""Main tag for aligning paragraphs.
Example uses::
[align=center]This text is centered horizontally.[/align]
[justify]This text is justified.[/justify]
See :ref:`markup-align` for more information.
"""
__slots__ = ("kind",)
ALIGN_KEYS: dict[str, Literal["center", "left", "right", "justify"]] = {
"center": "center",
"centre": "center",
"left": "left",
"right": "right",
"justify": "justify",
}
"""Alignment keys recognized as tags or tag values."""
kind: Literal["center", "left", "right", "justify"]
"""Kind of alignment."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.name == "[align]":
if self.value is None:
raise MissingValue()
if self.value not in self.ALIGN_KEYS:
raise InvalidValue(
"Expected one of these values:"
+ ", ".join(self.ALIGN_KEYS.keys()),
)
kind = self.value
elif (
not self.name.startswith("[")
or not self.name.endswith("]")
or not self.name[1:-1] in self.ALIGN_KEYS
):
raise ValueError(
"Only supported the following names: "
+ ", ".join(
f"[{name}]" for name in ("align", *self.ALIGN_KEYS)
),
)
elif self.value is not None:
raise UnexpectedValue()
else:
kind = self.name[1:-1]
self.kind = self.ALIGN_KEYS[kind]
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
div = container("", *children)
div.attributes["class"] = "align-" + self.kind
yield div
class QuoteTag(Tag):
"""Tag for presenting a quote.
Example uses::
[quote]Someone said that.[/]
[quote=Cakeisalie5]Ever realized that my name contained “Cake”?[/]
See :ref:`markup-quote` for more information.
"""
__slots__ = ()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
if self.value is not None:
children = [
strong("", emphasis("", f"{self.value} a écrit :")),
*children,
]
yield citation("", *children)
class CodeTag(RawTag):
"""Basic code tag, for displaying code.
Example uses::
[code]int main()
{
printf("hello, world");
}[/code]
See :ref:`markup-code` for more information.
"""
__slots__ = ()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
div = container("", *children)
div.attributes["class"] = "code"
yield div
class InlineCodeTag(RawTag):
"""Inline code tag.
This tag does not display a box, simply doesn't evaluate the content and
uses a monospace font.
Example uses::
`some inline code`
[inlinecode][b]The tags will be shown verbatim.[/b][/inlinecode]
[inlinecode][inlinecode][i]This also[/inlinecode] works![/inlinecode]
See :ref:`markup-code` for more information.
"""
__slots__ = ()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
node = literal("", *children)
node.attributes["class"] = "inline-code"
yield node
class NoEvalTag(RawTag):
"""Tag for not evaluating content.
Same as above, except doesn't apply any parent container or additional
style.
Example uses::
[noeval][b]wow, and no need for monospace![/b][/noeval]
"""
__slots__ = ()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
yield from children
class ImageTag(RawTag):
"""Tag for displaying an image.
Example uses::
[img]picture_url[/img]
[img=center]picture_url[/img]
[img=12x24]picture_url[/img]
[img=center|12x24]picture_url[/img]
[img=x24|right]picture_url[/img]
See :ref:`markup-image` for more information.
"""
__slots__ = ("width", "height", "alignment", "floating")
MODES: ClassVar[
dict[
str,
tuple[Literal["center", "left", "right"] | None, bool],
]
] = {
"center": ("center", False),
"centre": ("center", False),
"left": ("left", False),
"right": ("right", False),
"float": (None, True),
"floating": (None, True),
"float-left": ("left", True),
"float-center": ("center", True),
"float-centre": ("center", True),
"float-right": ("right", True),
}
"""The mapping between mode strings and alignment and floating."""
width: int | None
"""The width in pixels to display the image as, if provided."""
height: int | None
"""The height in pixels to display the image as, if provided."""
alignment: Literal["center", "left", "right"] | None
"""The alignment to display the image as, if provided."""
floating: bool
"""Whether the image should be floating or not."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
self.width = None
self.height = None
self.alignment = None
self.floating = False
if self.value is None:
return
for arg in self.value.split("|"):
arg = arg.strip().casefold()
if not arg:
continue
if arg[0] in "0123456789x":
try:
raw_w, *raw_hs = arg.split("x")
(raw_h,) = raw_hs if raw_hs else (raw_w,)
w = None
if raw_w:
w = int(raw_w)
h = None
if raw_h:
h = int(raw_h)
except ValueError:
continue
if w == 0 or h == 0:
continue
self.width = w
self.height = h
elif arg in self.MODES:
self.alignment, self.floating = self.MODES[arg]
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
url = self.get_text_from_raw_children(children)
yield self.build_element(url=url)
def build_element(self, *, url: str) -> Element:
"""Build the image element.
:param url: The URL of the image element.
:return: The node element.
"""
parsed_url = urlparse(url)
if parsed_url.scheme not in ("http", "https"):
raise ValueError(
f"Forbidden image source scheme: {parsed_url.scheme!r}",
)
style = []
classes = []
if self.width is not None:
style.append(f"width: {self.width}px")
if self.height is not None:
style.append(f"height: {self.height}px")
else:
style.append("height: auto")
elif self.height is not None:
style.append("width: auto")
style.append(f"height: {self.height}px")
if self.floating:
classes.append(f"img-float-{self.alignment or 'right'}")
elif self.alignment is not None:
classes.append(f"img-{self.alignment}")
img = image()
img.attributes["src"] = url
if style:
img.attributes["style"] = "; ".join(style)
if classes:
img.attributes["class"] = " ".join(classes)
return img
class AdminImageTag(ImageTag):
"""Tag for displaying an image from the administration.
This tag is special for Planète Casio, as it takes images from the
administration's (hence ``ad``) image folder. It adds the folder's prefix.
Example uses::
[adimg]some_picture.png[/img]
[adimg=center]some_picture.png[/img]
[adimg=12x24]some_picture.png[/img]
[adimg=center|12x24]some_picture.png[/img]
[adimg=x24|right]some_picture.png[/img]
See :ref:`markup-image` for more information.
"""
__slots__ = ()
def build_element(self, *, url: str) -> Element:
"""Build the image element.
:param url: The URL of the image element.
:return: The node element.
"""
return super().build_element(
url="https://www.planet-casio.com/images/ad/" + url,
)
class LabelTag(Tag):
"""Tag for defining an anchor at a point of the document.
Example uses::
[label=installation]Installation de tel logiciel... (no ending req.)
[label=compilation][/label] Compilation de tel logiciel...
See :ref:`markup-label` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is None:
raise MissingValue()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
yield target(refid=self.value)
yield from children
class TargetTag(Tag):
"""Tag for linking to an anchor defined in the document.
Example uses::
[target=installation]Check out the installation manual[/target]!
See :ref:`markup-label` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is None:
raise MissingValue()
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
if self.value is None: # pragma: no cover
raise MissingValue()
el = reference(refid=self.value)
el.extend(children)
yield el
class LinkTag(Tag):
"""Tag for linking to an external resource.
Example uses::
[url=https://example.org/hi]Go to example.org[/url]!
[url=/Fr/index.php][/url]
[url]https://random.org/randomize.php[/url]
See :ref:`markup-url` for more information.
"""
__slots__ = ("url",)
url: str | None
"""The stored URL for the link tag."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
self.url = None
if self.value is None:
return None
self.url = self.process_url(self.value)
if self.url is None:
raise InvalidValue("Not a valid URL: {url}", url=self.value)
def is_raw(self) -> bool:
"""Return whether the content of this tag should be read as raw.
:return: Whether the tag should be read as raw or not.
"""
return self.value is None
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
url = self.value
if url is None:
orig_url = self.get_text_from_raw_children(children)
url = self.process_url(orig_url)
if url is None:
raise ValueError(f"Not a valid URL: {orig_url}")
ref = reference(refuri=url)
ref.extend(children)
yield ref
def process_url(self, url: str) -> str | None:
"""Process the URL.
:param url: The URL to process.
:return: The adapted URL, or :py:data:`None` if the URL is invalid.
"""
for prefix in ("http://", "https://", "ftp://", "ftps://", "/"):
if url.startswith(prefix):
return url
return None
class ProfileTag(LinkTag):
"""Tag for linking to a profile for the current site.
This tag was originally made for Planète Casio's profiles.
It adds a prefix to the content, and sets the value.
Example uses::
[profil]Cakeisalie5[/]
See :ref:`markup-url` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process_url(self, url: str) -> str | None:
"""Process the URL.
:param url: The URL to process.
:return: The adapted URL, or :py:data:`None` if the URL is invalid.
"""
if any(
car not in "abcdefghijklmnopqrstuvwxyz0123456789_ -."
for car in url
):
return None
return (
"https://www.planet-casio.com/Fr/compte/voir_profil.php?membre="
+ url
)
class TopicTag(LinkTag):
"""Tag for linking topics for the current site.
Originally made for Planète Casio's forum topics.
Example uses::
[topic]234[/]
See :ref:`markup-url` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process_url(self, url: str) -> str | None:
"""Process the URL.
:param url: The URL to process.
:return: The adapted URL, or :py:data:`None` if the URL is invalid.
"""
try:
topic_id = int(url)
except ValueError:
return None
return (
"https://www.planet-casio.com/Fr/forums/lecture_sujet.php?id="
+ str(topic_id)
)
class TutorialTag(LinkTag):
"""Tag for linking tutorials for the current site.
Originally made for Planète Casio's tutorials.
Example uses::
[tutorial]71[/]
See :ref:`markup-url` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process_url(self, url: str) -> str | None:
"""Process the URL.
:param url: The URL to process.
:return: The adapted URL, or :py:data:`None` if the URL is invalid.
"""
try:
tutorial_id = int(url)
except ValueError:
return None
return (
"https://www.planet-casio.com/Fr/programmation/tutoriels.php?id="
+ str(tutorial_id)
)
class ProgramTag(LinkTag):
"""Tag for linking programs for the current site.
Originally made for Planète Casio's programs.
Example uses::
[program]3598[/]
See :ref:`markup-url` for more information.
"""
__slots__ = ()
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is not None:
raise UnexpectedValue()
def process_url(self, url: str) -> str | None:
"""Process the URL.
:param url: The URL to process.
:return: The adapted URL, or :py:data:`None` if the URL is invalid.
"""
try:
tutorial_id = int(url)
except ValueError:
return None
return (
"https://www.planet-casio.com/Fr/programmes/"
+ "voir_un_programme_casio.php?showid="
+ str(tutorial_id)
)
class ProgressTag(Tag):
"""Tag for displaying a progress bar.
Example uses::
[progress=50]My great progress bar[/progress]
[progress=100][/progress]
See :ref:`markup-progress-bar` for more information.
"""
__slots__ = ("progress_value",)
progress_value: int
"""The progress value, between 0 and 100 included."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.value is None:
raise MissingValue("Expected an integer between 0 and 100.")
try:
self.progress_value = int(self.value)
except ValueError:
raise InvalidValue(
"Value should have been an integer between 0 and 100.",
)
if self.progress_value < 0 or self.progress_value > 100:
raise InvalidValue(
"Value should have been an integer between 0 and 100.",
)
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
result = progress(value=self.progress_value)
result.extend(children)
yield result
class RotTag(RawTag):
"""Tag for un-rot13-ing raw text and returning such text.
Example uses::
[rot=13]obawbhe[/rot]
[rot13]Obawbhe[/rot13]
"""
__slots__ = "_rot"
general_tag_names = ("[rot]",)
"""The accepted tag names for this tag, with an expected value."""
embedded_tag_pattern = re.compile(r"\[rot0*?([0-9]|1[0-9]|2[0-5])\]", re.I)
"""The compiled pattern for tag names with embedded rot values."""
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
if self.name in self.general_tag_names:
if self.value is None:
raise MissingValue()
try:
self._rot = int(self.value)
except ValueError:
raise InvalidValue("Expected a rot value between 0 and 25")
if self._rot < 0 or self._rot >= 26:
raise InvalidValue("Expected a rot value between 0 and 25")
return
m = self.embedded_tag_pattern.match(self.name)
if m is None:
raise ValueError(f"Unsupported tag name {self.name!r} for rot")
if self.value is not None:
raise UnexpectedValue()
self._rot = int(m.group(1))
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
text = self.get_text_from_raw_children(children)
result = str.translate(
text,
str.maketrans(
ascii_uppercase + ascii_lowercase,
ascii_uppercase[self._rot :]
+ ascii_uppercase[: self._rot]
+ ascii_lowercase[self._rot :]
+ ascii_lowercase[: self._rot],
),
)
yield Text(result)
class SpoilerTag(Tag):
"""Tag for hiding content at first glance.
This tag produces an element that requires the reader to click on a
button to read its content. It can help to contain "secret" elements,
such as solutions, source code, or various other things.
Example uses::
[spoiler]This is hidden![/spoiler]
[spoiler=Y a quelque chose de caché !|Ah, bah en fait non :)]:E
And it's multiline, [big]and formatted[/big], as usual :D[/spoiler]
See :ref:`markup-spoiler` for more information.
"""
__slots__ = ("_closed_title", "_opened_title")
def validate(self) -> None:
"""Validate the name and value for this tag.
:raises TagValidationError: The name and value combination is invalid.
"""
closed, opened = "", ""
if self.value is not None:
closed, _, opened = self.value.partition("|")
self._closed_title = closed or "Cliquez pour découvrir"
self._opened_title = opened or "Cliquez pour recouvrir"
def process(self, *, children: Sequence[Node]) -> Iterator[Node]:
"""Process the tag with children to build document nodes.
:param children: The children to process.
:return: The produced nodes.
"""
result = spoiler(
opened_title=self._opened_title,
closed_title=self._closed_title,
)
result.extend(children)
yield result