1186 lines
33 KiB
Python
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
|