rougail-output-doc/src/rougail/output_doc/utils.py

392 lines
12 KiB
Python

"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2025
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import List
from io import BytesIO
from ruamel.yaml import YAML
import tabulate as tabulate_module
from tiramisu.error import display_list
from tabulate import tabulate
from .i18n import _
ROUGAIL_VARIABLE_TYPE = (
"https://rougail.readthedocs.io/en/latest/variable.html#variables-types"
)
ENTER = "\n\n"
DocTypes = {
"domainname": {
"params": {
"allow_startswith_dot": _("the domain name can starts by a dot"),
"allow_without_dot": _("the domain name can be a hostname"),
"allow_ip": _("the domain name can be an IP"),
"allow_cidr_network": _("the domain name can be network in CIDR format"),
},
},
"number": {
"params": {
"min_number": _("the minimum value is {0}"),
"max_number": _("the maximum value is {0}"),
},
},
"ip": {
"msg": "IP",
"params": {
"cidr": _("IP must be in CIDR format"),
"private_only": _("private IP are allowed"),
"allow_reserved": _("reserved IP are allowed"),
},
},
"hostname": {
"params": {
"allow_ip": _("the host name can be an IP"),
},
},
"web_address": {
"params": {
"allow_ip": _("the domain name in web address can be an IP"),
"allow_without_dot": _(
"the domain name in web address can be only a hostname"
),
},
},
"port": {
"params": {
"allow_range": _("can be range of port"),
"allow_protocol": _("can have the protocol"),
"allow_zero": _("port 0 is allowed"),
"allow_wellknown": _("ports 1 to 1023 are allowed"),
"allow_registred": _("ports 1024 to 49151 are allowed"),
"allow_private": _("ports greater than 49152 are allowed"),
},
},
"secret": {
"params": {
"min_len": _("minimum length for the secret"),
"max_len": _("maximum length for the secret"),
},
},
}
_yaml = YAML()
_yaml.indent(mapping=2, sequence=4, offset=2)
def dump(informations):
"""Dump variable, means transform bool, ... to yaml string"""
with BytesIO() as ymlfh:
_yaml.dump(informations, ymlfh)
ret = ymlfh.getvalue().decode("utf-8").strip()
if ret.endswith("..."):
ret = ret[:-3].strip()
return ret
def to_phrase(msg):
"""Add maj for the first character and ends with dot"""
if not msg:
# replace None to empty string
return ""
msg = str(msg).strip()
# a phrase must ends with a dot
if not msg.endswith("."):
msg += "."
# and start with a maj
return msg[0].upper() + msg[1:]
class CommonFormater:
"""Class with common function for formater"""
enter_table = "\n"
# tabulate module name
name = None
def __init__(self):
tabulate_module.PRESERVE_WHITESPACE = True
self.header_setted = False
# Class you needs implement to your Formater
def header(self):
"""Header of the documentation"""
raise NotImplementedError()
def title(
self,
title: str,
level: int,
) -> str:
"""Display family name as a title"""
raise NotImplementedError()
def join(
self,
lst: List[str],
) -> str:
"""Display line in table from a list"""
raise NotImplementedError()
def bold(
self,
msg: str,
) -> str:
"""Set a text to bold"""
raise NotImplementedError()
def stripped(
self,
text: str,
) -> str:
"""Return stripped text (as help)"""
raise NotImplementedError()
def list(
self,
choices: list,
) -> str:
"""Display a liste of element"""
raise NotImplementedError()
def prop(
self,
prop: str,
italic: bool,
) -> str:
"""Display property"""
raise NotImplementedError()
def link(
self,
comment: str,
link: str,
) -> str:
"""Add a link"""
raise NotImplementedError()
##################
def table_header(
self,
lst: list,
) -> tuple:
"""Manage the header of a table"""
return lst
def run(self, informations: dict, level: int) -> str:
"""Transform to string"""
msg = self.header()
if informations:
msg += self.dict_to_string(informations, level)
return msg
def dict_to_string(self, informations: dict, level: int) -> str:
"""Parse the dict to transform to dict"""
msg = ""
table_datas = []
ori_level = None
for value in informations.values():
if value["type"] == "namespace":
if ori_level is None:
ori_level = level
level += 1
msg += self.namespace_to_string(value["informations"], ori_level)
msg += self.dict_to_string(value["children"], level)
else:
if value["type"] == "variable":
self.variable_to_string(value, table_datas)
else:
if table_datas:
msg += self.table(table_datas)
msg += self.family_to_string(value["informations"], level)
msg += self.dict_to_string(value["children"], level + 1)
if table_datas:
msg += self.table(table_datas)
return msg
# FAMILY
def namespace_to_string(self, informations: dict, level: int) -> str:
"""manage namespace family"""
return self.title(
_('Variables for "{0}"').format(self.family_description(informations)),
level,
)
def family_to_string(self, informations: dict, level: int) -> str:
"""manage other family type"""
msg = self.title(self.family_description(informations), level)
calculated_properties = []
msg += self.property_to_string(informations, calculated_properties) + ENTER
if calculated_properties:
msg += self.join(calculated_properties) + ENTER
helps = informations.get("help")
if helps:
for help_ in helps:
msg += help_.strip() + ENTER
if "identifiers" in informations:
msg += self.section(_("Identifiers"), informations["identifiers"]) + ENTER
return msg
def family_description(self, informations: dict) -> str():
"""Get family name"""
if "description" in informations:
return informations["description"]
return display_list(
[
get_display_path(informations, index)
for index in range(len(informations["paths"]))
],
separator="or",
)
# VARIABLE
def variable_to_string(self, informations: dict, table_datas: dict) -> None:
"""Manage variable"""
calculated_properties = []
table_datas.append(
[
self.join(
self.variable_first_column(informations, calculated_properties)
),
self.join(
self.variable_second_column(informations, calculated_properties)
),
]
)
def variable_first_column(
self, informations: dict, calculated_properties: list
) -> list:
"""Collect string for the first column"""
first_col = [
self.join(
[
self.bold(get_display_path(informations, index))
for index in range(len(informations["paths"]))
]
),
self.property_to_string(informations, calculated_properties),
]
self.columns(first_col)
return first_col
def variable_second_column(
self, informations: dict, calculated_properties: list
) -> list:
"""Collect string for the second column"""
if "descriptions" in informations:
description = self.join(list(dict.fromkeys(informations["descriptions"])))
else:
description = to_phrase(
display_list(
list(dict.fromkeys(informations["names"])),
separator="or",
)
)
second_col = [self.stripped(description)]
for help_ in informations.get("help", []):
second_col.append(self.stripped(help_))
if "validators" in informations:
validators = informations["validators"]
if len(validators) == 1:
second_col.append(self.section(_("Validator"), validators[0]))
else:
second_col.append(self.section(_("Validators"), self.list(validators)))
if "choices" in informations:
second_col.append(self.section(_("Choices"), informations["choices"]))
if "default" in informations and informations.get("display_default", True):
second_col.append(self.section(_("Default"), informations["default"]))
if "examples" in informations:
examples = informations["examples"]
if len(examples) == 1:
second_col.append(self.section(_("Example"), examples[0]))
else:
second_col.append(self.section(_("Examples"), examples))
second_col.extend(calculated_properties)
self.columns(second_col)
return second_col
# OTHERs
def property_to_string(
self, informations: dict, calculated_properties: list
) -> str:
"""Transform properties to string"""
properties = []
for prop in informations.get("properties", []):
if prop["type"] == "type":
properties.append(self.link(prop["name"], ROUGAIL_VARIABLE_TYPE))
else:
if "annotation" in prop:
italic = True
calculated_properties.append(
self.section(prop["name"].capitalize(), prop["annotation"])
)
else:
italic = False
prop_str = self.prop(prop["name"], italic=italic)
properties.append(prop_str)
if not properties:
return ""
return " ".join(properties)
def columns(
self,
col: List[str], # pylint: disable=unused-argument
) -> None:
"""Manage column"""
return
def table(self, datas: list) -> str:
"""Transform list to a table in string format"""
msg = (
tabulate(
datas,
headers=self.table_header([_("Variable"), _("Description")]),
tablefmt=self._table_name,
)
+ "\n\n"
)
datas.clear()
return msg
def section(
self,
name: str,
msg: str,
) -> str:
"""Return something like Name: msg"""
if isinstance(msg, list):
if len(msg) == 1:
msg = msg[0]
else:
msg = self.list(msg)
if not isinstance(msg, str):
msg = dump(msg)
return _("{0}: {1}").format(self.bold(name), msg)
def get_display_path(informations: dict, index: int) -> str:
if "display_paths" in informations and index in informations["display_paths"]:
return informations["display_paths"][index]
return informations["paths"][index]