"""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 ""
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 ""
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 "
"
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, "
")
return replaced_hline.replace("\newline", "
")
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 += "= 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 += " | "
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 += ""
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 += "
"
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 = ""
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 += "
"
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")