@graph-knowledge/api 0.1.7 → 0.1.22

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.
Files changed (26) hide show
  1. package/README.md +109 -0
  2. package/package.json +8 -2
  3. package/src/index.d.ts +3 -1
  4. package/src/index.js +3 -1
  5. package/src/lib/graph-knowledge-api.d.ts +35 -0
  6. package/src/lib/graph-knowledge-api.js +47 -1
  7. package/src/lib/interfaces/template-operations.interface.d.ts +11 -1
  8. package/src/lib/interfaces/text-measurement.interface.d.ts +24 -0
  9. package/src/lib/interfaces/text-measurement.interface.js +2 -0
  10. package/src/lib/operations/template-operations.d.ts +5 -2
  11. package/src/lib/operations/template-operations.js +47 -1
  12. package/src/lib/types/api-types.d.ts +15 -0
  13. package/src/lib/types/text-measurement-types.d.ts +64 -0
  14. package/src/lib/types/text-measurement-types.js +2 -0
  15. package/src/lib/utils/text-measurement/browser-text-measurement-provider.d.ts +28 -0
  16. package/src/lib/utils/text-measurement/browser-text-measurement-provider.js +137 -0
  17. package/src/lib/utils/text-measurement/fallback-text-measurement-provider.d.ts +29 -0
  18. package/src/lib/utils/text-measurement/fallback-text-measurement-provider.js +122 -0
  19. package/src/lib/utils/text-measurement/node-text-measurement-provider.d.ts +35 -0
  20. package/src/lib/utils/text-measurement/node-text-measurement-provider.js +165 -0
  21. package/src/lib/utils/text-measurement/text-measurement-defaults.d.ts +5 -0
  22. package/src/lib/utils/text-measurement/text-measurement-defaults.js +13 -0
  23. package/src/lib/utils/text-measurement/text-measurement-service.d.ts +39 -0
  24. package/src/lib/utils/text-measurement/text-measurement-service.js +63 -0
  25. package/src/lib/validators/template-validator.d.ts +16 -0
  26. 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.7",
3
+ "version": "0.1.22",
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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
- constructor(firestore: IFirestoreClient, auth: IAuthClient);
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
- constructor(firestore, auth) {
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,5 @@
1
+ import { MeasureTextOptions } from "../../types/text-measurement-types";
2
+ /**
3
+ * Default values for text measurement options.
4
+ */
5
+ export declare const TEXT_MEASUREMENT_DEFAULTS: Required<Omit<MeasureTextOptions, "maxWidth">>;
@@ -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;