qtex 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -38
- package/index.js +8 -7
- package/package.json +20 -9
- package/src/ws-client.js +136 -0
package/README.md
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
# ⚡ qtex
|
|
1
|
+
# ⚡ qtex — Cloud LaTeX Compiler
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/srsergiolazaro/qtex/main/docs/assets/banner.jpeg" alt="qtex
|
|
4
|
+
<img src="https://raw.githubusercontent.com/srsergiolazaro/qtex/main/docs/assets/banner.jpeg" alt="qtex - Cloud LaTeX Compiler" width="500">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/qtex"><img src="https://img.shields.io/npm/v/qtex?style=flat-square&logo=npm&color=cb3837" alt="
|
|
9
|
-
<a href="
|
|
10
|
-
<a href="
|
|
11
|
-
<a href="
|
|
8
|
+
<a href="https://www.npmjs.com/package/qtex"><img src="https://img.shields.io/npm/v/qtex?style=flat-square&logo=npm&color=cb3837" alt="npm version"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/qtex"><img src="https://img.shields.io/npm/dm/qtex?style=flat-square&color=blue" alt="npm downloads"></a>
|
|
10
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Fair_Source-blue?style=flat-square" alt="License"></a>
|
|
11
|
+
<a href="https://latex.taptapp.xyz"><img src="https://img.shields.io/badge/engine-Tectonic-8b5cf6?style=flat-square&logo=rust" alt="Tectonic Engine"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<strong>
|
|
15
|
+
<strong>Compile LaTeX documents in the cloud. Sub-second builds. Zero configuration.</strong><br>
|
|
16
|
+
No TeXLive. No MikTeX. No 5GB downloads. Just fast PDF generation.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="#install">Install</a> •
|
|
21
|
+
<a href="#usage">Usage</a> •
|
|
22
|
+
<a href="#features">Features</a> •
|
|
23
|
+
<a href="https://srsergiolazaro.github.io/qtex/">Website</a>
|
|
16
24
|
</p>
|
|
17
25
|
|
|
18
26
|
---
|
|
@@ -21,70 +29,86 @@
|
|
|
21
29
|
|
|
22
30
|
**macOS / Linux**
|
|
23
31
|
```bash
|
|
24
|
-
curl -fsSL https://
|
|
32
|
+
curl -fsSL https://srsergiolazaro.github.io/qtex/install.sh | bash
|
|
25
33
|
```
|
|
26
34
|
|
|
27
35
|
**Windows (PowerShell)**
|
|
28
36
|
```powershell
|
|
29
|
-
irm https://
|
|
37
|
+
irm https://srsergiolazaro.github.io/qtex/install.ps1 | iex
|
|
30
38
|
```
|
|
31
39
|
|
|
32
|
-
**Or use directly
|
|
40
|
+
**Or use directly with npx (zero install):**
|
|
33
41
|
```bash
|
|
34
42
|
npx qtex ./my-project
|
|
35
43
|
```
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| Feature | Description |
|
|
42
|
-
|---------|-------------|
|
|
43
|
-
| ⚡ **Fast** | Rust-based Tectonic engine. Compile in milliseconds. |
|
|
44
|
-
| 📦 **Zero Config** | No TeXLive, no MikTeX. No 5GB downloads. |
|
|
45
|
-
| 👀 **Watch Mode** | Auto-recompile on save. Supports `.tex`, `.bib`, `.sty`, images. |
|
|
46
|
-
| 🔍 **Validation** | Pre-flight syntax checks before compilation. |
|
|
47
|
-
| 📂 **Multi-file** | Recursive asset discovery with nested folders. |
|
|
48
|
-
| 🔒 **Private** | Stateless & ephemeral. Files never stored. |
|
|
45
|
+
**Or install globally via npm:**
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g qtex
|
|
48
|
+
```
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
52
|
## Usage
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
# Compile a project
|
|
56
|
-
qtex ./my-
|
|
55
|
+
# Compile a LaTeX project
|
|
56
|
+
qtex ./my-thesis
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
qtex ./my-
|
|
58
|
+
# Live watch mode — auto-recompile on save
|
|
59
|
+
qtex ./my-thesis --watch
|
|
60
60
|
|
|
61
61
|
# Custom output filename
|
|
62
|
-
qtex ./my-
|
|
62
|
+
qtex ./my-thesis --output final.pdf
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# Show all options
|
|
65
65
|
qtex --help
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
| | Feature | Description |
|
|
73
|
+
|-|---------|-------------|
|
|
74
|
+
| ⚡ | **Blazing Fast** | Rust-based Tectonic engine compiles in milliseconds |
|
|
75
|
+
| 📦 | **Zero Config** | No TeXLive, no MikTeX, no local dependencies |
|
|
76
|
+
| 👀 | **Watch Mode** | Auto-recompile on `.tex`, `.bib`, `.sty`, and image changes |
|
|
77
|
+
| 🔍 | **Validation** | Pre-flight syntax checks before compilation |
|
|
78
|
+
| 📂 | **Multi-file** | Recursive asset discovery with nested folders |
|
|
79
|
+
| 🔒 | **Private** | Stateless & ephemeral — your files are never stored |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
70
83
|
## How it Works
|
|
71
84
|
|
|
72
85
|
<p align="center">
|
|
73
|
-
<img src="https://raw.githubusercontent.com/srsergiolazaro/qtex/main/docs/assets/flow.png" alt="qtex
|
|
86
|
+
<img src="https://raw.githubusercontent.com/srsergiolazaro/qtex/main/docs/assets/flow.png" alt="qtex workflow diagram" width="600">
|
|
74
87
|
</p>
|
|
75
88
|
|
|
76
|
-
1. **Scan** — Discover all
|
|
77
|
-
2. **Validate** — Pre-flight syntax check
|
|
78
|
-
3. **Compile** — Cloud processing
|
|
79
|
-
4. **
|
|
89
|
+
1. **Scan** — Discover all `.tex` files and dependencies
|
|
90
|
+
2. **Validate** — Pre-flight syntax check via API
|
|
91
|
+
3. **Compile** — Cloud processing with Tectonic engine
|
|
92
|
+
4. **Download** — Get your PDF instantly
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Why qtex?
|
|
97
|
+
|
|
98
|
+
| Traditional LaTeX | qtex |
|
|
99
|
+
|-------------------|------|
|
|
100
|
+
| 5GB+ TeXLive install | **Zero install** |
|
|
101
|
+
| Minutes to compile | **Milliseconds** |
|
|
102
|
+
| Complex setup | **One command** |
|
|
103
|
+
| Local resources | **Cloud-powered** |
|
|
80
104
|
|
|
81
105
|
---
|
|
82
106
|
|
|
83
|
-
## Infrastructure
|
|
107
|
+
## API & Infrastructure
|
|
84
108
|
|
|
85
109
|
- **Endpoint**: `https://latex.taptapp.xyz`
|
|
86
|
-
- **Engine**: Tectonic (Rust
|
|
87
|
-
- **Privacy**: Stateless processing
|
|
110
|
+
- **Engine**: [Tectonic](https://tectonic-typesetting.github.io/) (Rust/XeTeX)
|
|
111
|
+
- **Privacy**: Stateless processing — data is never stored
|
|
88
112
|
|
|
89
113
|
---
|
|
90
114
|
|
|
@@ -92,8 +116,8 @@ qtex --help
|
|
|
92
116
|
|
|
93
117
|
**Fair Source License**
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
✅ Free for individuals and teams ≤3 users
|
|
120
|
+
📧 Contact for enterprise licensing
|
|
97
121
|
|
|
98
122
|
See [LICENSE](./LICENSE) for details.
|
|
99
123
|
|
package/index.js
CHANGED
|
@@ -60,20 +60,24 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
60
60
|
autoUpdate(packageJson.version);
|
|
61
61
|
|
|
62
62
|
if (values.watch) {
|
|
63
|
+
const { TachyonWS } = await import('./src/ws-client.js');
|
|
64
|
+
const wsClient = new TachyonWS(directory, values);
|
|
65
|
+
await wsClient.connect();
|
|
66
|
+
|
|
63
67
|
const server = startServer();
|
|
64
68
|
const viewUrl = `http://localhost:${server.port}/view`;
|
|
65
69
|
ui.info(`Watching for changes in: ${colors.bold}${directory}${colors.reset}`);
|
|
66
70
|
ui.info(`View PDF at: ${colors.blue}${colors.underline}${viewUrl}${colors.reset}`);
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
// Initial compile over WS
|
|
73
|
+
await wsClient.sendProject();
|
|
69
74
|
|
|
70
75
|
// Auto-open browser
|
|
71
76
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
72
77
|
exec(`${openCmd} "${viewUrl}"`);
|
|
73
78
|
|
|
74
|
-
let isCompiling = false;
|
|
75
79
|
watch(directory, { recursive: true }, async (event, filename) => {
|
|
76
|
-
if (filename && !filename.startsWith('.') && !isCompiling) {
|
|
80
|
+
if (filename && !filename.startsWith('.') && !wsClient.isCompiling) {
|
|
77
81
|
const ext = extname(filename).toLowerCase();
|
|
78
82
|
const outputFileName = values.output || 'output.pdf';
|
|
79
83
|
|
|
@@ -82,10 +86,7 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
82
86
|
const isOutputFile = basename(filename) === basename(outputFileName);
|
|
83
87
|
|
|
84
88
|
if (watchExts.includes(ext) && !isOutputFile) {
|
|
85
|
-
|
|
86
|
-
console.log(`\n${colors.blue}🔄 Change detected in ${filename}, recompiling...${colors.reset}`);
|
|
87
|
-
await compile(directory, values);
|
|
88
|
-
isCompiling = false;
|
|
89
|
+
await wsClient.sendProject();
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qtex",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "Ultra-fast cloud LaTeX compiler. Compile .tex documents in milliseconds without installing TeXLive or MikTeX. Zero config, sub-second builds, live watch mode.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
"latex",
|
|
13
13
|
"tex",
|
|
14
14
|
"compiler",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"pdf",
|
|
16
|
+
"latex-compiler",
|
|
17
17
|
"cloud-latex",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"tectonic",
|
|
19
|
+
"xelatex",
|
|
20
|
+
"pdflatex",
|
|
21
|
+
"academic",
|
|
22
|
+
"thesis",
|
|
23
|
+
"paper",
|
|
24
|
+
"document",
|
|
25
|
+
"typesetting",
|
|
26
|
+
"markdown-to-pdf",
|
|
20
27
|
"cli",
|
|
21
|
-
"
|
|
28
|
+
"fast",
|
|
29
|
+
"rust"
|
|
22
30
|
],
|
|
23
31
|
"author": "Sergio Lázaro (srsergio)",
|
|
24
32
|
"repository": {
|
|
@@ -28,7 +36,7 @@
|
|
|
28
36
|
"bugs": {
|
|
29
37
|
"url": "https://github.com/srsergiolazaro/qtex/issues"
|
|
30
38
|
},
|
|
31
|
-
"homepage": "https://github.
|
|
39
|
+
"homepage": "https://srsergiolazaro.github.io/qtex/",
|
|
32
40
|
"license": "SEE LICENSE IN LICENSE",
|
|
33
41
|
"type": "module",
|
|
34
42
|
"bin": {
|
|
@@ -42,5 +50,8 @@
|
|
|
42
50
|
],
|
|
43
51
|
"directories": {
|
|
44
52
|
"example": "example"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
45
56
|
}
|
|
46
|
-
}
|
|
57
|
+
}
|
package/src/ws-client.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { resolve, extname, join, basename } from 'node:path';
|
|
2
|
+
import { colors, ui, Spinner } from './ui.js';
|
|
3
|
+
import { notifyClients } from './server.js';
|
|
4
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
const WS_URL = 'wss://latex.taptapp.xyz/ws'; // Production URL
|
|
7
|
+
|
|
8
|
+
export class TachyonWS {
|
|
9
|
+
constructor(directory, options) {
|
|
10
|
+
this.directory = directory;
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.socket = null;
|
|
13
|
+
this.isConnected = false;
|
|
14
|
+
this.spinner = new Spinner('');
|
|
15
|
+
this.isCompiling = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect() {
|
|
19
|
+
return new Promise((res, rej) => {
|
|
20
|
+
ui.info(`${colors.cyan}Connecting to Tachyon-Tex Live Engine...${colors.reset}`);
|
|
21
|
+
this.socket = new WebSocket(this.options.serverUrl || WS_URL);
|
|
22
|
+
|
|
23
|
+
this.socket.onopen = () => {
|
|
24
|
+
this.isConnected = true;
|
|
25
|
+
ui.info(`${colors.green}✅ Connected to Live Engine${colors.reset}`);
|
|
26
|
+
res();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.socket.onmessage = async (event) => {
|
|
30
|
+
const data = JSON.parse(event.data);
|
|
31
|
+
|
|
32
|
+
if (data.type === 'compile_success') {
|
|
33
|
+
if (this.spinner.timer) {
|
|
34
|
+
this.spinner.succeed(`${colors.green}PDF updated in ${colors.bold}${data.compile_time_ms}ms${colors.reset}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const outputFileName = this.options.output || 'output.pdf';
|
|
38
|
+
const outputPath = resolve(process.cwd(), outputFileName);
|
|
39
|
+
|
|
40
|
+
// Decode base64 PDF
|
|
41
|
+
const buffer = Buffer.from(data.pdf, 'base64');
|
|
42
|
+
await writeFile(outputPath, buffer);
|
|
43
|
+
|
|
44
|
+
// Notify local preview clients
|
|
45
|
+
notifyClients(outputPath);
|
|
46
|
+
this.isCompiling = false;
|
|
47
|
+
} else if (data.type === 'compile_error') {
|
|
48
|
+
if (this.spinner.timer) {
|
|
49
|
+
this.spinner.fail('Compilation failed');
|
|
50
|
+
}
|
|
51
|
+
console.log(`\n${colors.yellow}--- LaTeX Error ---${colors.reset}`);
|
|
52
|
+
console.log(`${colors.red}${data.error}${colors.reset}`);
|
|
53
|
+
console.log(`${colors.yellow}-------------------\n${colors.reset}`);
|
|
54
|
+
this.isCompiling = false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.socket.onerror = (err) => {
|
|
59
|
+
ui.error(`WebSocket Error: ${err.message}`);
|
|
60
|
+
rej(err);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.socket.onclose = () => {
|
|
64
|
+
this.isConnected = false;
|
|
65
|
+
ui.warn('WebSocket connection closed. Retrying in 2s...');
|
|
66
|
+
setTimeout(() => this.connect(), 2000);
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async sendProject() {
|
|
72
|
+
if (!this.isConnected || this.isCompiling) return;
|
|
73
|
+
|
|
74
|
+
this.isCompiling = true;
|
|
75
|
+
this.spinner = new Spinner(`${colors.blue}Syncing project...`).start();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const project = await this.buildProjectPayload();
|
|
79
|
+
this.socket.send(JSON.stringify(project));
|
|
80
|
+
} catch (e) {
|
|
81
|
+
this.spinner.fail(`Failed to sync: ${e.message}`);
|
|
82
|
+
this.isCompiling = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async buildProjectPayload() {
|
|
87
|
+
const absoluteDir = resolve(this.directory);
|
|
88
|
+
const files = await this.getProjectFiles(absoluteDir);
|
|
89
|
+
const payload = {
|
|
90
|
+
main: "main.tex", // Heuristic could be improved
|
|
91
|
+
files: {}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const fileObj of files) {
|
|
95
|
+
const ext = extname(fileObj.path).toLowerCase();
|
|
96
|
+
const textExts = ['.tex', '.sty', '.cls', '.bib', '.txt'];
|
|
97
|
+
const base = basename(fileObj.path);
|
|
98
|
+
|
|
99
|
+
if (textExts.includes(ext)) {
|
|
100
|
+
payload.files[fileObj.relative] = await readFile(fileObj.path, 'utf8');
|
|
101
|
+
if (base === 'main.tex') payload.main = fileObj.relative;
|
|
102
|
+
} else {
|
|
103
|
+
// Binary (images)
|
|
104
|
+
const buffer = await readFile(fileObj.path);
|
|
105
|
+
payload.files[fileObj.relative] = buffer.toString('base64');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getProjectFiles(dir, baseDir = dir) {
|
|
113
|
+
const { readdir } = await import('node:fs/promises');
|
|
114
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
115
|
+
const files = [];
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const res = join(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
|
|
121
|
+
files.push(...(await this.getProjectFiles(res, baseDir)));
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const ext = extname(entry.name).toLowerCase();
|
|
125
|
+
const allowed = ['.tex', '.bib', '.sty', '.cls', '.pdf', '.png', '.jpg', '.jpeg'];
|
|
126
|
+
if (allowed.includes(ext)) {
|
|
127
|
+
files.push({
|
|
128
|
+
path: res,
|
|
129
|
+
relative: join(dir.replace(baseDir, ''), entry.name).replace(/^[\\\/]/, '').replace(/\\/g, '/')
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return files;
|
|
135
|
+
}
|
|
136
|
+
}
|