apexfile 1.1.0

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/bin/cli.js ADDED
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * ApexDoc CLI
6
+ * Usage: apex <command> [options] <file>
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const apex = require('./index');
12
+
13
+ const VERSION = '1.0.0';
14
+
15
+ // ── Arg Parsing ─────────────────────────────────────────────────
16
+
17
+ const args = process.argv.slice(2);
18
+ const command = args[0];
19
+ const flags = parseFlags(args.slice(1));
20
+
21
+ function parseFlags(argv) {
22
+ const result = { _: [] };
23
+ let i = 0;
24
+ while (i < argv.length) {
25
+ const arg = argv[i];
26
+ if (arg.startsWith('--')) {
27
+ const key = arg.slice(2);
28
+ const next = argv[i + 1];
29
+ if (next && !next.startsWith('--')) {
30
+ result[key] = next;
31
+ i += 2;
32
+ } else {
33
+ result[key] = true;
34
+ i++;
35
+ }
36
+ } else {
37
+ result._.push(arg);
38
+ i++;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ // ── Command Router ──────────────────────────────────────────────
45
+
46
+ switch (command) {
47
+ case 'build': cmdBuild(); break;
48
+ case 'export': cmdExport(); break;
49
+ case 'serve': cmdServe(); break;
50
+ case 'lint': cmdLint(); break;
51
+ case 'ast': cmdAst(); break;
52
+ case 'format': cmdFormat(); break;
53
+ case 'new': cmdNew(); break;
54
+ case 'plugin': cmdPlugin(); break;
55
+ case 'theme': cmdTheme(); break;
56
+ case '--version':
57
+ case '-v': console.log(`apexdoc v${VERSION}`); break;
58
+ case 'help':
59
+ case '--help':
60
+ case '-h': printHelp(); break;
61
+ default:
62
+ if (!command) printHelp();
63
+ else die(`Unknown command: "${command}". Run "apex help" for usage.`);
64
+ }
65
+
66
+ // ── Commands ────────────────────────────────────────────────────
67
+
68
+ function cmdBuild() {
69
+ const file = flags._[0];
70
+ const target = flags.to || 'html';
71
+ const out = flags.out;
72
+
73
+ requireFile(file);
74
+
75
+ log(`Building ${file} → ${target.toUpperCase()}`);
76
+
77
+ try {
78
+ const outPath = apex.export(file, target, out ? path.join(out, outputName(file, target)) : null);
79
+ ok(`Output → ${outPath}`);
80
+ } catch (err) {
81
+ die(err.message);
82
+ }
83
+ }
84
+
85
+ function cmdExport() {
86
+ // Alias for build with explicit --to
87
+ cmdBuild();
88
+ }
89
+
90
+ function cmdServe() {
91
+ const file = flags._[0];
92
+ const port = parseInt(flags.port || flags.p || '3000');
93
+
94
+ requireFile(file);
95
+
96
+ try {
97
+ const http = require('http');
98
+
99
+ const respond = (res) => {
100
+ try {
101
+ const html = apex.compile(file, 'html');
102
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
103
+ res.end(html);
104
+ } catch (err) {
105
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
106
+ res.end('ApexDoc error:\n' + err.message);
107
+ }
108
+ };
109
+
110
+ const server = http.createServer((req, res) => {
111
+ if (req.url === '/favicon.ico') { res.end(); return; }
112
+ respond(res);
113
+ });
114
+
115
+ server.listen(port, () => {
116
+ ok(`Serving ${file} at http://localhost:${port}`);
117
+ log('Press Ctrl+C to stop. File changes will reflect on refresh.');
118
+ });
119
+
120
+ } catch (err) {
121
+ die('Could not start server: ' + err.message);
122
+ }
123
+ }
124
+
125
+ function cmdLint() {
126
+ const file = flags._[0];
127
+ requireFile(file);
128
+
129
+ log(`Linting ${file}...`);
130
+
131
+ const result = apex.lint(file);
132
+
133
+ if (result.errors.length > 0) {
134
+ result.errors.forEach(e => {
135
+ console.error(` ${red('✗')} [${e.code}] Line ${e.line}: ${e.message}`);
136
+ });
137
+ }
138
+
139
+ if (result.warnings.length > 0) {
140
+ result.warnings.forEach(w => {
141
+ console.warn(` ${yellow('⚠')} [${w.code}] Line ${w.line}: ${w.message}`);
142
+ });
143
+ }
144
+
145
+ if (result.valid) {
146
+ ok(`No errors found.${result.warnings.length > 0 ? ` (${result.warnings.length} warning(s))` : ''}`);
147
+ } else {
148
+ die(`${result.errors.length} error(s) found.`);
149
+ }
150
+ }
151
+
152
+ function cmdAst() {
153
+ const file = flags._[0];
154
+ requireFile(file);
155
+
156
+ log(`Inspecting AST for ${file}...`);
157
+ const json = apex.inspect(file);
158
+
159
+ if (flags.out) {
160
+ fs.writeFileSync(flags.out, json, 'utf8');
161
+ ok(`AST written to ${flags.out}`);
162
+ } else {
163
+ console.log(json);
164
+ }
165
+ }
166
+
167
+ function cmdFormat() {
168
+ const file = flags._[0];
169
+ requireFile(file);
170
+
171
+ // Basic formatter: normalize indentation, trim trailing spaces
172
+ const source = fs.readFileSync(file, 'utf8');
173
+ const lines = source.split('\n');
174
+ let depth = 0;
175
+ const indent = ' ';
176
+
177
+ const formatted = lines.map(line => {
178
+ const t = line.trim();
179
+ if (!t) return '';
180
+
181
+ // Dedent on %%end
182
+ if (t === '%%end') depth = Math.max(0, depth - 1);
183
+
184
+ const result = depth > 0 ? indent.repeat(depth) + t : t;
185
+
186
+ // Indent after section/block open
187
+ if (t.startsWith('%%') && !t.startsWith('%%end') && !t.includes('%%end')) {
188
+ depth++;
189
+ }
190
+
191
+ return result;
192
+ });
193
+
194
+ const output = formatted.join('\n');
195
+
196
+ if (flags.check) {
197
+ if (output !== source) {
198
+ console.log('File would be reformatted.');
199
+ process.exit(1);
200
+ } else {
201
+ ok('File is already formatted.');
202
+ }
203
+ } else {
204
+ fs.writeFileSync(file, output, 'utf8');
205
+ ok(`Formatted ${file}`);
206
+ }
207
+ }
208
+
209
+ function cmdNew() {
210
+ const name = flags._[0] || 'document.apx';
211
+ const type = flags.template || flags.t || 'document';
212
+ const filePath = name.endsWith('.apx') ? name : name + '.apx';
213
+
214
+ if (fs.existsSync(filePath) && !flags.force) {
215
+ die(`File already exists: ${filePath}. Use --force to overwrite.`);
216
+ }
217
+
218
+ const content = apex.template(type);
219
+ fs.writeFileSync(filePath, content, 'utf8');
220
+ ok(`Created ${filePath} (template: ${type})`);
221
+ log(`Edit it and run: apex serve ${filePath}`);
222
+ }
223
+
224
+ function cmdPlugin() {
225
+ const sub = flags._[0];
226
+
227
+ if (sub === 'list') {
228
+ log('Installed plugins: (none — plugin registry coming soon)');
229
+ return;
230
+ }
231
+
232
+ if (sub === 'install') {
233
+ const name = flags._[1];
234
+ if (!name) die('Usage: apex plugin install <name>');
235
+ log(`Installing plugin: ${name}...`);
236
+ log('Plugin registry coming in v1.1.0');
237
+ return;
238
+ }
239
+
240
+ if (sub === 'remove') {
241
+ const name = flags._[1];
242
+ if (!name) die('Usage: apex plugin remove <name>');
243
+ log(`Removing plugin: ${name}...`);
244
+ log('Plugin registry coming in v1.1.0');
245
+ return;
246
+ }
247
+
248
+ die('Usage: apex plugin <install|remove|list> [name]');
249
+ }
250
+
251
+ function cmdTheme() {
252
+ const sub = flags._[0];
253
+
254
+ const builtins = [
255
+ 'default', 'dark-ocean', 'neon-city', 'sunset',
256
+ 'minimal-light', 'forest', 'candy', 'terminal', 'academic', 'newspaper'
257
+ ];
258
+
259
+ if (sub === 'list') {
260
+ log('Built-in themes:');
261
+ builtins.forEach(t => console.log(` • ${t}`));
262
+ return;
263
+ }
264
+
265
+ if (sub === 'preview') {
266
+ const name = flags._[1];
267
+ if (!name) die('Usage: apex theme preview <name>');
268
+ if (!builtins.includes(name)) die(`Unknown theme: ${name}`);
269
+ log(`Preview for "${name}" coming in v1.1.0`);
270
+ return;
271
+ }
272
+
273
+ die('Usage: apex theme <list|preview> [name]');
274
+ }
275
+
276
+ // ── Help ────────────────────────────────────────────────────────
277
+
278
+ function printHelp() {
279
+ console.log(`
280
+ ${bold('apex')} — ApexDoc CLI v${VERSION}
281
+
282
+ ${bold('USAGE')}
283
+ apex <command> [options] <file.apx>
284
+
285
+ ${bold('COMMANDS')}
286
+ build <file> Build .apx to HTML (default)
287
+ export <file> --to <fmt> Export to: html, pdf, text, md, json
288
+ serve <file> [--port N] Serve with live preview
289
+ lint <file> Validate syntax and structure
290
+ ast <file> Inspect the parsed AST
291
+ format <file> Auto-format the source file
292
+ new <name> [--template] Create a new .apx file
293
+ plugin install|remove|list Manage plugins
294
+ theme list|preview Browse themes
295
+
296
+ ${bold('OPTIONS')}
297
+ --to <target> Export target: html (default), pdf, text, md, json
298
+ --out <dir> Output directory
299
+ --port <n> Port for serve command (default: 3000)
300
+ --template <t> Template for new: document, presentation, report
301
+ --force Overwrite existing files
302
+ --version Show version
303
+
304
+ ${bold('EXAMPLES')}
305
+ apex build report.apx
306
+ apex build report.apx --to pdf --out ./dist
307
+ apex serve slides.apx --port 4000
308
+ apex lint doc.apx
309
+ apex new mydoc.apx --template report
310
+ apex theme list
311
+ `);
312
+ }
313
+
314
+ // ── Helpers ─────────────────────────────────────────────────────
315
+
316
+ function requireFile(file) {
317
+ if (!file) die('No file specified. Usage: apex <command> <file.apx>');
318
+ if (!fs.existsSync(file)) die(`File not found: ${file}`);
319
+ }
320
+
321
+ function outputName(file, target) {
322
+ const ext = { html: '.html', text: '.txt', json: '.json', md: '.md', pdf: '.pdf' };
323
+ return path.basename(file, '.apx') + (ext[target] || '.html');
324
+ }
325
+
326
+ function log(msg) { console.log(` ${cyan('›')} ${msg}`); }
327
+ function ok(msg) { console.log(` ${green('✓')} ${msg}`); }
328
+ function die(msg) { console.error(` ${red('✗')} ${msg}`); process.exit(1); }
329
+
330
+ function bold(s) { return `\x1b[1m${s}\x1b[0m`; }
331
+ function red(s) { return `\x1b[31m${s}\x1b[0m`; }
332
+ function green(s) { return `\x1b[32m${s}\x1b[0m`; }
333
+ function yellow(s) { return `\x1b[33m${s}\x1b[0m`; }
334
+ function cyan(s) { return `\x1b[36m${s}\x1b[0m`; }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "apexfile",
3
+ "version": "1.1.0",
4
+ "description": "The last document format you'll ever need. A unified format with Markdown, LaTeX, SCSS, animations, live data, charts, themes and more — all from a single .apx file.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "apex": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/index.js",
11
+ "build": "node bin/cli.js build",
12
+ "serve": "node bin/cli.js serve",
13
+ "lint": "node bin/cli.js lint",
14
+ "prepublishOnly": "node test/index.js"
15
+ },
16
+ "keywords": [
17
+ "apexdoc",
18
+ "apx",
19
+ "document",
20
+ "format",
21
+ "parser",
22
+ "renderer",
23
+ "markdown",
24
+ "latex",
25
+ "template",
26
+ "themes",
27
+ "animations",
28
+ "charts",
29
+ "html",
30
+ "pdf",
31
+ "presentation",
32
+ "slides",
33
+ "documentation"
34
+ ],
35
+ "author": "Boniface Njuguna (https://github.com/bonifacenjuguna)",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/bonifacenjuguna/apexdoc.git"
40
+ },
41
+ "homepage": "https://apex.run",
42
+ "bugs": {
43
+ "url": "https://github.com/bonifacenjuguna/apexdoc/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=16.0.0"
47
+ },
48
+ "files": [
49
+ "src/",
50
+ "bin/",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "devDependencies": {},
55
+ "directories": {
56
+ "test": "test"
57
+ },
58
+ "type": "commonjs"
59
+ }
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ApexDoc AST Node Factories
5
+ * Every node in the AST is created via one of these functions.
6
+ * This ensures a consistent, predictable tree structure.
7
+ */
8
+
9
+ const Node = {
10
+
11
+ // ── Document Root ───────────────────────────────────────────────
12
+ document: (meta, style, body) => ({
13
+ type: 'Document',
14
+ meta: meta || {},
15
+ style: style || {},
16
+ body: body || [],
17
+ }),
18
+
19
+ // ── META ────────────────────────────────────────────────────────
20
+ meta: (fields) => ({
21
+ type: 'Meta',
22
+ fields: fields || {},
23
+ }),
24
+
25
+ // ── STYLE ───────────────────────────────────────────────────────
26
+ style: (variables, themes, directives) => ({
27
+ type: 'Style',
28
+ variables: variables || {}, // { '--primary': '#00f5d4' }
29
+ themes: themes || {}, // { 'dark-ocean': { '--bg': '...' } }
30
+ directives: directives || [], // @import, @media, @breakpoint
31
+ }),
32
+
33
+ theme: (name, variables) => ({
34
+ type: 'Theme',
35
+ name,
36
+ variables: variables || {},
37
+ }),
38
+
39
+ styleDirective: (directive, value) => ({
40
+ type: 'StyleDirective',
41
+ directive, // @import @media @breakpoint
42
+ value,
43
+ }),
44
+
45
+ // ── HEADINGS ────────────────────────────────────────────────────
46
+ heading: (level, children, props, line) => ({
47
+ type: 'Heading',
48
+ level: level || 1,
49
+ children: children || [],
50
+ props: props || {},
51
+ line,
52
+ }),
53
+
54
+ // ── TEXT NODES ──────────────────────────────────────────────────
55
+ text: (value, line) => ({
56
+ type: 'Text',
57
+ value: value || '',
58
+ line,
59
+ }),
60
+
61
+ bold: (children, line) => ({
62
+ type: 'Bold',
63
+ children: children || [],
64
+ line,
65
+ }),
66
+
67
+ italic: (children, line) => ({
68
+ type: 'Italic',
69
+ children: children || [],
70
+ line,
71
+ }),
72
+
73
+ underline: (children, line) => ({
74
+ type: 'Underline',
75
+ children: children || [],
76
+ line,
77
+ }),
78
+
79
+ strikethrough: (children, line) => ({
80
+ type: 'Strikethrough',
81
+ children: children || [],
82
+ line,
83
+ }),
84
+
85
+ superscript: (children, line) => ({
86
+ type: 'Superscript',
87
+ children: children || [],
88
+ line,
89
+ }),
90
+
91
+ subscript: (children, line) => ({
92
+ type: 'Subscript',
93
+ children: children || [],
94
+ line,
95
+ }),
96
+
97
+ codeInline: (value, line) => ({
98
+ type: 'CodeInline',
99
+ value: value || '',
100
+ line,
101
+ }),
102
+
103
+ highlight: (children, line) => ({
104
+ type: 'Highlight',
105
+ children: children || [],
106
+ line,
107
+ }),
108
+
109
+ inlineStyled: (children, props, line) => ({
110
+ type: 'InlineStyled',
111
+ children: children || [],
112
+ props: props || {},
113
+ line,
114
+ }),
115
+
116
+ link: (children, href, line) => ({
117
+ type: 'Link',
118
+ children: children || [],
119
+ href: href || '',
120
+ line,
121
+ }),
122
+
123
+ // ── PARAGRAPH ───────────────────────────────────────────────────
124
+ paragraph: (children, line) => ({
125
+ type: 'Paragraph',
126
+ children: children || [],
127
+ line,
128
+ }),
129
+
130
+ // ── BLOCKQUOTE ──────────────────────────────────────────────────
131
+ blockquote: (children, level, line) => ({
132
+ type: 'Blockquote',
133
+ children: children || [],
134
+ level: level || 1,
135
+ line,
136
+ }),
137
+
138
+ // ── HORIZONTAL RULE ─────────────────────────────────────────────
139
+ hr: (style, line) => ({
140
+ type: 'HR',
141
+ style: style || '---',
142
+ line,
143
+ }),
144
+
145
+ // ── LISTS ───────────────────────────────────────────────────────
146
+ list: (items, ordered, line) => ({
147
+ type: 'List',
148
+ ordered: ordered || false,
149
+ items: items || [],
150
+ line,
151
+ }),
152
+
153
+ listItem: (children, indent, line) => ({
154
+ type: 'ListItem',
155
+ children: children || [],
156
+ indent: indent || 0,
157
+ line,
158
+ }),
159
+
160
+ taskItem: (children, state, indent, line) => ({
161
+ type: 'TaskItem',
162
+ children: children || [],
163
+ state: state || 'todo', // 'todo' | 'done' | 'progress'
164
+ indent: indent || 0,
165
+ line,
166
+ }),
167
+
168
+ // ── MATH ────────────────────────────────────────────────────────
169
+ mathInline: (value, line) => ({
170
+ type: 'MathInline',
171
+ value: value || '',
172
+ line,
173
+ }),
174
+
175
+ mathBlock: (value, props, line) => ({
176
+ type: 'MathBlock',
177
+ value: value || '',
178
+ props: props || {},
179
+ dialect: props?.dialect || 'latex',
180
+ line,
181
+ }),
182
+
183
+ // ── EXPRESSION / VARIABLE ───────────────────────────────────────
184
+ expression: (value, line) => ({
185
+ type: 'Expression',
186
+ value: value || '',
187
+ line,
188
+ }),
189
+
190
+ // ── FOOTNOTES ───────────────────────────────────────────────────
191
+ footnoteRef: (id, line) => ({
192
+ type: 'FootnoteRef',
193
+ id,
194
+ line,
195
+ }),
196
+
197
+ footnoteDef: (id, content, line) => ({
198
+ type: 'FootnoteDef',
199
+ id,
200
+ content: content || '',
201
+ line,
202
+ }),
203
+
204
+ // ── LOGIC ───────────────────────────────────────────────────────
205
+ setVar: (name, value, line) => ({
206
+ type: 'SetVar',
207
+ name,
208
+ value,
209
+ line,
210
+ }),
211
+
212
+ conditional: (condition, consequent, alternates, fallback, line) => ({
213
+ type: 'Conditional',
214
+ condition,
215
+ consequent: consequent || [], // if branch
216
+ alternates: alternates || [], // elseif branches [ { condition, body } ]
217
+ fallback: fallback || [], // else branch
218
+ line,
219
+ }),
220
+
221
+ loop: (variable, source, body, line) => ({
222
+ type: 'Loop',
223
+ variable, // iteration variable name
224
+ source, // source expression string
225
+ body: body || [],
226
+ line,
227
+ }),
228
+
229
+ keyframe: (name, steps, line) => ({
230
+ type: 'Keyframe',
231
+ name,
232
+ steps: steps || {},
233
+ line,
234
+ }),
235
+
236
+ // ── GENERIC BLOCK ───────────────────────────────────────────────
237
+ block: (name, props, children, line) => ({
238
+ type: 'Block',
239
+ name: name || 'unknown',
240
+ props: props || {},
241
+ children: children || [],
242
+ line,
243
+ }),
244
+
245
+ // ── SELF-CLOSING BLOCK ──────────────────────────────────────────
246
+ selfCloseBlock: (name, props, line) => ({
247
+ type: 'SelfCloseBlock',
248
+ name: name || 'unknown',
249
+ props: props || {},
250
+ line,
251
+ }),
252
+
253
+ // ── NEWLINE ─────────────────────────────────────────────────────
254
+ newline: (line) => ({
255
+ type: 'Newline',
256
+ line,
257
+ }),
258
+ };
259
+
260
+ module.exports = Node;