@graph-knowledge/api 0.1.7 → 0.1.28
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 +109 -0
- package/package.json +8 -2
- package/src/index.d.ts +3 -1
- package/src/index.js +3 -1
- package/src/lib/graph-knowledge-api.d.ts +35 -0
- package/src/lib/graph-knowledge-api.js +47 -1
- package/src/lib/interfaces/template-operations.interface.d.ts +11 -1
- package/src/lib/interfaces/text-measurement.interface.d.ts +24 -0
- package/src/lib/interfaces/text-measurement.interface.js +2 -0
- package/src/lib/operations/template-operations.d.ts +5 -2
- package/src/lib/operations/template-operations.js +47 -1
- package/src/lib/types/api-types.d.ts +15 -0
- package/src/lib/types/text-measurement-types.d.ts +64 -0
- package/src/lib/types/text-measurement-types.js +2 -0
- package/src/lib/utils/text-measurement/browser-text-measurement-provider.d.ts +28 -0
- package/src/lib/utils/text-measurement/browser-text-measurement-provider.js +137 -0
- package/src/lib/utils/text-measurement/fallback-text-measurement-provider.d.ts +29 -0
- package/src/lib/utils/text-measurement/fallback-text-measurement-provider.js +122 -0
- package/src/lib/utils/text-measurement/node-text-measurement-provider.d.ts +35 -0
- package/src/lib/utils/text-measurement/node-text-measurement-provider.js +165 -0
- package/src/lib/utils/text-measurement/text-measurement-defaults.d.ts +5 -0
- package/src/lib/utils/text-measurement/text-measurement-defaults.js +13 -0
- package/src/lib/utils/text-measurement/text-measurement-service.d.ts +39 -0
- package/src/lib/utils/text-measurement/text-measurement-service.js +63 -0
- package/src/lib/validators/template-validator.d.ts +16 -0
- package/src/lib/validators/template-validator.js +51 -0
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ new GraphKnowledgeAPI(config: ApiConfig)
|
|
|
91
91
|
| `signIn(email, password)` | Signs in with email and password |
|
|
92
92
|
| `signOut()` | Signs out the current user |
|
|
93
93
|
| `waitForAuthInit()` | Waits for Firebase Auth to initialize |
|
|
94
|
+
| `measureText(text, options?)` | Measures text dimensions (static method, no auth required) |
|
|
94
95
|
|
|
95
96
|
#### Properties
|
|
96
97
|
|
|
@@ -243,6 +244,44 @@ await api.elements.create(documentId, nodeId, {
|
|
|
243
244
|
methods: "+ getName(): string\n+ setName(name: string): void"
|
|
244
245
|
});
|
|
245
246
|
|
|
247
|
+
// Create a line
|
|
248
|
+
await api.elements.create(documentId, nodeId, {
|
|
249
|
+
type: "line",
|
|
250
|
+
x: 100,
|
|
251
|
+
y: 100,
|
|
252
|
+
width: 200,
|
|
253
|
+
height: 100,
|
|
254
|
+
strokeColor: "#000000",
|
|
255
|
+
strokeWidth: 2,
|
|
256
|
+
lineStyle: "solid" // "solid" | "dashed" | "dotted"
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Create a block arrow
|
|
260
|
+
await api.elements.create(documentId, nodeId, {
|
|
261
|
+
type: "block-arrow",
|
|
262
|
+
x: 100,
|
|
263
|
+
y: 100,
|
|
264
|
+
width: 150,
|
|
265
|
+
height: 80,
|
|
266
|
+
fillColor: "#4a9eff",
|
|
267
|
+
strokeColor: "#000000",
|
|
268
|
+
strokeWidth: 2,
|
|
269
|
+
direction: "right" // "right" | "left" | "up" | "down"
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Create basic shapes (triangle, diamond, hexagon, ellipse)
|
|
273
|
+
// All basic shapes share the same properties
|
|
274
|
+
await api.elements.create(documentId, nodeId, {
|
|
275
|
+
type: "triangle", // or "diamond", "hexagon", "ellipse"
|
|
276
|
+
x: 100,
|
|
277
|
+
y: 100,
|
|
278
|
+
width: 150,
|
|
279
|
+
height: 130,
|
|
280
|
+
fillColor: "#4CAF50",
|
|
281
|
+
strokeColor: "#2E7D32",
|
|
282
|
+
strokeWidth: 2
|
|
283
|
+
});
|
|
284
|
+
|
|
246
285
|
// Update an element
|
|
247
286
|
await api.elements.update(documentId, nodeId, elementId, {
|
|
248
287
|
x: 200,
|
|
@@ -316,7 +355,13 @@ await api.batch.deleteElements(documentId, nodeId, [
|
|
|
316
355
|
| Type | Description |
|
|
317
356
|
|------|-------------|
|
|
318
357
|
| `rectangle` | Rectangle shape with fill, stroke, corner radius |
|
|
358
|
+
| `triangle` | Triangle shape with fill and stroke |
|
|
359
|
+
| `diamond` | Diamond/rhombus shape with fill and stroke |
|
|
360
|
+
| `hexagon` | Hexagon shape with fill and stroke |
|
|
361
|
+
| `ellipse` | Ellipse/oval shape with fill and stroke |
|
|
319
362
|
| `text` | Text element with font customization (supports multi-line with `\n`) |
|
|
363
|
+
| `line` | Freeform line with stroke styling |
|
|
364
|
+
| `block-arrow` | Block arrow shape with directional pointer |
|
|
320
365
|
| `connector` | Line connecting two elements |
|
|
321
366
|
| `uml-class` | UML class diagram element |
|
|
322
367
|
| `uml-interface` | UML interface element |
|
|
@@ -341,6 +386,70 @@ await api.elements.create(documentId, nodeId, {
|
|
|
341
386
|
});
|
|
342
387
|
```
|
|
343
388
|
|
|
389
|
+
## Text Measurement
|
|
390
|
+
|
|
391
|
+
Measure text dimensions before creating elements. Useful for calculating layouts and positioning:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { GraphKnowledgeAPI } from "@graph-knowledge/api";
|
|
395
|
+
|
|
396
|
+
// Static method - no API instance or authentication needed
|
|
397
|
+
const metrics = GraphKnowledgeAPI.measureText("Hello World", {
|
|
398
|
+
fontSize: 24,
|
|
399
|
+
fontFamily: "Arial",
|
|
400
|
+
fontWeight: 400
|
|
401
|
+
});
|
|
402
|
+
console.log(metrics);
|
|
403
|
+
// { width: 132.5, height: 24, lines: 1, anchorDx: 0, anchorDy: 0 }
|
|
404
|
+
|
|
405
|
+
// Measure multi-line text
|
|
406
|
+
const multiLine = GraphKnowledgeAPI.measureText("Line 1\nLine 2", {
|
|
407
|
+
fontSize: 16,
|
|
408
|
+
lineHeight: 1.4
|
|
409
|
+
});
|
|
410
|
+
console.log(multiLine.lines); // 2
|
|
411
|
+
|
|
412
|
+
// Measure with word wrapping
|
|
413
|
+
const wrapped = GraphKnowledgeAPI.measureText(
|
|
414
|
+
"This is a long sentence that will be wrapped",
|
|
415
|
+
{ fontSize: 16, maxWidth: 150 }
|
|
416
|
+
);
|
|
417
|
+
console.log(wrapped.lines); // > 1
|
|
418
|
+
console.log(wrapped.width); // <= 150
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### MeasureTextOptions
|
|
422
|
+
|
|
423
|
+
| Option | Type | Default | Description |
|
|
424
|
+
|--------|------|---------|-------------|
|
|
425
|
+
| `fontSize` | `number` | `16` | Font size in pixels |
|
|
426
|
+
| `fontFamily` | `string` | `"Arial"` | Font family |
|
|
427
|
+
| `fontWeight` | `number` | `400` | Font weight (100-900) |
|
|
428
|
+
| `textAlign` | `"left" \| "center" \| "right"` | `"left"` | Affects anchorDx |
|
|
429
|
+
| `lineHeight` | `number` | `1.2` | Line height multiplier |
|
|
430
|
+
| `maxWidth` | `number` | - | Max width for word wrapping |
|
|
431
|
+
|
|
432
|
+
### TextMetrics Result
|
|
433
|
+
|
|
434
|
+
| Property | Description |
|
|
435
|
+
|----------|-------------|
|
|
436
|
+
| `width` | Maximum line width in pixels |
|
|
437
|
+
| `height` | Total text height in pixels |
|
|
438
|
+
| `lines` | Number of lines |
|
|
439
|
+
| `anchorDx` | X offset for text alignment positioning |
|
|
440
|
+
| `anchorDy` | Y offset (0 for top baseline) |
|
|
441
|
+
|
|
442
|
+
### Node.js Support
|
|
443
|
+
|
|
444
|
+
For accurate text measurement in Node.js, install the optional `canvas` package:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
npm install canvas
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Without it, the API uses character-based estimation (less accurate).
|
|
451
|
+
The `canvas` package requires native compilation - see [node-canvas requirements](https://github.com/Automattic/node-canvas#compiling).
|
|
452
|
+
|
|
344
453
|
## Error Handling
|
|
345
454
|
|
|
346
455
|
The API throws typed errors for different failure cases:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graph-knowledge/api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Headless Document API for Graph Knowledge - programmatic access to documents, nodes, and elements",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Graph Knowledge Team",
|
|
@@ -35,7 +35,13 @@
|
|
|
35
35
|
"node": ">=18.0.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"firebase": ">=10.0.0 <13.0.0"
|
|
38
|
+
"firebase": ">=10.0.0 <13.0.0",
|
|
39
|
+
"canvas": ">=2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"canvas": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
39
45
|
},
|
|
40
46
|
"sideEffects": false,
|
|
41
47
|
"type": "commonjs"
|
package/src/index.d.ts
CHANGED
|
@@ -8,12 +8,14 @@ export type { IElementOperations } from "./lib/interfaces/element-operations.int
|
|
|
8
8
|
export type { IBatchOperations, BatchUpdateElementInput } from "./lib/interfaces/batch-operations.interface";
|
|
9
9
|
export type { ITemplateOperations } from "./lib/interfaces/template-operations.interface";
|
|
10
10
|
export type { IElementTypeValidator, IElementValidatorRegistry } from "./lib/interfaces/element-validator.interface";
|
|
11
|
-
export type { CreateDocumentInput, UpdateDocumentInput, DocumentResult, CreateNodeInput, UpdateNodeInput, NodeResult } from "./lib/types/api-types";
|
|
11
|
+
export type { CreateDocumentInput, UpdateDocumentInput, DocumentResult, CreateNodeInput, UpdateNodeInput, NodeResult, CreateTemplateInput } from "./lib/types/api-types";
|
|
12
12
|
export type { BaseElementInput, RectangleInput, TextInput, ConnectorInput, ConnectorAnchor, LineStyle, MarkerType, UmlClassInput, UmlInterfaceInput, UmlComponentInput, UmlPackageInput, UmlArtifactInput, UmlNoteInput, TriangleInput, DiamondInput, HexagonInput, EllipseInput, LineInput, BlockArrowInput, BlockArrowDirection, CustomShapeInput, AnyElementInput, UpdateElementInput } from "./lib/types/element-input-types";
|
|
13
13
|
export { GraphKnowledgeAPIError, AuthenticationError, NotFoundError, ValidationError, PermissionError } from "./lib/errors/api-errors";
|
|
14
14
|
export type { Document, GraphNode, GraphElement } from "./lib/models";
|
|
15
15
|
export { CURRENT_DOCUMENT_SCHEMA_VERSION } from "./lib/models";
|
|
16
16
|
export { LinkLevelManager } from "./lib/utils/link-level-manager";
|
|
17
17
|
export type { LevelChange, LinkState } from "./lib/utils/link-level-manager";
|
|
18
|
+
export type { MeasureTextOptions, TextMetrics } from "./lib/types/text-measurement-types";
|
|
19
|
+
export { TextMeasurementService } from "./lib/utils/text-measurement/text-measurement-service";
|
|
18
20
|
export { MockAuthClient } from "./lib/testing/mock-auth-client";
|
|
19
21
|
export { MockFirestoreClient } from "./lib/testing/mock-firestore-client";
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Graph Knowledge Headless Document API
|
|
4
4
|
// =============================================================================
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MockFirestoreClient = exports.MockAuthClient = exports.LinkLevelManager = exports.CURRENT_DOCUMENT_SCHEMA_VERSION = exports.PermissionError = exports.ValidationError = exports.NotFoundError = exports.AuthenticationError = exports.GraphKnowledgeAPIError = exports.GraphKnowledgeAPI = void 0;
|
|
6
|
+
exports.MockFirestoreClient = exports.MockAuthClient = exports.TextMeasurementService = exports.LinkLevelManager = exports.CURRENT_DOCUMENT_SCHEMA_VERSION = exports.PermissionError = exports.ValidationError = exports.NotFoundError = exports.AuthenticationError = exports.GraphKnowledgeAPIError = exports.GraphKnowledgeAPI = void 0;
|
|
7
7
|
// Main API Class
|
|
8
8
|
var graph_knowledge_api_1 = require("./lib/graph-knowledge-api");
|
|
9
9
|
Object.defineProperty(exports, "GraphKnowledgeAPI", { enumerable: true, get: function () { return graph_knowledge_api_1.GraphKnowledgeAPI; } });
|
|
@@ -23,6 +23,8 @@ Object.defineProperty(exports, "CURRENT_DOCUMENT_SCHEMA_VERSION", { enumerable:
|
|
|
23
23
|
// =============================================================================
|
|
24
24
|
var link_level_manager_1 = require("./lib/utils/link-level-manager");
|
|
25
25
|
Object.defineProperty(exports, "LinkLevelManager", { enumerable: true, get: function () { return link_level_manager_1.LinkLevelManager; } });
|
|
26
|
+
var text_measurement_service_1 = require("./lib/utils/text-measurement/text-measurement-service");
|
|
27
|
+
Object.defineProperty(exports, "TextMeasurementService", { enumerable: true, get: function () { return text_measurement_service_1.TextMeasurementService; } });
|
|
26
28
|
// =============================================================================
|
|
27
29
|
// Testing Utilities
|
|
28
30
|
// =============================================================================
|
|
@@ -5,6 +5,7 @@ import { INodeOperations } from "./interfaces/node-operations.interface";
|
|
|
5
5
|
import { IElementOperations } from "./interfaces/element-operations.interface";
|
|
6
6
|
import { IBatchOperations } from "./interfaces/batch-operations.interface";
|
|
7
7
|
import { ITemplateOperations } from "./interfaces/template-operations.interface";
|
|
8
|
+
import { MeasureTextOptions, TextMetrics } from "./types/text-measurement-types";
|
|
8
9
|
/**
|
|
9
10
|
* Main entry point for the Graph Knowledge Headless API.
|
|
10
11
|
*
|
|
@@ -50,6 +51,10 @@ export declare class GraphKnowledgeAPI {
|
|
|
50
51
|
private readonly _elements;
|
|
51
52
|
private readonly _batch;
|
|
52
53
|
private readonly _templates;
|
|
54
|
+
/**
|
|
55
|
+
* Shared text measurement service for static and instance methods.
|
|
56
|
+
*/
|
|
57
|
+
private static _textMeasurementService;
|
|
53
58
|
/**
|
|
54
59
|
* Creates a new GraphKnowledgeAPI instance.
|
|
55
60
|
* @param config Configuration including Firebase credentials
|
|
@@ -111,4 +116,34 @@ export declare class GraphKnowledgeAPI {
|
|
|
111
116
|
* @throws PermissionError if not premium
|
|
112
117
|
*/
|
|
113
118
|
requirePremium(): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Measures text dimensions. No authentication required.
|
|
121
|
+
* Useful for calculating layouts and positioning before creating elements.
|
|
122
|
+
*
|
|
123
|
+
* @param text The text to measure
|
|
124
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
125
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const metrics = GraphKnowledgeAPI.measureText("Hello World", { fontSize: 24 });
|
|
130
|
+
* console.log(metrics.width, metrics.height);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
static measureText(text: string, options?: MeasureTextOptions): TextMetrics;
|
|
134
|
+
/**
|
|
135
|
+
* Measures text dimensions. No authentication required.
|
|
136
|
+
* Instance method that delegates to the static method.
|
|
137
|
+
*
|
|
138
|
+
* @param text The text to measure
|
|
139
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
140
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const api = new GraphKnowledgeAPI({ firebaseConfig: {...} });
|
|
145
|
+
* const metrics = api.measureText("Hello", { fontSize: 16 });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
measureText(text: string, options?: MeasureTextOptions): TextMetrics;
|
|
114
149
|
}
|
|
@@ -14,6 +14,8 @@ const template_operations_1 = require("./operations/template-operations");
|
|
|
14
14
|
const document_validator_1 = require("./validators/document-validator");
|
|
15
15
|
const node_validator_1 = require("./validators/node-validator");
|
|
16
16
|
const element_validator_registry_1 = require("./validators/element-validator-registry");
|
|
17
|
+
const template_validator_1 = require("./validators/template-validator");
|
|
18
|
+
const text_measurement_service_1 = require("./utils/text-measurement/text-measurement-service");
|
|
17
19
|
/**
|
|
18
20
|
* Main entry point for the Graph Knowledge Headless API.
|
|
19
21
|
*
|
|
@@ -59,6 +61,10 @@ class GraphKnowledgeAPI {
|
|
|
59
61
|
_elements;
|
|
60
62
|
_batch;
|
|
61
63
|
_templates;
|
|
64
|
+
/**
|
|
65
|
+
* Shared text measurement service for static and instance methods.
|
|
66
|
+
*/
|
|
67
|
+
static _textMeasurementService = null;
|
|
62
68
|
/**
|
|
63
69
|
* Creates a new GraphKnowledgeAPI instance.
|
|
64
70
|
* @param config Configuration including Firebase credentials
|
|
@@ -80,7 +86,7 @@ class GraphKnowledgeAPI {
|
|
|
80
86
|
this._nodes = new node_operations_1.NodeOperations(firestoreClient, this._authClient, nodeValidator);
|
|
81
87
|
this._elements = new element_operations_1.ElementOperations(firestoreClient, this._authClient, elementValidatorRegistry);
|
|
82
88
|
this._batch = new batch_operations_1.BatchOperations(firestoreClient, this._authClient, elementValidatorRegistry);
|
|
83
|
-
this._templates = new template_operations_1.TemplateOperations(firestoreClient, this._authClient);
|
|
89
|
+
this._templates = new template_operations_1.TemplateOperations(firestoreClient, this._authClient, new template_validator_1.TemplateValidator());
|
|
84
90
|
}
|
|
85
91
|
/**
|
|
86
92
|
* Authentication client for sign-in/sign-out operations.
|
|
@@ -165,5 +171,45 @@ class GraphKnowledgeAPI {
|
|
|
165
171
|
async requirePremium() {
|
|
166
172
|
return this._authClient.requirePremium();
|
|
167
173
|
}
|
|
174
|
+
// =========================================================================
|
|
175
|
+
// Text Measurement (static and instance methods)
|
|
176
|
+
// =========================================================================
|
|
177
|
+
/**
|
|
178
|
+
* Measures text dimensions. No authentication required.
|
|
179
|
+
* Useful for calculating layouts and positioning before creating elements.
|
|
180
|
+
*
|
|
181
|
+
* @param text The text to measure
|
|
182
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
183
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const metrics = GraphKnowledgeAPI.measureText("Hello World", { fontSize: 24 });
|
|
188
|
+
* console.log(metrics.width, metrics.height);
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
static measureText(text, options = {}) {
|
|
192
|
+
if (!GraphKnowledgeAPI._textMeasurementService) {
|
|
193
|
+
GraphKnowledgeAPI._textMeasurementService = new text_measurement_service_1.TextMeasurementService();
|
|
194
|
+
}
|
|
195
|
+
return GraphKnowledgeAPI._textMeasurementService.measureText(text, options);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Measures text dimensions. No authentication required.
|
|
199
|
+
* Instance method that delegates to the static method.
|
|
200
|
+
*
|
|
201
|
+
* @param text The text to measure
|
|
202
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
203
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const api = new GraphKnowledgeAPI({ firebaseConfig: {...} });
|
|
208
|
+
* const metrics = api.measureText("Hello", { fontSize: 16 });
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
measureText(text, options = {}) {
|
|
212
|
+
return GraphKnowledgeAPI.measureText(text, options);
|
|
213
|
+
}
|
|
168
214
|
}
|
|
169
215
|
exports.GraphKnowledgeAPI = GraphKnowledgeAPI;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DocumentResult } from "../types/api-types";
|
|
1
|
+
import { CreateTemplateInput, DocumentResult } from "../types/api-types";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for template operations.
|
|
4
4
|
*
|
|
@@ -6,6 +6,16 @@ import { DocumentResult } from "../types/api-types";
|
|
|
6
6
|
* by premium users to create new documents.
|
|
7
7
|
*/
|
|
8
8
|
export interface ITemplateOperations {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new template document with a root node.
|
|
11
|
+
* Requires admin access.
|
|
12
|
+
*
|
|
13
|
+
* @param input Template creation parameters
|
|
14
|
+
* @returns The newly created template with its root node
|
|
15
|
+
* @throws AuthenticationError if not authenticated
|
|
16
|
+
* @throws PermissionError if not admin
|
|
17
|
+
*/
|
|
18
|
+
create(input: CreateTemplateInput): Promise<DocumentResult>;
|
|
9
19
|
/**
|
|
10
20
|
* Lists all published templates available to users.
|
|
11
21
|
* @returns Array of published template documents (shallow, without nodes)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MeasureTextOptions, TextMetrics } from "../types/text-measurement-types";
|
|
2
|
+
/**
|
|
3
|
+
* Interface for text measurement providers.
|
|
4
|
+
* Implements the Strategy pattern for cross-environment support.
|
|
5
|
+
*
|
|
6
|
+
* Providers:
|
|
7
|
+
* - BrowserTextMeasurementProvider: Uses native canvas API (browsers)
|
|
8
|
+
* - NodeTextMeasurementProvider: Uses canvas npm package (Node.js)
|
|
9
|
+
* - FallbackTextMeasurementProvider: Character-based estimation (always available)
|
|
10
|
+
*/
|
|
11
|
+
export interface ITextMeasurementProvider {
|
|
12
|
+
/**
|
|
13
|
+
* Measures text dimensions.
|
|
14
|
+
* @param text The text to measure
|
|
15
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
16
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
17
|
+
*/
|
|
18
|
+
measureText(text: string, options: MeasureTextOptions): TextMetrics;
|
|
19
|
+
/**
|
|
20
|
+
* Checks if this provider is available in the current environment.
|
|
21
|
+
* @returns true if the provider can be used
|
|
22
|
+
*/
|
|
23
|
+
isAvailable(): boolean;
|
|
24
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ITemplateOperations } from "../interfaces/template-operations.interface";
|
|
2
2
|
import { IFirestoreClient } from "../interfaces/firestore-client.interface";
|
|
3
3
|
import { IAuthClient } from "../interfaces/auth-client.interface";
|
|
4
|
-
import { DocumentResult } from "../types/api-types";
|
|
4
|
+
import { CreateTemplateInput, DocumentResult } from "../types/api-types";
|
|
5
|
+
import { TemplateValidator } from "../validators/template-validator";
|
|
5
6
|
/**
|
|
6
7
|
* Implementation of template operations.
|
|
7
8
|
*
|
|
@@ -11,7 +12,9 @@ import { DocumentResult } from "../types/api-types";
|
|
|
11
12
|
export declare class TemplateOperations implements ITemplateOperations {
|
|
12
13
|
private readonly firestore;
|
|
13
14
|
private readonly auth;
|
|
14
|
-
|
|
15
|
+
private readonly validator;
|
|
16
|
+
constructor(firestore: IFirestoreClient, auth: IAuthClient, validator: TemplateValidator);
|
|
17
|
+
create(input: CreateTemplateInput): Promise<DocumentResult>;
|
|
15
18
|
list(): Promise<DocumentResult[]>;
|
|
16
19
|
get(templateId: string): Promise<DocumentResult>;
|
|
17
20
|
clone(templateId: string, title?: string): Promise<DocumentResult>;
|
|
@@ -16,9 +16,55 @@ const DEFAULT_CANVAS_HEIGHT = 1080;
|
|
|
16
16
|
class TemplateOperations {
|
|
17
17
|
firestore;
|
|
18
18
|
auth;
|
|
19
|
-
|
|
19
|
+
validator;
|
|
20
|
+
constructor(firestore, auth, validator) {
|
|
20
21
|
this.firestore = firestore;
|
|
21
22
|
this.auth = auth;
|
|
23
|
+
this.validator = validator;
|
|
24
|
+
}
|
|
25
|
+
async create(input) {
|
|
26
|
+
const userId = this.auth.requireAuth();
|
|
27
|
+
await this.auth.requireAdmin();
|
|
28
|
+
this.validator.validateCreate(input);
|
|
29
|
+
const docId = (0, uuid_1.generateUuid)();
|
|
30
|
+
const rootNodeId = (0, uuid_1.generateUuid)();
|
|
31
|
+
const canvasWidth = input.canvasWidth ?? DEFAULT_CANVAS_WIDTH;
|
|
32
|
+
const canvasHeight = input.canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
|
|
33
|
+
// Create the template document
|
|
34
|
+
const templateDocument = {
|
|
35
|
+
schemaVersion: models_1.CURRENT_DOCUMENT_SCHEMA_VERSION,
|
|
36
|
+
title: input.title,
|
|
37
|
+
content: input.content ?? "",
|
|
38
|
+
owner: userId,
|
|
39
|
+
sharedWith: [],
|
|
40
|
+
createdAt: new Date(),
|
|
41
|
+
rootNodeId,
|
|
42
|
+
isPremiumContent: false,
|
|
43
|
+
isTemplate: true,
|
|
44
|
+
isPublished: input.isPublished ?? false,
|
|
45
|
+
nodes: [] // Nodes stored in sub-collection
|
|
46
|
+
};
|
|
47
|
+
// Create the root node
|
|
48
|
+
const rootNode = {
|
|
49
|
+
id: rootNodeId,
|
|
50
|
+
title: input.title,
|
|
51
|
+
content: "",
|
|
52
|
+
elements: [],
|
|
53
|
+
canvasWidth,
|
|
54
|
+
canvasHeight,
|
|
55
|
+
level: 0,
|
|
56
|
+
owner: userId
|
|
57
|
+
};
|
|
58
|
+
// Write everything in a batch
|
|
59
|
+
const batch = this.firestore.batch();
|
|
60
|
+
batch.set(`documents/${docId}`, templateDocument);
|
|
61
|
+
batch.set(`documents/${docId}/nodes/${rootNodeId}`, rootNode);
|
|
62
|
+
await batch.commit();
|
|
63
|
+
return {
|
|
64
|
+
id: docId,
|
|
65
|
+
...templateDocument,
|
|
66
|
+
nodes: [rootNode]
|
|
67
|
+
};
|
|
22
68
|
}
|
|
23
69
|
async list() {
|
|
24
70
|
this.auth.requireAuth();
|
|
@@ -59,3 +59,18 @@ export interface UpdateNodeInput {
|
|
|
59
59
|
export type NodeResult = GraphNode & {
|
|
60
60
|
id: string;
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Input for creating a new template (admin only).
|
|
64
|
+
*/
|
|
65
|
+
export interface CreateTemplateInput {
|
|
66
|
+
/** Template title (required) */
|
|
67
|
+
title: string;
|
|
68
|
+
/** Template description/content */
|
|
69
|
+
content?: string;
|
|
70
|
+
/** Root node canvas width (defaults to 1920) */
|
|
71
|
+
canvasWidth?: number;
|
|
72
|
+
/** Root node canvas height (defaults to 1080) */
|
|
73
|
+
canvasHeight?: number;
|
|
74
|
+
/** Whether to publish immediately (defaults to false - draft) */
|
|
75
|
+
isPublished?: boolean;
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for measuring text dimensions.
|
|
3
|
+
* All properties are optional and have sensible defaults.
|
|
4
|
+
*/
|
|
5
|
+
export interface MeasureTextOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Font size in pixels.
|
|
8
|
+
* @default 16
|
|
9
|
+
*/
|
|
10
|
+
fontSize?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Font family name.
|
|
13
|
+
* @default "Arial"
|
|
14
|
+
*/
|
|
15
|
+
fontFamily?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Font weight (100-900).
|
|
18
|
+
* @default 400
|
|
19
|
+
*/
|
|
20
|
+
fontWeight?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Text alignment. Affects anchorDx calculation.
|
|
23
|
+
* @default "left"
|
|
24
|
+
*/
|
|
25
|
+
textAlign?: "left" | "center" | "right";
|
|
26
|
+
/**
|
|
27
|
+
* Line height multiplier for multi-line text.
|
|
28
|
+
* @default 1.2
|
|
29
|
+
*/
|
|
30
|
+
lineHeight?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum width for word wrapping.
|
|
33
|
+
* When specified, text will be wrapped to fit within this width.
|
|
34
|
+
*/
|
|
35
|
+
maxWidth?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Result of text measurement containing dimensions and positioning offsets.
|
|
39
|
+
*/
|
|
40
|
+
export interface TextMetrics {
|
|
41
|
+
/**
|
|
42
|
+
* Maximum line width in pixels.
|
|
43
|
+
*/
|
|
44
|
+
width: number;
|
|
45
|
+
/**
|
|
46
|
+
* Total text height in pixels.
|
|
47
|
+
*/
|
|
48
|
+
height: number;
|
|
49
|
+
/**
|
|
50
|
+
* Number of lines in the text.
|
|
51
|
+
*/
|
|
52
|
+
lines: number;
|
|
53
|
+
/**
|
|
54
|
+
* X offset for text alignment positioning.
|
|
55
|
+
* - left: 0
|
|
56
|
+
* - center: -width/2
|
|
57
|
+
* - right: -width
|
|
58
|
+
*/
|
|
59
|
+
anchorDx: number;
|
|
60
|
+
/**
|
|
61
|
+
* Y offset (0 for top/hanging baseline).
|
|
62
|
+
*/
|
|
63
|
+
anchorDy: number;
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ITextMeasurementProvider } from "../../interfaces/text-measurement.interface";
|
|
2
|
+
import { MeasureTextOptions, TextMetrics } from "../../types/text-measurement-types";
|
|
3
|
+
/**
|
|
4
|
+
* Browser text measurement provider using native Canvas API.
|
|
5
|
+
* Provides accurate measurements when running in a browser environment.
|
|
6
|
+
*/
|
|
7
|
+
export declare class BrowserTextMeasurementProvider implements ITextMeasurementProvider {
|
|
8
|
+
private _canvas;
|
|
9
|
+
private _context;
|
|
10
|
+
measureText(text: string, options: MeasureTextOptions): TextMetrics;
|
|
11
|
+
isAvailable(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Gets or creates the canvas context for measurement.
|
|
14
|
+
*/
|
|
15
|
+
private _getContext;
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
18
|
+
*/
|
|
19
|
+
private _wrapLine;
|
|
20
|
+
/**
|
|
21
|
+
* Calculates the maximum width among all lines.
|
|
22
|
+
*/
|
|
23
|
+
private _calculateMaxLineWidth;
|
|
24
|
+
/**
|
|
25
|
+
* Calculates the X anchor offset based on text alignment.
|
|
26
|
+
*/
|
|
27
|
+
private _calculateAnchorDx;
|
|
28
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BrowserTextMeasurementProvider = void 0;
|
|
4
|
+
const text_measurement_defaults_1 = require("./text-measurement-defaults");
|
|
5
|
+
/**
|
|
6
|
+
* Browser text measurement provider using native Canvas API.
|
|
7
|
+
* Provides accurate measurements when running in a browser environment.
|
|
8
|
+
*/
|
|
9
|
+
class BrowserTextMeasurementProvider {
|
|
10
|
+
_canvas = null;
|
|
11
|
+
_context = null;
|
|
12
|
+
measureText(text, options) {
|
|
13
|
+
const fontSize = options.fontSize ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontSize;
|
|
14
|
+
const fontFamily = options.fontFamily ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontFamily;
|
|
15
|
+
const fontWeight = options.fontWeight ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontWeight;
|
|
16
|
+
const lineHeight = options.lineHeight ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.lineHeight;
|
|
17
|
+
const textAlign = options.textAlign ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.textAlign;
|
|
18
|
+
const maxWidth = options.maxWidth;
|
|
19
|
+
if (!text || text.length === 0) {
|
|
20
|
+
return {
|
|
21
|
+
width: 0,
|
|
22
|
+
height: 0,
|
|
23
|
+
lines: 0,
|
|
24
|
+
anchorDx: 0,
|
|
25
|
+
anchorDy: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const ctx = this._getContext();
|
|
29
|
+
if (!ctx) {
|
|
30
|
+
// Should not happen if isAvailable() is checked, but fallback gracefully
|
|
31
|
+
throw new Error("Canvas context not available");
|
|
32
|
+
}
|
|
33
|
+
// Set font for measurement
|
|
34
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
35
|
+
// Split by explicit newlines
|
|
36
|
+
const explicitLines = text.split("\n");
|
|
37
|
+
// Calculate wrapped lines if maxWidth is specified
|
|
38
|
+
const wrappedLines = [];
|
|
39
|
+
if (maxWidth !== undefined && maxWidth > 0) {
|
|
40
|
+
for (const line of explicitLines) {
|
|
41
|
+
const wrapped = this._wrapLine(ctx, line, maxWidth);
|
|
42
|
+
wrappedLines.push(...wrapped);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
wrappedLines.push(...explicitLines);
|
|
47
|
+
}
|
|
48
|
+
// Calculate dimensions
|
|
49
|
+
const lineCount = wrappedLines.length;
|
|
50
|
+
const maxLineWidth = this._calculateMaxLineWidth(ctx, wrappedLines);
|
|
51
|
+
const width = maxWidth !== undefined ? Math.min(maxLineWidth, maxWidth) : maxLineWidth;
|
|
52
|
+
// Height calculation: first line is fontSize, subsequent lines add lineHeight * fontSize
|
|
53
|
+
const height = lineCount > 0
|
|
54
|
+
? fontSize + (lineCount - 1) * lineHeight * fontSize
|
|
55
|
+
: 0;
|
|
56
|
+
// Calculate anchor offset based on alignment
|
|
57
|
+
const anchorDx = this._calculateAnchorDx(width, textAlign);
|
|
58
|
+
return {
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
lines: lineCount,
|
|
62
|
+
anchorDx,
|
|
63
|
+
anchorDy: 0 // Top/hanging baseline
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
isAvailable() {
|
|
67
|
+
return typeof document !== "undefined" && !!document.createElement;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Gets or creates the canvas context for measurement.
|
|
71
|
+
*/
|
|
72
|
+
_getContext() {
|
|
73
|
+
if (this._context) {
|
|
74
|
+
return this._context;
|
|
75
|
+
}
|
|
76
|
+
if (!this.isAvailable()) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
this._canvas = document.createElement("canvas");
|
|
80
|
+
this._context = this._canvas.getContext("2d");
|
|
81
|
+
return this._context;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
85
|
+
*/
|
|
86
|
+
_wrapLine(ctx, line, maxWidth) {
|
|
87
|
+
if (line.length === 0) {
|
|
88
|
+
return [""];
|
|
89
|
+
}
|
|
90
|
+
const words = line.split(/\s+/);
|
|
91
|
+
const lines = [];
|
|
92
|
+
let currentLine = "";
|
|
93
|
+
for (const word of words) {
|
|
94
|
+
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
95
|
+
const metrics = ctx.measureText(testLine);
|
|
96
|
+
if (metrics.width <= maxWidth || currentLine.length === 0) {
|
|
97
|
+
currentLine = testLine;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push(currentLine);
|
|
101
|
+
currentLine = word;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (currentLine.length > 0) {
|
|
105
|
+
lines.push(currentLine);
|
|
106
|
+
}
|
|
107
|
+
return lines.length > 0 ? lines : [""];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Calculates the maximum width among all lines.
|
|
111
|
+
*/
|
|
112
|
+
_calculateMaxLineWidth(ctx, lines) {
|
|
113
|
+
let maxWidth = 0;
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const metrics = ctx.measureText(line);
|
|
116
|
+
if (metrics.width > maxWidth) {
|
|
117
|
+
maxWidth = metrics.width;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return maxWidth;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Calculates the X anchor offset based on text alignment.
|
|
124
|
+
*/
|
|
125
|
+
_calculateAnchorDx(width, textAlign) {
|
|
126
|
+
switch (textAlign) {
|
|
127
|
+
case "center":
|
|
128
|
+
return -width / 2;
|
|
129
|
+
case "right":
|
|
130
|
+
return -width;
|
|
131
|
+
case "left":
|
|
132
|
+
default:
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.BrowserTextMeasurementProvider = BrowserTextMeasurementProvider;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ITextMeasurementProvider } from "../../interfaces/text-measurement.interface";
|
|
2
|
+
import { MeasureTextOptions, TextMetrics } from "../../types/text-measurement-types";
|
|
3
|
+
/**
|
|
4
|
+
* Fallback text measurement provider using character-based estimation.
|
|
5
|
+
* Always available, but less accurate than canvas-based providers.
|
|
6
|
+
*
|
|
7
|
+
* Uses average character width ratios derived from common fonts.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FallbackTextMeasurementProvider implements ITextMeasurementProvider {
|
|
10
|
+
/**
|
|
11
|
+
* Average character width as a ratio of font size.
|
|
12
|
+
* Based on typical proportional fonts like Arial.
|
|
13
|
+
*/
|
|
14
|
+
private static readonly AVG_CHAR_WIDTH_RATIO;
|
|
15
|
+
measureText(text: string, options: MeasureTextOptions): TextMetrics;
|
|
16
|
+
isAvailable(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
19
|
+
*/
|
|
20
|
+
private _wrapLine;
|
|
21
|
+
/**
|
|
22
|
+
* Calculates the maximum width among all lines.
|
|
23
|
+
*/
|
|
24
|
+
private _calculateMaxLineWidth;
|
|
25
|
+
/**
|
|
26
|
+
* Calculates the X anchor offset based on text alignment.
|
|
27
|
+
*/
|
|
28
|
+
private _calculateAnchorDx;
|
|
29
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FallbackTextMeasurementProvider = void 0;
|
|
4
|
+
const text_measurement_defaults_1 = require("./text-measurement-defaults");
|
|
5
|
+
/**
|
|
6
|
+
* Fallback text measurement provider using character-based estimation.
|
|
7
|
+
* Always available, but less accurate than canvas-based providers.
|
|
8
|
+
*
|
|
9
|
+
* Uses average character width ratios derived from common fonts.
|
|
10
|
+
*/
|
|
11
|
+
class FallbackTextMeasurementProvider {
|
|
12
|
+
/**
|
|
13
|
+
* Average character width as a ratio of font size.
|
|
14
|
+
* Based on typical proportional fonts like Arial.
|
|
15
|
+
*/
|
|
16
|
+
static AVG_CHAR_WIDTH_RATIO = 0.55;
|
|
17
|
+
measureText(text, options) {
|
|
18
|
+
const fontSize = options.fontSize ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontSize;
|
|
19
|
+
const lineHeight = options.lineHeight ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.lineHeight;
|
|
20
|
+
const textAlign = options.textAlign ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.textAlign;
|
|
21
|
+
const maxWidth = options.maxWidth;
|
|
22
|
+
if (!text || text.length === 0) {
|
|
23
|
+
return {
|
|
24
|
+
width: 0,
|
|
25
|
+
height: 0,
|
|
26
|
+
lines: 0,
|
|
27
|
+
anchorDx: 0,
|
|
28
|
+
anchorDy: 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Split by explicit newlines
|
|
32
|
+
const explicitLines = text.split("\n");
|
|
33
|
+
// Calculate estimated char width
|
|
34
|
+
const charWidth = fontSize * FallbackTextMeasurementProvider.AVG_CHAR_WIDTH_RATIO;
|
|
35
|
+
// Calculate wrapped lines if maxWidth is specified
|
|
36
|
+
const wrappedLines = [];
|
|
37
|
+
if (maxWidth !== undefined && maxWidth > 0) {
|
|
38
|
+
for (const line of explicitLines) {
|
|
39
|
+
const wrapped = this._wrapLine(line, charWidth, maxWidth);
|
|
40
|
+
wrappedLines.push(...wrapped);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
wrappedLines.push(...explicitLines);
|
|
45
|
+
}
|
|
46
|
+
// Calculate dimensions
|
|
47
|
+
const lineCount = wrappedLines.length;
|
|
48
|
+
const maxLineWidth = this._calculateMaxLineWidth(wrappedLines, charWidth);
|
|
49
|
+
const width = maxWidth !== undefined ? Math.min(maxLineWidth, maxWidth) : maxLineWidth;
|
|
50
|
+
// Height calculation: first line is fontSize, subsequent lines add lineHeight * fontSize
|
|
51
|
+
const height = lineCount > 0
|
|
52
|
+
? fontSize + (lineCount - 1) * lineHeight * fontSize
|
|
53
|
+
: 0;
|
|
54
|
+
// Calculate anchor offset based on alignment
|
|
55
|
+
const anchorDx = this._calculateAnchorDx(width, textAlign);
|
|
56
|
+
return {
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
lines: lineCount,
|
|
60
|
+
anchorDx,
|
|
61
|
+
anchorDy: 0 // Top/hanging baseline
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
isAvailable() {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
69
|
+
*/
|
|
70
|
+
_wrapLine(line, charWidth, maxWidth) {
|
|
71
|
+
if (line.length === 0) {
|
|
72
|
+
return [""];
|
|
73
|
+
}
|
|
74
|
+
const words = line.split(/\s+/);
|
|
75
|
+
const lines = [];
|
|
76
|
+
let currentLine = "";
|
|
77
|
+
for (const word of words) {
|
|
78
|
+
const wordWidth = word.length * charWidth;
|
|
79
|
+
const currentWidth = currentLine.length * charWidth;
|
|
80
|
+
const spaceWidth = currentLine.length > 0 ? charWidth : 0;
|
|
81
|
+
if (currentWidth + spaceWidth + wordWidth <= maxWidth || currentLine.length === 0) {
|
|
82
|
+
currentLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push(currentLine);
|
|
86
|
+
currentLine = word;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (currentLine.length > 0) {
|
|
90
|
+
lines.push(currentLine);
|
|
91
|
+
}
|
|
92
|
+
return lines.length > 0 ? lines : [""];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Calculates the maximum width among all lines.
|
|
96
|
+
*/
|
|
97
|
+
_calculateMaxLineWidth(lines, charWidth) {
|
|
98
|
+
let maxWidth = 0;
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const lineWidth = line.length * charWidth;
|
|
101
|
+
if (lineWidth > maxWidth) {
|
|
102
|
+
maxWidth = lineWidth;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return maxWidth;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Calculates the X anchor offset based on text alignment.
|
|
109
|
+
*/
|
|
110
|
+
_calculateAnchorDx(width, textAlign) {
|
|
111
|
+
switch (textAlign) {
|
|
112
|
+
case "center":
|
|
113
|
+
return -width / 2;
|
|
114
|
+
case "right":
|
|
115
|
+
return -width;
|
|
116
|
+
case "left":
|
|
117
|
+
default:
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.FallbackTextMeasurementProvider = FallbackTextMeasurementProvider;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ITextMeasurementProvider } from "../../interfaces/text-measurement.interface";
|
|
2
|
+
import { MeasureTextOptions, TextMetrics } from "../../types/text-measurement-types";
|
|
3
|
+
/**
|
|
4
|
+
* Node.js text measurement provider using the canvas npm package.
|
|
5
|
+
* Provides accurate measurements in Node.js when the canvas package is installed.
|
|
6
|
+
*
|
|
7
|
+
* @requires canvas - Optional peer dependency (npm install canvas)
|
|
8
|
+
*/
|
|
9
|
+
export declare class NodeTextMeasurementProvider implements ITextMeasurementProvider {
|
|
10
|
+
private _canvas;
|
|
11
|
+
private _context;
|
|
12
|
+
private _canvasModule;
|
|
13
|
+
measureText(text: string, options: MeasureTextOptions): TextMetrics;
|
|
14
|
+
isAvailable(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Attempts to load the canvas npm package.
|
|
17
|
+
*/
|
|
18
|
+
private _tryLoadCanvas;
|
|
19
|
+
/**
|
|
20
|
+
* Gets or creates the canvas context for measurement.
|
|
21
|
+
*/
|
|
22
|
+
private _getContext;
|
|
23
|
+
/**
|
|
24
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
25
|
+
*/
|
|
26
|
+
private _wrapLine;
|
|
27
|
+
/**
|
|
28
|
+
* Calculates the maximum width among all lines.
|
|
29
|
+
*/
|
|
30
|
+
private _calculateMaxLineWidth;
|
|
31
|
+
/**
|
|
32
|
+
* Calculates the X anchor offset based on text alignment.
|
|
33
|
+
*/
|
|
34
|
+
private _calculateAnchorDx;
|
|
35
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeTextMeasurementProvider = void 0;
|
|
4
|
+
const text_measurement_defaults_1 = require("./text-measurement-defaults");
|
|
5
|
+
/**
|
|
6
|
+
* Node.js text measurement provider using the canvas npm package.
|
|
7
|
+
* Provides accurate measurements in Node.js when the canvas package is installed.
|
|
8
|
+
*
|
|
9
|
+
* @requires canvas - Optional peer dependency (npm install canvas)
|
|
10
|
+
*/
|
|
11
|
+
class NodeTextMeasurementProvider {
|
|
12
|
+
_canvas = null;
|
|
13
|
+
_context = null;
|
|
14
|
+
_canvasModule = null;
|
|
15
|
+
measureText(text, options) {
|
|
16
|
+
const fontSize = options.fontSize ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontSize;
|
|
17
|
+
const fontFamily = options.fontFamily ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontFamily;
|
|
18
|
+
const fontWeight = options.fontWeight ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.fontWeight;
|
|
19
|
+
const lineHeight = options.lineHeight ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.lineHeight;
|
|
20
|
+
const textAlign = options.textAlign ?? text_measurement_defaults_1.TEXT_MEASUREMENT_DEFAULTS.textAlign;
|
|
21
|
+
const maxWidth = options.maxWidth;
|
|
22
|
+
if (!text || text.length === 0) {
|
|
23
|
+
return {
|
|
24
|
+
width: 0,
|
|
25
|
+
height: 0,
|
|
26
|
+
lines: 0,
|
|
27
|
+
anchorDx: 0,
|
|
28
|
+
anchorDy: 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const ctx = this._getContext();
|
|
32
|
+
if (!ctx) {
|
|
33
|
+
throw new Error("Node canvas context not available");
|
|
34
|
+
}
|
|
35
|
+
// Set font for measurement
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
38
|
+
// Split by explicit newlines
|
|
39
|
+
const explicitLines = text.split("\n");
|
|
40
|
+
// Calculate wrapped lines if maxWidth is specified
|
|
41
|
+
const wrappedLines = [];
|
|
42
|
+
if (maxWidth !== undefined && maxWidth > 0) {
|
|
43
|
+
for (const line of explicitLines) {
|
|
44
|
+
const wrapped = this._wrapLine(ctx, line, maxWidth);
|
|
45
|
+
wrappedLines.push(...wrapped);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
wrappedLines.push(...explicitLines);
|
|
50
|
+
}
|
|
51
|
+
// Calculate dimensions
|
|
52
|
+
const lineCount = wrappedLines.length;
|
|
53
|
+
const maxLineWidth = this._calculateMaxLineWidth(ctx, wrappedLines);
|
|
54
|
+
const width = maxWidth !== undefined ? Math.min(maxLineWidth, maxWidth) : maxLineWidth;
|
|
55
|
+
// Height calculation: first line is fontSize, subsequent lines add lineHeight * fontSize
|
|
56
|
+
const height = lineCount > 0
|
|
57
|
+
? fontSize + (lineCount - 1) * lineHeight * fontSize
|
|
58
|
+
: 0;
|
|
59
|
+
// Calculate anchor offset based on alignment
|
|
60
|
+
const anchorDx = this._calculateAnchorDx(width, textAlign);
|
|
61
|
+
return {
|
|
62
|
+
width,
|
|
63
|
+
height,
|
|
64
|
+
lines: lineCount,
|
|
65
|
+
anchorDx,
|
|
66
|
+
anchorDy: 0 // Top/hanging baseline
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
isAvailable() {
|
|
70
|
+
return this._tryLoadCanvas();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Attempts to load the canvas npm package.
|
|
74
|
+
*/
|
|
75
|
+
_tryLoadCanvas() {
|
|
76
|
+
if (this._canvasModule !== null) {
|
|
77
|
+
return this._canvasModule !== undefined;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
// Using Function constructor so bundlers don't statically include the optional 'canvas' package
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
82
|
+
const dynamicRequire = new Function("moduleName", "return require(moduleName)");
|
|
83
|
+
this._canvasModule = dynamicRequire("canvas");
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// canvas package not installed
|
|
88
|
+
this._canvasModule = undefined;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Gets or creates the canvas context for measurement.
|
|
94
|
+
*/
|
|
95
|
+
_getContext() {
|
|
96
|
+
if (this._context) {
|
|
97
|
+
return this._context;
|
|
98
|
+
}
|
|
99
|
+
if (!this._tryLoadCanvas() || !this._canvasModule) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
const { createCanvas } = this._canvasModule;
|
|
104
|
+
this._canvas = createCanvas(1, 1);
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
this._context = this._canvas.getContext("2d");
|
|
107
|
+
return this._context;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Wraps a single line of text to fit within maxWidth.
|
|
111
|
+
*/
|
|
112
|
+
_wrapLine(ctx, line, maxWidth) {
|
|
113
|
+
if (line.length === 0) {
|
|
114
|
+
return [""];
|
|
115
|
+
}
|
|
116
|
+
const words = line.split(/\s+/);
|
|
117
|
+
const lines = [];
|
|
118
|
+
let currentLine = "";
|
|
119
|
+
for (const word of words) {
|
|
120
|
+
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
const metrics = ctx.measureText(testLine);
|
|
123
|
+
if (metrics.width <= maxWidth || currentLine.length === 0) {
|
|
124
|
+
currentLine = testLine;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
lines.push(currentLine);
|
|
128
|
+
currentLine = word;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (currentLine.length > 0) {
|
|
132
|
+
lines.push(currentLine);
|
|
133
|
+
}
|
|
134
|
+
return lines.length > 0 ? lines : [""];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Calculates the maximum width among all lines.
|
|
138
|
+
*/
|
|
139
|
+
_calculateMaxLineWidth(ctx, lines) {
|
|
140
|
+
let maxWidth = 0;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
const metrics = ctx.measureText(line);
|
|
144
|
+
if (metrics.width > maxWidth) {
|
|
145
|
+
maxWidth = metrics.width;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return maxWidth;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Calculates the X anchor offset based on text alignment.
|
|
152
|
+
*/
|
|
153
|
+
_calculateAnchorDx(width, textAlign) {
|
|
154
|
+
switch (textAlign) {
|
|
155
|
+
case "center":
|
|
156
|
+
return -width / 2;
|
|
157
|
+
case "right":
|
|
158
|
+
return -width;
|
|
159
|
+
case "left":
|
|
160
|
+
default:
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.NodeTextMeasurementProvider = NodeTextMeasurementProvider;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEXT_MEASUREMENT_DEFAULTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Default values for text measurement options.
|
|
6
|
+
*/
|
|
7
|
+
exports.TEXT_MEASUREMENT_DEFAULTS = {
|
|
8
|
+
fontSize: 16,
|
|
9
|
+
fontFamily: "Arial",
|
|
10
|
+
fontWeight: 400,
|
|
11
|
+
textAlign: "left",
|
|
12
|
+
lineHeight: 1.2
|
|
13
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ITextMeasurementProvider } from "../../interfaces/text-measurement.interface";
|
|
2
|
+
import { MeasureTextOptions, TextMetrics } from "../../types/text-measurement-types";
|
|
3
|
+
/**
|
|
4
|
+
* Service for measuring text dimensions.
|
|
5
|
+
* Automatically detects the best available provider for the current environment.
|
|
6
|
+
*
|
|
7
|
+
* Provider selection order:
|
|
8
|
+
* 1. BrowserTextMeasurementProvider - Uses native canvas API (browsers)
|
|
9
|
+
* 2. NodeTextMeasurementProvider - Uses canvas npm package (Node.js)
|
|
10
|
+
* 3. FallbackTextMeasurementProvider - Character-based estimation (always available)
|
|
11
|
+
*/
|
|
12
|
+
export declare class TextMeasurementService {
|
|
13
|
+
private readonly _provider;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new TextMeasurementService.
|
|
16
|
+
* @param provider Optional custom provider. If not specified, auto-detects the best available.
|
|
17
|
+
*/
|
|
18
|
+
constructor(provider?: ITextMeasurementProvider);
|
|
19
|
+
/**
|
|
20
|
+
* Measures text dimensions.
|
|
21
|
+
* @param text The text to measure
|
|
22
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
23
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
24
|
+
*/
|
|
25
|
+
measureText(text: string, options: MeasureTextOptions): TextMetrics;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the current provider.
|
|
28
|
+
*/
|
|
29
|
+
get provider(): ITextMeasurementProvider;
|
|
30
|
+
/**
|
|
31
|
+
* Detects and returns the best available provider for the current environment.
|
|
32
|
+
*
|
|
33
|
+
* Detection order:
|
|
34
|
+
* 1. Browser canvas API
|
|
35
|
+
* 2. Node.js canvas package
|
|
36
|
+
* 3. Fallback estimation
|
|
37
|
+
*/
|
|
38
|
+
static detectProvider(): ITextMeasurementProvider;
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TextMeasurementService = void 0;
|
|
4
|
+
const browser_text_measurement_provider_1 = require("./browser-text-measurement-provider");
|
|
5
|
+
const node_text_measurement_provider_1 = require("./node-text-measurement-provider");
|
|
6
|
+
const fallback_text_measurement_provider_1 = require("./fallback-text-measurement-provider");
|
|
7
|
+
/**
|
|
8
|
+
* Service for measuring text dimensions.
|
|
9
|
+
* Automatically detects the best available provider for the current environment.
|
|
10
|
+
*
|
|
11
|
+
* Provider selection order:
|
|
12
|
+
* 1. BrowserTextMeasurementProvider - Uses native canvas API (browsers)
|
|
13
|
+
* 2. NodeTextMeasurementProvider - Uses canvas npm package (Node.js)
|
|
14
|
+
* 3. FallbackTextMeasurementProvider - Character-based estimation (always available)
|
|
15
|
+
*/
|
|
16
|
+
class TextMeasurementService {
|
|
17
|
+
_provider;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new TextMeasurementService.
|
|
20
|
+
* @param provider Optional custom provider. If not specified, auto-detects the best available.
|
|
21
|
+
*/
|
|
22
|
+
constructor(provider) {
|
|
23
|
+
this._provider = provider ?? TextMeasurementService.detectProvider();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Measures text dimensions.
|
|
27
|
+
* @param text The text to measure
|
|
28
|
+
* @param options Measurement options (fontSize, fontFamily, etc.)
|
|
29
|
+
* @returns TextMetrics with width, height, lines, anchorDx, anchorDy
|
|
30
|
+
*/
|
|
31
|
+
measureText(text, options) {
|
|
32
|
+
return this._provider.measureText(text, options);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Gets the current provider.
|
|
36
|
+
*/
|
|
37
|
+
get provider() {
|
|
38
|
+
return this._provider;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detects and returns the best available provider for the current environment.
|
|
42
|
+
*
|
|
43
|
+
* Detection order:
|
|
44
|
+
* 1. Browser canvas API
|
|
45
|
+
* 2. Node.js canvas package
|
|
46
|
+
* 3. Fallback estimation
|
|
47
|
+
*/
|
|
48
|
+
static detectProvider() {
|
|
49
|
+
// Try browser provider first
|
|
50
|
+
const browserProvider = new browser_text_measurement_provider_1.BrowserTextMeasurementProvider();
|
|
51
|
+
if (browserProvider.isAvailable()) {
|
|
52
|
+
return browserProvider;
|
|
53
|
+
}
|
|
54
|
+
// Try Node.js canvas provider
|
|
55
|
+
const nodeProvider = new node_text_measurement_provider_1.NodeTextMeasurementProvider();
|
|
56
|
+
if (nodeProvider.isAvailable()) {
|
|
57
|
+
return nodeProvider;
|
|
58
|
+
}
|
|
59
|
+
// Fall back to estimation
|
|
60
|
+
return new fallback_text_measurement_provider_1.FallbackTextMeasurementProvider();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.TextMeasurementService = TextMeasurementService;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CreateTemplateInput } from "../types/api-types";
|
|
2
|
+
/**
|
|
3
|
+
* Validator for template operations.
|
|
4
|
+
*/
|
|
5
|
+
export declare class TemplateValidator {
|
|
6
|
+
/**
|
|
7
|
+
* Validates input for template creation.
|
|
8
|
+
* @param input The creation input to validate
|
|
9
|
+
* @throws ValidationError if validation fails
|
|
10
|
+
*/
|
|
11
|
+
validateCreate(input: CreateTemplateInput): void;
|
|
12
|
+
/**
|
|
13
|
+
* Validates canvas dimensions.
|
|
14
|
+
*/
|
|
15
|
+
private validateCanvasDimensions;
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TemplateValidator = void 0;
|
|
4
|
+
const api_errors_1 = require("../errors/api-errors");
|
|
5
|
+
/**
|
|
6
|
+
* Validator for template operations.
|
|
7
|
+
*/
|
|
8
|
+
class TemplateValidator {
|
|
9
|
+
/**
|
|
10
|
+
* Validates input for template creation.
|
|
11
|
+
* @param input The creation input to validate
|
|
12
|
+
* @throws ValidationError if validation fails
|
|
13
|
+
*/
|
|
14
|
+
validateCreate(input) {
|
|
15
|
+
if (!input.title || typeof input.title !== "string") {
|
|
16
|
+
throw new api_errors_1.ValidationError("title is required", "title");
|
|
17
|
+
}
|
|
18
|
+
if (input.title.trim().length === 0) {
|
|
19
|
+
throw new api_errors_1.ValidationError("title cannot be empty", "title");
|
|
20
|
+
}
|
|
21
|
+
if (input.title.length > 200) {
|
|
22
|
+
throw new api_errors_1.ValidationError("title must be 200 characters or less", "title");
|
|
23
|
+
}
|
|
24
|
+
if (input.content !== undefined && typeof input.content !== "string") {
|
|
25
|
+
throw new api_errors_1.ValidationError("content must be a string", "content");
|
|
26
|
+
}
|
|
27
|
+
this.validateCanvasDimensions(input.canvasWidth, input.canvasHeight);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates canvas dimensions.
|
|
31
|
+
*/
|
|
32
|
+
validateCanvasDimensions(width, height) {
|
|
33
|
+
if (width !== undefined) {
|
|
34
|
+
if (typeof width !== "number" || isNaN(width)) {
|
|
35
|
+
throw new api_errors_1.ValidationError("canvasWidth must be a valid number", "canvasWidth");
|
|
36
|
+
}
|
|
37
|
+
if (width < 100 || width > 10000) {
|
|
38
|
+
throw new api_errors_1.ValidationError("canvasWidth must be between 100 and 10000", "canvasWidth");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (height !== undefined) {
|
|
42
|
+
if (typeof height !== "number" || isNaN(height)) {
|
|
43
|
+
throw new api_errors_1.ValidationError("canvasHeight must be a valid number", "canvasHeight");
|
|
44
|
+
}
|
|
45
|
+
if (height < 100 || height > 10000) {
|
|
46
|
+
throw new api_errors_1.ValidationError("canvasHeight must be between 100 and 10000", "canvasHeight");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.TemplateValidator = TemplateValidator;
|