init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)
公式支持三类来源:
- OMML(Word 2007+ 公式) → Python omml_mathml2latex
- MathType OLE 对象 → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力) → EqToLatexUtil(本次新增)
本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
移植自 jby backend-services office-core,EQ 语法 → LaTeX
(\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
\a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
(原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)
修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留
安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
OssService 的 @Value 默认密钥已清空
This commit is contained in:
60
scripts/mathtype2mathml/.dockerignore
Normal file
60
scripts/mathtype2mathml/.dockerignore
Normal file
@@ -0,0 +1,60 @@
|
||||
# Node.js
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Ruby
|
||||
.bundle
|
||||
vendor/bundle
|
||||
|
||||
# 开发工具
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
!README.md
|
||||
!SETUP_GUIDE.md
|
||||
|
||||
# 临时文件和缓存
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
*.tmp
|
||||
*.temp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
logs
|
||||
|
||||
# 编辑器文件
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 测试文件
|
||||
test_*
|
||||
spec/
|
||||
.rspec
|
||||
|
||||
# 环境文件
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 构建文件
|
||||
dist/
|
||||
build/
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
116
scripts/mathtype2mathml/DOCKER_SUMMARY.md
Normal file
116
scripts/mathtype2mathml/DOCKER_SUMMARY.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 🐳 MathType to MathML Docker 化完成
|
||||
|
||||
## 📁 新增的 Docker 文件
|
||||
|
||||
| 文件名 | 用途 | 描述 |
|
||||
|--------|------|------|
|
||||
| `Dockerfile` | 容器构建配置 | 定义如何构建包含 Ruby + Node.js 的容器镜像 |
|
||||
| `docker-compose.yml` | 服务编排 | 简化容器启动、端口映射和卷挂载 |
|
||||
| `.dockerignore` | 构建排除 | 指定哪些文件不需要复制到容器中 |
|
||||
| `entrypoint.sh` | 启动脚本 | 容器启动时的环境检查和初始化 |
|
||||
| `docker-build.sh` | 构建脚本 | 自动化构建和启动流程 |
|
||||
| `README.Docker.md` | 详细文档 | 完整的 Docker 部署和使用指南 |
|
||||
| `uploads/.gitkeep` | 目录占位 | 确保上传目录存在 |
|
||||
|
||||
## 🚀 快速启动(3种方式)
|
||||
|
||||
### 方式1:一键脚本(最简单)
|
||||
```bash
|
||||
./docker-build.sh
|
||||
```
|
||||
|
||||
### 方式2:Docker Compose(推荐)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 方式3:原生 Docker
|
||||
```bash
|
||||
docker build -t mathtype-converter .
|
||||
docker run -d --name mathtype-to-mathml -p 3000:3000 -v $(pwd)/uploads:/app/uploads mathtype-converter
|
||||
```
|
||||
|
||||
## 🌐 访问服务
|
||||
|
||||
启动后通过以下地址访问:
|
||||
|
||||
- **Web 界面**: http://localhost:3000/mathtype_converter_demo.html
|
||||
- **健康检查**: http://localhost:3000/health
|
||||
- **API 转换**: `POST http://localhost:3000/convert-mathtype`
|
||||
|
||||
## 📋 常用管理命令
|
||||
|
||||
```bash
|
||||
# 查看运行状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
|
||||
# 重启服务
|
||||
docker-compose restart
|
||||
|
||||
# 重建镜像
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
- **基础镜像**: Ruby 3.1 (Debian Bullseye)
|
||||
- **运行时**: Ruby + Node.js LTS
|
||||
- **端口**: 3000
|
||||
- **依赖**: mathtype_to_mathml gem + Express.js
|
||||
- **持久化**: uploads 目录挂载
|
||||
|
||||
## 📊 资源使用
|
||||
|
||||
- **镜像大小**: ~500-800MB
|
||||
- **运行内存**: ~100-200MB
|
||||
- **文件上传限制**: 10MB
|
||||
- **并发处理**: 支持多文件并发转换
|
||||
|
||||
## 🚀 部署到生产环境
|
||||
|
||||
1. **修改端口映射**:编辑 `docker-compose.yml` 中的端口配置
|
||||
2. **添加反向代理**:使用 Nginx/Apache 代理到容器端口
|
||||
3. **配置域名**:更新 Traefik 标签或反向代理配置
|
||||
4. **监控日志**:设置日志轮转和监控告警
|
||||
5. **备份数据**:定期备份 uploads 目录
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口冲突**: 修改 `docker-compose.yml` 中的端口映射
|
||||
2. **Ruby Gem 错误**: 重建镜像 `docker-compose build --no-cache`
|
||||
3. **文件权限**: 检查 uploads 目录权限
|
||||
4. **内存不足**: 增加 Docker 内存限制
|
||||
|
||||
### 调试命令
|
||||
```bash
|
||||
# 进入容器调试
|
||||
docker-compose exec mathtype-converter bash
|
||||
|
||||
# 查看详细日志
|
||||
docker-compose logs --tail=100 mathtype-converter
|
||||
|
||||
# 检查容器内文件
|
||||
docker exec mathtype-to-mathml ls -la /app
|
||||
```
|
||||
|
||||
## ✅ 验证部署
|
||||
|
||||
部署完成后,验证以下功能:
|
||||
|
||||
1. ✅ 健康检查响应正常
|
||||
2. ✅ Web 界面可访问
|
||||
3. ✅ 文件上传功能正常
|
||||
4. ✅ MathML 转换功能正常
|
||||
5. ✅ 容器重启后服务恢复
|
||||
|
||||
---
|
||||
|
||||
🎉 **Docker 化完成!** 现在您可以轻松地将此服务部署到任何支持 Docker 的平台上。
|
||||
71
scripts/mathtype2mathml/Dockerfile
Normal file
71
scripts/mathtype2mathml/Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# 使用官方 Ruby 镜像作为基础镜像
|
||||
FROM ruby:3.1-bullseye
|
||||
|
||||
# 设置镜像源(提高国内网络访问速度)
|
||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
gnupg \
|
||||
build-essential \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
pkg-config \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Node.js 16 (LTS) - 使用官方二进制包,增加重试机制
|
||||
RUN for i in 1 2 3; do \
|
||||
curl -fsSL --connect-timeout 10 --max-time 300 \
|
||||
https://nodejs.org/dist/v16.20.2/node-v16.20.2-linux-x64.tar.xz -o node.tar.xz \
|
||||
&& break || sleep 5; \
|
||||
done \
|
||||
&& tar -xJf node.tar.xz -C /usr/local --strip-components=1 \
|
||||
&& rm node.tar.xz \
|
||||
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 vendor 目录(包含本地gems)
|
||||
COPY vendor ./vendor
|
||||
|
||||
# 复制 Gemfile 和 package.json 先安装依赖(利用 Docker 缓存)
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# 配置npm使用国内镜像源(提高网络访问速度)
|
||||
RUN npm config set registry https://registry.npmmirror.com/ \
|
||||
&& npm config set timeout 60000 \
|
||||
&& npm config set network-timeout 300000
|
||||
|
||||
# 配置bundle使用国内gem源
|
||||
RUN bundle config mirror.https://rubygems.org https://gems.ruby-china.com
|
||||
|
||||
# 安装 Ruby 依赖(不使用 deployment 模式避免版本冲突)
|
||||
RUN bundle config set --local without 'development test' \
|
||||
&& bundle install --retry=3
|
||||
|
||||
# 安装 Node.js 依赖(使用重试机制)
|
||||
RUN npm install --production --retry=3 || \
|
||||
(npm config set registry https://registry.npmjs.org/ && npm install --production --retry=3)
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p uploads public
|
||||
|
||||
# 设置文件权限
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 设置启动脚本
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
# 默认命令
|
||||
CMD ["npm", "start"]
|
||||
21
scripts/mathtype2mathml/Gemfile
Normal file
21
scripts/mathtype2mathml/Gemfile
Normal file
@@ -0,0 +1,21 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# Use local, vendored gems
|
||||
gem 'mathtype_to_mathml', path: 'vendor/gems/mathtype_to_mathml'
|
||||
gem 'mathtype', path: 'vendor/gems/mathtype'
|
||||
|
||||
# Required dependency for XML processing
|
||||
gem 'nokogiri', '~> 1.13'
|
||||
|
||||
# Required dependency for mathtype_to_mathml gem
|
||||
gem 'pry'
|
||||
|
||||
# Concurrency support for batch processing
|
||||
gem 'concurrent-ruby', '~> 1.1'
|
||||
|
||||
# Multi-process parallel processing
|
||||
gem 'parallel'
|
||||
|
||||
# Optional: Add these for better development experience
|
||||
gem 'rake', '~> 13.0'
|
||||
gem 'rspec', '~> 3.0' if ENV['RAILS_ENV'] == 'test'
|
||||
67
scripts/mathtype2mathml/Gemfile.lock
Normal file
67
scripts/mathtype2mathml/Gemfile.lock
Normal file
@@ -0,0 +1,67 @@
|
||||
PATH
|
||||
remote: vendor/gems/mathtype_to_mathml
|
||||
specs:
|
||||
mathtype_to_mathml (0.0.7)
|
||||
mathtype (~> 0.1.0)
|
||||
nokogiri (~> 1.6)
|
||||
|
||||
PATH
|
||||
remote: vendor/gems/mathtype
|
||||
specs:
|
||||
mathtype (0.1.0)
|
||||
bindata (~> 2.1)
|
||||
nokogiri (~> 1.6)
|
||||
ruby-ole (~> 1.2)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
bindata (2.5.1)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.7)
|
||||
io-console (0.8.2)
|
||||
method_source (1.1.0)
|
||||
nokogiri (1.19.4-x64-mingw-ucrt)
|
||||
racc (~> 1.4)
|
||||
parallel (2.1.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
racc (1.8.1)
|
||||
rake (13.4.2)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
ruby-ole (1.2.13.1)
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw-ucrt
|
||||
|
||||
DEPENDENCIES
|
||||
concurrent-ruby (~> 1.1)
|
||||
mathtype!
|
||||
mathtype_to_mathml!
|
||||
nokogiri (~> 1.13)
|
||||
parallel
|
||||
pry
|
||||
rake (~> 13.0)
|
||||
|
||||
CHECKSUMS
|
||||
bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58
|
||||
bundler (4.0.15) sha256=a4ceb882fe94a0e0ac63cd0813932bbfd631a14e5ac0b7975189b19a4d28d9e7
|
||||
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
|
||||
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
|
||||
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
||||
mathtype (0.1.0)
|
||||
mathtype_to_mathml (0.0.7)
|
||||
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
|
||||
nokogiri (1.19.4-x64-mingw-ucrt) sha256=051da97b8eccfdb5444fed40246a35e10d7298b9efe759b4cd25455ea04c587e
|
||||
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
||||
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
|
||||
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
||||
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
||||
ruby-ole (1.2.13.1) sha256=578d10dd2a797a2b35a1286c6fb2c9525f67c24791346fc8015d39f0ffa3cb72
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.15
|
||||
182
scripts/mathtype2mathml/README.Docker.md
Normal file
182
scripts/mathtype2mathml/README.Docker.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# MathType to MathML Docker 部署指南
|
||||
|
||||
这个 Docker 化版本让您可以轻松在任何支持 Docker 的平台上运行 MathType 到 MathML 转换服务。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方法一:使用 Docker Compose(推荐)
|
||||
|
||||
```bash
|
||||
# 构建并启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 方法二:直接使用 Docker
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t mathtype-converter .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name mathtype-to-mathml \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
mathtype-converter
|
||||
|
||||
# 查看日志
|
||||
docker logs -f mathtype-to-mathml
|
||||
|
||||
# 停止容器
|
||||
docker stop mathtype-to-mathml
|
||||
docker rm mathtype-to-mathml
|
||||
```
|
||||
|
||||
## 📱 使用服务
|
||||
|
||||
服务启动后,可以通过以下方式使用:
|
||||
|
||||
### Web 界面
|
||||
打开浏览器访问:`http://localhost:3000/mathtype_converter_demo.html`
|
||||
|
||||
### API 接口
|
||||
|
||||
#### 健康检查
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
#### 转换 MathType 文件
|
||||
```bash
|
||||
curl -X POST \
|
||||
-F "file=@your_mathtype_file.bin" \
|
||||
http://localhost:3000/convert-mathtype
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
- `NODE_ENV`: 运行环境(默认:production)
|
||||
- `PORT`: 服务端口(默认:3000)
|
||||
|
||||
### 数据卷
|
||||
- `/app/uploads`: 上传文件存储目录
|
||||
- `/app/config`: 配置文件目录(可选)
|
||||
|
||||
### 端口映射
|
||||
- `3000`: Web 服务端口
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
/app/
|
||||
├── server_converter.js # Node.js 主服务器
|
||||
├── server/
|
||||
│ ├── converter.rb # Ruby 转换脚本
|
||||
│ └── setup.js # 设置脚本
|
||||
├── public/ # 静态文件
|
||||
├── uploads/ # 上传文件目录
|
||||
├── vendor/ # Ruby gems
|
||||
└── Gemfile # Ruby 依赖
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 查看容器日志
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose logs mathtype-converter
|
||||
|
||||
# 直接 Docker
|
||||
docker logs mathtype-to-mathml
|
||||
```
|
||||
|
||||
### 进入容器调试
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose exec mathtype-converter bash
|
||||
|
||||
# 直接 Docker
|
||||
docker exec -it mathtype-to-mathml bash
|
||||
```
|
||||
|
||||
### 重建镜像
|
||||
```bash
|
||||
# 清理并重建
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🚀 部署到生产环境
|
||||
|
||||
### 修改 docker-compose.yml
|
||||
```yaml
|
||||
services:
|
||||
mathtype-converter:
|
||||
# ... 其他配置
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
### 使用反向代理(Nginx 示例)
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 文件上传大小限制
|
||||
client_max_body_size 10M;
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 监控和维护
|
||||
|
||||
### 健康检查
|
||||
服务内置健康检查端点:`/health`
|
||||
|
||||
### 日志管理
|
||||
```bash
|
||||
# 设置日志轮转
|
||||
docker-compose up -d --log-opt max-size=10m --log-opt max-file=3
|
||||
```
|
||||
|
||||
### 备份上传文件
|
||||
```bash
|
||||
# 备份上传目录
|
||||
docker run --rm -v mathtype_uploads:/data -v $(pwd):/backup alpine tar czf /backup/uploads-backup.tar.gz -C /data .
|
||||
```
|
||||
|
||||
## 🔄 更新服务
|
||||
|
||||
```bash
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
|
||||
# 拉取最新代码(如果有)
|
||||
git pull
|
||||
|
||||
# 重建并启动
|
||||
docker-compose up -d --build
|
||||
```
|
||||
115
scripts/mathtype2mathml/README.md
Normal file
115
scripts/mathtype2mathml/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# MathType to MathML Converter
|
||||
|
||||
一个基于 Ruby gem 的 MathType 二进制文件转 MathML 的 Node.js 服务器。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 支持上传 MathType 二进制文件 (.bin)
|
||||
- 转换为标准 MathML 格式
|
||||
- RESTful API 接口
|
||||
- Web 界面演示
|
||||
- Docker 容器化部署
|
||||
- 网络优化和重试机制
|
||||
- 支持开发和生产环境配置
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:Docker 部署(推荐)
|
||||
|
||||
#### Windows 环境
|
||||
```powershell
|
||||
# 开发环境构建并启动
|
||||
.\docker-build.ps1
|
||||
|
||||
# 生产环境构建并启动
|
||||
.\docker-build.ps1 -Production
|
||||
|
||||
# 查看帮助
|
||||
.\docker-build.ps1 -Help
|
||||
```
|
||||
|
||||
#### Linux/macOS 环境
|
||||
```bash
|
||||
# 增强构建脚本(推荐)
|
||||
./docker-build-enhanced.sh
|
||||
|
||||
# 标准构建脚本
|
||||
./docker-build.sh
|
||||
|
||||
# 或使用 docker-compose
|
||||
docker-compose up -d
|
||||
|
||||
# 生产环境
|
||||
docker-compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
### 方式二:本地部署
|
||||
|
||||
1. 确保安装了 Ruby 和 Node.js
|
||||
2. 安装依赖:
|
||||
|
||||
```bash
|
||||
# 安装 Ruby 依赖
|
||||
bundle install
|
||||
|
||||
# 安装 Node.js 依赖
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 启动服务:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境配置
|
||||
|
||||
- **开发环境**: 使用 `docker-compose.yml`,包含代码热重载
|
||||
- **生产环境**: 使用 `docker-compose.production.yml`,优化性能和安全性
|
||||
|
||||
### 配置文件
|
||||
|
||||
- `config/app.json`: 应用基础配置
|
||||
- `uploads/`: 文件上传目录
|
||||
- `vendor/gems/`: 本地 Ruby gems
|
||||
|
||||
## API 使用
|
||||
|
||||
### 转换 MathType 文件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/convert-mathtype \
|
||||
-F "file=@path/to/mathtype.bin"
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
## Web 界面
|
||||
|
||||
启动服务后访问:http://localhost:3000/mathtype_converter_demo.html
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 网络问题
|
||||
如果遇到 Docker Hub 连接问题:
|
||||
1. 使用增强构建脚本:`./docker-build-enhanced.sh` 或 `.\docker-build.ps1`
|
||||
2. 脚本会自动检测网络状况并使用国内镜像源
|
||||
|
||||
### 常见问题
|
||||
- **端口冲突**: 修改 docker-compose.yml 中的端口映射
|
||||
- **构建失败**: 清理 Docker 缓存 `docker system prune`
|
||||
- **权限问题**: 确保 uploads 目录权限正确
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Node.js + Express
|
||||
- **Ruby Gem**: mathtype_to_mathml
|
||||
- **容器化**: Docker + Docker Compose
|
||||
- **前端**: 原生 HTML/JavaScript
|
||||
- **网络优化**: 国内镜像源支持
|
||||
217
scripts/mathtype2mathml/SETUP_GUIDE.md
Normal file
217
scripts/mathtype2mathml/SETUP_GUIDE.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# 🔧 Setup Guide: Converting Real MathType .bin Files
|
||||
|
||||
## Why You Need This Setup
|
||||
|
||||
The JavaScript client-side converter only shows a sample equation (x²) because **parsing MathType's proprietary binary format requires the Ruby gem**. To convert real `.bin` files, you need the server setup.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
### 1. Install Ruby and Gems
|
||||
```bash
|
||||
# On Windows (using RubyInstaller)
|
||||
# Download from: https://rubyinstaller.org/
|
||||
|
||||
# On macOS (using Homebrew)
|
||||
brew install ruby
|
||||
|
||||
# On Ubuntu/Debian
|
||||
sudo apt-get install ruby-full
|
||||
|
||||
# Install bundler and the required gems from the project root
|
||||
gem install bundler
|
||||
bundle install
|
||||
```
|
||||
|
||||
### 2. Install Node.js
|
||||
```bash
|
||||
# Download from: https://nodejs.org/
|
||||
# Or using package managers:
|
||||
|
||||
# macOS
|
||||
brew install node
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
# Windows
|
||||
# Download installer from nodejs.org
|
||||
```
|
||||
|
||||
## 🚀 Quick Setup
|
||||
|
||||
### Step 1: Install Server Dependencies
|
||||
```bash
|
||||
# In your project's server directory
|
||||
cd server
|
||||
npm install
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Step 2: Start the Server
|
||||
```bash
|
||||
# From the project root
|
||||
npm start
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
MathType converter server running at http://localhost:3000
|
||||
Make sure you have the mathtype_to_mathml Ruby gem installed.
|
||||
```
|
||||
|
||||
### Step 3: Open the Demo
|
||||
1. Open `http://localhost:3000/index.html`
|
||||
2. Upload a real MathType `.bin` file
|
||||
3. Click "Convert Real File (Server Required)"
|
||||
|
||||
## 📁 Finding MathType .bin Files
|
||||
|
||||
MathType equations in Word documents are typically stored as:
|
||||
- `oleObject1.bin`, `oleObject2.bin`, etc.
|
||||
- In the `word/embeddings/` folder when you unzip a `.docx` file
|
||||
|
||||
### Extract from Word Document:
|
||||
```bash
|
||||
# Rename .docx to .zip and extract
|
||||
cp document.docx document.zip
|
||||
unzip document.zip
|
||||
ls word/embeddings/ # Look for .bin files
|
||||
```
|
||||
|
||||
## 🧪 Testing the Setup
|
||||
|
||||
### Test 1: Server Health Check
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
# Should return: {"status":"ok","message":"MathType converter server is running"}
|
||||
```
|
||||
|
||||
### Test 2: Convert a File via API
|
||||
```bash
|
||||
# Make sure to be in the directory with oleObject1.bin
|
||||
curl -X POST -F "file=@oleObject1.bin" http://localhost:3000/convert-mathtype
|
||||
# Should return JSON with MathML
|
||||
```
|
||||
|
||||
### Test 3: Web Interface
|
||||
1. Open browser to `http://localhost:3000/index.html`
|
||||
2. Upload a `.bin` file
|
||||
3. Click "Convert Real File"
|
||||
4. Should see actual MathML output (not just x²)
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Error: "Ruby conversion failed"
|
||||
```bash
|
||||
# Check if Ruby gems are installed via bundle from the project root
|
||||
bundle check
|
||||
|
||||
# Reinstall if missing
|
||||
bundle install
|
||||
```
|
||||
|
||||
### Error: "Failed to spawn Ruby process"
|
||||
```bash
|
||||
# Check if Ruby is in PATH
|
||||
ruby --version
|
||||
|
||||
# The script now runs from the project root, so bundle should find the correct ruby executable.
|
||||
# If you still have issues, ensure 'bundle' command is in your system's PATH.
|
||||
```
|
||||
|
||||
### Error: "Cannot find module 'express'"
|
||||
```bash
|
||||
cd server
|
||||
npm install express multer cors
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Error: "File upload failed"
|
||||
- Check file size (limit is 10MB)
|
||||
- Ensure file has `.bin` extension
|
||||
- Verify file is actually a MathType binary
|
||||
|
||||
## 📊 Expected Behavior
|
||||
|
||||
### ✅ Success Case:
|
||||
1. Upload `oleObject1.bin` containing equation `E = mc²`
|
||||
2. Server processes file with Ruby gem
|
||||
3. Returns: `<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></math>`
|
||||
4. Browser displays rendered equation
|
||||
|
||||
### ❌ Fallback Case:
|
||||
1. Server unavailable or fails
|
||||
2. Client falls back to demo mode
|
||||
3. Always shows `x²` sample regardless of uploaded file
|
||||
4. Warning message: "Client-side demo completed (shows sample x² only)"
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
### For Development:
|
||||
```bash
|
||||
# Use nodemon for auto-restart (run from project root)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### For Production:
|
||||
```bash
|
||||
# Use PM2 or similar process manager (run from project root)
|
||||
npm install -g pm2
|
||||
pm2 start server/server.js --name mathtype-converter
|
||||
```
|
||||
|
||||
## 📈 Performance Notes
|
||||
|
||||
- **Ruby Process**: Each conversion spawns a Ruby process (~100-500ms overhead)
|
||||
- **File Size**: Typical `.bin` files are 1-50KB
|
||||
- **Concurrent Requests**: Server handles multiple files simultaneously
|
||||
- **Memory**: Each Ruby process uses ~10-50MB temporarily
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
For production deployment:
|
||||
- Add file type validation
|
||||
- Implement rate limiting
|
||||
- Add authentication if needed
|
||||
- Sanitize file names
|
||||
- Use temporary directories with cleanup
|
||||
|
||||
## 📦 Alternative Deployment
|
||||
|
||||
### Docker Option:
|
||||
```dockerfile
|
||||
FROM node:18
|
||||
RUN apt-get update && apt-get install -y ruby-full
|
||||
RUN gem install bundler
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy gem files and install gems
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
COPY vendor ./vendor
|
||||
RUN bundle install
|
||||
|
||||
# Copy server files and install node modules
|
||||
COPY server ./server
|
||||
WORKDIR /app/server
|
||||
RUN npm install
|
||||
|
||||
# Go back to root to run the server
|
||||
WORKDIR /app
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
### Cloud Function Option:
|
||||
Deploy to AWS Lambda, Google Cloud Functions, or Vercel with Ruby runtime support.
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you encounter issues:
|
||||
1. Check Ruby gem installation from root: `bundle check`
|
||||
2. Verify Node.js setup in server folder: `cd server && npm install && cd ..`
|
||||
3. Test with sample `.bin` files first
|
||||
4. Check server logs for detailed error messages
|
||||
|
||||
The key is having both Ruby (for binary parsing) and Node.js (for web interface) working together!
|
||||
211
scripts/mathtype2mathml/batch_converter.rb
Normal file
211
scripts/mathtype2mathml/batch_converter.rb
Normal file
@@ -0,0 +1,211 @@
|
||||
require_relative 'vendor/gems/mathtype_to_mathml/lib/mathtype_to_mathml'
|
||||
require 'json'
|
||||
require 'concurrent-ruby'
|
||||
require 'pathname'
|
||||
require 'parallel'
|
||||
|
||||
class BatchConverter
|
||||
def initialize(max_threads: 4)
|
||||
@max_threads = max_threads
|
||||
@thread_pool = Concurrent::ThreadPoolExecutor.new(
|
||||
min_threads: 1,
|
||||
max_threads: @max_threads,
|
||||
max_queue: 100,
|
||||
fallback_policy: :caller_runs
|
||||
)
|
||||
end
|
||||
|
||||
# 新增多进程处理方法
|
||||
def process_files_multi_process(file_paths)
|
||||
# 确保传入的是可枚举的哈希结构
|
||||
file_paths_hash = file_paths.is_a?(Hash) ? file_paths : {}
|
||||
# 根据平台自动选择并行模式:
|
||||
# Linux/macOS 支持 Process.fork,使用多进程实现真正的 CPU 并行
|
||||
# Windows 不支持 Process.fork,降级为多线程
|
||||
parallel_opts = Process.respond_to?(:fork) ? { in_processes: @max_threads } : { in_threads: @max_threads }
|
||||
result_array = Parallel.map(file_paths_hash.to_a, parallel_opts) do |key, value|
|
||||
begin
|
||||
result = process_single_file(value)
|
||||
[key, result]
|
||||
rescue => e
|
||||
[key, {
|
||||
success: false,
|
||||
error: e.message,
|
||||
mathml: nil
|
||||
}]
|
||||
end
|
||||
end
|
||||
# 将数组结果转换为哈希
|
||||
result_array.to_h
|
||||
end
|
||||
|
||||
def process_files(file_paths)
|
||||
results = {}
|
||||
futures = []
|
||||
|
||||
# 遍历json入参开始转换
|
||||
file_paths.each do |key, value|
|
||||
future = Concurrent::Future.execute(executor: @thread_pool) do
|
||||
process_single_file(value)
|
||||
end
|
||||
futures << [key, future]
|
||||
end
|
||||
|
||||
# Wait for all conversions to complete
|
||||
futures.each do |file_path, future|
|
||||
begin
|
||||
result = future.value(30) # 30 second timeout per file
|
||||
results[file_path] = result
|
||||
rescue Concurrent::TimeoutError
|
||||
results[file_path] = {
|
||||
success: false,
|
||||
error: 'Conversion timeout (30 seconds)',
|
||||
mathml: nil
|
||||
}
|
||||
rescue => e
|
||||
results[file_path] = {
|
||||
success: false,
|
||||
error: e.message,
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@thread_pool.shutdown
|
||||
@thread_pool.wait_for_termination(60)
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_single_file(file_path)
|
||||
begin
|
||||
# Validate file
|
||||
unless File.exist?(file_path)
|
||||
return {
|
||||
success: false,
|
||||
error: "File does not exist: #{file_path}",
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
|
||||
unless File.readable?(file_path)
|
||||
return {
|
||||
success: false,
|
||||
error: "File is not readable: #{file_path}",
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
|
||||
file_size = File.size(file_path)
|
||||
if file_size == 0
|
||||
return {
|
||||
success: false,
|
||||
error: "File is empty: #{file_path}",
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
|
||||
# Convert the file
|
||||
converter = MathTypeToMathML::Converter.new(file_path)
|
||||
mathml = converter.convert
|
||||
|
||||
# Validate output
|
||||
if mathml.nil? || mathml.strip.empty?
|
||||
return {
|
||||
success: false,
|
||||
error: "Conversion resulted in empty MathML",
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
|
||||
{
|
||||
success: true,
|
||||
error: nil,
|
||||
mathml: mathml.strip,
|
||||
file_size: file_size
|
||||
}
|
||||
|
||||
rescue NoMethodError => e
|
||||
{
|
||||
success: false,
|
||||
error: "Ruby parsing error - #{e.message}. This usually indicates an incompatible or corrupted MathType file",
|
||||
mathml: nil
|
||||
}
|
||||
rescue ArgumentError => e
|
||||
{
|
||||
success: false,
|
||||
error: "Invalid file format - #{e.message}",
|
||||
mathml: nil
|
||||
}
|
||||
rescue StandardError => e
|
||||
{
|
||||
success: false,
|
||||
error: "Unexpected error - #{e.class}: #{e.message}",
|
||||
mathml: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Command line interface
|
||||
if __FILE__ == $0
|
||||
begin
|
||||
if ARGV.empty?
|
||||
STDERR.puts "Usage: #{$0} file1.bin [file2.bin ...]"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Validate all files exist before processing
|
||||
# missing_files = ARGV.reject { |file| File.exist?(file) }
|
||||
# unless missing_files.empty?
|
||||
# STDERR.puts "Error: Files not found: #{missing_files.join(', ')}"
|
||||
# exit 1
|
||||
# end
|
||||
|
||||
# 入参可以是文件路径或JSON字符串
|
||||
arg = ARGV[0] || '{}'
|
||||
if File.exist?(arg) && File.file?(arg)
|
||||
json_data = File.read(arg, mode: 'rb:utf-8').gsub(/\A\xEF\xBB\xBF/, '')
|
||||
else
|
||||
json_data = arg.gsub(/\A'|'\z/, '')
|
||||
end
|
||||
begin
|
||||
data = JSON.parse(json_data)
|
||||
rescue JSON::ParserError => e
|
||||
STDERR.puts "ERROR: Invalid JSON input: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Process files
|
||||
# batch_converter = BatchConverter.new(max_threads: [ARGV.size, 8].min)
|
||||
# results = batch_converter.process_files(data)
|
||||
|
||||
batch_converter = BatchConverter.new(max_threads: [data.size, Parallel.processor_count].min)
|
||||
results = batch_converter.process_files_multi_process(data)
|
||||
|
||||
# Output results as JSON
|
||||
output = {
|
||||
total_files: data.size,
|
||||
successful: results.count { |_, result| result[:success] },
|
||||
failed: results.count { |_, result| !result[:success] },
|
||||
results: results
|
||||
}
|
||||
|
||||
puts "WP_RUBY_".concat(output.to_json)
|
||||
# puts JSON.pretty_generate(output)
|
||||
|
||||
# Exit with error code if any conversions failed
|
||||
exit_code = results.any? { |_, result| !result[:success] } ? 1 : 0
|
||||
# exit exit_code
|
||||
exit 0
|
||||
|
||||
rescue Interrupt
|
||||
STDERR.puts "\nBatch conversion interrupted by user"
|
||||
exit 130
|
||||
rescue => e
|
||||
STDERR.puts "Fatal error: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
1
scripts/mathtype2mathml/config/.gitkeep
Normal file
1
scripts/mathtype2mathml/config/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep this directory in git
|
||||
19
scripts/mathtype2mathml/config/app.json
Normal file
19
scripts/mathtype2mathml/config/app.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "MathType to MathML Converter",
|
||||
"version": "1.0.0",
|
||||
"description": "Server for converting MathType binary files to MathML",
|
||||
"port": 3000,
|
||||
"environment": "production",
|
||||
"limits": {
|
||||
"maxFileSize": "10MB",
|
||||
"maxFiles": 10
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"format": "combined"
|
||||
},
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"origin": "*"
|
||||
}
|
||||
}
|
||||
65
scripts/mathtype2mathml/converter.rb
Normal file
65
scripts/mathtype2mathml/converter.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
require 'mathtype_to_mathml'
|
||||
|
||||
def safe_convert(file_path)
|
||||
begin
|
||||
# Check if file exists and is readable
|
||||
unless File.exist?(file_path)
|
||||
STDERR.puts "Error: File does not exist: #{file_path}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless File.readable?(file_path)
|
||||
STDERR.puts "Error: File is not readable: #{file_path}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Check file size
|
||||
file_size = File.size(file_path)
|
||||
if file_size == 0
|
||||
STDERR.puts "Error: File is empty: #{file_path}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
STDERR.puts "Processing file: #{file_path} (#{file_size} bytes)"
|
||||
|
||||
# Try to convert the file
|
||||
converter = MathTypeToMathML::Converter.new(file_path)
|
||||
mathml = converter.convert
|
||||
|
||||
# Check if we got valid output
|
||||
if mathml.nil? || mathml.strip.empty?
|
||||
STDERR.puts "Error: Conversion resulted in empty MathML"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts mathml
|
||||
|
||||
rescue NoMethodError => e
|
||||
STDERR.puts "Error: Ruby parsing error - #{e.message}"
|
||||
STDERR.puts "This usually indicates an incompatible or corrupted MathType file"
|
||||
STDERR.puts "Backtrace: #{e.backtrace.first(3).join(', ')}"
|
||||
exit 1
|
||||
rescue ArgumentError => e
|
||||
STDERR.puts "Error: Invalid file format - #{e.message}"
|
||||
exit 1
|
||||
rescue StandardError => e
|
||||
STDERR.puts "Error: Unexpected error - #{e.class}: #{e.message}"
|
||||
STDERR.puts "Backtrace: #{e.backtrace.first(5).join(', ')}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
file_path = ARGV[0]
|
||||
|
||||
if file_path.nil? || file_path.empty?
|
||||
STDERR.puts "Error: No file path provided"
|
||||
exit 1
|
||||
end
|
||||
|
||||
safe_convert(file_path)
|
||||
|
||||
rescue => e
|
||||
STDERR.puts "Error: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
182
scripts/mathtype2mathml/docker-build-enhanced.sh
Normal file
182
scripts/mathtype2mathml/docker-build-enhanced.sh
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Enhanced Docker 构建脚本 - 网络优化版本
|
||||
set -e
|
||||
|
||||
echo "🐳 Enhanced MathType to MathML Docker 构建脚本"
|
||||
echo "================================================="
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 docker-compose 是否安装
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "⚠️ docker-compose 未安装,将使用 docker build"
|
||||
USE_COMPOSE=false
|
||||
else
|
||||
USE_COMPOSE=true
|
||||
fi
|
||||
|
||||
# 设置镜像名称
|
||||
IMAGE_NAME="mathtype-converter"
|
||||
CONTAINER_NAME="mathtype-to-mathml"
|
||||
|
||||
# 检查网络连接并选择最佳构建策略
|
||||
echo "🌐 检查网络连接..."
|
||||
|
||||
# 测试Docker Hub连接
|
||||
if curl -s --connect-timeout 5 https://registry.hub.docker.com/ > /dev/null; then
|
||||
echo "✅ Docker Hub 连接正常"
|
||||
NETWORK_MODE="standard"
|
||||
else
|
||||
echo "⚠️ Docker Hub 连接异常,使用网络优化模式"
|
||||
NETWORK_MODE="optimized"
|
||||
fi
|
||||
|
||||
# 根据网络状况选择构建参数
|
||||
if [ "$NETWORK_MODE" = "optimized" ]; then
|
||||
echo "📦 使用网络优化构建..."
|
||||
BUILD_ARGS="--build-arg USE_MIRROR=true --network=host"
|
||||
else
|
||||
echo "📦 使用标准构建..."
|
||||
BUILD_ARGS=""
|
||||
fi
|
||||
|
||||
echo "🔨 开始构建 Docker 镜像..."
|
||||
|
||||
# 构建函数
|
||||
build_image() {
|
||||
local attempt=$1
|
||||
echo "📝 构建尝试 $attempt/3..."
|
||||
|
||||
if [ "$USE_COMPOSE" = true ]; then
|
||||
if docker-compose build $BUILD_ARGS; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if docker build -t $IMAGE_NAME $BUILD_ARGS .; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 重试构建
|
||||
for attempt in 1 2 3; do
|
||||
if build_image $attempt; then
|
||||
echo "✅ Docker 镜像构建成功!"
|
||||
BUILD_SUCCESS=true
|
||||
break
|
||||
else
|
||||
echo "❌ 构建尝试 $attempt 失败"
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "⏳ 等待 10 秒后重试..."
|
||||
sleep 10
|
||||
fi
|
||||
BUILD_SUCCESS=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$BUILD_SUCCESS" != true ]; then
|
||||
echo "❌ 所有构建尝试都失败了"
|
||||
echo "💡 建议:"
|
||||
echo " 1. 检查网络连接"
|
||||
echo " 2. 确认 Docker 服务运行正常"
|
||||
echo " 3. 尝试手动清理 Docker 缓存: docker system prune"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证镜像构建
|
||||
echo "🔍 验证镜像..."
|
||||
if docker images | grep -q $IMAGE_NAME; then
|
||||
echo "✅ 镜像验证成功!"
|
||||
else
|
||||
echo "❌ 镜像验证失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 提供启动选项
|
||||
echo ""
|
||||
echo "🚀 启动选项:"
|
||||
echo "1. 使用 docker-compose(推荐):"
|
||||
echo " docker-compose up -d"
|
||||
echo ""
|
||||
echo "2. 直接使用 Docker:"
|
||||
echo " docker run -d --name $CONTAINER_NAME -p 3000:3000 -v \$(pwd)/uploads:/app/uploads $IMAGE_NAME"
|
||||
echo ""
|
||||
echo "📱 启动后访问:"
|
||||
echo " Web界面: http://localhost:3000/mathtype_converter_demo.html"
|
||||
echo " 健康检查: http://localhost:3000/health"
|
||||
echo ""
|
||||
echo "📋 管理命令:"
|
||||
echo " 查看日志: docker logs -f $CONTAINER_NAME"
|
||||
echo " 停止容器: docker stop $CONTAINER_NAME"
|
||||
echo " 删除容器: docker rm $CONTAINER_NAME"
|
||||
|
||||
# 询问是否立即启动
|
||||
read -p "是否立即启动容器? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🚀 启动容器..."
|
||||
if [ "$USE_COMPOSE" = true ]; then
|
||||
if docker-compose up -d; then
|
||||
echo "✅ 容器已启动!"
|
||||
echo "📊 查看状态: docker-compose ps"
|
||||
echo "📋 查看日志: docker-compose logs -f"
|
||||
else
|
||||
echo "❌ 容器启动失败"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# 停止并删除已存在的容器
|
||||
docker stop $CONTAINER_NAME 2>/dev/null || true
|
||||
docker rm $CONTAINER_NAME 2>/dev/null || true
|
||||
|
||||
# 启动新容器
|
||||
if docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/uploads:/app/uploads" \
|
||||
-v "$(pwd)/config:/app/config:ro" \
|
||||
$IMAGE_NAME; then
|
||||
|
||||
echo "✅ 容器已启动!"
|
||||
echo "📊 查看状态: docker ps"
|
||||
echo "📋 查看日志: docker logs -f $CONTAINER_NAME"
|
||||
else
|
||||
echo "❌ 容器启动失败"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
# 健康检查
|
||||
echo "🏥 执行健康检查..."
|
||||
for i in {1..6}; do
|
||||
if curl -s http://localhost:3000/health > /dev/null; then
|
||||
echo "✅ 服务启动成功!"
|
||||
echo "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html"
|
||||
break
|
||||
else
|
||||
if [ $i -eq 6 ]; then
|
||||
echo "⚠️ 健康检查超时,但服务可能仍在启动中"
|
||||
echo "💡 请手动检查: curl http://localhost:3000/health"
|
||||
else
|
||||
echo "⏳ 等待服务启动... ($i/6)"
|
||||
sleep 5
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 构建完成!"
|
||||
216
scripts/mathtype2mathml/docker-build.ps1
Normal file
216
scripts/mathtype2mathml/docker-build.ps1
Normal file
@@ -0,0 +1,216 @@
|
||||
# Enhanced Docker 构建脚本 - Windows PowerShell 版本
|
||||
param(
|
||||
[switch]$Production,
|
||||
[switch]$NoBuild,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "🐳 MathType to MathML Docker 构建脚本 - Windows版本" -ForegroundColor Cyan
|
||||
Write-Host "用法: .\docker-build.ps1 [参数]" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "参数:" -ForegroundColor Yellow
|
||||
Write-Host " -Production 使用生产环境配置 (docker-compose.production.yml)"
|
||||
Write-Host " -NoBuild 跳过构建,直接启动已有镜像"
|
||||
Write-Host " -Help 显示此帮助信息"
|
||||
Write-Host ""
|
||||
Write-Host "示例:" -ForegroundColor Yellow
|
||||
Write-Host " .\docker-build.ps1 # 开发环境构建和启动"
|
||||
Write-Host " .\docker-build.ps1 -Production # 生产环境构建和启动"
|
||||
Write-Host " .\docker-build.ps1 -NoBuild # 跳过构建,直接启动"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🐳 Enhanced MathType to MathML Docker 构建脚本" -ForegroundColor Cyan
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
if (!(Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "❌ Docker 未安装,请先安装 Docker Desktop" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查 docker-compose 是否安装
|
||||
if (!(Get-Command docker-compose -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "⚠️ docker-compose 未安装,将使用 docker build" -ForegroundColor Yellow
|
||||
$UseCompose = $false
|
||||
} else {
|
||||
$UseCompose = $true
|
||||
}
|
||||
|
||||
# 设置镜像名称和配置文件
|
||||
$ImageName = "mathtype-converter"
|
||||
$ContainerName = "mathtype-to-mathml"
|
||||
|
||||
if ($Production) {
|
||||
$ComposeFile = "docker-compose.production.yml"
|
||||
$ContainerName += "-prod"
|
||||
Write-Host "📦 使用生产环境配置" -ForegroundColor Green
|
||||
} else {
|
||||
$ComposeFile = "docker-compose.yml"
|
||||
Write-Host "📦 使用开发环境配置" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# 检查网络连接
|
||||
Write-Host "🌐 检查网络连接..." -ForegroundColor Yellow
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "https://registry.hub.docker.com/" -TimeoutSec 5 -UseBasicParsing
|
||||
Write-Host "✅ Docker Hub 连接正常" -ForegroundColor Green
|
||||
$NetworkMode = "standard"
|
||||
} catch {
|
||||
Write-Host "⚠️ Docker Hub 连接异常,使用网络优化模式" -ForegroundColor Yellow
|
||||
$NetworkMode = "optimized"
|
||||
}
|
||||
|
||||
if (!$NoBuild) {
|
||||
Write-Host "🔨 开始构建 Docker 镜像..." -ForegroundColor Yellow
|
||||
|
||||
# 构建函数
|
||||
function Build-DockerImage {
|
||||
param([int]$Attempt)
|
||||
|
||||
Write-Host "📝 构建尝试 $Attempt/3..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
if ($UseCompose) {
|
||||
if ($NetworkMode -eq "optimized") {
|
||||
docker-compose -f $ComposeFile build --build-arg USE_MIRROR=true
|
||||
} else {
|
||||
docker-compose -f $ComposeFile build
|
||||
}
|
||||
} else {
|
||||
if ($NetworkMode -eq "optimized") {
|
||||
docker build -t $ImageName --build-arg USE_MIRROR=true .
|
||||
} else {
|
||||
docker build -t $ImageName .
|
||||
}
|
||||
}
|
||||
return $true
|
||||
} catch {
|
||||
Write-Host "❌ 构建失败: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# 重试构建
|
||||
$BuildSuccess = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
if (Build-DockerImage -Attempt $attempt) {
|
||||
Write-Host "✅ Docker 镜像构建成功!" -ForegroundColor Green
|
||||
$BuildSuccess = $true
|
||||
break
|
||||
} else {
|
||||
if ($attempt -lt 3) {
|
||||
Write-Host "⏳ 等待 10 秒后重试..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$BuildSuccess) {
|
||||
Write-Host "❌ 所有构建尝试都失败了" -ForegroundColor Red
|
||||
Write-Host "💡 建议:" -ForegroundColor Yellow
|
||||
Write-Host " 1. 检查网络连接"
|
||||
Write-Host " 2. 确认 Docker Desktop 运行正常"
|
||||
Write-Host " 3. 尝试手动清理 Docker 缓存: docker system prune"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 验证镜像构建
|
||||
Write-Host "🔍 验证镜像..." -ForegroundColor Yellow
|
||||
$images = docker images --format "{{.Repository}}" | Where-Object { $_ -eq $ImageName }
|
||||
if ($images) {
|
||||
Write-Host "✅ 镜像验证成功!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ 镜像验证失败" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "⏭️ 跳过构建步骤" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 提供启动选项
|
||||
Write-Host ""
|
||||
Write-Host "🚀 启动选项:" -ForegroundColor Cyan
|
||||
if ($UseCompose) {
|
||||
Write-Host "1. 使用 docker-compose(推荐):" -ForegroundColor White
|
||||
Write-Host " docker-compose -f $ComposeFile up -d" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "1. 直接使用 Docker:" -ForegroundColor White
|
||||
Write-Host " docker run -d --name $ContainerName -p 3000:3000 -v `$(pwd)/uploads:/app/uploads -v `$(pwd)/config:/app/config:ro $ImageName" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "📱 启动后访问:" -ForegroundColor Cyan
|
||||
Write-Host " Web界面: http://localhost:3000/mathtype_converter_demo.html" -ForegroundColor White
|
||||
Write-Host " 健康检查: http://localhost:3000/health" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "📋 管理命令:" -ForegroundColor Cyan
|
||||
Write-Host " 查看日志: docker logs -f $ContainerName" -ForegroundColor Gray
|
||||
Write-Host " 停止容器: docker stop $ContainerName" -ForegroundColor Gray
|
||||
Write-Host " 删除容器: docker rm $ContainerName" -ForegroundColor Gray
|
||||
|
||||
# 询问是否立即启动
|
||||
$reply = Read-Host "`n是否立即启动容器? (y/N)"
|
||||
if ($reply -match "^[Yy]$") {
|
||||
Write-Host "🚀 启动容器..." -ForegroundColor Yellow
|
||||
|
||||
try {
|
||||
if ($UseCompose) {
|
||||
docker-compose -f $ComposeFile up -d
|
||||
Write-Host "✅ 容器已启动!" -ForegroundColor Green
|
||||
Write-Host "📊 查看状态: docker-compose -f $ComposeFile ps" -ForegroundColor Gray
|
||||
Write-Host "📋 查看日志: docker-compose -f $ComposeFile logs -f" -ForegroundColor Gray
|
||||
} else {
|
||||
# 停止并删除已存在的容器
|
||||
try { docker stop $ContainerName 2>$null } catch {}
|
||||
try { docker rm $ContainerName 2>$null } catch {}
|
||||
|
||||
# 启动新容器
|
||||
$currentDir = Get-Location
|
||||
docker run -d `
|
||||
--name $ContainerName `
|
||||
-p 3000:3000 `
|
||||
-v "${currentDir}/uploads:/app/uploads" `
|
||||
-v "${currentDir}/config:/app/config:ro" `
|
||||
$ImageName
|
||||
|
||||
Write-Host "✅ 容器已启动!" -ForegroundColor Green
|
||||
Write-Host "📊 查看状态: docker ps" -ForegroundColor Gray
|
||||
Write-Host "📋 查看日志: docker logs -f $ContainerName" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "⏳ 等待服务启动..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# 健康检查
|
||||
Write-Host "🏥 执行健康检查..." -ForegroundColor Yellow
|
||||
$healthCheckSuccess = $false
|
||||
for ($i = 1; $i -le 6; $i++) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:3000/health" -TimeoutSec 5 -UseBasicParsing
|
||||
Write-Host "✅ 服务启动成功!" -ForegroundColor Green
|
||||
Write-Host "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html" -ForegroundColor Cyan
|
||||
$healthCheckSuccess = $true
|
||||
break
|
||||
} catch {
|
||||
if ($i -eq 6) {
|
||||
Write-Host "⚠️ 健康检查超时,但服务可能仍在启动中" -ForegroundColor Yellow
|
||||
Write-Host "💡 请手动检查: Invoke-WebRequest http://localhost:3000/health" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "⏳ 等待服务启动... ($i/6)" -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Host "❌ 容器启动失败: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🎉 构建完成!" -ForegroundColor Green
|
||||
96
scripts/mathtype2mathml/docker-build.sh
Normal file
96
scripts/mathtype2mathml/docker-build.sh
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker 构建和测试脚本
|
||||
set -e
|
||||
|
||||
echo "🐳 MathType to MathML Docker 构建脚本"
|
||||
echo "=================================="
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 docker-compose 是否安装
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "⚠️ docker-compose 未安装,将使用 docker build"
|
||||
USE_COMPOSE=false
|
||||
else
|
||||
USE_COMPOSE=true
|
||||
fi
|
||||
|
||||
# 设置镜像名称
|
||||
IMAGE_NAME="mathtype-converter"
|
||||
CONTAINER_NAME="mathtype-to-mathml"
|
||||
|
||||
echo "📦 开始构建 Docker 镜像..."
|
||||
|
||||
if [ "$USE_COMPOSE" = true ]; then
|
||||
echo "使用 docker-compose 构建..."
|
||||
docker-compose build
|
||||
else
|
||||
echo "使用 docker build 构建..."
|
||||
docker build -t $IMAGE_NAME .
|
||||
fi
|
||||
|
||||
echo "✅ Docker 镜像构建完成!"
|
||||
|
||||
# 提供启动选项
|
||||
echo ""
|
||||
echo "🚀 启动选项:"
|
||||
echo "1. 使用 docker-compose(推荐):"
|
||||
echo " docker-compose up -d"
|
||||
echo ""
|
||||
echo "2. 直接使用 Docker:"
|
||||
echo " docker run -d --name $CONTAINER_NAME -p 3000:3000 -v \$(pwd)/uploads:/app/uploads $IMAGE_NAME"
|
||||
echo ""
|
||||
echo "📱 启动后访问:"
|
||||
echo " Web界面: http://localhost:3000/mathtype_converter_demo.html"
|
||||
echo " 健康检查: http://localhost:3000/health"
|
||||
echo ""
|
||||
echo "📋 管理命令:"
|
||||
echo " 查看日志: docker logs -f $CONTAINER_NAME"
|
||||
echo " 停止容器: docker stop $CONTAINER_NAME"
|
||||
echo " 删除容器: docker rm $CONTAINER_NAME"
|
||||
|
||||
# 询问是否立即启动
|
||||
read -p "是否立即启动容器? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🚀 启动容器..."
|
||||
if [ "$USE_COMPOSE" = true ]; then
|
||||
docker-compose up -d
|
||||
echo "✅ 容器已启动!"
|
||||
echo "📊 查看状态: docker-compose ps"
|
||||
echo "📋 查看日志: docker-compose logs -f"
|
||||
else
|
||||
# 停止并删除已存在的容器
|
||||
docker stop $CONTAINER_NAME 2>/dev/null || true
|
||||
docker rm $CONTAINER_NAME 2>/dev/null || true
|
||||
|
||||
# 启动新容器
|
||||
docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/uploads:/app/uploads" \
|
||||
$IMAGE_NAME
|
||||
|
||||
echo "✅ 容器已启动!"
|
||||
echo "📊 查看状态: docker ps"
|
||||
echo "📋 查看日志: docker logs -f $CONTAINER_NAME"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 5
|
||||
|
||||
# 健康检查
|
||||
echo "🏥 执行健康检查..."
|
||||
if curl -s http://localhost:3000/health > /dev/null; then
|
||||
echo "✅ 服务启动成功!"
|
||||
echo "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html"
|
||||
else
|
||||
echo "⚠️ 服务可能还在启动中,请稍等片刻后再次访问"
|
||||
fi
|
||||
fi
|
||||
29
scripts/mathtype2mathml/docker-compose.production.yml
Normal file
29
scripts/mathtype2mathml/docker-compose.production.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
mathtype-converter:
|
||||
build: .
|
||||
container_name: mathtype-to-mathml-prod
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
# 持久化上传文件目录
|
||||
- ./uploads:/app/uploads
|
||||
# 挂载配置文件
|
||||
- ./config:/app/config:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mathtype-prod.rule=Host(`mathtype.local`)"
|
||||
- "traefik.http.services.mathtype-prod.loadbalancer.server.port=3000"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: mathtype-network-prod
|
||||
37
scripts/mathtype2mathml/docker-compose.yml
Normal file
37
scripts/mathtype2mathml/docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
services:
|
||||
mathtype-converter:
|
||||
build: .
|
||||
container_name: mathtype-to-mathml
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
# 持久化上传文件目录
|
||||
- ./uploads:/app/uploads
|
||||
# 挂载配置文件
|
||||
- ./config:/app/config:ro
|
||||
# 开发模式:实时同步源代码
|
||||
- ./server_converter.js:/app/server_converter.js
|
||||
- ./server:/app/server
|
||||
- ./public:/app/public
|
||||
- ./vendor:/app/vendor
|
||||
- ./package.json:/app/package.json
|
||||
- ./package-lock.json:/app/package-lock.json
|
||||
- ./entrypoint.sh:/app/entrypoint.sh
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mathtype.rule=Host(`mathtype.local`)"
|
||||
- "traefik.http.services.mathtype.loadbalancer.server.port=3000"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: mathtype-network
|
||||
48
scripts/mathtype2mathml/entrypoint.sh
Normal file
48
scripts/mathtype2mathml/entrypoint.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting MathType to MathML Converter..."
|
||||
|
||||
# 检查必要的目录
|
||||
echo "📁 Checking directories..."
|
||||
mkdir -p uploads
|
||||
mkdir -p public
|
||||
mkdir -p vendor
|
||||
|
||||
# 确保上传目录权限正确
|
||||
chmod 755 uploads
|
||||
|
||||
# 检查 Ruby 和 gem 是否可用
|
||||
echo "💎 Checking Ruby environment..."
|
||||
ruby --version
|
||||
bundle --version
|
||||
|
||||
# 检查关键 gem 是否安装
|
||||
echo "🔍 Verifying gem installation..."
|
||||
if ! bundle exec ruby -e "require 'mathtype_to_mathml'; puts 'mathtype_to_mathml gem loaded successfully'"; then
|
||||
echo "❌ mathtype_to_mathml gem not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Node.js 环境
|
||||
echo "📦 Checking Node.js environment..."
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
# 检查关键文件是否存在
|
||||
echo "📄 Checking critical files..."
|
||||
for file in "server_converter.js" "server/converter.rb" "public/mathtype_converter_demo.html"; do
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "❌ Critical file missing: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Environment check completed successfully!"
|
||||
|
||||
# 创建健康检查端点(如果 server_converter.js 没有的话)
|
||||
echo "🏥 Setting up health check..."
|
||||
|
||||
# 启动应用
|
||||
echo "🎯 Starting application..."
|
||||
exec "$@"
|
||||
43
scripts/mathtype2mathml/install/JBY-README.md
Normal file
43
scripts/mathtype2mathml/install/JBY-README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 本地开发环境配置
|
||||
1. 安装ruby
|
||||
2. 安装依赖包
|
||||
```
|
||||
进入到jby-office-formula目录下
|
||||
bundle install
|
||||
```
|
||||
3. 测试
|
||||
```
|
||||
ruby batch_converter.rb '{"testkey":"C:\\embeddings\\oleObject1.bin"}'
|
||||
ruby batch_converter.rb '{"testkey":"/opt/embeddings/oleObject1.bin"}'
|
||||
ruby batch_converter.rb '{"testkey":"install/example.bin"}'
|
||||
```
|
||||
|
||||
# 线上环境配置
|
||||
1. ruby环境基础镜像(bjjby-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com/base/image:ruby-v1)核心命令
|
||||
安装包: install/ruby-2.6.0.tar.gz、install/gems/*
|
||||
```
|
||||
cd /opt
|
||||
&& tar -zxf ruby-2.6.0.tar.gz
|
||||
&& cd /opt/ruby-2.6.0
|
||||
&& ./configure --prefix=/usr/local/ruby
|
||||
&& make -j$(nproc)
|
||||
&& make install
|
||||
&& export PATH=/usr/local/ruby/bin:$PATH
|
||||
&& gem --version
|
||||
&& unzip /opt/gems.zip -d /opt/
|
||||
&& gem install --local /opt/gems/one/*
|
||||
&& gem install --local /opt/gems/two/*
|
||||
&& make clean
|
||||
&& rm -rf /opt/ruby/ruby-2.6.0
|
||||
&& rm -rf /opt/gems
|
||||
&& rm -rf /var/cache/*
|
||||
```
|
||||
2. 业务容器镜像Dockerfile
|
||||
```
|
||||
FROM bjjby-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com/base/image:ruby-v1
|
||||
LABEL maintainer="chen 15662138562@163.com"
|
||||
RUN mkdir /opt/servers
|
||||
COPY jby-office-formula /opt/servers/jby-office-formula
|
||||
ADD jby-office-file-server/target/jby-office-file-server-3.0.jar /opt/servers/office-file.jar
|
||||
CMD ["java","-jar", "/opt/servers/office-file.jar"]
|
||||
```
|
||||
BIN
scripts/mathtype2mathml/install/example.bin
Normal file
BIN
scripts/mathtype2mathml/install/example.bin
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/one/coderay-1.1.3.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/one/coderay-1.1.3.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/one/method_source-1.1.0.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/one/method_source-1.1.0.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/one/racc-1.8.1.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/one/racc-1.8.1.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/third/parallel-1.19.2.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/third/parallel-1.19.2.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/two/bindata-2.5.1.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/two/bindata-2.5.1.gem
Normal file
Binary file not shown.
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/two/mini_portile2-2.8.9.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/two/mini_portile2-2.8.9.gem
Normal file
Binary file not shown.
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/two/pry-0.15.2.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/two/pry-0.15.2.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/two/rake-13.3.0.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/two/rake-13.3.0.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/gems/two/ruby-ole-1.2.13.1.gem
Normal file
BIN
scripts/mathtype2mathml/install/gems/two/ruby-ole-1.2.13.1.gem
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/install/ruby-2.6.0.tar.gz
Normal file
BIN
scripts/mathtype2mathml/install/ruby-2.6.0.tar.gz
Normal file
Binary file not shown.
1397
scripts/mathtype2mathml/package-lock.json
generated
Normal file
1397
scripts/mathtype2mathml/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
scripts/mathtype2mathml/package.json
Normal file
32
scripts/mathtype2mathml/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "mathtype-to-mathml-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Server for converting MathType binary files to MathML using Ruby gem",
|
||||
"main": "server_converter.js",
|
||||
"scripts": {
|
||||
"setup": "node server/setup.js",
|
||||
"start": "node server_converter.js",
|
||||
"dev": "nodemon server_converter.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mathtype",
|
||||
"mathml",
|
||||
"equation",
|
||||
"converter",
|
||||
"ruby",
|
||||
"gem"
|
||||
],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
891
scripts/mathtype2mathml/public/mathtype_converter_demo.html
Normal file
891
scripts/mathtype2mathml/public/mathtype_converter_demo.html
Normal file
@@ -0,0 +1,891 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MathType to MathML Converter Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.upload-section:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.output-panel {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.output-panel h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.mathml-output {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mathml-preview {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status.info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.architecture-diagram {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.feature-card h4 {
|
||||
margin-top: 0;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.batch-upload-zone {
|
||||
border: 2px dashed #007bff;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
background-color: #f8f9fa;
|
||||
margin: 20px 0;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.batch-upload-zone:hover {
|
||||
border-color: #0056b3;
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
|
||||
.batch-upload-zone.dragover {
|
||||
border-color: #28a745;
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: white;
|
||||
padding: 8px 12px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-item .remove-file {
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin: 20px 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background-color: #007bff;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.batch-results {
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.result-item.success {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.result-item.error {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-mathml {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.output-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧮 MathType to MathML Converter</h1>
|
||||
<p class="subtitle">JavaScript implementation for converting MathType binary files to MathML format</p>
|
||||
|
||||
<div class="architecture-diagram">
|
||||
<strong>Conversion Pipeline:</strong><br>
|
||||
Binary MTEF → XML Structure → Preprocessing → MathML Transform → Final Output
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<h3>📁 Upload Single MathType File</h3>
|
||||
<p>Select a MathType binary file (.bin) to convert to MathML</p>
|
||||
<div class="status info" style="margin: 10px 0; font-size: 14px;">
|
||||
<strong>💡 For Real Conversion:</strong> Start the server with <code>npm start</code> to convert actual .bin files.<br>
|
||||
<strong>🎮 Demo Mode:</strong> Works without server but only shows sample equation (x²).
|
||||
</div>
|
||||
<input type="file" id="fileInput" accept=".bin" />
|
||||
<br>
|
||||
<button id="convertBtn" onclick="convertFile()">Convert Real File (Server Required)</button>
|
||||
<button id="demoBtn" onclick="runDemo()">Run Client Demo (x² Sample)</button>
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<h3>📁 Batch Upload MathType Files</h3>
|
||||
<p>Upload multiple MathType binary files (.bin) for batch conversion</p>
|
||||
<div class="status info" style="margin: 10px 0; font-size: 14px;">
|
||||
<strong>🚀 High Performance:</strong> Process multiple files in parallel using optimized batch processing.<br>
|
||||
<strong>📊 Progress Tracking:</strong> Real-time progress updates for each file in the batch.
|
||||
</div>
|
||||
|
||||
<div class="batch-upload-zone" id="batchUploadZone" onclick="document.getElementById('batchFileInput').click()">
|
||||
<div>
|
||||
<h4>🎯 Drop files here or click to select</h4>
|
||||
<p>Drag and drop multiple .bin files, or click to browse</p>
|
||||
<p style="font-size: 14px; color: #6c757d;">Supports up to 50 files per batch</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="batchFileInput" accept=".bin" multiple style="display: none;" />
|
||||
|
||||
<div class="file-list" id="fileList" style="display: none;">
|
||||
<h4>Selected Files:</h4>
|
||||
<div id="fileItems"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<button id="batchConvertBtn" onclick="convertBatchFiles()" style="display: none;">
|
||||
🚀 Convert Batch (Server Required)
|
||||
</button>
|
||||
<button id="clearBatchBtn" onclick="clearBatchFiles()" style="display: none; margin-left: 10px;">
|
||||
🗑️ Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<h4>Conversion Progress:</h4>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" id="progressBarFill"></div>
|
||||
</div>
|
||||
<p id="progressText">0 / 0 files processed</p>
|
||||
</div>
|
||||
|
||||
<div class="batch-results" id="batchResults">
|
||||
<h4>Conversion Results:</h4>
|
||||
<div id="resultItems"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<div class="output-section">
|
||||
<div class="output-panel">
|
||||
<h3>📄 Generated MathML</h3>
|
||||
<div id="mathmlOutput" class="mathml-output">
|
||||
<!-- Generated MathML will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="output-panel">
|
||||
<h3>🔍 MathML Preview</h3>
|
||||
<div id="mathmlPreview" class="mathml-preview">
|
||||
<!-- Rendered math will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="debugInfo" class="debug-info" style="display: none;">
|
||||
<!-- Debug information will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>🚀 Features</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h4>Multi-Stage Pipeline</h4>
|
||||
<p>Follows the same architecture as the Ruby mathtype_to_mathml gem with binary parsing, preprocessing, and transformation stages.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h4>Character Mapping</h4>
|
||||
<p>Comprehensive Unicode character support including Greek letters, mathematical operators, and special symbols.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h4>Mathematical Structures</h4>
|
||||
<p>Supports fractions, subscripts, superscripts, roots, matrices, integrals, summations, and more.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h4>Web Compatible</h4>
|
||||
<p>Pure JavaScript implementation that works in browsers without external dependencies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>⚠️ Important Notes</h2>
|
||||
<div class="status info">
|
||||
<strong>Binary Parsing Limitation:</strong> This demo uses a simplified MTEF parser. For production use, you would need:
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>A complete MTEF binary format parser</li>
|
||||
<li>WebAssembly port of the Ruby mathtype gem</li>
|
||||
<li>Server-side conversion with the Ruby gem</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the converter script -->
|
||||
<script src="mathtype_to_mathml_converter.js"></script>
|
||||
|
||||
<script>
|
||||
// Global converter instance
|
||||
let converter;
|
||||
|
||||
// Initialize converter when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
try {
|
||||
converter = new MathTypeToMathMLConverter();
|
||||
showStatus('Converter initialized successfully', 'success');
|
||||
} catch (error) {
|
||||
showStatus('Failed to initialize converter: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// File conversion function - uses server for real binary parsing
|
||||
async function convertFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
showStatus('Please select a file first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('Converting file using server...', 'info');
|
||||
setButtonsEnabled(false);
|
||||
|
||||
// Show debug info about the binary data
|
||||
showDebugInfo(`Uploading file: ${file.name} (${file.size} bytes)`);
|
||||
|
||||
// Send file to server for conversion
|
||||
const mathml = await convertFileOnServer(file);
|
||||
|
||||
// Display results
|
||||
displayResults(mathml);
|
||||
showStatus('Server conversion completed successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
showStatus('Server conversion failed: ' + error.message, 'error');
|
||||
console.error('Conversion error:', error);
|
||||
|
||||
// Fallback to client-side demo
|
||||
showStatus('Falling back to client-side demo...', 'info');
|
||||
try {
|
||||
const binaryData = await loadBinaryFile(fileInput);
|
||||
const mathml = await converter.convert(binaryData);
|
||||
displayResults(mathml);
|
||||
showStatus('Client-side demo completed (shows sample x² only)', 'success');
|
||||
} catch (fallbackError) {
|
||||
showStatus('Both server and client conversion failed', 'error');
|
||||
}
|
||||
} finally {
|
||||
setButtonsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side conversion function
|
||||
async function convertFileOnServer(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/convert-mathtype', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Conversion failed');
|
||||
}
|
||||
|
||||
return result.mathml;
|
||||
}
|
||||
|
||||
// Demo function with sample data
|
||||
async function runDemo() {
|
||||
if (!converter) {
|
||||
showStatus('Converter not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('Running demo with sample equation (x²)...', 'info');
|
||||
setButtonsEnabled(false);
|
||||
|
||||
// Create sample binary data (this would normally be read from a .bin file)
|
||||
const sampleData = new Uint8Array([0x01, 0x02, 0x03, 0x04]); // Placeholder binary data
|
||||
|
||||
showDebugInfo('Demo using simulated binary data for equation: x²');
|
||||
|
||||
// Convert using the sample data
|
||||
const mathml = await converter.convert(sampleData);
|
||||
|
||||
// Display results
|
||||
displayResults(mathml);
|
||||
showStatus('Demo completed successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
showStatus('Demo failed: ' + error.message, 'error');
|
||||
console.error('Demo error:', error);
|
||||
} finally {
|
||||
setButtonsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Display conversion results
|
||||
function displayResults(mathml) {
|
||||
// Show raw MathML
|
||||
document.getElementById('mathmlOutput').textContent = mathml;
|
||||
|
||||
// Try to render MathML preview
|
||||
const previewDiv = document.getElementById('mathmlPreview');
|
||||
try {
|
||||
previewDiv.innerHTML = mathml;
|
||||
|
||||
// If MathJax is available, re-render
|
||||
if (window.MathJax && window.MathJax.typesetPromise) {
|
||||
MathJax.typesetPromise([previewDiv]).catch((err) => {
|
||||
console.warn('MathJax rendering failed:', err);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
previewDiv.innerHTML = '<em>Preview not available (MathML not supported in this browser)</em>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show status message
|
||||
function showStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
||||
|
||||
// Auto-hide success/info messages after 5 seconds
|
||||
if (type === 'success' || type === 'info') {
|
||||
setTimeout(() => {
|
||||
statusDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Show debug information
|
||||
function showDebugInfo(info) {
|
||||
const debugDiv = document.getElementById('debugInfo');
|
||||
debugDiv.style.display = 'block';
|
||||
debugDiv.textContent = info;
|
||||
}
|
||||
|
||||
// Enable/disable buttons
|
||||
function setButtonsEnabled(enabled) {
|
||||
document.getElementById('convertBtn').disabled = !enabled;
|
||||
document.getElementById('demoBtn').disabled = !enabled;
|
||||
}
|
||||
|
||||
// Batch processing variables
|
||||
let batchFiles = [];
|
||||
|
||||
// Initialize batch upload functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const batchUploadZone = document.getElementById('batchUploadZone');
|
||||
const batchFileInput = document.getElementById('batchFileInput');
|
||||
|
||||
// File input change handler
|
||||
batchFileInput.addEventListener('change', function(e) {
|
||||
handleBatchFiles(Array.from(e.target.files));
|
||||
});
|
||||
|
||||
// Drag and drop handlers for batch upload
|
||||
batchUploadZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
batchUploadZone.classList.add('dragover');
|
||||
});
|
||||
|
||||
batchUploadZone.addEventListener('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
batchUploadZone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
batchUploadZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
batchUploadZone.classList.remove('dragover');
|
||||
|
||||
const files = Array.from(e.dataTransfer.files).filter(file =>
|
||||
file.name.toLowerCase().endsWith('.bin')
|
||||
);
|
||||
|
||||
if (files.length > 0) {
|
||||
handleBatchFiles(files);
|
||||
} else {
|
||||
showStatus('No valid .bin files found in the dropped files', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle batch file selection
|
||||
function handleBatchFiles(files) {
|
||||
// Filter for .bin files and check for duplicates
|
||||
const validFiles = files.filter(file => {
|
||||
if (!file.name.toLowerCase().endsWith('.bin')) {
|
||||
return false;
|
||||
}
|
||||
// Check for duplicates
|
||||
return !batchFiles.some(existing =>
|
||||
existing.name === file.name && existing.size === file.size
|
||||
);
|
||||
});
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showStatus('No new valid .bin files to add', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check total file limit
|
||||
if (batchFiles.length + validFiles.length > 50) {
|
||||
showStatus('Maximum 50 files allowed per batch', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add files to batch
|
||||
batchFiles.push(...validFiles);
|
||||
updateFileList();
|
||||
showStatus(`Added ${validFiles.length} file(s) to batch (${batchFiles.length} total)`, 'success');
|
||||
}
|
||||
|
||||
// Update file list display
|
||||
function updateFileList() {
|
||||
const fileList = document.getElementById('fileList');
|
||||
const fileItems = document.getElementById('fileItems');
|
||||
const batchConvertBtn = document.getElementById('batchConvertBtn');
|
||||
const clearBatchBtn = document.getElementById('clearBatchBtn');
|
||||
|
||||
if (batchFiles.length === 0) {
|
||||
fileList.style.display = 'none';
|
||||
batchConvertBtn.style.display = 'none';
|
||||
clearBatchBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
fileList.style.display = 'block';
|
||||
batchConvertBtn.style.display = 'inline-block';
|
||||
clearBatchBtn.style.display = 'inline-block';
|
||||
|
||||
fileItems.innerHTML = batchFiles.map((file, index) => `
|
||||
<div class="file-item">
|
||||
<span>${file.name} (${formatFileSize(file.size)})</span>
|
||||
<span class="remove-file" onclick="removeFile(${index})">✕</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Remove file from batch
|
||||
function removeFile(index) {
|
||||
batchFiles.splice(index, 1);
|
||||
updateFileList();
|
||||
showStatus('File removed from batch', 'info');
|
||||
}
|
||||
|
||||
// Clear all batch files
|
||||
function clearBatchFiles() {
|
||||
batchFiles = [];
|
||||
updateFileList();
|
||||
document.getElementById('progressContainer').style.display = 'none';
|
||||
document.getElementById('batchResults').style.display = 'none';
|
||||
showStatus('Batch cleared', 'info');
|
||||
}
|
||||
|
||||
// Convert batch files
|
||||
async function convertBatchFiles() {
|
||||
if (batchFiles.length === 0) {
|
||||
showStatus('No files selected for batch conversion', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus(`Starting batch conversion of ${batchFiles.length} files...`, 'info');
|
||||
|
||||
// Show progress container
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressBarFill = document.getElementById('progressBarFill');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const batchResults = document.getElementById('batchResults');
|
||||
const resultItems = document.getElementById('resultItems');
|
||||
|
||||
progressContainer.style.display = 'block';
|
||||
batchResults.style.display = 'block';
|
||||
resultItems.innerHTML = '';
|
||||
|
||||
// Update progress
|
||||
progressBarFill.style.width = '0%';
|
||||
progressText.textContent = `0 / ${batchFiles.length} files processed`;
|
||||
|
||||
// Disable buttons
|
||||
document.getElementById('batchConvertBtn').disabled = true;
|
||||
document.getElementById('clearBatchBtn').disabled = true;
|
||||
|
||||
// Send files to server for batch conversion
|
||||
const results = await convertBatchOnServer(batchFiles);
|
||||
|
||||
// Update progress to 100%
|
||||
progressBarFill.style.width = '100%';
|
||||
progressText.textContent = `${batchFiles.length} / ${batchFiles.length} files processed`;
|
||||
|
||||
// Display results
|
||||
displayBatchResults(results);
|
||||
|
||||
const successCount = results.results.filter(r => r.success).length;
|
||||
const failCount = results.results.length - successCount;
|
||||
|
||||
showStatus(
|
||||
`Batch conversion completed! ${successCount} successful, ${failCount} failed`,
|
||||
failCount === 0 ? 'success' : 'info'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
showStatus('Batch conversion failed: ' + error.message, 'error');
|
||||
console.error('Batch conversion error:', error);
|
||||
} finally {
|
||||
// Re-enable buttons
|
||||
document.getElementById('batchConvertBtn').disabled = false;
|
||||
document.getElementById('clearBatchBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side batch conversion function
|
||||
async function convertBatchOnServer(files) {
|
||||
const formData = new FormData();
|
||||
|
||||
// Append all files
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
const response = await fetch('/convert-mathtype-batch', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Batch conversion failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Display batch conversion results
|
||||
function displayBatchResults(results) {
|
||||
const resultItems = document.getElementById('resultItems');
|
||||
|
||||
resultItems.innerHTML = results.results.map((result, index) => {
|
||||
const statusClass = result.success ? 'success' : 'error';
|
||||
const statusIcon = result.success ? '✅' : '❌';
|
||||
|
||||
return `
|
||||
<div class="result-item ${statusClass}">
|
||||
<div class="result-header">
|
||||
<span>${statusIcon} ${result.originalName}</span>
|
||||
<span>${result.success ? 'Success' : 'Failed'}</span>
|
||||
</div>
|
||||
${result.error ? `<div style="color: #dc3545; margin-bottom: 10px;">Error: ${result.error}</div>` : ''}
|
||||
${result.success && result.mathml ? `
|
||||
<div class="result-mathml">${result.mathml}</div>
|
||||
<button onclick="copyToClipboard('${escapeHtml(result.mathml)}')" style="margin-top: 10px;">
|
||||
📋 Copy MathML
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text.replace(/'/g, "'")).then(() => {
|
||||
showStatus('MathML copied to clipboard', 'success');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
showStatus('Failed to copy to clipboard', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// File drop functionality
|
||||
const uploadSection = document.querySelector('.upload-section');
|
||||
|
||||
uploadSection.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadSection.style.borderColor = '#007bff';
|
||||
uploadSection.style.backgroundColor = '#f0f8ff';
|
||||
});
|
||||
|
||||
uploadSection.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
uploadSection.style.borderColor = '#ddd';
|
||||
uploadSection.style.backgroundColor = '#fafafa';
|
||||
});
|
||||
|
||||
uploadSection.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadSection.style.borderColor = '#ddd';
|
||||
uploadSection.style.backgroundColor = '#fafafa';
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
document.getElementById('fileInput').files = files;
|
||||
showStatus('File dropped successfully: ' + files[0].name, 'success');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Optional: Include MathJax for better MathML rendering -->
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js"></script>
|
||||
<script>
|
||||
window.MathJax = {
|
||||
startup: {
|
||||
ready: () => {
|
||||
console.log('MathJax is loaded and ready');
|
||||
MathJax.startup.defaultReady();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
766
scripts/mathtype2mathml/public/mathtype_to_mathml_converter.js
Normal file
766
scripts/mathtype2mathml/public/mathtype_to_mathml_converter.js
Normal file
@@ -0,0 +1,766 @@
|
||||
/**
|
||||
* MathType to MathML Converter (JavaScript Implementation)
|
||||
*
|
||||
* This JavaScript implementation follows the same architectural principles
|
||||
* as the Ruby mathtype_to_mathml gem, providing a multi-stage conversion
|
||||
* pipeline from MathType binary format to MathML.
|
||||
*
|
||||
* Architecture:
|
||||
* Binary MTEF → XML Structure → Preprocessing → XSLT-like Transform → MathML
|
||||
*/
|
||||
|
||||
class MathTypeToMathMLConverter {
|
||||
constructor() {
|
||||
this.characterReplacer = new CharacterReplacer();
|
||||
this.mover = new ElementMover();
|
||||
this.transformer = new MathMLTransformer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MathType binary data to MathML
|
||||
* @param {ArrayBuffer|Uint8Array} binaryData - MathType binary data
|
||||
* @returns {Promise<string>} MathML string
|
||||
*/
|
||||
async convert(binaryData) {
|
||||
try {
|
||||
// Stage 1: Parse binary MTEF to XML structure
|
||||
const xmlStructure = await this.parseBinaryMTEF(binaryData);
|
||||
|
||||
// Stage 2: Preprocessing
|
||||
this.mover.restructureElements(xmlStructure);
|
||||
this.characterReplacer.replaceCharacters(xmlStructure);
|
||||
|
||||
// Stage 3: Transform to MathML
|
||||
const mathml = this.transformer.transformToMathML(xmlStructure);
|
||||
|
||||
// Stage 4: Cleanup and namespace handling
|
||||
return this.finalizeMathML(mathml);
|
||||
} catch (error) {
|
||||
throw new Error(`MathType conversion failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse binary MTEF data to XML-like structure
|
||||
* Note: This is a simplified implementation. In practice, you would need
|
||||
* a full MTEF parser or use a WebAssembly port of the mathtype gem.
|
||||
*/
|
||||
async parseBinaryMTEF(binaryData) {
|
||||
// For demo purposes, we'll simulate the XML structure
|
||||
// In a real implementation, this would parse the actual MTEF binary format
|
||||
|
||||
if (binaryData instanceof ArrayBuffer) {
|
||||
binaryData = new Uint8Array(binaryData);
|
||||
}
|
||||
|
||||
// Placeholder: In reality, this would parse the binary MTEF format
|
||||
// You could use a WebAssembly port of the Ruby mathtype gem
|
||||
// or implement a native JavaScript MTEF parser
|
||||
|
||||
return this.createSampleXMLStructure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sample XML structure for demonstration
|
||||
* This would be replaced by actual MTEF parsing
|
||||
*/
|
||||
createSampleXMLStructure() {
|
||||
return {
|
||||
type: 'mtef',
|
||||
equation_options: 'inline',
|
||||
children: [
|
||||
{
|
||||
type: 'slot',
|
||||
children: [
|
||||
{
|
||||
type: 'char',
|
||||
mt_code_value: '0x0078', // 'x'
|
||||
typeface: '1'
|
||||
},
|
||||
{
|
||||
type: 'tmpl',
|
||||
selector: 'tmSUP',
|
||||
children: [
|
||||
{
|
||||
type: 'slot',
|
||||
children: [
|
||||
{
|
||||
type: 'char',
|
||||
mt_code_value: '0x0032', // '2'
|
||||
typeface: '2'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize MathML output with proper namespace
|
||||
*/
|
||||
finalizeMathML(mathmlContent) {
|
||||
return `<math xmlns="http://www.w3.org/1998/Math/MathML">${mathmlContent}</math>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Character Replacement Module
|
||||
* Handles Unicode character conversion and mathematical symbol mapping
|
||||
*/
|
||||
class CharacterReplacer {
|
||||
constructor() {
|
||||
this.replacements = this.initializeReplacements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize character replacement mappings
|
||||
* Based on the Ruby CharReplacer class
|
||||
*/
|
||||
initializeReplacements() {
|
||||
return {
|
||||
// Basic Latin digits
|
||||
'0x0030': { mathmode: '<mn>0</mn>', textmode: '0' },
|
||||
'0x0031': { mathmode: '<mn>1</mn>', textmode: '1' },
|
||||
'0x0032': { mathmode: '<mn>2</mn>', textmode: '2' },
|
||||
'0x0033': { mathmode: '<mn>3</mn>', textmode: '3' },
|
||||
'0x0034': { mathmode: '<mn>4</mn>', textmode: '4' },
|
||||
'0x0035': { mathmode: '<mn>5</mn>', textmode: '5' },
|
||||
'0x0036': { mathmode: '<mn>6</mn>', textmode: '6' },
|
||||
'0x0037': { mathmode: '<mn>7</mn>', textmode: '7' },
|
||||
'0x0038': { mathmode: '<mn>8</mn>', textmode: '8' },
|
||||
'0x0039': { mathmode: '<mn>9</mn>', textmode: '9' },
|
||||
|
||||
// Basic Latin letters (uppercase)
|
||||
'0x0041': { mathmode: '<mi>A</mi>', textmode: 'A' },
|
||||
'0x0042': { mathmode: '<mi>B</mi>', textmode: 'B' },
|
||||
'0x0043': { mathmode: '<mi>C</mi>', textmode: 'C' },
|
||||
'0x0044': { mathmode: '<mi>D</mi>', textmode: 'D' },
|
||||
'0x0045': { mathmode: '<mi>E</mi>', textmode: 'E' },
|
||||
'0x0046': { mathmode: '<mi>F</mi>', textmode: 'F' },
|
||||
|
||||
// Basic Latin letters (lowercase)
|
||||
'0x0061': { mathmode: '<mi>a</mi>', textmode: 'a' },
|
||||
'0x0062': { mathmode: '<mi>b</mi>', textmode: 'b' },
|
||||
'0x0063': { mathmode: '<mi>c</mi>', textmode: 'c' },
|
||||
'0x0064': { mathmode: '<mi>d</mi>', textmode: 'd' },
|
||||
'0x0065': { mathmode: '<mi>e</mi>', textmode: 'e' },
|
||||
'0x0066': { mathmode: '<mi>f</mi>', textmode: 'f' },
|
||||
'0x0067': { mathmode: '<mi>g</mi>', textmode: 'g' },
|
||||
'0x0068': { mathmode: '<mi>h</mi>', textmode: 'h' },
|
||||
'0x0069': { mathmode: '<mi>i</mi>', textmode: 'i' },
|
||||
'0x006A': { mathmode: '<mi>j</mi>', textmode: 'j' },
|
||||
'0x006B': { mathmode: '<mi>k</mi>', textmode: 'k' },
|
||||
'0x006C': { mathmode: '<mi>l</mi>', textmode: 'l' },
|
||||
'0x006D': { mathmode: '<mi>m</mi>', textmode: 'm' },
|
||||
'0x006E': { mathmode: '<mi>n</mi>', textmode: 'n' },
|
||||
'0x006F': { mathmode: '<mi>o</mi>', textmode: 'o' },
|
||||
'0x0070': { mathmode: '<mi>p</mi>', textmode: 'p' },
|
||||
'0x0071': { mathmode: '<mi>q</mi>', textmode: 'q' },
|
||||
'0x0072': { mathmode: '<mi>r</mi>', textmode: 'r' },
|
||||
'0x0073': { mathmode: '<mi>s</mi>', textmode: 's' },
|
||||
'0x0074': { mathmode: '<mi>t</mi>', textmode: 't' },
|
||||
'0x0075': { mathmode: '<mi>u</mi>', textmode: 'u' },
|
||||
'0x0076': { mathmode: '<mi>v</mi>', textmode: 'v' },
|
||||
'0x0077': { mathmode: '<mi>w</mi>', textmode: 'w' },
|
||||
'0x0078': { mathmode: '<mi>x</mi>', textmode: 'x' },
|
||||
'0x0079': { mathmode: '<mi>y</mi>', textmode: 'y' },
|
||||
'0x007A': { mathmode: '<mi>z</mi>', textmode: 'z' },
|
||||
|
||||
// Mathematical operators
|
||||
'0x002B': { mathmode: '<mo>+</mo>', textmode: '+' },
|
||||
'0x002D': { mathmode: '<mo>-</mo>', textmode: '-' },
|
||||
'0x002A': { mathmode: '<mo>*</mo>', textmode: '*' },
|
||||
'0x002F': { mathmode: '<mo>/</mo>', textmode: '/' },
|
||||
'0x003D': { mathmode: '<mo>=</mo>', textmode: '=' },
|
||||
'0x003C': { mathmode: '<mo><</mo>', textmode: '<' },
|
||||
'0x003E': { mathmode: '<mo>></mo>', textmode: '>' },
|
||||
|
||||
// Parentheses and brackets
|
||||
'0x0028': { mathmode: '<mo stretchy="false">(</mo>', textmode: '(' },
|
||||
'0x0029': { mathmode: '<mo stretchy="false">)</mo>', textmode: ')' },
|
||||
'0x005B': { mathmode: '<mo stretchy="false">[</mo>', textmode: '[' },
|
||||
'0x005D': { mathmode: '<mo stretchy="false">]</mo>', textmode: ']' },
|
||||
'0x007B': { mathmode: '<mo>{</mo>', textmode: '{' },
|
||||
'0x007D': { mathmode: '<mo>}</mo>', textmode: '}' },
|
||||
|
||||
// Greek letters (common mathematical symbols)
|
||||
'0x03B1': { mathmode: '<mi>α</mi>', textmode: 'α' }, // alpha
|
||||
'0x03B2': { mathmode: '<mi>β</mi>', textmode: 'β' }, // beta
|
||||
'0x03B3': { mathmode: '<mi>γ</mi>', textmode: 'γ' }, // gamma
|
||||
'0x03B4': { mathmode: '<mi>δ</mi>', textmode: 'δ' }, // delta
|
||||
'0x03B5': { mathmode: '<mi>ε</mi>', textmode: 'ε' }, // epsilon
|
||||
'0x03B8': { mathmode: '<mi>θ</mi>', textmode: 'θ' }, // theta
|
||||
'0x03BB': { mathmode: '<mi>λ</mi>', textmode: 'λ' }, // lambda
|
||||
'0x03BC': { mathmode: '<mi>μ</mi>', textmode: 'μ' }, // mu
|
||||
'0x03C0': { mathmode: '<mi>π</mi>', textmode: 'π' }, // pi
|
||||
'0x03C3': { mathmode: '<mi>σ</mi>', textmode: 'σ' }, // sigma
|
||||
'0x03C4': { mathmode: '<mi>τ</mi>', textmode: 'τ' }, // tau
|
||||
'0x03C6': { mathmode: '<mi>φ</mi>', textmode: 'φ' }, // phi
|
||||
'0x03C7': { mathmode: '<mi>χ</mi>', textmode: 'χ' }, // chi
|
||||
'0x03C8': { mathmode: '<mi>ψ</mi>', textmode: 'ψ' }, // psi
|
||||
'0x03C9': { mathmode: '<mi>ω</mi>', textmode: 'ω' }, // omega
|
||||
|
||||
// Mathematical symbols
|
||||
'0x221E': { mathmode: '<mo>∞</mo>', textmode: '∞' }, // infinity
|
||||
'0x2211': { mathmode: '<mo>∑</mo>', textmode: '∑' }, // summation
|
||||
'0x222B': { mathmode: '<mo>∫</mo>', textmode: '∫' }, // integral
|
||||
'0x2206': { mathmode: '<mo>∆</mo>', textmode: '∆' }, // delta
|
||||
'0x2207': { mathmode: '<mo>∇</mo>', textmode: '∇' }, // nabla
|
||||
'0x2208': { mathmode: '<mo>∈</mo>', textmode: '∈' }, // element of
|
||||
'0x2209': { mathmode: '<mo>∉</mo>', textmode: '∉' }, // not element of
|
||||
'0x220B': { mathmode: '<mo>∋</mo>', textmode: '∋' }, // contains
|
||||
'0x2260': { mathmode: '<mo>≠</mo>', textmode: '≠' }, // not equal
|
||||
'0x2264': { mathmode: '<mo>≤</mo>', textmode: '≤' }, // less than or equal
|
||||
'0x2265': { mathmode: '<mo>≥</mo>', textmode: '≥' }, // greater than or equal
|
||||
'0x00B1': { mathmode: '<mo>±</mo>', textmode: '±' }, // plus-minus
|
||||
'0x00D7': { mathmode: '<mo>×</mo>', textmode: '×' }, // multiplication
|
||||
'0x00F7': { mathmode: '<mo>÷</mo>', textmode: '÷' }, // division
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace characters in the XML structure
|
||||
*/
|
||||
replaceCharacters(xmlStructure) {
|
||||
this.traverseAndReplace(xmlStructure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse and replace characters
|
||||
*/
|
||||
traverseAndReplace(node) {
|
||||
if (node.type === 'char') {
|
||||
const replacement = this.getCharacterReplacement(node);
|
||||
if (replacement) {
|
||||
node.mathml = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(child => this.traverseAndReplace(child));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character replacement based on mt_code_value
|
||||
*/
|
||||
getCharacterReplacement(charNode) {
|
||||
const codeValue = charNode.mt_code_value;
|
||||
const isTextMode = charNode.variation === 'textmode';
|
||||
const replacement = this.replacements[codeValue];
|
||||
|
||||
if (!replacement) {
|
||||
// Handle character ranges
|
||||
return this.handleCharacterRange(codeValue, isTextMode);
|
||||
}
|
||||
|
||||
return isTextMode ? replacement.textmode : replacement.mathmode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle character ranges for undefined individual characters
|
||||
*/
|
||||
handleCharacterRange(codeValue, isTextMode) {
|
||||
const codeNum = parseInt(codeValue, 16);
|
||||
|
||||
// Numbers (0-9)
|
||||
if (codeNum >= 0x0030 && codeNum <= 0x0039) {
|
||||
const char = String.fromCharCode(codeNum);
|
||||
return isTextMode ? char : `<mn>${char}</mn>`;
|
||||
}
|
||||
|
||||
// Uppercase letters (A-Z)
|
||||
if (codeNum >= 0x0041 && codeNum <= 0x005A) {
|
||||
const char = String.fromCharCode(codeNum);
|
||||
return isTextMode ? char : `<mi>${char}</mi>`;
|
||||
}
|
||||
|
||||
// Lowercase letters (a-z)
|
||||
if (codeNum >= 0x0061 && codeNum <= 0x007A) {
|
||||
const char = String.fromCharCode(codeNum);
|
||||
return isTextMode ? char : `<mi>${char}</mi>`;
|
||||
}
|
||||
|
||||
// Mathematical operators range
|
||||
if (codeNum >= 0x2200 && codeNum <= 0x22FF) {
|
||||
const char = String.fromCharCode(codeNum);
|
||||
return isTextMode ? char : `<mo>${char}</mo>`;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
try {
|
||||
const char = String.fromCharCode(codeNum);
|
||||
return isTextMode ? char : `<mi>${char}</mi>`;
|
||||
} catch (e) {
|
||||
return 'Unsupported character';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Element Mover Module
|
||||
* Handles structural reorganization for subscripts/superscripts
|
||||
*/
|
||||
class ElementMover {
|
||||
constructor() {
|
||||
this.subscriptSelectors = ['tmSUP', 'tmSUB', 'tmSUBSUP'];
|
||||
this.parenthesesPairs = {
|
||||
'0x0028': '0x0029', // ( )
|
||||
'0x005B': '0x005D', // [ ]
|
||||
'0x007B': '0x007D', // { }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restructure elements to handle MathType vs MathML differences
|
||||
*/
|
||||
restructureElements(xmlStructure) {
|
||||
this.moveSubscriptsAndSuperscripts(xmlStructure);
|
||||
this.invertEmbellishments(xmlStructure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move base elements into subscript/superscript containers
|
||||
*/
|
||||
moveSubscriptsAndSuperscripts(node) {
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i];
|
||||
|
||||
if (child.type === 'tmpl' && this.subscriptSelectors.includes(child.selector)) {
|
||||
// Find preceding element(s) to use as base
|
||||
const baseElements = this.findBaseElements(node.children, i);
|
||||
if (baseElements.length > 0) {
|
||||
// Create new slot for base elements
|
||||
const baseSlot = {
|
||||
type: 'slot',
|
||||
children: baseElements
|
||||
};
|
||||
|
||||
// Insert base slot as first child of template
|
||||
child.children = child.children || [];
|
||||
child.children.unshift(baseSlot);
|
||||
|
||||
// Remove base elements from original position
|
||||
baseElements.forEach(elem => {
|
||||
const index = node.children.indexOf(elem);
|
||||
if (index > -1) {
|
||||
node.children.splice(index, 1);
|
||||
if (index < i) i--; // Adjust loop index
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process children
|
||||
this.moveSubscriptsAndSuperscripts(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find base elements for subscript/superscript
|
||||
*/
|
||||
findBaseElements(siblings, templateIndex) {
|
||||
const baseElements = [];
|
||||
|
||||
// Look for preceding elements
|
||||
for (let i = templateIndex - 1; i >= 0; i--) {
|
||||
const sibling = siblings[i];
|
||||
|
||||
// For simple cases, take the immediately preceding element
|
||||
if (sibling.type === 'char' ||
|
||||
(sibling.type === 'tmpl' && !this.subscriptSelectors.includes(sibling.selector))) {
|
||||
baseElements.unshift(sibling);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return baseElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invert embellishment structures: char[embell] -> embell[char]
|
||||
*/
|
||||
invertEmbellishments(node) {
|
||||
if (node.type === 'char' && node.embellishment) {
|
||||
// Move char content into embellishment
|
||||
const embellishment = node.embellishment;
|
||||
embellishment.children = embellishment.children || [];
|
||||
embellishment.children.push({
|
||||
type: 'char',
|
||||
mt_code_value: node.mt_code_value,
|
||||
typeface: node.typeface
|
||||
});
|
||||
|
||||
// Replace char node with embellishment
|
||||
Object.keys(node).forEach(key => delete node[key]);
|
||||
Object.assign(node, embellishment);
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(child => this.invertEmbellishments(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MathML Transformer Module
|
||||
* Converts preprocessed XML structure to MathML
|
||||
*/
|
||||
class MathMLTransformer {
|
||||
constructor() {
|
||||
this.templateHandlers = this.initializeTemplateHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize template transformation handlers
|
||||
*/
|
||||
initializeTemplateHandlers() {
|
||||
return {
|
||||
// Fractions
|
||||
'tmFRACT': (node) => this.handleFraction(node),
|
||||
|
||||
// Subscripts and superscripts
|
||||
'tmSUP': (node) => this.handleSuperscript(node),
|
||||
'tmSUB': (node) => this.handleSubscript(node),
|
||||
'tmSUBSUP': (node) => this.handleSubSuperscript(node),
|
||||
|
||||
// Roots
|
||||
'tmROOT': (node) => this.handleRoot(node),
|
||||
'tmSQRT': (node) => this.handleSquareRoot(node),
|
||||
|
||||
// Large operators
|
||||
'tmSUM': (node) => this.handleSummation(node),
|
||||
'tmINT': (node) => this.handleIntegral(node),
|
||||
|
||||
// Matrices
|
||||
'tmMATRIX': (node) => this.handleMatrix(node),
|
||||
|
||||
// Fences (parentheses, brackets)
|
||||
'tmPARENS': (node) => this.handleParentheses(node),
|
||||
'tmBRACK': (node) => this.handleBrackets(node),
|
||||
'tmBRACE': (node) => this.handleBraces(node),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform XML structure to MathML
|
||||
*/
|
||||
transformToMathML(xmlStructure) {
|
||||
return this.processNode(xmlStructure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single node
|
||||
*/
|
||||
processNode(node) {
|
||||
switch (node.type) {
|
||||
case 'mtef':
|
||||
return this.processMTEF(node);
|
||||
case 'slot':
|
||||
return this.processSlot(node);
|
||||
case 'char':
|
||||
return this.processCharacter(node);
|
||||
case 'tmpl':
|
||||
return this.processTemplate(node);
|
||||
default:
|
||||
return this.processChildren(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process MTEF root node
|
||||
*/
|
||||
processMTEF(node) {
|
||||
const content = this.processChildren(node);
|
||||
const displayMode = node.equation_options === 'block' ? ' display="block"' : '';
|
||||
return content; // Math wrapper added in finalizeMathML
|
||||
}
|
||||
|
||||
/**
|
||||
* Process slot (grouping) node
|
||||
*/
|
||||
processSlot(node) {
|
||||
const content = this.processChildren(node);
|
||||
return content ? `<mrow>${content}</mrow>` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process character node
|
||||
*/
|
||||
processCharacter(node) {
|
||||
return node.mathml || `<mi>${String.fromCharCode(parseInt(node.mt_code_value, 16))}</mi>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process template node
|
||||
*/
|
||||
processTemplate(node) {
|
||||
const handler = this.templateHandlers[node.selector];
|
||||
if (handler) {
|
||||
return handler(node);
|
||||
}
|
||||
|
||||
// Default processing for unknown templates
|
||||
return this.processChildren(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process children nodes
|
||||
*/
|
||||
processChildren(node) {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return node.children
|
||||
.map(child => this.processNode(child))
|
||||
.filter(result => result !== '')
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Template handlers
|
||||
|
||||
/**
|
||||
* Handle fraction templates
|
||||
*/
|
||||
handleFraction(node) {
|
||||
const children = node.children || [];
|
||||
const numerator = children[0] ? this.processNode(children[0]) : '';
|
||||
const denominator = children[1] ? this.processNode(children[1]) : '';
|
||||
|
||||
if (node.variation === 'tvFR_SLASH') {
|
||||
return `<mfrac bevelled="true">${numerator}${denominator}</mfrac>`;
|
||||
} else if (node.variation === 'tvFR_SMALL') {
|
||||
return `<mstyle scriptlevel="+1"><mfrac>${numerator}${denominator}</mfrac></mstyle>`;
|
||||
} else {
|
||||
return `<mfrac>${numerator}${denominator}</mfrac>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle superscript templates
|
||||
*/
|
||||
handleSuperscript(node) {
|
||||
const children = node.children || [];
|
||||
const base = children[0] ? this.processNode(children[0]) : '';
|
||||
const superscript = children[1] ? this.processNode(children[1]) : '';
|
||||
|
||||
if (node.variation === 'tvSU_PRECEDES') {
|
||||
return `<mmultiscripts>${base}<mprescripts/><none/>${superscript}</mmultiscripts>`;
|
||||
} else {
|
||||
return `<msup>${base}${superscript}</msup>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscript templates
|
||||
*/
|
||||
handleSubscript(node) {
|
||||
const children = node.children || [];
|
||||
const base = children[0] ? this.processNode(children[0]) : '';
|
||||
const subscript = children[1] ? this.processNode(children[1]) : '';
|
||||
|
||||
if (node.variation === 'tvSU_PRECEDES') {
|
||||
return `<mmultiscripts>${base}<mprescripts/>${subscript}<none/></mmultiscripts>`;
|
||||
} else {
|
||||
return `<msub>${base}${subscript}</msub>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle combined subscript-superscript templates
|
||||
*/
|
||||
handleSubSuperscript(node) {
|
||||
const children = node.children || [];
|
||||
const base = children[0] ? this.processNode(children[0]) : '';
|
||||
const subscript = children[1] ? this.processNode(children[1]) : '';
|
||||
const superscript = children[2] ? this.processNode(children[2]) : '';
|
||||
|
||||
if (node.variation === 'tvSU_PRECEDES') {
|
||||
return `<mmultiscripts>${base}<mprescripts/>${subscript}${superscript}</mmultiscripts>`;
|
||||
} else {
|
||||
return `<msubsup>${base}${subscript}${superscript}</msubsup>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle square root templates
|
||||
*/
|
||||
handleSquareRoot(node) {
|
||||
const children = node.children || [];
|
||||
const content = children[0] ? this.processNode(children[0]) : '';
|
||||
return `<msqrt>${content}</msqrt>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle nth root templates
|
||||
*/
|
||||
handleRoot(node) {
|
||||
const children = node.children || [];
|
||||
const radicand = children[0] ? this.processNode(children[0]) : '';
|
||||
const index = children[1] ? this.processNode(children[1]) : '';
|
||||
return `<mroot>${radicand}${index}</mroot>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle summation templates
|
||||
*/
|
||||
handleSummation(node) {
|
||||
const children = node.children || [];
|
||||
const base = '<mo>∑</mo>';
|
||||
|
||||
if (children.length === 1) {
|
||||
return `<mrow>${base}${this.processNode(children[0])}</mrow>`;
|
||||
} else if (children.length === 2) {
|
||||
return `<munder>${base}${this.processNode(children[0])}</munder>`;
|
||||
} else if (children.length === 3) {
|
||||
return `<munderover>${base}${this.processNode(children[1])}${this.processNode(children[2])}</munderover>`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle integral templates
|
||||
*/
|
||||
handleIntegral(node) {
|
||||
const children = node.children || [];
|
||||
const base = '<mo>∫</mo>';
|
||||
|
||||
if (children.length === 1) {
|
||||
return `<mrow>${base}${this.processNode(children[0])}</mrow>`;
|
||||
} else if (children.length === 2) {
|
||||
return `<munder>${base}${this.processNode(children[0])}</munder>`;
|
||||
} else if (children.length === 3) {
|
||||
return `<munderover>${base}${this.processNode(children[1])}${this.processNode(children[2])}</munderover>`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle matrix templates
|
||||
*/
|
||||
handleMatrix(node) {
|
||||
const children = node.children || [];
|
||||
let tableContent = '';
|
||||
|
||||
children.forEach(row => {
|
||||
if (row.type === 'mtr') {
|
||||
tableContent += '<mtr>';
|
||||
if (row.children) {
|
||||
row.children.forEach(cell => {
|
||||
if (cell.type === 'mtd') {
|
||||
tableContent += `<mtd>${this.processNode(cell)}</mtd>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
tableContent += '</mtr>';
|
||||
}
|
||||
});
|
||||
|
||||
return `<mtable>${tableContent}</mtable>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle parentheses templates
|
||||
*/
|
||||
handleParentheses(node) {
|
||||
const content = this.processChildren(node);
|
||||
return `<mrow><mo>(</mo>${content}<mo>)</mo></mrow>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bracket templates
|
||||
*/
|
||||
handleBrackets(node) {
|
||||
const content = this.processChildren(node);
|
||||
return `<mrow><mo>[</mo>${content}<mo>]</mo></mrow>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle brace templates
|
||||
*/
|
||||
handleBraces(node) {
|
||||
const content = this.processChildren(node);
|
||||
return `<mrow><mo>{</mo>${content}<mo>}</mo></mrow>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions for working with binary data
|
||||
|
||||
/**
|
||||
* Load binary file from input element
|
||||
*/
|
||||
function loadBinaryFile(fileInput) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target.result);
|
||||
reader.onerror = (e) => reject(new Error('Failed to read file'));
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ArrayBuffer to hex string for debugging
|
||||
*/
|
||||
function arrayBufferToHex(buffer) {
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
return Array.from(uint8Array)
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Export for Node.js or browser usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
MathTypeToMathMLConverter,
|
||||
CharacterReplacer,
|
||||
ElementMover,
|
||||
MathMLTransformer,
|
||||
loadBinaryFile,
|
||||
arrayBufferToHex
|
||||
};
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.MathTypeToMathMLConverter = MathTypeToMathMLConverter;
|
||||
window.CharacterReplacer = CharacterReplacer;
|
||||
window.ElementMover = ElementMover;
|
||||
window.MathMLTransformer = MathMLTransformer;
|
||||
window.loadBinaryFile = loadBinaryFile;
|
||||
window.arrayBufferToHex = arrayBufferToHex;
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
// In a web page:
|
||||
const converter = new MathTypeToMathMLConverter();
|
||||
|
||||
// From file input
|
||||
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
||||
try {
|
||||
const binaryData = await loadBinaryFile(e.target);
|
||||
const mathml = await converter.convert(binaryData);
|
||||
document.getElementById('output').innerHTML = mathml;
|
||||
} catch (error) {
|
||||
console.error('Conversion failed:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// From existing binary data
|
||||
const binaryData = new Uint8Array([...]); // Your binary MTEF data
|
||||
converter.convert(binaryData).then(mathml => {
|
||||
console.log('Generated MathML:', mathml);
|
||||
});
|
||||
*/
|
||||
387
scripts/mathtype2mathml/server_converter.js
Normal file
387
scripts/mathtype2mathml/server_converter.js
Normal file
@@ -0,0 +1,387 @@
|
||||
// Node.js server for MathType to MathML conversion
|
||||
// This uses the actual Ruby gem to parse binary files
|
||||
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const crypto = require('crypto'); // Added for filename generation
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Define the correct paths relative to this file location
|
||||
const MATHTYPE_DIR = __dirname; // Current directory is the mathtype_to_mathml folder
|
||||
const CONVERTER_SCRIPT = path.join(MATHTYPE_DIR, 'server', 'converter.rb');
|
||||
const BATCH_CONVERTER_SCRIPT = path.join(MATHTYPE_DIR, 'server', 'batch_converter.rb');
|
||||
const PUBLIC_DIR = path.join(MATHTYPE_DIR, 'public');
|
||||
|
||||
// Enable CORS for cross-origin requests
|
||||
app.use(cors());
|
||||
|
||||
// Configure multer for file uploads with proper extension preservation
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'uploads/');
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// Preserve the original file extension for proper format detection
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = crypto.randomBytes(16).toString('hex');
|
||||
cb(null, name + ext);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
|
||||
});
|
||||
|
||||
// Serve static files from the correct public directory
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
|
||||
// Convert MathType binary to MathML using Ruby gem
|
||||
app.post('/convert-mathtype', upload.single('file'), async (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const filePath = req.file.path;
|
||||
console.log(`File uploaded: ${req.file.originalname}`);
|
||||
console.log(`Temp file path: ${filePath}`);
|
||||
console.log(`File exists: ${fs.existsSync(filePath)}`);
|
||||
|
||||
try {
|
||||
// Call Ruby script to convert the file
|
||||
const mathml = await convertWithRubyGem(filePath);
|
||||
|
||||
// Clean up uploaded file
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mathml: mathml,
|
||||
originalName: req.file.originalname
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Clean up uploaded file
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
console.error('Conversion error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Conversion failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to call Ruby gem for conversion
|
||||
function convertWithRubyGem(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if converter script exists
|
||||
if (!fs.existsSync(CONVERTER_SCRIPT)) {
|
||||
reject(new Error(`Converter script not found at: ${CONVERTER_SCRIPT}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert relative path to absolute path since Ruby script runs from different directory
|
||||
const absoluteFilePath = path.resolve(filePath);
|
||||
console.log(`Converting relative path: ${filePath}`);
|
||||
console.log(`To absolute path: ${absoluteFilePath}`);
|
||||
console.log(`Absolute file exists: ${fs.existsSync(absoluteFilePath)}`);
|
||||
|
||||
// Add debugging for working directory
|
||||
console.log(`Working directory (MATHTYPE_DIR): ${MATHTYPE_DIR}`);
|
||||
console.log(`Working directory exists: ${fs.existsSync(MATHTYPE_DIR)}`);
|
||||
console.log(`Converter script path: ${CONVERTER_SCRIPT}`);
|
||||
console.log(`Converter script exists: ${fs.existsSync(CONVERTER_SCRIPT)}`);
|
||||
|
||||
// Check if the file actually exists
|
||||
if (!fs.existsSync(absoluteFilePath)) {
|
||||
reject(new Error(`Upload file not found at: ${absoluteFilePath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate working directory exists
|
||||
if (!fs.existsSync(MATHTYPE_DIR)) {
|
||||
reject(new Error(`Working directory not found at: ${MATHTYPE_DIR}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute Ruby script with bundle exec from the correct directory
|
||||
// Removed shell: true to fix path issues with spaces
|
||||
const ruby = spawn('bundle', ['exec', 'ruby', CONVERTER_SCRIPT, absoluteFilePath], {
|
||||
cwd: MATHTYPE_DIR,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
ruby.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
ruby.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
ruby.on('close', (code) => {
|
||||
console.log(`Ruby process exited with code: ${code}`);
|
||||
if (errorOutput) {
|
||||
console.log(`Ruby stderr output: ${errorOutput}`);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Ruby script exited with code ${code}: ${errorOutput}`);
|
||||
|
||||
// Provide more specific error messages based on the error content
|
||||
let errorMessage = 'Ruby conversion failed';
|
||||
|
||||
if (errorOutput.includes('undefined method')) {
|
||||
errorMessage = 'MathType file parsing error - this may be due to an incompatible or corrupted MathType file format';
|
||||
} else if (errorOutput.includes('No such file')) {
|
||||
errorMessage = 'File not found during conversion';
|
||||
} else if (errorOutput.includes('File is empty')) {
|
||||
errorMessage = 'The uploaded file appears to be empty';
|
||||
} else if (errorOutput.includes('Invalid file format')) {
|
||||
errorMessage = 'The file does not appear to be a valid MathType binary file';
|
||||
} else if (errorOutput.includes('Permission denied')) {
|
||||
errorMessage = 'Permission error accessing the file';
|
||||
} else if (errorOutput.includes('command not found') || errorOutput.includes('bundle: not found')) {
|
||||
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed';
|
||||
} else if (errorOutput.includes('Could not locate Gemfile')) {
|
||||
errorMessage = 'Gemfile not found - please run bundle install from the project directory';
|
||||
}
|
||||
|
||||
reject(new Error(`${errorMessage}: ${errorOutput || 'Unknown error'}`));
|
||||
} else {
|
||||
const mathML = output.trim();
|
||||
if (!mathML) {
|
||||
reject(new Error('Conversion resulted in empty MathML.'));
|
||||
} else {
|
||||
console.log('Ruby conversion successful, MathML length:', mathML.length);
|
||||
resolve(mathML);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ruby.on('error', (err) => {
|
||||
console.error('Failed to start Ruby process:', err);
|
||||
console.error('Error details:', {
|
||||
code: err.code,
|
||||
errno: err.errno,
|
||||
syscall: err.syscall,
|
||||
path: err.path
|
||||
});
|
||||
|
||||
let errorMessage = `Failed to start Ruby process: ${err.message}`;
|
||||
if (err.code === 'ENOENT') {
|
||||
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed and in your PATH';
|
||||
}
|
||||
|
||||
reject(new Error(errorMessage));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'MathType converter server is running',
|
||||
paths: {
|
||||
mathtypeDir: MATHTYPE_DIR,
|
||||
converterScript: CONVERTER_SCRIPT,
|
||||
publicDir: PUBLIC_DIR
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Batch convert multiple MathType files to MathML
|
||||
app.post('/convert-mathtype-batch', upload.array('files', 50), async (req, res) => {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({ error: 'No files uploaded' });
|
||||
}
|
||||
|
||||
console.log(`Batch conversion requested for ${req.files.length} files`);
|
||||
|
||||
try {
|
||||
// Prepare file paths for batch processing
|
||||
const filePaths = req.files.map(file => path.resolve(file.path));
|
||||
const fileInfo = req.files.map(file => ({
|
||||
originalName: file.originalname,
|
||||
tempPath: file.path,
|
||||
size: file.size
|
||||
}));
|
||||
|
||||
console.log('Files to process:', fileInfo.map(f => f.originalName));
|
||||
|
||||
// Call Ruby batch converter
|
||||
const results = await convertBatchWithRubyGem(filePaths);
|
||||
|
||||
// Clean up uploaded files
|
||||
req.files.forEach(file => {
|
||||
if (fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
}
|
||||
});
|
||||
|
||||
// Combine results with file info
|
||||
const response = {
|
||||
success: true,
|
||||
totalFiles: req.files.length,
|
||||
successful: results.successful,
|
||||
failed: results.failed,
|
||||
results: Object.entries(results.results).map(([filePath, result], index) => ({
|
||||
originalName: fileInfo[index].originalName,
|
||||
success: result.success,
|
||||
mathml: result.mathml,
|
||||
error: result.error,
|
||||
fileSize: result.file_size || fileInfo[index].size
|
||||
}))
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
|
||||
} catch (error) {
|
||||
// Clean up uploaded files in case of error
|
||||
req.files.forEach(file => {
|
||||
if (fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
}
|
||||
});
|
||||
|
||||
console.error('Batch conversion error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Batch conversion failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to call Ruby batch converter
|
||||
function convertBatchWithRubyGem(filePaths) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if batch converter script exists
|
||||
if (!fs.existsSync(BATCH_CONVERTER_SCRIPT)) {
|
||||
reject(new Error(`Batch converter script not found at: ${BATCH_CONVERTER_SCRIPT}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate working directory exists
|
||||
if (!fs.existsSync(MATHTYPE_DIR)) {
|
||||
reject(new Error(`Working directory not found at: ${MATHTYPE_DIR}`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting batch conversion of ${filePaths.length} files`);
|
||||
console.log(`Using batch converter: ${BATCH_CONVERTER_SCRIPT}`);
|
||||
|
||||
// Execute Ruby batch script
|
||||
const args = ['exec', 'ruby', BATCH_CONVERTER_SCRIPT, ...filePaths];
|
||||
const ruby = spawn('bundle', args, {
|
||||
cwd: MATHTYPE_DIR,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
ruby.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
ruby.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
ruby.on('close', (code) => {
|
||||
console.log(`Ruby batch process exited with code: ${code}`);
|
||||
if (errorOutput) {
|
||||
console.log(`Ruby stderr output: ${errorOutput}`);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Ruby batch script exited with code ${code}: ${errorOutput}`);
|
||||
reject(new Error(`Batch conversion failed: ${errorOutput || 'Unknown error'}`));
|
||||
} else {
|
||||
try {
|
||||
const results = JSON.parse(output.trim());
|
||||
console.log(`Batch conversion completed: ${results.successful}/${results.total_files} successful`);
|
||||
resolve(results);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse batch conversion results:', parseError);
|
||||
reject(new Error(`Failed to parse batch conversion results: ${parseError.message}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ruby.on('error', (err) => {
|
||||
console.error('Failed to start Ruby batch process:', err);
|
||||
let errorMessage = `Failed to start Ruby batch process: ${err.message}`;
|
||||
if (err.code === 'ENOENT') {
|
||||
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed and in your PATH';
|
||||
}
|
||||
reject(new Error(errorMessage));
|
||||
});
|
||||
|
||||
// Set timeout for the entire batch (5 minutes)
|
||||
const timeout = setTimeout(() => {
|
||||
ruby.kill('SIGTERM');
|
||||
reject(new Error('Batch conversion timeout (5 minutes)'));
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
ruby.on('close', () => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Diagnostic endpoint for testing file uploads
|
||||
app.post('/test-upload', upload.single('file'), (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const filePath = req.file.path;
|
||||
const absoluteFilePath = path.resolve(filePath);
|
||||
|
||||
// Get file info
|
||||
const stats = fs.statSync(absoluteFilePath);
|
||||
|
||||
// Clean up uploaded file immediately after getting info
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
fileInfo: {
|
||||
originalName: req.file.originalname,
|
||||
size: stats.size,
|
||||
mimetype: req.file.mimetype,
|
||||
encoding: req.file.encoding,
|
||||
tempPath: filePath,
|
||||
absolutePath: absoluteFilePath,
|
||||
existed: true
|
||||
},
|
||||
message: 'File upload test successful - file was received and cleaned up'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(port, () => {
|
||||
console.log(`MathType converter server running at http://localhost:${port}`);
|
||||
console.log(`Using MathType directory: ${MATHTYPE_DIR}`);
|
||||
console.log(`Converter script: ${CONVERTER_SCRIPT}`);
|
||||
console.log(`Public directory: ${PUBLIC_DIR}`);
|
||||
console.log('Make sure you have the mathtype_to_mathml Ruby gem installed:');
|
||||
console.log(` cd "${MATHTYPE_DIR}" && bundle install`);
|
||||
console.log(`Demo page available at: http://localhost:${port}/mathtype_converter_demo.html`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
BIN
scripts/mathtype2mathml/test/input/25.bin
Normal file
BIN
scripts/mathtype2mathml/test/input/25.bin
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/input/26.bin
Normal file
BIN
scripts/mathtype2mathml/test/input/26.bin
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/input/40.bin
Normal file
BIN
scripts/mathtype2mathml/test/input/40.bin
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/input/61.bin
Normal file
BIN
scripts/mathtype2mathml/test/input/61.bin
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/output/25.wmf
Normal file
BIN
scripts/mathtype2mathml/test/output/25.wmf
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/output/26.wmf
Normal file
BIN
scripts/mathtype2mathml/test/output/26.wmf
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/output/40.wmf
Normal file
BIN
scripts/mathtype2mathml/test/output/40.wmf
Normal file
Binary file not shown.
BIN
scripts/mathtype2mathml/test/output/61.wmf
Normal file
BIN
scripts/mathtype2mathml/test/output/61.wmf
Normal file
Binary file not shown.
1
scripts/mathtype2mathml/uploads/.gitkeep
Normal file
1
scripts/mathtype2mathml/uploads/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep this directory in git
|
||||
2
scripts/mathtype2mathml/vendor/gems/.rspec
vendored
Normal file
2
scripts/mathtype2mathml/vendor/gems/.rspec
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
--format documentation
|
||||
--color
|
||||
1
scripts/mathtype2mathml/vendor/gems/.ruby-version
vendored
Normal file
1
scripts/mathtype2mathml/vendor/gems/.ruby-version
vendored
Normal file
@@ -0,0 +1 @@
|
||||
2.2.2
|
||||
3
scripts/mathtype2mathml/vendor/gems/.travis.yml
vendored
Normal file
3
scripts/mathtype2mathml/vendor/gems/.travis.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
language: ruby
|
||||
rvm:
|
||||
- 2.2.2
|
||||
2
scripts/mathtype2mathml/vendor/gems/mathtype/.rspec
vendored
Normal file
2
scripts/mathtype2mathml/vendor/gems/mathtype/.rspec
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
--color
|
||||
--require spec_helper
|
||||
28
scripts/mathtype2mathml/vendor/gems/mathtype/.travis.yml
vendored
Normal file
28
scripts/mathtype2mathml/vendor/gems/mathtype/.travis.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
language: ruby
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: 2.1
|
||||
before_script: mv gemspecs/mathtype.ruby-2-2.gemspec mathtype.gemspec
|
||||
- rvm: 2.2
|
||||
before_script: mv gemspecs/mathtype.ruby-2-2.gemspec mathtype.gemspec
|
||||
- rvm: 2.3
|
||||
before_install:
|
||||
- gem update --system
|
||||
- gem install bundler
|
||||
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
|
||||
- rvm: 2.4
|
||||
before_install:
|
||||
- gem update --system
|
||||
- gem install bundler
|
||||
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
|
||||
- rvm: 2.5
|
||||
before_install:
|
||||
- gem update --system
|
||||
- gem install bundler
|
||||
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
|
||||
- rvm: 2.6
|
||||
before_install:
|
||||
- gem update --system
|
||||
- gem install bundler
|
||||
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
|
||||
4
scripts/mathtype2mathml/vendor/gems/mathtype/Gemfile
vendored
Normal file
4
scripts/mathtype2mathml/vendor/gems/mathtype/Gemfile
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# Specify your gem's dependencies in mathtype.gemspec
|
||||
gemspec
|
||||
22
scripts/mathtype2mathml/vendor/gems/mathtype/LICENSE.txt
vendored
Normal file
22
scripts/mathtype2mathml/vendor/gems/mathtype/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2015 Jure Triglav
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
43
scripts/mathtype2mathml/vendor/gems/mathtype/README.md
vendored
Normal file
43
scripts/mathtype2mathml/vendor/gems/mathtype/README.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# What does the `mathtype` gem do?
|
||||
|
||||
This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this line to your application's Gemfile:
|
||||
|
||||
```ruby
|
||||
gem 'mathtype'
|
||||
```
|
||||
|
||||
And then execute:
|
||||
|
||||
$ bundle
|
||||
|
||||
Or install it yourself as:
|
||||
|
||||
$ gem install mathtype
|
||||
|
||||
## Usage
|
||||
|
||||
To convert a MathType binary equation (e.g. extracted from a Word document) to an XML form:
|
||||
|
||||
```
|
||||
xml = Mathtype::Converter.new("equation1.bin") # to get the Nokogiri XML object
|
||||
puts xml.to_xml # to get the string XML representation
|
||||
```
|
||||
|
||||
# Testing
|
||||
|
||||
Run `bundle exec rspec` to run specs.
|
||||
|
||||
# Maintainers
|
||||
- Sebastian Bulka ([sbulka](https://github.com/sbulka))
|
||||
- Jure Triglav ([jure](https://github.com/jure))
|
||||
|
||||
## Contributing
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create a new Pull Request
|
||||
10
scripts/mathtype2mathml/vendor/gems/mathtype/Rakefile
vendored
Normal file
10
scripts/mathtype2mathml/vendor/gems/mathtype/Rakefile
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
require "bundler/gem_tasks"
|
||||
|
||||
task :default => :spec
|
||||
|
||||
begin
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
28
scripts/mathtype2mathml/vendor/gems/mathtype/gemspecs/mathtype.ruby-2-2.gemspec
vendored
Normal file
28
scripts/mathtype2mathml/vendor/gems/mathtype/gemspecs/mathtype.ruby-2-2.gemspec
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# coding: utf-8
|
||||
lib = File.expand_path('../../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'mathtype/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "mathtype"
|
||||
spec.version = Mathtype::VERSION
|
||||
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
|
||||
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
|
||||
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
|
||||
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
|
||||
spec.homepage = ""
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*")
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_development_dependency "bundler", "> 1.7"
|
||||
spec.add_development_dependency "rake", "~> 10.0"
|
||||
spec.add_development_dependency "rspec", "~> 3.3"
|
||||
spec.add_development_dependency "pry", "~> 0.10"
|
||||
spec.add_dependency "bindata", "~> 2.1"
|
||||
spec.add_dependency "nokogiri", "~> 1.6"
|
||||
spec.add_dependency "ruby-ole", "~> 1.2"
|
||||
end
|
||||
28
scripts/mathtype2mathml/vendor/gems/mathtype/gemspecs/mathtype.ruby-2-6.gemspec
vendored
Normal file
28
scripts/mathtype2mathml/vendor/gems/mathtype/gemspecs/mathtype.ruby-2-6.gemspec
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# coding: utf-8
|
||||
lib = File.expand_path('../../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'mathtype/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "mathtype"
|
||||
spec.version = Mathtype::VERSION
|
||||
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
|
||||
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
|
||||
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
|
||||
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
|
||||
spec.homepage = ""
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*")
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_development_dependency "bundler", "~> 2.0"
|
||||
spec.add_development_dependency "rake", "~> 10.0"
|
||||
spec.add_development_dependency "rspec", "~> 3.3"
|
||||
spec.add_development_dependency "pry", "~> 0.10"
|
||||
spec.add_dependency "bindata", "~> 2.1"
|
||||
spec.add_dependency "nokogiri", "~> 1.6"
|
||||
spec.add_dependency "ruby-ole", "~> 1.2"
|
||||
end
|
||||
30
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/eps.rb
vendored
Normal file
30
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/eps.rb
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
module Mathtype
|
||||
class EpsFileParser < FileParser
|
||||
def initialize(path)
|
||||
super(path)
|
||||
extract_mtef_from_epscomment(@raw)
|
||||
end
|
||||
|
||||
def extract_mtef_from_epscomment(comment)
|
||||
string64 = @raw.gsub /\r/, ""
|
||||
# d for delimiter
|
||||
d = string64[(/MathType(.)MTEF\1/ =~ string64) + 8]
|
||||
matches = string64.match /.*MathType#{d}MTEF#{d}(.)#{d}(.)#{d}(.)(.)(.*)#{d}([0-9A-F]{4})#{d}.*/m
|
||||
leading_chrs = matches[1].to_i
|
||||
trailing_chrs = matches[2].to_i - 1
|
||||
a64_chr1 = matches[3]
|
||||
a64_chr2 = matches[4]
|
||||
A64[a64_chr1] = 62
|
||||
A64[a64_chr2] = 63
|
||||
A64[62] = a64_chr1
|
||||
A64[63] = a64_chr2
|
||||
string64 = matches[5]
|
||||
mtchecksum = matches[6]
|
||||
string64.gsub! /.{#{trailing_chrs}}(\n).{#{leading_chrs}}/m, ""
|
||||
@equation = decode string64
|
||||
unless checksum == mtchecksum
|
||||
raise ArgumentError, "Checksums do not match, expected #{mtchecksum} but got #{checksum}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/ole.rb
vendored
Normal file
15
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/ole.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
require "bindata"
|
||||
require "ole/storage"
|
||||
|
||||
module Mathtype
|
||||
class OleFileParser < FileParser
|
||||
def initialize(path)
|
||||
read_from_file(path)
|
||||
end
|
||||
def read_from_file(path)
|
||||
ole = Ole::Storage.open(path, "rb+")
|
||||
@equation = ole.file.read("Equation Native")[28..-1]
|
||||
ole.close
|
||||
end
|
||||
end
|
||||
end
|
||||
102
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/parser.rb
vendored
Normal file
102
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/parser.rb
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
module Mathtype
|
||||
class FileParser
|
||||
attr_reader :raw
|
||||
attr_reader :equation
|
||||
def initialize(path)
|
||||
read_from_file(path)
|
||||
end
|
||||
|
||||
def checksum
|
||||
(@equation.bytes.reduce(:+) & 0xFFFF).to_s(16).upcase
|
||||
end
|
||||
|
||||
def read_from_file(path)
|
||||
f = File.open(path, "rb")
|
||||
@raw = f.read
|
||||
f.close
|
||||
end
|
||||
|
||||
A64 =
|
||||
{
|
||||
"a" => 0, "b" => 1, "c" => 2, "d" => 3, "e" => 4,
|
||||
"f" => 5, "g" => 6, "h" => 7, "i" => 8, "j" => 9,
|
||||
"k" => 10, "l" => 11, "m" => 12, "n" => 13, "o" => 14, "p" => 15,
|
||||
"q" => 16, "r" => 17, "s" => 18, "t" => 19, "u" => 20, "v" => 21,
|
||||
"w" => 22, "x" => 23, "y" => 24, "z" => 25, "A" => 26, "B" => 27,
|
||||
"C" => 28, "D" => 29, "E" => 30, "F" => 31, "G" => 32, "H" => 33,
|
||||
"I" => 34, "J" => 35, "K" => 36, "L" => 37, "M" => 38, "N" => 39,
|
||||
"O" => 40, "P" => 41, "Q" => 42, "R" => 43, "S" => 44, "T" => 45,
|
||||
"U" => 46, "V" => 47, "W" => 48, "X" => 49, "Y" => 50, "Z" => 51,
|
||||
"0" => 52, "1" => 53, "2" => 54, "3" => 55, "4" => 56, "5" => 57,
|
||||
"6" => 58, "7" => 59, "8" => 60, "9" => 61, "+" => 62, "-" => 63,
|
||||
0 => "a", 1 => "b", 2 => "c", 3 => "d", 4 => "e",
|
||||
5 => "f", 6 => "g", 7 => "h", 8 => "i", 9 => "j",
|
||||
10 => "k", 11 => "l", 12 => "m", 13 => "n", 14 => "o", 15 => "p",
|
||||
16 => "q", 17 => "r", 18 => "s", 19 => "t", 20 => "u", 21 => "v",
|
||||
22 => "w", 23 => "x", 24 => "y", 25 => "z", 26 => "A", 27 => "B",
|
||||
28 => "C", 29 => "D", 30 => "E", 31 => "F", 32 => "G", 33 => "H",
|
||||
34 => "I", 35 => "J", 36 => "K", 37 => "L", 38 => "M", 39 => "N",
|
||||
40 => "O", 41 => "P", 42 => "Q", 43 => "R", 44 => "S", 45 => "T",
|
||||
46 => "U", 47 => "V", 48 => "W", 49 => "X", 50 => "Y", 51 => "Z",
|
||||
52 => "0", 53 => "1", 54 => "2", 55 => "3", 56 => "4", 57 => "5",
|
||||
58 => "6", 59 => "7", 60 => "8", 61 => "9", 62 => "+", 63 => "-"
|
||||
}
|
||||
|
||||
def decode(string64)
|
||||
#decode from base64 mtef to binary mtef
|
||||
l = string64.length
|
||||
i = b0 = b1 = carry = 0
|
||||
out = []
|
||||
while i < l
|
||||
chr = A64[string64[i]]
|
||||
case i % 4
|
||||
when 0
|
||||
b0 = chr
|
||||
b1 = 0
|
||||
when 1
|
||||
b0 += ((chr & 3) << 6)
|
||||
b1 = ((chr & 0xfc) >> 2)
|
||||
out << b0
|
||||
when 2
|
||||
b0 = ((chr & 0x30) >> 4)
|
||||
b1 += ((chr & 0xf) << 4)
|
||||
when 3
|
||||
b0 += chr << 2
|
||||
out << b1 << b0
|
||||
b0=b1=0
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
out << b0 if i % 2 == 1
|
||||
return out.pack("C*")
|
||||
end
|
||||
|
||||
def encode(mtef)
|
||||
#encode from binary mtef to base64 mtef
|
||||
out = ""
|
||||
bytes = mtef.bytes
|
||||
i = shft = 0
|
||||
l = bytes.length
|
||||
carry = 0
|
||||
while i < l
|
||||
chr = bytes[i] << shft
|
||||
chr = chr | carry
|
||||
out << A64[chr & 0x3f]
|
||||
carry = chr >> 6
|
||||
i += 1
|
||||
shft = (i % 3) * 2
|
||||
if shft == 0
|
||||
out << A64[carry]
|
||||
carry = 0
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
require_relative "ole.rb"
|
||||
require_relative "wmf.rb"
|
||||
require_relative "eps.rb"
|
||||
|
||||
28
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/wmf.rb
vendored
Normal file
28
scripts/mathtype2mathml/vendor/gems/mathtype/lib/file_parser/wmf.rb
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
module Mathtype
|
||||
class WmfFileParser < FileParser
|
||||
def initialize(path)
|
||||
super(path)
|
||||
extract_mtef_from_mfcomment(@raw)
|
||||
end
|
||||
|
||||
def extract_mtef_from_mfcomment(comment)
|
||||
match = comment.match("MathTypeUU")
|
||||
if match
|
||||
lenpos = match.end(0)
|
||||
len = comment[lenpos..lenpos+1].unpack("H*")[0].to_i 16
|
||||
start = lenpos + 2
|
||||
else
|
||||
match = comment.match("AppsMFCC\x01")
|
||||
raise ::NotImplementedError, "No MathType Equation found in wmf" unless match
|
||||
len_start = match.end(0)
|
||||
totallen = comment[len_start..len_start + 3].reverse.chars.rotate(-1).join.unpack("H*")[0].to_i 16
|
||||
datalen = comment[len_start + 4 .. len_start + 7].reverse.chars.rotate(-1).join.unpack("H*")[0].to_i 16
|
||||
signature = comment[len_start + 8.. -1].match(/.+?\x00/)
|
||||
raise ::NotImplementedError, "Equation split over multiple comments" unless totallen == datalen
|
||||
start = len_start + 8 + signature.end(0)
|
||||
len = datalen
|
||||
end
|
||||
@equation = comment[start..(start + len)]
|
||||
end
|
||||
end
|
||||
end
|
||||
86
scripts/mathtype2mathml/vendor/gems/mathtype/lib/mathtype.rb
vendored
Normal file
86
scripts/mathtype2mathml/vendor/gems/mathtype/lib/mathtype.rb
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
require_relative "mathtype/version"
|
||||
require "nokogiri"
|
||||
require_relative "file_parser/parser.rb"
|
||||
require_relative "records/mtef.rb"
|
||||
require_relative "records3/mtef.rb"
|
||||
require_relative "records5/mtef.rb"
|
||||
|
||||
|
||||
module Mathtype
|
||||
class Converter
|
||||
attr_reader :parser
|
||||
attr_reader :xml
|
||||
attr_reader :builder
|
||||
attr_reader :version
|
||||
def initialize(equation)
|
||||
set_parser(equation)
|
||||
raise ::NotImplementedError, "Equation format not supported. Only .eps, .wmf and .bin (OLE-Object) currently supported, supplied was: #{equation}" unless @parser.equation
|
||||
@version = @parser.equation[0].unpack('C')[0].to_i
|
||||
raise ::NotImplementedError, "Only MTEF Version 3 and 5 currently supported, version is #{version}" unless (version==3 or version==5)
|
||||
case @version
|
||||
when 3
|
||||
data = Mathtype3::Equation.read(@parser.equation).snapshot
|
||||
when 5
|
||||
data = Mathtype5::Equation.read(@parser.equation).snapshot
|
||||
end
|
||||
@builder = Nokogiri::XML::Builder.new do |xml|
|
||||
@xml = xml
|
||||
xml.root do
|
||||
process(object: data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_parser(equation)
|
||||
if equation.end_with?(".bin")
|
||||
@parser = Mathtype::OleFileParser.new equation
|
||||
elsif equation.end_with?(".wmf")
|
||||
@parser = Mathtype::WmfFileParser.new equation
|
||||
else
|
||||
@parser = Mathtype::EpsFileParser.new equation
|
||||
end
|
||||
end
|
||||
|
||||
def to_xml
|
||||
@builder.to_xml
|
||||
end
|
||||
|
||||
def process(element: "mtef", object:)
|
||||
if object.is_a? Hash
|
||||
case @version
|
||||
when 3
|
||||
name = Mathtype3::RECORD_NAMES[object[:record_type]]
|
||||
else
|
||||
name = Mathtype5::RECORD_NAMES[object[:record_type]]
|
||||
end
|
||||
if name
|
||||
xml.send(name) do
|
||||
(object[:payload] || {}).each do |k, v|
|
||||
process(element: k, object: v)
|
||||
end
|
||||
end
|
||||
else
|
||||
xml.send(element) do
|
||||
object.each do |k, v|
|
||||
process(element: k, object: v)
|
||||
end
|
||||
end
|
||||
end
|
||||
elsif object.is_a? Array
|
||||
object.each do |a|
|
||||
process(element: element, object: a)
|
||||
end
|
||||
else
|
||||
process_final_element(element, object)
|
||||
end
|
||||
end
|
||||
|
||||
def process_final_element(element, object)
|
||||
if object.is_a? Hash
|
||||
xml.send(element, object)
|
||||
else
|
||||
xml.send(element) { xml.text object }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
scripts/mathtype2mathml/vendor/gems/mathtype/lib/mathtype/version.rb
vendored
Normal file
3
scripts/mathtype2mathml/vendor/gems/mathtype/lib/mathtype/version.rb
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module Mathtype
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
37
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/bintypes.rb
vendored
Normal file
37
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/bintypes.rb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
module Mathtype
|
||||
class Mtef16 < BinData::Primitive
|
||||
uint8 :low, :initial_value => 0
|
||||
uint8 :high, :initial_value => 0
|
||||
|
||||
def get; (high << 8) + low end
|
||||
def getlow
|
||||
self.low
|
||||
end
|
||||
def gethigh
|
||||
self.high
|
||||
end
|
||||
def set(v)
|
||||
self.low = v & 0xFF
|
||||
self.high = (v & 0xFF00) >> 8
|
||||
end
|
||||
end
|
||||
|
||||
class MatrixLineByte < BinData::Primitive
|
||||
# row/columlines are represented as 2-bit values in a full byte, in reverse
|
||||
# see MATRIX RECORD (5)
|
||||
# a 3x3 matrix with rows "solid none dashed solid" would be represented as
|
||||
# 01 10 00 01 = reverse 01 00 10 01
|
||||
# a 4x4 matrix with "solid none dashed solid solid " accordingly does
|
||||
# 01100001 00000001 = 01 00 10 01 01 00 00 00
|
||||
uint8 :_byte
|
||||
|
||||
def get
|
||||
[
|
||||
(self._byte & 0x03),
|
||||
(self._byte & 0x0C) >> 2,
|
||||
(self._byte & 0x30) >> 4,
|
||||
(self._byte & 0xC0) >> 6
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
8
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/end.rb
vendored
Normal file
8
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/end.rb
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module Mathtype
|
||||
class RecordEnd < BinData::Primitive
|
||||
def get; ""; end
|
||||
def set(v); end
|
||||
|
||||
def to_formatted_s(indent = 0); to_s; end
|
||||
end
|
||||
end
|
||||
36
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/mtef.rb
vendored
Normal file
36
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/mtef.rb
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
require_relative 'snapshot'
|
||||
require_relative 'bintypes'
|
||||
require_relative 'end'
|
||||
require_relative 'nudge'
|
||||
require_relative 'ruler'
|
||||
require_relative 'typesizes'
|
||||
|
||||
module Mathtype
|
||||
|
||||
HALIGN = {
|
||||
1 => "left",
|
||||
2 => "center",
|
||||
3 => "right",
|
||||
4 => "al", # relational
|
||||
5 => "dec" # decimal
|
||||
}
|
||||
|
||||
VALIGN = {
|
||||
0 => "top_baseline",
|
||||
1 => "center_baseline",
|
||||
2 => "bottom_baseline",
|
||||
3 => "center", # vertical centering
|
||||
4 => "axis" # math axis (center of +,-, brace points, etc.)
|
||||
}
|
||||
|
||||
class Equation < BinData::Record
|
||||
endian :little
|
||||
uint8 :mtef_version
|
||||
uint8 :platform
|
||||
uint8 :product
|
||||
uint8 :product_version
|
||||
uint8 :product_subversion
|
||||
include Snapshot
|
||||
end
|
||||
|
||||
end
|
||||
38
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/nudge.rb
vendored
Normal file
38
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/nudge.rb
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Nudge values:
|
||||
# LINE, CHAR, TMPL, PILE, MATRIX, and EMBELL records may store the result of
|
||||
# nudging (small offsets applied by the user). A nudged record has the
|
||||
# mtefOPT_NUDGE option (0x8) and the option byte is followed immediately by
|
||||
# the nudge offset. The nudge offset consists of either two bytes or six,
|
||||
# depending on the amount of offset. If -128 <= dx < +128 and
|
||||
# -128 <= dy < +128, then the offsets are stored as two bytes, dx followed
|
||||
# by dy, where each value has 128 added to it before it is written. Otherwise,
|
||||
# two bytes of 128 are stored, followed by the offsets, dx and dy, stored as
|
||||
# 16-bit values, low byte followed by high byte.
|
||||
|
||||
require_relative "snapshot"
|
||||
|
||||
module Mathtype
|
||||
class RecordNudge < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(dx dy)
|
||||
|
||||
endian :little
|
||||
|
||||
int8 :_small_dx
|
||||
int8 :_small_dy
|
||||
mtef16 :_large_dx, :onlyif => lambda { has_large_offset }
|
||||
mtef16 :_large_dy, :onlyif => lambda { has_large_offset }
|
||||
|
||||
def dx
|
||||
has_large_offset ? _large_dx : (_small_dx - 128)
|
||||
end
|
||||
|
||||
def dy
|
||||
has_large_offset ? _large_dy : (_small_dy - 128)
|
||||
end
|
||||
|
||||
def has_large_offset
|
||||
_small_dx == -128 and _small_dy == -128
|
||||
end
|
||||
end
|
||||
end
|
||||
22
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/ruler.rb
vendored
Normal file
22
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/ruler.rb
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# RULER record (7):
|
||||
# Consists of:
|
||||
# record type (7)
|
||||
# [n_stops] number of tab-stops
|
||||
# [tab-stop list] tab-stops in order from left-to-right
|
||||
# Each tab stop is described by a tab-stop type
|
||||
# (0 for left, 1 for center, 2 for right, 3 for equal, 4 for decimal),
|
||||
# followed by a 16-bit integer offset from the left end of the slot or pile
|
||||
# with which it is associated.
|
||||
|
||||
module Mathtype
|
||||
class RecordRuler < BinData::Record
|
||||
endian :little
|
||||
|
||||
int8 :n_stops
|
||||
|
||||
array :tab_stops, initial_length: :n_stops do
|
||||
int8 :tab_stop_type
|
||||
mtef16 :tab_stop
|
||||
end
|
||||
end
|
||||
end
|
||||
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/snapshot.rb
vendored
Normal file
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/snapshot.rb
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
require "bindata"
|
||||
|
||||
module Mathtype
|
||||
module Snapshot
|
||||
def snapshot
|
||||
snapshot = BinData::Record::Snapshot.new
|
||||
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
|
||||
exposed.each do |name|
|
||||
obj = find_obj_for_name(name)
|
||||
if obj
|
||||
snapshot[name] = obj.snapshot if include_obj?(obj)
|
||||
else
|
||||
snapshot[name] = self.send(name)
|
||||
end
|
||||
end
|
||||
snapshot
|
||||
end
|
||||
end
|
||||
end
|
||||
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/typesizes.rb
vendored
Normal file
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records/typesizes.rb
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Typesize records (10-14):
|
||||
# Consists of:
|
||||
# record type (10-14)
|
||||
# These records are just short ways of specifying a simple typesize where dsize
|
||||
# is zero. The tag value represents an lsize + 10. So if the tag value is 10,
|
||||
# it means equation content following it will be Full size (szFULL), tag value
|
||||
# 11 means szSUB, and so on. See typesize.
|
||||
|
||||
module Mathtype
|
||||
class RecordFull < BinData::Record; end
|
||||
|
||||
class RecordSub < BinData::Record; end
|
||||
|
||||
class RecordSub2 < BinData::Record; end
|
||||
|
||||
class RecordSym < BinData::Record; end
|
||||
|
||||
class RecordSubsym < BinData::Record; end
|
||||
end
|
||||
53
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/char.rb
vendored
Normal file
53
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/char.rb
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# CHAR record (2):
|
||||
# Consists of:
|
||||
# record type (2)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [typeface] typeface value (signed integer; see FONT_STYLE_DEF record below)
|
||||
# [character] character value (see below)
|
||||
# [embellishment list] if mtefOPT_CHAR_EMBELL is set (embellishments)
|
||||
# The character value itself is represented by one or more values. The presence or absence of these value is indicated by options and appear in this order:
|
||||
# 16-bit integer MTCode value present unless the mtefOPT_CHAR_ENC_NO_MTCODE option is set
|
||||
# 8-bit font position present if the mtefOPT_CHAR_ENC_CHAR_8 option is set
|
||||
# 16-bit integer font position present if the mtefOPT_CHAR_ENC_CHAR_16 option is set
|
||||
# The MTCode value defines the character independent of its font. MTCode is a superset of Unicode and is described in MTCode Encoding Tables. The 8-bit and 16-bit font positions are mutually exclusive but may both be absent. This is the position of the character within its font. Some of the common font encodings are given in Font Encoding Tables.
|
||||
|
||||
require_relative "snapshot"
|
||||
|
||||
module Mathtype3
|
||||
class RecordEmbell < BinData::Record; end
|
||||
class RecordChar < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge typeface mt_code_value embellishment_list)
|
||||
|
||||
endian :little
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
int8 :_typeface
|
||||
|
||||
mtef16 :_mt_code_value
|
||||
|
||||
array :embellishment_list,
|
||||
onlyif: lambda { _options & OPTIONS["xfEMBELL"] > 0 },
|
||||
read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def mt_code_value
|
||||
sprintf("0x%04X", _mt_code_value)
|
||||
end
|
||||
|
||||
def typeface
|
||||
_typeface + 128
|
||||
end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
end
|
||||
end
|
||||
109
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/embell.rb
vendored
Normal file
109
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/embell.rb
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# EMBELL record (6):
|
||||
# Consists of:
|
||||
# record type (6)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [embell] embellishment type
|
||||
# The embellishment types are:
|
||||
|
||||
# value symbol description
|
||||
# 2 emb1DOT over single dot
|
||||
# 3 emb2DOT over double dot
|
||||
# 4 emb3DOT over triple dot
|
||||
# 5 emb1PRIME single prime
|
||||
# 6 emb2PRIME double prime
|
||||
# 7 embBPRIME backwards prime (left of character)
|
||||
# 8 embTILDE tilde
|
||||
# 9 embHAT hat (circumflex)
|
||||
# 10 embNOT diagonal slash through character
|
||||
# 11 embRARROW over right arrow
|
||||
# 12 embLARROW over left arrow
|
||||
# 13 embBARROW over both arrow (left and right)
|
||||
# 14 embR1ARROW over right single-barbed arrow
|
||||
# 15 embL1ARROW over left single-barbed arrow
|
||||
# 16 embMBAR mid-height horizontal bar
|
||||
# 17 embOBAR over-bar
|
||||
# 18 emb3PRIME triple prime
|
||||
# 19 embFROWN over-arc, concave downward
|
||||
# 20 embSMILE over-arc, concave upward
|
||||
# 21 embX_BARS double diagonal bars
|
||||
# 22 embUP_BAR bottom-left to top-right diagonal bar
|
||||
# 23 embDOWN_BAR top-left to bottom-right diagonal bar
|
||||
# 24 emb4DOT over quad dot
|
||||
# 25 embU_1DOT under single dot
|
||||
# 26 embU_2DOT under double dot
|
||||
# 27 embU_3DOT under triple dot
|
||||
# 28 embU_4DOT under quad dot
|
||||
# 29 embU_BAR under bar
|
||||
# 30 embU_TILDE under tilde (~)
|
||||
# 31 embU_FROWN under arc (ends point down)
|
||||
# 32 embU_SMILE under arc (ends point up)
|
||||
# 33 embU_RARROW under right arrow
|
||||
# 34 embU_LARROW under left arrow
|
||||
# 35 embU_BARROW under both arrow (left and right)
|
||||
# 36 embU_R1ARROW under right arrow (1 barb)
|
||||
# 37 embU_L1ARROW under left arrow (1 barb)
|
||||
|
||||
module Mathtype3
|
||||
class RecordEmbell < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge embell)
|
||||
|
||||
EMBELL = {
|
||||
2 => "emb1DOT", # over single dot
|
||||
3 => "emb2DOT", # over double dot
|
||||
4 => "emb3DOT", # over triple dot
|
||||
5 => "emb1PRIME", # single prime
|
||||
6 => "emb2PRIME", # double prime
|
||||
7 => "embBPRIME", # backwards prime (left of character)
|
||||
8 => "embTILDE", # tilde
|
||||
9 => "embHAT", # hat (circumflex)
|
||||
10 => "embNOT", # diagonal slash through character
|
||||
11 => "embRARROW", # over right arrow
|
||||
12 => "embLARROW", # over left arrow
|
||||
13 => "embBARROW", # over both arrow (left and right)
|
||||
14 => "embR1ARROW", # over right single-barbed arrow
|
||||
15 => "embL1ARROW", # over left single-barbed arrow
|
||||
16 => "embMBAR", # mid-height horizontal bar
|
||||
17 => "embOBAR", # over-bar
|
||||
18 => "emb3PRIME", # triple prime
|
||||
19 => "embFROWN", # over-arc, concave downward
|
||||
20 => "embSMILE", # over-arc, concave upward
|
||||
21 => "embX_BARS", # double diagonal bars
|
||||
22 => "embUP_BAR", # bottom-left to top-right diagonal bar
|
||||
23 => "embDOWN_BAR", # top-left to bottom-right diagonal bar
|
||||
24 => "emb4DOT", # over quad dot
|
||||
25 => "embU_1DOT", # under single dot
|
||||
26 => "embU_2DOT", # under double dot
|
||||
27 => "embU_3DOT", # under triple dot
|
||||
28 => "embU_4DOT", # under quad dot
|
||||
29 => "embU_BAR", # under bar
|
||||
30 => "embU_TILDE", # under tilde (~)
|
||||
31 => "embU_FROWN", # under arc (ends point down)
|
||||
32 => "embU_SMILE", # under arc (ends point up)
|
||||
33 => "embU_RARROW", # under right arrow
|
||||
34 => "embU_LARROW", # under left arrow
|
||||
35 => "embU_BARROW", # under both arrow (left and right)
|
||||
36 => "embU_R1ARROW", # under right arrow (1 barb)
|
||||
37 => "embU_L1ARROW", # under left arrow (1 barb)
|
||||
}
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
int8 :_embell
|
||||
int8 :_none
|
||||
|
||||
def embell
|
||||
EMBELL[_embell]
|
||||
end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
end
|
||||
end
|
||||
27
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/font.rb
vendored
Normal file
27
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/font.rb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
module Mathtype3
|
||||
class RecordFont < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options name typeface style)
|
||||
|
||||
endian :little
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
int8 :_typeface
|
||||
|
||||
int8 :style
|
||||
|
||||
stringz :name
|
||||
def typeface
|
||||
_typeface + 128
|
||||
end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
end
|
||||
end
|
||||
47
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/line.rb
vendored
Normal file
47
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/line.rb
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# LINE record (1):
|
||||
# Consists of:
|
||||
# record type (1)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [line spacing] if mtefOPT_LINE_LSPACE is set (16-bit integer)
|
||||
# [RULER record] if mtefOPT_LP_RULER is set
|
||||
# object list of line (single pile, characters and templates, or nothing)
|
||||
# The line spacing value, if present, is the distance between the baseline of
|
||||
# this line and the line above it.
|
||||
|
||||
require_relative "snapshot"
|
||||
|
||||
module Mathtype3
|
||||
class NamedRecord < BinData::Record; end
|
||||
class RecordLine < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge line_spacing ruler object_list)
|
||||
endian :little
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
mtef16 :line_spacing, onlyif: (lambda do
|
||||
_options & OPTIONS["xfLSPACE"] > 0
|
||||
end)
|
||||
|
||||
record_ruler :ruler, onlyif: (lambda do
|
||||
_options & OPTIONS["xfRULER"] > 0
|
||||
end)
|
||||
|
||||
array :object_list,
|
||||
onlyif: lambda { _options & OPTIONS["xfNULL"] == 0 },
|
||||
read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def to_formatted_s(indent = 0); to_s; end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
end
|
||||
end
|
||||
80
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/matrix.rb
vendored
Normal file
80
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/matrix.rb
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# MATRIX record (5):
|
||||
# record type (5)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [valign] vertical alignment of matrix within container
|
||||
# [h_just] horizontal alignment within columns
|
||||
# [v_just] vertical alignment within columns
|
||||
# [rows] number of rows
|
||||
# [cols] number of columns
|
||||
# [row_parts] row partition line types (see below)
|
||||
# [col_parts] column partition line types (see below)
|
||||
# [object list] list of lines, one for each element of the matrix, in order from
|
||||
# left-to-right and top-to-bottom
|
||||
# The values for valign, h_just, and v_just are described in PILE above.
|
||||
|
||||
# The row partition line type list consists of two-bit values for each possible
|
||||
# partition line (one more than the number of rows), rounded out to the nearest
|
||||
# byte. Each value determines the line style of the corresponding partition line
|
||||
# (0 for none, 1 for solid, 2 for dashed, or 3 for dotted). Similarly for the
|
||||
# column partition lines.
|
||||
|
||||
module Mathtype3
|
||||
class RecordMatrix < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge valign h_just v_just rows cols
|
||||
row_parts col_parts object_list)
|
||||
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
int8 :_valign
|
||||
int8 :_h_just
|
||||
int8 :_v_just
|
||||
int8 :rows
|
||||
int8 :cols
|
||||
|
||||
array :row_parts, initial_length: lambda { rows + 1 } do
|
||||
bit nbits: 2
|
||||
end
|
||||
|
||||
bit :_realign_rows, nbits: lambda { realign(rows) }
|
||||
|
||||
array :col_parts, initial_length: lambda { cols + 1 } do
|
||||
bit nbits: 2
|
||||
end
|
||||
|
||||
bit :_realign_cols, nbits: lambda { realign(cols) }
|
||||
|
||||
array :object_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def valign
|
||||
VALIGN[_valign]
|
||||
end
|
||||
|
||||
def h_just
|
||||
HALIGN[_h_just]
|
||||
end
|
||||
|
||||
def v_just
|
||||
VALIGN[_v_just]
|
||||
end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
|
||||
def realign (nparts)
|
||||
offset = (((nparts + 1) * 2) % 8)
|
||||
return offset == 0 ? 0 : (8 - offset)
|
||||
end
|
||||
end
|
||||
end
|
||||
97
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/mtef.rb
vendored
Normal file
97
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/mtef.rb
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
require_relative "line"
|
||||
require_relative "embell"
|
||||
require_relative "char"
|
||||
require_relative "pile"
|
||||
require_relative "tmpl"
|
||||
require_relative "font"
|
||||
require_relative "matrix"
|
||||
require_relative "size"
|
||||
|
||||
module Mathtype3
|
||||
class RecordEnd < Mathtype::RecordEnd; end
|
||||
class RecordNudge < Mathtype::RecordNudge; end
|
||||
class RecordRuler < Mathtype::RecordRuler; end
|
||||
class RecordNudge < Mathtype::RecordNudge; end
|
||||
class RecordFull < Mathtype::RecordFull; end
|
||||
class RecordSub < Mathtype::RecordSub; end
|
||||
class RecordSub2 < Mathtype::RecordSub2; end
|
||||
class RecordSym < Mathtype::RecordSym; end
|
||||
class RecordSubsym < Mathtype::RecordSubsym; end
|
||||
|
||||
HALIGN = Mathtype::HALIGN
|
||||
VALIGN = Mathtype::VALIGN
|
||||
|
||||
RECORD_NAMES = {
|
||||
0 => "end",
|
||||
1 => "slot",
|
||||
2 => "char",
|
||||
3 => "tmpl",
|
||||
4 => "pile",
|
||||
5 => "matrix",
|
||||
6 => "embell",
|
||||
7 => "ruler",
|
||||
8 => "font",
|
||||
9 => "size",
|
||||
10 => "full",
|
||||
11 => "sub",
|
||||
12 => "sub2",
|
||||
13 => "sym",
|
||||
14 => "subsym",
|
||||
}
|
||||
|
||||
OPTIONS = {
|
||||
# value => symbol # description
|
||||
# Option flag values for all equation structure records:
|
||||
"xfLMOVE" => 0x08, # nudge values follow tag
|
||||
# Option flag values for CHAR records:
|
||||
"xfLAUTO" => 0x01, # character is followed by an embellishment list
|
||||
"xfEMBELL" => 0x02, # character starts a function (sin, cos, etc.)
|
||||
# Option flag values for LINE records:
|
||||
"xfNULL" => 0x01, # line is a placeholder only (i.e. not displayed)
|
||||
"xfLSPACE" => 0x04, # line spacing value follows tag
|
||||
# Option flag values for LINE and PILE records:
|
||||
"xfRULER" => 0x02, # RULER record follows LINE or PILE record
|
||||
}
|
||||
|
||||
class Payload < BinData::Choice
|
||||
opt = {:_options => :options}
|
||||
record_end 0 # end is a reserved keyword
|
||||
record_line 1, opt
|
||||
record_char 2, opt
|
||||
record_tmpl 3, opt
|
||||
record_pile 4, opt
|
||||
record_matrix 5, opt
|
||||
record_embell 6, opt
|
||||
record_ruler 7, opt
|
||||
record_font 8, opt
|
||||
record_size 9, opt
|
||||
record_full 10, opt
|
||||
record_sub 11, opt
|
||||
record_sub2 12, opt
|
||||
record_sym 13, opt
|
||||
record_subsym 14, opt
|
||||
end
|
||||
|
||||
## Payload is the most important class to understand.
|
||||
## This abstraction allows recursive formats.
|
||||
## eg. lists can contain lists can contain lists.
|
||||
|
||||
class NamedRecord < BinData::Record
|
||||
bit4 :record_options
|
||||
bit4 :record_type
|
||||
payload :payload, :onlyif => :not_end_tag?, :selection => :record_type, :options => :record_options
|
||||
|
||||
def not_end_tag?
|
||||
record_type != 0
|
||||
end
|
||||
end
|
||||
|
||||
class Equation < Mathtype::Equation
|
||||
EXPOSED_IN_SNAPSHOT = %i(mtef_version platform product product_version
|
||||
product_subversion equation)
|
||||
|
||||
array :equation, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
end
|
||||
end
|
||||
5
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/options.rb
vendored
Normal file
5
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/options.rb
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module Mathtype3
|
||||
class mtef_options < BinData::Record
|
||||
|
||||
end
|
||||
end
|
||||
43
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/pile.rb
vendored
Normal file
43
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/pile.rb
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# PILE record (4):
|
||||
|
||||
# Consists of:
|
||||
|
||||
# record type (4)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [halign] horizontal alignment
|
||||
# [valign] vertical alignment
|
||||
# [[RULER record]] if mtefOPT_LP_RULER is set
|
||||
# [object list] list of lines contained by the pile
|
||||
|
||||
module Mathtype3
|
||||
class RecordPile < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options halign valign object_list)
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
int8 :_halign
|
||||
int8 :_valign
|
||||
|
||||
record_ruler :ruler, onlyif: lambda { _options & OPTIONS["xfRULER"] > 0 }
|
||||
|
||||
array :object_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def halign
|
||||
HALIGN[_halign]
|
||||
end
|
||||
|
||||
def valign
|
||||
VALIGN[_valign]
|
||||
end
|
||||
end
|
||||
end
|
||||
75
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/size.rb
vendored
Normal file
75
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/size.rb
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# SIZE record (9):
|
||||
# Consists of one of the following cases:
|
||||
|
||||
# if lsize < 0 (explicit point size):
|
||||
# record type (9)
|
||||
# 101
|
||||
# -point size (16 bit integer)
|
||||
# else if -128 < dsize < +128:
|
||||
# record type (9)
|
||||
# lsize (typesize)
|
||||
# dsize + 128
|
||||
# else: (large delta)
|
||||
# record type (9)
|
||||
# 100
|
||||
# lsize (typesize)
|
||||
# dsize (16 bit integer)
|
||||
# Sizes in MathType are represented as a pair of values, lsize and dsize. Lsize
|
||||
# stands for "logical size", dsize for "delta size". If it is negative, it is an
|
||||
# explicit point size (in 32nds of a point) negated and dsize is ignored.
|
||||
# Otherwise, lsize is a typesize value and dsize is a delta from that size:
|
||||
# Simple typesizes, without a delta value, are written using the records
|
||||
# described in the next section.
|
||||
|
||||
module Mathtype3
|
||||
class RecordSize < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options lsize dsize point_size)
|
||||
|
||||
endian :little
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :_tag_options, :value => lambda{ _options }
|
||||
|
||||
int8 :_size_select
|
||||
|
||||
mtef16 :_point_size, onlyif: lambda { _size_select == 101 }
|
||||
uint8 :_lsize_large_delta, onlyif: lambda { _size_select == 100 }
|
||||
|
||||
uint8 :_dsize, onlyif: lambda { _size_select != 100 && _size_select != 101 }
|
||||
mtef16 :_dsize_large, onlyif: lambda { _size_select == 100 }
|
||||
|
||||
def dsize
|
||||
case _size_select
|
||||
when 100
|
||||
_dsize_large / 32
|
||||
when 101
|
||||
nil
|
||||
else
|
||||
(_dsize - 128) / 32 # in 32nds of a point
|
||||
end
|
||||
end
|
||||
|
||||
def lsize
|
||||
case _size_select
|
||||
when 100
|
||||
_lsize_large_delta
|
||||
when 101
|
||||
nil
|
||||
else
|
||||
_size_select
|
||||
end
|
||||
end
|
||||
|
||||
def point_size
|
||||
-_point_size if _point_size != 0
|
||||
end
|
||||
|
||||
def options
|
||||
_tag_options
|
||||
end
|
||||
end
|
||||
end
|
||||
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/snapshot.rb
vendored
Normal file
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/snapshot.rb
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
require "bindata"
|
||||
|
||||
module Mathtype3
|
||||
module Snapshot
|
||||
def snapshot
|
||||
snapshot = BinData::Record::Snapshot.new
|
||||
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
|
||||
exposed.each do |name|
|
||||
obj = find_obj_for_name(name)
|
||||
if obj
|
||||
snapshot[name] = obj.snapshot if include_obj?(obj)
|
||||
else
|
||||
snapshot[name] = self.send(name)
|
||||
end
|
||||
end
|
||||
snapshot
|
||||
end
|
||||
end
|
||||
end
|
||||
751
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/tmpl.rb
vendored
Normal file
751
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records3/tmpl.rb
vendored
Normal file
@@ -0,0 +1,751 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# TMPL record (3):
|
||||
|
||||
# Consists of:
|
||||
# record type (3)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [selector] template selector code
|
||||
# [variation] template variation code (1 or 2 bytes; see below)
|
||||
# [options] template-specific options
|
||||
# [subobject list] either a single character (e.g. sigma in a summation) and/or lines
|
||||
# The template selector and variation codes determine the class of the
|
||||
# template and various properties of the template, such as which subobjects
|
||||
# can be deleted by the user (see Templates). The class of a template
|
||||
# determines the order and meaning of each of its subobjects (see Template
|
||||
# subobject order).
|
||||
|
||||
# The variation code may be 1 or 2 bytes long. If the first byte value has the
|
||||
# high bit set (0x80), the next byte is read and combined with the first
|
||||
# according to this formula:
|
||||
|
||||
# variation code = (byte1 & 0x7F) | (byte2 << 8)
|
||||
|
||||
# Template-specific options field only used for integrals & fence templates:
|
||||
# Fence template option field values (fence alignment):
|
||||
# 0 center fence on math axis, place math axis of contents on math axis of
|
||||
# containing line (default);
|
||||
# 1 center fence on contents, place math axis of contents on math axis of
|
||||
# containing line;
|
||||
# 2 center fence on contents, center contents on math axis of containing line.
|
||||
# Warning: the expanding integral property is duplicated in the integral
|
||||
# templates variation codes (see Limit variations). On reading, MathType only
|
||||
# looks at the variation code.
|
||||
|
||||
# Integral template option field values:
|
||||
# 0 fixed-size integral;
|
||||
# 1 the integral expands vertically to fit its contents.
|
||||
|
||||
# Limit variations:
|
||||
|
||||
# The following variation codes apply to all templates whose class is BigOpBoxClass or LimBoxClass:
|
||||
|
||||
# variation bits symbol description
|
||||
# 0x0001 tvBO_LOWER lower limit is present
|
||||
# 0x0002 tvBO_UPPER upper limit is present
|
||||
# 0x0040 tvBO_SUM summation-style limit positions, else integral-style
|
||||
|
||||
# Template selectors and variations:
|
||||
# Fences (parentheses, etc.):
|
||||
# selector symbol description class
|
||||
# 0 tmANGLE angle brackets ParBoxClass
|
||||
# 1 tmPAREN parentheses ParBoxClass
|
||||
# 2 tmBRACE braces (curly brackets) ParBoxClass
|
||||
# 3 tmBRACK square brackets ParBoxClass
|
||||
# 4 tmBAR vertical bars ParBoxClass
|
||||
# 5 tmDBAR double vertical bars ParBoxClass
|
||||
# 6 tmFLOOR floor brackets ParBoxClass
|
||||
# 7 tmCEILING ceiling brackets ParBoxClass
|
||||
# 8 tmOBRACK open (white) brackets ParBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvFENCE_L left fence is present
|
||||
# 0x0002 tvFENCE_R right fence is present
|
||||
# Intervals:
|
||||
# selector symbol description class
|
||||
# 9 tmINTERVAL unmatched brackets and parentheses ParBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0000 tvINTV_LEFT_LP left fence is left parenthesis
|
||||
# 0x0001 tvINTV_LEFT_RP left fence is right parenthesis
|
||||
# 0x0002 tvINTV_LEFT_LB left fence is left bracket
|
||||
# 0x0003 tvINTV_LEFT_RB left fence is right bracket
|
||||
# 0x0000 tvINTV_RIGHT_LP right fence is left parenthesis
|
||||
# 0x0010 tvINTV_RIGHT_RP right fence is right parenthesis
|
||||
# 0x0020 tvINTV_RIGHT_LB right fence is left bracket
|
||||
# 0x0030 tvINTV_RIGHT_RB right fence is right bracket
|
||||
# Radicals (square and nth roots):
|
||||
# selector symbol description class
|
||||
# 10 tmROOT radical RootBoxClass
|
||||
# variations variation symbol description
|
||||
# 0 tvROOT_SQ square root
|
||||
# 1 tvROOT_NTH nth root
|
||||
# Fractions:
|
||||
# selector symbol description class
|
||||
# 11 tmFRACT fractions FracBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvFR_SMALL subscript-size slots (piece fraction)
|
||||
# 0x0002 tvFR_SLASH fraction bar is a slash
|
||||
# 0x0004 tvFR_BASE num. and denom. are baseline aligned
|
||||
# Over and Underbars:
|
||||
# selector symbol description class
|
||||
# 12 tmUBAR underbar BarBoxClass
|
||||
# 13 tmOBAR overbar BarBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvBAR_DOUBLE bar is doubled, else single
|
||||
# Arrows:
|
||||
# selector symbol description class
|
||||
# 14 tmARROW arrow ArroBoxClass
|
||||
# variations variation symbol description
|
||||
# 0x0000 tvAR_SINGLE single arrow
|
||||
# 0x0001 tvAR_DOUBLE double arrow
|
||||
# 0x0002 tvAR_HARPOON harpoon
|
||||
# 0x0004 tvAR_TOP top slot is present
|
||||
# 0x0008 tvAR_BOTTOM bottom slot is present
|
||||
# 0x0010 tvAR_LEFT if single, arrow points left
|
||||
# 0x0020 tvAR_RIGHT if single, arrow points right
|
||||
# 0x0010 tvAR_LOS if double or harpoon, large over small
|
||||
# 0x0020 tvAR_SOL if double or harpoon, small over large
|
||||
# Integrals (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 15 tmINTEG integral BigOpBoxClass
|
||||
# variations variation symbol description
|
||||
# 0x0001 tvINT_1 single integral sign
|
||||
# 0x0002 tvINT_2 double integral sign
|
||||
# 0x0003 tvINT_3 triple integral sign
|
||||
# 0x0004 tvINT_LOOP has loop w/o arrows
|
||||
# 0x0008 tvINT_CW_LOOP has clockwise loop
|
||||
# 0x000C tvINT_CCW_LOOP has counter-clockwise loop
|
||||
# 0x0100 tvINT_EXPAND integral signs expand
|
||||
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 16 tmSUM sum BigOpBoxClass
|
||||
# 17 tmPROD product BigOpBoxClass
|
||||
# 18 tmCOPROD coproduct BigOpBoxClass
|
||||
# 19 tmUNION union BigOpBoxClass
|
||||
# 20 tmINTER intersection BigOpBoxClass
|
||||
# 21 tmINTOP integral-style big operator BigOpBoxClass
|
||||
# 22 tmSUMOP summation-style big operator BigOpBoxClass
|
||||
# Limits (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 23 tmLIM limits LimBoxClass
|
||||
# variations variation symbol description
|
||||
# 0 tvSUBAR single underbar
|
||||
# 1 tvDUBAR double underbar
|
||||
# Horizontal braces and brackets:
|
||||
# selector symbol description class
|
||||
# 24 tmHBRACE horizontal brace HFenceBoxClass
|
||||
# 25 tmHBRACK horizontal bracket HFenceBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvHB_TOP slot is on the top, else on the bottom
|
||||
# Long division:
|
||||
# selector symbol description class
|
||||
# 26 tmLDIV long division LDivBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvLD_UPPER upper slot is present
|
||||
# Subscripts and superscripts:
|
||||
# selector symbol description class
|
||||
# 27 tmSUB subscript ScrBoxClass
|
||||
# 28 tmSUP superscript ScrBoxClass
|
||||
# 29 tmSUBSUP subscript and superscript ScrBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvSU_PRECEDES script precedes scripted item,
|
||||
# else follows
|
||||
# Dirac bra-ket notation:
|
||||
# selector symbol description class
|
||||
# 30 tmDIRAC bra-ket notation DiracBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvDI_LEFT left part is present
|
||||
# 0x0002 tvDI_RIGHT right part is present
|
||||
# Vectors:
|
||||
# selector symbol description class
|
||||
# 31 tmVEC vector HatBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvVE_LEFT arrow points left
|
||||
# 0x0002 tvVE_RIGHT arrow points right
|
||||
# 0x0004 tvVE_UNDER arrow under slot, else over slot
|
||||
# 0x0008 tvVE_HARPOON harpoon
|
||||
# Hats, arcs, tilde, joint status:
|
||||
# selector symbol description class
|
||||
# 32 tmTILDE tilde over characters HatBoxClass
|
||||
# 33 tmHAT hat over characters HatBoxClass
|
||||
# 34 tmARC arc over characters HatBoxClass
|
||||
# 35 tmJSTATUS joint status construct HatBoxClass
|
||||
# Overstrikes (cross-outs):
|
||||
# selector symbol description class
|
||||
# 36 tmSTRIKE overstrike (cross-out) StrikeBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvST_HORIZ line is horizontal, else slashes
|
||||
# 0x0002 tvST_UP if slashes, slash from lower-left to upper-right is present
|
||||
# 0x0004 tvST_DOWN if slashes, slash from upper-left to lower-right is present
|
||||
# Boxes:
|
||||
# selector symbol description class
|
||||
# 37 tmBOX box TBoxBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvBX_ROUND corners are round, else square
|
||||
# 0x0002 tvBX_LEFT left side is present
|
||||
# 0x0004 tvBX_RIGHT right side is present
|
||||
# 0x0008 tvBX_TOP top side is present
|
||||
# 0x0010 tvBX_BOTTOM bottom side is present
|
||||
|
||||
module Mathtype3
|
||||
class RecordTmpl < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options selector variation template_specific_options nudge
|
||||
subobject_list)
|
||||
|
||||
SCRIPT_SELECTOR = {
|
||||
0 => "tmSUP",
|
||||
1 => "tmSUB",
|
||||
2 => "tmSUBSUP"
|
||||
}
|
||||
|
||||
SELECTORS = {
|
||||
0 => "tmANGLE",
|
||||
1 => "tmPAREN",
|
||||
2 => "tmBRACE",
|
||||
3 => "tmBRACK",
|
||||
4 => "tmBAR",
|
||||
5 => "tmDBAR",
|
||||
6 => "tmFLOOR",
|
||||
7 => "tmCEILING",
|
||||
8 => "tmINTERVAL",
|
||||
9 => "tmINTERVAL",
|
||||
10 => "tmINTERVAL",
|
||||
11 => "tmINTERVAL",
|
||||
12 => "tmINTERVAL",
|
||||
13 => "tmROOT",
|
||||
14 => "tmFRACT",
|
||||
15 => "tmSCRIPT",
|
||||
16 => "tmUBAR",
|
||||
17 => "tmOBAR",
|
||||
18 => "tmARROW",
|
||||
19 => "tmARROW",
|
||||
20 => "tmARROW",
|
||||
21 => "tmINTEG",
|
||||
22 => "tmINTEG",
|
||||
23 => "tmINTEG",
|
||||
24 => "tmINTEG",
|
||||
25 => "tmINTEG",
|
||||
26 => "tmINTEG",
|
||||
27 => "tmHBRACE",
|
||||
28 => "tmHBRACE",
|
||||
29 => "tmSUM",
|
||||
30 => "tmSUM",
|
||||
31 => "tmPROD",
|
||||
32 => "tmPROD",
|
||||
33 => "tmCOPROD",
|
||||
34 => "tmCOPROD",
|
||||
35 => "tmUNION",
|
||||
36 => "tmUNION",
|
||||
37 => "tmINTER",
|
||||
38 => "tmINTER",
|
||||
39 => "tmLIM",
|
||||
40 => "tmLDIV",
|
||||
41 => "tmFRACT",
|
||||
42 => "tmINTOP",
|
||||
43 => "tmSUMOP",
|
||||
44 => "tmSCRIPT",
|
||||
45 => "tmDIRAC",
|
||||
46 => "tmVEC",
|
||||
47 => "tmVEC",
|
||||
48 => "tmBOX"
|
||||
}
|
||||
FENCE_BOTH = [
|
||||
"tvFENCE_L",
|
||||
"tvFENCE_R"
|
||||
]
|
||||
|
||||
FENCE_VARIATIONS = {
|
||||
0 => FENCE_BOTH,
|
||||
1 => ["tvFENCE_L"], # left fence is present
|
||||
2 => ["tvFENCE_R"] # right fence is present
|
||||
}
|
||||
|
||||
BAR_VARIATIONS = {
|
||||
0 => ["tvBAR_SINGLE"],
|
||||
1 => ["tvBAR_DOUBLE"] # bar is doubled, else single
|
||||
}
|
||||
|
||||
|
||||
VARIATIONS = {
|
||||
# Fences (parentheses, etc.):
|
||||
0 => FENCE_VARIATIONS,
|
||||
1 => FENCE_VARIATIONS,
|
||||
2 => FENCE_VARIATIONS,
|
||||
3 => FENCE_VARIATIONS,
|
||||
4 => FENCE_VARIATIONS,
|
||||
5 => FENCE_VARIATIONS,
|
||||
6 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
7 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
# Intervals:
|
||||
# 0x0000 => "tvINTV_LEFT_LP", # left fence is left parenthesis
|
||||
# 0x0001 => "tvINTV_LEFT_RP", # left fence is right parenthesis
|
||||
# 0x0002 => "tvINTV_LEFT_LB", # left fence is left bracket
|
||||
# 0x0003 => "tvINTV_LEFT_RB", # left fence is right bracket
|
||||
# 0x0004 => "tvINTV_RIGHT_LP", # right fence is left parenthesis # WARNING: DOCUMENTATION SAYS 0x0000?
|
||||
# 0x0010 => "tvINTV_RIGHT_RP", # right fence is right parenthesis
|
||||
# 0x0020 => "tvINTV_RIGHT_LB", # right fence is left bracket
|
||||
# 0x0030 => "tvINTV_RIGHT_RB", # right fence is right bracket
|
||||
# Replaced above to match MathML translator
|
||||
8 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
9 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
10 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
11 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
12 => {
|
||||
0 => FENCE_BOTH
|
||||
},
|
||||
|
||||
# Radicals (square and nth roots):
|
||||
13 => {
|
||||
0 => ["tvROOT_SQ"], # square root
|
||||
1 => ["tvROOT_NTH"] # nth root
|
||||
},
|
||||
|
||||
# Fractions:
|
||||
14 => {
|
||||
#TODO: full-size slots?
|
||||
0 => ["tvFR_FULL"],
|
||||
1 => ["tvFR_SMALL"] # subscript-size slots (piece fraction)
|
||||
},
|
||||
|
||||
# Over and Underbars:
|
||||
16 => BAR_VARIATIONS,
|
||||
17 => BAR_VARIATIONS,
|
||||
|
||||
# Arrows:
|
||||
18 => {
|
||||
0 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_LEFT",
|
||||
"tvAR_TOP",
|
||||
], # single arrow
|
||||
1 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_LEFT",
|
||||
"tvAR_BOTTOM"
|
||||
]
|
||||
},
|
||||
|
||||
19 => {
|
||||
0 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_RIGHT",
|
||||
"tvAR_TOP"
|
||||
],
|
||||
1 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_RIGHT",
|
||||
"tvAR_TOP"
|
||||
]
|
||||
},
|
||||
|
||||
20 => {
|
||||
0 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_RIGHT",
|
||||
"tvAR_LEFT",
|
||||
"tvAR_TOP"
|
||||
],
|
||||
1 => [
|
||||
"tvAR_SINGLE",
|
||||
"tvAR_RIGHT",
|
||||
"tvAR_LEFT",
|
||||
"tvAR_TOP"
|
||||
]
|
||||
},
|
||||
|
||||
# Integrals (see Limit Variations):
|
||||
21 => {
|
||||
0 => [
|
||||
"tvINT_1" # single integral sign
|
||||
],
|
||||
1 => [
|
||||
"tvINT_1",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
2 => [
|
||||
"tvINT_1",
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
],
|
||||
3 => [
|
||||
"tvINT_1",
|
||||
],
|
||||
4 => [
|
||||
"tvINT_1",
|
||||
"tvBO_LOWER"
|
||||
]
|
||||
},
|
||||
|
||||
22 => {
|
||||
0 => [
|
||||
"tvINT_2" # double integral sign
|
||||
],
|
||||
1 => [
|
||||
"tvINT_2",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
2 => [
|
||||
"tvINT_2",
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
],
|
||||
3 => [
|
||||
"tvINT_2",
|
||||
],
|
||||
4 => [
|
||||
"tvINT_2",
|
||||
"tvBO_LOWER"
|
||||
]
|
||||
},
|
||||
|
||||
23 => {
|
||||
0 => [
|
||||
"tvINT_3" # triple integral sign
|
||||
],
|
||||
1 => [
|
||||
"tvINT_3",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
2 => [
|
||||
"tvINT_3",
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
],
|
||||
3 => ["tvINT_3"],
|
||||
4 => [
|
||||
"tvINT_3",
|
||||
"tvBO_LOWER"
|
||||
]
|
||||
},
|
||||
|
||||
24 => {
|
||||
0 => [
|
||||
"tvINT_1", # single integral sign
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
],
|
||||
1 => [
|
||||
"tvINT_1",
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
2 => [
|
||||
"tvINT_1",
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER",
|
||||
]
|
||||
},
|
||||
|
||||
25 => {
|
||||
0 => [
|
||||
"tvINT_2", # double integral sign
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER",
|
||||
],
|
||||
1 => [
|
||||
"tvINT_2",
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER"
|
||||
]
|
||||
},
|
||||
|
||||
26 => {
|
||||
0 => [
|
||||
"tvINT_3", # triple integral sign
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
1 => [
|
||||
"tvINT_3",
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER"
|
||||
]
|
||||
},
|
||||
|
||||
# Horizontal braces and brackets:
|
||||
27 => {
|
||||
0 => ["tvHB_TOP"] # slot is on the top
|
||||
},
|
||||
|
||||
28 => {
|
||||
# default on the bottom
|
||||
0 => ["tvHB_BOT"]
|
||||
},
|
||||
|
||||
# Sums, products, coproducts, unions, intersections, etc.:
|
||||
29 => {
|
||||
0 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_SUM" # summation-style limit positions,
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER", # upper limit is present
|
||||
"tvBO_SUM"
|
||||
],
|
||||
2 => ["tvBO_SUM"]
|
||||
},
|
||||
|
||||
30 => {
|
||||
0 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
31 => {
|
||||
0 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_SUM" # summation-style limit positions,
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER", # upper limit is present
|
||||
"tvBO_SUM"
|
||||
],
|
||||
2 => ["tvBO_SUM"]
|
||||
},
|
||||
|
||||
32 => {
|
||||
0 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
33 => {
|
||||
0 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_SUM" # summation-style limit positions,
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER", # upper limit is present
|
||||
"tvBO_SUM"
|
||||
],
|
||||
2 => ["tvBO_SUM"]
|
||||
},
|
||||
|
||||
34 => {
|
||||
0 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
35 => {
|
||||
0 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_SUM" # summation-style limit positions,
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER", # upper limit is present
|
||||
"tvBO_SUM"
|
||||
],
|
||||
2 => ["tvBO_SUM"]
|
||||
},
|
||||
|
||||
36 => {
|
||||
0 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
37 => {
|
||||
0 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_SUM" # summation-style limit positions,
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER", # upper limit is present
|
||||
"tvBO_SUM"
|
||||
],
|
||||
2 => ["tvBO_SUM"]
|
||||
},
|
||||
|
||||
38 => {
|
||||
0 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
39 => {
|
||||
0 => [
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
],
|
||||
1 => [
|
||||
"tvBO_LOWER" # lower limit is present
|
||||
],
|
||||
2 => [
|
||||
"tvBO_LOWER", # lower limit is present
|
||||
"tvBO_UPPER" # upper limit is present
|
||||
]
|
||||
},
|
||||
|
||||
# Long division:
|
||||
40 => {
|
||||
0 => ["tvLD_UPPER"]
|
||||
},
|
||||
|
||||
# Frac with Slash:
|
||||
41 => {
|
||||
0 => ["tvFR_SLASH"],
|
||||
1 => [
|
||||
"tvFR_SLASH",
|
||||
"tvFR_BASE"
|
||||
],
|
||||
2 => [
|
||||
"tvFR_SLASH",
|
||||
"tvFR_SMALL"
|
||||
]
|
||||
},
|
||||
|
||||
# Big integral-style operators:
|
||||
42 => {
|
||||
0 => ["tvBO_LOWER"],
|
||||
1 => ["tvBO_UPPER"],
|
||||
2 => [
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
]
|
||||
},
|
||||
|
||||
# Big summation-style operators:
|
||||
43 => {
|
||||
0 => [
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER"
|
||||
],
|
||||
1 => [
|
||||
"tvBO_SUM",
|
||||
"tvBO_UPPER"
|
||||
],
|
||||
2 => [
|
||||
"tvBO_SUM",
|
||||
"tvBO_LOWER",
|
||||
"tvBO_UPPER"
|
||||
]
|
||||
},
|
||||
|
||||
44 => {
|
||||
0 => ["tvSU_PRECEDES"],
|
||||
1 => ["tvSU_PRECEDES"],
|
||||
2 => ["tvSU_PRECEDES"],
|
||||
3 => ["tvSU_PRECEDES"]
|
||||
},
|
||||
|
||||
45 => {
|
||||
0 => [
|
||||
"tvDI_LEFT",
|
||||
"tvDI_RIGHT"
|
||||
],
|
||||
1 => ["tvDI_LEFT"],
|
||||
2 => ["tvDI_RIGHT"]
|
||||
},
|
||||
|
||||
46 => {
|
||||
0 => [
|
||||
"tvVE_UNDER",
|
||||
"tvVE_LEFT"
|
||||
],
|
||||
1 => [
|
||||
"tvVE_UNDER",
|
||||
"tvVE_RIGHT"
|
||||
],
|
||||
2 => [
|
||||
"tvVE_UNDER",
|
||||
"tvVE_LEFT",
|
||||
"tvVE_RIGHT"
|
||||
],
|
||||
},
|
||||
|
||||
47 => {
|
||||
0 => ["tvVE_LEFT"],
|
||||
1 => ["tvVE_RIGHT"],
|
||||
2 => [
|
||||
"tvVE_LEFT",
|
||||
"tvVE_RIGHT"
|
||||
],
|
||||
},
|
||||
|
||||
# Selectors with empty variation:
|
||||
15 => {0=>nil,1=>nil,2=>nil,3=>nil,4=>nil},
|
||||
48 => {0=>nil,1=>nil,2=>nil,3=>nil,4=>nil}
|
||||
}
|
||||
|
||||
|
||||
# When options overlap in the binary space, ordinary bitmasks
|
||||
# are not the correct tool to use for detection. We use digit
|
||||
# position and presence instead.
|
||||
|
||||
DIGIT_MODE_VARIATIONS = [9]
|
||||
|
||||
# Top-level keys are template identifiers, defined in TEMPLATES.
|
||||
# Second-level keys are bits for certain variations, negative keys mean
|
||||
# that the variation is present if the bit is absent.
|
||||
|
||||
mandatory_parameter :_options
|
||||
|
||||
virtual :options, :value => lambda{ _options }
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
|
||||
|
||||
int8 :_selector
|
||||
|
||||
int8 :_variation
|
||||
|
||||
int8 :template_specific_options
|
||||
|
||||
array :subobject_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def selector
|
||||
if is_script _selector
|
||||
#SCRIPT has to be handled separate
|
||||
SCRIPT_SELECTOR[_variation]
|
||||
else
|
||||
SELECTORS[_selector]
|
||||
end
|
||||
end
|
||||
def is_script(sel)
|
||||
return sel == 44 || sel == 15
|
||||
end
|
||||
def variation
|
||||
VARIATIONS[_selector][_variation]
|
||||
end
|
||||
end
|
||||
end
|
||||
130
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/char.rb
vendored
Normal file
130
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/char.rb
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
# CHAR record (2):
|
||||
# Consists of:
|
||||
# record type (2)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [typeface] typeface value (signed integer; see FONT_STYLE_DEF record below)
|
||||
# [character] character value (see below)
|
||||
# [embellishment list] if mtefOPT_CHAR_EMBELL is set (embellishments)
|
||||
# The character value itself is represented by one or more values. The presence or absence of these value is indicated by options and appear in this order:
|
||||
# 16-bit integer MTCode value present unless the mtefOPT_CHAR_ENC_NO_MTCODE option is set
|
||||
# 8-bit font position present if the mtefOPT_CHAR_ENC_CHAR_8 option is set
|
||||
# 16-bit integer font position present if the mtefOPT_CHAR_ENC_CHAR_16 option is set
|
||||
# The MTCode value defines the character independent of its font. MTCode is a superset of Unicode and is described in MTCode Encoding Tables. The 8-bit and 16-bit font positions are mutually exclusive but may both be absent. This is the position of the character within its font. Some of the common font encodings are given in Font Encoding Tables.
|
||||
|
||||
require_relative "snapshot"
|
||||
|
||||
module Mathtype5
|
||||
class RecordEmbell < BinData::Record; end
|
||||
class RecordChar < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(nudge typeface mt_code_value options font_position
|
||||
variation embellishment_list)
|
||||
|
||||
endian :little
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
int8 :_typeface
|
||||
|
||||
mtef16 :_mt_code_value, onlyif: (lambda do
|
||||
options & OPTIONS["mtefOPT_CHAR_ENC_NO_MTCODE"] == 0
|
||||
end)
|
||||
|
||||
font_position_choice = lambda do
|
||||
char_enc_char_8 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_8"] > 0
|
||||
char_enc_char_16 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_16"] > 0
|
||||
|
||||
if char_enc_char_8
|
||||
8
|
||||
elsif char_enc_char_16
|
||||
16
|
||||
end
|
||||
end
|
||||
|
||||
font_position_present = lambda do
|
||||
char_enc_char_8 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_8"] > 0
|
||||
char_enc_char_16 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_16"] > 0
|
||||
|
||||
char_enc_char_8 || char_enc_char_16
|
||||
end
|
||||
|
||||
choice :font_position,
|
||||
selection: font_position_choice,
|
||||
onlyif: font_position_present do
|
||||
|
||||
uint8 8
|
||||
mtef16 16
|
||||
end
|
||||
|
||||
array :embellishment_list,
|
||||
onlyif: lambda { options & OPTIONS["mtefOPT_CHAR_EMBELL"] > 0 },
|
||||
read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def mt_code_value
|
||||
sprintf("0x%04X", _mt_code_value)
|
||||
end
|
||||
|
||||
def typeface
|
||||
_typeface + 128
|
||||
end
|
||||
|
||||
def variation
|
||||
# Check for overrides from eqn_prefs
|
||||
# typeface < 128 implies it's a style index
|
||||
if typeface < 128
|
||||
prefs = find_eqn_prefs
|
||||
# Check if styles exist and index is valid
|
||||
# Note: styles is a BinData::Array, access via [] works
|
||||
if prefs && prefs.respond_to?(:styles)
|
||||
# BinData arrays might be lazy or behave differently, but generally index access works
|
||||
# We need to ensure we don't access out of bounds
|
||||
if typeface < prefs.styles.length
|
||||
style = prefs.styles[typeface]
|
||||
# Check italic bit (bit 1, value 2). If 0, it is upright (textmode)
|
||||
if (style.font_style & 2) == 0
|
||||
return "textmode"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case typeface
|
||||
when 1, 9, 10
|
||||
"textmode"
|
||||
else
|
||||
"mathmode"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_eqn_prefs
|
||||
# Traverse up parent chain to find the equation array
|
||||
# Hierarchy: RecordChar -> Payload (Choice) -> NamedRecord -> Array -> Equation
|
||||
node = parent
|
||||
while node
|
||||
# BinData::Array usually behaves like an Array
|
||||
if node.respond_to?(:find) && node.respond_to?(:each)
|
||||
# We suspect this is the record list.
|
||||
# Verify if it contains records with record_type
|
||||
# We can check the first element or just try to find
|
||||
# Note: We need to use safe navigation if elements aren't populated yet?
|
||||
# But parsing is done sequentially. If we are in Char, EqnPrefs (usually earlier) should be readable.
|
||||
|
||||
# Search for record_type 18 (eqn_prefs)
|
||||
# Element is NamedRecord
|
||||
prefs_record = node.find { |r| r.respond_to?(:record_type) && r.record_type == 18 }
|
||||
if prefs_record
|
||||
return prefs_record.payload
|
||||
end
|
||||
end
|
||||
node = node.parent
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
13
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/color.rb
vendored
Normal file
13
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/color.rb
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# COLOR records (15):
|
||||
# Consists of:
|
||||
# record type (15)
|
||||
# [color_def_index] index of corresponding COLOR_DEF record (unsigned integer)
|
||||
# The appearance of this record in the stream indicates that all following
|
||||
# equation records (until the next COLOR record) have the color defined by the
|
||||
# indicated COLOR_DEF record.
|
||||
|
||||
module Mathtype5
|
||||
class RecordColor < BinData::Record
|
||||
mt_uint :color_def_index
|
||||
end
|
||||
end
|
||||
37
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/color_def.rb
vendored
Normal file
37
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/color_def.rb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# COLOR_DEF records (16):
|
||||
# Consists of:
|
||||
# record type (16)
|
||||
# [options] model is RGB unless mtefCOLOR_CMYK bit is set; type is process
|
||||
# unless mtefCOLOR_SPOT bit is set; color is unnamed unless mtefCOLOR_NAME bit is set
|
||||
# [color values] if RGB, 3 values (red, green, blue); if CMYK, 4 values (cyan,
|
||||
# magenta, yellow, black); see below for details
|
||||
# [name] null-terminated color name appears only if mtefCOLOR_NAME is set
|
||||
# This record defines a color (see Definition records). Each color value is
|
||||
# written as a 16-bit integer that ranges between 0 and 1000 where 0 is the
|
||||
# absence of the color and 1000 is a fully saturated color. So, an RGB color
|
||||
# definition for black has all three components at 0.
|
||||
|
||||
module Mathtype5
|
||||
class RecordColorDef < BinData::Record
|
||||
endian :little
|
||||
|
||||
int8 :options
|
||||
|
||||
struct :rgb, onlyif: lambda { options & OPTIONS["mtefCOLOR_CMYK"] == 0 } do
|
||||
mtef16 :r
|
||||
mtef16 :g
|
||||
mtef16 :b
|
||||
end
|
||||
|
||||
struct :cmyk, onlyif: lambda { options & OPTIONS["mtefCOLOR_CMYK"] > 0} do
|
||||
mtef16 :c
|
||||
mtef16 :m
|
||||
mtef16 :y
|
||||
mtef16 :k
|
||||
end
|
||||
|
||||
stringz :color_name, onlyif: (lambda do
|
||||
options & OPTIONS["mtefCOLOR_NAME"] > 0
|
||||
end)
|
||||
end
|
||||
end
|
||||
15
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/comment.rb
vendored
Normal file
15
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/comment.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# If the record type is 100 or greater, it represents a record that will be
|
||||
# defined in a future version of MTEF. For now, readers can assume that an
|
||||
# unsigned integer follows the record type and is the number of bytes following
|
||||
# it in the record (i.e. it doesn't include the record type and length). This
|
||||
# makes it easy for software that reads MTEF to skip these records. Although it
|
||||
# might be handy if all records had such a length value, it will only be present
|
||||
# on future expansion records (i.e. those with record types >= 100).
|
||||
|
||||
module Mathtype5
|
||||
class RecordMtComment < BinData::Record
|
||||
mt_uint :comment_length
|
||||
stringz :comment_type
|
||||
stringz :comment_data
|
||||
end
|
||||
end
|
||||
103
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/embell.rb
vendored
Normal file
103
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/embell.rb
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# EMBELL record (6):
|
||||
# Consists of:
|
||||
# record type (6)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [embell] embellishment type
|
||||
# The embellishment types are:
|
||||
|
||||
# value symbol description
|
||||
# 2 emb1DOT over single dot
|
||||
# 3 emb2DOT over double dot
|
||||
# 4 emb3DOT over triple dot
|
||||
# 5 emb1PRIME single prime
|
||||
# 6 emb2PRIME double prime
|
||||
# 7 embBPRIME backwards prime (left of character)
|
||||
# 8 embTILDE tilde
|
||||
# 9 embHAT hat (circumflex)
|
||||
# 10 embNOT diagonal slash through character
|
||||
# 11 embRARROW over right arrow
|
||||
# 12 embLARROW over left arrow
|
||||
# 13 embBARROW over both arrow (left and right)
|
||||
# 14 embR1ARROW over right single-barbed arrow
|
||||
# 15 embL1ARROW over left single-barbed arrow
|
||||
# 16 embMBAR mid-height horizontal bar
|
||||
# 17 embOBAR over-bar
|
||||
# 18 emb3PRIME triple prime
|
||||
# 19 embFROWN over-arc, concave downward
|
||||
# 20 embSMILE over-arc, concave upward
|
||||
# 21 embX_BARS double diagonal bars
|
||||
# 22 embUP_BAR bottom-left to top-right diagonal bar
|
||||
# 23 embDOWN_BAR top-left to bottom-right diagonal bar
|
||||
# 24 emb4DOT over quad dot
|
||||
# 25 embU_1DOT under single dot
|
||||
# 26 embU_2DOT under double dot
|
||||
# 27 embU_3DOT under triple dot
|
||||
# 28 embU_4DOT under quad dot
|
||||
# 29 embU_BAR under bar
|
||||
# 30 embU_TILDE under tilde (~)
|
||||
# 31 embU_FROWN under arc (ends point down)
|
||||
# 32 embU_SMILE under arc (ends point up)
|
||||
# 33 embU_RARROW under right arrow
|
||||
# 34 embU_LARROW under left arrow
|
||||
# 35 embU_BARROW under both arrow (left and right)
|
||||
# 36 embU_R1ARROW under right arrow (1 barb)
|
||||
# 37 embU_L1ARROW under left arrow (1 barb)
|
||||
|
||||
module Mathtype5
|
||||
class RecordEmbell < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge embell)
|
||||
|
||||
EMBELL = {
|
||||
2 => "emb1DOT", # over single dot
|
||||
3 => "emb2DOT", # over double dot
|
||||
4 => "emb3DOT", # over triple dot
|
||||
5 => "emb1PRIME", # single prime
|
||||
6 => "emb2PRIME", # double prime
|
||||
7 => "embBPRIME", # backwards prime (left of character)
|
||||
8 => "embTILDE", # tilde
|
||||
9 => "embHAT", # hat (circumflex)
|
||||
10 => "embNOT", # diagonal slash through character
|
||||
11 => "embRARROW", # over right arrow
|
||||
12 => "embLARROW", # over left arrow
|
||||
13 => "embBARROW", # over both arrow (left and right)
|
||||
14 => "embR1ARROW", # over right single-barbed arrow
|
||||
15 => "embL1ARROW", # over left single-barbed arrow
|
||||
16 => "embMBAR", # mid-height horizontal bar
|
||||
17 => "embOBAR", # over-bar
|
||||
18 => "emb3PRIME", # triple prime
|
||||
19 => "embFROWN", # over-arc, concave downward
|
||||
20 => "embSMILE", # over-arc, concave upward
|
||||
21 => "embX_BARS", # double diagonal bars
|
||||
22 => "embUP_BAR", # bottom-left to top-right diagonal bar
|
||||
23 => "embDOWN_BAR", # top-left to bottom-right diagonal bar
|
||||
24 => "emb4DOT", # over quad dot
|
||||
25 => "embU_1DOT", # under single dot
|
||||
26 => "embU_2DOT", # under double dot
|
||||
27 => "embU_3DOT", # under triple dot
|
||||
28 => "embU_4DOT", # under quad dot
|
||||
29 => "embU_BAR", # under bar
|
||||
30 => "embU_TILDE", # under tilde (~)
|
||||
31 => "embU_FROWN", # under arc (ends point down)
|
||||
32 => "embU_SMILE", # under arc (ends point up)
|
||||
33 => "embU_RARROW", # under right arrow
|
||||
34 => "embU_LARROW", # under left arrow
|
||||
35 => "embU_BARROW", # under both arrow (left and right)
|
||||
36 => "embU_R1ARROW", # under right arrow (1 barb)
|
||||
37 => "embU_L1ARROW", # under left arrow (1 barb)
|
||||
}
|
||||
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
int8 :_embell
|
||||
|
||||
def embell
|
||||
EMBELL[_embell]
|
||||
end
|
||||
end
|
||||
end
|
||||
22
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/encoding_def.rb
vendored
Normal file
22
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/encoding_def.rb
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# ENCODING_DEF records (19):
|
||||
# Consists of:
|
||||
# record type (19)
|
||||
# [name] null-terminated encoding name
|
||||
# This record defines (see Definition records) a font encoding and is referred
|
||||
# to by a FONT_DEF record. In order to reduce the size of the MTEF stream,
|
||||
# the following 4 encodings are predefined:
|
||||
|
||||
# ENCODING_DEF index encoding name
|
||||
# 1 MTCode
|
||||
# 2 Unknown
|
||||
# 3 Symbol
|
||||
# 4 MTExtra
|
||||
# This means that the first ENCODING_DEF record in the MTEF stream is considered
|
||||
# to have an index of 5. See Extending MathType's Font and Character Information
|
||||
# and MathType's Character Encodings for more information on font encodings.
|
||||
|
||||
module Mathtype5
|
||||
class RecordEncodingDef < BinData::Record
|
||||
stringz :name
|
||||
end
|
||||
end
|
||||
44
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/eqn_prefs.rb
vendored
Normal file
44
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/eqn_prefs.rb
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# EQN_PREFS records (18):
|
||||
|
||||
# Consists of:
|
||||
|
||||
# record type (18)
|
||||
# [options] none defined in this version of MTEF
|
||||
# [sizes] dimension array for typesize definitions
|
||||
# [spaces] dimension array for spacing definitions (see below)
|
||||
# [styles] array of style definitions (see below)
|
||||
# When reading arrays, the number of values may be less than or greater than
|
||||
# expected. MTEF readers should be driven by the array count.
|
||||
# If the array is shorter than expected, assume defaults for the missing values.
|
||||
# If the array is longer than expected, the extra values must be skipped to stay
|
||||
# in sync with the MTEF stream.
|
||||
|
||||
module Mathtype5
|
||||
class Entry < BinData::Record
|
||||
bit4 :unit
|
||||
array :nibbles, read_until: lambda { element == 0xF } do
|
||||
bit4
|
||||
end
|
||||
end
|
||||
|
||||
class RecordEqnPrefs < BinData::Record
|
||||
int8 :options
|
||||
|
||||
int8 :sizes_count
|
||||
array :sizes, initial_length: :sizes_count do
|
||||
entry
|
||||
end
|
||||
|
||||
int8 :spaces_count
|
||||
array :spaces, initial_length: :spaces_count do
|
||||
entry
|
||||
end
|
||||
|
||||
int8 :styles_count
|
||||
array :styles, initial_length: :styles_count do
|
||||
int8 :font_def
|
||||
int8 :font_style, onlyif: lambda { font_def != 0x00 }
|
||||
end
|
||||
resume_byte_alignment
|
||||
end
|
||||
end
|
||||
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/font_def.rb
vendored
Normal file
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/font_def.rb
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# FONT_DEF records (17):
|
||||
# Consists of:
|
||||
# record type (17)
|
||||
# [enc_def_index] index of corresponding ENCODING_DEF record (unsigned integer)
|
||||
# [name] null-terminated font name
|
||||
# This record associates an font encoding with a font name. See Definition
|
||||
# records.
|
||||
|
||||
module Mathtype5
|
||||
class RecordFontDef < BinData::Record
|
||||
mt_uint :enc_def_index
|
||||
stringz :font_name
|
||||
end
|
||||
end
|
||||
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/font_style_def.rb
vendored
Normal file
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/font_style_def.rb
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# FONT_STYLE_DEF record (8):
|
||||
# Consists of:
|
||||
# record type (8)
|
||||
# [font_def_index] index of mtefFONT_DEF record (unsigned integer)
|
||||
# [char_style] character style bits
|
||||
# This record associates a character style with a font. See Definition records.
|
||||
|
||||
module Mathtype5
|
||||
class RecordFontStyleDef < BinData::Record
|
||||
mt_uint :font_def_index
|
||||
|
||||
int8 :char_style
|
||||
end
|
||||
end
|
||||
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/future.rb
vendored
Normal file
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/future.rb
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# If the record type is 100 or greater, it represents a record that will be
|
||||
# defined in a future version of MTEF. For now, readers can assume that an
|
||||
# unsigned integer follows the record type and is the number of bytes following
|
||||
# it in the record (i.e. it doesn't include the record type and length). This
|
||||
# makes it easy for software that reads MTEF to skip these records. Although it
|
||||
# might be handy if all records had such a length value, it will only be present
|
||||
# on future expansion records (i.e. those with record types >= 100).
|
||||
|
||||
module Mathtype5
|
||||
class RecordFuture < BinData::Record
|
||||
uint8 :skip
|
||||
skip length: :skip
|
||||
end
|
||||
end
|
||||
36
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/line.rb
vendored
Normal file
36
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/line.rb
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# LINE record (1):
|
||||
# Consists of:
|
||||
# record type (1)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [line spacing] if mtefOPT_LINE_LSPACE is set (16-bit integer)
|
||||
# [RULER record] if mtefOPT_LP_RULER is set
|
||||
# object list of line (single pile, characters and templates, or nothing)
|
||||
# The line spacing value, if present, is the distance between the baseline of
|
||||
# this line and the line above it.
|
||||
|
||||
module Mathtype5
|
||||
class NamedRecord < BinData::Record; end
|
||||
class RecordLine < BinData::Record
|
||||
endian :little
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
mtef16 :line_spacing, onlyif: (lambda do
|
||||
options & OPTIONS["mtefOPT_LINE_LSPACE"] > 0
|
||||
end)
|
||||
|
||||
record_ruler :ruler, onlyif: (lambda do
|
||||
options & OPTIONS["mtefOPT_LP_RULER"] > 0
|
||||
end)
|
||||
|
||||
array :object_list,
|
||||
onlyif: lambda { options & OPTIONS["mtefOPT_LINE_NULL"] == 0 },
|
||||
read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def to_formatted_s(indent = 0); to_s; end
|
||||
end
|
||||
end
|
||||
77
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/matrix.rb
vendored
Normal file
77
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/matrix.rb
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# MATRIX record (5):
|
||||
# record type (5)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [valign] vertical alignment of matrix within container
|
||||
# [h_just] horizontal alignment within columns
|
||||
# [v_just] vertical alignment within columns
|
||||
# [rows] number of rows
|
||||
# [cols] number of columns
|
||||
# [row_parts] row partition line types (see below)
|
||||
# [col_parts] column partition line types (see below)
|
||||
# [object list] list of lines, one for each element of the matrix, in order from
|
||||
# left-to-right and top-to-bottom
|
||||
# The values for valign, h_just, and v_just are described in PILE above.
|
||||
|
||||
# The row partition line type list consists of two-bit values for each possible
|
||||
# partition line (one more than the number of rows), rounded out to the nearest
|
||||
# byte. Each value determines the line style of the corresponding partition line
|
||||
# (0 for none, 1 for solid, 2 for dashed, or 3 for dotted). Similarly for the
|
||||
# column partition lines.
|
||||
|
||||
module Mathtype5
|
||||
class RecordMatrix < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options nudge valign h_just v_just rows cols
|
||||
row_parts col_parts object_list)
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
int8 :_valign
|
||||
int8 :_h_just
|
||||
int8 :_v_just
|
||||
int8 :rows
|
||||
int8 :cols
|
||||
|
||||
array :_row_parts, initial_length: lambda { (rows + 4) / 4 } do
|
||||
matrix_line_byte
|
||||
end
|
||||
|
||||
array :_col_parts, initial_length: lambda { (cols + 4) / 4 } do
|
||||
matrix_line_byte
|
||||
end
|
||||
|
||||
array :object_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def valign
|
||||
VALIGN[_valign]
|
||||
end
|
||||
|
||||
def h_just
|
||||
HALIGN[_h_just]
|
||||
end
|
||||
|
||||
def v_just
|
||||
VALIGN[_v_just]
|
||||
end
|
||||
|
||||
def truncate_parts parts, cnt
|
||||
full_parts = cnt / 4
|
||||
all = [parts.first(full_parts), parts.last]
|
||||
all.flatten[0 .. (full_parts*4 + cnt%4)]
|
||||
end
|
||||
|
||||
def row_parts
|
||||
truncate_parts _row_parts, rows
|
||||
end
|
||||
|
||||
def col_parts
|
||||
truncate_parts _col_parts, cols
|
||||
end
|
||||
end
|
||||
end
|
||||
134
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/mtef.rb
vendored
Normal file
134
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/mtef.rb
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
require_relative 'mtuint'
|
||||
|
||||
require_relative "line"
|
||||
require_relative "embell"
|
||||
require_relative "char"
|
||||
require_relative "pile"
|
||||
require_relative "tmpl"
|
||||
require_relative "eqn_prefs"
|
||||
require_relative "font_def"
|
||||
require_relative "font_style_def"
|
||||
require_relative "matrix"
|
||||
require_relative "encoding_def"
|
||||
require_relative "embell"
|
||||
require_relative "size"
|
||||
require_relative "color"
|
||||
require_relative "color_def"
|
||||
require_relative "future"
|
||||
require_relative "comment"
|
||||
|
||||
module Mathtype5
|
||||
class RecordEnd < Mathtype::RecordEnd; end
|
||||
class RecordNudge < Mathtype::RecordNudge; end
|
||||
class RecordRuler < Mathtype::RecordRuler; end
|
||||
class RecordNudge < Mathtype::RecordNudge; end
|
||||
class RecordFull < Mathtype::RecordFull; end
|
||||
class RecordSub < Mathtype::RecordSub; end
|
||||
class RecordSub2 < Mathtype::RecordSub2; end
|
||||
class RecordSym < Mathtype::RecordSym; end
|
||||
class RecordSubsym < Mathtype::RecordSubsym; end
|
||||
|
||||
HALIGN = Mathtype::HALIGN
|
||||
VALIGN = Mathtype::VALIGN
|
||||
|
||||
RECORD_NAMES = {
|
||||
0 => "end",
|
||||
1 => "slot",
|
||||
2 => "char",
|
||||
3 => "tmpl",
|
||||
4 => "pile",
|
||||
5 => "matrix",
|
||||
6 => "embell",
|
||||
7 => "ruler",
|
||||
8 => "font_style_def",
|
||||
9 => "size",
|
||||
10 => "full",
|
||||
11 => "sub",
|
||||
12 => "sub2",
|
||||
13 => "sym",
|
||||
14 => "subsym",
|
||||
15 => "color",
|
||||
16 => "color_def",
|
||||
17 => "font_def",
|
||||
18 => "eqn_prefs",
|
||||
19 => "encoding_def",
|
||||
100 => "future",
|
||||
102 => "mt_comment"
|
||||
}
|
||||
|
||||
OPTIONS = {
|
||||
# value => symbol # description
|
||||
# Option flag values for all equation structure records:
|
||||
"mtefOPT_NUDGE" => 0x08, # nudge values follow tag
|
||||
# Option flag values for CHAR records:
|
||||
"mtefOPT_CHAR_EMBELL" => 0x01, # character is followed by an embellishment list
|
||||
"mtefOPT_CHAR_FUNC_START" => 0x02, # character starts a function (sin, cos, etc.)
|
||||
"mtefOPT_CHAR_ENC_CHAR_8" => 0x04, # character is written with an 8-bit encoded value
|
||||
"mtefOPT_CHAR_ENC_CHAR_16" => 0x10, # character is written with an 16-bit encoded value
|
||||
"mtefOPT_CHAR_ENC_NO_MTCODE" => 0x20, # character is written without an 16-bit MTCode value
|
||||
# Option flag values for LINE records:
|
||||
"mtefOPT_LINE_NULL" => 0x01, # line is a placeholder only (i.e. not displayed)
|
||||
"mtefOPT_LINE_LSPACE" => 0x04, # line spacing value follows tag
|
||||
# Option flag values for LINE and PILE records:
|
||||
"mtefOPT_LP_RULER" => 0x02, # RULER record follows LINE or PILE record
|
||||
# Option flag values for COLOR_DEF records:
|
||||
"mtefCOLOR_CMYK" => 0x01, # color model is CMYK, else RGB
|
||||
"mtefCOLOR_SPOT" => 0x02, # color is a spot color, else a process color
|
||||
"mtefCOLOR_NAME" => 0x04, # color has a name, else no name
|
||||
}
|
||||
|
||||
## Payload is the most important class to understand.
|
||||
## This abstraction allows recursive formats.
|
||||
## eg. lists can contain lists can contain lists.
|
||||
|
||||
class Payload < BinData::Choice
|
||||
opt = {:_options => :options}
|
||||
record_end 0 # end is a reserved keyword
|
||||
record_line 1
|
||||
record_char 2
|
||||
record_tmpl 3
|
||||
record_pile 4
|
||||
record_matrix 5
|
||||
record_embell 6
|
||||
record_ruler 7
|
||||
record_font_style_def 8
|
||||
record_size 9
|
||||
record_full 10
|
||||
record_sub 11
|
||||
record_sub2 12
|
||||
record_sym 13
|
||||
record_subsym 14
|
||||
record_color 15
|
||||
record_color_def 16
|
||||
record_font_def 17
|
||||
record_eqn_prefs 18
|
||||
record_encoding_def 19
|
||||
record_future 100
|
||||
record_mt_comment 102
|
||||
end
|
||||
|
||||
class NamedRecord < BinData::Record
|
||||
int8 :record_type
|
||||
payload :payload, :onlyif => :not_end_tag?, :selection => :record_type, :options => :record_options
|
||||
|
||||
def not_end_tag?
|
||||
record_type != 0
|
||||
end
|
||||
end
|
||||
|
||||
class Equation < Mathtype::Equation
|
||||
EXPOSED_IN_SNAPSHOT = %i(mtef_version platform product product_version
|
||||
product_subversion application_key equation_options equation)
|
||||
|
||||
stringz :application_key
|
||||
uint8 :_equation_options
|
||||
|
||||
def equation_options
|
||||
_equation_options == 1 ? "inline" : "block"
|
||||
end
|
||||
|
||||
array :equation, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
end
|
||||
end
|
||||
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/mtuint.rb
vendored
Normal file
14
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/mtuint.rb
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
module Mathtype
|
||||
class Mt_uint < BinData::Primitive
|
||||
uint8 :_first
|
||||
uint8 :_second, onlyif: lambda { _first == 0xFF }
|
||||
uint8 :_third, onlyif: lambda { _first == 0xFF }
|
||||
|
||||
def get
|
||||
_first < 0xFF ?
|
||||
_first :
|
||||
_third << 0x8 | _second
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
41
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/pile.rb
vendored
Normal file
41
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/pile.rb
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# PILE record (4):
|
||||
|
||||
# Consists of:
|
||||
|
||||
# record type (4)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [halign] horizontal alignment
|
||||
# [valign] vertical alignment
|
||||
# [[RULER record]] if mtefOPT_LP_RULER is set
|
||||
# [object list] list of lines contained by the pile
|
||||
|
||||
module Mathtype5
|
||||
class RecordPile < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(options halign valign object_list)
|
||||
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
int8 :_halign
|
||||
int8 :_valign
|
||||
|
||||
record_ruler :ruler, onlyif: lambda { options & OPTIONS["mtefOPT_LP_RULER"] > 0 }
|
||||
|
||||
array :object_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def halign
|
||||
HALIGN[_halign]
|
||||
end
|
||||
|
||||
def valign
|
||||
VALIGN[_valign]
|
||||
end
|
||||
end
|
||||
end
|
||||
72
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/size.rb
vendored
Normal file
72
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/size.rb
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# SIZE record (9):
|
||||
# Consists of one of the following cases:
|
||||
|
||||
# if lsize < 0 (explicit point size):
|
||||
# record type (9)
|
||||
# 101
|
||||
# -point size (16 bit integer)
|
||||
# else if -128 < dsize < +128:
|
||||
# record type (9)
|
||||
# lsize (typesize)
|
||||
# dsize + 128
|
||||
# else: (large delta)
|
||||
# record type (9)
|
||||
# 100
|
||||
# lsize (typesize)
|
||||
# dsize (16 bit integer)
|
||||
# Sizes in MathType are represented as a pair of values, lsize and dsize. Lsize
|
||||
# stands for "logical size", dsize for "delta size". If it is negative, it is an
|
||||
# explicit point size (in 32nds of a point) negated and dsize is ignored.
|
||||
# Otherwise, lsize is a typesize value and dsize is a delta from that size:
|
||||
# Simple typesizes, without a delta value, are written using the records
|
||||
# described in the next section.
|
||||
|
||||
module Mathtype5
|
||||
class RecordSize < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(lsize dsize point_size)
|
||||
|
||||
endian :little
|
||||
int8 :_size_select
|
||||
|
||||
mtef16 :_point_size, onlyif: lambda { _size_select == 101 }
|
||||
uint8 :_lsize_large_delta, onlyif: lambda { _size_select == 100 }
|
||||
|
||||
uint8 :_dsize, onlyif: lambda { _size_select != 100 && _size_select != 101 }
|
||||
mtef16 :_dsize_large, onlyif: lambda { _size_select == 100 }
|
||||
|
||||
def dsize
|
||||
__dsize = nil
|
||||
case _size_select
|
||||
when 100
|
||||
if _dsize_large > 255
|
||||
__dsize = -(_dsize_large.getlow)
|
||||
else
|
||||
__dsize = _dsize_large
|
||||
end
|
||||
when 101
|
||||
__dsize = _dsize
|
||||
else
|
||||
__dsize = (_dsize - 128) # in 32nds of a point
|
||||
end
|
||||
__dsize / 32
|
||||
end
|
||||
|
||||
def lsize
|
||||
case _size_select
|
||||
when 100
|
||||
_lsize_large_delta
|
||||
when 101
|
||||
nil
|
||||
else
|
||||
_size_select
|
||||
end
|
||||
end
|
||||
|
||||
def point_size
|
||||
-_point_size if _point_size != 0
|
||||
end
|
||||
end
|
||||
end
|
||||
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/snapshot.rb
vendored
Normal file
19
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/snapshot.rb
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
require "bindata"
|
||||
|
||||
module Mathtype5
|
||||
module Snapshot
|
||||
def snapshot
|
||||
snapshot = BinData::Record::Snapshot.new
|
||||
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
|
||||
exposed.each do |name|
|
||||
obj = find_obj_for_name(name)
|
||||
if obj
|
||||
snapshot[name] = obj.snapshot if include_obj?(obj)
|
||||
else
|
||||
snapshot[name] = self.send(name)
|
||||
end
|
||||
end
|
||||
snapshot
|
||||
end
|
||||
end
|
||||
end
|
||||
498
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/tmpl.rb
vendored
Normal file
498
scripts/mathtype2mathml/vendor/gems/mathtype/lib/records5/tmpl.rb
vendored
Normal file
@@ -0,0 +1,498 @@
|
||||
require_relative "snapshot"
|
||||
|
||||
# TMPL record (3):
|
||||
|
||||
# Consists of:
|
||||
# record type (3)
|
||||
# options
|
||||
# [nudge] if mtefOPT_NUDGE is set
|
||||
# [selector] template selector code
|
||||
# [variation] template variation code (1 or 2 bytes; see below)
|
||||
# [options] template-specific options
|
||||
# [subobject list] either a single character (e.g. sigma in a summation) and/or lines
|
||||
# The template selector and variation codes determine the class of the
|
||||
# template and various properties of the template, such as which subobjects
|
||||
# can be deleted by the user (see Templates). The class of a template
|
||||
# determines the order and meaning of each of its subobjects (see Template
|
||||
# subobject order).
|
||||
|
||||
# The variation code may be 1 or 2 bytes long. If the first byte value has the
|
||||
# high bit set (0x80), the next byte is read and combined with the first
|
||||
# according to this formula:
|
||||
|
||||
# variation code = (byte1 & 0x7F) | (byte2 << 8)
|
||||
|
||||
# Template-specific options field only used for integrals & fence templates:
|
||||
# Fence template option field values (fence alignment):
|
||||
# 0 center fence on math axis, place math axis of contents on math axis of
|
||||
# containing line (default);
|
||||
# 1 center fence on contents, place math axis of contents on math axis of
|
||||
# containing line;
|
||||
# 2 center fence on contents, center contents on math axis of containing line.
|
||||
# Warning: the expanding integral property is duplicated in the integral
|
||||
# templates variation codes (see Limit variations). On reading, MathType only
|
||||
# looks at the variation code.
|
||||
|
||||
# Integral template option field values:
|
||||
# 0 fixed-size integral;
|
||||
# 1 the integral expands vertically to fit its contents.
|
||||
|
||||
# Limit variations:
|
||||
|
||||
# The following variation codes apply to all templates whose class is BigOpBoxClass or LimBoxClass:
|
||||
|
||||
# variation bits symbol description
|
||||
# 0x0001 tvBO_LOWER lower limit is present
|
||||
# 0x0002 tvBO_UPPER upper limit is present
|
||||
# 0x0040 tvBO_SUM summation-style limit positions, else integral-style
|
||||
|
||||
# Template selectors and variations:
|
||||
# Fences (parentheses, etc.):
|
||||
# selector symbol description class
|
||||
# 0 tmANGLE angle brackets ParBoxClass
|
||||
# 1 tmPAREN parentheses ParBoxClass
|
||||
# 2 tmBRACE braces (curly brackets) ParBoxClass
|
||||
# 3 tmBRACK square brackets ParBoxClass
|
||||
# 4 tmBAR vertical bars ParBoxClass
|
||||
# 5 tmDBAR double vertical bars ParBoxClass
|
||||
# 6 tmFLOOR floor brackets ParBoxClass
|
||||
# 7 tmCEILING ceiling brackets ParBoxClass
|
||||
# 8 tmOBRACK open (white) brackets ParBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvFENCE_L left fence is present
|
||||
# 0x0002 tvFENCE_R right fence is present
|
||||
# Intervals:
|
||||
# selector symbol description class
|
||||
# 9 tmINTERVAL unmatched brackets and parentheses ParBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0000 tvINTV_LEFT_LP left fence is left parenthesis
|
||||
# 0x0001 tvINTV_LEFT_RP left fence is right parenthesis
|
||||
# 0x0002 tvINTV_LEFT_LB left fence is left bracket
|
||||
# 0x0003 tvINTV_LEFT_RB left fence is right bracket
|
||||
# 0x0000 tvINTV_RIGHT_LP right fence is left parenthesis
|
||||
# 0x0010 tvINTV_RIGHT_RP right fence is right parenthesis
|
||||
# 0x0020 tvINTV_RIGHT_LB right fence is left bracket
|
||||
# 0x0030 tvINTV_RIGHT_RB right fence is right bracket
|
||||
# Radicals (square and nth roots):
|
||||
# selector symbol description class
|
||||
# 10 tmROOT radical RootBoxClass
|
||||
# variations variation symbol description
|
||||
# 0 tvROOT_SQ square root
|
||||
# 1 tvROOT_NTH nth root
|
||||
# Fractions:
|
||||
# selector symbol description class
|
||||
# 11 tmFRACT fractions FracBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvFR_SMALL subscript-size slots (piece fraction)
|
||||
# 0x0002 tvFR_SLASH fraction bar is a slash
|
||||
# 0x0004 tvFR_BASE num. and denom. are baseline aligned
|
||||
# Over and Underbars:
|
||||
# selector symbol description class
|
||||
# 12 tmUBAR underbar BarBoxClass
|
||||
# 13 tmOBAR overbar BarBoxClass
|
||||
# variations variation bits symbol description
|
||||
# 0x0001 tvBAR_DOUBLE bar is doubled, else single
|
||||
# Arrows:
|
||||
# selector symbol description class
|
||||
# 14 tmARROW arrow ArroBoxClass
|
||||
# variations variation symbol description
|
||||
# 0x0000 tvAR_SINGLE single arrow
|
||||
# 0x0001 tvAR_DOUBLE double arrow
|
||||
# 0x0002 tvAR_HARPOON harpoon
|
||||
# 0x0004 tvAR_TOP top slot is present
|
||||
# 0x0008 tvAR_BOTTOM bottom slot is present
|
||||
# 0x0010 tvAR_LEFT if single, arrow points left
|
||||
# 0x0020 tvAR_RIGHT if single, arrow points right
|
||||
# 0x0010 tvAR_LOS if double or harpoon, large over small
|
||||
# 0x0020 tvAR_SOL if double or harpoon, small over large
|
||||
# Integrals (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 15 tmINTEG integral BigOpBoxClass
|
||||
# variations variation symbol description
|
||||
# 0x0001 tvINT_1 single integral sign
|
||||
# 0x0002 tvINT_2 double integral sign
|
||||
# 0x0003 tvINT_3 triple integral sign
|
||||
# 0x0004 tvINT_LOOP has loop w/o arrows
|
||||
# 0x0008 tvINT_CW_LOOP has clockwise loop
|
||||
# 0x000C tvINT_CCW_LOOP has counter-clockwise loop
|
||||
# 0x0100 tvINT_EXPAND integral signs expand
|
||||
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 16 tmSUM sum BigOpBoxClass
|
||||
# 17 tmPROD product BigOpBoxClass
|
||||
# 18 tmCOPROD coproduct BigOpBoxClass
|
||||
# 19 tmUNION union BigOpBoxClass
|
||||
# 20 tmINTER intersection BigOpBoxClass
|
||||
# 21 tmINTOP integral-style big operator BigOpBoxClass
|
||||
# 22 tmSUMOP summation-style big operator BigOpBoxClass
|
||||
# Limits (see Limit Variations):
|
||||
# selector symbol description class
|
||||
# 23 tmLIM limits LimBoxClass
|
||||
# variations variation symbol description
|
||||
# 0 tvSUBAR single underbar
|
||||
# 1 tvDUBAR double underbar
|
||||
# Horizontal braces and brackets:
|
||||
# selector symbol description class
|
||||
# 24 tmHBRACE horizontal brace HFenceBoxClass
|
||||
# 25 tmHBRACK horizontal bracket HFenceBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvHB_TOP slot is on the top, else on the bottom
|
||||
# Long division:
|
||||
# selector symbol description class
|
||||
# 26 tmLDIV long division LDivBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvLD_UPPER upper slot is present
|
||||
# Subscripts and superscripts:
|
||||
# selector symbol description class
|
||||
# 27 tmSUB subscript ScrBoxClass
|
||||
# 28 tmSUP superscript ScrBoxClass
|
||||
# 29 tmSUBSUP subscript and superscript ScrBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvSU_PRECEDES script precedes scripted item,
|
||||
# else follows
|
||||
# Dirac bra-ket notation:
|
||||
# selector symbol description class
|
||||
# 30 tmDIRAC bra-ket notation DiracBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvDI_LEFT left part is present
|
||||
# 0x0002 tvDI_RIGHT right part is present
|
||||
# Vectors:
|
||||
# selector symbol description class
|
||||
# 31 tmVEC vector HatBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvVE_LEFT arrow points left
|
||||
# 0x0002 tvVE_RIGHT arrow points right
|
||||
# 0x0004 tvVE_UNDER arrow under slot, else over slot
|
||||
# 0x0008 tvVE_HARPOON harpoon
|
||||
# Hats, arcs, tilde, joint status:
|
||||
# selector symbol description class
|
||||
# 32 tmTILDE tilde over characters HatBoxClass
|
||||
# 33 tmHAT hat over characters HatBoxClass
|
||||
# 34 tmARC arc over characters HatBoxClass
|
||||
# 35 tmJSTATUS joint status construct HatBoxClass
|
||||
# Overstrikes (cross-outs):
|
||||
# selector symbol description class
|
||||
# 36 tmSTRIKE overstrike (cross-out) StrikeBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvST_HORIZ line is horizontal, else slashes
|
||||
# 0x0002 tvST_UP if slashes, slash from lower-left to upper-right is present
|
||||
# 0x0004 tvST_DOWN if slashes, slash from upper-left to lower-right is present
|
||||
# Boxes:
|
||||
# selector symbol description class
|
||||
# 37 tmBOX box TBoxBoxClass
|
||||
# variation symbol description
|
||||
# 0x0001 tvBX_ROUND corners are round, else square
|
||||
# 0x0002 tvBX_LEFT left side is present
|
||||
# 0x0004 tvBX_RIGHT right side is present
|
||||
# 0x0008 tvBX_TOP top side is present
|
||||
# 0x0010 tvBX_BOTTOM bottom side is present
|
||||
|
||||
module Mathtype5
|
||||
class RecordTmpl < BinData::Record
|
||||
include Snapshot
|
||||
EXPOSED_IN_SNAPSHOT = %i(selector variation template_specific_options nudge
|
||||
subobject_list)
|
||||
|
||||
SELECTORS = {
|
||||
# Fences (parentheses, etc.):
|
||||
0 => "tmANGLE",
|
||||
1 => "tmPAREN",
|
||||
2 => "tmBRACE",
|
||||
3 => "tmBRACK",
|
||||
4 => "tmBAR",
|
||||
5 => "tmDBAR",
|
||||
6 => "tmFLOOR",
|
||||
7 => "tmCEILING",
|
||||
8 => "tmOBRACK",
|
||||
# Intervals:
|
||||
9 => "tmINTERVAL",
|
||||
# Radicals (square and nth roots):
|
||||
10 => "tmROOT",
|
||||
# Fractions:
|
||||
11 => "tmFRACT",
|
||||
# Over and Underbars:
|
||||
12 => "tmUBAR",
|
||||
13 => "tmOBAR",
|
||||
# Arrows:
|
||||
14 => "tmARROW",
|
||||
# Integrals (see Limit Variations):
|
||||
15 => "tmINTEG",
|
||||
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
|
||||
16 => "tmSUM",
|
||||
17 => "tmPROD",
|
||||
18 => "tmCOPROD",
|
||||
19 => "tmUNION",
|
||||
20 => "tmINTER",
|
||||
21 => "tmINTOP",
|
||||
22 => "tmSUMOP",
|
||||
# Limits (see Limit Variations):
|
||||
23 => "tmLIM",
|
||||
# Horizontal braces and brackets:
|
||||
24 => "tmHBRACE",
|
||||
25 => "tmHBRACK",
|
||||
# Long division:
|
||||
26 => "tmLDIV",
|
||||
# Subscripts and superscripts:
|
||||
27 => "tmSUB",
|
||||
28 => "tmSUP",
|
||||
29 => "tmSUBSUP",
|
||||
# Dirac bra-ket notation:
|
||||
30 => "tmDIRAC",
|
||||
# Vectors:
|
||||
31 => "tmVEC",
|
||||
# Hats, arcs, tilde, joint status:
|
||||
32 => "tmTILDE",
|
||||
33 => "tmHAT",
|
||||
34 => "tmARC",
|
||||
35 => "tmJSTATUS",
|
||||
# Overstrikes (cross-outs):
|
||||
36 => "tmSTRIKE",
|
||||
# Boxes:
|
||||
37 => "tmBOX"
|
||||
}
|
||||
|
||||
|
||||
# When options overlap in the binary space, ordinary bitmasks
|
||||
# are not the correct tool to use for detection. We use digit
|
||||
# position and presence instead.
|
||||
|
||||
DIGIT_MODE_VARIATIONS = [9]
|
||||
|
||||
# Top-level keys are template identifiers, defined in TEMPLATES.
|
||||
# Second-level keys are bits for certain variations, negative keys mean
|
||||
# that the variation is present if the bit is absent.
|
||||
|
||||
VARIATIONS = {
|
||||
# Fences (parentheses, etc.):
|
||||
0..8 => {
|
||||
0x0001 => "tvFENCE_L", # left fence is present
|
||||
0x0002 => "tvFENCE_R", # right fence is present
|
||||
},
|
||||
|
||||
# Intervals:
|
||||
9 => {
|
||||
# 0x0000 => "tvINTV_LEFT_LP", # left fence is left parenthesis
|
||||
# 0x0001 => "tvINTV_LEFT_RP", # left fence is right parenthesis
|
||||
# 0x0002 => "tvINTV_LEFT_LB", # left fence is left bracket
|
||||
# 0x0003 => "tvINTV_LEFT_RB", # left fence is right bracket
|
||||
# 0x0004 => "tvINTV_RIGHT_LP", # right fence is left parenthesis # WARNING: DOCUMENTATION SAYS 0x0000?
|
||||
# 0x0010 => "tvINTV_RIGHT_RP", # right fence is right parenthesis
|
||||
# 0x0020 => "tvINTV_RIGHT_LB", # right fence is left bracket
|
||||
# 0x0030 => "tvINTV_RIGHT_RB", # right fence is right bracket
|
||||
# Replaced above to match MathML translator
|
||||
0x0002 => {
|
||||
0x0020 => "tvINTV_LBLB", # left bracket, left bracket
|
||||
0x0010 => "tvINTV_LBRP", # left bracket, right parenthesis
|
||||
},
|
||||
0x0003 => {
|
||||
0x0030 => "tvINTV_RBRB", # right bracket, right bracket
|
||||
0x0020 => "tvINTV_RBLB", # right bracket, left bracket
|
||||
},
|
||||
0x0000 => {
|
||||
0x0030 => "tvINTV_LPRB", # left parenthesis, right bracket
|
||||
}
|
||||
},
|
||||
|
||||
# Radicals (square and nth roots):
|
||||
10 => {
|
||||
0 => "tvROOT_SQ", # square root
|
||||
1 => "tvROOT_NTH", # nth root
|
||||
},
|
||||
|
||||
# Fractions:
|
||||
11 => {
|
||||
0x0001 => "tvFR_SMALL", # subscript-size slots (piece fraction)
|
||||
0x0002 => "tvFR_SLASH", # fraction bar is a slash
|
||||
0x0004 => "tvFR_BASE", # num. and denom. are baseline aligned
|
||||
},
|
||||
|
||||
# Over and Underbars:
|
||||
12..13 => {
|
||||
0x0001 => "tvBAR_DOUBLE", # bar is doubled, else single
|
||||
},
|
||||
|
||||
# Arrows:
|
||||
14 => {
|
||||
0x0000 => "tvAR_SINGLE", # single arrow
|
||||
0x0001 => "tvAR_DOUBLE", # double arrow
|
||||
0x0010 => {
|
||||
0x0001 => "tvAR_LOS", # if double, large over small,
|
||||
0x0002 => "tvAR_LOS", # if harpoon, large over small,
|
||||
0x0010 => "tvAR_LEFT" # if single, arrow points left
|
||||
},
|
||||
0x0020 => {
|
||||
0x0001 => "tvAR_SOL", # if double, small over large
|
||||
0x0002 => "tvAR_SOL", # if harpoon, small over large
|
||||
0x0010 => "tvAR_RIGHT", # if single, arrow points right
|
||||
},
|
||||
0x0002 => "tvAR_HARPOON", # harpoon
|
||||
0x0004 => { # top slot is present
|
||||
0x0008 => "tvAR_TOPBOTTOM", # both slots are present
|
||||
0x0004 => "tvAR_TOP"
|
||||
},
|
||||
0x0008 => { # bottom slot is present
|
||||
0x0004 => "tvAR_TOPBOTTOM", # both slots are present
|
||||
0x0008 => "tvAR_BOTTOM"
|
||||
}
|
||||
},
|
||||
|
||||
# Integrals (see Limit Variations):
|
||||
15 => {
|
||||
0x0001 => "tvINT_1", # single integral sign
|
||||
0x0002 => "tvINT_2", # double integral sign
|
||||
0x0003 => "tvINT_3", # triple integral sign
|
||||
0x0004 => "tvINT_LOOP", # has loop w/o arrows
|
||||
0x0008 => "tvINT_CW_LOOP", # has clockwise loop
|
||||
0x000C => "tvINT_CCW_LOOP", # has counter-clockwise loop
|
||||
0x0100 => "tvINT_EXPAND", # integral signs expand
|
||||
},
|
||||
|
||||
# Limit variations
|
||||
15..23 => {
|
||||
0x0010 => "tvBO_LOWER", # lower limit is present
|
||||
0x0020 => "tvBO_UPPER", # upper limit is present
|
||||
0x0040 => "tvBO_SUM", # summation-style limit positions,
|
||||
-0x0040 => "tvBO_INT" # else integral-style
|
||||
},
|
||||
|
||||
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
|
||||
23 => {
|
||||
0 => "tvSUBAR", # single underbar
|
||||
1 => "tvDUBAR", # double underbar
|
||||
},
|
||||
|
||||
# Horizontal braces and brackets:
|
||||
24..25 => {
|
||||
0x0001 => "tvHB_TOP", # slot is on the top, else on the bottom
|
||||
},
|
||||
|
||||
# Long division:
|
||||
26 => {
|
||||
0x0001 => "tvLD_UPPER", # upper slot is present
|
||||
},
|
||||
|
||||
# Subscripts and superscripts:
|
||||
27..29 => {
|
||||
0x0001 => "tvSU_PRECEDES", # script precedes scripted item, else follows
|
||||
},
|
||||
|
||||
# Dirac bra-ket notation:
|
||||
30 => {
|
||||
0x0001 => "tvDI_LEFT", # left part is present
|
||||
0x0002 => "tvDI_RIGHT", # right part is present
|
||||
},
|
||||
|
||||
# Vectors:
|
||||
31 => {
|
||||
0x0001 => "tvVE_LEFT", # arrow points left
|
||||
0x0002 => "tvVE_RIGHT", # arrow points right
|
||||
0x0004 => "tvVE_UNDER", # arrow under slot, else over slot
|
||||
0x0008 => "tvVE_HARPOON", # harpoon
|
||||
},
|
||||
|
||||
# Hats, arcs, tilde, joint status:
|
||||
32..35 => {},
|
||||
|
||||
# Overstrikes (cross-outs):
|
||||
36 => {
|
||||
0x0001 => "tvST_HORIZ", # line is horizontal, else slashes
|
||||
0x0002 => "tvST_UP", # if slashes, slash from lower-left to upper-right is present
|
||||
0x0004 => "tvST_DOWN", # if slashes, slash from upper-left to lower-right is present
|
||||
},
|
||||
|
||||
# Boxes:
|
||||
37 => {
|
||||
0x0001 => "tvBX_ROUND", # corners are round, else square
|
||||
0x0002 => "tvBX_LEFT", # left side is present
|
||||
0x0004 => "tvBX_RIGHT", # right side is present
|
||||
0x0008 => "tvBX_TOP", # top side is present
|
||||
0x0010 => "tvBX_BOTTOM", # bottom side is present
|
||||
}
|
||||
}
|
||||
|
||||
int8 :options
|
||||
|
||||
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
|
||||
|
||||
int8 :_selector
|
||||
|
||||
int8 :_variation_first_byte
|
||||
int8 :_variation_second_byte, onlyif: (lambda do
|
||||
_variation_first_byte & 0x80 > 0
|
||||
end)
|
||||
|
||||
int8 :template_specific_options
|
||||
|
||||
array :subobject_list, read_until: lambda { element.record_type == 0 } do
|
||||
named_record
|
||||
end
|
||||
|
||||
def variation
|
||||
@variation = (_variation_first_byte & 0x7F) | (_variation_second_byte << 8)
|
||||
@variations = VARIATIONS.select do |selector, _|
|
||||
selector === _selector
|
||||
end.values.reduce(Hash.new, :merge)
|
||||
|
||||
process_variations
|
||||
end
|
||||
|
||||
def process_variations
|
||||
@variations.select do |flag, _|
|
||||
if flag < 0 # flag should NOT be active
|
||||
!check_flag(-flag)
|
||||
else # flag should be active
|
||||
check_flag(flag)
|
||||
end
|
||||
end.map do |flag, value|
|
||||
case value
|
||||
when Hash # Conditional variations
|
||||
result = value.detect do |conditional, _|
|
||||
check_flag(conditional)
|
||||
end
|
||||
result.last if result
|
||||
else
|
||||
value
|
||||
end
|
||||
end.uniq
|
||||
end
|
||||
|
||||
def check_flag(flag)
|
||||
case mode
|
||||
when :bitmask
|
||||
check_bitmask(flag)
|
||||
when :digit
|
||||
check_digit(flag)
|
||||
end
|
||||
end
|
||||
|
||||
def check_bitmask(flag)
|
||||
if flag == 0
|
||||
@variation & 0xf == 0
|
||||
else
|
||||
@variation & flag == flag
|
||||
end
|
||||
end
|
||||
|
||||
# E.g. is 0x3 present in 0x33
|
||||
def check_digit(flag)
|
||||
digits = if flag == 0
|
||||
1
|
||||
else
|
||||
(Math.log(flag+1)/Math.log(16)).ceil # digits in a hex number
|
||||
end
|
||||
mask = (15<<(4*digits-4)) # e.g. 0xf0
|
||||
variation_digit = (@variation & mask) >> (digits * 4 - 4)
|
||||
flag_digit = (flag & mask) >> (digits * 4 - 4)
|
||||
variation_digit == flag_digit
|
||||
end
|
||||
|
||||
|
||||
def mode
|
||||
DIGIT_MODE_VARIATIONS.include?(_selector) ? :digit : :bitmask
|
||||
end
|
||||
|
||||
def selector
|
||||
SELECTORS[_selector]
|
||||
end
|
||||
end
|
||||
end
|
||||
28
scripts/mathtype2mathml/vendor/gems/mathtype/mathtype.gemspec
vendored
Normal file
28
scripts/mathtype2mathml/vendor/gems/mathtype/mathtype.gemspec
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# coding: utf-8
|
||||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'mathtype/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "mathtype"
|
||||
spec.version = Mathtype::VERSION
|
||||
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
|
||||
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
|
||||
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
|
||||
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
|
||||
spec.homepage = ""
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*")
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_development_dependency "bundler", "> 1.7"
|
||||
spec.add_development_dependency "rake", ">= 12.3.3"
|
||||
spec.add_development_dependency "rspec", "~> 3.3"
|
||||
spec.add_development_dependency "pry", "~> 0.10"
|
||||
spec.add_dependency "bindata", "~> 2.1"
|
||||
spec.add_dependency "nokogiri", "~> 1.6"
|
||||
spec.add_dependency "ruby-ole", "~> 1.2"
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user