Source code for lynguine.util.tex

import os
import re

from ..config.interface import Interface

interface = Interface.from_file(raise_error_if_not_found=False)

# Using list comprehensions and set for avoiding duplicate directories.
TEX_DIRECTORIES = list(set(["."] + interface.get("bibinputs", "").split(":") + interface.get("texinputs", "").split(":")))
if "TEXINPUTS" in os.environ:
    TEX_DIRECTORIES += os.environ["TEXINPUTS"].split(":")
if "BIBINPUTS" in os.environ:
    TEX_DIRECTORIES += os.environ["BIBINPUTS"].split(":")
TEX_DIRECTORIES = [os.path.expandvars(directory) for directory in TEX_DIRECTORIES]

[docs] def extract_bib_files(text): """ Extract all the bib files listed in the file lines. :param text: The text of the file to be parsed. :type text: str :return: The list of bib files. :rtype: list """ if isinstance(text, list): text = "\n".join(text) # Regular expressions for matching bibliography definitions patterns = [r'\\bibliography{([^}]*)}', r'\\begin{btSect}.*{([^}]*)}'] # Extract the bib files bib_files = set() for pattern in patterns: for match in re.findall(pattern, text): bib_files.update(match.split(',')) return list(bib_files)
[docs] def substitute_inputs(filename, directories=None): """ Take the base file and substitute in any input and include files. :param filename: The filename to be substituted. :type filename: str :param directories: The directories to search for the input files. :type directories: list :return: The substituted file. :rtype: str """ file_dir = os.path.dirname(filename) if directories == None: directories = [file_dir] filename = os.path.basename(filename) elif len(file_dir) > 0: if file_dir not in directories: directories.append(file_dir) for directory in TEX_DIRECTORIES: if directory not in directories: directories.append(directory) if filename[0] == "#": # it's a macro return None tex_file_handle = None for directory in directories: full_filename = os.path.join(directory, filename) if os.path.exists(full_filename): tex_file_handle = open(full_filename, "r") dirname = directory break if not tex_file_handle: return None lines = tex_file_handle.readlines() new_lines = "" # Avoid parsing defined commands in notation def. for line in lines: if not line[0] == "%": match_inp = re.compile(r"""\\newsection *{([^}]*)} *{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(2)), directories) if subs: replace_string = ( "\\section{" + match.group(1) + "}" + "\n" * 2 + subs ) line = line.replace(match.group(0), replace_string) match_inp = re.compile(r"""\\newsubsection *{([^}]*)} *{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(2)), directories) if subs: replace_string = ( "\\subsection{" + match.group(1) + "}" + "\n" * 2 + subs ) line = line.replace(match.group(0), replace_string) match_inp = re.compile(r"""\\inputdiagram{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(1)), directories) if subs: replace_string = "\\small" + subs + "\\vspace{0.5cm}" line = line.replace(match.group(0), replace_string) match_inp = re.compile(r"""\\input{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(1)), directories) if subs: line = line.replace(match.group(0), subs) match_inp = re.compile(r"""\\include{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(1)), directories) if subs: line = line.replace(match.group(0), subs) match_inp = re.compile(r"""\\includetalkfile{([^}]*)}""") for match in match_inp.finditer(line): subs = substitute_inputs(input_file_name(match.group(1)), directories) if subs: line = line.replace(match.group(0), subs) new_lines += line return new_lines
[docs] def input_file_name(filename, extension=".tex"): """ Return the filename with the extension if it exists. :param filename: The filename to be checked. :type filename: str :param extension: The extension to be checked. :type extension: str :return: The filename with the extension if it exists. :rtype: str """ ext_list = ["md", "tex"] if os.path.exists(filename): return filename else: for ext in ext_list: if os.path.exists(filename + "." + ext): return filename + "." + ext
[docs] def process_file(filename, extension=".tex"): """ Process a file and return the lines. :param filename: The filename to be processed. :type filename: str :param extension: The extension to be processed. :type extension: str :return: The lines of the processed file. :rtype: list """ tex_file_handle = open(filename, "r") lines = tex_file_handle.readlines() inp_list = extract_inputs(lines) for inp in inp_list: if not inp[0] == "#": if inp[-len(extension) :] == extension: lines += process_file(inp, extension) else: lines += process_file(inp + extension, extension) return lines
[docs] def extract_inputs(text): """ Extract latex file dependencies. :param text: The text of the file to be processed. :type text: str or list of str (for backwards compatability) :return: The list of files. :rtype: list """ if isinstance(text, str): lines = text.split("\n") else: lines = text def extract_input(line, matchstr): line_inp = re.compile(matchstr).findall(line) inp_list = [] if line_inp: for inp in line_inp: inp_list = inp_list + inp.split(",") return inp_list inp_list = [] for line in lines: inp_list += extract_input(line, r"""\\newsection *{[^}]*} *{([^}]*)}""") inp_list += extract_input(line, r"""\\newsubsection *{[^}]*} *{([^}]*)}""") inp_list += extract_input(line, r"""\\includetalkfile{([^}]*)}""") inp_list += extract_input(line, r"""\\input *{([^}]*)}""") inp_list += extract_input(line, r"""\\include *{([^}]*)}""") return inp_list
_DEFINE_MACRO_RE = re.compile(r"""\\define\{([^}]*)\}\{([^}]*)}""") _CONCAT_MACRO_RE = re.compile(r"""\\concat\{([^}]*)\}\{([^}]*)}""") def _extract_balanced_brace_content(text, open_brace_index): """ Return the content inside balanced braces starting at ``open_brace_index``. :param text: Source text. :type text: str :param open_brace_index: Index of the opening ``{``. :type open_brace_index: int :return: Inner content without the surrounding braces, or None. :rtype: str or None """ if open_brace_index >= len(text) or text[open_brace_index] != "{": return None depth = 0 for index in range(open_brace_index, len(text)): if text[index] == "{": depth += 1 elif text[index] == "}": depth -= 1 if depth == 0: return text[open_brace_index + 1 : index] return None def _extract_braced_command_arguments(line, command): """ Extract first braced arguments for each ``command`` occurrence on a line. Optional ``[...]`` and ``<...>`` modifiers between the command and the opening brace are skipped. """ arguments = [] start = 0 while True: index = line.find(command, start) if index == -1: break pos = index + len(command) while pos < len(line) and line[pos] in " \t": pos += 1 if pos < len(line) and line[pos] == "[": closing = line.find("]", pos) if closing == -1: start = index + 1 continue pos = closing + 1 while pos < len(line) and line[pos] in " \t": pos += 1 if pos < len(line) and line[pos] == "<": closing = line.find(">", pos) if closing == -1: start = index + 1 continue pos = closing + 1 while pos < len(line) and line[pos] in " \t": pos += 1 if pos < len(line) and line[pos] == "[": closing = line.find("]", pos) if closing == -1: start = index + 1 continue pos = closing + 1 while pos < len(line) and line[pos] in " \t": pos += 1 if pos < len(line) and line[pos] == "{": content = _extract_balanced_brace_content(line, pos) if content is not None: arguments.append(content) start = index + 1 return arguments
[docs] def collect_define_macros(lines): """ Collect gpp-style ``\\define{name}{value}`` macros from file lines. Supports ``\\define{\\name}{value}`` and ``\\define{name}{value}``. Later definitions in the same file override earlier ones. :param lines: File lines or a single string. :type lines: list or str :return: Mapping from macro name (without leading backslash) to value. :rtype: dict """ if isinstance(lines, str): text = lines else: text = "".join(lines) macros = {} for match in _DEFINE_MACRO_RE.finditer(text): name = match.group(1).strip() if name.startswith("\\"): name = name[1:] macros[name] = match.group(2).strip() return macros
[docs] def expand_diagram_path(path, macros, max_depth=20): """ Expand a bounded subset of gpp macros used in diagram paths. Supported forms: - ``\\name`` substitution using macros from :func:`collect_define_macros` - ``\\concat{left}{right}`` (including nested ``\\concat``) This is not a general gpp preprocessor. Paths that still contain unsupported ``\\`` tokens after expansion are left unchanged so callers can skip them. Nested ``\\concat`` inside a ``\\concat`` argument is not supported; use a single ``\\concat`` with ``\\define`` macros instead. :param path: Diagram path string, typically from :func:`extract_diagrams`. :type path: str :param macros: Macro name to replacement value mapping. :type macros: dict :param max_depth: Maximum ``\\concat`` expansion iterations. :type max_depth: int :return: Path with supported macros expanded. :rtype: str """ if not macros and "\\concat" not in path: return path if max_depth <= 0: return path def substitute_names(fragment): result = fragment for name, value in sorted(macros.items(), key=lambda item: len(item[0]), reverse=True): result = result.replace("\\" + name, value) return result path = substitute_names(path) match = _CONCAT_MACRO_RE.search(path) if match: left = expand_diagram_path(match.group(1), macros, max_depth - 1) right = expand_diagram_path(match.group(2), macros, max_depth - 1) path = path[: match.start()] + left + right + path[match.end() :] return expand_diagram_path(path, macros, max_depth - 1) return path
[docs] def extract_diagrams(lines, type="all"): """ Extract all the diagrams listed in the file. :param lines: The lines of the file to be processed. :type lines: list :param type: The type of diagrams to be extracted. :type type: str :return: The list of diagrams. :rtype: list """ diagram_list = [] rebases = {} rebases["diagram"] = [ r"\includediagram", r"\includediagramclass", r"\inlinediagram", r"\inputdiagram", ] rebases["img"] = [r"\includeimg"] rebases["png"] = [r"\includepng"] rebases["gif"] = [r"\includegif"] rebases["jpg"] = [r"\includejpg"] all_val = [] for key in rebases: all_val += rebases[key] rebases["all"] = all_val for rebase in rebases[type]: for line in lines: for argument in _extract_braced_command_arguments(line, rebase): diagram_list += argument.split(",") return diagram_list
[docs] def extract_citations(lines): """ Extract all the citations listed in the file lines. :param lines: The lines of the file to be processed. :type lines: list :return: The list of citations. :rtype: list """ citations_list = [] match_cite = re.compile(r"""\\cite[^\{]*{([^}\\#]+)}""") full_text = "" for line in lines: full_text += line line_cite = match_cite.findall(full_text) if line_cite: for cite in line_cite: citations_list = citations_list + cite.split(",") for i in range(len(citations_list)): for j in range(i + 1, len(citations_list)): if citations_list[i] == citations_list[j]: citations_list[j] = [] return citations_list
[docs] def make_bib_file(citations_list, bib_files): """ Create a new bibliography file for a given list of citations. :param citations_list: The list of citations. :type citations_list: list :param bib_files: The list of bib files. :type bib_files: list :return: The new bibliography file. :rtype: str """ if citations_list: citations_list.sort() cross_ref_list = [] string_list = [] out = "" # Get the location of the bibfiles bib_dir = TEX_DIRECTORIES # Regular expressions match_bib_field = re.compile(r"""(\@\w+{)""") match_cross_ref = re.compile( r"""\bcrossref\s*=\s*[\"|{](.*)[}|\"]""", re.IGNORECASE ) match_string = re.compile(r"""\b\w*\s*=\s*(\w[^0-9]\w*),""") for dir in bib_dir: if not dir: dir = "." for filename in bib_files: if os.access(os.path.join(dir, filename) + ".bib", os.R_OK): bib_file_handle = open(os.path.join(dir, filename) + ".bib", "r") bib_file = bib_file_handle.read() # Split the bib file at the entries. bib_comp = match_bib_field.split(bib_file) for i in range(len(citations_list)): if citations_list[i]: if i > 0 and citations_list[i] == citations_list[i - 1]: citations_list[i] = [] continue for j in range(2, len(bib_comp)): entry = bib_comp[j].split(",") entry = entry[0].split("=") if not entry[0].find(citations_list[i]) == -1: # print entry[0] # Adds the entry to output out = out + bib_comp[j - 1] + bib_comp[j] # Removes the citation from the list citations_list[i] = [] cross_refs = match_cross_ref.findall(bib_comp[j]) if cross_refs: cross_ref_list = cross_ref_list + cross_refs else: strings = match_string.findall(bib_comp[j]) if strings: for string in strings: if not string_list.count(string): string_list.append(string) break return ( get_bib_strings(string_list, bib_files) + out + get_bib_cross_refs(cross_ref_list, bib_files) ) else: return ""
[docs] def get_bib_strings(string_list, bib_files): """ Create a new bibliography file for a given list of bibtex strings. :param string_list: The list of bibtex strings. :type string_list: list :param bib_files: The list of bib files. :type bib_files: list :return: The new bibliography file. :rtype: str """ if string_list: return make_bib_file(string_list, bib_files) else: return ""
[docs] def get_bib_cross_refs(string_list, bib_files): """ Create a new bibliography file for a given list of cross references. :param string_list: The list of cross references. :type string_list: list :param bib_files: The list of bib files. :type bib_files: list :return: The new bibliography file. :rtype: str """ if string_list: return make_bib_file(string_list, bib_files) else: return ""
[docs] def create_bib_file_given_tex(lines): """ Create a new bibliography file for a given latex file. :param lines: The lines of the file to be processed. :type lines: list :return: The new bibliography file. :rtype: str """ bib_files = extract_bib_files(lines) citations_list = extract_citations(lines) return make_bib_file(citations_list, bib_files)