"""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 += "" 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 = "
= 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 += "
" 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")