mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-11 00:27:08 +01:00
kuhy chose the strict branch: no per-file-ignores at all, not even the S101 block that ruff's own docs and the rest of this fleet carry for test code. So 125 test assertions were rewritten to raise instead of assert. Rather than 125 copies of `if x != y: raise AssertionError(...)`, each course grows a small expect module -- expect_equal / expect_in / expect_true / expect_false. That is also strictly better output than a bare assert here: pytest's assertion rewriting does not reach into a helper, so the helper has to print both values, and it does. The rewrite itself went through ast, not a regex, because seven of ECOTE's asserts were wrapped over several lines. Everything else in the two courses is gone as well: - 66 argument and 37 return annotations on the translator, worked out from what each function actually returns rather than stamped on as Any -- `list[str] | str` where a function returns "Error!" or the parsed columns. - 16 prints became logger.error with %-style arguments. The messages still reach stderr with no basicConfig, via logging's last-resort handler, so the translator behaves exactly as before. - Two `open()` calls became Path.open / Path.write_text, six RET504 assign-then-return pairs collapsed, one 380-character expected-HTML literal split into implicitly concatenated pieces (it cannot be wrapped any other way, which is what ISC001 being off is for), and one block of commented-out code deleted. - ECRYPT_PROJECT/__init__.py removed. It made the COURSE directory a package, which nothing imports, and it was the sole cause of `N999 Invalid module name: 'ECRYPT_PROJECT'`; with it gone, sieve is the top-level package and its name is fine. Verified by running both, not by reading: ECOTE 26 tests pass and `python -m translator.main --filename examples/complicated/table1.tex` still writes the same HTML with the same three column-count warnings on stderr; ECRYPT_PROJECT 99 tests pass. Repo-wide: 3,686 findings -> 3,159. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
534 lines
18 KiB
Python
534 lines
18 KiB
Python
"""Program for converting Latex files into html files."""
|
|
|
|
import argparse
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def command_name_check(latex_string: str, command_name: str) -> str:
|
|
"""Check if command name given from latex was spelled correctly."""
|
|
if latex_string[1 : (len(command_name) + 1)] != command_name:
|
|
logger.error("%sError! command misspeled!", latex_string)
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def generic_checks(latex_string: str) -> str:
|
|
"""Check that a string was given and that its parameters close.
|
|
|
|
"Close" means the latex parameter list ends with a curly bracket.
|
|
"""
|
|
if latex_string == "":
|
|
logger.error("%sError! No input given to function", latex_string)
|
|
return "Error!"
|
|
if "}" not in latex_string:
|
|
logger.error("%sError! curly bracket not closed!", latex_string)
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def generic_checks_command(latex_string: str) -> str:
|
|
r"""Run the generic checks, and also require a leading backslash."""
|
|
if generic_checks(latex_string) == "Error!":
|
|
return "Error!"
|
|
if latex_string[0] != "\\":
|
|
logger.error("%sError! has no slash at begining", latex_string)
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def document_class_only_checks(latex_string: str) -> str:
|
|
"""Check only errors connected to document class."""
|
|
if len(latex_string) < len("\\documentclass{"):
|
|
logger.error(
|
|
"%sError! latex string to short to be document class!", latex_string
|
|
)
|
|
return "Error!"
|
|
if latex_string[len("\\documentclass{") - 1] != "{":
|
|
logger.error("%sError! curly bracket not opened!", latex_string)
|
|
return "Error!"
|
|
if command_name_check(latex_string, "documentclass") == "Error!":
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def document_class(latex_string: str) -> str:
|
|
r"""Convert LaTeX documentclass method to html.
|
|
|
|
\documentclass{article}.
|
|
"""
|
|
if generic_checks_command(latex_string) == "Error!":
|
|
return "Error!"
|
|
if document_class_only_checks(latex_string) == "Error!":
|
|
return "Error!"
|
|
return "<!DOCTYPE html>"
|
|
|
|
|
|
def begin_document(latex_string: str) -> str:
|
|
r"""Convert LaTeX begin document method to html.
|
|
|
|
\begin{document}.
|
|
"""
|
|
if generic_checks_command(latex_string) == "Error!":
|
|
return "Error!"
|
|
if command_name_check(latex_string, "begin") == "Error!":
|
|
return "Error!"
|
|
return "<html>"
|
|
|
|
|
|
def begin_tabular(latex_string: str) -> str:
|
|
r"""Check if LaTeX begin tabular method is correct."""
|
|
if generic_checks_command(latex_string) == "Error!":
|
|
return "Error!"
|
|
if command_name_check(latex_string, "begin") == "Error!":
|
|
return "Error!"
|
|
return "<table>"
|
|
|
|
|
|
def tabular_parameters(latex_string: str) -> str:
|
|
r"""Check if LaTeX tabular environment has any parameters."""
|
|
if latex_string == "":
|
|
return ""
|
|
if latex_string == "[]":
|
|
return ""
|
|
return "Error!"
|
|
|
|
|
|
def only_pipes_and_space(latex_string: str) -> bool:
|
|
"""Check if latex string only contains | or " ".
|
|
|
|
if yes returns True, if no returns false.
|
|
"""
|
|
return all(char in ("|", " ", "{", "}") for char in latex_string)
|
|
|
|
|
|
def handle_complicated_parameter(
|
|
latex_string: str, return_array: list[str], i: int
|
|
) -> tuple[list[str], int, str]:
|
|
"""Handle single complicated parameter from loop."""
|
|
closing_bracket = latex_string.find("}", i + 1)
|
|
columns_string = latex_string[i : closing_bracket + 1]
|
|
result = tabular_columns_parameters(columns_string)
|
|
if result == "Error!":
|
|
return return_array, i, result
|
|
return_array.append(result)
|
|
i = closing_bracket
|
|
return return_array, i, ""
|
|
|
|
|
|
def main_tabular_parameters_loop_inside(
|
|
latex_string: str,
|
|
i: int,
|
|
simple_parameters_dictionary: dict[str, str],
|
|
return_array: list[str],
|
|
) -> tuple[list[str], int, str]:
|
|
"""Handle single tabular parameter character."""
|
|
character = latex_string[i]
|
|
if character in ["l", "c", "r", "|"]:
|
|
return_array.append(simple_parameters_dictionary.get(latex_string[i]))
|
|
i += 1
|
|
return return_array, i, ""
|
|
if character in ["p", "m", "b"]:
|
|
return handle_complicated_parameter(latex_string, return_array, i)
|
|
i += 1
|
|
return return_array, i, ""
|
|
|
|
|
|
def main_tabular_parameters_loop(
|
|
latex_string: str, simple_parameters_dictionary: dict[str, str]
|
|
) -> list[str] | str:
|
|
"""Convert simple and paragraph table parameters into html style code."""
|
|
return_array = []
|
|
i = 0
|
|
latex_string_length = len(latex_string)
|
|
while i < latex_string_length:
|
|
return_array, i, result = main_tabular_parameters_loop_inside(
|
|
latex_string, i, simple_parameters_dictionary, return_array
|
|
)
|
|
if result == "Error!":
|
|
return result
|
|
return return_array
|
|
|
|
|
|
def tabular_required_errors(latex_string: str) -> str:
|
|
"""Run the generic checks and.
|
|
|
|
checks if there are any parameters put to latex table.
|
|
"""
|
|
if generic_checks(latex_string) == "Error!":
|
|
return "Error!"
|
|
if latex_string == "{}":
|
|
logger.error(
|
|
"tabular_required_parameters: required table parameters are empty: %s",
|
|
latex_string,
|
|
)
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def simple_parameters_dictionary_return() -> dict[str, str]:
|
|
"""Return the html styles for the simple tabular parameters."""
|
|
return {
|
|
"l": "align='left'",
|
|
"c": "align='center'",
|
|
"r": "align='right'",
|
|
"|": "style='border-left: 1px solid black'",
|
|
}
|
|
|
|
|
|
def tabular_required_parameters(latex_string: str) -> list[str] | str:
|
|
"""Convert simple tabular parameters to html style commands."""
|
|
if tabular_required_errors(latex_string) == "Error!":
|
|
return "Error!"
|
|
simple_parameters_dictionary = simple_parameters_dictionary_return()
|
|
if only_pipes_and_space(latex_string):
|
|
logger.error(
|
|
"tabular_required_parameters: parameters are only pipes and spaces: %s",
|
|
latex_string,
|
|
)
|
|
return "Error!"
|
|
return main_tabular_parameters_loop(latex_string, simple_parameters_dictionary)
|
|
|
|
|
|
def length_conversions(latex_length: str) -> float | None:
|
|
"""Convert latex lengths to html lengths."""
|
|
length_dictionary = {
|
|
"pt": [1.3, "px"],
|
|
"mm": [1, "mm"],
|
|
"cm": [1, "cm"],
|
|
"in": [1, "in"],
|
|
"ex": [1, "ex"],
|
|
"em": [1, "em"],
|
|
}
|
|
return length_dictionary.get(latex_length, "Error!")
|
|
|
|
|
|
def construct_return_string(
|
|
vertical_align_type: str, final_length: float, conversed_unit: str
|
|
) -> str:
|
|
"""Construsts entire html style of parameters."""
|
|
return (
|
|
"style='"
|
|
+ vertical_align_type
|
|
+ " width: "
|
|
+ str(final_length)
|
|
+ conversed_unit[1]
|
|
+ ";'"
|
|
)
|
|
|
|
|
|
def return_parameter_dictionary() -> dict[str, str]:
|
|
"""Return html styles for each complicated latex parameter."""
|
|
return {
|
|
"p": "vertical-align: top;",
|
|
"m": "vertical-align: middle;",
|
|
"b": "vertical-align: bottom;",
|
|
}
|
|
|
|
|
|
def tabular_columns_parameters_errors(
|
|
latex_string: str, vertical_align_type: str
|
|
) -> str:
|
|
"""Check for intermediate errors.
|
|
|
|
Checks if parameter length starts with curly bracket.
|
|
"""
|
|
if vertical_align_type == "Error!":
|
|
logger.error(
|
|
"tabular_columns_parameters: unknown parameter: %s", latex_string[0]
|
|
)
|
|
return "Error!"
|
|
if latex_string[1] != "{":
|
|
logger.error(
|
|
"tabular_columns_parameters: parameter length does not start with {: %s",
|
|
latex_string,
|
|
)
|
|
return "Error!"
|
|
return ""
|
|
|
|
|
|
def string_length_no_whitespace(length_parameter: str) -> tuple[int, str]:
|
|
"""Measure the string width, ignoring whitespace."""
|
|
i = 0
|
|
length_value = ""
|
|
for character in length_parameter:
|
|
if character.isalpha():
|
|
break
|
|
length_value += character
|
|
i += 1
|
|
return i, length_value
|
|
|
|
|
|
def tabular_columns_parameters(latex_string: str) -> str:
|
|
"""Handle tabular paragraph parameters and translates them to html style."""
|
|
if generic_checks(latex_string) == "Error!":
|
|
return "Error!"
|
|
parameter_dictionary = return_parameter_dictionary()
|
|
vertical_align_type = parameter_dictionary.get(latex_string[0], "Error!")
|
|
if tabular_columns_parameters_errors(latex_string, vertical_align_type) == "Error!":
|
|
return "Error!"
|
|
length_parameter_with_bracket = latex_string.partition("{")[2]
|
|
length_parameter = length_parameter_with_bracket.partition("}")[0]
|
|
i, length_value = string_length_no_whitespace(length_parameter)
|
|
length_unit = length_parameter[i:]
|
|
conversed_unit = length_conversions(length_unit)
|
|
if conversed_unit == "Error!":
|
|
logger.error(
|
|
"tabular_columns_parameters: unit could not be converted: %s", latex_string
|
|
)
|
|
return "Error!"
|
|
final_length = round(float(length_value) * conversed_unit[0], 2)
|
|
return construct_return_string(vertical_align_type, final_length, conversed_unit)
|
|
|
|
|
|
def split_rows(latex_string: str) -> list[str]:
|
|
"""Split table row string into separate strings and puts them in array."""
|
|
double_backslash = "\\"
|
|
return latex_string.split(double_backslash)
|
|
|
|
|
|
def split_columns(table_row: str, column_count: int) -> list[str] | str:
|
|
"""Split table row columns into separate strings and puts them in array."""
|
|
columns = table_row.split("&")
|
|
if len(columns) != column_count and columns != [""]:
|
|
logger.error(
|
|
"Error! extracted columns length: %s and columns_count: %s is different!",
|
|
columns,
|
|
column_count,
|
|
)
|
|
return "Error!"
|
|
return columns
|
|
|
|
|
|
def translate_column(latex_column: str) -> str:
|
|
"""Translate insides of a single latex tabular column to html."""
|
|
hline_string_literal = "\\hline"
|
|
replaced_hline = latex_column.replace(hline_string_literal, "<hr>")
|
|
return replaced_hline.replace("\newline", "<br>")
|
|
|
|
|
|
def handle_line_strings(
|
|
line_string: str, column_style: list[str], column_number: int, return_string: str
|
|
) -> tuple[str, int]:
|
|
"""Convert lines untill there are no more lines."""
|
|
current_style = column_style[column_number]
|
|
if current_style == line_string:
|
|
while current_style == line_string:
|
|
return_string += line_string
|
|
column_number += 1
|
|
current_style = column_style[column_number]
|
|
return return_string, column_number
|
|
|
|
|
|
def handle_single_column(
|
|
column: str,
|
|
column_style: list[str],
|
|
column_number: int,
|
|
line_string: str,
|
|
return_string: str,
|
|
) -> tuple[str, int]:
|
|
"""Handle single column from columns loop."""
|
|
return_string += "<td "
|
|
if column_number >= len(column_style):
|
|
logger.error(
|
|
"Error! column_number index %s is past the end of column_style %s",
|
|
column_number,
|
|
column_style,
|
|
)
|
|
return "Error!", column_number
|
|
return_string, column_number = handle_line_strings(
|
|
line_string, column_style, column_number, return_string
|
|
)
|
|
|
|
return_string += column_style[column_number] + ">"
|
|
return_string += translate_column(column)
|
|
column_number += 1
|
|
return_string += "</td>"
|
|
return return_string, column_number
|
|
|
|
|
|
def handle_latex_columns(
|
|
columns: list[str],
|
|
column_style: list[str],
|
|
column_number: int,
|
|
line_string: str,
|
|
return_string: str,
|
|
) -> tuple[str, int]:
|
|
"""Go through every column in a row and translates it to html."""
|
|
if columns != "Error!":
|
|
for column in columns:
|
|
return_string, column_number = handle_single_column(
|
|
column, column_style, column_number, line_string, return_string
|
|
)
|
|
return return_string, column_number
|
|
|
|
|
|
def go_through_latex_rows(
|
|
rows: list[str],
|
|
return_string: str,
|
|
column_amount: int,
|
|
column_style: list[str],
|
|
line_string: str,
|
|
) -> str:
|
|
"""Go through latex table insides row by row."""
|
|
for row in rows:
|
|
return_string += "<tr>"
|
|
columns = split_columns(row, column_amount)
|
|
column_number = 0
|
|
return_string, column_number = handle_latex_columns(
|
|
columns, column_style, column_number, line_string, return_string
|
|
)
|
|
|
|
return_string += "</tr>"
|
|
return return_string
|
|
|
|
|
|
def translate_inside_to_html(latex_table_inside: str, column_style: list[str]) -> str:
|
|
"""Translate entire table insides to html."""
|
|
return_string = "<table>"
|
|
column_amount = 0
|
|
line_string = "style='border-left: 1px solid black'"
|
|
for style in column_style:
|
|
if style != line_string:
|
|
column_amount += 1
|
|
rows = split_rows(latex_table_inside)
|
|
return_string = go_through_latex_rows(
|
|
rows, return_string, column_amount, column_style, line_string
|
|
)
|
|
return_string += "</table>"
|
|
return return_string
|
|
|
|
|
|
def read_file(tex_filename: str) -> str:
|
|
"""Read a latex file into one string, with newlines stripped."""
|
|
with Path(tex_filename).open(encoding="UTF-8") as tex_file:
|
|
return tex_file.read().replace("\n", "")
|
|
|
|
|
|
def read_document_class(latex_full_string: str) -> tuple[str, str] | str:
|
|
"""Find documentclass in latex file and checks function correctness."""
|
|
document_class_index = latex_full_string.find("\\documentclass")
|
|
if document_class_index == -1:
|
|
logger.error("Main function error! documentclass not found")
|
|
return "Error!"
|
|
document_class_close_bracket = latex_full_string.find("}", document_class_index)
|
|
document_class_string = latex_full_string[
|
|
document_class_index : document_class_close_bracket + 1
|
|
]
|
|
latex_full_string = latex_full_string[
|
|
document_class_close_bracket + 1 : len(latex_full_string) - 1
|
|
]
|
|
return document_class(document_class_string), latex_full_string
|
|
|
|
|
|
def read_begin_document(latex_full_string: str) -> tuple[str, str]:
|
|
r"""Find the LaTeX \begin{document} command and return its html counterpart."""
|
|
begin_document_index = latex_full_string.find(r"\begin{document}")
|
|
if begin_document_index == -1:
|
|
logger.error("read_begin_document error! begin{document not found")
|
|
return "Error!", latex_full_string
|
|
begin_document_close_bracket = latex_full_string.find("}", begin_document_index)
|
|
begin_document_string = latex_full_string[
|
|
begin_document_index : begin_document_close_bracket + 1
|
|
]
|
|
latex_full_string = latex_full_string[
|
|
begin_document_close_bracket + 1 : len(latex_full_string) - 1
|
|
]
|
|
return_string = begin_document(begin_document_string)
|
|
return return_string, latex_full_string
|
|
|
|
|
|
def read_start(html_string: str, data: str) -> tuple[str, str]:
|
|
"""Find and read the latex document class.
|
|
|
|
and begin document and translates them to html.
|
|
"""
|
|
document_class_result, data = read_document_class(data)
|
|
if document_class_result == "Error!":
|
|
return "Error!", data
|
|
html_string += document_class_result
|
|
|
|
begin_document_results, data = read_begin_document(data)
|
|
if begin_document_results == "Error!":
|
|
return "Error!", data
|
|
html_string += begin_document_results
|
|
return html_string, data
|
|
|
|
|
|
def handle_table_whole(
|
|
html_string: str, data: str, table_start: int, table_end: int
|
|
) -> tuple[str, str] | str:
|
|
"""Read entire table, handles its parameters.
|
|
|
|
and insides and translates to html.
|
|
"""
|
|
if table_start == -1 or table_end == -1:
|
|
return html_string, data
|
|
tabular_begin_string = r"\begin{tabular}"
|
|
|
|
parameters_start_index = data.find("{", table_start + len(tabular_begin_string))
|
|
parameters_end_index = data.find("}", parameters_start_index)
|
|
parameters_string = data[parameters_start_index : parameters_end_index + 1]
|
|
parameters_array = tabular_required_parameters(parameters_string)
|
|
inside_table = data[parameters_end_index + 1 : table_end]
|
|
html_string += translate_inside_to_html(inside_table, parameters_array)
|
|
return html_string
|
|
|
|
|
|
def handle_insides(html_string: str, data: str) -> tuple[str, str]:
|
|
"""Find inside of table and translates them to html."""
|
|
tabular_index = 0
|
|
tabular_end_string = r"\end{tabular}"
|
|
while tabular_index != -1:
|
|
tabular_index_start = data.find(r"\begin{tabular}", 0)
|
|
tabular_index_end = data.find(tabular_end_string, tabular_index_start)
|
|
if tabular_index_start == -1 or tabular_index_end == -1:
|
|
return html_string, data
|
|
html_string += data[0:tabular_index_start]
|
|
|
|
html_string = handle_table_whole(
|
|
html_string, data, tabular_index_start, tabular_index_end
|
|
)
|
|
data = data[tabular_index_end + len(tabular_end_string) : len(data)]
|
|
tabular_index = tabular_index_end
|
|
return html_string, data
|
|
|
|
|
|
def main_function(tex_filename: str) -> str:
|
|
"""Ran by default by program,.
|
|
|
|
gets latex file and translates to html.
|
|
"""
|
|
data = read_file(tex_filename)
|
|
html_string = ""
|
|
html_string, data = read_start(html_string, data)
|
|
if html_string == "Error!":
|
|
logger.error(
|
|
"main_function error!\n when reading documentclass or begin document"
|
|
)
|
|
return "Error"
|
|
html_string, data = handle_insides(html_string, data)
|
|
return html_string.replace("}", "")
|
|
|
|
|
|
def handle_arguments() -> str:
|
|
"""Parse the command line and return the .tex filename to translate."""
|
|
parser = argparse.ArgumentParser(description="Specify tex filename and path")
|
|
parser.add_argument(
|
|
"--filename",
|
|
"-f",
|
|
help="Specify tex filename, include path if needed ",
|
|
required=False,
|
|
default="texfile.tex",
|
|
)
|
|
args = parser.parse_args()
|
|
return args.filename
|
|
|
|
|
|
if __name__ == "__main__":
|
|
TEX_FILENAME = handle_arguments()
|
|
FINAL_HTML = main_function(TEX_FILENAME)
|
|
Path(TEX_FILENAME + ".html").write_text(FINAL_HTML, encoding="UTF-8")
|