├── .github └── workflows │ └── docker-build-push.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── backend ├── .gitignore ├── Dockerfile ├── README.md ├── config │ ├── config.py │ ├── config.yaml │ └── server_config.yaml ├── docker-compose.yaml ├── docx_handler │ ├── __init__.py │ ├── assets │ │ └── hust │ │ │ └── logo.png │ ├── general.py │ ├── hust.py │ └── utils.py ├── filters │ ├── general.py │ └── templates │ │ ├── __init__.py │ │ └── templates.py ├── logger │ └── __init__.py ├── main.py ├── md2report.py ├── poetry.lock ├── pyproject.toml ├── reference-docs │ └── HUST.docx └── test │ ├── test_case.zip │ ├── test_case │ ├── 5.2数据结构实验报告.md │ ├── 初次使用-2.png │ ├── 初次使用.png │ ├── 编码-2.png │ ├── 编码-3.png │ ├── 编码.png │ ├── 解码-2.png │ ├── 解码.png │ ├── 调用关系.png │ ├── 输出树-2.png │ ├── 输出树.png │ └── 输出编码.png │ └── test_cxx2flow │ └── test_cxx2flow.md ├── docs ├── contribute.md ├── grammar.md ├── img │ ├── Headings.png │ ├── TOC.png │ ├── abstract_en.png │ ├── abstract_zh.png │ ├── code.png │ ├── cxx2flow.png │ ├── cxx2flow.svg │ ├── figure.png │ ├── front_page.png │ └── table.png ├── index.md ├── introduction.md ├── preview.md └── webui.md ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── config.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── tailwind.config.js └── mkdocs.yml /.github/workflows/docker-build-push.yml: -------------------------------------------------------------------------------- 1 | name: docker-build-push 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | lfs: true 16 | - run: git lfs pull 17 | - 18 | name: Set up QEMU 19 | uses: docker/setup-qemu-action@v2 20 | - 21 | name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | - 24 | name: Login to Docker Hub 25 | uses: docker/login-action@v2 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | - 30 | name: Build and push 31 | uses: docker/build-push-action@v3.2.0 32 | with: 33 | context: ./backend 34 | file: ./backend/Dockerfile 35 | push: true 36 | tags: woolensheep/md2report:latest,woolensheep/md2report:${{ github.event.release.tag_name }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | md2report 2 | Copyright (C) <2022> woolensheep <2460563632@qq.com> 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # md2report 2 | 3 | 一个用于将Markdown文件转换为可以直接提交给学校的实验报告/大作业报告/期末小论文的工具。 4 | 5 | 如果你的院系/课程要求必须提交docx格式的报告,并且: 6 | - 你认为word/docx实在是太蠢了,并且习惯于使用markdown编辑文档,md2report能够大幅缩短你在报告格式、docx样式以及排版上花费的时间; 7 | - 你没有试过使用markdown,不妨尝试一下: [Markdown Guides](https://www.markdownguide.org/), [Mardown 教程](https://markdown.com.cn/); 8 | 9 | 如果你的院系/课程允许提交pdf格式的报告,寻找一个好用的tex模板或许是一个更好的方案。但是考虑到学习成本以及使用难度,md2report仍然可以作为一个替代选项。 10 | 11 | [预览更多](https://woolen-sheep.github.io/md2report/preview/) 12 | 13 | ![Preview](docs/img/front_page.png) 14 | 15 | ## Quick Start 16 | 17 | 虽然md2report使用的都是标准markdown语法,但是markdown标记到docx的样式映射可能与你的习惯不同。 18 | 因此,请确保除了本小节内容以外,开始使用md2report之前先阅读文档中关于[语法](https://woolen-sheep.github.io/md2report/grammar/)的部分。 19 | 20 | ### Web UI 21 | 22 | md2report提供了[Web UI](https://md2report.hust.online), 无需安装即可使用。 23 | 24 | WebUI仅为不会使用python的用户提供便利,不能保证可用性,也不保证是最新版本。大体积文件建议使用CLI。 25 | 26 | 请不要上传zip bomb,如果服务受到攻击可能考虑关闭服务。 27 | 28 | ### CLI 29 | 30 | md2report提供了CLI,如果想使用CLI,需要: 31 | 32 | - python 3.10+ 33 | - poetry in PATH 34 | - pandoc(version >= 2.11) in PATH 35 | - [cxx2flow](https://github.com/Enter-tainer/cxx2flow) 36 | - 可选,仅在使用cxx2flow功能时需要 37 | 38 | ```bash 39 | git clone https://github.com/woolen-sheep/md2report.git 40 | cd backend 41 | poetry install 42 | poetry shell 43 | 44 | python md2report.py -h 45 | # usage: md2report.py [-h] [-c CONFIG] [--highlight HIGHLIGHT] [-o OUTPUT] -i INPUT [-t TEMPLATE] 46 | # 47 | # options: 48 | # -h, --help show this help message and exit 49 | # -c CONFIG, --config CONFIG 50 | # config file path 51 | # --highlight HIGHLIGHT 52 | # enable highlight of code blocks 53 | # -o OUTPUT, --output OUTPUT 54 | # output docx filename 55 | # -i INPUT, --input INPUT 56 | # input markdown filename 57 | # -t TEMPLATE, --template TEMPLATE 58 | # template to use 59 | # --indent-font-size INDENT_FONT_SIZE 60 | # first line indent font size in pt 61 | # --indent-font-num INDENT_FONT_NUM 62 | # first line indent num 63 | # --first_line_indent FIRST_LINE_INDENT 64 | enable the first line indent 65 | 66 | python md2report.py -i test/test_case/5.2数据结构实验报告.md 67 | 68 | # see output.docx 69 | 70 | ``` 71 | 72 | ### Docker 73 | 74 | ```bash 75 | docker run --name md2report -d woolensheep/md2report:latest 76 | ``` 77 | 78 | 此docker为 `backend/Dockerfile` build 产物,无法作为完整的前后端使用。但是你可以进入其中运行CLI而无需搭建环境: 79 | 80 | ```bash 81 | ➜ no-more-docx-report git:(master) ✗ docker exec -it md2report bash 82 | root@f35bd61ef8bc:/app# python md2report.py -h 83 | usage: md2report.py [-h] [-c CONFIG] [--highlight HIGHLIGHT] [-o OUTPUT] -i INPUT [-t TEMPLATE] 84 | 85 | options: 86 | -h, --help show this help message and exit 87 | -c CONFIG, --config CONFIG 88 | config file path 89 | --highlight HIGHLIGHT 90 | enable highlight of code blocks 91 | -o OUTPUT, --output OUTPUT 92 | output docx filename 93 | -i INPUT, --input INPUT 94 | input markdown filename 95 | -t TEMPLATE, --template TEMPLATE 96 | template to use 97 | --first_line_indent FIRST_LINE_INDENT 98 | enable the first line indent 99 | ``` 100 | 101 | ### Self-Hosted Web UI 102 | 103 | md2docx的Web UI也是开源的,你可以使用docker-compose部署。 104 | 需要注意的是现在的`docker-compose.yaml`中挂载了绝对路径,使用之前请先修改。 105 | 106 | ```bash 107 | cd backend 108 | docker compose up --build -d 109 | docker compose ps 110 | ``` 111 | 112 | ## Features 113 | 114 | 目前支持的特性如下: 115 | 116 | - [x] Title and SubTitle 117 | - [x] Abstract 118 | - [x] Heading (H1 to H4) 119 | - [x] Image Caption 120 | - [x] Table 121 | - [x] Table Caption 122 | - [x] Code Highlight 123 | - [x] Table of Content 124 | - [x] Header and Footer 125 | - [x] First line indent 126 | - [x] Page Numbering 127 | - [ ] Skip numbering of TOC and Abstract 128 | - [x] Template of Specific School 129 | - [x] HUST 130 | - [x] School Logo 131 | - [x] Student Infomation 132 | 133 | 由于依赖了pandoc,除了以上内容,pandoc原生支持的markdown语法也应该正常工作。 134 | 135 | ## Compatibility 136 | 137 | 目前仅在MS Office 2019上进行过测试,测试版本为`Microsoft® Word 2019MSO (版本 2210 Build 16.0.15726.20070) 64 位`,能够正常打开生成的文档。 138 | 打开文档时若提示`是否更新文档中的这些域?`,请选择`是`。另存文件可以消除该提示。 139 | 140 | - 使用LibreOffice的用户打开生成的文档后,需要手动更新目录。 141 | - 若使用其它Word编辑器的用户打开后发现目录为空,也可尝试手动更新目录。 142 | 143 | ## Contribute 144 | 145 | 欢迎PR,欢迎feature request。 146 | 147 | 在open issue之前请先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。 148 | 149 | 在报告bug时应提供: 150 | 151 | - bug现象 152 | - Office版本 153 | - 可供复现的输入文件 154 | 155 | 我不确定是否只有我所在的学校存在报告过多的现象,或者这是一个普遍的现象。如果你有同样的困扰,可以开PR来补充你们学校的template。贡献方式详见 [帮助开发](https://woolen-sheep.github.io/md2report/contribute/) 156 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | output.docx 163 | *.cxx2flow.svg 164 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim as python 2 | ENV PYTHONUNBUFFERED=true 3 | WORKDIR /app 4 | 5 | 6 | FROM python as poetry 7 | ENV POETRY_HOME=/opt/poetry 8 | ENV POETRY_VIRTUALENVS_IN_PROJECT=true 9 | ENV PATH="$POETRY_HOME/bin:$PATH" 10 | RUN python -c 'from urllib.request import urlopen; print(urlopen("https://install.python-poetry.org").read().decode())' | python - 11 | COPY . ./ 12 | RUN poetry install --no-interaction --no-ansi -vvv 13 | 14 | 15 | 16 | FROM python as runtime 17 | ENV PATH="/app/.venv/bin:$PATH" 18 | RUN echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ sid main contrib non-free" > /etc/apt/sources.list &&\ 19 | apt clean && apt update &&\ 20 | apt-get -y install pandoc wget graphviz &&\ 21 | wget https://github.com/Enter-tainer/cxx2flow/releases/download/v0.5.11/cxx2flow-linux-amd64 -O /bin/cxx2flow &&\ 22 | chmod +x /bin/cxx2flow 23 | 24 | COPY --from=poetry /app /app 25 | EXPOSE 8000 26 | CMD ["uvicorn","main:app","--workers","4"] 27 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | Backend of md2report, including server and md2report CLI. 4 | -------------------------------------------------------------------------------- /backend/config/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import yaml 3 | from typing import Optional, Dict, List 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class ConfigLoadFailed(Exception): 8 | pass 9 | 10 | 11 | class ConfigParseFailed(Exception): 12 | pass 13 | 14 | 15 | class TemplateConfig(BaseModel): 16 | pandoc_filters: List[str] = [] 17 | docx_handlers: Dict[str, Dict] = {} 18 | reference: str = "" 19 | 20 | 21 | class Config(BaseModel): 22 | config: str = Field(default="", description="Path to the config file.") 23 | highlight: bool = Field( 24 | default=True, description="Enable the highlight of code blocks." 25 | ) 26 | output: str = Field(default="output.docx", description="Output filename.") 27 | input: str = Field(description="Input filename.") 28 | template: str = Field(default="HUST", description="Template config name.") 29 | templates: Dict[str, TemplateConfig] = {} 30 | first_line_indent: bool = Field( 31 | default=True, description="Enable the first line indent" 32 | ) 33 | 34 | 35 | class ServerConfig(BaseModel): 36 | cache_path: str = "/tmp/md2report" 37 | 38 | 39 | def load_config(args: Dict, filename: str = "config/config.yaml"): 40 | try: 41 | with open(filename) as stream: 42 | config = yaml.safe_load(stream=stream) 43 | config.update(args) 44 | return Config.parse_obj(config) 45 | except Exception as e: 46 | raise ConfigLoadFailed 47 | 48 | 49 | def load_server_config(filename: str = "config/server_config.yaml"): 50 | try: 51 | with open(filename) as stream: 52 | config = yaml.safe_load(stream=stream) 53 | return ServerConfig.parse_obj(config) 54 | except Exception as e: 55 | logging.error("load server onfig failed") 56 | raise ConfigLoadFailed 57 | 58 | 59 | server_config = load_server_config() 60 | -------------------------------------------------------------------------------- /backend/config/config.yaml: -------------------------------------------------------------------------------- 1 | highlight: true 2 | 3 | templates: 4 | HUST: 5 | reference: "HUST.docx" 6 | pandoc_filters: 7 | - "general.py" 8 | docx_handlers: 9 | HUST: {} 10 | first_line_indent: 11 | indent_size: 24 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/config/server_config.yaml: -------------------------------------------------------------------------------- 1 | cache_path: /tmp/md2report 2 | -------------------------------------------------------------------------------- /backend/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | app: 6 | image: woolensheep/md2report:latest 7 | ports: 8 | - 8004:8000 9 | command: uvicorn --host 0.0.0.0 main:app --workers 4 10 | restart: always 11 | volumes: 12 | - /mnt/disk2/md2report/tmp:/tmp/md2report 13 | environment: 14 | - CELERY_BROKER_URL=redis://redis:6379/0 15 | - CELERY_RESULT_BACKEND=redis://redis:6379/0 16 | depends_on: 17 | - redis 18 | 19 | worker: 20 | image: woolensheep/md2report:latest 21 | command: celery -A main.celery worker --loglevel=info 22 | restart: always 23 | environment: 24 | - CELERY_BROKER_URL=redis://redis:6379/0 25 | - CELERY_RESULT_BACKEND=redis://redis:6379/0 26 | volumes: 27 | - /mnt/disk2/md2report/tmp:/tmp/md2report 28 | depends_on: 29 | - app 30 | - redis 31 | 32 | redis: 33 | image: redis:7.0.5-alpine3.16 34 | restart: "always" 35 | ports: 36 | - 6378:6379 37 | volumes: 38 | - /mnt/disk2/md2report/data:/data 39 | command: redis-server 40 | -------------------------------------------------------------------------------- /backend/docx_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict 2 | from docx_handler.hust import process_hust_docx 3 | from docx_handler.general import insert_indent 4 | 5 | handler_map: Dict[str, Callable] = { 6 | "HUST": process_hust_docx, 7 | "first_line_indent": insert_indent, 8 | } 9 | -------------------------------------------------------------------------------- /backend/docx_handler/assets/hust/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/docx_handler/assets/hust/logo.png -------------------------------------------------------------------------------- /backend/docx_handler/general.py: -------------------------------------------------------------------------------- 1 | from docx import Document 2 | from docx.document import Document as TDocument 3 | from docx.text.paragraph import Paragraph 4 | from docx.shared import Pt 5 | 6 | 7 | def insert_indent(filename: str, indent_size: float = 24) -> None: 8 | """ 9 | Insert indent of two Chinese characters to the first line 10 | of style `Body Text` and `First Paragraph` 11 | 12 | Note: When the font is `宋体 小四`, `indent_size` should 13 | be 24. If the template uses another font, you should modify 14 | the `indent_size` by yourself. 15 | `indent_size` should be `pt_size_of_fone * 2`, and you can 16 | find pt size here: 17 | https://en.wikipedia.org/wiki/Traditional_point-size_names 18 | """ 19 | doc: TDocument = Document(filename) 20 | styles = ["Body Text", "First Paragraph"] 21 | indent_size = Pt(indent_size) 22 | p: Paragraph 23 | for p in doc.paragraphs: 24 | if p.style.name not in styles: 25 | continue 26 | p.paragraph_format.first_line_indent = indent_size 27 | doc.save(filename) 28 | -------------------------------------------------------------------------------- /backend/docx_handler/hust.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | 4 | from docx import Document 5 | from docx.document import Document as TDocument 6 | from docx.oxml.xmlchemy import _OxmlElementBase 7 | from docx.table import _Cell, Table 8 | from docx.text.paragraph import Paragraph, Run 9 | from docx.image.image import Image 10 | from docx.enum.text import WD_BREAK 11 | from docx.shared import Inches 12 | from docx.oxml.ns import qn 13 | 14 | from docx_handler.utils import get_paras_by_style_name 15 | 16 | 17 | def add_logo(doc: TDocument): 18 | paras = get_paras_by_style_name(doc, "Title") 19 | if len(paras) != 1: 20 | logging.error("Title of docx not found") 21 | title: Paragraph = paras[0] 22 | logo_path: pathlib.Path = ( 23 | pathlib.Path(__file__).parent.resolve() / "assets" / "hust" / "logo.png" 24 | ) 25 | image_para: Paragraph = doc.add_paragraph() 26 | image_para.style = doc.styles["Figure"] 27 | r = image_para.add_run() 28 | for _ in range(3): 29 | r.add_break(WD_BREAK.LINE) 30 | r.add_picture(str(logo_path), width=Inches(4)) 31 | for _ in range(2): 32 | r.add_break(WD_BREAK.LINE) 33 | title._p.addprevious(image_para._p) 34 | 35 | 36 | def add_student_info_table(doc: TDocument): 37 | """ 38 | Add the table of student info. 39 | 40 | There will be two columns in the table, the first 41 | col is title and the second is content. 42 | 43 | The first col has no border and the second col will have 44 | only the bottom border. 45 | 46 | The style of table is `StudentInfoTable`, and the style 47 | of text is `StudentInfo` 48 | """ 49 | paras = get_paras_by_style_name(doc, "Subtitle") 50 | if len(paras) != 1: 51 | logging.error("Subtitle of docx not found") 52 | title: Paragraph = paras[0] 53 | 54 | front_break: Paragraph = doc.add_paragraph() 55 | r: Run = front_break.add_run() 56 | r.add_break() 57 | title._p.addprevious(front_break._p) 58 | 59 | back_break: Paragraph = doc.add_paragraph() 60 | r: Run = back_break.add_run() 61 | for _ in range(2): 62 | r.add_break() 63 | 64 | table: Table = doc.add_table(rows=5, cols=2) 65 | table.style = doc.styles["StudentInfoTable"] 66 | cell: _Cell 67 | 68 | table.columns[0].cells[0].text = "院系" 69 | table.columns[0].cells[1].text = "专业班级" 70 | table.columns[0].cells[2].text = "姓名" 71 | table.columns[0].cells[3].text = "学号" 72 | table.columns[0].cells[4].text = "指导老师" 73 | 74 | for cell in table.columns[1].cells: 75 | cell.width = Inches(3) 76 | cell.paragraphs[0].style = doc.styles["StudentInfo"] 77 | 78 | for cell in table.columns[0].cells: 79 | cell.width = Inches(2) 80 | cell.paragraphs[0].style = doc.styles["StudentInfo"] 81 | 82 | # set table border 83 | bottom = table._element.xpath("./w:tblPr/w:tblLook")[0] 84 | bottom.set(qn("w:lastColumn"), "1") 85 | bottom.set(qn("w:firstRow"), "0") 86 | 87 | title._p.addnext(table._tbl) 88 | title._p.addnext(back_break._p) 89 | 90 | def replace_top_caption(doc:TDocument): 91 | """ 92 | Replace the caption of "Table of content" by Chinese. 93 | 94 | `sdt` is for "structured document tags", which is the root element TOC. 95 | The caption of TOC is in the first paragraph of `sdtContent`. 96 | """ 97 | 98 | caption = doc.element.xpath("./w:body/w:sdt/w:sdtContent/w:p/w:r/w:t")[0] 99 | caption.text = "目录" 100 | 101 | def process_hust_docx(filename: str): 102 | doc: TDocument = Document(filename) 103 | add_logo(doc) 104 | add_student_info_table(doc) 105 | replace_top_caption(doc) 106 | doc.save(filename) 107 | -------------------------------------------------------------------------------- /backend/docx_handler/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from docx.document import Document as TDocument 3 | from docx.text.paragraph import Paragraph 4 | 5 | 6 | def get_paras_by_style_name(doc: TDocument, style_name: str) -> List[Paragraph]: 7 | res = [] 8 | p: Paragraph 9 | for p in doc.paragraphs: 10 | if p.style.name == style_name: 11 | res.append(p) 12 | return res 13 | -------------------------------------------------------------------------------- /backend/filters/general.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from uuid import uuid4 4 | import xml.etree.ElementTree as ET 5 | from typing import Optional, Tuple, Union, cast, List 6 | from subprocess import Popen, PIPE 7 | 8 | import panflute as pf 9 | 10 | from templates.templates import DocxTemplates 11 | 12 | IMAGE_CAPITAL_TEMPLATE = "{} " 13 | 14 | # If the svg height is too large, part of the image will 15 | # be outside of A4 page. So limit it to 700px. 16 | MAX_SVG_HEIGHT = 700 17 | 18 | 19 | class Index: 20 | """ 21 | For storing the section number, image number and table number 22 | of the document. 23 | """ 24 | 25 | image: int = 0 26 | table: int = 0 27 | number: List[int] = [0, 0, 0] 28 | 29 | @classmethod 30 | def add(cls, level: int) -> None: 31 | """ 32 | Add the index by levelself. 33 | 34 | Note: only the first 3 levels will be stored (i.e. H1 to H3). 35 | """ 36 | if level >= 4: 37 | return 38 | level -= 1 39 | cls.number[level] += 1 40 | if level == 1: 41 | cls.image = 0 42 | cls.table = 0 43 | 44 | @classmethod 45 | def to_str(cls) -> str: 46 | res = "" 47 | for n in cls.number: 48 | if n != 0: 49 | res += f"{n}." 50 | return res 51 | 52 | @classmethod 53 | def get_section_id(cls, level: int): 54 | if level >= 4: 55 | return 0 56 | return cls.number[level - 1] 57 | 58 | @classmethod 59 | def image_index(cls) -> str: 60 | return cls.to_str() + f"{cls.image}" 61 | 62 | @classmethod 63 | def table_index(cls) -> str: 64 | return cls.to_str() + f"{cls.table}" 65 | 66 | 67 | class DocState: 68 | # Table titles are placed in front of the table. 69 | # So we need to store it here. 70 | table_title: str = "" 71 | # Chinese abstract 72 | abstract_zh: str = "" 73 | # English abstract 74 | abstract_en: str = "" 75 | # Flag of whether the image label is inserted. 76 | # if there exists blank in the image caption 77 | # there will be multiple `pf.Str`. The lable 78 | # should be inserted in front of the first 79 | # label. 80 | image_label_inserted: bool = False 81 | 82 | 83 | def add_image_capition(elem, doc) -> Optional[List[pf.Inline]]: 84 | """ 85 | Add image number with section number to image caption. 86 | """ 87 | if type(elem) == pf.Str and not DocState.image_label_inserted: 88 | elem_str: pf.Str = cast(pf.Str, elem) 89 | # Insert the image number with template 90 | number = pf.RawInline( 91 | DocxTemplates.FIGURE_NUMBER.format( 92 | section_id=Index.get_section_id(level=1), image_id=Index.image 93 | ), 94 | format="openxml", 95 | ) 96 | DocState.image_label_inserted = True 97 | return [ 98 | number, 99 | pf.Str(" " + elem_str.text), 100 | ] 101 | 102 | 103 | def append_abstract(elem, doc): 104 | """ 105 | Append abstract after the subtitle. 106 | """ 107 | if type(elem) == pf.Str: 108 | res = [elem] 109 | abstract_zh_str = doc.get_metadata("abstract_zh", None) 110 | if abstract_zh_str: 111 | abstract_zh = pf.RawInline( 112 | DocxTemplates.ABSTRACT_ZH.format(content=abstract_zh_str), 113 | format="openxml", 114 | ) 115 | res.append(abstract_zh) 116 | abstract_en_str = doc.get_metadata("abstract_en", None) 117 | if abstract_en_str != "": 118 | abstract_en = pf.RawInline( 119 | DocxTemplates.ABSTRACT_EN.format(content=abstract_en_str), 120 | format="openxml", 121 | ) 122 | res.append(abstract_en) 123 | 124 | return res 125 | 126 | 127 | def get_svg_height(filename: str) -> int: 128 | """ 129 | Get the height of svg file. If the height is greater then 130 | `MAX_SVG_HEIGHT`, return `MAX_SVG_HEIGHT`; 131 | """ 132 | tree = ET.parse(filename) 133 | root = tree.getroot() 134 | height = int(root.get("height")[:-2]) 135 | if height < MAX_SVG_HEIGHT: 136 | return height 137 | return int(MAX_SVG_HEIGHT) 138 | 139 | 140 | def insert_cxx2flow_svg(code_block: pf.CodeBlock, title: str) -> Union[pf.Para, None]: 141 | """ 142 | all cxx2flow and dot to get the svg flow chart. Then insert 143 | it to the document. 144 | """ 145 | if len(title) == 0: 146 | title = "程序流程图" 147 | elif title[0] == ":": 148 | title = title[1:] 149 | Index.image += 1 150 | # call cxx2flow, which will output a dot file 151 | p = Popen(["cxx2flow"], stdout=PIPE, stdin=PIPE, stderr=PIPE) 152 | dot_data = p.communicate(input=code_block.text.encode("utf-8"))[0] 153 | id = uuid4().hex 154 | output_file = f"{id}.cxx2flow.svg" 155 | # convert dot file to svg 156 | p = Popen( 157 | ["dot", "-Tsvg", "-o", output_file], 158 | stdin=PIPE, 159 | stderr=PIPE, 160 | ) 161 | p.communicate(input=dot_data) 162 | 163 | if not os.path.exists(output_file): 164 | return None 165 | # limit svg's height 166 | height = get_svg_height(output_file) 167 | img = pf.Image( 168 | pf.Str(title), 169 | url=output_file, 170 | title="fig:", 171 | attributes={"height": f"{height}px"}, 172 | ) 173 | DocState.image_label_inserted = False 174 | img.walk(add_image_capition) 175 | return pf.Para(img) 176 | 177 | 178 | def process_report(elem, doc): 179 | """ 180 | Traverse the JSON AST provided by pandoc, and apply 181 | modifications such as styles, captions, reference, etc. 182 | """ 183 | try: 184 | if type(elem) == pf.MetaMap: 185 | meta: pf.MetaMap = cast(pf.MetaMap, elem) 186 | meta.content["subtitle"].walk(append_abstract) # type: ignore 187 | elif type(elem) == pf.Image: 188 | img: pf.Image = cast(pf.Image, elem) 189 | Index.image += 1 190 | DocState.image_label_inserted = False 191 | img.walk(add_image_capition) 192 | return img 193 | elif type(elem) == pf.Table: 194 | table: pf.Table = cast(pf.Table, elem) 195 | Index.table += 1 196 | number = pf.RawInline( 197 | DocxTemplates.TABLE_NUMBER.format( 198 | section_id=Index.get_section_id(level=1), table_id=Index.table 199 | ), 200 | format="openxml", 201 | ) 202 | table.caption = pf.Caption(pf.Para(number, pf.Str(DocState.table_title))) 203 | DocState.table_title = "" 204 | return table 205 | elif type(elem) == pf.Para: 206 | elem_str: pf.Para = cast(pf.Para, elem) 207 | content = pf.stringify(elem_str) 208 | # store the table caption and remove it from the doc. 209 | if content.startswith("#table "): 210 | DocState.table_title = content[7:] 211 | return pf.Para() 212 | elif type(elem) == pf.Header: 213 | header: pf.Header = cast(pf.Header, elem) 214 | Index.add(header.level) 215 | elif type(elem) == pf.CodeBlock: 216 | code_block: pf.CodeBlock = cast(pf.CodeBlock, elem) 217 | lang = code_block.to_json()["c"][0][1] 218 | if len(lang) == 1 and lang[0].startswith("cxx2flow"): 219 | return insert_cxx2flow_svg(code_block=code_block, title=lang[0][8:]) 220 | except Exception as e: 221 | pf.debug(f"Exception in general filter: {e}") 222 | 223 | 224 | if __name__ == "__main__": 225 | pf.toJSONFilter(process_report) 226 | -------------------------------------------------------------------------------- /backend/filters/templates/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/filters/templates/templates.py: -------------------------------------------------------------------------------- 1 | class DocxTemplates: 2 | # Template of figure number under fugures. 3 | # Usage: `FIUGURE_NUMBER.format(section_id = s_id, image_id=i_id)` 4 | FIGURE_NUMBER: str = r""" 5 | 6 | 7 | 8 | 9 | 10 | 图  11 | 12 | 13 | 14 | {section_id} 15 | 16 | 17 | 18 | - 19 | 20 | 21 | 22 | {image_id} 23 | 24 | 25 | 26 | """ 27 | TABLE_NUMBER: str = r""" 28 | 29 | 30 | 31 | 32 | 33 | 表  34 | 35 | 36 | 37 | {section_id} 38 | 39 | 40 | 41 | - 42 | 43 | 44 | 45 | {table_id} 46 | 47 | 48 | 49 | """ 50 | 51 | # `ABSTRACT` is only used to append abstract to the subtitle. 52 | # Since the subtitle is a meta value, we can't append abstract 53 | # right behind it. Because subtitle is a paragraph, we can 54 | # escape the openxml `` to append abstract. 55 | # 56 | # usage: `ABSTRACT.format(content = "content")` 57 | ABSTRACT_EN: str = r""" 58 | 59 | 60 | 61 | 62 | 63 | 64 | Abstract 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {content} 73 | 74 | """ 75 | 76 | ABSTRACT_ZH: str = r""" 77 | 78 | 79 | 80 | 81 | 82 | 83 | 摘要 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | {content} 92 | 93 | """ 94 | 95 | -------------------------------------------------------------------------------- /backend/logger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig( 4 | level=logging.INFO, 5 | format="%(asctime)s [%(levelname)s] %(message)s", 6 | handlers=[ 7 | logging.FileHandler("debug.log"), 8 | logging.StreamHandler() 9 | ] 10 | ) 11 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from re import L 2 | from logger import logging 3 | import pathlib 4 | from typing import Dict 5 | import uuid 6 | import os 7 | from zipfile import ZipFile 8 | from tempfile import TemporaryFile 9 | 10 | from pydantic import BaseModel, Field 11 | 12 | from config.config import load_config, server_config 13 | 14 | from fastapi import ( 15 | Depends, 16 | FastAPI, 17 | Form, 18 | HTTPException, 19 | UploadFile, 20 | status, 21 | APIRouter, 22 | BackgroundTasks, 23 | ) 24 | from fastapi.responses import FileResponse, JSONResponse 25 | from fastapi.middleware.cors import CORSMiddleware 26 | from celery import Celery 27 | from celery.result import AsyncResult 28 | import aiofiles 29 | 30 | from md2report import convert_md_to_docx 31 | 32 | celery = Celery("tasks") 33 | celery.conf.broker_url = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0") # type: ignore 34 | celery.conf.result_backend = os.environ.get( # type: ignore 35 | "CELERY_RESULT_BACKEND", "redis://localhost:6379/0" 36 | ) 37 | celery.conf.broker_transport_options = {"visibility_timeout": 3600} 38 | 39 | 40 | @celery.task 41 | def create_convert_task(args: Dict) -> str: 42 | conf = load_config(args=args) 43 | return convert_md_to_docx(conf) 44 | 45 | 46 | app = FastAPI() 47 | 48 | # allow the origin of local frontend 49 | origins = [ 50 | "http://192.168.2.230:3000", 51 | ] 52 | 53 | app.add_middleware( 54 | CORSMiddleware, 55 | allow_origins=origins, 56 | allow_credentials=True, 57 | allow_methods=["*"], 58 | allow_headers=["*"], 59 | ) 60 | 61 | router = APIRouter(prefix="/api") 62 | 63 | 64 | def form_body(cls): 65 | cls.__signature__ = cls.__signature__.replace( 66 | parameters=[ 67 | arg.replace(default=Form(...)) 68 | for arg in cls.__signature__.parameters.values() 69 | ] 70 | ) 71 | return cls 72 | 73 | 74 | @form_body 75 | class CreateTaskParam(BaseModel): 76 | # name of template to use. 77 | # templates are defined in `config/config.yaml` 78 | template: str = "HUST" 79 | # if enable highlight 80 | highlight: bool = True 81 | # if enable first line indent 82 | first_line_indent: bool = True 83 | 84 | 85 | @router.get("/healthz") 86 | async def health_check(): 87 | return {"message": "Hello World"} 88 | 89 | 90 | @router.post("/tasks") 91 | async def convert_markdown(file: UploadFile, param: CreateTaskParam = Depends()): 92 | """ 93 | Create a generate taskself. 94 | 95 | The markdown file (or extracted file) will be palced in a `cache_path`. 96 | Then a celery task will be created to process the file(s). 97 | 98 | Params: 99 | file (UploadFile): The file need to be convert. Should be *.zip or *.md 100 | param (CreateTaskParam): params passed to worker, see `CreateTaskParam` 101 | for more info. 102 | """ 103 | ext_name = file.filename.split(".")[-1] 104 | if ext_name != "zip" and ext_name != "md": 105 | raise HTTPException( 106 | status_code=status.HTTP_400_BAD_REQUEST, 107 | detail="please upload a .zip or .md file", 108 | ) 109 | save_path = os.path.join(server_config.cache_path, uuid.uuid4().hex) 110 | os.makedirs(save_path) 111 | save_file = pathlib.Path(save_path).resolve() / "markdown.md" 112 | if ext_name == "zip": 113 | tmp_file = TemporaryFile() 114 | tmp_file.write(await file.read()) 115 | with ZipFile(tmp_file) as zip: 116 | for fn in zip.namelist(): 117 | extracted_path = pathlib.Path(zip.extract(fn, save_path)) 118 | # TODO: ensure the behavior of zip files created on not-windows os. 119 | # convert encoding to gbk, to prevent garbled chars in filename. 120 | new_filename = os.path.join(save_path, fn.encode("cp437").decode("gbk")) 121 | extracted_path.rename(new_filename) 122 | # find the first markdown file 123 | # we assmue that there will be only one md file 124 | files = os.listdir(save_path) 125 | for f in files: 126 | if f.endswith(".md"): 127 | os.rename(os.path.join(save_path, f), str(save_file)) 128 | break 129 | else: 130 | async with aiofiles.open(save_file, "wb") as out_file: 131 | content = await file.read() 132 | await out_file.write(content) 133 | output_file = os.path.join(save_path, "output.docx") 134 | args = {"input": str(save_file), "output": output_file} 135 | args.update(param.dict()) 136 | task = create_convert_task.delay(args) 137 | return JSONResponse({"task_id": task.id}) 138 | 139 | 140 | @router.get("/tasks/{task_id}/status") 141 | def get_task_status(task_id): 142 | """ 143 | Get celery task status. 144 | 145 | Note: if a task is not existed, the returned status 146 | will be `PENDING`. 147 | """ 148 | task_result: AsyncResult = celery.AsyncResult(task_id) 149 | return JSONResponse({"status": task_result.status}) 150 | 151 | 152 | def remove_task_files(task_path:str): 153 | p: pathlib.Path = pathlib.Path(task_path).parent.absolute() 154 | os.removedirs(p.name) 155 | 156 | 157 | @router.get("/tasks/{task_id}") 158 | def get_task_result(task_id: str, background_tasks: BackgroundTasks): 159 | """ 160 | Return the generated docx file when task is ready. 161 | """ 162 | task_result: AsyncResult = celery.AsyncResult(task_id) 163 | if task_result.ready(): 164 | task_path = task_result.result 165 | background_tasks.add_task(remove_task_files, task_path) 166 | return FileResponse(task_path, filename="output.docx") 167 | raise HTTPException( 168 | status_code=status.HTTP_400_BAD_REQUEST, detail="task pending or not existed" 169 | ) 170 | 171 | 172 | app.include_router(router) 173 | -------------------------------------------------------------------------------- /backend/md2report.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from config.config import Config, load_config 3 | import yaml 4 | import subprocess 5 | import configargparse 6 | import pathlib 7 | import os 8 | from docx_handler import handler_map 9 | 10 | 11 | class Metadata(BaseModel): 12 | title: str = "主标题" 13 | subtitle: str = "副标题" 14 | abstract_zh: str = "" 15 | abstract_en: str = "" 16 | author: str = "" 17 | school: str = "" 18 | major: str = "" 19 | 20 | 21 | def validate_metadata(filename: str): 22 | reading_meta = False 23 | meta_str = "" 24 | data = "" 25 | with open(filename, "r", encoding="utf-8") as f: 26 | lines = f.readlines() 27 | for i, l in enumerate(lines): 28 | l = l.strip() 29 | if l == "": 30 | continue 31 | if not reading_meta and l != "---": 32 | data = "".join(lines[i:]) 33 | break 34 | if l == "---": 35 | if not reading_meta: 36 | reading_meta = True 37 | continue 38 | else: 39 | data = "".join(lines[i + 1 :]) 40 | break 41 | meta_str += l + "\n" 42 | if meta_str == "": 43 | meta = Metadata() 44 | else: 45 | meta = Metadata.parse_obj(yaml.safe_load(meta_str)) 46 | meta_str = yaml.safe_dump(meta.dict()) 47 | filename += ".validated.md" 48 | with open(filename, "w", encoding="utf-8") as f: 49 | f.write("\n".join(["---", meta_str, "---\n"])) 50 | 51 | f.write(data) 52 | return filename 53 | 54 | 55 | def convert_md_to_docx(conf: Config): 56 | filter_path: pathlib.Path = pathlib.Path(__file__).parent.resolve() / "filters" 57 | reference_path: pathlib.Path = ( 58 | pathlib.Path(__file__).parent.resolve() / "reference-docs" 59 | ) 60 | if conf.output.startswith("/"): 61 | output_path: pathlib.Path = pathlib.Path(conf.output).resolve() 62 | else: 63 | output_path: pathlib.Path = pathlib.Path(os.getcwd()).resolve() / conf.output 64 | input_path: pathlib.Path = pathlib.Path(conf.input).parent.resolve() 65 | input_file: pathlib.Path = pathlib.Path(conf.input).resolve() 66 | 67 | input_file = pathlib.Path(validate_metadata(str(input_file))).resolve() 68 | 69 | command = ["pandoc", "-s", "--toc"] 70 | template = conf.templates[conf.template] 71 | 72 | command.append(str(input_file)) 73 | 74 | command.extend(["--reference-doc", str(reference_path / template.reference)]) 75 | 76 | for f in conf.templates[conf.template].pandoc_filters: 77 | command.extend(["--filter", str(filter_path / f)]) 78 | 79 | if not conf.highlight: 80 | command.extend(["--highlight-style", "monochrome"]) 81 | 82 | command.extend(["-o", str(output_path.absolute())]) 83 | subprocess.run(command, cwd=input_path) 84 | 85 | # calling handlers one by one 86 | for handler_name, params in conf.templates[conf.template].docx_handlers.items(): 87 | # if a handler is set to `False` in config, then skip it 88 | enabled = conf.dict().get(handler_name, True) 89 | if not enabled: 90 | continue 91 | handler_map[handler_name](str(output_path.absolute()), **params) 92 | 93 | os.remove(input_file) 94 | 95 | return str(output_path.absolute()) 96 | 97 | 98 | if __name__ == "__main__": 99 | print("Please give me a star if this application helps you!") 100 | print("如果这个应用有帮助到你,请给我点一个 star!") 101 | print("https://github.com/woolen-sheep/md2report") 102 | p = configargparse.ArgParser() 103 | p.add_argument( 104 | "-c", 105 | "--config", 106 | default="", 107 | required=False, 108 | is_config_file=True, 109 | help="config file path", 110 | ) 111 | p.add_argument( 112 | "--highlight", 113 | default=True, 114 | required=False, 115 | help="enable highlight of code blocks", 116 | ) 117 | p.add_argument( 118 | "-o", 119 | "--output", 120 | default="output.docx", 121 | required=False, 122 | help="output docx filename", 123 | ) 124 | p.add_argument("-i", "--input", required=True, help="input markdown filename") 125 | p.add_argument( 126 | "-t", "--template", default="HUST", required=False, help="template to use" 127 | ) 128 | p.add_argument( 129 | "--first_line_indent", 130 | default=True, 131 | required=False, 132 | help="enable the first line indent", 133 | ) 134 | 135 | args = p.parse_args() 136 | 137 | if args.config != "": 138 | conf = load_config(vars(args), args.config) 139 | else: 140 | conf = load_config(vars(args)) 141 | convert_md_to_docx(conf) 142 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "md2report" 3 | version = "0.1.0" 4 | description = "A tool converting MarkDown files to HUST docx report." 5 | authors = ["woolensheep <2460563632@qq.com>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | black = "^22.10.0" 12 | pyright = "^1.1.277" 13 | panflute = "^2.2.3" 14 | pydantic = "^1.10.2" 15 | configargparse = "^1.5.3" 16 | pydantic-argparse = "^0.6.1" 17 | python-docx = "^0.8.11" 18 | fastapi = {extras = ["all"], version = "^0.86.0"} 19 | celery = {extras = ["redis"], version = "^5.2.7"} 20 | redis = "^4.3.4" 21 | aiofiles = "^22.1.0" 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | panflute = "^2.2.3" 25 | 26 | 27 | [[tool.poetry.source]] 28 | name = "tuna" 29 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 30 | default = true 31 | secondary = false 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | -------------------------------------------------------------------------------- /backend/reference-docs/HUST.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/reference-docs/HUST.docx -------------------------------------------------------------------------------- /backend/test/test_case.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case.zip -------------------------------------------------------------------------------- /backend/test/test_case/5.2数据结构实验报告.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 数据结构实验报告 3 | subtitle: 实验二-哈夫曼编/译码器 4 | abstract_zh: 中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要 5 | abstract_en: English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract 6 | --- 7 | 8 | # 需求分析 9 | 10 | ## 任务 11 | 12 | 利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。但是,这要求在发送端通过一个编码系统对待传数据预先编码,在接收端将传来的数据进行译码(复原)。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站写一个哈夫曼码的编/译码系统。 13 | 14 | - 使用哈夫曼树的构造算法构造哈夫曼树; 15 | - 利用哈夫曼树结构进行编码与译码; 16 | - 进行文件读写操作,包括二进制文件的读写与文本文件的读写; 17 | - 遍历哈夫曼树以输出树结构。 18 | 19 | ## 输入数据 20 | 21 | ### 指令 22 | 23 | 程序通过命令行与用户交互,用户须按照格式输入指令执行对应功能。 24 | 25 | 指令都是按照 `<功能代码> <参数1> <参数2> ...` 的格式定义的。其中,*功能代码*为与功能对应的大写英文字母,参数是不包含空格与特殊符号的字符串。 26 | 27 | ### 字符集与频度 28 | 29 | 当需要从命令行输入字符集与频度构建哈夫曼树时,输入的字符集大小不应超过 100。输入时,每一行需要符合 `<字符> <频度>` 格式,其中频度是不超过 $10^{16}$ 的非负整数。输入结束时,输入一行空行来表示终止输入。 30 | 31 | 为了输入换行符、空格等非图形字符,可以使用转义序列代替特殊字符,支持的转义序列如下表所示: 32 | 33 | #table 转义序列表 34 | 35 | | **字符** | **转义序列** | **描述** | 36 | | -------- | ------------ | ------------- | 37 | | `\x0A` | `\n` | 换行符(LF) | 38 | | `\x20` | `\b` | 半角空格 | 39 | | `\x09` | `\t` | 制表符(Tab) | 40 | 41 | ### 编码文本与译码数据 42 | 43 | 编码与译码的数据直接从用户指定的文件中读取。 44 | 45 | ## 输出数据 46 | 47 | ### 编码结果与译码结果 48 | 49 | 编码与译码的结果直接写入用户指定的文件中,除编译码数据自带的空行外,文件末尾不会增加空行。 50 | 51 | ### 哈夫曼树 52 | 53 | 输出哈夫曼树结构时,使用了凹入表的模式,凹入表每行按照层次缩进,除缩进以外的部分符合格式 `<0/1>: <字符>`,其中的 0/1 代表为左儿子或右儿子。若结点上无字符,输出 `(NULL)`;若结点字符为文件终止符,输出 `(EOF)`。 54 | 55 | # 概要设计 56 | 57 | ## 数据结构 58 | 59 | ### 哈夫曼树 60 | 61 | 哈夫曼树结点类名为 `HfmTreeNode`,包含三个 public 成员变量: 62 | 63 | | **变量名** | **类型** | **描述** | 64 | | ---------- | --------------- | -------------------------------------------------------------- | 65 | | ch | `char` | 节点上的字符,若为文件终止符值为 `char(-1)`,若无字符值为 `\0` | 66 | | lch | `HfmTreeNode *` | 左儿子指针,若无左儿子值为 `nullptr` | 67 | | rch | `HfmTreeNode *` | 右儿子指针,若无右儿子值为 `nullptr` | 68 | 69 | 哈夫曼树类名为 `HfmTree`,包含一个用于二进制序列化树结构的 `private` 结构体类型 `DataUnion`,包含一个 `public` 变量 `root` 指针指向树根结点,实现了构造函数与析构函数,其他 `public` 成员函数定义见下表: 70 | 71 | | **函数名** | **参数列表** | **返回值类型** | **描述** | 72 | | ---------- | --------------------------------------------- | -------------- | ------------------------------------------------------ | 73 | | init | (空) | `void` | 从二进制文件中读取数据初始化哈夫曼树 | 74 | | init | `std::map &charset` | `void` | 从给定字符集初始化哈夫曼树 | 75 | | save | (空) | `void` | 将当前哈夫曼树结构二进制序列化并写入文件 | 76 | | print | `const std::string &output = "TreePrint.txt"` | `void` | 以凹入表形式输出当前哈夫曼树结构,并将输入内容写入文件 | 77 | 78 | ### 优先队列/堆 79 | 80 | 实现哈夫曼树的构造算法时使用了优先队列/堆,该数据结构使用了 STL 中的大根堆实现 `std::priority_queue, std::vector>, std::greater<>>`,其定义参见 https://zh.cppreference.com/w/cpp/container/priority_queue。 81 | 82 | ### 队列 83 | 84 | 实现从文件读取哈夫曼树结构、将结构写入文件与析构函数时使用了队列,该数据结构使用了 STL 中的单端队列实现 `std::queue`,其定义参见 https://zh.cppreference.com/w/cpp/container/queue。 85 | 86 | ## 编码/解码器 87 | 88 | 编码/解码器类名为 `EnDecoder`,包含两个 `private` 成员变量: 89 | 90 | | **变量名** | **类型** | **描述** | 91 | | ---------- | ----------------------------------------------- | -------------------- | 92 | | tree | `HfmTree` | 编解码使用的哈夫曼树 | 93 | | mp | `std::map>` | 缓存每个字符对应 | 94 | 95 | 构造器接受一个参数 `const HfmTree &_tree`,该参数提供了编解码使用的哈夫曼树。该类同时包含三个 `public` 成员函数: 96 | 97 | | **函数名** | **参数列表** | **返回值类型** | **描述** | 98 | | ---------- | --------------------------------------------------------------------------------------- | -------------- | -------------------------- | 99 | | encode | `const std::string &input = "ToBeTran.txt", const std::string &output = "CodeFile.dat"` | `void` | 根据用户指定的文件进行编码 | 100 | | decode | `const std::string &input = "CodeFile.dat", const std::string &output = "TextFile.txt"` | `void` | 根据用户指定的文件进行解码 | 101 | | print_code | (空) | `void` | 输出用户指定文件中的编码 | 102 | 103 | ## 异常 104 | 105 | 程序在遇到错误时以抛出异常的方式处理,因此自定义了异常类型 `ExpException`,`public` 继承 STL 的异常基类 `std::exception`。构造器接收异常信息参数 `std::string _msg`,该信息在 `what()` 成员函数中被展示。 106 | 107 | ## 主程序 108 | 109 | 主程序的流程如下: 110 | 111 | 1. 检查是否有记录哈夫曼树结构的文件,若有则从文件初始化哈夫曼树,否则要求用户从命令行输入字符集与频度信息并初始化哈夫曼树; 112 | 2. 初始化编码/解码器; 113 | 3. 输出程序帮助信息; 114 | 4. 读取与解析指令,按照指令调用对应功能执行。 115 | 116 | # 详细设计 117 | 118 | 以下内容省略了一部分非核心设计的细节伪代码,仅保留了核心部分的伪代码。 119 | 120 | ## 哈夫曼树 121 | 122 | ### 从字符集构建哈夫曼树 123 | 124 | ```cpp 125 | void InitHfmTree(HfmTree *tree, Charset charset) { 126 | // 检查字符集是否合法 127 | if (!HasChar(charset, EOF)) InsertChar(charset, EOF, 0); 128 | if (charset.size > CHARSET_SIZE_MAX) throw ...; 129 | 130 | // 利用优先队列/堆构建哈夫曼树 131 | PriorityQueue pq; 132 | for (Char c : charset) { 133 | // 创建每个字符对应的叶子结点 134 | HfmTreeNode *p; 135 | NewNode(&p); 136 | p->ch = c.ch; 137 | p->lch = p->rch = nullptr; 138 | EnQueue(&pq, Pair(c.freq, p)); 139 | } 140 | while (pq.size > 1) { 141 | // 取出队列中频率最小的两个结点,将其接至同一个父结点,设置父结点频度为两儿子之和,将父结点放回队列 142 | Pair p1, p2; 143 | DeQueue(&pq, &p1); 144 | DeQueue(&pq, &p2); 145 | HfmTreeNode *p; 146 | p->ch = 0; 147 | p->lch = p1.p; 148 | p->rch = p2.p; 149 | EnQueue(&pq, Pair(p1.freq + p2.freq, p)); 150 | } 151 | // 队列中最后剩下的结点为哈夫曼树的根结点 152 | Pair p; 153 | DeQueue(&pq, &p); 154 | tree->root = p.p; 155 | 156 | // 将构建完成的哈夫曼树写入文件 157 | SaveHfmTree(tree); 158 | } 159 | ``` 160 | 161 | ### 将哈夫曼树结构写入文件 162 | 163 | ```cpp 164 | void SaveHfmTree(HfmTree *tree) { 165 | // 打开文件 166 | File f; 167 | OpenFile(&f, "hfmTree.dat", BINARY_FILE); 168 | 169 | // 初始化用于遍历哈夫曼树的队列 170 | Queue q; 171 | InitQueue(&q); 172 | EnQueue(&q, tree.root); 173 | 174 | DataUnion dat; // 二进制序列化遍历的结构体 175 | int tail_idx = 1; // 队列尾元素的编号,从 1 开始编号 176 | while (!QueueEmpty(q)) { 177 | HfmTreeNode *p; 178 | DeQueue(&q, &p); 179 | // 二进制序列化 180 | dat.ch = p->ch; 181 | if (p->lch != nullptr) { 182 | dat.lch = ++tail; 183 | EnQueue(&p, p->lch); 184 | } else { 185 | dat.lch = 0; 186 | } 187 | if (p->rch != nullptr) { 188 | dat.rch = ++tail; 189 | EnQueue(&p, p->rch); 190 | } else { 191 | dat.rch = 0; 192 | } 193 | // 将序列化后的结果写入文件 194 | WriteFile(f, dat); 195 | } 196 | 197 | CloseFile(f); 198 | } 199 | ``` 200 | 201 | ### 从文件中读取哈夫曼树结构 202 | 203 | ```cpp 204 | void InitHfmTree(HfmTree *tree) { 205 | // 打开文件 206 | if (!FileExists("hfmTree.dat")) throw ...; 207 | File f; 208 | OpenFile(&f, "hfmTree.dat", BINARY_FILE); 209 | 210 | // 从文件中读取哈夫曼树结构信息 211 | Array nodes; 212 | DataUnion dat; 213 | while (ReadFile(f, &dat)) { 214 | HfmTreeNode *p; 215 | NewNode(&p); 216 | AppendArray(&nodes, dat, p); 217 | } 218 | CloseFile(f); 219 | 220 | // 恢复哈夫曼树结构 221 | if (ArrayEmpty(nodes)) throw ...; 222 | for (DataUnion dat, HfmTreeNode *p : nodes) { 223 | if (!ValidIndex(dat.lch) || !ValidIndex(dat.rch)) throw ...; 224 | p->ch = dat.ch; 225 | p->lch = p->rch = nullptr; 226 | // 根据儿子的编号建立到儿子的指针 227 | if (dat.lch > 0) p->lch = nodes[dat.lch - 1].p; 228 | if (dat.rch > 0) p->rch = nodes[dat.rch - 1].p; 229 | } 230 | tree->root = nodes[0].p; 231 | } 232 | ``` 233 | 234 | ### 输出树结构 235 | 236 | ```cpp 237 | void PrintHfmTree(HfmTreeNode *p, int num, int dep) { 238 | // 按凹入表格式递归输出树结构 239 | for (int i = 0; i < dep; i++) Write('\t'); // 缩进 240 | Write(char(num + '0') + ": "); // 左右儿子标记 241 | if (IsLeafNode(p)) { 242 | // 如果为叶子结点,输出记录的字符 243 | if (p->ch == EOF) Write("(EOF)"); 244 | else Write(p->ch); 245 | } else { 246 | Write("(NULL)"); 247 | } 248 | if (p->lch != nullptr) PrintHfmTree(p->lch, 0, dep + 1); 249 | if (p->rch != nullptr) PrintHfmTree(p->rch, 1, dep + 1); 250 | } 251 | ``` 252 | 253 | ## 编码/解码器 254 | 255 | ### 编码 256 | 257 | ```cpp 258 | void Encode(EnDecoder endecoder, File in, File out) { 259 | // 编码输入数据,并写入输出文件中 260 | while (!FileEOF(in)) { 261 | char c; 262 | ReadChar(in, &c); 263 | if (!MapFind(endecoder.mp, c)) throw ...; 264 | Write(out, endecoder.mp[c]); // 在实现时使用了 buffer,实际细节比伪代码多 265 | } 266 | CloseFile(in); 267 | CloseFile(out); 268 | } 269 | ``` 270 | 271 | ### 解码 272 | 273 | ```cpp 274 | void Decode(EnDecoder endecoder, File in, File out) { 275 | // 解码输入数据,并写入输出文件中 276 | HfmTreeNode *p = endecoder.tree->root; 277 | while (!FileEOF(out)) { 278 | int t; 279 | ReadBit(in, &t); 280 | // 根据编码的 0/1 判断应该向左儿子或右儿子走 281 | if (t == 0) p = p->lch; 282 | else p = p->rch; 283 | if (p == nullptr) throw ...; 284 | if (IsLeafNode(p)) { 285 | // 到达叶子节点说明一个字符解码完成 286 | if (p->ch == EOF) break; 287 | Write(out, p->ch); // 在实现时使用了 buffer,实际细节比伪代码多 288 | p = tree->root; 289 | } 290 | } 291 | CloseFile(in); 292 | CloseFile(out); 293 | } 294 | ``` 295 | 296 | ### 输出编码 297 | 298 | ```cpp 299 | void PrintCode(EnDecoder endecoder, File in) { 300 | // 输出输入文件中的编码 301 | HfmTreeNode *p = endecoder.tree->root; 302 | while (!FileEOF(out)) { 303 | int t; 304 | ReadBit(in, &t); 305 | Write(out, char(t + '0')); 306 | if (t == 0) p = p->lch; 307 | else p = p->rch; 308 | if (IsLeafNode(p)) { 309 | if (p->ch == EOF) break; 310 | p = tree->root; 311 | } 312 | } 313 | CloseFile(in); 314 | CloseFile(out); 315 | } 316 | ``` 317 | 318 | ## 主程序 319 | 320 | ```cpp 321 | int main() { 322 | // 初始化哈夫曼树 323 | if (!FileExists("hfmTree.dat")) { 324 | tree = InitHfmTree(); 325 | } else { 326 | tree->init(); 327 | } 328 | 329 | // 初始化编码/解码器 330 | EnDecoder endecoder; 331 | InitEnDecoder(&endecoder, tree); 332 | 333 | // 输出帮助信息 334 | PrintHelpMsg(); 335 | File in, out; 336 | while (TRUE) { 337 | // 读取用户指令,根据指令调用指定功能 338 | PrintLineHeader(); 339 | Cmd op; 340 | ReadOp(&op); 341 | if (op == 'I') { 342 | tree = InitHfmTree(); 343 | } else if (op == 'E') { 344 | ProcCmd(&in, &out); 345 | Encode(endecoder, in, out); 346 | } else if (op == 'D') { 347 | ProcCmd(&in, &out); 348 | Decode(endecoder, in, out); 349 | } else if (op == 'P') { 350 | ProcCmd(&in); 351 | PrintCode(endecoder, in); 352 | } else if (op == 'T') { 353 | ProcCmd(&out); 354 | PrintTree(tree, out); 355 | } else { 356 | Print("Unknow command!"); // 未知输入异常 357 | continue; 358 | } 359 | Print("Success!"); 360 | } 361 | return 0; 362 | } 363 | ``` 364 | 365 | ### 函数调用关系 366 | 367 | ![调用 关系](调用关系.png) 368 | 369 | # 调试分析 370 | 371 | ### 技术细节 372 | 373 | - 语言及标准:ISO C++ 17 374 | - 开发环境:JetBrains CLion 2019.3.4 375 | - 编译器:GNU C++ Compiler 9.2.0 376 | - 编译选项:`-Wall -Wextra -O2` 377 | - 生成器:CMake 3.15 378 | 379 | ### 设计与实现 380 | 381 | 1. 采用了类似面向对象(Object-Oriented)思想的设计,使功能得到了很好的分类; 382 | 2. 使用转义序列代替不好通过键盘输入的特殊字符,解决了输入字符集的问题; 383 | 3. 灵活运用 C++ 新特性,如 lambda 表达式、自动类型推断等,使编码更简单; 384 | 4. 采用了自定义指令的交互方式,使交互更便捷; 385 | 5. 采用了基于异常处理的错误处理方式; 386 | 6. 进行了许多边界与不合法状况的检查,避免错误数据造成程序作出异常行为。 387 | 388 | ### 时空分析 389 | 390 | #### 时间复杂度 391 | 392 | ##### 哈夫曼树的构造 393 | 394 | 用 $n$ 代表字符集的大小,由于使用了数据结构堆,其单次操作的复杂度为 $O(\log n)$,因此总时间复杂度为 $O(n \log n)$。 395 | 396 | ##### 编码与解码 397 | 398 | 用 $n$ 代表编码/解码数据长度, 399 | 400 | - 在编码时,每次查询到该字符的编码并拼接,单次复杂度为 $O(1)$,总复杂度为 $O(n)$; 401 | - 在解码时,每次读到一位 0/1 都在哈夫曼树上向儿子走一步,单次复杂度为 $O(1)$,总复杂度为 $O(n)$。 402 | 403 | #### 空间复杂度 404 | 405 | ##### 哈夫曼树 406 | 407 | 用 $n$ 代表字符集大小,根据构造算法,每次提取两个结点的同时新建一个结点,最终的结点数规模为 $O(n)$,因此空间复杂度为 $O(n)$。 408 | 409 | ##### 编码与解码 410 | 411 | 用 $n$ 代表编码/解码数据长度,$k$ 代表字符集大小: 412 | 413 | - 在编码时,期望情况下树高为 $O(\log k)$ 规模的,因此总空间复杂度为 $O(n \log k)$,而最坏情况下可以达到 $O(nk)$; 414 | - 在解码时,由于解码是编码的逆运算,易得期望情况下的复杂度为 $O(n \log^{-1} k)$,最坏情况下的复杂度为 $O(n)$。 415 | 416 | ### 问题与经验 417 | 418 | 1. 未注意到端序对变量存储的影响,这个错误使二进制序列化得到了错误的结果,通过学习端序、采用结构体二进制序列化解决了问题,并学习了 C++ 关键字 `reinterpret_cast` 的使用; 419 | 2. `getline` 输入函数会读入多余空行,导致输入数据为空的问题,通过多次读入消耗掉空行解决了问题; 420 | 3. 未使用二进制文件模式打开文件,使文件中多出了一些换行符,干扰了二进制序列化的数据,改用二进制文件模式解决了问题; 421 | 4. 在手动处理 buffer 读写的过程中出现了位运算的使用错误,通过系统检查修正了错误,并且加深了对位运算的学习与理解; 422 | 5. 错误使用了流函数 `readsome`,用 `read` 函数替换解决了该问题,说明对 STL 流输入输出的使用不够熟练,需要多加学习; 423 | 6. 在析构 `EnDecoder` 对象时也会析构对象内部的 `HfmTree` 对象,因此不应重复析构 `HfmTree` 对象,此处的问题导致了程序的异常行为,修复该问题加深了对析构特性的理解。 424 | 425 | # **五、用户使用说明** 426 | 427 | 实验使用的二进制可执行文件为 `exp52.exe`(Linux 下为 `exp52`),用户通过命令行输入指令进行交互。 428 | 429 | ## 初次使用 430 | 431 | 初次使用时,程序会显示以下信息提醒输入字符集与频数以初始化哈夫曼树,按照提示中的格式输入字符集并以空行结束输入即可。 432 | 433 | ![初次使用](初次使用.png) 434 | 435 | 输入字符集示例如下图所示。 436 | 437 | ![初次使用-2](初次使用-2.png) 438 | 439 | ## 编码 440 | 441 | 先将待编码信息写入文本文件中,然后使用指令 `E ` 进行编码,如图所示。 442 | 443 | ![编码](编码.png) 444 | 445 | ![编码-2](编码-2.png) 446 | 447 | 示例指令运行完毕后,可以在 `out.dat` 中找到编码后的数据,如图所示。 448 | 449 | ![编码-3](编码-3.png) 450 | 451 | ## 解码 452 | 453 | 先将待编码信息保存在指定文件中,然后使用指令 `D ` 进行解码,如图所示。 454 | 455 | ![解码](解码.png) 456 | 457 | 示例指令运行完毕后,可以在 `out.txt` 中找到解码后的数据,如图所示。 458 | 459 | ![解码-2](解码-2.png) 460 | 461 | ## 输出编码结果 462 | 463 | 使用指令 `P ` 输出编码,如图所示。 464 | 465 | ![输出编码](输出编码.png) 466 | 467 | ## 输出哈夫曼树 468 | 469 | 使用指令 `T ` 输出哈夫曼树结构,如图所示。 470 | 471 | ![输出树](输出树.png) 472 | 473 | ![输出树-2](输出树-2.png) 474 | 475 | ## 退出程序 476 | 477 | 输入指令 `X` 退出程序。 478 | 479 | # 测试结果 480 | 481 | 以下测试结果皆为对程序的正确性校验结果。 482 | 483 | ## 数据 1 484 | 485 | **输入:** 486 | 487 | ``` 488 | (Encode) 489 | THIS PROGRAM IS MY FAVORITE 490 | ``` 491 | 492 | **输出:** 493 | 494 | ``` 495 | 11010001011000111111000100010100110000100101010110 496 | 01011101100011111110010100011111110011101011000001 497 | 00100100110110101011000010010 498 | ``` 499 | 500 | ## 数据 2 501 | 502 | **输入:** 503 | 504 | ``` 505 | (Decode) 506 | 11010001011000111111000100010100110000100101010110 507 | 01011101100011111110010100011111110011101011000001 508 | 00100100110110101011000010010 509 | ``` 510 | 511 | **输出:** 512 | 513 | ``` 514 | THIS PROGRAM IS MY FAVORITE 515 | ``` 516 | 517 | ## 数据 3 518 | 519 | **输入:** 520 | 521 | ``` 522 | (Encode) 523 | ON APRIL FIRST PRESIDENT XI JINPING INSTRUCTED GOVERNMENT OFFICIALS TO TAKE MORE MEASURES TO COORDINATE DISEASE PREVENTION AND THE RESUMPTION OF ECONOMIC PRODUCTION IN AN EFFORT TO FULFILL THIS YEARS DEVELOPMENT GOALS DURING HIS FOUR DAY INSPECTION TRIP TO ZHEJIANG PROVINCE 524 | ``` 525 | 526 | **输出:** 527 | 528 | ``` 529 | 10010111111101010001000100110101111111100110110001 530 | 00011110111110001000100100011011010110010011111011 531 | 11110000101101101111100001001101100111100010011001 532 | 11100001111011001110011110100100000100000110101010 533 | 11011110000110011100000010001001111100100100111110 534 | 11111001110011110011011000000011010101011100111111 535 | 10110011111101101011000011010111110010100100100101 536 | 11110010010101000110000100100100011111110110011110 537 | 00001001100100101011001100111101011010101111011001 538 | 10001101010100011010111100010001001011000000100111 539 | 11010110100101111111010011110110111110100010101110 540 | 01001000110000111001010001011010110100101111111001 541 | 11001111101000000100101111001110010011000000111100 542 | 01000101001101100000100000110101101001011111101100 543 | 11111110100111111010110011110011100100101101111110 544 | 11001111110011000011011111001101101011110111111110 545 | 10001011000111111000110101010001000111111011001011 546 | 00000010101111001100010110010010011111011111000011 547 | 00110101011100111111011000001001001100111100001111 548 | 00010110001111111001110010000100101111011010101000 549 | 11111011001110011100010010000001101011010010111111 550 | 11010010011010001011111011001111110000100000010101 551 | 10000100110110101001111000011111000100010100111000 552 | 00011001110000001010010010110111111011001111110011 553 | 00001101111100110110101111011111111010001011000111 554 | 1110001101010100010001111111000010010 555 | ``` 556 | 557 | ## 数据 4 558 | 559 | **输入:** 560 | 561 | ``` 562 | (Decode) 563 | 01010100010010000100010100100000010011100100111101 564 | 01011001000101010011000010000001000011010011110101 565 | 00100100111101001110010000010101011001001001010100 566 | 10010101010101001100100000010100000100000101001110 567 | 01000100010001010100110101001001010000110010000001 568 | 00100101010011001000000101010001001000010001010010 569 | 00000101001101000101010101100100010101010010010001 570 | 01010100110101010000100000010000110101001001001001 571 | 01010011010010010101001100100000010101000100100001 572 | 00010100100000010101110100111101010010010011000100 573 | 01000010000001001000010000010101001100100000010001 574 | 10010000010100001101000101010001000010000001010100 575 | 01001000010010010101001100100000010000110100010101 576 | 00111001010100010101010101001001011001001000000101 577 | 01110100111101010010010100110100010100100000010101 578 | 00010010000100000101001110001000000101010001001000 579 | 01000101001000000100011101001100010011110100001001 580 | 00000101001100001000000100011001001001010011100100 581 | 00010100111001000011010010010100000101001100001000 582 | 00010000110101001001001001010100110100100101010011 583 | 00100000010101000100100001000001010101000010000001 584 | 01010001001000010001010010000001000111010011000100 585 | 11110100001001000001010011000010000001010000010101 586 | 01010000100100110001001001010000110010000001001000 587 | 01000101010000010100110001010100010010000010000001 588 | 00010101001101010001010101001001000111010001010100 589 | 11100100001101011001001000000100001101000001010011 590 | 10001000000100011101001001010101100100010100100000 591 | 01010010010010010101001101000101001000000101010001 592 | 00111100100000010000010010000001001000010011110101 593 | 00110101010000100000010011110100011000100000010000 594 | 11010100100100100101010011010001010101001100100000 595 | 01001001010011100100001101001100010101010100010001 596 | 00100101001110010001110010000001000101010000110100 597 | 11110100111001001111010011010100100101000011001000 598 | 00010100110100111101000011010010010100000101001100 599 | 00100000010000010100111001000100001000000101000001 600 | 00111101001100010010010101010001001001010000110100 601 | 00010100110000100000010000110101001001001001010100 602 | 11010001010101001100100000010001110100100101010110 603 | 01000101010011100010000001001001010101000101001100 604 | 10000001001000010101010100011101000101001000000100 605 | 10010100110101010000010000010100001101010100001000 606 | 00010011110100111000100000010101000100100001000101 607 | 00100000010001110100110001001111010000100100000101 608 | 00110000100000010100110101010101010000010100000100 609 | 11000101100100100000010010010100111001000100010101 610 | 01010100110101010001010010010010010100000101001100 611 | 00100000010000010100111001000100001000000101011001 612 | 00000101001100010101010100010100100000010000110100 613 | 10000100000101001001010011100101001100100000010100 614 | 11010010000100111101010101010011000100010000100000 615 | 01000111010010010101011001000101001000000100000101 616 | 00111000100000010010010100010001000101010000010010 617 | 00000100000101000010010011110101010101010100001000 618 | 00010010010101010001010011001000000100010001001001 619 | 01010010010001010010000001001100010011110100111001 620 | 00011100100000010101000100010101010010010011010010 621 | 00000100010101000110010001100100010101000011010101 622 | 00010100110100111000100000010101000100100001000101 623 | 00100000010001110100110001001111010000100100000101 624 | 00110000100000010100110101010101010000010100000100 625 | 11000101100100100000010010010100111001000100010101 626 | 01010100110101010001010010010010010100000101001100 627 | ``` 628 | 629 | **输出:** 630 | 631 | ``` 632 | THE NOVEL CORONAVIRUS PANDEMIC IS THE SEVEREST CRISIS THE WORLD HAS FACED THIS CENTURY WORSE THAN THE GLOBAL FINANCIAL CRISIS THAT THE GLOBAL PUBLIC HEALTH EMERGENCY CAN GIVE RISE TO A HOST OF CRISES INCLUDING ECONOMIC SOCIAL AND POLITICAL CRISES GIVEN ITS HUGE IMPACT ON THE GLOBAL SUPPLY INDUSTRIAL AND VALUE CHAINS SHOULD GIVE AN IDEA ABOUT ITS DIRE LONG TERM EFFECTS 633 | ``` 634 | 635 | ## 数据 5 636 | 637 | 对程序代码 `main.cpp` 进行字符统计与编码,编码文件为 `main_cpp.dat`。 638 | 639 | ## 数据 6 640 | 641 | 输出根据字符集 642 | 643 | ``` 644 | \b 186 645 | A 64 646 | B 13 647 | C 22 648 | D 32 649 | E 103 650 | F 21 651 | G 15 652 | H 47 653 | I 57 654 | J 1 655 | K 5 656 | L 32 657 | M 20 658 | N 57 659 | O 63 660 | P 15 661 | Q 1 662 | R 48 663 | S 51 664 | T 80 665 | U 23 666 | V 8 667 | W 18 668 | X 1 669 | Y 16 670 | Z 1 671 | ``` 672 | 673 | 构造的哈夫曼树,输出文件见 `tree_test.txt`。 674 | 675 | # 附录 676 | 677 | 实验报告文件清单: 678 | 679 | | 文件名 | 描述 | 680 | | ------------------------ | ----------------------------------------- | 681 | | 5.2 数据结构实验报告.pdf | 实验报告文档 | 682 | | 5.2 数据结构实验报告.md | 实验报告 Markdown 源文件 | 683 | | exp52.exe | Windows 下的二进制可执行实验程序 | 684 | | main_cpp.dat | 测试 5 的输出文件 | 685 | | tree_test.txt | 测试 6 的输出文件 | 686 | | gen_charset.cpp | 用于统计文本文档中字符集的工具 C++ 源文件 | 687 | | code/CMakeList.txt | CMake 配置文件 | 688 | | code/main.cpp | 主程序源文件 | 689 | | code/coding.h | 编码部分头文件 | 690 | | code/coding.cpp | 编码部分源文件 | 691 | | code/common.h | 常量配置部分头文件 | 692 | | code/common.cpp | 常量配置部分源文件 | 693 | | code/except.h | 异常部分头文件 | 694 | | code/hfmtree.h | 哈夫曼树部分头文件 | 695 | | code/hfmtree.cpp | 哈夫曼树部分源文件 | 696 | -------------------------------------------------------------------------------- /backend/test/test_case/初次使用-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/初次使用-2.png -------------------------------------------------------------------------------- /backend/test/test_case/初次使用.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/初次使用.png -------------------------------------------------------------------------------- /backend/test/test_case/编码-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/编码-2.png -------------------------------------------------------------------------------- /backend/test/test_case/编码-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/编码-3.png -------------------------------------------------------------------------------- /backend/test/test_case/编码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/编码.png -------------------------------------------------------------------------------- /backend/test/test_case/解码-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/解码-2.png -------------------------------------------------------------------------------- /backend/test/test_case/解码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/解码.png -------------------------------------------------------------------------------- /backend/test/test_case/调用关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/调用关系.png -------------------------------------------------------------------------------- /backend/test/test_case/输出树-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/输出树-2.png -------------------------------------------------------------------------------- /backend/test/test_case/输出树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/输出树.png -------------------------------------------------------------------------------- /backend/test/test_case/输出编码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/backend/test/test_case/输出编码.png -------------------------------------------------------------------------------- /backend/test/test_cxx2flow/test_cxx2flow.md: -------------------------------------------------------------------------------- 1 | 2 | # 流程图 3 | 4 | ```cxx2flow:流程图标题 5 | int main() { 6 | // 初始化哈夫曼树 7 | if (!FileExists("hfmTree.dat")) { 8 | tree = InitHfmTree(); 9 | } else { 10 | tree->init(); 11 | } 12 | 13 | // 初始化编码/解码器 14 | EnDecoder endecoder; 15 | InitEnDecoder(&endecoder, tree); 16 | 17 | // 输出帮助信息 18 | PrintHelpMsg(); 19 | File in, out; 20 | while (TRUE) { 21 | // 读取用户指令,根据指令调用指定功能 22 | PrintLineHeader(); 23 | Cmd op; 24 | ReadOp(&op); 25 | if (op == 'I') { 26 | tree = InitHfmTree(); 27 | } else if (op == 'E') { 28 | ProcCmd(&in, &out); 29 | Encode(endecoder, in, out); 30 | } else if (op == 'D') { 31 | ProcCmd(&in, &out); 32 | Decode(endecoder, in, out); 33 | } else if (op == 'P') { 34 | ProcCmd(&in); 35 | PrintCode(endecoder, in); 36 | } else if (op == 'T') { 37 | ProcCmd(&out); 38 | PrintTree(tree, out); 39 | } else { 40 | Print("Unknow command!"); // 未知输入异常 41 | continue; 42 | } 43 | Print("Success!"); 44 | } 45 | return 0; 46 | } 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # 帮助开发 2 | 3 | ## 工作原理 4 | 5 | md2report 大体上是一个 pandoc filter ,对 pandoc 生成的 Json AST 做了一些修改,使之符合大学报告的要求。对于有些无法在 AST 中实现的修改,则使用 python-docx 修改最后生成的 docx 文件。 6 | 7 | - pandoc filters 位于 `backend/filters` , 称其为filters。 8 | - 调用pandoc时会使用 `--reference-doc` 参数指定模板,模板位于 `backend/reference-docs`。 9 | - pandoc生成出docx文件后,会调用 `python-docx` 对其进行修改, 代码位于 `backend/docx_han`。 10 | 11 | ### `filters/general.py` 12 | 13 | 此文件的定位是一个通用filter,负责添加摘要、添加图注、添加表格标题等。一般来说可以直接使用这个filter。 14 | 15 | ## 环境搭建 16 | 17 | 使用poetry管理开发环境。安装poetry后: 18 | 19 | ```bash 20 | cd backend 21 | poetry install 22 | poetry shell 23 | 24 | which python 25 | # should output a venv python 26 | 27 | python md2report.py -h 28 | ``` 29 | 30 | ## 如何添加模板 31 | 32 | 开始添加模板之前,建议先了解: 33 | 34 | - [pandoc filter](https://pandoc.org/filters.html) 35 | - [pandoc 文档](https://pandoc.org/MANUAL.html) 中的 `--reference-doc` 参数 36 | - [panflute 文档](http://scorreia.com/software/panflute/) 37 | - [python-docx 文档](https://python-docx.readthedocs.io/en/latest/) 38 | 39 | ### 新增reference doc 40 | 41 | 复制一份HUST.docx,修改为你需要的名称,然后编辑其中你需要修改的样式(比如正文样式,标题样式,页眉页脚等)。 42 | 43 | ### 新增filter 44 | 45 | 大部分情况下,可以直接使用general.py。如果你真的有需要,可以把新增的filter置于 `backend/filters` 下。 46 | 47 | ### 新增docx handler 48 | 49 | 可以参考 `hust.py`, 在这一部分可以完成你们学校的一些定制化的设定,比如学校的logo。 50 | 51 | ### 修改config 52 | 53 | 编辑 `backend/config/config.yaml`, 参考现有配置增加一个新的配置,并且指定你添加的filter和handler 54 | 55 | !!! note 56 | 57 | filter和handler可以不止有一个,config中指定的是一个列表,将会被按顺序调用。 58 | 59 | ### 修改前端 60 | 61 | 如果需要,将你的配置名称添加到前端的select列表里。 62 | -------------------------------------------------------------------------------- /docs/grammar.md: -------------------------------------------------------------------------------- 1 | # 标准语法 2 | 3 | md2report使用的大部分是标准markdown语法,但是markdown标记到docx的样式映射可能与你的习惯不同。 4 | 按照推荐的方式使用markdown标记能生成更加规范的报告。 5 | 6 | ## 标题与副标题 7 | 8 | 大部分人在写markdown时习惯于将`H1`(`# `)作为主标题,但在md2report中,应使用`metadata`来指定标题。 9 | H1在md2report中为一级节标题。 10 | 11 | 应该这样: 12 | 13 | ```markdown 14 | --- 15 | title: 数据结构实验报告 16 | subtitle: 哈夫曼编/译码器 17 | --- 18 | 19 | # (第一节标题) 20 | ``` 21 | 22 | 而不是这样: 23 | ```markdown 24 | # 数据结构实验报告 25 | 26 | ##(第一节标题) 27 | ``` 28 | 29 | 第二种写法会导致章节编号错位。 30 | 31 | ## 摘要 32 | 33 | 你可以通过添加metadata的方式为你的文档添加摘要: 34 | 35 | ```markdown 36 | --- 37 | title: 数据结构实验报告 38 | subtitle: 实验二-哈夫曼编/译码器 39 | abstract_zh: 中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要中文摘要 40 | abstract_en: English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract English Abstract 41 | --- 42 | 43 | # 第一节标题 44 | ``` 45 | 46 | ## 章节标题 47 | 48 | md2report支持markdown heading (H1-H9)的识别,并支持H1-H4的自动编号。因此在编写报告时请不要手动加入文本编号。 49 | 50 | 应该这样: 51 | 52 | ```markdown 53 | # Heading text 54 | 55 | ## Heading text 56 | ``` 57 | 58 | 而不是这样: 59 | 60 | ```markdown 61 | # 1. Heading text 62 | 63 | ## 1.1 Heading text 64 | ``` 65 | 66 | ## 图注 67 | 68 | md2report会将所有的图片标题转换为图片图注,并且添加支持引用的图片编号。因此请不要留空图片标题。 69 | 70 | ```markdown 71 | ![解码](Decoding.png) 72 | ``` 73 | 74 | !!! warning 75 | 76 | 虽然`backend/test`中的测试文件使用了中文文件名,但是在使用md2report的时候请尽量避免使用中文文件名,防止因为编码问题引起的转换失败。如果你遇到了这种情况,请放心提交issue。 77 | 78 | ## 表标题 79 | 80 | 虽然markdown并不支持表标题,但是你可以添加能被md2report识别的注解信息作为表标题: 81 | 82 | ```markdown 83 | #table 转义序列表 84 | 85 | | **字符** | **转义序列** | **描述** | 86 | | -------- | ------------ | ------------- | 87 | | `\x0A` | `\n` | 换行符(LF) | 88 | | `\x20` | `\b` | 半角空格 | 89 | | `\x09` | `\t` | 制表符(Tab) | 90 | ``` 91 | 92 | 同样的,md2report也会为表格添加支持引用的表格编号。 93 | 94 | # 追加特性 95 | 96 | ## cxx2flow 97 | 98 | md2report 集成了 [cxx2flow](https://github.com/Enter-tainer/cxx2flow),可以将c++代码转化为流程图。 99 | 100 | ````markdown 101 | 102 | ```cxx2flow:流程图标题 103 | int main() { 104 | // 初始化哈夫曼树 105 | if (!FileExists("hfmTree.dat")) { 106 | tree = InitHfmTree(); 107 | } else { 108 | tree->init(); 109 | } 110 | 111 | // 初始化编码/解码器 112 | EnDecoder endecoder; 113 | InitEnDecoder(&endecoder, tree); 114 | 115 | // 输出帮助信息 116 | PrintHelpMsg(); 117 | File in, out; 118 | while (TRUE) { 119 | // 读取用户指令,根据指令调用指定功能 120 | PrintLineHeader(); 121 | Cmd op; 122 | ReadOp(&op); 123 | if (op == 'I') { 124 | tree = InitHfmTree(); 125 | } else if (op == 'E') { 126 | ProcCmd(&in, &out); 127 | Encode(endecoder, in, out); 128 | } else if (op == 'D') { 129 | ProcCmd(&in, &out); 130 | Decode(endecoder, in, out); 131 | } else if (op == 'P') { 132 | ProcCmd(&in); 133 | PrintCode(endecoder, in); 134 | } else if (op == 'T') { 135 | ProcCmd(&out); 136 | PrintTree(tree, out); 137 | } else { 138 | Print("Unknow command!"); // 未知输入异常 139 | continue; 140 | } 141 | Print("Success!"); 142 | } 143 | return 0; 144 | } 145 | ``` 146 | 147 | ```` 148 | 149 | 生成效果: 150 | 151 | ![cxx2flow](img/cxx2flow.svg) 152 | 153 | !!! note 154 | 155 | cxx2flow仅会生成 `main` 函数的流程图,如果有多个函数的流程图需要生成,则需要编写多个包含main函数的 `cxx2flow` 代码块。 156 | 157 | !!! warning 158 | 159 | cxx2flow生成的流程图并不能完美符合部分大学对于流程图的要求,用户需自行斟酌是否使用。 160 | -------------------------------------------------------------------------------- /docs/img/Headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/Headings.png -------------------------------------------------------------------------------- /docs/img/TOC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/TOC.png -------------------------------------------------------------------------------- /docs/img/abstract_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/abstract_en.png -------------------------------------------------------------------------------- /docs/img/abstract_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/abstract_zh.png -------------------------------------------------------------------------------- /docs/img/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/code.png -------------------------------------------------------------------------------- /docs/img/cxx2flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/cxx2flow.png -------------------------------------------------------------------------------- /docs/img/cxx2flow.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | D0 14 | 15 | begin 16 | 17 | 18 | 19 | D4 20 | 21 | (!FileExists("hfmTree.dat"))? 22 | 23 | 24 | 25 | D0->D4 26 | 27 | 28 | 29 | 30 | 31 | D1 32 | 33 | end 34 | 35 | 36 | 37 | D9 38 | 39 | tree = InitHfmTree(); 40 | 41 | 42 | 43 | D4:s->D9:n 44 | 45 | 46 | Y 47 | 48 | 49 | 50 | D13 51 | 52 | tree->init(); 53 | 54 | 55 | 56 | D4:e->D13:n 57 | 58 | 59 | N 60 | 61 | 62 | 63 | D15 64 | 65 | EnDecoder endecoder; 66 | 67 | 68 | 69 | D9->D15 70 | 71 | 72 | 73 | 74 | 75 | D13->D15 76 | 77 | 78 | 79 | 80 | 81 | D17 82 | 83 | InitEnDecoder(&endecoder, tree); 84 | 85 | 86 | 87 | D15->D17 88 | 89 | 90 | 91 | 92 | 93 | D19 94 | 95 | PrintHelpMsg(); 96 | 97 | 98 | 99 | D17->D19 100 | 101 | 102 | 103 | 104 | 105 | D21 106 | 107 | File in, out; 108 | 109 | 110 | 111 | D19->D21 112 | 113 | 114 | 115 | 116 | 117 | D23 118 | 119 | (TRUE)? 120 | 121 | 122 | 123 | D21->D23 124 | 125 | 126 | 127 | 128 | 129 | D28 130 | 131 | PrintLineHeader(); 132 | 133 | 134 | 135 | D23:s->D28:n 136 | 137 | 138 | Y 139 | 140 | 141 | 142 | D85 143 | 144 | return 0; 145 | 146 | 147 | 148 | D23:e->D85:n 149 | 150 | 151 | N 152 | 153 | 154 | 155 | D30 156 | 157 | Cmd op; 158 | 159 | 160 | 161 | D28->D30 162 | 163 | 164 | 165 | 166 | 167 | D32 168 | 169 | ReadOp(&op); 170 | 171 | 172 | 173 | D30->D32 174 | 175 | 176 | 177 | 178 | 179 | D34 180 | 181 | (op == 'I')? 182 | 183 | 184 | 185 | D32->D34 186 | 187 | 188 | 189 | 190 | 191 | D39 192 | 193 | tree = InitHfmTree(); 194 | 195 | 196 | 197 | D34:s->D39:n 198 | 199 | 200 | Y 201 | 202 | 203 | 204 | D41 205 | 206 | (op == 'E')? 207 | 208 | 209 | 210 | D34:e->D41:n 211 | 212 | 213 | N 214 | 215 | 216 | 217 | D83 218 | 219 | Print("Success!"); 220 | 221 | 222 | 223 | D39->D83 224 | 225 | 226 | 227 | 228 | 229 | D46 230 | 231 | ProcCmd(&in, &out); 232 | 233 | 234 | 235 | D41:s->D46:n 236 | 237 | 238 | Y 239 | 240 | 241 | 242 | D50 243 | 244 | (op == 'D')? 245 | 246 | 247 | 248 | D41:e->D50:n 249 | 250 | 251 | N 252 | 253 | 254 | 255 | D48 256 | 257 | Encode(endecoder, in, out); 258 | 259 | 260 | 261 | D46->D48 262 | 263 | 264 | 265 | 266 | 267 | D48->D83 268 | 269 | 270 | 271 | 272 | 273 | D55 274 | 275 | ProcCmd(&in, &out); 276 | 277 | 278 | 279 | D50:s->D55:n 280 | 281 | 282 | Y 283 | 284 | 285 | 286 | D59 287 | 288 | (op == 'P')? 289 | 290 | 291 | 292 | D50:e->D59:n 293 | 294 | 295 | N 296 | 297 | 298 | 299 | D57 300 | 301 | Decode(endecoder, in, out); 302 | 303 | 304 | 305 | D55->D57 306 | 307 | 308 | 309 | 310 | 311 | D57->D83 312 | 313 | 314 | 315 | 316 | 317 | D64 318 | 319 | ProcCmd(&in); 320 | 321 | 322 | 323 | D59:s->D64:n 324 | 325 | 326 | Y 327 | 328 | 329 | 330 | D68 331 | 332 | (op == 'T')? 333 | 334 | 335 | 336 | D59:e->D68:n 337 | 338 | 339 | N 340 | 341 | 342 | 343 | D66 344 | 345 | PrintCode(endecoder, in); 346 | 347 | 348 | 349 | D64->D66 350 | 351 | 352 | 353 | 354 | 355 | D66->D83 356 | 357 | 358 | 359 | 360 | 361 | D73 362 | 363 | ProcCmd(&out); 364 | 365 | 366 | 367 | D68:s->D73:n 368 | 369 | 370 | Y 371 | 372 | 373 | 374 | D79 375 | 376 | Print("Unknow command!"); 377 | 378 | 379 | 380 | D68:e->D79:n 381 | 382 | 383 | N 384 | 385 | 386 | 387 | D75 388 | 389 | PrintTree(tree, out); 390 | 391 | 392 | 393 | D73->D75 394 | 395 | 396 | 397 | 398 | 399 | D75->D83 400 | 401 | 402 | 403 | 404 | 405 | D81 406 | 407 | continue 408 | 409 | 410 | 411 | D79->D81 412 | 413 | 414 | 415 | 416 | 417 | D81->D23 418 | 419 | 420 | 421 | 422 | 423 | D83->D23 424 | 425 | 426 | 427 | 428 | 429 | D85->D1 430 | 431 | 432 | 433 | 434 | 435 | -------------------------------------------------------------------------------- /docs/img/figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/figure.png -------------------------------------------------------------------------------- /docs/img/front_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/front_page.png -------------------------------------------------------------------------------- /docs/img/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/docs/img/table.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # md2report 2 | 3 | 一个用于将Markdown文件转换为可以直接提交给学校的实验报告/大作业报告/期末小论文的工具。 4 | 5 | 如果你的院系/课程要求必须提交docx格式的报告,并且: 6 | 7 | - 你认为word/docx实在是太蠢了,并且习惯于使用markdown编辑文档,md2report能够大幅缩短你在报告格式、docx样式以及排版上花费的时间; 8 | - 你没有试过使用markdown,不妨尝试一下: [Markdown Guides](https://www.markdownguide.org/), [Mardown 教程](https://markdown.com.cn/); 9 | 10 | 如果你的院系/课程允许提交pdf格式的报告,寻找一个好用的tex模板或许是一个更好的方案。但是考虑到学习成本以及使用难度,md2report仍然可以作为一个替代选项。 11 | 12 | ## Quick Start 13 | 14 | 虽然md2report使用的都是标准markdown语法,但是markdown标记到docx的样式映射可能与你的习惯不同。 15 | 因此,请确保除了本小节内容以外,开始使用md2report之前先阅读文档中关于[语法](/grammar)的部分。 16 | 17 | ### Web UI 18 | 19 | md2report提供了[Web UI](https://md2report.hust.online), 无需安装即可使用。 20 | 21 | ### CLI 22 | 23 | md2report提供了CLI,如果想使用CLI,需要: 24 | 25 | - python 3.10+ 26 | - poetry in PATH 27 | - pandoc in PATH 28 | 29 | 30 | ```bash 31 | git clone https://github.com/woolen-sheep/md2report.git 32 | cd backend 33 | poetry install 34 | poetry shell 35 | 36 | python md2report.py -h 37 | # usage: md2report.py [-h] [-c CONFIG] [--highlight HIGHLIGHT] [-o OUTPUT] -i INPUT [-t TEMPLATE] 38 | # 39 | # options: 40 | # -h, --help show this help message and exit 41 | # -c CONFIG, --config CONFIG 42 | # config file path 43 | # --highlight HIGHLIGHT 44 | # enable highlight of code blocks 45 | # -o OUTPUT, --output OUTPUT 46 | # output docx filename 47 | # -i INPUT, --input INPUT 48 | # input markdown filename 49 | # -t TEMPLATE, --template TEMPLATE 50 | # template to use 51 | # 52 | # Args that start with '--' (eg. --highlight) can also be set in a config file (specified via -c). Config file syntax allows: 53 | # key=value, flag=true, stuff=[a,b,c] (for details, see syntax at https://goo.gl/R74nmi). If an arg is specified in more than 54 | # one place, then commandline values override config file values which override defaults. 55 | 56 | python md2report.py -i test/test_case/5.2数据结构实验报告.md 57 | 58 | # see output.docx 59 | 60 | ``` 61 | 62 | ### Self-Hosted Web UI 63 | 64 | md2docx的Web UI也是开源的,你可以使用docker-compose部署。 65 | 需要注意的是现在的`docker-compose.yaml`中挂载了绝对路径,使用之前请先修改。 66 | 67 | ```bash 68 | cd backend 69 | docker compose up --build -d 70 | docker compose ps 71 | ``` 72 | 73 | ## Features 74 | 75 | 目前支持的特性如下: 76 | 77 | - [x] Title and SubTitle 78 | - [x] Abstract 79 | - [x] Heading (H1 to H4) 80 | - [x] Image Caption 81 | - [x] Table 82 | - [x] Table Caption 83 | - [x] Code Highlight 84 | - [x] Table of Content 85 | - [x] Header and Footer 86 | - [x] Page Numbering 87 | - [ ] Skip numbering of TOC and Abstract 88 | - [x] Template of Specific School 89 | - [x] HUST 90 | - [x] School Logo 91 | - [x] Student Infomation 92 | 93 | 由于依赖了pandoc,除了以上内容,pandoc原生支持的markdown语法也应该正常工作。 94 | 95 | ## Compatibility 96 | 97 | 目前仅在MS Office 2019上进行过测试,测试版本为`Microsoft® Word 2019MSO (版本 2210 Build 16.0.15726.20070) 64 位`,能够正常打开生成的文档。 98 | 打开文档时若提示`是否更新文档中的这些域?`,请选择`是`。另存文件可以消除该提示。 99 | 100 | ## Helping Development 101 | 102 | 在open issue之前请先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。 103 | 104 | 我不确定是否只有我所在的学校存在报告过多的现象,或者这是一个普遍的现象。如果你有同样的困扰,可以开PR来补充你们学校的template。 105 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | md2report是一个用于将Markdown文件转换为可以直接提交给学校的实验报告/大作业报告/期末小论文的工具。 4 | 5 | 如果你的院系/课程要求必须提交docx格式的报告,并且: 6 | 7 | - 你认为word/docx实在是太蠢了,并且习惯于使用markdown编辑文档,md2report能够大幅缩短你在报告格式、docx样式以及排版上花费的时间; 8 | - 你没有试过使用markdown,不妨尝试一下: [Markdown Guides](https://www.markdownguide.org/), [Mardown 教程](https://markdown.com.cn/); 9 | 10 | 如果你的院系/课程允许提交pdf格式的报告,寻找一个好用的tex模板或许是一个更好的方案。但是考虑到学习成本以及使用难度,md2report仍然可以作为一个替代选项。 11 | -------------------------------------------------------------------------------- /docs/preview.md: -------------------------------------------------------------------------------- 1 | # 效果预览 2 | 3 | ## 标题页 4 | 5 | ![标题页](img/front_page.png) 6 | 7 | ## 摘要 8 | 9 | ![中文摘要](img/abstract_zh.png) 10 | 11 | ![image-20221109113340062](img/abstract_en.png) 12 | 13 | ## 目录 14 | 15 | ![目录](img/TOC.png) 16 | 17 | ## 节标题与自动编号 18 | 19 | ![节标题与自动编号](img/Headings.png) 20 | 21 | ## 表格 22 | 23 | ![表格](img/table.png) 24 | 25 | ## 图片 26 | 27 | ![图片](img/figure.png) 28 | 29 | ## 代码 30 | 31 | ![代码](img/code.png) 32 | 33 | !!! note 34 | 35 | 根据设置的选项,也可以生成不带有代码高亮的代码块。 36 | 37 | ## 流程图 38 | 39 | ![流程图](img/cxx2flow.png) -------------------------------------------------------------------------------- /docs/webui.md: -------------------------------------------------------------------------------- 1 | # webui 2 | 3 | 在使用[webui](https://md2report.hust.online)时,如果要上传zip文件: 4 | 5 | - md文件应位于zip文件的根目录,而不应该在子文件夹中,参考`backend/test/test_case.zip` 6 | - 尽量使用英文命名,并且不包含空格 7 | 8 | !!! warning 9 | 10 | WebUI仅为不会使用python的用户提供便利,不能保证可用性,也不保证是最新版本。大体积文件建议使用CLI。请不要上传zip bomb,如果服务受到攻击可能考虑关闭服务。 11 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "md2report", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.1.3", 10 | "daisyui": "^2.38.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "url-join": "^5.0.0", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^10.4.13", 43 | "postcss": "^8.4.18", 44 | "tailwindcss": "^3.2.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 29 | md2report 30 | 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "md2report", 3 | "name": "md2report", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woolen-sheep/md2report/e9f427b23124e6dee3ed031650572f88a41d521c/frontend/src/App.css -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useState } from 'react'; 3 | import './App.css'; 4 | import config from "./config.js" 5 | import urlJoin from 'url-join'; 6 | 7 | function App () { 8 | const [highlight, setHighlight] = useState(true) 9 | const [indent, setIndent] = useState(true) 10 | const [templateName, setTemplateName] = useState("HUST") 11 | const [markdownFile, setMarkdownFile] = useState(null); 12 | const [errorToastVisible, setErrorToastVisible] = useState(false) 13 | const [errorToast, setErrorToast] = useState("") 14 | 15 | const showError = (msg, duration) => { 16 | setErrorToast(msg) 17 | setErrorToastVisible(true) 18 | setTimeout(() => { 19 | setErrorToastVisible(false) 20 | }, duration); 21 | } 22 | 23 | const submitGenerateTask = () => { 24 | const formData = new FormData() 25 | formData.append("file", markdownFile) 26 | formData.append("template", templateName) 27 | formData.append("highlight", highlight) 28 | formData.append("first_line_indent", indent) 29 | axios.post(config.TASKS_URL, formData) 30 | .then((resp) => { 31 | const task_id = resp.data.task_id 32 | const interval = setInterval(() => { 33 | // poll the status of task 34 | axios.get(urlJoin(config.TASKS_URL, task_id, config.TASKS_STATUS_SUFFIX)).then((resp) => { 35 | if (resp.data.status === "PENDING") return 36 | clearInterval(interval); 37 | // download file 38 | axios.get(urlJoin(config.TASKS_URL, task_id), { responseType: 'blob' }).then((resp) => { 39 | const href = URL.createObjectURL(resp.data) 40 | const link = document.createElement('a') 41 | link.href = href 42 | link.setAttribute('download', 'output.docx') 43 | document.body.appendChild(link) 44 | link.click() 45 | document.body.removeChild(link) 46 | URL.revokeObjectURL(href) 47 | }).catch((err) => { 48 | console.log(err) 49 | if (err.response.data.detail) { 50 | showError(err.response.data.detail, 5000) 51 | } else { 52 | showError("download file failed", 5000) 53 | } 54 | }) 55 | }).catch((err) => { 56 | if (err.response.data.detail) { 57 | showError(err.response.data.detail, 5000) 58 | } else { 59 | showError("get task status file failed", 5000) 60 | clearInterval(interval) 61 | } 62 | }) 63 | }, 1000); 64 | }) 65 | } 66 | 67 | return ( 68 |
69 |
70 |
71 |
72 | md2report 73 |
74 |
75 |
76 | 81 |
82 |
83 |
84 |
85 | 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {errorToast} 98 |
99 |
100 |
101 |
102 |
103 |

No More Docx Reports!

104 |

generate your reports in seconds

105 |
106 | 107 |
108 | 109 | 110 | 111 |
112 |
113 | 114 | Chose your markdown file (or zip file) 115 | 123 | 124 |
125 |
126 | setMarkdownFile(e.target.files[0])} /> 128 |
129 | 130 |
131 | 132 | 133 | 134 |
135 |
136 | Set generate options 137 |
138 |
139 | 142 | 147 |
148 |
149 | 157 |
158 |
159 | 167 |
168 | 169 |
170 | 171 | 172 | 173 |
174 |
175 | Get your docx report 176 |
177 |
178 | 179 |
180 | 181 |
182 |
183 | ); 184 | } 185 | 186 | export default App; 187 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | let config = {} 2 | 3 | if (process.env.NODE_ENV === "development") { 4 | config.BASE_URL = "http://192.168.2.230:8000/api" 5 | } else { 6 | config.BASE_URL = "/api" 7 | } 8 | 9 | config.TASKS_URL = config.BASE_URL + "/tasks" 10 | config.TASKS_STATUS_SUFFIX = "/status" 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #root, 8 | #app, 9 | #app>div { 10 | height: 100% 11 | } 12 | 13 | body { 14 | margin: 0; 15 | height: 100%; 16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 17 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 18 | sans-serif; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | code { 24 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 25 | monospace; 26 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [require("daisyui")], 10 | daisyui: { 11 | styled: true, 12 | themes: true, 13 | base: true, 14 | utils: true, 15 | logs: true, 16 | rtl: false, 17 | prefix: "", 18 | darkTheme: "dark", 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: md2report 2 | site_url: https://example.com/ 3 | repo_url: https://github.com/woolen-sheep/md2report 4 | nav: 5 | - 简介: introduction.md 6 | - 效果预览: preview.md 7 | - 语法: grammar.md 8 | - WebUI: webui.md 9 | - 帮助开发: contribute.md 10 | theme: 11 | name: material 12 | markdown_extensions: 13 | - admonition 14 | - pymdownx.details 15 | - pymdownx.superfences 16 | --------------------------------------------------------------------------------