@cristianormazabal/triton-core 0.1.12 → 0.1.13
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.
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Discovery Utility — src/theme/discover.ts
|
|
3
|
+
*
|
|
4
|
+
* Scans directories for `.triton-theme.json` files, validates them, resolves
|
|
5
|
+
* them over their declared base preset, and returns a name→ResolvedTheme
|
|
6
|
+
* registry.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ This module performs filesystem I/O (node:fs / node:path).
|
|
9
|
+
* It MUST NOT be imported from src/index.ts or src/frontend/index.ts.
|
|
10
|
+
* Hosts (VS Code extension, triton-latex CLI) consume it by relative path:
|
|
11
|
+
* import { discoverThemes } from '../../src/theme/discover.js'
|
|
12
|
+
*/
|
|
13
|
+
import type { ResolvedTheme } from '../contracts/theme.js';
|
|
14
|
+
import { type Result } from '../contracts/result.js';
|
|
15
|
+
export interface ThemeDiscoveryResult {
|
|
16
|
+
/** Resolved themes keyed by name (declared `name` field or filename-derived). */
|
|
17
|
+
readonly themes: ReadonlyMap<string, ResolvedTheme>;
|
|
18
|
+
/** Human-readable warnings for skipped/invalid files. Never throws. */
|
|
19
|
+
readonly warnings: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load, validate, and resolve a single `.triton-theme.json` file.
|
|
23
|
+
*
|
|
24
|
+
* Returns ok(ResolvedTheme) on success, err(...) on any failure.
|
|
25
|
+
* Never throws.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadThemeFile(filePath: string): Result<ResolvedTheme>;
|
|
28
|
+
/**
|
|
29
|
+
* Scan `dir` for all `.triton-theme.json` files, load and resolve each one,
|
|
30
|
+
* and return a name→ResolvedTheme map plus any warnings for skipped files.
|
|
31
|
+
*
|
|
32
|
+
* Rules (Cristian's decisions):
|
|
33
|
+
* - Name = the theme's `name` field if present; else derived from filename.
|
|
34
|
+
* - Derived name must satisfy slug rule; if not → warning + skipped.
|
|
35
|
+
* - If name is a built-in preset name → warning + skipped (no shadowing).
|
|
36
|
+
* - Duplicate custom name within same dir → last wins + warning.
|
|
37
|
+
* - Errors (bad JSON, validation failure) → warning + skipped. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
export declare function discoverThemes(dir: string): ThemeDiscoveryResult;
|
|
40
|
+
/**
|
|
41
|
+
* Walk up from `startDir` looking for a `.triton/themes/` directory.
|
|
42
|
+
* Returns its absolute path when found, or undefined if not found.
|
|
43
|
+
*
|
|
44
|
+
* Stops at the filesystem root or after 10 levels (infinite-loop guard).
|
|
45
|
+
*/
|
|
46
|
+
export declare function findTritonThemesDir(startDir: string): string | undefined;
|
|
47
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../../src/theme/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAO9D,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACpD,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAcD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC,CAkCrE;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAuDhE;AAiBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoBxE"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Discovery Utility — src/theme/discover.ts
|
|
3
|
+
*
|
|
4
|
+
* Scans directories for `.triton-theme.json` files, validates them, resolves
|
|
5
|
+
* them over their declared base preset, and returns a name→ResolvedTheme
|
|
6
|
+
* registry.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ This module performs filesystem I/O (node:fs / node:path).
|
|
9
|
+
* It MUST NOT be imported from src/index.ts or src/frontend/index.ts.
|
|
10
|
+
* Hosts (VS Code extension, triton-latex CLI) consume it by relative path:
|
|
11
|
+
* import { discoverThemes } from '../../src/theme/discover.js'
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, basename, resolve } from 'node:path';
|
|
15
|
+
import { ok, err } from '../contracts/result.js';
|
|
16
|
+
import { validateThemeInput, isBuiltinThemeName } from './validate.js';
|
|
17
|
+
import { resolveTheme } from './resolver.js';
|
|
18
|
+
import { getThemePreset } from './preset.js';
|
|
19
|
+
// ─── Name derivation ──────────────────────────────────────────────────────────
|
|
20
|
+
const THEME_SUFFIX = '.triton-theme.json';
|
|
21
|
+
const SLUG = /^[a-z0-9-]+$/;
|
|
22
|
+
/** Strip `.triton-theme.json` from a filename to derive a theme name. */
|
|
23
|
+
function deriveNameFromFilename(filename) {
|
|
24
|
+
return basename(filename, THEME_SUFFIX);
|
|
25
|
+
}
|
|
26
|
+
// ─── loadThemeFile ────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Load, validate, and resolve a single `.triton-theme.json` file.
|
|
29
|
+
*
|
|
30
|
+
* Returns ok(ResolvedTheme) on success, err(...) on any failure.
|
|
31
|
+
* Never throws.
|
|
32
|
+
*/
|
|
33
|
+
export function loadThemeFile(filePath) {
|
|
34
|
+
// 1. Read file
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = readFileSync(filePath, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
return err('THEME_VALIDATION_ERROR', `Cannot read theme file "${filePath}": ${String(cause)}`, cause);
|
|
41
|
+
}
|
|
42
|
+
// 2. Parse JSON
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
return err('THEME_VALIDATION_ERROR', `Invalid JSON in theme file "${filePath}": ${String(cause)}`, cause);
|
|
49
|
+
}
|
|
50
|
+
// 3. Validate against ThemeInput schema (strict — unknown keys are errors)
|
|
51
|
+
const validated = validateThemeInput(parsed);
|
|
52
|
+
if (!validated.ok) {
|
|
53
|
+
return err('THEME_VALIDATION_ERROR', `Theme file "${filePath}" failed validation: ${validated.error.message}`, validated.error);
|
|
54
|
+
}
|
|
55
|
+
// 4. Resolve over declared base (or 'default' if absent)
|
|
56
|
+
const input = validated.value;
|
|
57
|
+
const baseName = parsed['base'] ?? 'default';
|
|
58
|
+
const base = getThemePreset(baseName);
|
|
59
|
+
const resolved = resolveTheme(input, base);
|
|
60
|
+
return ok(resolved);
|
|
61
|
+
}
|
|
62
|
+
// ─── discoverThemes ───────────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Scan `dir` for all `.triton-theme.json` files, load and resolve each one,
|
|
65
|
+
* and return a name→ResolvedTheme map plus any warnings for skipped files.
|
|
66
|
+
*
|
|
67
|
+
* Rules (Cristian's decisions):
|
|
68
|
+
* - Name = the theme's `name` field if present; else derived from filename.
|
|
69
|
+
* - Derived name must satisfy slug rule; if not → warning + skipped.
|
|
70
|
+
* - If name is a built-in preset name → warning + skipped (no shadowing).
|
|
71
|
+
* - Duplicate custom name within same dir → last wins + warning.
|
|
72
|
+
* - Errors (bad JSON, validation failure) → warning + skipped. Never throws.
|
|
73
|
+
*/
|
|
74
|
+
export function discoverThemes(dir) {
|
|
75
|
+
const themes = new Map();
|
|
76
|
+
const warnings = [];
|
|
77
|
+
// Gracefully handle missing / unreadable directory
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = readdirSync(dir);
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
warnings.push(`Cannot read themes directory "${dir}": ${String(cause)}`);
|
|
84
|
+
return { themes, warnings };
|
|
85
|
+
}
|
|
86
|
+
const themeFiles = entries.filter(e => e.endsWith(THEME_SUFFIX));
|
|
87
|
+
for (const filename of themeFiles) {
|
|
88
|
+
const filePath = join(dir, filename);
|
|
89
|
+
// Load + validate + resolve
|
|
90
|
+
const result = loadThemeFile(filePath);
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
warnings.push(result.error.message);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const resolved = result.value;
|
|
96
|
+
// Determine registered name:
|
|
97
|
+
// - declared `name` field in the file → use it (already validated as a slug)
|
|
98
|
+
// - no `name` field → derive from filename
|
|
99
|
+
// We re-read the raw name field here rather than relying on resolved.name,
|
|
100
|
+
// because resolveTheme always fills name from the base preset when absent.
|
|
101
|
+
const name = extractNameFromFile(filePath) ?? deriveNameFromFilename(filename);
|
|
102
|
+
// Validate slug rule on derived name
|
|
103
|
+
if (!SLUG.test(name)) {
|
|
104
|
+
warnings.push(`Theme file "${filename}": derived name "${name}" is not a valid slug (^[a-z0-9-]+$); skipped`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Built-in collision check
|
|
108
|
+
if (isBuiltinThemeName(name)) {
|
|
109
|
+
warnings.push(`Theme "${name}" shadows a built-in preset; skipped`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Duplicate within same dir → last wins + warning
|
|
113
|
+
if (themes.has(name)) {
|
|
114
|
+
warnings.push(`Duplicate theme name "${name}": "${filename}" overrides an earlier definition`);
|
|
115
|
+
}
|
|
116
|
+
themes.set(name, resolved);
|
|
117
|
+
}
|
|
118
|
+
return { themes, warnings };
|
|
119
|
+
}
|
|
120
|
+
// ─── extractNameFromFile ──────────────────────────────────────────────────────
|
|
121
|
+
/** Read the `name` field directly from the raw JSON (returns null if absent or unreadable). */
|
|
122
|
+
function extractNameFromFile(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
return typeof parsed['name'] === 'string' ? parsed['name'] : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ─── findTritonThemesDir ──────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Walk up from `startDir` looking for a `.triton/themes/` directory.
|
|
135
|
+
* Returns its absolute path when found, or undefined if not found.
|
|
136
|
+
*
|
|
137
|
+
* Stops at the filesystem root or after 10 levels (infinite-loop guard).
|
|
138
|
+
*/
|
|
139
|
+
export function findTritonThemesDir(startDir) {
|
|
140
|
+
const MAX_LEVELS = 10;
|
|
141
|
+
let current = resolve(startDir);
|
|
142
|
+
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
143
|
+
const candidate = join(current, '.triton', 'themes');
|
|
144
|
+
try {
|
|
145
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
146
|
+
return candidate;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// permission error or similar — keep walking up
|
|
151
|
+
}
|
|
152
|
+
const parent = resolve(join(current, '..'));
|
|
153
|
+
if (parent === current)
|
|
154
|
+
break; // filesystem root
|
|
155
|
+
current = parent;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../../src/theme/discover.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cristianormazabal/triton-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Triton diagram compiler — deterministic SVG rendering for 50+ diagram types (Mermaid-compatible, trees, data structures, posters).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|