feat: update somethings
This commit is contained in:
@@ -1,9 +1,20 @@
|
|||||||
import cli.baguthesis
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
import latex2xthesis
|
||||||
|
import xthesis2docx
|
||||||
|
from cli.baguthesis import LaTeX2DocxCli, parse_cli
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(opts: LaTeX2DocxCli):
|
||||||
args = cli.baguthesis.parse_cli()
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create a temporary directory and XThesis intermediate file in it
|
||||||
|
temp_dir_path = Path(temp_dir).resolve()
|
||||||
|
temp_xthesis_path = temp_dir_path / "temp.xthesis"
|
||||||
|
# Break the options into frontend and backend options and run them respectively
|
||||||
|
frontend_opts, backend_opts = opts.break_into(temp_xthesis_path)
|
||||||
|
latex2xthesis.main(frontend_opts)
|
||||||
|
xthesis2docx.main(backend_opts)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main(parse_cli())
|
||||||
|
|||||||
54
src/common.py
Normal file
54
src/common.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from logger import LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class BaGuException(Exception):
|
||||||
|
"""The exception raised by this project."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_resource(tobe_resolved: str, resource_dir: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Resolve given path in resource directory or current work directory.
|
||||||
|
|
||||||
|
This function will use absolute path directly if it is and it is exist.
|
||||||
|
If not, this function will try to resolve it in resource directory first,
|
||||||
|
and then in current work directory.
|
||||||
|
|
||||||
|
:param tobe_resolved: The path to be resolved.
|
||||||
|
:param resource_dir: The resource directory.
|
||||||
|
:return: The resolved path if resolved, otherwise None.
|
||||||
|
"""
|
||||||
|
tobe_resolved_path = Path(tobe_resolved)
|
||||||
|
LOGGER.debug(f'Resolving {tobe_resolved_path} ...')
|
||||||
|
|
||||||
|
# Return absolute path directly
|
||||||
|
if tobe_resolved_path.is_absolute():
|
||||||
|
if tobe_resolved_path.is_file():
|
||||||
|
LOGGER.debug(f'Resolved {tobe_resolved_path}')
|
||||||
|
return tobe_resolved_path
|
||||||
|
else:
|
||||||
|
LOGGER.debug(f'{tobe_resolved_path} is absolute path but not a file.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Resolve it in resource directory first
|
||||||
|
resource_dir_path = Path(resource_dir).resolve()
|
||||||
|
resolved = resource_dir_path / tobe_resolved_path
|
||||||
|
if resolved.is_file():
|
||||||
|
LOGGER.debug(f'Resolved {resolved}')
|
||||||
|
return resolved
|
||||||
|
else:
|
||||||
|
LOGGER.debug(f'Resolved failed in resource path because {resolved} is not a file.')
|
||||||
|
|
||||||
|
# Resolve it in work directory
|
||||||
|
cwd_path = Path.cwd().resolve()
|
||||||
|
resolved = cwd_path / tobe_resolved_path
|
||||||
|
if resolved.is_file():
|
||||||
|
LOGGER.debug(f'Resolved {resolved}')
|
||||||
|
return resolved
|
||||||
|
else:
|
||||||
|
LOGGER.debug(f'Resolved failed in current work directory because {resolved} is not a file.')
|
||||||
|
|
||||||
|
# Not resolved
|
||||||
|
return None
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import cli.latex2xthesis
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = cli.latex2xthesis.parse_cli()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
74
src/latex2xthesis/__init__.py
Normal file
74
src/latex2xthesis/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterator
|
||||||
|
from pylatexenc import latexwalker
|
||||||
|
from cli.latex2xthesis import LaTeX2XThesisCli, parse_cli
|
||||||
|
from common import BaGuException
|
||||||
|
from logger import LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class LatexWalkerEnvironment:
|
||||||
|
|
||||||
|
__loaded_texs: set[Path]
|
||||||
|
"""The set storing all loaded tex files to avoid circular including"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__loaded_texs = set()
|
||||||
|
|
||||||
|
def register_tex_filename(self, filename: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Register tex file name as loaded.
|
||||||
|
|
||||||
|
:return: False if this file name is already registered, otherwise true.
|
||||||
|
"""
|
||||||
|
if filename in self.__loaded_texs:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.__loaded_texs.add(filename)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LatexWalker:
|
||||||
|
|
||||||
|
environment: LatexWalkerEnvironment
|
||||||
|
"""The environment of this walker"""
|
||||||
|
walker: latexwalker.LatexWalker | None
|
||||||
|
"""The underlying walker"""
|
||||||
|
|
||||||
|
def __init__(self, environment: LatexWalkerEnvironment, filename: Path) -> None:
|
||||||
|
# Check environment
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
# Try to load file.
|
||||||
|
try:
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
self.walker = latexwalker.LatexWalker(f.read())
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.warning(f'Fail to read LaTeX file: {filename}. Reason: {e}.')
|
||||||
|
self.walker = None
|
||||||
|
|
||||||
|
def iter(self) -> Iterator[latexwalker.LatexNode]:
|
||||||
|
if self.walker is not None:
|
||||||
|
# get node list
|
||||||
|
nodelists: list[latexwalker.LatexNode]
|
||||||
|
(nodelists, _, _) = self.walker.get_latex_nodes()
|
||||||
|
|
||||||
|
# special treat for command node for inserted document
|
||||||
|
for node in nodelists:
|
||||||
|
if isinstance(node, latexwalker.LatexMacroNode):
|
||||||
|
node.macroname
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
yield node
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main(opts: LaTeX2XThesisCli):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(parse_cli())
|
||||||
0
src/latex2xthesis/extractor.py
Normal file
0
src/latex2xthesis/extractor.py
Normal file
236
src/latex2xthesis/latexwalker.py
Normal file
236
src/latex2xthesis/latexwalker.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Iterator, Iterable
|
||||||
|
from typing import cast
|
||||||
|
from pylatexenc.latexwalker import (
|
||||||
|
LatexWalker as PyLatexWalker,
|
||||||
|
LatexNode as PyLatexNode,
|
||||||
|
LatexCommentNode as PyLatexCommentNode,
|
||||||
|
LatexMacroNode as PyLatexMacroNode,
|
||||||
|
)
|
||||||
|
from ..logger import LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class LatexWalker(Iterator[PyLatexNode]):
|
||||||
|
"""
|
||||||
|
The interface of all LaTeX walkers.
|
||||||
|
|
||||||
|
A LaTeX walker is an iterator that iterates over LaTeX nodes.
|
||||||
|
And for the convenience, we also provide a method `peek` to peek the next node without advancing the walker.
|
||||||
|
and an Rust-like iterator interface called `next` to fetch the next node and advance the walker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def peek(self) -> PyLatexNode | None:
|
||||||
|
"""
|
||||||
|
Peek the next node without advancing the walker.
|
||||||
|
|
||||||
|
:return: The next node or None if there is no more node.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def next(self) -> PyLatexNode | None:
|
||||||
|
"""
|
||||||
|
Fetch the next node and advance the walker.
|
||||||
|
|
||||||
|
:return: The next node or None if there is no more node.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[PyLatexNode]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> PyLatexNode:
|
||||||
|
node = self.next()
|
||||||
|
if node is None:
|
||||||
|
raise StopIteration
|
||||||
|
else:
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class FileLatexWalker(LatexWalker):
|
||||||
|
"""
|
||||||
|
A trivial implementation of LaTeX walker which only output LaTeX nodes one by one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
walker: PyLatexWalker | None
|
||||||
|
"""The underlying walker"""
|
||||||
|
nodelist: list[PyLatexNode]
|
||||||
|
"""The list of nodes provided by underlying walker"""
|
||||||
|
cnt: int
|
||||||
|
"""The count of all nodes"""
|
||||||
|
i: int
|
||||||
|
"""The current index"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, walker: PyLatexWalker | None, nodelist: list[PyLatexNode]
|
||||||
|
) -> None:
|
||||||
|
self.walker = walker
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.cnt = len(nodelist)
|
||||||
|
self.i = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_file(filename: Path) -> "FileLatexWalker":
|
||||||
|
# Try to load file.
|
||||||
|
walker: PyLatexWalker | None
|
||||||
|
try:
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
walker = PyLatexWalker(f.read())
|
||||||
|
(nodelist, _, _) = walker.get_latex_nodes()
|
||||||
|
return FileLatexWalker(walker, nodelist)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.warning(f"Fail to read LaTeX file: {filename}. Reason: {e}.")
|
||||||
|
return FileLatexWalker(None, list())
|
||||||
|
|
||||||
|
def peek(self) -> PyLatexNode | None:
|
||||||
|
if self.walker is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.i >= self.cnt:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.nodelist[self.i]
|
||||||
|
|
||||||
|
def next(self) -> PyLatexNode | None:
|
||||||
|
node = self.peek()
|
||||||
|
if node is not None:
|
||||||
|
self.i += 1
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class ResolvingIncludeContext:
|
||||||
|
__loaded_texs: set[Path]
|
||||||
|
"""The set storing all loaded tex files to avoid circular including"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__loaded_texs = set()
|
||||||
|
|
||||||
|
def test(self, filename: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Register tex file name as loaded.
|
||||||
|
|
||||||
|
:return: False if this file name is already registered, otherwise true.
|
||||||
|
"""
|
||||||
|
if filename in self.__loaded_texs:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.__loaded_texs.add(filename)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ResolvingIncludeLatexWalker(LatexWalker):
|
||||||
|
"""
|
||||||
|
A LaTeX walker wrapper that can resolve include command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
underlying: LatexWalker
|
||||||
|
"""The underlying walker"""
|
||||||
|
loaded: LatexWalker | None
|
||||||
|
"""The walker loaded by LaTeX include command"""
|
||||||
|
context: ResolvingIncludeContext
|
||||||
|
"""The context for resolving include command"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, underlying: LatexWalker, context: ResolvingIncludeContext
|
||||||
|
) -> None:
|
||||||
|
self.underlying = underlying
|
||||||
|
self.loaded = None
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
# def from_filename(filename: Path) -> 'ResolvingIncludeLatexWalker':
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# def from_base_tex(walker: LatexWalker) -> 'ResolvingIncludeLatexWalker':
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# def from_included_tex(walker: LatexWalker, context: ResolvingIncludeContext) -> 'ResolvingIncludeLatexWalker':
|
||||||
|
# pass
|
||||||
|
|
||||||
|
def peek(self) -> PyLatexNode | None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def next(self) -> PyLatexNode | None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Condition(ABC):
|
||||||
|
"""
|
||||||
|
A condition that can break a LaTeX walker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_break(self, node: PyLatexNode) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the given node can trigger the walker to break.
|
||||||
|
|
||||||
|
:param node: The node to check.
|
||||||
|
:return: True if the node can trigger the walker to break, otherwise false.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultiCommandCondition(Condition):
|
||||||
|
"""
|
||||||
|
A condition that can break a LaTeX walker when one of the certain commands is encountered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__commands: set[str]
|
||||||
|
"""The set of commands that can trigger the walker to break"""
|
||||||
|
|
||||||
|
def __init__(self, commands: Iterable[str]) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the condition with a set of commands.
|
||||||
|
|
||||||
|
:param commands: The commands that can trigger the walker to break.
|
||||||
|
"""
|
||||||
|
self.__commands = set(commands)
|
||||||
|
|
||||||
|
def can_break(self, node: PyLatexNode) -> bool:
|
||||||
|
if isinstance(node, PyLatexMacroNode):
|
||||||
|
macro_name = cast(str, node.macroname)
|
||||||
|
return macro_name in self.__commands
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CommentCondition(Condition):
|
||||||
|
"""
|
||||||
|
A condition that can break a LaTeX walker when a certain comment is encountered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__comment: str
|
||||||
|
"""The comment that can trigger the walker to break"""
|
||||||
|
|
||||||
|
def __init__(self, comment: str) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the condition with a comment.
|
||||||
|
|
||||||
|
:param comment: The comment that can trigger the walker to break.
|
||||||
|
"""
|
||||||
|
self.__comment = comment
|
||||||
|
|
||||||
|
def can_break(self, node: PyLatexNode) -> bool:
|
||||||
|
if isinstance(node, PyLatexCommentNode):
|
||||||
|
node_comment = cast(str, node.comment)
|
||||||
|
return node_comment == self.__comment
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionalLatexWalker(LatexWalker):
|
||||||
|
"""
|
||||||
|
A LaTeX walker wrapper that can break when a certain condition is met.
|
||||||
|
|
||||||
|
The node triggering the break is not consumed by this walker,
|
||||||
|
and can be accessed by the underlying walker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
underlying: LatexWalker
|
||||||
|
"""The underlying walker"""
|
||||||
|
condition: Condition
|
||||||
|
|
||||||
|
def __init__(self, underlying: LatexWalker, condition: Condition) -> None:
|
||||||
|
self.underlying = underlying
|
||||||
|
self.condition = condition
|
||||||
42
src/logger.py
Normal file
42
src/logger.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
def _build_logger() -> tuple[logging.Logger, logging.Handler]:
|
||||||
|
# Create a new logger which is independent with Flask
|
||||||
|
logger = logging.getLogger("my_console_logger")
|
||||||
|
# Avoid message was propagated to root logger or captured by Flask logger.
|
||||||
|
logger.propagate = False
|
||||||
|
# Set initial level.
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Create StreamHandler to output into stderr.
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.DEBUG)
|
||||||
|
# Set format for it.
|
||||||
|
formatter = logging.Formatter("[%(levelname)s] %(message)s")
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
# Add handler
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
return (logger, console_handler)
|
||||||
|
|
||||||
|
|
||||||
|
(LOGGER, CONSOLE_HANDLER) = _build_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerLevel(enum.IntEnum):
|
||||||
|
DEBUG = enum.auto()
|
||||||
|
INFO = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
def set_level(level: LoggerLevel) -> None:
|
||||||
|
logging_level: int = logging.INFO
|
||||||
|
match level:
|
||||||
|
case LoggerLevel.DEBUG:
|
||||||
|
logging_level = logging.DEBUG
|
||||||
|
case LoggerLevel.INFO:
|
||||||
|
logging_level = logging.INFO
|
||||||
|
|
||||||
|
LOGGER.setLevel(logging_level)
|
||||||
|
CONSOLE_HANDLER.setLevel(logging_level)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import cli.xthesis2docx
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = cli.xthesis2docx.parse_cli()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
9
src/xthesis2docx/__init__.py
Normal file
9
src/xthesis2docx/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from cli.xthesis2docx import XThesis2DocxCli, parse_cli
|
||||||
|
|
||||||
|
|
||||||
|
def main(opts: XThesis2DocxCli):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(parse_cli())
|
||||||
9
src/xthesis2docx/__main__.py
Normal file
9
src/xthesis2docx/__main__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from cli.xthesis2docx import XThesis2DocxCli, parse_cli
|
||||||
|
|
||||||
|
|
||||||
|
def main(opts: XThesis2DocxCli):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(parse_cli())
|
||||||
Reference in New Issue
Block a user