├── BUILD ├── demo.gif ├── freemindlatex ├── example │ ├── BUILD │ ├── slides.mm │ └── report.mm ├── test_data │ ├── BUILD │ ├── additional_dollar.mm │ └── multi_layered_enums.mm ├── static_files │ ├── BUILD │ ├── report.tex │ ├── slides.tex │ └── authordate1-4.sty ├── compilation_client_lib_test.py ├── init_dir_lib.py ├── integration_test_lib.py ├── compilation_service.proto ├── BUILD ├── test_on_mm_files.py ├── compilation_client_lib.py ├── freemindlatex_app_main.py ├── compilation_server_lib.py └── convert_lib.py ├── bazel ├── requirements.txt ├── .travis.yml ├── .pylintrc ├── install.sh ├── .gitignore ├── WORKSPACE ├── README.md └── LICENSE /BUILD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuehuichao/freemind-latex/HEAD/demo.gif -------------------------------------------------------------------------------- /freemindlatex/example/BUILD: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "example", 3 | srcs = glob(["*"]), 4 | visibility = ["//visibility:public"], 5 | ) 6 | -------------------------------------------------------------------------------- /freemindlatex/test_data/BUILD: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "test_data", 3 | srcs = glob(["*"]), 4 | visibility = ["//visibility:public"], 5 | ) 6 | -------------------------------------------------------------------------------- /freemindlatex/static_files/BUILD: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "static_files", 3 | srcs = glob(["*"]), 4 | visibility = ["//visibility:public"], 5 | ) 6 | -------------------------------------------------------------------------------- /bazel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export USE_BAZEL_VERSION=1.0.1 3 | exec python -c "$(curl -L -s https://raw.githubusercontent.com/bazelbuild/bazelisk/master/bazelisk.py)" "$@" 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | python-gflags 3 | bibtexparser 4 | pypdf2 5 | timeout-decorator 6 | portpicker 7 | google-apputils 8 | futures 9 | gevent 10 | grpcio 11 | protobuf 12 | pypdf2 13 | timeout-decorator 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: python 4 | python: 5 | - "2.7" 6 | 7 | before_install: 8 | - sudo apt-get -qq update && sudo apt-get install -y texlive-full 9 | 10 | # command to run tests 11 | script: 12 | - ./bazel test ... 13 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=invalid-name,no-member,missing-docstring,unused-argument,fixme,bad-continuation,global-statement,too-many-branches,too-few-public-methods,too-many-public-methods,star-args,duplicate-code,locally-disabled,cell-var-from-loop 3 | max-args=6 4 | 5 | [FORMAT] 6 | indent-string=' ' 7 | max-line-length=80 8 | 9 | [REPORTS] 10 | reports=no -------------------------------------------------------------------------------- /freemindlatex/compilation_client_lib_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from freemindlatex import compilation_client_lib 4 | 5 | 6 | class TestGettingCompiledDocPath(unittest.TestCase): 7 | def testGettingCompiledDocPath(self): 8 | self.assertEquals( 9 | '/tmp/testdir/testdir.pdf', 10 | compilation_client_lib.LatexCompilationClient.GetCompiledDocPath( 11 | '/tmp/testdir')) 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -e 3 | 4 | if [[ $# -ne 1 ]]; then 5 | cat < 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /freemindlatex/compilation_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message LatexCompilationRequest { 4 | message FileInfo { 5 | string filepath = 1; 6 | bytes content = 2; 7 | } 8 | 9 | enum Mode { 10 | BEAMER = 0; 11 | REPORT = 1; 12 | HTML = 2; 13 | } 14 | 15 | repeated FileInfo file_infos = 1; 16 | Mode compilation_mode = 2; 17 | } 18 | 19 | message LatexCompilationResponse { 20 | enum Status { 21 | ERROR = 0; 22 | SUCCESS = 1; 23 | EMBEDDED = 2; 24 | // The attempt to fix it worked. Error messages are embedded into the compiled slides. 25 | CANNOTFIX = 3; 26 | // The attempt to fix it still failed. 27 | } 28 | Status status = 1; 29 | string source_code = 2; 30 | string compilation_log = 3; 31 | bytes pdf_content = 4; 32 | } 33 | 34 | service LatexCompilation { 35 | rpc CompilePackage(LatexCompilationRequest) returns(LatexCompilationResponse) {}; 36 | } 37 | 38 | message HealthCheckRequest { 39 | string service = 1; 40 | } 41 | 42 | message HealthCheckResponse { 43 | enum ServingStatus { 44 | UNKNOWN = 0; 45 | SERVING = 1; 46 | NOT_SERVING = 2; 47 | } 48 | ServingStatus status = 1; 49 | } 50 | 51 | service Health { 52 | rpc Check(HealthCheckRequest) returns(HealthCheckResponse); 53 | } 54 | -------------------------------------------------------------------------------- /freemindlatex/test_data/additional_dollar.mm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /freemindlatex/example/report.mm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /freemindlatex/static_files/report.tex: -------------------------------------------------------------------------------- 1 | \documentclass[CJK,12pt]{article} 2 | \usepackage[margin=1in]{geometry}% Change the margins here if you wish. 3 | \setlength{\parindent}{0pt} % This is the set the indent length for new paragraphs, change if you want. 4 | \setlength{\parskip}{5pt} % This sets the distance between paragraphs, which will be used anytime you have a blank line in your LaTeX code 5 | 6 | \usepackage[utf8x]{inputenc} 7 | \usepackage{CJK} 8 | \usepackage{verbatim} 9 | \usepackage{todonotes} 10 | \presetkeys{todonotes}{inline}{} 11 | 12 | \usepackage{url} 13 | \usepackage{graphicx} 14 | \usepackage{tikz} 15 | \usepackage[normalem]{ulem} 16 | \usepackage{authordate1-4} 17 | %\usepackage[round]{natbib} 18 | %\renewcommand{\cite}[1]{\citep{#1}} 19 | \newcommand{\newcite}[1]{\cite{#1}} 20 | %\newcommand{\shortcite}[1]{\citet{#1}} 21 | \usepackage{amsmath,amsthm,amssymb} 22 | \usepackage{centernot} 23 | \usepackage{xcolor} 24 | \definecolor{olive}{rgb}{0.3, 0.4, .1} 25 | \definecolor{fore}{RGB}{249,242,215} 26 | \definecolor{back}{RGB}{51,51,51} 27 | \definecolor{title}{RGB}{255,0,90} 28 | \definecolor{dgreen}{rgb}{0.,0.6,0.} 29 | \definecolor{gold}{rgb}{1.,0.84,0.} 30 | \definecolor{JungleGreen}{cmyk}{0.99,0,0.52,0} 31 | \definecolor{BlueGreen}{cmyk}{0.85,0,0.33,0} 32 | \definecolor{RawSienna}{cmyk}{0,0.72,1,0.45} 33 | \definecolor{Magenta}{cmyk}{0,1,0,0} 34 | 35 | \newcommand{\vectornorm}[1]{\left|\left|#1\right|\right|} 36 | 37 | % \AtBeginSubsection[] 38 | % { 39 | % \begin{frame} 40 | % \frametitle{Table of Contents} 41 | % \tableofcontents[currentsection,currentsubsection] 42 | % \end{frame} 43 | % } 44 | 45 | \begin{document} 46 | \begin{CJK*}{UTF8}{gbsn} 47 | 48 | \input{mindmap.tex} 49 | 50 | %\begin{frame}[allowframebreaks]{References} 51 | %\tiny 52 | %\bibliographystyle{authordate2} 53 | %\bibliography{bib.bib} % file name of the bibtex 54 | \end{CJK*} 55 | \end{document} 56 | -------------------------------------------------------------------------------- /freemindlatex/static_files/slides.tex: -------------------------------------------------------------------------------- 1 | \documentclass[CJK]{beamer} 2 | \usepackage[accumulated]{beamerseminar} 3 | % remove ``accumulated'' option 4 | % for original behaviour 5 | %%\usepackage{beamerthemeclassic} 6 | \usepackage[utf8x]{inputenc} 7 | \usepackage{CJK} 8 | \usepackage{verbatim} 9 | \usepackage{todonotes} 10 | \presetkeys{todonotes}{inline}{} 11 | 12 | \usetheme{boxes} 13 | \setbeamertemplate{frametitle}[default][center] 14 | 15 | \setbeamerfont{page number in head/foot}{size=\large} 16 | \setbeamertemplate{footline}[frame number] 17 | \usepackage{url} 18 | \usepackage{graphicx} 19 | \usepackage{tikz} 20 | \usepackage[normalem]{ulem} 21 | \usepackage{authordate1-4} 22 | %\usepackage[round]{natbib} 23 | %\renewcommand{\cite}[1]{\citep{#1}} 24 | \newcommand{\newcite}[1]{\cite{#1}} 25 | %\newcommand{\shortcite}[1]{\citet{#1}} 26 | \usepackage{amsmath} 27 | \usepackage{centernot} 28 | \usepackage{xcolor} 29 | \definecolor{olive}{rgb}{0.3, 0.4, .1} 30 | \definecolor{fore}{RGB}{249,242,215} 31 | \definecolor{back}{RGB}{51,51,51} 32 | \definecolor{title}{RGB}{255,0,90} 33 | \definecolor{dgreen}{rgb}{0.,0.6,0.} 34 | \definecolor{gold}{rgb}{1.,0.84,0.} 35 | \definecolor{JungleGreen}{cmyk}{0.99,0,0.52,0} 36 | \definecolor{BlueGreen}{cmyk}{0.85,0,0.33,0} 37 | \definecolor{RawSienna}{cmyk}{0,0.72,1,0.45} 38 | \definecolor{Magenta}{cmyk}{0,1,0,0} 39 | 40 | \newcommand{\vectornorm}[1]{\left|\left|#1\right|\right|} 41 | 42 | % \AtBeginSubsection[] 43 | % { 44 | % \begin{frame} 45 | % \frametitle{Table of Contents} 46 | % \tableofcontents[currentsection,currentsubsection] 47 | % \end{frame} 48 | % } 49 | 50 | \begin{document} 51 | \begin{CJK*}{UTF8}{gbsn} 52 | 53 | \input{mindmap.tex} 54 | 55 | %\begin{frame}[allowframebreaks]{References} 56 | %\tiny 57 | %\bibliographystyle{authordate2} 58 | %\bibliography{bib.bib} % file name of the bibtex 59 | \end{CJK*} 60 | \end{document} 61 | -------------------------------------------------------------------------------- /freemindlatex/test_data/multi_layered_enums.mm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 3 | 4 | http_archive( 5 | name = "freemind", 6 | url = "https://codingchitu.s3.amazonaws.com/freemind-bin-1.0.0.zip", 7 | type = "zip", 8 | sha256 = "c97b19bb9b021cc0987a85bf0fd8dffa127f02e7736cf7aaf9f86723305c9a7d", 9 | build_file_content = r""" 10 | filegroup( 11 | name = "freemind", 12 | srcs = glob([ 13 | "*", 14 | "**/*", 15 | ]), 16 | visibility = ["//visibility:public"], 17 | ) 18 | """, 19 | ) 20 | 21 | http_archive( 22 | name = "rules_python", 23 | url = "https://github.com/bazelbuild/rules_python/releases/download/0.0.1/rules_python-0.0.1.tar.gz", 24 | sha256 = "aa96a691d3a8177f3215b14b0edc9641787abaaa30363a080165d06ab65e1161", 25 | ) 26 | 27 | load("@rules_python//python:repositories.bzl", "py_repositories") 28 | 29 | py_repositories() 30 | 31 | # Only needed if using the packaging rules. 32 | load("@rules_python//python:pip.bzl", "pip_import", "pip_repositories") 33 | 34 | pip_repositories() 35 | 36 | pip_import( 37 | name = "py_deps", 38 | requirements = "//:requirements.txt", 39 | ) 40 | 41 | load("@py_deps//:requirements.bzl", "pip_install") 42 | 43 | pip_install() 44 | 45 | http_archive( 46 | name = "com_github_grpc_grpc", 47 | urls = [ 48 | "https://github.com/grpc/grpc/archive/v1.25.0.tar.gz", 49 | ], 50 | strip_prefix = "grpc-1.25.0", 51 | sha256 = "ffbe61269160ea745e487f79b0fd06b6edd3d50c6d9123f053b5634737cf2f69", 52 | ) 53 | 54 | load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") 55 | 56 | grpc_deps() 57 | 58 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") 59 | 60 | protobuf_deps() 61 | 62 | load("@upb//bazel:workspace_deps.bzl", "upb_deps") 63 | 64 | upb_deps() 65 | 66 | load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies") 67 | 68 | apple_rules_dependencies() 69 | 70 | load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies") 71 | 72 | apple_support_dependencies() 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/xuehuichao/freemind-latex.svg?branch=master)](https://travis-ci.org/xuehuichao/freemind-latex) 2 | 3 | 4 | # Focus on Ideas, and Slides will Follow 5 | This tool converts a mindmap into PDF slides (via LaTeX beamer). You can write [complex yet fine-tuned slides](http://www.xuehuichao.com/thesis_slides.pdf) with this tool. 6 | 7 | ![Focus on your idea, and slides will be generated automatically.](demo.gif) 8 | 9 | ### Usage 10 | Go to an empty directory and start editing it 11 | ```sh 12 | cd /path/to/your/document/directory 13 | python /path/to/freemindlatex.zip 14 | ``` 15 | 16 | It will bring up freemind for editing, evince for slides preview, and keep monitoring the file changes. While you edit the mindmap, slides content will refresh. 17 | 18 | ## Why not just PowerPoint? 19 | 20 | Tweaking fonts suck. But you do it all the time. 21 | 22 | * During the first round. 23 | * After you move slides. 24 | * After you add content. 25 | * After you indent a paragraph 26 | * ... 27 | 28 | With freemindlatex, we ask you to just focus on the logic. 29 | 30 | * Work on the outline, with freemind. 31 | * Auto formatting, with LaTeX beamer. 32 | * Bonus: LaTex math equations for free. 33 | 34 | 35 | ## Installation 36 | 37 | The software is packaged into a zip file. It supports Mac and linux. 38 | 39 | bash -c "$(curl -L -s https://raw.githubusercontent.com/xuehuichao/freemind-latex/master/install.sh)" -- /path/to/freemindlatex.zip 40 | 41 | ### Prerequisites 42 | 43 | By default, the tool opens a PDF viewer, and compiles LaTeX locally. We will need to 44 | install a PDF viewer as well as a LaTeX compiler. 45 | 46 | For LaTeX compiler, we need the full texlive (https://www.tug.org/texlive). 47 | 48 | 1. On MacOS: https://tug.org/mactex/ 49 | 2. On Ubuntu: `sudo apt-get install texlive-full` 50 | 51 | For PDF viewer: we need evince, or skim: 52 | 53 | 1. Evince, for linux: https://wiki.gnome.org/Apps/Evince 54 | 2. Skim, for MacOS: http://skim-app.sourceforge.net/ 55 | 56 | 57 | ### Running LaTeX remotely 58 | You may also use a remote server (e.g. sword.xuehuichao.com:8117) for LaTeX compilation. 59 | Then, instead of `freemindlatex`, please run `freemindlatex client` in your working directory. 60 | 61 | 62 | ## For development 63 | 64 | ### Testing 65 | ```sh 66 | bazel test ... 67 | ``` 68 | 69 | ### Code style checking 70 | ```sh 71 | find freemindlatex/ -name *.py | xargs pylint --rcfile=.pylintrc 72 | ``` 73 | -------------------------------------------------------------------------------- /freemindlatex/BUILD: -------------------------------------------------------------------------------- 1 | load("@py_deps//:requirements.bzl", "requirement") 2 | load("@com_github_grpc_grpc//bazel:python_rules.bzl", "py_grpc_library", "py_proto_library") 3 | 4 | py_library( 5 | name = "init_dir_lib", 6 | srcs = ["init_dir_lib.py"], 7 | data = [ 8 | "//freemindlatex/example", 9 | ], 10 | ) 11 | 12 | proto_library( 13 | name = "compilation_service_proto", 14 | srcs = ["compilation_service.proto"], 15 | ) 16 | 17 | py_proto_library( 18 | name = "compilation_service_pb2", 19 | deps = [":compilation_service_proto"], 20 | ) 21 | 22 | py_grpc_library( 23 | name = "compilation_service_pb2_grpc", 24 | srcs = [":compilation_service_proto"], 25 | deps = [":compilation_service_pb2"], 26 | ) 27 | 28 | py_library( 29 | name = "compilation_client_lib", 30 | srcs = ["compilation_client_lib.py"], 31 | data = [ 32 | "@freemind", 33 | ], 34 | deps = [ 35 | requirement("python-gflags"), 36 | ":compilation_service_pb2", 37 | ":compilation_service_pb2_grpc", 38 | requirement("grpcio"), 39 | ], 40 | ) 41 | 42 | py_test( 43 | name = "compilation_client_lib_test", 44 | srcs = ["compilation_client_lib_test.py"], 45 | deps = [ 46 | ":compilation_client_lib", 47 | ], 48 | python_version = "PY2", 49 | ) 50 | 51 | py_library( 52 | name = "convert_lib", 53 | srcs = ["convert_lib.py"], 54 | deps = [ 55 | requirement("python-gflags"), 56 | requirement("bibtexparser"), 57 | requirement("pyparsing"), 58 | requirement("future"), 59 | ], 60 | ) 61 | 62 | py_library( 63 | name = "compilation_server_lib", 64 | srcs = ["compilation_server_lib.py"], 65 | data = [ 66 | "@freemind", 67 | ], 68 | deps = [ 69 | ":compilation_service_pb2", 70 | ":compilation_service_pb2_grpc", 71 | ":convert_lib", 72 | requirement("futures"), 73 | requirement("grpcio"), 74 | ], 75 | ) 76 | 77 | py_binary( 78 | name = "freemindlatex_app_main", 79 | srcs = [ 80 | "freemindlatex_app_main.py", 81 | ], 82 | data = [ 83 | "//freemindlatex/static_files", 84 | ], 85 | deps = [ 86 | ":compilation_client_lib", 87 | ":compilation_server_lib", 88 | ":compilation_service_pb2", 89 | ":compilation_service_pb2_grpc", 90 | ":init_dir_lib", 91 | requirement("six"), 92 | requirement("portpicker"), 93 | ], 94 | python_version = "PY2", 95 | ) 96 | 97 | py_library( 98 | name = "integration_test_lib", 99 | testonly = 1, 100 | srcs = ["integration_test_lib.py"], 101 | data = [":freemindlatex_app_main"], 102 | ) 103 | 104 | py_test( 105 | name = "test_on_mm_files", 106 | srcs = ["test_on_mm_files.py"], 107 | data = ["//freemindlatex/test_data"], 108 | deps = [ 109 | ":init_dir_lib", 110 | ":integration_test_lib", 111 | ":compilation_service_pb2", 112 | ":compilation_service_pb2_grpc", 113 | requirement("pypdf2"), 114 | requirement("timeout-decorator"), 115 | ], 116 | python_version = "PY2", 117 | ) 118 | -------------------------------------------------------------------------------- /freemindlatex/static_files/authordate1-4.sty: -------------------------------------------------------------------------------- 1 | % This LaTeX style-option file, authordate1-4.sty, is intended for documents 2 | % that use the author-date citation system. When used in conjunction with 3 | % authordate1.bst, ... , authordate4.bst, it allows citations of the 4 | % form \shortcite{bloggs-60} as well as the usual \cite{bloggs-60}. 5 | % When 6 | % ... \cite{bloggs-60} ... \shortcite{bloggs-60} ... 7 | % appears in the input file, 8 | % ... (Bloggs, 1960) ... (1960) ... 9 | % appears in the final document. 10 | % 11 | % Note that: 12 | % - a command \bibtitle is defined, to generate the bibliography title 13 | % - a command \bibheadtitle is defined, to generate the text to be used 14 | % by \pagestyle{headings} 15 | % - \bibtitle and \bibheadtitle are set to generate "References" and 16 | % "REFERENCES" respectively. 17 | % - an \addcontentsline{toc}{...}{\bibtitle} is issued. 18 | % Thus: 19 | % - the default bibliography title is "References", but can be changed by 20 | % having \renewcommand{\bibtitle}{...} before \begin{thebibliography} 21 | % or \bibliography 22 | % - the default page header text is "REFERENCES", but can be changed by 23 | % \renewcommand{\bibheadtitle}{...} 24 | % - \tableofcontents will give a table of contents that mentions the 25 | % bibliography. 26 | % 27 | % David Rhead 28 | % Cripps Computing Centre 29 | % February 1990 30 | 31 | % Code taken from apalike.sty. 32 | % 33 | \def\@cite#1#2{(#1\if@tempswa , #2\fi)} 34 | \def\@biblabel#1{} 35 | 36 | % Set length of hanging indentation for bibliography entries 37 | % 38 | \newlength{\bibhang} 39 | \setlength{\bibhang}{2em} 40 | 41 | % Define default title and page-head text. 42 | % 43 | \def\bibtitle{References} 44 | \def\bibheadtitle{REFERENCES} 45 | 46 | \@ifundefined{chapter}{\def\thebibliography#1{\section*{\bibtitle\@mkboth 47 | {\bibheadtitle}{\bibheadtitle}} 48 | \addcontentsline{toc}{section}{\bibtitle}\list % For styles in 49 | {\relax}{\setlength{\labelsep}{0em} % which \chapter 50 | \setlength{\itemindent}{-\bibhang} % is undefined. 51 | \setlength{\leftmargin}{\bibhang}} 52 | \def\newblock{\hskip .11em plus .33em minus .07em} 53 | \sloppy\clubpenalty4000\widowpenalty4000 54 | \sfcode`\.=1000\relax}}% 55 | {\def\thebibliography#1{\chapter*{\bibtitle\@mkboth % For styles in 56 | {\bibheadtitle}{\bibheadtitle}} % which \chapter 57 | \addcontentsline{toc}{chapter}{\bibtitle}\list % is defined. 58 | {\relax}{\setlength{\labelsep}{0em} 59 | \setlength{\itemindent}{-\bibhang} 60 | \setlength{\leftmargin}{\bibhang}} 61 | \def\newblock{\hskip .11em plus .33em minus .07em} 62 | \sloppy\clubpenalty4000\widowpenalty4000 63 | \sfcode`\.=1000\relax}} 64 | % 65 | % Code taken from aaai.sty. 66 | % 67 | % don't box citations, add space between multiple citations, separate with ; 68 | \def\@citex[#1]#2{\if@filesw\immediate\write\@auxout{\string\citation{#2}}\fi 69 | \def\@citea{}\@cite{\@for\@citeb:=#2\do 70 | {\@citea\def\@citea{; }\@ifundefined 71 | {b@\@citeb}{{\bf ?}\@warning 72 | {Citation `\@citeb' on page \thepage \space undefined}}% 73 | {\csname b@\@citeb\endcsname}}}{#1}} 74 | % Allow short (name-less) citations, when used in 75 | % conjunction with a bibliography style that creates labels like 76 | % \citename{, } 77 | \let\@internalcite\cite 78 | \def\cite{\def\citename##1{##1}\@internalcite} 79 | \def\shortcite{\def\citename##1{}\@internalcite} 80 | -------------------------------------------------------------------------------- /freemindlatex/test_on_mm_files.py: -------------------------------------------------------------------------------- 1 | """Using the script on existing mm files""" 2 | 3 | import os 4 | import shutil 5 | import unittest 6 | 7 | import PyPDF2 8 | import timeout_decorator 9 | 10 | import integration_test_lib 11 | 12 | from freemindlatex import init_dir_lib 13 | from freemindlatex import compilation_service_pb2 14 | 15 | 16 | class TestBasicUsecase( 17 | integration_test_lib.ClientServerIntegrationTestFixture): 18 | """Our program compiles in working directories.""" 19 | 20 | def testCompilingInitialDirectory(self): 21 | """In a new directory, we will prepare an empty content to start with.""" 22 | doc_dir = os.path.join(self._test_dir, 'test_slides') 23 | os.mkdir(doc_dir) 24 | mode = compilation_service_pb2.LatexCompilationRequest.BEAMER 25 | init_dir_lib.InitDir(doc_dir, mode) 26 | # Compilation successful 27 | self.assertTrue( 28 | self._compilation_client.CompileDir(doc_dir, mode)) 29 | 30 | slides_file_loc = os.path.join(doc_dir, "test_slides.pdf") 31 | self.assertTrue(os.path.exists(slides_file_loc)) 32 | pdf_file = PyPDF2.PdfFileReader(open(slides_file_loc, "rb")) 33 | self.assertEquals(4, pdf_file.getNumPages()) 34 | self.assertIn("Author", pdf_file.getPage(0).extractText()) 35 | 36 | @timeout_decorator.timeout(5) 37 | def testCompilingReports(self): 38 | """When compiling in the report mode, renders report.pdf 39 | """ 40 | doc_dir = os.path.join(self._test_dir, 'test_report') 41 | os.mkdir(doc_dir) 42 | mode = compilation_service_pb2.LatexCompilationRequest.REPORT 43 | init_dir_lib.InitDir(doc_dir, mode) 44 | self.assertTrue( 45 | self._compilation_client.CompileDir(doc_dir, mode) 46 | ) 47 | 48 | report_file_loc = os.path.join(doc_dir, "test_report.pdf") 49 | self.assertTrue(os.path.exists(report_file_loc)) 50 | 51 | pdf_file = PyPDF2.PdfFileReader(open(report_file_loc, "rb")) 52 | self.assertEquals(1, pdf_file.getNumPages()) 53 | self.assertIn("Author", pdf_file.getPage(0).extractText()) 54 | 55 | 56 | class TestHandlingErrors( 57 | integration_test_lib.ClientServerIntegrationTestFixture): 58 | 59 | def _AssertErrorOnSecondPage(self, slides_file_loc, error_msg): 60 | self.assertTrue(os.path.exists(slides_file_loc)) 61 | pdf_file = PyPDF2.PdfFileReader(open(slides_file_loc, "rb")) 62 | 63 | self.assertEquals(4, pdf_file.getNumPages()) 64 | 65 | # Error message should appear on the 2nd page 66 | # Note that the extracted text don't have spaces, so I have to trim the 67 | # spaces 68 | self.assertIn( 69 | "".join( 70 | error_msg.split()), 71 | pdf_file.getPage(1).extractText()) 72 | 73 | # Other pages should remain intact 74 | self.assertIn("Author", pdf_file.getPage(0).extractText()) 75 | self.assertIn("Secondslide", pdf_file.getPage(2).extractText()) 76 | 77 | @timeout_decorator.timeout(5) 78 | def testOnMissingDollarSign(self): 79 | """Missing dollar sign causes Latex to error.""" 80 | mode = compilation_service_pb2.LatexCompilationRequest.BEAMER 81 | doc_dir = os.path.join(self._test_dir, 'test_slides') 82 | os.mkdir(doc_dir) 83 | init_dir_lib.InitDir(doc_dir, mode) 84 | shutil.copy(os.path.join( 85 | os.environ["TEST_SRCDIR"], 86 | "__main__/freemindlatex/test_data/additional_dollar.mm"), 87 | os.path.join(doc_dir, "mindmap.mm")) 88 | 89 | self.assertFalse(self._compilation_client.CompileDir(doc_dir, mode)) 90 | self.assertIn( 91 | "Missing $ inserted", 92 | open( 93 | os.path.join( 94 | doc_dir, 95 | "latex.log")).read()) 96 | 97 | self._AssertErrorOnSecondPage( 98 | os.path.join(doc_dir, "test_slides.pdf"), 99 | "Missing $ inserted") 100 | 101 | @timeout_decorator.timeout(5) 102 | def testOnFourLayersOfNestedEnums(self): 103 | """Latex does not support multi-layered enums. 104 | """ 105 | doc_dir = os.path.join(self._test_dir, 'test_slides') 106 | os.mkdir(doc_dir) 107 | mode = compilation_service_pb2.LatexCompilationRequest.BEAMER 108 | init_dir_lib.InitDir(doc_dir, mode) 109 | shutil.copy( 110 | os.path.join( 111 | os.environ["TEST_SRCDIR"], 112 | "__main__/freemindlatex/test_data/multi_layered_enums.mm"), 113 | os.path.join(doc_dir, "mindmap.mm")) 114 | self.assertFalse( 115 | self._compilation_client.CompileDir(doc_dir, mode)) 116 | self.assertIn( 117 | "Too deeply nested", 118 | open( 119 | os.path.join( 120 | doc_dir, 121 | "latex.log")).read()) 122 | 123 | self._AssertErrorOnSecondPage( 124 | os.path.join( 125 | doc_dir, 126 | "test_slides.pdf"), 127 | "Too deeply nested") 128 | 129 | 130 | if __name__ == "__main__": 131 | unittest.main() 132 | -------------------------------------------------------------------------------- /freemindlatex/compilation_client_lib.py: -------------------------------------------------------------------------------- 1 | """Client-side of the latex compilation service. 2 | """ 3 | 4 | import logging 5 | import os 6 | import time 7 | 8 | import gflags 9 | import grpc 10 | from freemindlatex import compilation_service_pb2, compilation_service_pb2_grpc 11 | 12 | gflags.DEFINE_string("watched_file_extensions", "mm,png,jpg", 13 | "Files extensions to watch for LaTeX compilation.") 14 | gflags.DEFINE_integer( 15 | "max_health_retries", 16 | 5, 17 | "Number of health check retries before giving up.") 18 | gflags.DEFINE_string("latex_error_log_filename", "latex.log", 19 | "Log file for latex compilation errors.") 20 | 21 | 22 | def _GetMTime(filename): 23 | """Get the time of the last modification. 24 | """ 25 | try: 26 | return os.path.getmtime(filename) 27 | except OSError as _: # file does not exist. 28 | return None 29 | 30 | 31 | def GetMTimeListForDir(directory): 32 | """Getting the modification time for all user files in a directory. 33 | 34 | Returns: a sorted list of pairs in form of ('file1', 1234567), where the paths 35 | are relative paths 36 | """ 37 | suffixes = [ 38 | '.%s' % 39 | i for i in gflags.FLAGS.watched_file_extensions.split(',')] 40 | mtime_list = [] 41 | for dirpath, _, filenames in os.walk(directory): 42 | for filename in [f for f in filenames if any( 43 | f.endswith(suf) for suf in suffixes)]: 44 | filepath = os.path.join(dirpath, filename) 45 | mtime_list.append( 46 | (os.path.relpath( 47 | filepath, 48 | directory), 49 | _GetMTime(filepath))) 50 | return sorted(mtime_list) 51 | 52 | 53 | class LatexCompilationClient(object): 54 | """Client-side of latex compilation. 55 | """ 56 | 57 | def __init__(self, server_address): 58 | self._channel = grpc.insecure_channel(server_address) 59 | self._healthz_stub = compilation_service_pb2_grpc.HealthStub(self._channel) 60 | self._compilation_stub = compilation_service_pb2_grpc.LatexCompilationStub( 61 | self._channel) 62 | 63 | def CheckHealthy(self): 64 | try: 65 | response = self._healthz_stub.Check( 66 | compilation_service_pb2.HealthCheckRequest()) 67 | except grpc.RpcError as e: 68 | if e.code() == grpc.StatusCode.UNAVAILABLE: 69 | return False 70 | raise 71 | return (response.status == 72 | compilation_service_pb2.HealthCheckResponse.SERVING) 73 | 74 | @staticmethod 75 | def GetCompiledDocPath(directory): 76 | """Get path to the compiled PDF file. 77 | 78 | Args: 79 | directory: The directory where the compilations happend. 80 | e.g. /tmp/testdir 81 | 82 | Returns: 83 | The file name of the output document, e.g. /tmp/testdir/testdir.pdf 84 | """ 85 | return os.path.join( 86 | directory, "{}.pdf".format(os.path.basename(directory))) 87 | 88 | def CompileDir(self, directory, mode): 89 | """Compiles the files in user's directory, and update the pdf file. 90 | 91 | The function will prepare the directory content, send it over for 92 | compilation. When there is a latex compilation error, we will put 93 | the latex error log at latex.log (or anything else specified by 94 | latex_error_log_filename). 95 | 96 | Args: 97 | directory: directory where user's files locate 98 | mode: the way to compile, 99 | e.g. compilation_service_pb2.LatexCompilationRequest.BEAMER 100 | 101 | Returns: boolean indicating if the compilation was successful. 102 | When unceccessful, leaves log files. 103 | """ 104 | 105 | filename_and_mtime_list = GetMTimeListForDir(directory) 106 | compilation_request = compilation_service_pb2.LatexCompilationRequest() 107 | for filename, _ in filename_and_mtime_list: 108 | with open(os.path.join(directory, filename)) as infile: 109 | new_file_info = compilation_request.file_infos.add() 110 | new_file_info.filepath = filename 111 | new_file_info.content = infile.read() 112 | compilation_request.compilation_mode = mode 113 | target_pdf_loc = self.GetCompiledDocPath(directory) 114 | 115 | response = self._compilation_stub.CompilePackage(compilation_request) 116 | if response.pdf_content: 117 | open(target_pdf_loc, 'w').write(response.pdf_content) 118 | 119 | if (response.status != 120 | compilation_service_pb2.LatexCompilationResponse.SUCCESS): 121 | latex_log_file = os.path.join( 122 | directory, gflags.FLAGS.latex_error_log_filename) 123 | with open(latex_log_file, 'w') as ofile: 124 | ofile.write(response.compilation_log) 125 | 126 | return (response.status == 127 | compilation_service_pb2.LatexCompilationResponse.SUCCESS) 128 | 129 | 130 | def WaitTillHealthy(server_address): 131 | """Wait until the server is healthy, with RPC calls. 132 | """ 133 | client = LatexCompilationClient(server_address) 134 | retries = 0 135 | while not client.CheckHealthy() and retries < gflags.FLAGS.max_health_retries: 136 | retries += 1 137 | logging.info("Compilation server not healthy yet (%d's retry)", retries) 138 | time.sleep(1) 139 | -------------------------------------------------------------------------------- /freemindlatex/freemindlatex_app_main.py: -------------------------------------------------------------------------------- 1 | """Command-line tool for compiling freemind document into a pdf document. 2 | 3 | Basic usage: 4 | Run "freemindlatex" in your working directory. 5 | 6 | It will create the freemind file for you, launch freemind and evince, then 7 | recompile the freemind file into slides upon your modifications. 8 | 9 | Advanced usages: 10 | freemindlatex local # Use your own computer for latex compilation 11 | freemindlatex --port 8000 server # Start the latex compilation server at a 12 | selected port 13 | freemindlatex --using_server localhost:8000 client # Compiles documents 14 | with a non-default server. 15 | """ 16 | 17 | import logging 18 | import os 19 | import platform 20 | import subprocess 21 | import sys 22 | import time 23 | 24 | import gflags 25 | import portpicker 26 | from freemindlatex import (compilation_client_lib, compilation_server_lib, 27 | compilation_service_pb2, init_dir_lib) 28 | 29 | gflags.DEFINE_string( 30 | "using_server", 31 | "sword.xuehuichao.com:8117", 32 | "The latex compilation server address, ip:port. When not specified, " 33 | "will start the server at an unused port.") 34 | gflags.DEFINE_integer("seconds_between_rechecking", 1, 35 | "Time between checking if files have changed.") 36 | gflags.DEFINE_integer( 37 | "port", 38 | None, 39 | "Port to listen to, for the compilation request. " 40 | "When not set, will pick a random port.") 41 | gflags.DEFINE_string( 42 | "dir", 43 | "", 44 | "Directory to run freemindlatex." 45 | ) 46 | gflags.DEFINE_string( 47 | "mode", 48 | "beamer", 49 | "Compiling mode: beamer, HTML or report") 50 | 51 | 52 | FLAGS = gflags.FLAGS 53 | 54 | _COMPILATION_MODE_MAP = { 55 | 'beamer': compilation_service_pb2.LatexCompilationRequest.BEAMER, 56 | 'report': compilation_service_pb2.LatexCompilationRequest.REPORT, 57 | } 58 | 59 | 60 | class UserExitedEditingEnvironment(Exception): 61 | pass 62 | 63 | 64 | def _LaunchViewerProcess(filename, log_file): 65 | """Launch the viewer application under the current platform 66 | 67 | Args: 68 | filename: the filename of the pdf file to view 69 | log_file: an already open, writable file object to write logs in. 70 | Returns: 71 | The subprocess of the viewer 72 | """ 73 | launch_base_command = [] 74 | if platform.system() == "Darwin": # MacOSX 75 | launch_base_command = ["open", "-W", "-a", "Skim"] 76 | elif platform.system() == "Linux": 77 | launch_base_command = ["evince"] 78 | 79 | return subprocess.Popen(launch_base_command + 80 | [filename], stdout=log_file, stderr=log_file) 81 | 82 | 83 | def RunEditingEnvironment(directory, server_address): 84 | """Start the editing/previewing/compilation environment, monitor file changes. 85 | 86 | Args: 87 | directory: the directory user is editing at 88 | server_address: address of latex compilation server, 89 | e.g. http://127.0.0.1:8000 90 | """ 91 | compilation_mode = _COMPILATION_MODE_MAP[gflags.FLAGS.mode] 92 | mindmap_file_loc = os.path.join(directory, 'mindmap.mm') 93 | if not os.path.exists(mindmap_file_loc): 94 | logging.info("Empty directory... Initializing it") 95 | init_dir_lib.InitDir(directory, compilation_mode) 96 | 97 | latex_client = compilation_client_lib.LatexCompilationClient(server_address) 98 | 99 | latex_client.CompileDir(directory, compilation_mode) 100 | freemind_log_path = os.path.join(directory, 'freemind.log') 101 | freemind_log_file = open(freemind_log_path, 'w') 102 | 103 | viewer_log_path = os.path.join(directory, 'viewer.log') 104 | viewer_log_file = open(viewer_log_path, 'w') 105 | 106 | compiled_doc_path = ( 107 | compilation_client_lib.LatexCompilationClient.GetCompiledDocPath(directory)) 108 | viewer_proc = _LaunchViewerProcess( 109 | os.path.join( 110 | directory, 111 | compiled_doc_path 112 | ), 113 | viewer_log_file) 114 | 115 | freemind_sh_path = os.path.realpath( 116 | os.path.join( 117 | os.path.dirname(sys.modules[__name__].__file__), 118 | "../../freemind/freemind.sh")) 119 | freemind_proc = subprocess.Popen( 120 | ['sh', freemind_sh_path, mindmap_file_loc], 121 | stdout=freemind_log_file, stderr=freemind_log_file, cwd=directory) 122 | 123 | mtime_list = compilation_client_lib.GetMTimeListForDir(directory) 124 | try: 125 | while True: 126 | time.sleep(FLAGS.seconds_between_rechecking) 127 | if freemind_proc.poll() is not None or viewer_proc.poll() is not None: 128 | raise UserExitedEditingEnvironment 129 | 130 | new_mtime_list = compilation_client_lib.GetMTimeListForDir(directory) 131 | if new_mtime_list != mtime_list: 132 | time.sleep(0.5) # Wait till files are fully written 133 | mtime_list = new_mtime_list 134 | latex_client.CompileDir(directory, compilation_mode) 135 | 136 | except KeyboardInterrupt as _: 137 | logging.info("User exiting with ctrl-c.") 138 | 139 | except UserExitedEditingEnvironment as _: 140 | logging.info("Exiting because one editing window has been closed.") 141 | 142 | finally: 143 | logging.info("Exiting freemindlatex ...") 144 | freemind_log_file.close() 145 | try: 146 | freemind_proc.kill() 147 | except OSError: 148 | pass 149 | try: 150 | viewer_proc.kill() 151 | except OSError: 152 | pass 153 | 154 | 155 | def main(): 156 | argv = FLAGS(sys.argv) 157 | logging.basicConfig( 158 | level=logging.INFO, 159 | format='%(levelname)s: %(threadName)s %(message)s') 160 | 161 | directory = FLAGS.dir or os.getcwd() 162 | 163 | if argv[1:] == ['server']: 164 | port = FLAGS.port or portpicker.pick_unused_port() 165 | compilation_server_lib.RunServerAtPort(port) 166 | 167 | elif argv[1:] == ['client']: 168 | if not FLAGS.using_server: 169 | logging.fatal( 170 | "Please specify the server address when running in the client mode " 171 | "via --using_server") 172 | RunEditingEnvironment(directory, server_address=FLAGS.using_server) 173 | 174 | elif argv[1:] == []: 175 | port = FLAGS.port or portpicker.pick_unused_port() 176 | server_proc = subprocess.Popen( 177 | ["python", argv[0], "--port", str(port), "server"]) 178 | server_address = '127.0.0.1:{}'.format(port) 179 | compilation_client_lib.WaitTillHealthy(server_address) 180 | try: 181 | RunEditingEnvironment( 182 | directory, 183 | server_address=server_address) 184 | finally: 185 | try: 186 | logging.info("Terminating latex compilation server.") 187 | server_proc.kill() 188 | except OSError: 189 | pass 190 | 191 | else: 192 | print "Unable to recognize command %r" % argv 193 | print __doc__ 194 | sys.exit(1) 195 | 196 | 197 | if __name__ == "__main__": 198 | main() 199 | -------------------------------------------------------------------------------- /freemindlatex/compilation_server_lib.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import collections 3 | import errno 4 | import logging 5 | import os 6 | import re 7 | import shutil 8 | import subprocess 9 | import tempfile 10 | import time 11 | from concurrent import futures 12 | 13 | import grpc 14 | from freemindlatex import ( 15 | compilation_service_pb2, 16 | compilation_service_pb2_grpc, 17 | convert_lib) 18 | 19 | _LATEX_MAIN_FILE_BASENAME_MAP = { 20 | compilation_service_pb2.LatexCompilationRequest.BEAMER: 'slides', 21 | compilation_service_pb2.LatexCompilationRequest.REPORT: 'report' 22 | } 23 | _LATEX_CONTENT_TEX_FILE_NAME = "mindmap.tex" 24 | 25 | 26 | def _MkdirP(directory): 27 | """Makes sure the directory exists. Otherwise, will try creating it. 28 | 29 | Args: 30 | directory: the directory to make sure exist 31 | 32 | Returns: 33 | Nothing 34 | 35 | Raises: 36 | OSError, when encounter problems creating the directory. 37 | """ 38 | 39 | try: 40 | os.makedirs(directory) 41 | except OSError as exc: # Python >2.5 42 | if exc.errno == errno.EEXIST and os.path.isdir(directory): 43 | pass 44 | else: 45 | raise 46 | 47 | 48 | def _CompileLatexAtDir(working_dir, compilation_mode): 49 | """Runs pdflatex at the working directory. 50 | 51 | Args: 52 | working_dir: the working directory of the freemindlatex compilation process. 53 | Normally a temporary directory (e.g. /tmp/123). 54 | compilation_mode: 55 | e.g. compilation_service_pb2.LatexCompilationRequest.BEAMER or REPORT 56 | 57 | Returns: 58 | A compilation_service_pb2.LatexCompilationResponse, whose status is either 59 | compilation_service_pb2.LatexCompilationResponse.SUCCESS 60 | or compilation_service_pb2.LatexCompilationResponse.ERROR 61 | """ 62 | basename = _LATEX_MAIN_FILE_BASENAME_MAP[compilation_mode] 63 | proc = subprocess.Popen( 64 | ["pdflatex", "-interaction=nonstopmode", 65 | "{}.tex".format(basename)], cwd=working_dir, 66 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 67 | stdout, _ = proc.communicate() 68 | return_code = proc.returncode 69 | 70 | result = compilation_service_pb2.LatexCompilationResponse() 71 | result.compilation_log = stdout 72 | result.source_code = open( 73 | os.path.join( 74 | working_dir, 75 | _LATEX_CONTENT_TEX_FILE_NAME)).read() 76 | result.status = ( 77 | compilation_service_pb2.LatexCompilationResponse.SUCCESS 78 | if return_code == 0 79 | else compilation_service_pb2.LatexCompilationResponse.ERROR) 80 | if return_code == 0: 81 | result.pdf_content = open( 82 | os.path.join(working_dir, "{}.pdf".format( 83 | basename))).read() 84 | 85 | return result 86 | 87 | 88 | class BibtexCompilationError(Exception): 89 | pass 90 | 91 | 92 | def _CompileBibtexAtDir(working_dir, compilation_mode): 93 | """Runs bibtex at the working directory. 94 | 95 | Args: 96 | working_dir: the working directory of the freemindlatex project, 97 | e.g. /tmp/123 98 | compilation_mode: 99 | e.g. compilation_service_pb2.LatexCompilationRequest.BEAMER or REPORT 100 | 101 | Raises: 102 | BibtexCompilationError: when bibtex compilation encounters some errors 103 | or warnings 104 | """ 105 | proc = subprocess.Popen( 106 | ["bibtex", 107 | _LATEX_MAIN_FILE_BASENAME_MAP[compilation_mode]], 108 | cwd=working_dir, stdout=subprocess.PIPE, 109 | stderr=subprocess.PIPE) 110 | stdout, _ = proc.communicate() 111 | if proc.returncode != 0: 112 | raise BibtexCompilationError(stdout) 113 | 114 | 115 | def _ParseNodeIdAndErrorMessageMapping( 116 | latex_content, latex_compilation_error_msg): 117 | """Parse the latex compilation error message, to see which frames have errors. 118 | 119 | Args: 120 | latex_content: the mindmap.tex file content, including frames' 121 | node markers. We use it to extract mappings between line numbers and 122 | frames 123 | latex_compilation_error_msg: the latex compilation error message, containing 124 | line numbers and error messages. 125 | 126 | Returns: 127 | A map of frame IDs and the compilation errors within it. For example: 128 | { "node12345" : ["nested too deep"] } 129 | """ 130 | result = collections.defaultdict(list) 131 | 132 | lineno_frameid_map = {} 133 | 134 | frame_node_id = None 135 | for line_no, line in enumerate(latex_content.split("\n")): 136 | line_no = line_no + 1 137 | if line.startswith("%%frame: "): 138 | frame_node_id = re.match(r'%%frame: (.*)%%', line).group(1) 139 | 140 | if frame_node_id is not None: 141 | lineno_frameid_map[line_no] = frame_node_id 142 | 143 | lineno_and_errors = [] 144 | error_message = None 145 | for line in latex_compilation_error_msg.split("\n"): 146 | if line.startswith("! "): 147 | error_message = line[2:] 148 | mo = re.match(r'l.(\d+)', line) 149 | if mo is not None: 150 | lineno_and_errors.append((int(mo.group(1)), error_message)) 151 | 152 | for lineno, error in lineno_and_errors: 153 | frame_id = lineno_frameid_map[lineno] 154 | result[frame_id].append(error) 155 | 156 | return result 157 | 158 | 159 | def _LatexCompileOrTryEmbedErrorMessage(org, work_dir, compilation_mode): 160 | """Try compiling. If fails, try embedding error messages into the frame. 161 | 162 | Args: 163 | org: the in-memory slides organization 164 | work_dir: Directory containing the running files: mindmap.mm, 165 | and the image files. 166 | 167 | Returns: 168 | A compilation_service_pb2.LatexCompilationResponse object. 169 | """ 170 | output_tex_file_loc = os.path.join(work_dir, "mindmap.tex") 171 | if compilation_mode == compilation_service_pb2.LatexCompilationRequest.BEAMER: 172 | org.OutputToBeamerLatex(output_tex_file_loc) 173 | elif (compilation_mode == 174 | compilation_service_pb2.LatexCompilationRequest.REPORT): 175 | org.OutputToLatex(output_tex_file_loc) 176 | else: 177 | raise ValueError 178 | 179 | # First attempt 180 | result = _CompileLatexAtDir(work_dir, compilation_mode) 181 | 182 | if result.status == compilation_service_pb2.LatexCompilationResponse.SUCCESS: 183 | return result 184 | 185 | # Second attempt 186 | try: 187 | frame_and_error_message_map = _ParseNodeIdAndErrorMessageMapping( 188 | result.source_code, result.compilation_log) 189 | except KeyError as _: 190 | logging.error( 191 | "Error parsing node-id from error message: %s", 192 | result.compilation_log) 193 | result.status = compilation_service_pb2.LatexCompilationResponse.CANNOTFIX 194 | return result 195 | 196 | org.LabelErrorsOnFrames(frame_and_error_message_map) 197 | org.OutputToBeamerLatex(output_tex_file_loc) 198 | 199 | second_attempt_result = _CompileLatexAtDir(work_dir, compilation_mode) 200 | if (second_attempt_result.status == 201 | compilation_service_pb2.LatexCompilationResponse.SUCCESS): 202 | result.status = compilation_service_pb2.LatexCompilationResponse.EMBEDDED 203 | 204 | else: 205 | result.status = compilation_service_pb2.LatexCompilationResponse.CANNOTFIX 206 | return result 207 | 208 | 209 | def _PrepareCompilationBaseDirectory(directory): 210 | """Copies the template (slides.tex) into the empty directory. 211 | """ 212 | static_file_dir = os.path.join( 213 | os.path.dirname( 214 | os.path.realpath(__file__)), "static_files") 215 | for filename in os.listdir(static_file_dir): 216 | shutil.copyfile( 217 | os.path.join( 218 | static_file_dir, filename), os.path.join( 219 | directory, filename)) 220 | 221 | 222 | class CompilationServer(compilation_service_pb2_grpc.LatexCompilationServicer): 223 | 224 | def CompilePackage(self, request, context): # pylint: disable=no-self-use 225 | """Compile the mindmap along with the files attached in the request. 226 | 227 | We will create a working directory, prepare its content, and compile. 228 | When there is a latex compilation error, we will put the latex error log 229 | into the response. 230 | 231 | Returns: 232 | A compilation_service_pb2.LatexCompilationResponse object. 233 | 234 | Args: 235 | A compilation_service_pb2.LatexCompilationRequest object, containing 236 | all the involved file content. 237 | """ 238 | compile_dir = tempfile.mkdtemp() 239 | work_dir = os.path.join(compile_dir, "working") 240 | logging.info("Compiling at %s", work_dir) 241 | _MkdirP(work_dir) 242 | 243 | # Preparing the temporary directory content 244 | _PrepareCompilationBaseDirectory(work_dir) 245 | for file_info in request.file_infos: 246 | target_loc = os.path.join(work_dir, file_info.filepath) 247 | dirname = os.path.dirname(target_loc) 248 | _MkdirP(dirname) 249 | with open(target_loc, 'w') as ofile: 250 | ofile.write(file_info.content) 251 | 252 | # Compile 253 | org = convert_lib.Organization( 254 | codecs.open( 255 | os.path.join( 256 | work_dir, 257 | "mindmap.mm"), 258 | 'r', 259 | 'utf8').read()) 260 | 261 | initial_compilation_result = _LatexCompileOrTryEmbedErrorMessage( 262 | org, work_dir, request.compilation_mode) 263 | if (initial_compilation_result.status == 264 | compilation_service_pb2.LatexCompilationResponse.CANNOTFIX): 265 | return initial_compilation_result 266 | 267 | try: 268 | _CompileBibtexAtDir(work_dir, request.compilation_mode) 269 | except BibtexCompilationError as _: 270 | pass 271 | _CompileLatexAtDir(work_dir, request.compilation_mode) 272 | final_compilation_result = _CompileLatexAtDir( 273 | work_dir, request.compilation_mode) 274 | 275 | result = initial_compilation_result 276 | result.pdf_content = final_compilation_result.pdf_content 277 | 278 | # Clean-up 279 | shutil.rmtree(compile_dir) 280 | 281 | return result 282 | 283 | 284 | class HealthzServer(compilation_service_pb2_grpc.HealthServicer): 285 | 286 | def Check(self, request, context): # pylint: disable=no-self-use 287 | response = compilation_service_pb2.HealthCheckResponse() 288 | response.status = compilation_service_pb2.HealthCheckResponse.SERVING 289 | return response 290 | 291 | 292 | def RunServerAtPort(port): 293 | """Run the latex compilation server at port, and wait till termination. 294 | """ 295 | logging.info("Running the LaTeX compilation server at port %d", port) 296 | 297 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 298 | compilation_service_pb2_grpc.add_LatexCompilationServicer_to_server( 299 | CompilationServer(), server) 300 | compilation_service_pb2_grpc.add_HealthServicer_to_server( 301 | HealthzServer(), server) 302 | server.add_insecure_port('[::]:%d' % port) 303 | server.start() 304 | try: 305 | while True: 306 | time.sleep(60 * 60 * 24) 307 | except KeyboardInterrupt: 308 | server.stop(0) 309 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /freemindlatex/convert_lib.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import logging 3 | import os 4 | import re 5 | import sys 6 | from xml.dom import minidom 7 | 8 | import gflags 9 | from bibtexparser.bparser import BibTexParser 10 | 11 | gflags.DEFINE_string('mindmap_file', None, 'the mindmap filename') 12 | gflags.DEFINE_boolean('use_absolute_paths_for_images', False, 13 | 'when set, will use absolute paths for images') 14 | gflags.DEFINE_string('html_file', None, 'the html filename') 15 | gflags.DEFINE_string('latex_file', None, 'the latex filename') 16 | gflags.DEFINE_string('beamer_latex_file', None, 'the beamer latex filename') 17 | gflags.DEFINE_string('bib_file', '~/Dropbox/bib.bib', 18 | 'bib file location') 19 | 20 | 21 | class BibDatabase(object): 22 | 23 | def __init__(self, bib_file_location=None): 24 | if bib_file_location is None: 25 | bib_file_location = gflags.FLAGS.bib_file 26 | bib_file_location = re.sub('~', os.environ['HOME'], bib_file_location) 27 | with open(bib_file_location) as bibfile: 28 | content = bibfile.read() 29 | bp = BibTexParser(content) 30 | self.entry_map = {} 31 | for ent in bp.get_entry_list(): 32 | self.entry_map[ent['id']] = ent 33 | 34 | def _RetrieveEntry(self, name): 35 | return self.entry_map[name] 36 | 37 | db = None 38 | 39 | @staticmethod 40 | def GetTheDB(): 41 | if BibDatabase.db is None: 42 | BibDatabase.db = BibDatabase() 43 | return BibDatabase.db 44 | 45 | @staticmethod 46 | def GetFormattedAuthor(bib_authorname): 47 | names = bib_authorname.split(' and ') 48 | first_author_lastname = names[0].split(',')[0] 49 | if len(names) == 1: 50 | return first_author_lastname 51 | 52 | if len(names) >= 3: 53 | return "%s et. al. " % first_author_lastname 54 | 55 | second_author_lastname = names[1].split(',')[0] 56 | return "%s and %s" % (first_author_lastname, 57 | second_author_lastname) 58 | 59 | def GetOneArtCiteHTML(self, name): 60 | try: 61 | ent = self._RetrieveEntry(name) 62 | except KeyError as _: 63 | return "InvalidBibEntry:%s" % name 64 | return "%s, %s" % ( 65 | ent["title"], 66 | self.GetFormattedAuthor(ent['author']), 67 | ent['year']) 68 | 69 | def GetOneArtNewciteHTML(self, name): 70 | try: 71 | ent = self._RetrieveEntry(name) 72 | except KeyError as _: 73 | return "InvalidBibEntry:%s" % name 74 | return "%s (%s)" % ( 75 | ent["title"], 76 | self.GetFormattedAuthor(ent['author']), 77 | ent['year']) 78 | 79 | def GetCiteHTML(self, name): 80 | return '(%s)' % ( 81 | "; ".join(self.GetOneArtCiteHTML(x) for x in name.split(','))) 82 | 83 | def GetNewciteHTML(self, name): 84 | return ", ".join(self.GetOneArtNewciteHTML(x) for x in name.split(',')) 85 | 86 | 87 | class Node(object): 88 | accepted_nodes = ['node', 'richcontent'] 89 | 90 | def __init__(self, dom_node, level=0): 91 | self.type = dom_node.nodeName 92 | self.level = level 93 | 94 | try: 95 | self.nodeid = dom_node.attributes['ID'].value 96 | except KeyError as _: 97 | self.nodeid = "NONE" 98 | 99 | try: 100 | self.text = dom_node.attributes['TEXT'].value 101 | except KeyError as _: 102 | self.text = "NONE" 103 | 104 | self.printing_func = None 105 | 106 | self.children = [] 107 | for child in dom_node.childNodes: 108 | self.AddInfoForChild(child) 109 | 110 | def AddInfoForChild(self, child): 111 | if child.nodeType == child.TEXT_NODE: 112 | return 113 | if child.nodeName not in Node.accepted_nodes: 114 | return 115 | 116 | if child.nodeName == 'richcontent': 117 | self.children.append(ImageNode(child, self.level + 1)) 118 | return 119 | 120 | if 'TEXT' not in child.attributes.keys(): 121 | for g in child.childNodes: 122 | self.AddInfoForChild(g) 123 | return 124 | 125 | if child.attributes['TEXT'].value.startswith('#'): 126 | return 127 | 128 | self.children.append(Node(child, self.level + 1)) 129 | 130 | def __str__(self): 131 | pass 132 | 133 | def GetText(self, print_format='html'): # pylint: disable=too-many-locals 134 | def ReplaceCitations(s): 135 | def get_cite_html(mo): 136 | citation = BibDatabase.GetTheDB().GetCiteHTML(mo.group(1)) 137 | return citation 138 | 139 | def get_newcite_html(mo): 140 | citation = BibDatabase.GetTheDB().GetNewciteHTML(mo.group(1)) 141 | return citation 142 | 143 | s = re.sub( 144 | r'\\cite{(.*?)}', 145 | get_cite_html, 146 | s) 147 | s = re.sub( 148 | r'\\newcite{(.*?)}', 149 | get_newcite_html, 150 | s) 151 | return s 152 | 153 | def ReplaceEmphMarkups(s): 154 | return re.sub( 155 | r'\\emph{(.*?)}', 156 | lambda x: '%s' % x.group(1), 157 | s) 158 | 159 | def ReplaceSubScores(s): 160 | return re.sub( 161 | r'\_', 162 | '_', 163 | s) 164 | 165 | def ReplacePercScores(s): 166 | return re.sub( 167 | r'\%', 168 | '%', 169 | s) 170 | 171 | def ReplaceTextBFMarkups(s): 172 | return re.sub( 173 | r'\\textbf{(.*?)}', 174 | lambda x: '%s' % x.group(1), 175 | s) 176 | 177 | def ReplaceFootnoteMarkups(s): 178 | return re.sub( 179 | r'\\footnote{(.*)}', 180 | lambda x: 'FOOTNOTE' % x.group( 181 | 1), 182 | s) 183 | 184 | def ReplaceUnderlineMarkups(s): 185 | return re.sub( 186 | r'\\underline{(.*?)}', 187 | lambda x: '%s' % x.group(1), 188 | s) 189 | 190 | def ReplaceTextSFMarkups(s): 191 | return re.sub( 192 | r'\\textsf{(.*?)}', 193 | lambda x: '%s' % x.group(1), 194 | s) 195 | 196 | def ReplaceSoutMarkups(s): 197 | return re.sub( 198 | r'\\sout{(.*?)}', 199 | lambda x: '%s' % x.group(1), 200 | s) 201 | 202 | def ReplaceTildas(s): 203 | return s.replace('~', ' ') 204 | 205 | def ReplaceLdots(s): 206 | return s.replace('\\ldots', '...') 207 | 208 | def ReplaceDollarSigns(s): 209 | s1 = re.sub(r'\$\$(.*?)\$\$', lambda mo: r"\[%s\]" % mo.group(1), s) 210 | s2 = re.sub(r'\$(.*?)\$', lambda mo: r"\(%s\)" % mo.group(1), s1) 211 | return s2 212 | 213 | filters = [ReplaceTildas, 214 | ReplacePercScores, 215 | ReplaceTextBFMarkups, 216 | ReplaceEmphMarkups, 217 | ReplaceTextSFMarkups, 218 | ReplaceSoutMarkups, 219 | ReplaceUnderlineMarkups, 220 | ReplaceCitations, 221 | ReplaceLdots, 222 | ReplaceDollarSigns, 223 | ReplaceSubScores, 224 | ReplaceFootnoteMarkups, 225 | ] 226 | 227 | txt = self.text 228 | if print_format == 'beamer_latex': 229 | print_format = 'latex' 230 | if print_format == 'latex': 231 | if '> outputfile, """ 440 | 441 | 454 | 455 | 475 | 476 | """ 477 | 478 | self.doc.GetPrinter()(outputfile) 479 | 480 | def OutputToLatex(self, filename): 481 | with codecs.open(filename, 'w', 'utf8') as outputfile: 482 | self.doc.GetPrinter()(outputfile, 'latex') 483 | 484 | def OutputToBeamerLatex(self, filename): 485 | with codecs.open(filename, 'w', 'utf8') as outputfile: 486 | self.doc.GetPrinter()(outputfile, 'beamer_latex') 487 | 488 | 489 | def OutputOrderedList(current_node): 490 | def PrintTo(writer, print_format='html'): 491 | if print_format == 'html': 492 | PrintInHTMLFormat(writer) 493 | elif print_format == 'latex': 494 | PrintInLatexFormat(writer) 495 | elif print_format == 'beamer_latex': 496 | PrintInBeamerLatexFormat(writer) 497 | 498 | def PrintInBeamerLatexFormat(writer): 499 | PrintInLatexFormatWithTag(writer, 'beamer_latex') 500 | 501 | def PrintInHTMLFormat(writer): 502 | current_node.PrintSelfToWriter(writer) 503 | if current_node.GetPrintableChildren(): 504 | writer.write('
    ') 505 | for t in current_node.GetPrintableChildren(): 506 | if t.IsStoryNode() or t.IsCommentNode(): 507 | t.GetPrinter()(writer) 508 | writer.write('
    ') 509 | else: 510 | writer.write('
  1. ') 511 | t.GetPrinter()(writer) 512 | writer.write('
  2. ') 513 | writer.write('
') 514 | 515 | def PrintInLatexFormat(writer): 516 | PrintInLatexFormatWithTag(writer, 'latex') 517 | 518 | def PrintInLatexFormatWithTag(writer, tag='latex'): 519 | current_node.PrintSelfToWriter(writer, tag) 520 | if current_node.GetPrintableChildren(): 521 | writer.write(r'\begin{enumerate}') 522 | for t in current_node.GetPrintableChildren(): 523 | if t.IsStoryNode() or t.IsCommentNode(): 524 | t.GetPrinter()(writer, tag) 525 | writer.write('\n') 526 | else: 527 | writer.write(r'\item ') 528 | t.GetPrinter()(writer, tag) 529 | writer.write('\n') 530 | writer.write(r'\end{enumerate}') 531 | 532 | return PrintTo 533 | 534 | 535 | def OutputUnorderedList(current_node): 536 | def PrintTo(writer, print_format='html'): 537 | if print_format == 'html': 538 | PrintInHTMLFormat(writer) 539 | elif print_format == 'latex': 540 | PrintInLatexFormat(writer) 541 | elif print_format == 'beamer_latex': 542 | PrintInBeamerLatexFormat(writer) 543 | 544 | def PrintInBeamerLatexFormat(writer): 545 | PrintInLatexFormatWithFormatTag(writer, 'beamer_latex') 546 | 547 | def PrintInHTMLFormat(writer): 548 | current_node.PrintSelfToWriter(writer) 549 | if current_node.GetPrintableChildren(): 550 | writer.write('
    ') 551 | for t in current_node.GetPrintableChildren(): 552 | if t.IsStoryNode() or t.IsCommentNode(): 553 | t.GetPrinter()(writer) 554 | writer.write('
    ') 555 | else: 556 | writer.write('
  • ') 557 | t.GetPrinter()(writer) 558 | writer.write('
  • ') 559 | writer.write('
') 560 | 561 | def PrintInLatexFormat(writer): 562 | PrintInLatexFormatWithFormatTag(writer, 'latex') 563 | 564 | def PrintInLatexFormatWithFormatTag(writer, tag='latex'): 565 | current_node.PrintSelfToWriter(writer, tag) 566 | if current_node.GetPrintableChildren(): 567 | writer.write(r'\begin{itemize}') 568 | for t in current_node.GetPrintableChildren(): 569 | if t.IsStoryNode() or t.IsCommentNode(): 570 | t.GetPrinter()(writer, tag) 571 | writer.write('\n') 572 | else: 573 | writer.write(r'\item ') 574 | t.GetPrinter()(writer, tag) 575 | writer.write('\n') 576 | writer.write(r'\end{itemize}') 577 | 578 | return PrintTo 579 | 580 | 581 | def OutputHAlignedList(current_node): 582 | def PrintTo(writer, print_format='html'): 583 | if print_format == 'html': 584 | PrintInHTMLFormat(writer) 585 | elif print_format == 'latex': 586 | PrintInLatexFormat(writer) 587 | elif print_format == 'beamer_latex': 588 | PrintInBeamerLatexFormat(writer) 589 | 590 | def PrintInBeamerLatexFormat(writer): 591 | PrintInLatexFormatWithFormatTag(writer, 'beamer_latex') 592 | 593 | def PrintInHTMLFormat(writer): 594 | current_node.PrintSelfToWriter(writer) 595 | if current_node.GetPrintableChildren(): 596 | writer.write('
    ') 597 | for t in current_node.GetPrintableChildren(): 598 | if t.IsStoryNode() or t.IsCommentNode(): 599 | t.GetPrinter()(writer) 600 | writer.write('
    ') 601 | else: 602 | writer.write('
  • ') 603 | t.GetPrinter()(writer) 604 | writer.write('
  • ') 605 | writer.write('
') 606 | 607 | def PrintInLatexFormat(writer): 608 | PrintInLatexFormatWithFormatTag(writer, 'latex') 609 | 610 | def PrintInLatexFormatWithFormatTag(writer, tag='latex'): 611 | current_node.PrintSelfToWriter(writer, tag) 612 | if current_node.GetPrintableChildren(): 613 | all_children = current_node.GetPrintableChildren() 614 | algned_children = [t for t in all_children if not ( 615 | t.IsStoryNode() or t.IsCommentNode())] 616 | n = len(algned_children) 617 | writer.write(r'\vspace{0.2cm}\begin{columns}[onlytextwidth]') 618 | col_width = 0.9 / n 619 | for t in all_children: 620 | if t.IsStoryNode() or t.IsCommentNode(): 621 | t.GetPrinter()(writer, tag) 622 | writer.write('\n') 623 | else: 624 | writer.write( 625 | r'\begin{column}{%.2f\textwidth} \centering ' % 626 | col_width) 627 | t.GetPrinter()(writer, tag) 628 | writer.write(r'\end{column}') 629 | writer.write(r'\end{columns}') 630 | 631 | return PrintTo 632 | 633 | 634 | def OutputStory(current_node): 635 | def PrintTo(writer, print_format='html'): 636 | if print_format == 'html': 637 | PrintInHTMLFormat(writer) 638 | elif print_format == 'latex': 639 | PrintInLatexFormat(writer) 640 | elif print_format == 'beamer_latex': 641 | PrintInBeamerLatexFormat(writer) 642 | 643 | def PrintInBeamerLatexFormat(writer): 644 | pass 645 | 646 | def PrintInHTMLFormat(writer): 647 | writer.write('') 648 | current_node.PrintSelfToWriter(writer) 649 | writer.write('') 650 | 651 | def PrintInLatexFormat(writer): 652 | writer.write('%%') 653 | current_node.PrintSelfToWriter(writer, 'latex') 654 | 655 | return PrintTo 656 | 657 | 658 | def OutputImage(current_node, width=None): 659 | def PrintTo(writer, print_format='html'): 660 | if print_format == 'html': 661 | PrintInHTMLFormat(writer, current_node, width) 662 | elif print_format == 'latex': 663 | PrintInLatexFormat(writer, current_node, width) 664 | elif print_format == 'beamer_latex': 665 | PrintInBeamerLatexFormat(writer, current_node, width) 666 | 667 | def PrintInBeamerLatexFormat(writer, current_node, width): 668 | if width is None: 669 | width = r'.7\textwidth' 670 | elif width <= 1: 671 | width = r'%.2f\textwidth' % width 672 | else: 673 | width = r'%.2fpx' % width 674 | 675 | writer.write(r'\begin{centering}\includegraphics[width=%s]{%s}' % ( 676 | width, current_node.GetImageLoc())) 677 | writer.write(r'\end{centering}') 678 | 679 | def PrintInHTMLFormat(writer, current_node, width): 680 | if width is None: 681 | width = 500 682 | writer.write( 683 | '
' % 684 | (current_node.GetImageLoc(), width)) 685 | writer.write('
') 686 | 687 | def PrintInLatexFormat(writer, current_node, width): 688 | if width is None: 689 | width = r'.7\textwidth' 690 | else: 691 | width = r'%.2f\textwidth' % width 692 | 693 | writer.write(r'\begin{figure}\includegraphics[width=%s]{%s}' % ( 694 | width, current_node.GetImageLoc())) 695 | writer.write(r'\end{figure}') 696 | 697 | return PrintTo 698 | 699 | 700 | def OutputComment(current_node): 701 | def PrintTo(writer, print_format='html'): 702 | if print_format == 'html': 703 | PrintInHTMLFormat(writer) 704 | elif print_format == 'latex': 705 | PrintInLatexFormat(writer) 706 | elif print_format == 'beamer_latex': 707 | PrintInBeamerLatexFormat(writer) 708 | 709 | def PrintInBeamerLatexFormat(writer): 710 | PrintInLatexFormat(writer) 711 | 712 | def PrintInHTMLFormat(writer): 713 | writer.write('') 714 | current_node.PrintSelfToWriter(writer) 715 | writer.write('') 716 | 717 | def PrintInLatexFormat(writer): 718 | writer.write(r'\todo[size=\tiny]{') 719 | current_node.PrintSelfToWriter(writer, 'latex') 720 | writer.write(r'}') 721 | 722 | return PrintTo 723 | 724 | 725 | def OutputParagraph(current_node): 726 | def PrintTo(writer, print_format='html'): 727 | if print_format == 'html': 728 | PrintInHTMLFormat(writer) 729 | elif print_format == 'latex': 730 | PrintInLatexFormat(writer) 731 | elif print_format == 'beamer_latex': 732 | PrintInBeamerLatexFormat(writer) 733 | 734 | def PrintInBeamerLatexFormat(writer): 735 | writer.write("\n%%frame: {}%%\n".format(current_node.nodeid)) 736 | writer.write(r'\begin{frame}{') 737 | current_node.PrintSelfToWriter(writer, 'beamer_latex') 738 | writer.write(r'}') 739 | for i in current_node.GetPrintableChildren(): 740 | i.GetPrinter()(writer, 'beamer_latex') 741 | writer.write('\n') 742 | writer.write(r'\end{frame}') 743 | 744 | def PrintInHTMLFormat(writer): 745 | writer.write( 746 | '

') 747 | current_node.PrintSelfToWriter(writer) 748 | writer.write('') 749 | for i in current_node.GetPrintableChildren(): 750 | writer.write('
') 751 | i.GetPrinter()(writer) 752 | writer.write('

') 753 | 754 | def PrintInLatexFormat(writer): 755 | writer.write('\n%%') 756 | current_node.PrintSelfToWriter(writer, 'latex') 757 | writer.write('\n') 758 | for i in current_node.GetPrintableChildren(): 759 | i.GetPrinter()(writer, 'latex') 760 | writer.write('\n') 761 | writer.write('\n') 762 | 763 | return PrintTo 764 | 765 | 766 | def DirectlyPrintSub(current_node): 767 | def PrintTo(writer, print_format='html'): 768 | if print_format == 'html': 769 | PrintInHTMLFormat(writer) 770 | elif print_format == 'latex': 771 | PrintInLatexFormat(writer) 772 | elif print_format == 'beamer_latex': 773 | PrintInBeamerLatexFormat(writer) 774 | 775 | def PrintInBeamerLatexFormat(writer): 776 | for t in current_node.GetPrintableChildren(): 777 | t.GetPrinter()(writer, 'beamer_latex') 778 | writer.write('\n') 779 | 780 | def PrintInHTMLFormat(writer): 781 | for t in current_node.GetPrintableChildren(): 782 | t.GetPrinter()(writer) 783 | writer.write('
') 784 | 785 | def PrintInLatexFormat(writer): 786 | for t in current_node.GetPrintableChildren(): 787 | t.GetPrinter()(writer, 'latex') 788 | writer.write('\n') 789 | 790 | return PrintTo 791 | 792 | 793 | def OutputFrameAndDebugMessage(current_node, error_messages): 794 | """Output the error message as title, and normal content as content. 795 | 796 | This printer is used when there is an error on this page. 797 | 798 | Args: 799 | current_node: the current node (a frame). 800 | error_messages: a list of latex compilation messages for errors in this 801 | frame. 802 | 803 | Returns: 804 | A printer for printing the latex code into a writer. 805 | """ 806 | 807 | def PrintTo(writer, print_format='beamer_latex'): 808 | if print_format == 'beamer_latex': 809 | PrintInBeamerLatexFormat(writer) 810 | else: 811 | logging.fatal("Unsupported format %s", format) 812 | 813 | def PrintInBeamerLatexFormat(writer): 814 | writer.write(r'\begin{frame}[fragile]{Error on page\ldots}') 815 | writer.write(r'\begin{verbatim}') 816 | writer.write('\n') 817 | for msg in error_messages: 818 | writer.write(msg) 819 | writer.write("\n") 820 | writer.write(r'\end{verbatim}') 821 | writer.write('\n') 822 | writer.write(r'\end{frame}') 823 | 824 | return PrintTo 825 | 826 | 827 | def DirectlyPrintThisAndSub(current_node): 828 | def PrintTo(writer, print_format='html'): 829 | if print_format == 'html': 830 | PrintInHTMLFormat(writer) 831 | elif print_format == 'latex': 832 | PrintInLatexFormat(writer) 833 | elif print_format == 'beamer_latex': 834 | PrintInBeamerLatexFormat(writer) 835 | 836 | def PrintInBeamerLatexFormat(writer): 837 | writer.write(current_node.GetText(print_format='beamer_latex')) 838 | writer.write('\n') 839 | for t in current_node.GetPrintableChildren(): 840 | t.GetPrinter()(writer, 'beamer_latex') 841 | writer.write('\n') 842 | 843 | def PrintInHTMLFormat(writer): 844 | writer.write(current_node.GetText()) 845 | writer.write('
') 846 | for t in current_node.GetPrintableChildren(): 847 | t.GetPrinter()(writer) 848 | writer.write('
') 849 | 850 | def PrintInLatexFormat(writer): 851 | writer.write(current_node.GetText(print_format='latex')) 852 | writer.write('\n') 853 | for t in current_node.GetPrintableChildren(): 854 | t.GetPrinter()(writer, 'latex') 855 | writer.write('\n') 856 | 857 | return PrintTo 858 | 859 | 860 | def PrintCurrentAsSection(current_node, tag): 861 | def PrintTo(writer, print_format='html'): 862 | if print_format == 'html': 863 | PrintInHTMLFormat(writer) 864 | elif print_format == 'latex': 865 | PrintInLatexFormat(writer) 866 | elif print_format == 'beamer_latex': 867 | PrintInBeamerLatexFormat(writer) 868 | 869 | def PrintInBeamerLatexFormat(writer): 870 | latex_tag = None 871 | if tag == 'h2': 872 | latex_tag = 'section' 873 | elif tag == 'h3': 874 | latex_tag = 'subsection' 875 | elif tag == 'h4': 876 | latex_tag = 'subsubsection' 877 | assert latex_tag is not None 878 | txt = current_node.GetText('latex') 879 | if txt.strip(): 880 | writer.write(r"\%s{" % latex_tag) 881 | writer.write(txt) 882 | writer.write("}\n") 883 | DirectlyPrintSub(current_node)(writer, 'beamer_latex') 884 | 885 | def PrintInHTMLFormat(writer): 886 | writer.write("<%s>" % tag) 887 | writer.write(current_node.GetText()) 888 | writer.write("" % tag) 889 | DirectlyPrintSub(current_node)(writer) 890 | 891 | def PrintInLatexFormat(writer): 892 | latex_tag = None 893 | if tag == 'h2': 894 | latex_tag = 'section' 895 | elif tag == 'h3': 896 | latex_tag = 'subsection' 897 | elif tag == 'h4': 898 | latex_tag = 'subsubsection' 899 | assert latex_tag is not None 900 | txt = current_node.GetText('latex') 901 | if txt.strip(): 902 | writer.write(r"\%s{" % latex_tag) 903 | writer.write(txt) 904 | writer.write("}\n") 905 | DirectlyPrintSub(current_node)(writer, 'latex') 906 | return PrintTo 907 | 908 | 909 | def PrintTopLevel(current_node): 910 | def PrintTo(writer, print_format='html'): 911 | if print_format == 'html': 912 | PrintInHTMLFormat(writer) 913 | elif print_format == 'latex': 914 | PrintInLatexFormat(writer) 915 | elif print_format == 'beamer_latex': 916 | PrintInBeamerLatexFormat(writer) 917 | 918 | def PrintInBeamerLatexFormat(writer): 919 | cur_text_lines = current_node.GetText().split("\n") 920 | title = cur_text_lines[0] 921 | subtitle = "" 922 | author = "" 923 | if len(cur_text_lines) >= 2: 924 | subtitle = cur_text_lines[1] 925 | if len(cur_text_lines) >= 3: 926 | author = cur_text_lines[2] 927 | 928 | writer.write(r""" 929 | \title{%s} 930 | \subtitle{%s} 931 | \author[%s]{%s} 932 | \date{} 933 | 934 | \begin{frame} 935 | \maketitle 936 | \end{frame} 937 | """ % (title, subtitle, author, author)) 938 | 939 | DirectlyPrintSub(current_node)(writer, print_format='beamer_latex') 940 | 941 | def PrintInLatexFormat(writer): 942 | cur_text_lines = current_node.GetText().split("\n") 943 | title = cur_text_lines[0] 944 | subtitle = "" 945 | author = "" 946 | if len(cur_text_lines) >= 2: 947 | subtitle = cur_text_lines[1] 948 | if len(cur_text_lines) >= 3: 949 | author = cur_text_lines[2] 950 | 951 | writer.write(r""" 952 | \title{%s} 953 | \date{%s} 954 | \author{%s} 955 | 956 | \maketitle 957 | """ % (title, subtitle, author)) 958 | 959 | DirectlyPrintSub(current_node)(writer, print_format='latex') 960 | 961 | def PrintInHTMLFormat(writer): 962 | writer.write("

") 963 | writer.write(current_node.GetText()) 964 | writer.write("

") 965 | DirectlyPrintSub(current_node)(writer) 966 | return PrintTo 967 | 968 | 969 | def main(): 970 | try: 971 | gflags.FLAGS(sys.argv) 972 | except gflags.FlagsError as e: 973 | print '%s\nUsage: %s ARGS\n%s' % (e, sys.argv[0], gflags.FLAGS) 974 | sys.exit(1) 975 | logging.basicConfig(level=logging.INFO) 976 | 977 | if gflags.FLAGS.mindmap_file is None: 978 | print 'Usage: %s ARGS\n%s' % (sys.argv[0], gflags.FLAGS) 979 | sys.exit(1) 980 | 981 | org = Organization( 982 | codecs.open(gflags.FLAGS.mindmap_file, 'r', 'utf8').read()) 983 | if gflags.FLAGS.html_file is not None: 984 | org.OutputToHTML(gflags.FLAGS.html_file) 985 | 986 | if gflags.FLAGS.beamer_latex_file is not None: 987 | org.OutputToBeamerLatex(gflags.FLAGS.beamer_latex_file) 988 | 989 | if gflags.FLAGS.latex_file is not None: 990 | org.OutputToLatex(gflags.FLAGS.latex_file) 991 | 992 | 993 | if __name__ == "__main__": 994 | main() 995 | --------------------------------------------------------------------------------