1055 lines
30 KiB
Python
1055 lines
30 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,
|
||
container,
|
||
emphasis,
|
||
image,
|
||
literal,
|
||
reference,
|
||
strong,
|
||
)
|
||
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 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]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__slots__ = ("kind",)
|
||
|
||
ALIGN_KEYS = {
|
||
"center": "center",
|
||
"centre": "center",
|
||
"left": "left",
|
||
"right": "right",
|
||
"justify": "justify",
|
||
}
|
||
|
||
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 CodeTag(RawTag):
|
||
"""Basic code tag, for displaying code.
|
||
|
||
Example uses::
|
||
|
||
[code]int main()
|
||
{
|
||
printf("hello, world");
|
||
}[/code]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__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...
|
||
"""
|
||
|
||
__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 reference(name=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]!
|
||
"""
|
||
|
||
__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()
|
||
|
||
yield reference(uri="#" + self.value)
|
||
yield from children
|
||
|
||
|
||
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]
|
||
"""
|
||
|
||
__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(uri=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[/]
|
||
"""
|
||
|
||
__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[/]
|
||
"""
|
||
|
||
__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[/]
|
||
"""
|
||
|
||
__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[/]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__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]
|
||
"""
|
||
|
||
__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
|